Reputation: 1584
I am very new to Playwright. Due to my test suites, I need to login into my application before running each test. Inside a single spec file that is easy, I can simply call test.beforeEach
. My issue is: I need to before the login before each test of each spec file.
test.describe('Test', () => {
//I need to make the code inside this beforeEach a exported
//function to call inside the before each of every spec file I have
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.click('text=Log in with Google account');
await page.fill('id=identifierId', LoginAutomationCredentials.USER);
await page.click('button[jsname="LgbsSe"]');
await page.fill('input[type="password"]', LoginAutomationCredentials.PASSWORD);
await page.click('button[jsname="LgbsSe"]');
const otp = authenticator.generateToken(LoginAutomationCredentials.TOKEN);
await page.fill('id=totpPin', otp);
await page.click('button[jsname="LgbsSe"]');
});
it('Some description', async ({ page }) => {
await page.goto('/foo');
const dateFilter = await page.inputValue('input[placeholder="2011/03/02"]');
expect(dateFilter).toBe('2021/12/07');
});
});
I tried simply taking that code and and making it a function inside a separate .ts file and then importing it, but I figured the context is needed in order to do this. This is probably something every tester that uses playwright knows and uses regularly, however, I did not find anything on the subject.
How can I avoid copying the entire code of beforeEach
and pasting it to all my spec files? How can I make it a function and call it whenever I want?
Upvotes: 16
Views: 39043
Reputation: 57259
Global setup and teardown is an option for pre-test logins.
Here's a sketch to show what this might look like on your code, based on the docs (untested).
global-setup.ts
:
import {chromium, FullConfig} from "@playwright/test";
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
// do your login:
await page.goto("/login");
await page.click("text=Log in with Google account");
await page.fill(
"id=identifierId",
LoginAutomationCredentials.USER
);
await page.click('button[jsname="LgbsSe"]');
await page.fill(
'input[type="password"]',
LoginAutomationCredentials.PASSWORD
);
await page.click('button[jsname="LgbsSe"]');
const otp = authenticator.generateToken(
LoginAutomationCredentials.TOKEN
);
await page.fill("id=totpPin", otp);
await page.click('button[jsname="LgbsSe"]');
// see below for further discussion
// const { baseURL, storageState } = config.projects[0].use;
// await page.context().storageState({ path: storageState as string });
// await browser.close();
}
export default globalSetup;
playwright.config.ts
:
import {defineConfig} from "@playwright/test";
export default defineConfig({
globalSetup: require.resolve("./global-setup"),
use: {
baseURL: "http://localhost:3000/",
storageState: "state.json",
},
});
tests/foo.spec.js
:
test.describe("Test", () => {
it("Some description", async ({page}) => {
await page.goto("/foo");
const dateFilter = await page.inputValue(
'input[placeholder="2011/03/02"]'
);
expect(dateFilter).toBe("2021/12/07");
});
});
Now, in the original Playwright example, their globalSetup
function writes to storage state:
// ...
await page.getByText('Sign in').click();
await page.context().storageState({ path: storageState as string });
await browser.close();
// ...
But your code doesn't. You might need to do so as the last line of your globalSetup
function, then call await browser.close()
to save and carry those tokens between one browser session and the next.
They're pulling this storageState
string from const { baseURL, storageState } = config.projects[0].use;
so that it doesn't need to be hardcoded in multiple places. See their docs for a full example.
Once you have that working and you've written your state.json
file, you may want to skip running future logins if the tokens are still valid. You can do so with the following modifications to
global-setup.ts
:
import fs from "node:fs/promises";
// ...
async function exists(path: string): Promise<boolean> {
return !!(await fs.stat(path).catch(() => false));
}
async function globalSetup(config: FullConfig) {
const {storageState} = config.projects[0].use;
if (await exists(storageState)) {
return;
}
const browser = await chromium.launch();
const page = await browser.newPage();
// do your login:
await page.goto("/login");
// ...
}
// ...
Most likely, state.json
should be added to your project's .gitignore
.
Upvotes: 2
Reputation: 25062
There two aproaches:
project dependency
Form Playwright 1.31
with project dependency
you can set test what will be executed before any other tests and pass i.e. browser storage between tests.
See working and tested example (entering google search page and accepting policies, and then entering this page again with cookies):
playwright.config.ts:
import { PlaywrightTestConfig } from "@playwright/test";
import path from "path";
//here you save session
export const STORAGE_STATE = path.join(__dirname, 'login.json')
const config: PlaywrightTestConfig = {
timeout: 10 * 1000,
expect: {
timeout: 3 * 1000,
},
testDir: './tests',
use:{
baseURL: 'https://google.com',
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
// here we set main project and dependent one
projects: [
{
name: 'login',
grep: /@login/
},
{
name: 'depend e2e',
grep: /@e2e/,
dependencies: ['login'],
use: {
storageState: STORAGE_STATE
}
}
]
};
export default config;
tests/example.spec.ts:
import { test} from '@playwright/test';
import { STORAGE_STATE } from '../playwright.config';
test('login to service @login', async({page}) => {
await page.goto('/');
// below example is to reject cookies from google
await page.waitForLoadState('domcontentloaded');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await page.getByRole('menuitem', { name: "English (United States)" }).press('Enter');
await page.getByRole('button', { name: 'Reject all' }).click();
// Save storage:
await page.context().storageState({path: STORAGE_STATE})
})
test('logged in test @e2e', async ({page}) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
// BOOM - you are in!
// Screenshot shows that settings regarding cookies were saved
await page.screenshot({ path: 'screenshot.png' })
})
Video with example: https://www.youtube.com/watch?v=PI50YAPTAs4&t=286s
In above example I'm using tags to identify tests: https://playwright.dev/docs/test-annotations#tag-tests
fixtures
Let test entering google search page and accepting policies
In file fixtures.ts
:
import { test as base } from "@playwright/test";
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
// We have a few cases where we need our app to know it's running in Playwright.
// This is inspired by Cypress that auto-injects window.Cypress.
await page.addInitScript(() => {
(window as any).Playwright = true;
});
await page.goto("/");
// below example is to reject cookies from google
await page.waitForLoadState("domcontentloaded");
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
await page
.getByRole("menuitem", { name: "English (United States)" })
.press("Enter");
await page.getByRole("button", { name: "Reject all" }).click();
use(page);
},
});
export { expect } from "@playwright/test";
And then in test use new test
object instead importing it form "@playwright/test"
import { test, expect } from "../fixture";
test("logged in test @e2e", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("domcontentloaded");
// BOOM - you are in!
// Screenshot shows that it works
await page.screenshot({ path: "screenshot.png" });
});
Inspired by: https://github.com/microsoft/playwright/issues/9468#issuecomment-1403214587
If you need example with global request intercepting see here: https://stackoverflow.com/a/76234592/1266040
Upvotes: 9
Reputation: 814
Fixtures is the right way, but there is a better option than having to remember to use login
instead of page
when you want to login. This is how I do it...
First I have this in playwright/src/index.ts
where I setup all the fixtures for my project:
import { test as base_test, expect } from 'playwright_test';
type TestFixtures = {
user: string;
};
export const test = base_test.extend<TestFixtures>( {
user : '[email protected]',
async context( { user, context, request }, use ) {
// This makes a REST request to the backend to get a JWT token
// and then stores that token in the browsers localStorage,
// but you could replace this with whatever makes sense for auth
// in your app
if ( user ) {
const content = await getAuthScript( user, request );
await context.addInitScript( { content } );
}
await use( context );
},
} );
/**
* This makes a REST request to the backend to get a JWT token
* and then stores that token in the browsers localStorage,
* but you could replace this with whatever makes sense for auth
* in your app.
*/
async function getAuthScript( user, request ) {
const res = await request.post( '/api/test/auth', { data : { user } } );
const { token } = await res.json();
return `window.localStorage.setItem( 'jwt-token', "${token}" );`;
}
export { test, expect }
I also make sure that playwright/tsconfig.json
includes this:
{
"extends": "../path/to/base/tsconfig.json",
"compilerOptions": {
"noEmit": true,
"paths": {
"~": [ "./src" ],
"~*": [ "./src/*" ]
},
"baseUrl": ".",
"rootUrl": ".",
},
"include": [
"src/**/*.ts",
"test/**/*.ts"
],
}
Now every test will automatically login as [email protected]
, but if you need a test to login as a different user all you need to do in that test file is:
import { test, expect } from '~';
test.use( { user : '[email protected]' } );
test( 'can login as somebody-else', async ( { page } ) => {
// Run your tests here...
} );
Upvotes: 6
Reputation: 164
Use fixtures.
fixture.js:
const base = require('@playwright/test')
const newTest = base.test.extend({
login: async({page}, use) => {
await login();
await use(page); //runs test here
//logic after test
}
})
exports.newTest = newTest
exports.expect = newTest.expect
Then in your tests:
const {newTest} = require('fixture.js')
newTest('mytest', async ({login}) => {
//test logic
login.goto(''); // using login here since I pass it as page in the fixture.
})
Upvotes: 14