playwright-fixtures-and-hooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Playwright Fixtures and Hooks

Playwright Fixtures与生命周期钩子

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.
掌握Playwright的fixture系统与生命周期钩子,创建可复用的测试基础设施、管理测试状态并构建可维护的测试套件。本内容涵盖内置fixtures、自定义fixtures以及测试初始化与清理的最佳实践。

Built-in Fixtures

内置Fixtures

Core Fixtures

核心Fixtures

typescript
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/);
});
typescript
import { test, expect } from '@playwright/test';

test('using built-in fixtures', async ({
  page,      // Page实例
  context,   // 浏览器上下文
  browser,   // 浏览器实例
  request,   // API请求上下文
}) => {
  // 每个测试都会获取全新的page和context
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

Page Fixture

Page Fixture

typescript
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');
});
typescript
test('page fixture examples', async ({ page }) => {
  // 导航
  await page.goto('https://example.com');

  // 交互
  await page.getByRole('button', { name: 'Click me' }).click();

  // 等待
  await page.waitForLoadState('networkidle');

  // 执行脚本
  const title = await page.title();
  expect(title).toBe('Example Domain');
});

Context Fixture

Context Fixture

typescript
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');
});
typescript
test('context fixture examples', async ({ context, page }) => {
  // 添加Cookie
  await context.addCookies([
    {
      name: 'session',
      value: 'abc123',
      domain: 'example.com',
      path: '/',
    },
  ]);

  // 设置权限
  await context.grantPermissions(['geolocation']);

  // 在同一上下文中创建新页面
  const page2 = await context.newPage();
  await page2.goto('https://example.com');

  // 两个页面共享Cookie和存储
  await page.goto('https://example.com');
});

Browser Fixture

Browser Fixture

typescript
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();
});
typescript
test('browser fixture examples', async ({ browser }) => {
  // 创建带配置选项的自定义上下文
  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

Request Fixture

typescript
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();
});
typescript
test('API testing with request fixture', async ({ request }) => {
  // 发送GET请求
  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);

  // 发送POST请求
  const createResponse = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com',
    },
  });
  expect(createResponse.ok()).toBeTruthy();
});

Custom Fixtures

自定义Fixtures

Basic Custom Fixture

基础自定义Fixture

typescript
// 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';
typescript
// 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');
});
typescript
// 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';
typescript
// 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

带初始化和清理逻辑的Fixture

typescript
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');
  },
});
typescript
import { test as base } from '@playwright/test';

type DatabaseFixtures = {
  database: Database;
};

export const test = base.extend<DatabaseFixtures>({
  database: async ({}, use) => {
    // 初始化:创建数据库连接
    const db = await createDatabaseConnection();
    console.log('Database connected');

    // 为测试提供fixture
    await use(db);

    // 清理:关闭数据库连接
    await db.close();
    console.log('Database closed');
  },
});

Fixture Scopes: Test vs Worker

Fixture作用域:测试级 vs 工作进程级

typescript
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' },
  ],
});
typescript
import { test as base } from '@playwright/test';

type TestScopedFixtures = {
  uniqueId: string;
};

type WorkerScopedFixtures = {
  apiToken: string;
};

export const test = base.extend<TestScopedFixtures, WorkerScopedFixtures>({
  // 测试级作用域:为每个测试创建
  uniqueId: async ({}, use) => {
    const id = `test-${Date.now()}-${Math.random()}`;
    await use(id);
  },

  // 工作进程级作用域:每个工作进程仅创建一次
  apiToken: [
    async ({}, use) => {
      const token = await generateApiToken();
      await use(token);
      await revokeApiToken(token);
    },
    { scope: 'worker' },
  ],
});

Authentication Fixtures

鉴权Fixtures

Authenticated User Fixture

已认证用户Fixture

typescript
// 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';
typescript
// 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) => {
    // 创建带鉴权信息的新上下文
    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

多用户角色Fixture

typescript
// 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();
  },
});
typescript
// 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

鉴权初始化脚本

typescript
// 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' });
});
typescript
// 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

数据库Fixtures

Test Database Fixture

测试数据库Fixture

typescript
// 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();
  },
});
typescript
// 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) => {
    // 测试前清理数据库
    await db.user.deleteMany();
    await db.product.deleteMany();
    await db.order.deleteMany();

    await use();

    // 测试后清理数据库
    await db.user.deleteMany();
    await db.product.deleteMany();
    await db.order.deleteMany();
  },
});

Seeded Data Fixture

带初始数据的Fixture

typescript
// 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);
  },
});
typescript
// 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

API模拟Fixtures

Mock API Fixture

模拟API的Fixture

typescript
// 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();
  },
});
typescript
// 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) => {
    // 模拟API响应
    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();

    // 清理:移除所有路由规则
    await page.unrouteAll();
  },
});

Conditional Mocking

条件模拟

typescript
// 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();
  },
});
typescript
// 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) => {
      // 模拟慢速网络
      await new Promise((resolve) => setTimeout(resolve, 3000));
      await route.continue();
    });

    await use();
    await page.unrouteAll();
  },
});

Lifecycle Hooks

生命周期钩子

Test Hooks

测试钩子

typescript
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
  });
});
typescript
import { test, expect } from '@playwright/test';

