Q12 of 42 · Playwright
What is a fixture in Playwright and how does it differ from beforeEach?
Short answer
Short answer: A fixture is a named, scoped setup/teardown unit injected into tests by the runner. `beforeEach` runs setup imperatively for every test in the file. Fixtures are reusable, composable, can be worker-scoped, and override per-file via `test.use()`. Use fixtures for cross-file reuse; `beforeEach` for one-off setup.
Detail
Playwright's fixture model goes beyond Mocha-style hooks. The key concepts:
Built-in fixtures: page, browser, context, request are fixtures the runner injects into every test. They're scoped: page per-test, browser per-worker.
Custom fixtures: extend test to define your own. They live in a single file and any test that imports the extended test gets them.
import { test as base } from '@playwright/test';
type Fixtures = { authedPage: Page };
export const test = base.extend<Fixtures>({
authedPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('[data-test=email]', 'alice@x.com');
await page.fill('[data-test=password]', 'pwd');
await page.click('[data-test=submit]');
await use(page); // pass to the test
// teardown after `use` returns
},
});
// In a spec
test('uses an already-authed page', async ({ authedPage }) => {
await authedPage.goto('/dashboard');
});
Why fixtures beat beforeEach for reuse:
- A fixture is named and self-contained; cross-file reuse is just an import.
- Fixtures compose.
authedPagecan depend onapiClient, which depends onbaseURL. The runner resolves the dependency graph. - Per-file overrides via
test.use({ ... })change the fixture's behaviour for that file only. - Fixtures can be worker-scoped (run once per worker) for expensive setup like database seeding.
beforeEach is still useful for spec-local setup that doesn't generalise — visiting a specific page, clearing local state. Don't fixturise everything.
Worker-scoped fixtures for one-time-per-worker setup:
export const test = base.extend<{}, { seedDb: void }>({
seedDb: [async ({}, use) => {
await db.seed();
await use();
await db.cleanup();
}, { scope: 'worker' }],
});
The { scope: 'worker' } annotation makes it run once per worker process, not per test.