Playwright JavaScript - How can I reuse locator variables within different tests? - javascript

I want to be able to use a locator variable within all the tests without having to define it every time inside each test.
Something like:
// #ts-check
const { test, expect } = require('#playwright/test');
test.beforeEach( async ({ page }) => {
await page.goto('[desired URL]');
});
// I want to make this variable global to be able to use it within all the tests.
const signInBtn = page.getByTestId('some-button'); // how to resolve 'page' here??
test.describe('My set of tests', () => {
test('My test 1', async ({ page }) => {
await expect(page).toHaveTitle(/Some-Title/);
await expect(signInBtn).toBeEnabled(); // I wanna use the variable here...
});
test('My test 2', async ({ page }) => {
await signInBtn.click(); // ...and here, without having to define it every time inside each test.
});
});
PS: This snippet is just an example to pass the idea, not the actual project, pls don't be attached to it.

You don't have to .Use Page Object Model.. Keep the tests clean.
By using page object model we separate out locator definitions and test method definitions from the actual test to keep it simple, clean & reusable.
See below example:
//Page Object
// playwright-dev-page.js
const { expect } = require('#playwright/test');
exports.PlaywrightDevPage = class PlaywrightDevPage {
/**
* #param {import('#playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.getStartedLink = page.locator('a', { hasText: 'Get started' });
this.gettingStartedHeader = page.locator('h1', { hasText: 'Installation' });
this.pomLink = page.locator('li', { hasText: 'Guides' }).locator('a', { hasText: 'Page Object Model' });
this.tocList = page.locator('article div.markdown ul > li > a');
}
async goto() {
await this.page.goto('https://playwright.dev');
}
async getStarted() {
await this.getStartedLink.first().click();
await expect(this.gettingStartedHeader).toBeVisible();
}
async pageObjectModel() {
await this.getStarted();
await this.pomLink.click();
}
}
Now we can use the PlaywrightDevPage class in our tests.
// example.spec.js
const { test, expect } = require('#playwright/test');
const { PlaywrightDevPage } = require('./playwright-dev-page');
test('getting started should contain table of contents', async ({ page }) => {
const playwrightDev = new PlaywrightDevPage(page);
await playwrightDev.goto();
await playwrightDev.getStarted();
await expect(playwrightDev.tocList).toHaveText([
`How to install Playwright`,
`What's Installed`,
`How to run the example test`,
`How to open the HTML test report`,
`Write tests using web first assertions, page fixtures and locators`,
`Run single test, multiple tests, headed mode`,
`Generate tests with Codegen`,
`See a trace of your tests`
]);
});
test('should show Page Object Model article', async ({ page }) => {
const playwrightDev = new PlaywrightDevPage(page);
await playwrightDev.goto();
await playwrightDev.pageObjectModel();
await expect(page.locator('article')).toContainText('Page Object Model is a common pattern');
});

You could move it all into a describe block. So something like this should work:
test.describe('My set of tests', () => {
let signInBtn:Locator;
test.beforeEach( async ({ page }) => {
await page.goto('[desired URL]');
signInBtn = page.getByTestId('some-button');
});
test('My test 1', async ({ page }) => {
await expect(page).toHaveTitle(/Some-Title/);
await expect(signInBtn).toBeEnabled();
});
test('My test 2', async ({ page }) => {
await signInBtn.click();
});
});

Related

Jest spyOn mockResolved with asynchronous tests seems to not work properly in certain condition, what's happening?

Recently I had to write some tests that mock API requests with jest (24.8.0).
So, I wrote something like this:
class MyApi {
public async getSomeData() {
// request backend
}
}
describe("Some test", () => {
const apiSpy = jest.spyOn(MyApi, 'getSomeData');
afterEach(() => jest.restoreAllMocks());
test('some test 1', async () => {
// arrange
apiSpy.mockResolved(null);
// act
const result = await someFunctionCallingMyApi();
// assert
});
test('some test 2', async () => {
// arrange
apiSpy.mockResolved(someMockObject);
// act
const result = await someFunctionCallingMyApi();
// assert
});
// ...and more
});
but it appears that some mockResolved are ignored and my function is calling real API instead of mocked one.
Later I changed this code to:
describe("Some test", () => {
let apiSpy: jest.SpyInstance;
beforeEach(() => {
apiSpy = jest.spyOn(MyApi, 'getSomeData');
});
afterEach(() => jest.restoreAllMocks());
test('some test 1', async () => {
// arrange
apiSpy.mockResolved(null);
// act
const result = await someFunctionCallingMyApi();
// assert
});
test('some test 2', async () => {
// arrange
apiSpy.mockResolved(someMockObject);
// act
const result = await someFunctionCallingMyApi();
// assert
});
// ...and more
});
and my tests seem to work, its properly call mocked function.
Can someone explain why the first code does not work?

Go to page URL using test.BeforeAll for playwright-test runner

test.beforeAll(async ({ page }) => {
// Go to the starting url before each test.
await page.goto('https://my.start.url/');
});
Is there a nice way in playwright-test runner to setup the browser and navigate to the page related to the page object to run the tests in that file?
I want to avoid doing this "test.beforeEach" for everyone if possible. Currently its not possible to do this in playwright-test but it is possible in jest. Any ideas?
Property 'page' does not exist on type 'PlaywrightWorkerArgs & PlaywrightWorkerOptions'.
As said above (by Leon), you can create your own page, but to avoid creating a new page for each test (Joaquin Casco asked about it), just don't pass page as a parameter to your test function.
I mean:
const { chromium } = require('playwright');
const { test } = require('#playwright/test');
test.beforeAll(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// Go to the starting url before each test.
await page.goto('https://my.start.url/');
});
// Will create a new page instance
test('Test with page as parameter', async ({ page }) => {})
// Does not create new page, so you can use the page from beforeAll hook (if you bring it into the scope)
test('Test without page as parameter', async () => {})
// example.spec.ts
import { test, Page } from '#playwright/test';
test.describe.configure({ mode: 'serial' });
let page: Page;
test.beforeAll(async ({ browser }) => {
// Create page once and sign in.
page = await browser.newPage();
await page.goto('https://github.com/login');
await page.locator('input[name="user"]').fill('user');
await page.locator('input[name="password"]').fill('password');
await page.locator('text=Sign in').click();
});
test.afterAll(async () => {
await page.close();
});
test('first test', async () => {
// page is signed in.
});
test('second test', async () => {
// page is signed in.
});
https://playwright.dev/docs/test-auth#reuse-the-signed-in-page-in-multiple-tests
You could create a page by yourself:
const { chromium } = require('playwright');
const { test } = require('#playwright/test');
test.beforeAll(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// Go to the starting url before each test.
await page.goto('https://my.start.url/');
});
You need install the playwright package.

jest mock functions but keep some original code for others?

Scenario is, I have a function that needs to be unit tested: AppleLoginService().
It will contain 3 other functions within it. These need to be mocked. Simple.
//UserService.js
const AppleLoginService = async () => {
await verifyAppleToken();
const response = await verifyAuthorizationCode(authorizationCode);
const id = await RegisterIdpAccount(); //Can ignore for now.
return {id}
}
async function verifyAppleToken() {
//etc code
}
async function verifyAuthorizationCode() {
//etc code
}
The issue is, I need to retain AppleLoginService original code but mock everything else but cannot do so.
I've tried:
But obviously, mocks AppleLoginService which isn't what I want.
jest.mock("UserService");
The functions doesn't actually get mocked in this way which is weird. I've put logs in the functions and ends up printing.
jest.mock("UserService", () => {
const original = jest.requireActual("UserService");
return {
...original,
verifyAppleToken: jest.fn(),
verifyAuthorizationCode: jest.fn()
}
});
Also doesn't get mocked. Original implementation runs.
const verifyToken = jest.spyOn(services, "verifyAppleToken").mockImplementation(() => true);
const verifyCode = jest.spyOn(services, "verifyAuthorizationCode").mockImplementation(() => 1);
const services = require("UserService.js");
const queries = require("SQLTransactionService.js"));
/*
jest.mock() ETC
*/
describe("When using AppleLoginService,", () => {
it("given all values are correct then, return response", async () => {
services.verifyAppleToken.mockReturnValueOnce(true);
services.verifyAuthorizationCode.mockResolvedValueOnce("ANYTHING");
queries.RegisterIdpAccount.mockResolvedValueOnce(1);
const res = await services.AppleLoginService("apple", signUpPayload);
expect(services.verifyAppleToken).toBeCalledTimes(1); // Is called 0 times
expect(services.verifyAuthorizationCode).toBeCalledTimes(1); // Is called 0 times.
expect(res).toBeDefined();
});
});

How do I clear a mocked playwright response?

Per playwright documentation you can mock an API call
This works fine for me but I have two tests that have different mock responses in the same file.
submitMock = (response) => page.route(/submit/, (route) =>
route.fulfill({
status: status || 200,
body: JSON.stringify(response),
}),
);
/// my test code
describe('fill form', () => {
beforeEach(async () => {
jest.resetAllMocks(); // doesn't clear mock requests
await setupAppMocks(); // some basic mocks
await page.goto(
`${localhost}/?foo=bar`,
);
});
afterAll(async () => {
await browser.close();
});
it('should test scenario 1', async () => {
expect.hasAssertions();
const responseMock = { test: 'test' };
await submitMock(responseMock); // first mock created here
await fillForm();
await clickSubmit();
await page.waitForSelector('[class*="CongratulationsText"]');
const sectionText = await page.$eval(
'[class*="CongratulationsText"]',
(e) => e.textContent,
);
expect(sectionText).toBe(
`Congratulations, expecting message for scenario 1`,
);
});
it('should test scenario 2', async () => {
expect.hasAssertions();
const responseMock = { test: 'test 2' };
await submitMock(responseMock); // second mock created here
await fillForm();
await clickSubmit();
await page.waitForSelector('[class*="CongratulationsText"]');
const sectionText = await page.$eval(
'[class*="CongratulationsText"]',
(e) => e.textContent,
);
// fails because first mock is still active
expect(sectionText).toBe(
`Congratulations, expecting message for scenario 2`,
);
});
});
Is there a way to clear the previous mock?
What I've tried so far is basically adding another mock but it doesn't seem to override the previous one. I'm assuming that the issue is that the mocks for the same pattern can't be overridden, but I don't have an easy way to fix the issue.
The way to unmock an API call is to use page.unroute Documentation here
page.unroute(/submit/);

How do I setup this JS code to do better testing?

Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.
const $ = require('jquery')
const axios = require('axios')
let workerComplete = () => {
window.location.reload()
}
async function checkWorkerStatus() {
const worker_id = $(".worker-waiter").data('worker-id')
const response = await axios.get(`/v1/workers/${worker_id}`)
return response.data
}
function waitForWorker() {
if (!$('.worker-waiter').length) {
return
}
checkWorkerStatus().then(data => {
// delay next action by 1 second e.g. calling api again
return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}).then(worker_response => {
const working_statuses = ['queued', 'working']
if (worker_response && working_statuses.includes(worker_response.status)) {
waitForWorker()
} else {
workerComplete()
}
})
}
export {
waitForWorker,
checkWorkerStatus,
workerComplete
}
if (process.env.NODE_ENV !== 'test') $(waitForWorker)
Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'
import axios from 'axios'
import * as Worker from 'worker_waiter'
jest.mock('axios')
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() }
})
});
beforeEach(() => jest.resetAllMocks() )
afterEach(() => {
jest.restoreAllMocks();
});
describe('worker is complete after 2 API calls a', () => {
const worker_id = Math.random().toString(36).slice(-5) // random string
beforeEach(() => {
axios.get
.mockResolvedValueOnce({ data: { status: 'working' } })
.mockResolvedValueOnce({ data: { status: 'complete' } })
jest.spyOn(Worker, 'waitForWorker')
jest.spyOn(Worker, 'checkWorkerStatus')
document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
})
it('polls the correct endpoint twice a', async() => {
const endpoint = `/v1/workers/${worker_id}`
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint]])
expect(data).toMatchObject({"status": "working"})
})
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
expect(data).toMatchObject({"status": "complete"})
})
})
it('polls the correct endpoint twice b', async() => {
jest.mock('waitForWorker', () => {
expect(Worker.checkWorkerStatus).toBeCalled()
})
expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
await Worker.waitForWorker()
})
I think there are a couple things you can do here.
Inject status handlers
You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.
function waitForWorker(onComplete, onBusy) {
// instead of calling waitForWorker call onBusy.
// instead of calling workerComplete call onComplete.
}
Now to test, you really just need to create mock functions.
const onComplete = jest.fn();
const onBusy = jest.fn();
And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.
Return a promise
You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.
async function waitForWorker() {
let result = { status: 'empty' };
if (!$('.worker-waiter').length) {
return result;
}
try {
const working_statuses = ['queued', 'working'];
const data = await checkWorkerStatus();
if (data && working_statuses.includes(data.status)) {
await waitForWorker();
} else {
result = { status: 'complete' };
}
} catch (e) {
result = { status: 'error' };
}
return result;
}
The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.
If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.
async function waitForWorker(request) {
// ...
try {
const working_statuses = ['queued', 'working'];
const data = await request();
} catch (e) {
// ...
}
// ...
}
You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.
const result = await waitForWorker(checkWorkerStatus);
if (result.status === 'complete') {
workerComplete();
}

Categories