playwright-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Playwright 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.ts
tests/
├── auth/
│   ├── login.spec.ts
│   └── signup.spec.ts
├── dashboard/
│   └── dashboard.spec.ts
├── fixtures/
│   └── test-data.ts
├── pages/
│   └── login.page.ts
└── playwright.config.ts

Naming 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)

优先级顺序(从优到劣)

  1. getByRole
    - Accessible, resilient
  2. getByLabel
    - Form inputs
  3. getByPlaceholder
    - When no label
  4. getByText
    - Visible text
  5. getByTestId
    - When no better option
  6. CSS/XPath - Last resort
  1. getByRole
    - 可访问性强、适应性好
  2. getByLabel
    - 表单输入框专用
  3. getByPlaceholder
    - 无标签时使用
  4. getByText
    - 可见文本定位
  5. getByTestId
    - 无更好选项时使用
  6. 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
undefined
bash
undefined

Run 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
undefined
npx playwright test --headed
undefined

Trace 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。