Playwright Fixtures and Hooks
Master Playwright's fixture system and lifecycle hooks to create reusable test infrastructure, manage test state, and build maintainable test suites. This skill covers built-in fixtures, custom fixtures, and best practices for test setup and teardown.
Built-in Fixtures
Core Fixtures
import { test, expect } from '@playwright/test';
test('using built-in fixtures', async ({ page, // Page instance context, // Browser context browser, // Browser instance request, // API request context }) => { // Each test gets fresh page and context await page.goto('https://example.com'); await expect(page).toHaveTitle(/Example/); });
Page Fixture
test('page fixture examples', async ({ page }) => { // Navigate await page.goto('https://example.com');
// Interact await page.getByRole('button', { name: 'Click me' }).click();
// Wait await page.waitForLoadState('networkidle');
// Evaluate const title = await page.title(); expect(title).toBe('Example Domain'); });
Context Fixture
test('context fixture examples', async ({ context, page }) => { // Add cookies await context.addCookies([ { name: 'session', value: 'abc123', domain: 'example.com', path: '/', }, ]);
// Set permissions await context.grantPermissions(['geolocation']);
// Create additional page in same context const page2 = await context.newPage(); await page2.goto('https://example.com');
// Both pages share cookies and storage await page.goto('https://example.com'); });
Browser Fixture
test('browser fixture examples', async ({ browser }) => { // Create custom context with options const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, locale: 'en-US', timezoneId: 'America/New_York', permissions: ['geolocation'], });
const page = await context.newPage(); await page.goto('https://example.com');
await context.close(); });
Request Fixture
test('API testing with request fixture', async ({ request }) => { // Make GET request const response = await request.get('https://api.example.com/users'); expect(response.ok()).toBeTruthy(); expect(response.status()).toBe(200);
const users = await response.json(); expect(users).toHaveLength(10);
// Make POST request const createResponse = await request.post('https://api.example.com/users', { data: { name: 'John Doe', email: 'john@example.com', }, }); expect(createResponse.ok()).toBeTruthy(); });
Custom Fixtures
Basic Custom Fixture
// fixtures/base-fixtures.ts import { test as base } from '@playwright/test';
type MyFixtures = { timestamp: string; };
export const test = base.extend<MyFixtures>({ timestamp: async ({}, use) => { const timestamp = new Date().toISOString(); await use(timestamp); }, });
export { expect } from '@playwright/test';
// tests/example.spec.ts import { test, expect } from '../fixtures/base-fixtures';
test('using custom timestamp fixture', async ({ timestamp, page }) => {
console.log(Test started at: ${timestamp});
await page.goto('https://example.com');
});
Fixture with Setup and Teardown
import { test as base } from '@playwright/test';
type DatabaseFixtures = { database: Database; };
export const test = base.extend<DatabaseFixtures>({ database: async ({}, use) => { // Setup: Create database connection const db = await createDatabaseConnection(); console.log('Database connected');
// Provide fixture to test
await use(db);
// Teardown: Close database connection
await db.close();
console.log('Database closed');
}, });
Fixture Scopes: Test vs Worker
import { test as base } from '@playwright/test';
type TestScopedFixtures = { uniqueId: string; };
type WorkerScopedFixtures = { apiToken: string; };
export const test = base.extend<TestScopedFixtures, WorkerScopedFixtures>({
// Test-scoped: Created for each test
uniqueId: async ({}, use) => {
const id = test-${Date.now()}-${Math.random()};
await use(id);
},
// Worker-scoped: Created once per worker apiToken: [ async ({}, use) => { const token = await generateApiToken(); await use(token); await revokeApiToken(token); }, { scope: 'worker' }, ], });
Authentication Fixtures
Authenticated User Fixture
// fixtures/auth-fixtures.ts import { test as base } from '@playwright/test';
type AuthFixtures = { authenticatedPage: Page; };
export const test = base.extend<AuthFixtures>({ authenticatedPage: async ({ browser }, use) => { // Create new context with authentication const context = await browser.newContext({ storageState: 'auth.json', });
const page = await context.newPage();
await use(page);
await context.close();
}, });
export { expect } from '@playwright/test';
Multiple User Roles
// fixtures/multi-user-fixtures.ts import { test as base } from '@playwright/test';
type UserFixtures = { adminPage: Page; userPage: Page; guestPage: Page; };
export const test = base.extend<UserFixtures>({ adminPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: 'auth/admin.json', }); const page = await context.newPage(); await use(page); await context.close(); },
userPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: 'auth/user.json', }); const page = await context.newPage(); await use(page); await context.close(); },
guestPage: async ({ browser }, use) => { const context = await browser.newContext(); const page = await context.newPage(); await use(page); await context.close(); }, });
Authentication Setup
// auth/setup.ts import { test as setup } from '@playwright/test';
setup('authenticate as admin', async ({ page }) => { await page.goto('https://example.com/login'); await page.getByLabel('Email').fill('admin@example.com'); await page.getByLabel('Password').fill('admin123'); await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: 'auth/admin.json' }); });
setup('authenticate as user', async ({ page }) => { await page.goto('https://example.com/login'); await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('user123'); await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: 'auth/user.json' }); });
Database Fixtures
Test Database Fixture
// fixtures/database-fixtures.ts import { test as base } from '@playwright/test'; import { PrismaClient } from '@prisma/client';
type DatabaseFixtures = { db: PrismaClient; cleanDb: void; };
export const test = base.extend<DatabaseFixtures>({ db: [ async ({}, use) => { const db = new PrismaClient(); await use(db); await db.$disconnect(); }, { scope: 'worker' }, ],
cleanDb: async ({ db }, use) => { // Clean database before test await db.user.deleteMany(); await db.product.deleteMany(); await db.order.deleteMany();
await use();
// Clean database after test
await db.user.deleteMany();
await db.product.deleteMany();
await db.order.deleteMany();
}, });
Seeded Data Fixture
// fixtures/seed-fixtures.ts import { test as base } from './database-fixtures';
type SeedFixtures = { testUser: User; testProducts: Product[]; };
export const test = base.extend<SeedFixtures>({ testUser: async ({ db, cleanDb }, use) => { const user = await db.user.create({ data: { email: 'test@example.com', name: 'Test User', password: 'hashedpassword', }, });
await use(user);
},
testProducts: async ({ db, cleanDb }, use) => { const products = await db.product.createMany({ data: [ { name: 'Product 1', price: 10.99 }, { name: 'Product 2', price: 20.99 }, { name: 'Product 3', price: 30.99 }, ], });
const allProducts = await db.product.findMany();
await use(allProducts);
}, });
API Mocking Fixtures
Mock API Fixture
// fixtures/mock-api-fixtures.ts import { test as base } from '@playwright/test';
type MockApiFixtures = { mockApi: void; };
export const test = base.extend<MockApiFixtures>({ mockApi: async ({ page }, use) => { // Mock API responses await page.route('**/api/users', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }, ]), }); });
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 },
]),
});
});
await use();
// Cleanup: Unroute all
await page.unrouteAll();
}, });
Conditional Mocking
// fixtures/conditional-mock-fixtures.ts import { test as base } from '@playwright/test';
type ConditionalMockFixtures = { mockFailedApi: void; mockSlowApi: void; };
export const test = base.extend<ConditionalMockFixtures>({ mockFailedApi: async ({ page }, use) => { await page.route('/api/', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Internal Server Error' }), }); });
await use();
await page.unrouteAll();
},
mockSlowApi: async ({ page }, use) => { await page.route('/api/', async (route) => { // Simulate slow network await new Promise((resolve) => setTimeout(resolve, 3000)); await route.continue(); });
await use();
await page.unrouteAll();
}, });
Lifecycle Hooks
Test Hooks
import { test, expect } from '@playwright/test';
test.describe('User Management', () => { test.beforeAll(async () => { // Runs once before all tests in this describe block console.log('Setting up test suite'); });
test.beforeEach(async ({ page }) => { // Runs before each test await page.goto('https://example.com'); console.log('Test starting'); });
test.afterEach(async ({ page }, testInfo) => {
// Runs after each test
console.log(Test ${testInfo.status}: ${testInfo.title});
if (testInfo.status !== testInfo.expectedStatus) {
// Test failed - capture additional debug info
const screenshot = await page.screenshot();
await testInfo.attach('failure-screenshot', {
body: screenshot,
contentType: 'image/png',
});
}
});
test.afterAll(async () => { // Runs once after all tests in this describe block console.log('Cleaning up test suite'); });
test('test 1', async ({ page }) => { // Test implementation });
test('test 2', async ({ page }) => { // Test implementation }); });
Nested Hooks
test.describe('Parent Suite', () => { test.beforeEach(async ({ page }) => { console.log('Parent beforeEach'); await page.goto('https://example.com'); });
test.describe('Child Suite 1', () => { test.beforeEach(async ({ page }) => { console.log('Child 1 beforeEach'); await page.getByRole('link', { name: 'Products' }).click(); });
test('test in child 1', async ({ page }) => {
// Parent beforeEach runs first, then child beforeEach
});
});
test.describe('Child Suite 2', () => { test.beforeEach(async ({ page }) => { console.log('Child 2 beforeEach'); await page.getByRole('link', { name: 'About' }).click(); });
test('test in child 2', async ({ page }) => {
// Parent beforeEach runs first, then child beforeEach
});
}); });
Conditional Hooks
test.describe('Feature Tests', () => { test.beforeEach(async ({ page, browserName }) => { // Skip setup for Firefox if (browserName === 'firefox') { test.skip(); }
await page.goto('https://example.com');
});
test.afterEach(async ({ page }, testInfo) => {
// Only run teardown for failed tests
if (testInfo.status === 'failed') {
await page.screenshot({ path: failure-${testInfo.title}.png });
}
});
test('feature test', async ({ page }) => { // Test implementation }); });
Fixture Dependencies
Dependent Fixtures
// fixtures/dependent-fixtures.ts import { test as base } from '@playwright/test';
type DependentFixtures = { config: Config; apiClient: ApiClient; authenticatedClient: ApiClient; };
export const test = base.extend<DependentFixtures>({ // Base fixture config: async ({}, use) => { const config = { apiUrl: process.env.API_URL || 'http://localhost:3000', timeout: 30000, }; await use(config); },
// Depends on config apiClient: async ({ config }, use) => { const client = new ApiClient(config.apiUrl, config.timeout); await use(client); },
// Depends on apiClient authenticatedClient: async ({ apiClient }, use) => { const token = await apiClient.login('user@example.com', 'password'); apiClient.setAuthToken(token); await use(apiClient); }, });
Combining Multiple Fixtures
// fixtures/combined-fixtures.ts import { test as base } from '@playwright/test';
type CombinedFixtures = { setupComplete: void; };
export const test = base.extend<CombinedFixtures>({ setupComplete: async ( { page, db, mockApi, testUser }, use ) => { // All dependent fixtures are initialized await page.goto('https://example.com'); await page.context().addCookies([ { name: 'userId', value: testUser.id.toString(), domain: 'example.com', path: '/', }, ]);
await use();
}, });
Advanced Fixture Patterns
Factory Fixtures
// fixtures/factory-fixtures.ts import { test as base } from '@playwright/test';
type FactoryFixtures = { createUser: (data: Partial<User>) => Promise<User>; createProduct: (data: Partial<Product>) => Promise<Product>; };
export const test = base.extend<FactoryFixtures>({ createUser: async ({ db }, use) => { const users: User[] = [];
const createUser = async (data: Partial<User>) => {
const user = await db.user.create({
data: {
email: data.email || `user-${Date.now()}@example.com`,
name: data.name || 'Test User',
password: data.password || 'password123',
...data,
},
});
users.push(user);
return user;
};
await use(createUser);
// Cleanup: Delete all created users
for (const user of users) {
await db.user.delete({ where: { id: user.id } });
}
},
createProduct: async ({ db }, use) => { const products: Product[] = [];
const createProduct = async (data: Partial<Product>) => {
const product = await db.product.create({
data: {
name: data.name || `Product ${Date.now()}`,
price: data.price || 9.99,
description: data.description || 'Test product',
...data,
},
});
products.push(product);
return product;
};
await use(createProduct);
// Cleanup: Delete all created products
for (const product of products) {
await db.product.delete({ where: { id: product.id } });
}
}, });
Option Fixtures
// fixtures/option-fixtures.ts import { test as base } from '@playwright/test';
type OptionsFixtures = { slowNetwork: boolean; };
export const test = base.extend<OptionsFixtures>({ slowNetwork: [false, { option: true }],
page: async ({ page, slowNetwork }, use) => { if (slowNetwork) { await page.route('**/*', async (route) => { await new Promise((resolve) => setTimeout(resolve, 1000)); await route.continue(); }); }
await use(page);
}, });
// tests/slow-network.spec.ts import { test, expect } from '../fixtures/option-fixtures';
test('test with slow network', async ({ page }) => { test.use({ slowNetwork: true });
await page.goto('https://example.com'); // This will be slow due to network throttling });
test('test with normal network', async ({ page }) => { await page.goto('https://example.com'); // Normal speed });
Test Info and Attachments
Using Test Info
test('example with test info', async ({ page }, testInfo) => {
console.log(Test title: ${testInfo.title});
console.log(Project: ${testInfo.project.name});
console.log(Retry: ${testInfo.retry});
await page.goto('https://example.com');
// Attach screenshot const screenshot = await page.screenshot(); await testInfo.attach('page-screenshot', { body: screenshot, contentType: 'image/png', });
// Attach JSON data await testInfo.attach('test-data', { body: JSON.stringify({ foo: 'bar' }), contentType: 'application/json', });
// Attach text await testInfo.attach('notes', { body: 'Test completed successfully', contentType: 'text/plain', }); });
Conditional Test Execution
test('browser-specific test', async ({ page, browserName }) => { test.skip(browserName === 'webkit', 'Not supported in Safari');
await page.goto('https://example.com'); // Test only runs in Chromium and Firefox });
test('slow test', async ({ page }) => { test.slow(); // Triple timeout for this test
await page.goto('https://slow-site.example.com'); // Long-running operations });
test('expected to fail', async ({ page }) => { test.fail(); // Mark as expected failure
await page.goto('https://example.com'); await expect(page.getByText('Non-existent')).toBeVisible(); });
Fixture Best Practices
Organizing Fixtures
fixtures/ ├── index.ts # Export all fixtures ├── auth-fixtures.ts # Authentication fixtures ├── database-fixtures.ts # Database fixtures ├── mock-api-fixtures.ts # API mocking fixtures └── page-fixtures.ts # Page-related fixtures
// fixtures/index.ts import { test as authTest } from './auth-fixtures'; import { test as dbTest } from './database-fixtures'; import { test as mockTest } from './mock-api-fixtures';
export const test = authTest.extend(dbTest.fixtures).extend(mockTest.fixtures);
export { expect } from '@playwright/test';
Fixture Naming Conventions
// Good naming export const test = base.extend({ authenticatedPage: async ({}, use) => { /* ... / }, testUser: async ({}, use) => { / ... / }, mockApi: async ({}, use) => { / ... */ }, });
// Avoid export const test = base.extend({ page2: async ({}, use) => { /* ... / }, // Not descriptive data: async ({}, use) => { / ... / }, // Too generic fixture1: async ({}, use) => { / ... */ }, // Meaningless name });
When to Use This Skill
-
Setting up reusable test infrastructure
-
Managing authentication state across tests
-
Creating database seeding and cleanup logic
-
Implementing API mocking for tests
-
Building factory fixtures for test data generation
-
Establishing test lifecycle patterns
-
Creating worker-scoped fixtures for performance
-
Organizing complex test setup and teardown
-
Implementing conditional test behavior
-
Building type-safe fixture systems
Resources
-
Playwright Fixtures: https://playwright.dev/docs/test-fixtures
-
Playwright Test Hooks: https://playwright.dev/docs/test-hooks
-
Playwright API Testing: https://playwright.dev/docs/api-testing