一键导入
test-fixtures-setup
Custom fixtures, test.extend(), setup/teardown projects, global setup, and test lifecycle management in Playwright
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Custom fixtures, test.extend(), setup/teardown projects, global setup, and test lifecycle management in Playwright
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Accessibility (a11y) testing patterns with Playwright and axe-core. Use when adding WCAG 2.1 compliance checks, keyboard navigation testing, screen reader compatibility, or color contrast validation to your test suite.
Storage state reuse, 2FA/TOTP testing, multi-role auth, session management, OAuth flows, and secure credential handling in Playwright
GitHub Actions setup, parallel execution, sharding, test filtering, artifact management, and pipeline optimization for Playwright tests
UIActions pattern for centralized Playwright interactions. Use when implementing clean page object interactions, creating reusable action classes for buttons, inputs, dropdowns, checkboxes, or building a centralized interaction gateway.
REST API testing patterns using Playwright built-in request context. Use when testing backend APIs, setting up test data via API calls, validating request/response schemas, handling authentication, or mocking API responses for isolated UI testing.
AssertUtils and ExpectUtils patterns for centralized test validation in Playwright. Use when implementing reusable assertions, soft assertions, or building a consistent validation layer across your test suite.
| name | Test Fixtures & Setup |
| description | Custom fixtures, test.extend(), setup/teardown projects, global setup, and test lifecycle management in Playwright |
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.
// ❌ BAD: Setup logic scattered in every test
test('user can view dashboard', async ({ page }) => {
// Login setup repeated everywhere
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');
// Actual test starts here...
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
// ✅ GOOD: Fixture handles setup, test focuses on behavior
test('user can view dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
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
});
test.extend()// fixtures/base.ts
import { test as base, expect } from '@playwright/test';
// Define fixture types
type MyFixtures = {
// Per-test fixtures (fresh for each test)
homePage: Page;
apiClient: APIRequestContext;
// Per-worker fixtures (shared across tests in a worker)
adminToken: string;
};
export const test = base.extend<MyFixtures>({
// Simple fixture - runs setup before test, teardown after
homePage: async ({ page }, use) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await use(page); // <-- test runs here
// Teardown: anything after use() runs after the test
},
// Fixture with API client
apiClient: async ({ request }, use) => {
// Setup: create authenticated API client
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);
// Teardown: dispose API context
await apiContext.dispose();
},
});
export { expect };
// fixtures/worker-fixtures.ts
import { test as base } from '@playwright/test';
type WorkerFixtures = {
adminToken: string;
testDatabase: { connectionString: string };
};
export const test = base.extend<{}, WorkerFixtures>({
// Worker-scoped: created once per worker, shared across tests
adminToken: [async ({}, use) => {
// Expensive setup - only runs once per worker
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);
// Worker teardown
console.log('Admin session cleaned up');
}, { scope: 'worker' }],
testDatabase: [async ({}, use) => {
// Create isolated test database per worker
const db = await createTestDatabase();
await use(db);
await db.cleanup();
}, { scope: 'worker' }],
});
// fixtures/index.ts
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);
},
// Fixture that depends on another fixture
authenticatedPage: async ({ page, loginPage }, use) => {
await loginPage.loginAs('user@example.com', 'password123');
await use(page); // page is now authenticated
},
});
export { expect };
export const test = base.extend<{ autoTracing: void }>({
// Auto-fixture: runs for every test without being referenced
autoTracing: [async ({ page }, use, testInfo) => {
// Start tracing before test
await page.context().tracing.start({
screenshots: true,
snapshots: true,
});
await use();
// Save trace on failure
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 }], // <-- auto: true means it runs for every test
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
// Setup project: runs first, creates shared auth state
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Test projects: depend on setup, run after it completes
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
],
});
// tests/auth.setup.ts
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 }) => {
// Perform login
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();
// Wait for auth to complete
await page.waitForURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Save storage state (cookies + localStorage)
await page.context().storageState({ path: authFile });
});
// playwright.config.ts - reference the saved auth state
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/user.json', // Reuse saved auth
},
dependencies: ['setup'],
},
],
});
// tests/auth.setup.ts
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 });
});
// playwright.config.ts
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.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
// Seed test database
await seedDatabase();
// Create test data via API
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;
// global-teardown.ts
import { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
// Clean up test data
await cleanupDatabase();
// Remove temp files
await removeTestArtifacts();
}
export default globalTeardown;
// playwright.config.ts
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
});
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 point
await page.goto('/');
// Accept cookies if banner appears
const 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 failure
if (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/test-data.ts
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)}`);
},
});
// fixtures/api-data.ts
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) => {
// Create user via API before test
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);
// Cleanup: delete user after test
await request.delete(`/api/users/${user.id}`);
},
createdOrder: async ({ request, createdUser }, use) => {
// Create order that depends on user fixture
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);
// Cleanup: cancel order after test
await request.delete(`/api/orders/${order.id}`);
},
});
// Test with different viewport sizes
type ViewportFixtures = {
viewportSize: { width: number; height: number };
};
export const test = base.extend<ViewportFixtures>({
viewportSize: [{ width: 1280, height: 720 }, { option: true }],
});
// Override in config or test
test.use({ viewportSize: { width: 375, height: 667 } }); // Mobile
export const test = base.extend({
slowService: [async ({}, use) => {
const service = await startSlowService();
await use(service);
await service.stop();
}, { timeout: 60_000 }], // 60 second timeout for this fixture
});
// fixtures/index.ts - combine all fixtures
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';
// Merge all fixtures into a single test object
export const test = mergeTests(authTest, dataTest, pageTest);
export { expect } from '@playwright/test';
// ❌ BAD: Global mutable state
let authToken: string;
test.beforeAll(async () => {
authToken = await getAuthToken();
});
test('uses global token', async ({ page }) => {
// authToken might be undefined if beforeAll fails
await page.setExtraHTTPHeaders({ Authorization: `Bearer ${authToken}` });
});
// ✅ GOOD: Use a worker-scoped fixture
export const test = base.extend<{}, { authToken: string }>({
authToken: [async ({}, use) => {
const token = await getAuthToken();
await use(token);
}, { scope: 'worker' }],
});
// ❌ BAD: No cleanup - test data accumulates
test('create user', async ({ request }) => {
await request.post('/api/users', { data: userData });
// User is never cleaned up!
});
// ✅ GOOD: Fixture handles cleanup automatically
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}`); // Always cleans up
},
});
// ❌ BAD: Hardcoded data causes collisions in parallel runs
const TEST_EMAIL = 'test@example.com';
// ✅ GOOD: Generate unique data per test
const TEST_EMAIL = `test-${Date.now()}@example.com`;
| 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 |