Fixtures provide dependencies to tests — browser, context, page, request — with automatic setup and teardown. Extend fixtures for shared auth, API clients, or page objects.
Built-In Fixtures
Tests declare needed fixtures in the function signature: async ({ page, request }). The runner creates, scopes, and disposes them automatically.
Fixture scope defaults to per-test. Worker-scoped fixtures ({ scope: 'worker' }) share expensive setup across tests in a worker.
import { test as base, expect } from '@playwright/test';
type Fixtures = { todoPage: TodoPage };
export const test = base.extend<Fixtures>({
todoPage: async ({ page }, use) => {
const todo = new TodoPage(page);
await todo.goto();
await use(todo);
},
});
test('adds todo', async ({ todoPage }) => {
await todoPage.add('Buy milk');
});
Custom fixtures wrap setup/teardown around use().
Fixture Lifecycle
myFixture: async ({ page }, use) => {
// setup before use
await use(value);
// teardown after test
},
Code before use() runs before the test.
Code after use() runs after the test completes.
Fixtures can depend on other fixtures via destructuring.
Export extended test from fixtures file; import in specs.
Use for expensive shared resources — mind parallel isolation.
Worker-Scoped Fixtures
Fixtures can be scoped to test, worker, or custom scopes — worker scope shares expensive setup like a database seed across tests in one worker process.
Common Mistakes
Global variables instead of fixtures for shared setup.
Forgetting teardown after use() for created data.
Worker fixtures holding per-test mutable state.
Circular fixture dependencies.
Key Takeaways
Fixtures inject setup into tests cleanly.
test.extend creates composable custom fixtures.
Setup/teardown wraps use() callback.
Choose test vs worker scope deliberately.
Pro Tip
Put auth + page object fixtures in tests/fixtures.ts and import test from there everywhere — one pattern for the whole repo.
Continue with Test Data to build on what you learned here.