Advanced — Use Playwright Test fixtures
Learn how to organize your code with fixtures to give more structure.
If you finished the previous exercises, you should have a test that logs a user in.
Wouldn't it be great if we could reuse a logged-in session in every test case?
You could now start restructuring your code and implement helper methods or use beforeEach, but Playwright provides a built-in mechanism to share and reuse code across test cases and files — fixtures.
The term "fixtures" might sound complicated but you used built-in fixtures the entire time.
If you want to learn how fixtures work, read more about them in the docs or check this YouTube explainer.
Built-in fixtures
Playwright uses fixtures to provide you with everything you need to control a browser in your tests.
page— Isolated page for this test run.context— Isolated context for this test run. Thepagefixture belongs to this context as well.browser— Browsers are shared across tests to optimize resources.browserName— The name of the browser currently running the test. Eitherchromium,firefoxorwebkit.
Whenever you've accessed the page oject in your test cases, you've been using the provided page fixture.
// this test case uses the pre-defined `page` and `browserName` fixture
test("has title", async ({ page, browserName }) => {
// your test case
});
Each page object is isolated to a particular test run. There are no
collisions or shared state.
Create a custom fixture
When it comes to Playwright Test, there are always multiple ways of doing things, but let's assume you want to provide a page object that has the user logged-in already. How could you do this with a fixture that's available in all your tests?
Extend Playwright's test object.
// example.spec.ts
import { test as base, expect } from "@playwright/test";
// 1. extend the provided `test` method
const test = base.test.extend({
// note that custom fixture can also reuse existing fixtures such as `browser`
loggedInPage: async ({ page }, use) => {
// this is before the fixture is used (similar to `beforeEach`)
console.log("before custom fixture");
await page.goto(/);
// ... more logic ...
// ...
// the provided object will be accessed from a test case
await use(page);
// this is after the fixture was used (similar to `afterEach`)
console.log("after custom fixture");
},
});
// 2. use your extended `test` runner
test.describe("A logged in test", () => {
// 3. access your newly defined `darkPage` fixture
test("take screenshot", async ({ loggedInPage }) => {
await loggedInPage.goto("/");
await loggedInPage.screenshot({ path: "logged-in.png" });
});
});
But now your fixture still lives in the same file as your test. It's time to restructure things.
tests
|_ my-setup.js
|_ example.spec.js
Create a new my-setup.js file and export your extended test and the expect object.
import { test as base } from "@playwright/test";
export const test = base.extend({
loggedInPage: async ({ browser }, use) => {
// your fixture logic
},
});
export { expect } from "@playwright/test";
And import your custom test and expect in your spec files.
// Require the extended `test` from your setup
import { test, expect } from "./my-setup";
// all your test logic
test.describe("A light and dark mode page", () => {
// `darkPage` is now available here
test("has title", async ({ loggedInPage }) => {
// ...
});
});
And you made it! You now have a my-setup file that can be reused across tests and is able to hold all your custom business logic.
Check the official Playwright docs to learn more about fixtures.
Custom project and fixture options
Personally, I like to make my fixtures configurable on a project level.
Here is a configuration to run tests in normal and "strict" mode. In strict mode every console message or page exception will lead to a test failure.
// playwright.config.js
// @ts-check
import { defineConfig } from "@playwright/test";
export default defineConfig({
projects: [
{
name: "base",
},
{
name: "strict",
use: { noJsErrors: true, noJsLogs: true },
},
],
});
These projects run the same files, but with different configuration.
$ npx playwright test # all tests run (base & strick)
$ npx playwright test --project=base # only "base" tests run
$ npx playwright test --project=strict # only "strict" tests run
How can you define custom project and fixture parameters? Head over to your setup file (my-setup.js) where you defined your loggedInPage fixture. You can also define custom options and configuration in there.
// ./my-setup.js
import { test as base } from "@playwright/test";
export const test = base.extend({
// define `false` default value
noJsErrors: [false, { option: true }],
// define `false` default value
noJsLogs: [false, { option: true }],
// ...
});
export { expect } from "@playwright/test";
The noJsErrors and noJsLogs are then available in your tests and even other fixtures.
// example.spec.js
// @ts-check
import { test, expect } from "./my-setup";
test.describe("danube tests", () => {
test("add to cart", async ({ page, noJsErrors }) => {
if (noJsErrors) {
// ...
}
});
});
And if you need to, you can even overwrite these option per test file or group.
// example.spec.js
// @ts-check
import { test, expect } from "./my-setup";
test.describe("danube tests", () => {
// overwrite project options
test.use({ noJsErrors: false });
test("add to cart", async ({ page, noJsErrors }) => {
// ...
});
});
Make code reusable with fixtures.
- Extend the
testfunction. - Define your new fixture (in our case it's
loggedInPage). - Add your custom setup to another file and import/require it in your spec files.
💡 If you're stuck, find a working example on GitHub.