Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
// ✅ GOOD: Fixture handles setup, test focuses on behavior
test
'user can view dashboard'
async
await
goto
'/dashboard'
await
expect
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 }) => {
// page = isolated page, fresh for each test// context = browser context with its own cookies/storage// browser = shared browser instance across tests in a worker// request = API request context for direct API calls
});
2. Creating Custom Fixtures with test.extend()
// fixtures/base.tsimport { test as base, expect } from'@playwright/test';
// Define fixture typestypeMyFixtures = {
// Per-test fixtures (fresh for each test)homePage: Page;
apiClient: APIRequestContext;
// Per-worker fixtures (shared across tests in a worker)adminToken: string;
};
exportconst test = base.extend<MyFixtures>({
// Simple fixture - runs setup before test, teardown afterhomePage: async ({ page }, use) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
awaituse(page); // <-- test runs here// Teardown: anything after use() runs after the test
},
// Fixture with API clientapiClient: async ({ request }, use) => {
// Setup: create authenticated API clientconst 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}` }
});
awaituse(apiContext);
// Teardown: dispose API contextawait apiContext.dispose();
},
});
export { expect };
3. Worker-Scoped Fixtures (Shared Across Tests)
// fixtures/worker-fixtures.tsimport { test as base } from'@playwright/test';
typeWorkerFixtures = {
adminToken: string;
testDatabase: { connectionString: string };
};
exportconst test = base.extend<{}, WorkerFixtures>({
// Worker-scoped: created once per worker, shared across testsadminToken: [async ({}, use) => {
// Expensive setup - only runs once per workerconst response = awaitfetch('https://api.example.com/auth/admin', {
method: 'POST',
body: JSON.stringify({ key: process.env.ADMIN_KEY }),
});
const { token } = await response.json();
awaituse(token);
// Worker teardownconsole.log('Admin session cleaned up');
}, { scope: 'worker' }],
testDatabase: [async ({}, use) => {
// Create isolated test database per workerconst db = awaitcreateTestDatabase();
awaituse(db);
await db.cleanup();
}, { scope: 'worker' }],
});
4. Fixture Composition (Building on Other Fixtures)
import { test, expect } from'@playwright/test';
// Runs once before all tests in this file
test.beforeAll(async ({ browser }) => {
console.log('Starting test suite...');
});
// Runs before each test
test.beforeEach(async ({ page }) => {
// Navigate to starting pointawait page.goto('/');
// Accept cookies if banner appearsconst cookieBanner = page.getByRole('button', { name: 'Accept' });
if (await cookieBanner.isVisible({ timeout: 1000 }).catch(() =>false)) {
await cookieBanner.click();
}
});
// Runs after each test
test.afterEach(async ({ page }, testInfo) => {
// Screenshot on failureif (testInfo.status !== 'passed') {
await page.screenshot({
path: `screenshots/${testInfo.title}-failure.png`,
fullPage: true,
});
}
});
// Runs once after all tests in this file
test.afterAll(async () => {
console.log('Test suite complete.');
});
// fixtures/api-data.tsimport { test as base } from'@playwright/test';
typeApiDataFixtures = {
createdUser: { id: string; email: string };
createdOrder: { id: string; total: number };
};
exportconst test = base.extend<ApiDataFixtures>({
createdUser: async ({ request }, use) => {
// Create user via API before testconst response = await request.post('/api/users', {
data: {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
role: 'user',
},
});
const user = await response.json();
awaituse(user);
// Cleanup: delete user after testawait request.delete(`/api/users/${user.id}`);
},
createdOrder: async ({ request, createdUser }, use) => {
// Create order that depends on user fixtureconst response = await request.post('/api/orders', {
data: {
userId: createdUser.id,
items: [{ productId: 'prod-1', quantity: 1 }],
},
});
const order = await response.json();
awaituse(order);
// Cleanup: cancel order after testawait request.delete(`/api/orders/${order.id}`);
},
});
Common Patterns
13. Parameterized Fixtures
// Test with different viewport sizestypeViewportFixtures = {
viewportSize: { width: number; height: number };
};
exportconst test = base.extend<ViewportFixtures>({
viewportSize: [{ width: 1280, height: 720 }, { option: true }],
});
// Override in config or test
test.use({ viewportSize: { width: 375, height: 667 } }); // Mobile
14. Fixture with Timeout
exportconst test = base.extend({
slowService: [async ({}, use) => {
const service = awaitstartSlowService();
awaituse(service);
await service.stop();
}, { timeout: 60_000 }], // 60 second timeout for this fixture
});
15. Merging Multiple Fixture Files
// fixtures/index.ts - combine all fixturesimport { 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';
// Merge all fixtures into a single test objectexportconst test = mergeTests(authTest, dataTest, pageTest);
export { expect } from'@playwright/test';
Anti-Patterns to Avoid
❌ Don't Use Global Variables for Shared State
// ❌ BAD: Global mutable stateletauthToken: string;
test.beforeAll(async () => {
authToken = awaitgetAuthToken();
});
test('uses global token', async ({ page }) => {
// authToken might be undefined if beforeAll failsawait page.setExtraHTTPHeaders({ Authorization: `Bearer ${authToken}` });
});
// ✅ GOOD: Use a worker-scoped fixtureexportconst test = base.extend<{}, { authToken: string }>({
authToken: [async ({}, use) => {
const token = awaitgetAuthToken();
awaituse(token);
}, { scope: 'worker' }],
});
❌ Don't Skip Teardown
// ❌ BAD: No cleanup - test data accumulatestest('create user', async ({ request }) => {
await request.post('/api/users', { data: userData });
// User is never cleaned up!
});
// ✅ GOOD: Fixture handles cleanup automaticallyexportconst test = base.extend({
testUser: async ({ request }, use) => {
const resp = await request.post('/api/users', { data: userData });
const user = await resp.json();
awaituse(user);
await request.delete(`/api/users/${user.id}`); // Always cleans up
},
});
❌ Don't Hardcode Setup Data
// ❌ BAD: Hardcoded data causes collisions in parallel runsconstTEST_EMAIL = 'test@example.com';
// ✅ GOOD: Generate unique data per testconstTEST_EMAIL = `test-${Date.now()}@example.com`;