Custom fixtures let you encapsulate application-specific setup such as logging in, seeding data or configuring feature flags. Done well, they keep tests concise and expressive while centralising complex setup logic.
Creating Custom Fixtures for Your Application
You can extend the base test with new fixtures that build on existing ones. For example, an authPage fixture can log in once and provide an authenticated page object to tests.
// fixtures/auth.ts
import { test as base } from '@playwright/test';
export type AuthFixtures = {
authPage: import('@playwright/test').Page;
};
export const test = base.extend({
authPage: async ({ page }, use) => {
await page.goto('https://demo.myshop.com/login');
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('SuperSecret123');
await page.getByRole('button', { name: 'Sign in' }).click();
await use(page);
},
});
// auth-tests.spec.ts
import { expect } from '@playwright/test';
import { test } from './fixtures/auth';
test('accesses a protected page using auth fixture', async ({ authPage }) => {
await authPage.goto('https://demo.myshop.com/orders');
await expect(authPage.getByRole('heading', { name: 'Your orders' })).toBeVisible();
});
page) and reuse them rather than re-creating browser contexts manually.By centralising authentication flows and other repeated setup, you reduce duplication while keeping tests focused on their specific behaviours.
Common Mistakes
Mistake 1 β Embedding too much logic in a single mega-fixture
This becomes hard to reason about.
β Wrong: One fixture that logs in, seeds data, toggles flags and manipulates the UI in many ways.
β Correct: Compose several smaller fixtures and use only what each test needs.
Mistake 2 β Making fixtures depend on global mutable state
This hurts isolation.
β Wrong: Using out-of-band globals to share data between fixtures.
β Correct: Pass data through fixture parameters or configuration instead.