| name | playwright-fixtures-and-hooks |
| description | Use when managing test state and infrastructure with reusable Playwright fixtures and lifecycle hooks for efficient test setup and teardown. |
| allowed-tools | Bash, Read, Write, Edit |
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