实战项目

真实场景案例

TypeScript 版 Playwright 的完整实战示例,覆盖 E2E 测试、API 测试、视觉回归等常见场景。

// 案例 01

E2E 登录测试套件

使用 Playwright Test 编写完整的登录流程测试,包含 Page Object 和 Fixture 模式。

E2E 测试 Page Object Fixture
pages/login-page.ts — Page Object
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  private readonly usernameInput: Locator;
  private readonly passwordInput: Locator;
  private readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(private readonly page: Page) {
    this.usernameInput = page.getByLabel('用户名');
    this.passwordInput = page.getByLabel('密码');
    this.submitButton = page.getByRole('button', { name: '登录' });
    this.errorMessage = page.locator('.error-message');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}
tests/login.spec.ts — 测试用例
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';

test.describe('登录功能', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('正确账密登录成功', async ({ page }) => {
    await loginPage.login('admin', 'password123');
    await expect(page).toHaveURL('**/dashboard');
    await expect(page.getByText('欢迎回来')).toBeVisible();
  });

  test('错误密码显示提示', async ({ page }) => {
    await loginPage.login('admin', 'wrong');
    await expect(loginPage.errorMessage).toBeVisible();
    await expect(loginPage.errorMessage).toContainText('密码错误');
    await expect(page).toHaveURL('**/login');
  });

  test('空字段表单验证', async ({ page }) => {
    await page.getByRole('button', { name: '登录' }).click();
    await expect(page.getByText('请输入用户名')).toBeVisible();
    await expect(page.getByText('请输入密码')).toBeVisible();
  });
});
// 案例 02

API 集成测试

使用 Playwright 的 request fixture 进行纯 API 测试,无需打开浏览器。

API 测试 REST 类型安全
tests/api.spec.ts
import { test, expect } from '@playwright/test';

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserResponse {
  user: User;
  token: string;
}

test.describe('Users API', () => {
  let token: string;

  test.beforeAll(async ({ request }) => {
    // 登录获取 token
    const res = await request.post('/api/auth/login', {
      data: { email: '[email protected]', password: 'secret' },
    });
    expect(res.ok()).toBeTruthy();
    token = (await res.json()).token;
  });

  test('GET /api/users 返回用户列表', async ({ request }) => {
    const res = await request.get('/api/users', {
      headers: { Authorization: `Bearer ${token}` },
    });
    expect(res.status()).toBe(200);

    const users: User[] = await res.json();
    expect(users.length).toBeGreaterThan(0);
    expect(users[0]).toHaveProperty('email');
  });

  test('POST /api/users 创建新用户', async ({ request }) => {
    const res = await request.post('/api/users', {
      headers: { Authorization: `Bearer ${token}` },
      data: { name: 'Test User', email: '[email protected]' },
    });
    expect(res.status()).toBe(201);

    const body: CreateUserResponse = await res.json();
    expect(body.user.name).toBe('Test User');
  });

  test('未认证请求返回 401', async ({ request }) => {
    const res = await request.get('/api/users');
    expect(res.status()).toBe(401);
  });
});
// 案例 03

视觉回归测试

利用 Playwright Test 内置的 toHaveScreenshot() 进行像素级 UI 对比。

Visual 截图对比 多主题
tests/visual.spec.ts
import { test, expect } from '@playwright/test';

test.describe('视觉回归', () => {
  test('首页外观一致', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveScreenshot('home-full.png', {
      fullPage: true,
      maxDiffPixelRatio: 0.01,
    });
  });

  test('导航栏组件一致', async ({ page }) => {
    await page.goto('/');
    const nav = page.locator('nav');
    await expect(nav).toHaveScreenshot('navbar.png');
  });

  // 测试暗色模式
  test('暗色模式外观', async ({ page }) => {
    await page.emulateMedia({ colorScheme: 'dark' });
    await page.goto('/');
    await expect(page).toHaveScreenshot('home-dark.png');
  });

  // 隐藏动态内容后截图
  test('隐藏动态元素', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveScreenshot('stable.png', {
      mask: [
        page.locator('.timestamp'),
        page.locator('.avatar'),
      ],
    });
  });
});

