Fixtures
When to Use
Use fixtures when state needs to be shared across tests cleanly. Fixtures are Playwright's idiomatic alternative to
beforeEach/globals — they're typed, composable, and guarantee teardown in reverse-of-setup order even on failure.
Decision
Use worker scope ({ scope: 'worker' }) when |
Use test scope (default) when |
|---|---|
| Setup is expensive and shared safely between tests | Tests must not see each other's state |
| OAuth token, DB connection pool, external service handshake | Fresh page, page object, freshly seeded DB row |
| Setup runs once per worker process | Setup runs once per test |
A worker-scoped fixture cannot depend on a test-scoped one (the framework rejects it).
Pattern
// fixtures.ts
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
authedPage: Page; // test-scoped
todoPage: TodoPage; // test-scoped page object
};
type MyWorkerFixtures = {
apiToken: string; // worker-scoped — fetched once per worker
};
export const test = base.extend<MyFixtures, MyWorkerFixtures>({
apiToken: [async ({}, use) => {
const res = await fetch('https://example.com/oauth/token', { /* ... */ });
const { access_token } = await res.json();
await use(access_token);
}, { scope: 'worker' }],
authedPage: async ({ page, apiToken }, use) => {
await page.addInitScript(token => {
window.localStorage.setItem('token', token);
}, apiToken);
await page.goto('/dashboard');
await use(page);
},
todoPage: async ({ authedPage }, use) => {
await use(new TodoPage(authedPage));
},
});
export { expect };
// spec.ts — import from fixtures, not @playwright/test
import { test, expect } from './fixtures';
test('user creates a todo', async ({ todoPage }) => {
await todoPage.add('Buy milk');
await expect(todoPage.items).toHaveCount(1);
});
Auto fixtures
// Runs even if no test asks for it — for cross-cutting concerns
export const test = base.extend<{ logger: void }>({
logger: [async ({}, use, testInfo) => {
console.log(`>>> ${testInfo.title}`);
await use();
console.log(`<<< ${testInfo.title} (${testInfo.status})`);
}, { auto: true }],
});
Overriding built-in fixtures
export const test = base.extend({
page: async ({ page }, use) => {
await page.goto('/');
await page.route('**/api/analytics/**', r => r.abort());
await use(page);
},
});
Common Mistakes
- Wrong: Using
beforeEachfor state that produces a value — fixtures are typed and compose; hooks aren't - Wrong: Worker fixture trying to depend on test fixture — framework error
- Wrong: Auto fixture for everything — adds overhead even to tests that don't need it
See Also
- Authentication — shared auth state per role using fixtures
- Test Organization — hooks vs fixtures for cross-cutting setup
- Reference: Playwright Fixtures