playwright-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright Expert
Playwright 专家
Expert in Playwright for E2E testing, browser automation, and cross-browser testing.
专注于Playwright端到端(E2E)测试、浏览器自动化及跨浏览器测试的专家。
When Invoked
调用场景
Recommend Specialist
推荐其他专家
- Unit/integration tests: recommend jest-expert or vitest-expert
- React component testing: recommend testing-expert
- API testing only: recommend rest-api-expert
- 单元/集成测试:推荐 jest-expert 或 vitest-expert
- React组件测试:推荐 testing-expert
- 仅API测试:推荐 rest-api-expert
Environment Detection
环境检测
bash
npx playwright --version 2>/dev/null
ls playwright.config.* 2>/dev/null
find . -name "*.spec.ts" -path "*e2e*" | head -5bash
npx playwright --version 2>/dev/null
ls playwright.config.* 2>/dev/null
find . -name "*.spec.ts" -path "*e2e*" | head -5Problem Playbooks
问题解决方案手册
Project Setup
项目搭建
bash
undefinedbash
undefinedInitialize Playwright
Initialize Playwright
npm init playwright@latest
npm init playwright@latest
Install browsers
Install browsers
npx playwright install
```typescript
// 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,
},
});npx playwright install
```typescript
// 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,
},
});Writing Tests
编写测试用例
typescript
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('should login successfully', 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('Welcome');
});
test('should show error for invalid credentials', 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();
});
});typescript
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('should login successfully', 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('Welcome');
});
test('should show error for invalid credentials', 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();
});
});Page Object Model
页面对象模型
typescript
// 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();
}
}
// Usage in test
test('login test', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
});typescript
// 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();
}
}
// Usage in test
test('login test', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
});Network Interception
网络拦截
typescript
test('mock API response', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
await page.goto('/users');
await expect(page.locator('.user-name')).toContainText('Mock User');
});typescript
test('mock API response', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
await page.goto('/users');
await expect(page.locator('.user-name')).toContainText('Mock User');
});Visual Regression
视觉回归测试
typescript
test('visual comparison', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.1,
});
});typescript
test('visual comparison', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.1,
});
});Handling Flaky Tests
处理不稳定测试用例
typescript
// Retry flaky tests
test('flaky network test', async ({ page }) => {
test.slow(); // Triple timeout
await page.goto('/');
await page.waitForLoadState('networkidle');
// Use polling assertions
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.ok()).toBeTruthy();
}).toPass({ timeout: 10000 });
});typescript
// Retry flaky tests
test('flaky network test', async ({ page }) => {
test.slow(); // Triple timeout
await page.goto('/');
await page.waitForLoadState('networkidle');
// Use polling assertions
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.ok()).toBeTruthy();
}).toPass({ timeout: 10000 });
});Running Tests
运行测试
bash
undefinedbash
undefinedRun all tests
Run all tests
npx playwright test
npx playwright test
Run specific file
Run specific file
npx playwright test login.spec.ts
npx playwright test login.spec.ts
Run in headed mode
Run in headed mode
npx playwright test --headed
npx playwright test --headed
Run in UI mode
Run in UI mode
npx playwright test --ui
npx playwright test --ui
Debug mode
Debug mode
npx playwright test --debug
npx playwright test --debug
Generate report
Generate report
npx playwright show-report
undefinednpx playwright show-report
undefinedCode Review Checklist
代码审查清单
- data-testid attributes for selectors
- Page Object Model for complex flows
- Network requests mocked where needed
- Proper wait strategies (no arbitrary waits)
- Screenshots on failure configured
- Parallel execution enabled
- 选择器使用data-testid属性
- 复杂流程使用页面对象模型
- 按需模拟网络请求
- 使用合理的等待策略(避免硬编码等待)
- 配置失败时自动截图
- 启用并行执行
Anti-Patterns
反模式
- Hardcoded waits - Use proper assertions
- Fragile selectors - Use data-testid
- Shared state between tests - Isolate tests
- No retries in CI - Add retry for flakiness
- Testing implementation details - Test user behavior
- 硬编码等待 - 使用合理的断言替代
- 不稳定的选择器 - 使用data-testid
- 测试用例间共享状态 - 隔离各个测试用例
- CI环境未配置重试 - 为不稳定的测试添加重试机制
- 测试实现细节 - 测试用户实际行为而非内部实现