Skip to content

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 beforeEach for 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