test.describe('User Management', () => {
  test.beforeAll(async () => {
    // 在该describe块的所有测试执行前运行一次
    console.log('Setting up test suite');
  });

  test.beforeEach(async ({ page }) => {
    // 在每个测试执行前运行
    await page.goto('https://example.com');
    console.log('Test starting');
  });

  test.afterEach(async ({ page }, testInfo) => {
    // 在每个测试执行后运行
    console.log(`Test ${testInfo.status}: ${testInfo.title}`);

    if (testInfo.status !== testInfo.expectedStatus) {
      // 测试失败 - 捕获额外调试信息
      const screenshot = await page.screenshot();
      await testInfo.attach('failure-screenshot', {
        body: screenshot,
        contentType: 'image/png',
      });
    }
  });

  test.afterAll(async () => {
    // 在该describe块的所有测试执行后运行一次
    console.log('Cleaning up test suite');
  });

  test('test 1', async ({ page }) => {
    // 测试实现
  });

  test('test 2', async ({ page }) => {
    // 测试实现
  });
});

Nested Hooks

嵌套钩子

typescript
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
    });
  });
});
typescript
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 }) => {
      // 先执行父级beforeEach,再执行子级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 }) => {
      // 先执行父级beforeEach,再执行子级beforeEach
    });
  });
});

Conditional Hooks

条件钩子

typescript
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
  });
});
typescript
test.describe('Feature Tests', () => {
  test.beforeEach(async ({ page, browserName }) => {
    // 跳过Firefox浏览器的初始化
    if (browserName === 'firefox') {
      test.skip();
    }

    await page.goto('https://example.com');
  });

  test.afterEach(async ({ page }, testInfo) => {
    // 仅在测试失败时执行清理
    if (testInfo.status === 'failed') {
      await page.screenshot({ path: `failure-${testInfo.title}.png` });
    }
  });

  test('feature test', async ({ page }) => {
    // 测试实现
  });
});

Fixture Dependencies

Fixture依赖关系

Dependent Fixtures

依赖型Fixtures

typescript
// 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);
  },
});
typescript
// 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>({
  // 基础fixture
  config: async ({}, use) => {
    const config = {
      apiUrl: process.env.API_URL || 'http://localhost:3000',
      timeout: 30000,
    };
    await use(config);
  },

  // 依赖于config
  apiClient: async ({ config }, use) => {
    const client = new ApiClient(config.apiUrl, config.timeout);
    await use(client);
  },

  // 依赖于apiClient
  authenticatedClient: async ({ apiClient }, use) => {
    const token = await apiClient.login('user@example.com', 'password');
    apiClient.setAuthToken(token);
    await use(apiClient);
  },
});

Combining Multiple Fixtures

组合多个Fixtures

typescript
// 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();
  },
});
typescript
// 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
  ) => {
    // 所有依赖的fixtures已初始化
    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

高级Fixture模式

Factory Fixtures

工厂模式Fixtures

typescript
// 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 } });
    }
  },
});
typescript
// 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);

    // 清理:删除所有创建的用户
    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);

    // 清理:删除所有创建的产品
    for (const product of products) {
      await db.product.delete({ where: { id: product.id } });
    }
  },
});

Option Fixtures

选项式Fixtures

typescript
// 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);
  },
});
typescript
// 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
});
typescript
// 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);
  },
});
typescript
// 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');
  // 由于网络限流,此测试会变慢
});

test('test with normal network', async ({ page }) => {
  await page.goto('https://example.com');
  // 正常速度
});

Test Info and Attachments

Test Info与附件

Using Test Info

使用Test Info

typescript
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',
  });
});
typescript
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');

  // 附加截图
  const screenshot = await page.screenshot();
  await testInfo.attach('page-screenshot', {
    body: screenshot,
    contentType: 'image/png',
  });

  // 附加JSON数据
  await testInfo.attach('test-data', {
    body: JSON.stringify({ foo: 'bar' }),
    contentType: 'application/json',
  });

  // 附加文本
  await testInfo.attach('notes', {
    body: 'Test completed successfully',
    contentType: 'text/plain',
  });
});

Conditional Test Execution

条件测试执行

typescript
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();
});
typescript
test('browser-specific test', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'Not supported in Safari');

  await page.goto('https://example.com');
  // 此测试仅在Chromium和Firefox中运行
});

test('slow test', async ({ page }) => {
  test.slow(); // 为此测试设置三倍超时时间

  await page.goto('https://slow-site.example.com');
  // 长时间运行的操作
});

test('expected to fail', async ({ page }) => {
  test.fail(); // 标记为预期失败的测试

  await page.goto('https://example.com');
  await expect(page.getByText('Non-existent')).toBeVisible();
});

Fixture Best Practices

Fixture最佳实践

Organizing Fixtures

组织Fixtures

text
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
typescript
// 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';
text
fixtures/
├── index.ts              # 导出所有fixtures
├── auth-fixtures.ts      # 鉴权相关fixtures
├── database-fixtures.ts  # 数据库相关fixtures
├── mock-api-fixtures.ts  # API模拟相关fixtures
└── page-fixtures.ts      # 页面相关fixtures
typescript
// 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

Fixture命名规范

typescript
// 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
});
typescript
// 推荐的命名方式
export const test = base.extend({
  authenticatedPage: async ({}, use) => { /* ... */ },
  testUser: async ({}, use) => { /* ... */ },
  mockApi: async ({}, use) => { /* ... */ },
});

// 不推荐的命名方式
export const test = base.extend({
  page2: async ({}, use) => { /* ... */ },  // 描述性不足
  data: async ({}, use) => { /* ... */ },   // 过于通用
  fixture1: async ({}, use) => { /* ... */ }, // 无意义的名称
});

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
  • 搭建可复用的测试基础设施
  • 在多个测试间管理鉴权状态
  • 创建数据库初始化与清理逻辑
  • 为测试实现API模拟
  • 构建用于生成测试数据的工厂模式fixtures
  • 建立测试生命周期模式
  • 创建工作进程级作用域的fixtures以提升性能
  • 组织复杂的测试初始化与清理逻辑
  • 实现条件化测试行为
  • 构建类型安全的fixture系统

Resources

参考资源