Fixtures and Test Hooks
Every Playwright test function receives an object of built-in fixtures — page, context, browser, browserName, request — that are automatically created before the test body runs and automatically torn down after it finishes, without any explicit setup or cleanup code in the test itself. This is fundamentally different from traditional beforeEach/afterEach hooks in that fixtures are declared as dependencies (you simply list the ones your test needs as destructured parameters), and Playwright's dependency graph figures out what to instantiate, in what order, and lazily skips creating fixtures a given test never actually asks for.
Cricket analogy: It's like a franchise's central contract pool automatically assigning a wicketkeeper, physio, and analyst to whichever match XI needs them, rather than each team manager manually booking those roles before every single game.
Built-in Test Hooks
Alongside fixtures, Playwright supports traditional lifecycle hooks — test.beforeEach() and test.afterEach() run before and after every test in a file or describe block, while test.beforeAll() and test.afterAll() run once per worker process for the whole file, not once per test. A common mistake is using beforeAll() for stateful setup like logging in a user, since beforeAll() shares its result across every test in that worker and any test that mutates that shared state (like changing a setting) can leak side effects into the next test, which is why per-test isolation with fixtures or beforeEach() is usually the safer default.
Cricket analogy: It's like a team doing a full fitness re-assessment before every single match (beforeEach) versus only doing it once before the entire tour begins (beforeAll) — skipping the per-match check risks missing an injury that developed mid-series.
Creating Custom Fixtures
Custom fixtures are defined with test.extend(), which takes an object where each key is a new fixture name and each value is a function receiving the fixtures it depends on plus a use() callback — code before use() is setup, the value passed to use() becomes the fixture's value inside the test, and code after await use() is teardown, guaranteed to run even if the test fails. A common pattern is an authenticatedPage fixture that logs in via the UI or by injecting a storage-state cookie, then hands back an already-authenticated page object so every test that depends on it skips repeating login steps, keeping test bodies focused purely on the behavior under test.
Cricket analogy: It's like a net-bowling machine being pre-loaded and calibrated to a batter's preferred line and length before their session starts, so the actual practice time is spent purely on shot-making rather than setup.
Fixture Composition and Auto Fixtures
Fixtures can depend on other fixtures, so an adminPage fixture might itself depend on an authenticatedPage fixture, which depends on the built-in page fixture, forming a composable chain that Playwright resolves automatically based on which fixtures a test actually destructures. Marking a fixture with { auto: true } makes it run for every test in scope even if no test explicitly requests it — useful for things like a fixture that starts network request logging or seeds a database before each test — while fixture scope can be set to 'test' (default, fresh per test) or 'worker' (created once and reused across all tests run by that worker process, appropriate for expensive-to-create but stateless resources like a database connection pool).
Cricket analogy: It's like a franchise's fitness trainer being on-site for every single training day automatically, whether or not that day's session specifically requested them, because conditioning underpins every drill regardless of what's on the plan.
// fixtures.ts
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
authenticatedPage: import('@playwright/test').Page;
};
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
// setup: runs before the test body
await page.goto('/login');
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Password').fill('secret123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome back')).toBeVisible();
await use(page); // value handed to the test
// teardown: runs after the test, even on failure
await page.context().clearCookies();
},
});
export { expect };
// dashboard.spec.ts
import { test, expect } from './fixtures';
test('dashboard shows account balance', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage.getByTestId('balance')).toBeVisible();
});Fixtures are lazy by default — Playwright only instantiates a fixture (and whatever it depends on) if a test actually destructures it as a parameter, so unused fixtures never run and never slow down unrelated tests.
Worker-scoped fixtures ({ scope: 'worker' }) are created once and reused across every test that worker runs, so any test that mutates that shared resource (e.g., writing to a shared database record) can leak state into the next test in the same worker — prefer test-scoped fixtures unless the resource is genuinely expensive to create and safely reusable.
- Built-in fixtures (page, context, browser, request) are automatically created and torn down per test with no manual setup code.
- test.beforeEach()/afterEach() run per test; test.beforeAll()/afterAll() run once per worker for the whole file.
- beforeAll() shares state across tests in a worker, which can leak side effects — prefer per-test isolation for stateful setup.
- Custom fixtures are declared with test.extend(); code before await use() is setup, code after is guaranteed teardown.
- Fixtures can depend on other fixtures, forming a composable, automatically-resolved dependency graph.
- { auto: true } fixtures run for every test in scope automatically, even if no test explicitly requests them.
- Fixture scope can be 'test' (fresh per test, default) or 'worker' (shared across a worker's tests, for expensive stateless resources).
Practice what you learned
1. What is the key architectural difference between Playwright fixtures and traditional beforeEach() setup code?
2. How often does test.beforeAll() run for a given test file?
3. In a custom fixture defined with test.extend(), what happens to code written after await use()?
4. What does marking a fixture with { auto: true } do?
5. Why is a worker-scoped fixture riskier for stateful data than a test-scoped one?
Was this page helpful?
You May Also Like
Tracing and Debugging with Playwright Inspector
How to use Playwright's Trace Viewer and Inspector to diagnose flaky and failing tests, both locally and in CI.
Cross-Browser and Device Emulation
How Playwright's project configuration runs the same suite across Chromium, Firefox, and WebKit, and emulates mobile devices, locales, and permissions.
Component Testing with Playwright
How Playwright's component testing mode mounts individual React, Vue, or Svelte components in a real browser for fast, isolated tests.