In this chapter, we’ll dive deep into fixtures, one of Playwright’s most powerful mechanisms for organizing test setup, teardown, and resource management. Fixtures make it easy to define reusable contexts like browser sessions, authenticated users, or API clients and use them seamlessly across your test suite.
By mastering fixtures, you’ll simplify your test code, reduce duplication, and gain fine-grained control over how your environment is prepared and cleaned up.
What Are Fixtures?
In Playwright, fixtures are reusable functions that prepare test resources before execution and clean them up afterward. They are the backbone of test initialization.
For example, you can define a fixture that launches a browser, logs in a user, or prepares test data, and automatically inject it into tests that need it.
Why use fixtures?
- Reuse setup logic across multiple tests.
- Keep test code clean and focused on behavior.
- Ensure consistency and isolation between tests.
- Automatically manage setup and teardown.
Using Built-in Fixtures
Playwright provides several built-in fixtures out of the box:
| Fixture | Description |
|---|---|
page | A single browser tab |
context | A browser context (like an incognito session) |
browser | The underlying browser instance |
request | API request context |
Example using built-in fixtures:
import { test, expect } from '@playwright/test';
test('verify title using built-in page fixture', async ({ page }) => {
await page.goto('https://playwright.dev');
await expect(page).toHaveTitle(/Playwright/);
});
Here, the page fixture is created before the test and automatically destroyed afterward.
Hooks – beforeEach and afterEach
Hooks help run setup and cleanup code around each test.
Example:
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com/login');
});
test.afterEach(async () => {
console.log('Test complete, cleaning up');
});
test('login button should be visible', async ({ page }) => {
await expect(page.locator('#login-button')).toBeVisible();
});
Use
beforeAllandafterAllfor setup/teardown that runs once per file.
Creating Custom Fixtures
You can define your own fixtures using the test.extend() function.
Example: Logged-in User Fixture
import { test as base } from '@playwright/test';
export const test = base.extend({
loggedInPage: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://www.saucedemo.com/');
await page.fill('#user-name', 'standard_user');
await page.fill('#password', 'secret_sauce');
await page.click('#login-button');
await use(page); // Make the logged-in page available to tests
await context.close(); // Cleanup
},
});
Now use it in your test:
import { test, expect } from '../fixtures/authFixture';
test('verify user is logged in', async ({ loggedInPage }) => {
await expect(loggedInPage).toHaveURL(/inventory/);
});
This approach eliminates repetitive login steps and maintains clean test logic.
Fixture Dependencies
Fixtures can depend on one another. For example, a userData fixture can feed data into a loggedInPage fixture.
import { test as base } from '@playwright/test';
export const test = base.extend({
userData: async ({}, use) => {
await use({ username: 'standard_user', password: 'secret_sauce' });
},
loggedInPage: async ({ browser, userData }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://www.saucedemo.com/');
await page.fill('#user-name', userData.username);
await page.fill('#password', userData.password);
await page.click('#login-button');
await use(page);
await context.close();
},
});
This makes your fixtures modular and composable.
Global Setup and Teardown
Some setup steps only need to run once before the entire test suite like initializing databases or creating API tokens.
You can define these in your Playwright configuration:
playwright.config.js:
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: './global-setup.js',
globalTeardown: './global-teardown.js',
});
global-setup.js:
export default async () => {
console.log('Running global setup...');
};
global-teardown.js:
export default async () => {
console.log('Global teardown complete.');
};
Combining Fixtures with the Page Object Model (POM)
Fixtures work beautifully with Page Object Models, allowing you to inject page objects directly into your tests.
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
export const test = base.extend({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
Usage:
import { test, expect } from '../fixtures/pageFixture';
test('user can log in successfully', async ({ loginPage }) => {
await loginPage.goto();
await loginPage.login('standard_user', 'secret_sauce');
await expect(loginPage.page).toHaveURL(/inventory/);
});
Fixture Scope and Lifetime
Playwright fixtures can have different lifetimes:
| Scope | Description |
test | Created and destroyed for each test (default) |
worker | Shared across all tests running in the same worker thread |
Example:
const test = base.extend({
apiClient: [async ({}, use) => {
const client = new ApiClient();
await use(client);
}, { scope: 'worker' }],
});
Use
workerscope for expensive resources (e.g., DB connections, API clients).
Best Practices
- Use built-in fixtures like
pageandcontextwhenever possible. - Modularize fixture logic into separate files.
- Combine fixtures with POM for clean architecture.
- Keep fixtures simple and avoid business logic inside them.
- Use
workerscope for shared resources andtestscope for isolation. - Always close browser contexts to prevent memory leaks.
Exercise
Task 1: Create a fixture that sets up a logged-in user.
Task 2: Add a userData fixture to inject login credentials.
Task 3: Combine fixtures with your page objects.
Task 4: Implement a global setup that initializes an API token.
Bonus: Use a worker-scoped fixture to cache the API token across tests.
Cheat Sheet
| Concept | Code Example |
| Built-in fixture | { page, browser, context } |
| Custom fixture | test.extend({ myFixture: async ({}, use) => {...} }) |
| Fixture dependency | test.extend({ fixtureA, fixtureB: async ({ fixtureA }, use) => {...} }) |
| Scope | { scope: 'worker' } for shared fixtures |
| Global setup | globalSetup: './global-setup.js' |
Summary
In this chapter, you learned how to:
- Use Playwright’s built-in fixtures (
page,browser,context). - Implement setup and teardown using hooks.
- Build and combine custom fixtures.
- Control fixture lifetime using
testandworkerscopes. - Integrate fixtures with Page Object Models.
Fixtures provide structure, reusability, and consistency to your Playwright framework. They turn repetitive setup logic into clean, maintainable, and modular components.