// 更新基准图: npx playwright test --update-snapshots
// 案例 04

全局认证 Setup + 并行测试

登录一次、全局复用,所有测试并行运行且共享认证状态。

Global Setup 并行 状态复用
playwright.config.ts — 配置 setup 项目
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    // Setup 项目:执行登录
    {
      name: 'setup',
      testMatch: '**/*.setup.ts',
    },
    // 测试项目:依赖 setup,共享认证状态
    {
      name: 'chromium',
      dependencies: ['setup'],
      use: {
        storageState: '.auth/user.json',
      },
    },
  ],
});
tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = '.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await page.waitForURL('/dashboard');
  await expect(page.getByText('Welcome')).toBeVisible();

  // 保存认证状态
  await page.context().storageState({ path: authFile });
});
tests/dashboard.spec.ts — 使用已认证状态
import { test, expect } from '@playwright/test';

// 不需要登录!storageState 在 config 中已配置
test('仪表盘显示用户数据', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page.getByText('Welcome')).toBeVisible();
  await expect(page.locator('.stats-card')).toHaveCount(4);
});

test('可以修改个人资料', async ({ page }) => {
  await page.goto('/settings/profile');
  await page.getByLabel('Display Name').fill('New Name');
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByText('Saved successfully')).toBeVisible();
});
// 案例 05

网络 Mock + 数据驱动测试

Mock API 响应隔离前端测试,配合参数化测试覆盖多种场景。

Mock 数据驱动 参数化
tests/products.spec.ts
import { test, expect } from '@playwright/test';

// Mock 数据
const mockProducts = [
  { id: 1, name: 'TypeScript 入门', price: 49 },
  { id: 2, name: 'Playwright 实战', price: 69 },
  { id: 3, name: 'Node.js 进阶', price: 59 },
];

test.describe('商品列表', () => {
  test.beforeEach(async ({ page }) => {
    // 拦截 API 返回 mock 数据
    await page.route('**/api/products', async route => {
      await route.fulfill({ json: mockProducts });
    });
    await page.goto('/products');
  });

  test('显示所有商品', async ({ page }) => {
    const cards = page.locator('.product-card');
    await expect(cards).toHaveCount(3);
    await expect(cards.first()).toContainText('TypeScript 入门');
  });

  test('空列表显示提示', async ({ page }) => {
    await page.route('**/api/products', async route => {
      await route.fulfill({ json: [] });
    });
    await page.reload();
    await expect(page.getByText('暂无商品')).toBeVisible();
  });

  test('API 错误显示提示', async ({ page }) => {
    await page.route('**/api/products', async route => {
      await route.fulfill({ status: 500 });
    });
    await page.reload();
    await expect(page.getByText('加载失败')).toBeVisible();
  });
});

// ===== 数据驱动 / 参数化测试 =====
const searchCases = [
  { query: 'TypeScript', expectedCount: 1 },
  { query: '入门', expectedCount: 1 },
  { query: 'xyz', expectedCount: 0 },
];

for (const { query, expectedCount } of searchCases) {
  test(`搜索 "${query}" 返回 ${expectedCount} 个结果`, async ({ page }) => {
    await page.route('**/api/products', route =>
      route.fulfill({ json: mockProducts })
    );
    await page.goto('/products');
    await page.getByPlaceholder('搜索').fill(query);
    await expect(page.locator('.product-card')).toHaveCount(expectedCount);
  });
}
// 实用技巧

TypeScript 版实战小贴士

💡 Codegen 生成 TypeScript

npx playwright codegen https://example.com

默认生成 TypeScript 代码。Inspector 窗口可以复制 Locator 到测试代码中。

🔎 UI Mode 调试

npx playwright test --ui

交互式 UI 运行器,可以选择性运行测试、查看时间线、检查 DOM 快照。TypeScript 版独有。

🚀 CI 最佳实践

# GitHub Actions
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: playwright-report
    path: playwright-report/

失败时自动上传 HTML 报告和 trace 文件到 Artifacts。

📦 测试分片

# 将测试拆分到多个 CI 机器
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4

Playwright Test 内置分片支持,不需要额外工具即可在 CI 中分布式运行。