| name | Test Fixtures & Setup |
| description | Custom fixtures, test.extend(), setup/teardown projects, global setup, and test lifecycle management in Playwright |
Test Fixtures & Setup Skill
Overview
Playwright fixtures are the foundation of well-structured tests. They provide reusable setup/teardown logic, enable dependency injection, and keep tests isolated and maintainable. This skill covers custom fixtures, setup projects, global configuration, and test lifecycle patterns.
Why Fixtures Matter
test('user can view dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('user can view dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Core Concepts
1. Built-in Fixtures
Playwright provides these fixtures out of the box:
| Fixture | Scope | Purpose |
|---|
page | Test | Isolated browser page |
context | Test | Browser context (cookies, storage) |
browser | Worker | Shared browser instance |
browserName | Worker | Current browser name |
request | Test | API request context |
test('uses built-in fixtures', async ({ page, context, browser, request }) => {
});
2. Creating Custom Fixtures with test.extend()
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
homePage: Page;
apiClient: APIRequestContext;
adminToken: string;
};
export const test = base.extend<MyFixtures>({
homePage: async ({ page }, use) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await use(page);
},
apiClient: async ({ request }, use) => {
const response = await request.post('/api/auth/login', {
data: { email: 'test@example.com', password: 'password123' }
});
const { token } = await response.json();
const apiContext = await request.newContext({
extraHTTPHeaders: { Authorization: `Bearer ${token}` }
});
await use(apiContext);
await apiContext.dispose();
},
});
export { expect };
3. Worker-Scoped Fixtures (Shared Across Tests)
import { test as base } from '@playwright/test';
type WorkerFixtures = {
adminToken: string;
testDatabase: { connectionString: string };
};
export const test = base.extend<{}, WorkerFixtures>({
adminToken: [async ({}, use) => {
const response = await fetch('https://api.example.com/auth/admin', {
method: 'POST',
body: JSON.stringify({ key: process.env.ADMIN_KEY }),
});
const { token } = await response.json();
await use(token);
console.log('Admin session cleaned up');
}, { scope: 'worker' }],
testDatabase: [async ({}, use) => {
const db = await createTestDatabase();
await use(db);
await db.cleanup();
}, { scope: 'worker' }],
});
4. Fixture Composition (Building on Other Fixtures)
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type PageFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
};
export const test = base.extend<PageFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page, loginPage }, use) => {
await loginPage.loginAs('user@example.com', 'password123');
await use(page);
},
});
export { expect };
5. Auto-Fixtures (Always Run, Even If Not Referenced)
export const test = base.extend<{ autoTracing: void }>({
autoTracing: [async ({ page }, use, testInfo) => {
await page.context().tracing.start({
screenshots: true,
snapshots: true,
});
await use();
if (testInfo.status !== 'passed') {
const tracePath = testInfo.outputPath('trace.zip');
await page.context().tracing.stop({ path: tracePath });
testInfo.attachments.push({
name: 'trace',
path: tracePath,
contentType: 'application/zip',
});
} else {
await page.context().tracing.stop();
}
}, { auto: true }],
});
Setup Projects
6. Global Setup with Setup Projects
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
],
});
7. Authentication Setup Project
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../.auth/user.json');
setup('authenticate as user', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await page.context().storageState({ path: authFile });
});
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/user.json',
},
dependencies: ['setup'],
},
],
});
8. Multiple Auth Roles
import { test as setup } from '@playwright/test';
const adminFile = '.auth/admin.json';
const userFile = '.auth/user.json';
const readonlyFile = '.auth/readonly.json';
setup('authenticate as admin', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.ADMIN_EMAIL!);
await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/admin');
await page.context().storageState({ path: adminFile });
});
setup('authenticate as user', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: userFile });
});
setup('authenticate as readonly', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.READONLY_EMAIL!);
await page.getByLabel('Password').fill(process.env.READONLY_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: readonlyFile });
});
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'admin-tests',
testDir: './tests/admin',
use: { storageState: '.auth/admin.json' },
dependencies: ['setup'],
},
{
name: 'user-tests',
testDir: './tests/user',
use: { storageState: '.auth/user.json' },
dependencies: ['setup'],
},
{
name: 'readonly-tests',
testDir: './tests/readonly',
use: { storageState: '.auth/readonly.json' },
dependencies: ['setup'],
},
],
});
Global Setup & Teardown
9. Global Setup File
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
await seedDatabase();
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('/api/test/seed');
await page.waitForResponse(resp => resp.url().includes('/api/test/seed') && resp.ok());
await browser.close();
}
export default globalSetup;
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
await cleanupDatabase();
await removeTestArtifacts();
}
export default globalTeardown;
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
});
10. Test Lifecycle Hooks
import { test, expect } from '@playwright/test';
test.beforeAll(async ({ browser }) => {
console.log('Starting test suite...');
});
test.beforeEach(async ({ page }) => {
await page.goto('/');
const cookieBanner = page.getByRole('button', { name: 'Accept' });
if (await cookieBanner.isVisible({ timeout: 1000 }).catch(() => false)) {
await cookieBanner.click();
}
});
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({
path: `screenshots/${testInfo.title}-failure.png`,
fullPage: true,
});
}
});
test.afterAll(async () => {
console.log('Test suite complete.');
});
Test Data Patterns
11. Test Data Factory Fixture
import { test as base } from '@playwright/test';
import { faker } from '@faker-js/faker';
type TestDataFixtures = {
testUser: { email: string; password: string; name: string };
testProduct: { name: string; price: number; sku: string };
uniqueId: string;
};
export const test = base.extend<TestDataFixtures>({
testUser: async ({}, use) => {
const user = {
email: faker.internet.email(),
password: faker.internet.password({ length: 12 }),
name: faker.person.fullName(),
};
await use(user);
},
testProduct: async ({}, use) => {
const product = {
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price({ min: 10, max: 500 })),
sku: faker.string.alphanumeric(8).toUpperCase(),
};
await use(product);
},
uniqueId: async ({}, use) => {
await use(`test-${Date.now()}-${faker.string.nanoid(6)}`);
},
});
12. API-Based Test Data Setup
import { test as base } from '@playwright/test';
type ApiDataFixtures = {
createdUser: { id: string; email: string };
createdOrder: { id: string; total: number };
};
export const test = base.extend<ApiDataFixtures>({
createdUser: async ({ request }, use) => {
const response = await request.post('/api/users', {
data: {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
role: 'user',
},
});
const user = await response.json();
await use(user);
await request.delete(`/api/users/${user.id}`);
},
createdOrder: async ({ request, createdUser }, use) => {
const response = await request.post('/api/orders', {
data: {
userId: createdUser.id,
items: [{ productId: 'prod-1', quantity: 1 }],
},
});
const order = await response.json();
await use(order);
await request.delete(`/api/orders/${order.id}`);
},
});
Common Patterns
13. Parameterized Fixtures
type ViewportFixtures = {
viewportSize: { width: number; height: number };
};
export const test = base.extend<ViewportFixtures>({
viewportSize: [{ width: 1280, height: 720 }, { option: true }],
});
test.use({ viewportSize: { width: 375, height: 667 } });
14. Fixture with Timeout
export const test = base.extend({
slowService: [async ({}, use) => {
const service = await startSlowService();
await use(service);
await service.stop();
}, { timeout: 60_000 }],
});
15. Merging Multiple Fixture Files
import { mergeTests } from '@playwright/test';
import { test as authTest } from './auth-fixtures';
import { test as dataTest } from './data-fixtures';
import { test as pageTest } from './page-fixtures';
export const test = mergeTests(authTest, dataTest, pageTest);
export { expect } from '@playwright/test';
Anti-Patterns to Avoid
❌ Don't Use Global Variables for Shared State
let authToken: string;
test.beforeAll(async () => {
authToken = await getAuthToken();
});
test('uses global token', async ({ page }) => {
await page.setExtraHTTPHeaders({ Authorization: `Bearer ${authToken}` });
});
export const test = base.extend<{}, { authToken: string }>({
authToken: [async ({}, use) => {
const token = await getAuthToken();
await use(token);
}, { scope: 'worker' }],
});
❌ Don't Skip Teardown
test('create user', async ({ request }) => {
await request.post('/api/users', { data: userData });
});
export const test = base.extend({
testUser: async ({ request }, use) => {
const resp = await request.post('/api/users', { data: userData });
const user = await resp.json();
await use(user);
await request.delete(`/api/users/${user.id}`);
},
});
❌ Don't Hardcode Setup Data
const TEST_EMAIL = 'test@example.com';
const TEST_EMAIL = `test-${Date.now()}@example.com`;
Quick Reference
| Pattern | When to Use | Scope |
|---|
test.extend() | Custom reusable setup/teardown | Test or Worker |
| Setup projects | Auth state, database seeding | Before all tests |
globalSetup | One-time environment prep | Before entire run |
beforeAll/afterAll | Per-file setup/teardown | Test file |
beforeEach/afterEach | Per-test setup/teardown | Each test |
| Auto-fixtures | Tracing, logging, screenshots | Every test |
mergeTests() | Combining fixture files | Imports |
Related Skills