真实场景案例
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 中分布式运行。