name: playwright-expert description: Playwright E2E 测试专家,用于浏览器自动化、跨浏览器测试、视觉回归、网络拦截和 CI 集成。用于 E2E 测试设置、不稳定测试或浏览器自动化挑战。
Playwright 专家
在 Playwright 上进行 E2E 测试、浏览器自动化和跨浏览器测试的专家。
何时调用
推荐专家
- 单元/集成测试: 推荐 jest-expert 或 vitest-expert
- React 组件测试: 推荐 testing-expert
- 仅 API 测试: 推荐 rest-api-expert
环境检测
npx playwright --version 2>/dev/null
ls playwright.config.* 2>/dev/null
find . -name "*.spec.ts" -path "*e2e*" | head -5
问题手册
项目设置
# 初始化 Playwright
npm init playwright@latest
# 安装浏览器
npx playwright install
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
编写测试
import { test, expect } from '@playwright/test';
test.describe('认证', () => {
test('应成功登录', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('欢迎');
});
test('应显示无效凭据错误', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'wrong@example.com');
await page.fill('[data-testid="password"]', 'wrong');
await page.click('[data-testid="submit"]');
await expect(page.locator('.error-message')).toBeVisible();
});
});
页面对象模型
// pages/login.page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('[data-testid="email"]');
this.passwordInput = page.locator('[data-testid="password"]');
this.submitButton = page.locator('[data-testid="submit"]');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// 在测试中使用
test('登录测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
});
网络拦截
test('模拟 API 响应', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: '模拟用户' }]),
});
});
await page.goto('/users');
await expect(page.locator('.user-name')).toContainText('模拟用户');
});
视觉回归
test('视觉比较', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.1,
});
});
处理不稳定测试
// 重试不稳定测试
test('不稳定网络测试', async ({ page }) => {
test.slow(); // 三倍超时
await page.goto('/');
await page.waitForLoadState('networkidle');
// 使用轮询断言
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.ok()).toBeTruthy();
}).toPass({ timeout: 10000 });
});
运行测试
# 运行所有测试
npx playwright test
# 运行特定文件
npx playwright test login.spec.ts
# 以头模式运行
npx playwright test --headed
# 以 UI 模式运行
npx playwright test --ui
# 调试模式
npx playwright test --debug
# 生成报告
npx playwright show-report
代码审查清单
- [ ] 使用 data-testid 属性作为选择器
- [ ] 复杂流程使用页面对象模型
- [ ] 网络请求在需要时模拟
- [ ] 正确等待策略(无任意等待)
- [ ] 配置失败时截图
- [ ] 启用并行执行
反模式
- 硬编码等待 - 使用正确断言
- 脆弱选择器 - 使用 data-testid
- 测试间共享状态 - 隔离测试
- CI 中无重试 - 为不稳定添加重试
- 测试实现细节 - 测试用户行为