playwright-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright Testing Best Practices
Playwright 测试最佳实践
Test Organization
测试组织
File Structure
文件结构
tests/
├── auth/
│ ├── login.spec.ts
│ └── signup.spec.ts
├── dashboard/
│ └── dashboard.spec.ts
├── fixtures/
│ └── test-data.ts
├── pages/
│ └── login.page.ts
└── playwright.config.tstests/
├── auth/
│ ├── login.spec.ts
│ └── signup.spec.ts
├── dashboard/
│ └── dashboard.spec.ts
├── fixtures/
│ └── test-data.ts
├── pages/
│ └── login.page.ts
└── playwright.config.tsNaming Conventions
命名规范
- Files:
feature-name.spec.ts - Tests: Describe user behavior, not implementation
- Good:
test('user can reset password via email') - Bad:
test('test reset password')
- 文件:
feature-name.spec.ts - 测试:描述用户行为,而非实现细节
- 示例(好):
test('user can reset password via email') - 示例(差):
test('test reset password')
Page Object Model
页面对象模型
Basic Pattern
基础模式
typescript
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel("Email").fill(email);
await this.page.getByLabel("Password").fill(password);
await this.page.getByRole("button", { name: "Sign in" }).click();
}
}
// tests/login.spec.ts
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password");
await expect(page).toHaveURL("/dashboard");
});typescript
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel("Email").fill(email);
await this.page.getByLabel("Password").fill(password);
await this.page.getByRole("button", { name: "Sign in" }).click();
}
}
// tests/login.spec.ts
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password");
await expect(page).toHaveURL("/dashboard");
});Locator Strategies
定位器策略
Priority Order (Best to Worst)
优先级顺序(从优到劣)
- - Accessible, resilient
getByRole - - Form inputs
getByLabel - - When no label
getByPlaceholder - - Visible text
getByText - - When no better option
getByTestId - CSS/XPath - Last resort
- - 可访问性强、适应性好
getByRole - - 表单输入框专用
getByLabel - - 无标签时使用
getByPlaceholder - - 可见文本定位
getByText - - 无更好选项时使用
getByTestId - CSS/XPath - 最后选择
Examples
示例
typescript
// Preferred
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email address").fill("user@example.com");
// Acceptable
await page.getByTestId("submit-button").click();
// Avoid
await page.locator("#submit-btn").click();
await page.locator('//button[@type="submit"]').click();typescript
// 推荐用法
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email address").fill("user@example.com");
// 可接受用法
await page.getByTestId("submit-button").click();
// 应避免
await page.locator("#submit-btn").click();
await page.locator('//button[@type="submit"]').click();Authentication Handling
身份验证处理
Storage State (Recommended)
存储状态(推荐)
Save logged-in state and reuse across tests:
typescript
// global-setup.ts
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL);
await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD);
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "auth.json" });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
storageState: "auth.json",
},
});保存登录状态并在测试间复用:
typescript
// global-setup.ts
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL);
await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD);
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "auth.json" });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
storageState: "auth.json",
},
});Multi-User Scenarios
多用户场景
typescript
// Create different auth states
const adminAuth = "admin-auth.json";
const userAuth = "user-auth.json";
test.describe("admin features", () => {
test.use({ storageState: adminAuth });
// Admin tests
});
test.describe("user features", () => {
test.use({ storageState: userAuth });
// User tests
});typescript
// 创建不同的身份验证状态
const adminAuth = "admin-auth.json";
const userAuth = "user-auth.json";
test.describe("admin features", () => {
test.use({ storageState: adminAuth });
// 管理员测试
});
test.describe("user features", () => {
test.use({ storageState: userAuth });
// 普通用户测试
});File Upload Handling
文件上传处理
Basic Upload
基础上传
typescript
// Single file
await page.getByLabel("Upload file").setInputFiles("path/to/file.pdf");
// Multiple files
await page
.getByLabel("Upload files")
.setInputFiles(["path/to/file1.pdf", "path/to/file2.pdf"]);
// Clear file input
await page.getByLabel("Upload file").setInputFiles([]);typescript
// 单个文件
await page.getByLabel("Upload file").setInputFiles("path/to/file.pdf");
// 多个文件
await page
.getByLabel("Upload files")
.setInputFiles(["path/to/file1.pdf", "path/to/file2.pdf"]);
// 清空文件输入框
await page.getByLabel("Upload file").setInputFiles([]);Drag and Drop Upload
拖拽上传
typescript
// Create file from buffer
const buffer = Buffer.from("file content");
await page.getByTestId("dropzone").dispatchEvent("drop", {
dataTransfer: {
files: [{ name: "test.txt", mimeType: "text/plain", buffer }],
},
});typescript
// 从缓冲区创建文件
const buffer = Buffer.from("file content");
await page.getByTestId("dropzone").dispatchEvent("drop", {
dataTransfer: {
files: [{ name: "test.txt", mimeType: "text/plain", buffer }],
},
});File Download
文件下载
typescript
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download" }).click();
const download = await downloadPromise;
await download.saveAs("downloads/" + download.suggestedFilename());typescript
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download" }).click();
const download = await downloadPromise;
await download.saveAs("downloads/" + download.suggestedFilename());Waiting Strategies
等待策略
Auto-Wait (Preferred)
自动等待(推荐)
Playwright auto-waits for elements. Use assertions:
typescript
// Auto-waits for element to be visible and stable
await page.getByRole("button", { name: "Submit" }).click();
// Auto-waits for condition
await expect(page.getByText("Success")).toBeVisible();Playwright会自动等待元素就绪。使用断言:
typescript
// 自动等待元素可见且稳定
await page.getByRole("button", { name: "Submit" }).click();
// 自动等待条件满足
await expect(page.getByText("Success")).toBeVisible();Explicit Waits (When Needed)
显式等待(必要时使用)
typescript
// Wait for navigation
await page.waitForURL("**/dashboard");
// Wait for network idle
await page.waitForLoadState("networkidle");
// Wait for specific response
await page.waitForResponse((resp) => resp.url().includes("/api/data"));typescript
// 等待导航完成
await page.waitForURL("**/dashboard");
// 等待网络空闲
await page.waitForLoadState("networkidle");
// 等待特定响应
await page.waitForResponse((resp) => resp.url().includes("/api/data"));Network Mocking
网络模拟
Mock API Responses
模拟API响应
typescript
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Test User" }]),
});
});
// Mock error response
await page.route("**/api/users", async (route) => {
await route.fulfill({ status: 500 });
});typescript
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Test User" }]),
});
});
// 模拟错误响应
await page.route("**/api/users", async (route) => {
await route.fulfill({ status: 500 });
});Intercept and Modify
拦截并修改响应
typescript
await page.route("**/api/data", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});typescript
await page.route("**/api/data", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});CI/CD Integration
CI/CD 集成
GitHub Actions Example
GitHub Actions 示例
yaml
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/yaml
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/Parallel Execution
并行执行
typescript
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
fullyParallel: true,
});typescript
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
fullyParallel: true,
});Debugging Failed Tests
失败测试调试
Debug Tools
调试工具
bash
undefinedbash
undefinedRun with UI mode
以UI模式运行
npx playwright test --ui
npx playwright test --ui
Run with inspector
以检查器模式运行
npx playwright test --debug
npx playwright test --debug
Show browser
显示浏览器窗口运行
npx playwright test --headed
undefinednpx playwright test --headed
undefinedTrace Viewer
追踪查看器
typescript
// playwright.config.ts
use: {
trace: 'on-first-retry', // Capture trace on failure
}typescript
// playwright.config.ts
use: {
trace: 'on-first-retry', // 失败时捕获追踪信息
}Flaky Test Fixes
不稳定测试修复
Common Causes and Solutions
常见原因与解决方案
Race conditions:
- Use proper assertions instead of hard waits
- Wait for network requests to complete
Animation issues:
- Disable animations in test config
- Wait for animation to complete
Dynamic content:
- Use flexible locators (text content, not position)
- Wait for loading states to resolve
Test isolation:
- Each test should set up its own state
- Don't depend on other tests' side effects
竞态条件:
- 使用合适的断言而非硬等待
- 等待网络请求完成
动画问题:
- 在测试配置中禁用动画
- 等待动画完成
动态内容:
- 使用灵活的定位器(基于文本内容而非位置)
- 等待加载状态完成
测试隔离:
- 每个测试应独立设置自身状态
- 不要依赖其他测试的副作用
Anti-Patterns to Avoid
应避免的反模式
typescript
// Bad: Hard sleep
await page.waitForTimeout(5000);
// Good: Wait for condition
await expect(page.getByText("Loaded")).toBeVisible();
// Bad: Flaky selector
await page.locator(".btn:nth-child(3)").click();
// Good: Semantic selector
await page.getByRole("button", { name: "Submit" }).click();typescript
// 不良实践:硬等待
await page.waitForTimeout(5000);
// 良好实践:等待条件满足
await expect(page.getByText("Loaded")).toBeVisible();
// 不良实践:不稳定的选择器
await page.locator(".btn:nth-child(3)").click();
// 良好实践:语义化选择器
await page.getByRole("button", { name: "Submit" }).click();Responsive Design Testing
响应式设计测试
For comprehensive responsive testing across viewport breakpoints, use the responsive-tester agent. It automatically:
- Tests pages across 7 standard breakpoints (375px to 1536px)
- Detects horizontal overflow issues
- Verifies mobile-first design patterns
- Checks touch target sizes (44x44px minimum)
- Flags anti-patterns like fixed widths without mobile fallback
Trigger it by asking to "test responsiveness", "check breakpoints", or "test mobile/desktop layout".
如需在不同视口断点进行全面的响应式测试,请使用responsive-tester agent。它会自动:
- 在7个标准断点(375px至1536px)测试页面
- 检测水平溢出问题
- 验证移动优先的设计模式
- 检查触摸目标尺寸(最小44x44px)
- 标记无移动端回退的固定宽度等反模式
可通过请求「测试响应式」、「检查断点」或「测试移动端/桌面端布局」来触发该agent。