e2e-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseE2E Testing
端到端(E2E)测试
When to Use
适用场景
Activate this skill when:
- Writing E2E tests for complete user workflows (login, CRUD operations, multi-page flows)
- Creating critical path regression tests that validate the full stack
- Testing cross-browser compatibility (Chromium, Firefox, WebKit)
- Validating authentication flows end-to-end
- Testing file upload/download workflows
- Writing smoke tests for deployment verification
Do NOT use this skill for:
- React component unit tests (use )
react-testing-patterns - Python backend unit/integration tests (use )
pytest-patterns - TDD workflow enforcement (use )
tdd-workflow - API contract testing without a browser (use with httpx)
pytest-patterns
在以下场景中使用本技能:
- 为完整用户流程(登录、CRUD操作、多页面流程)编写E2E测试
- 创建验证全栈的关键路径回归测试
- 测试跨浏览器兼容性(Chromium、Firefox、WebKit)
- 端到端验证认证流程
- 测试文件上传/下载工作流
- 编写用于部署验证的冒烟测试
请勿在以下场景中使用本技能:
- React组件单元测试(请使用)
react-testing-patterns - Python后端单元/集成测试(请使用)
pytest-patterns - TDD工作流实施(请使用)
tdd-workflow - 无浏览器的API契约测试(请结合httpx使用)
pytest-patterns
Instructions
操作指南
Test Structure
测试结构
e2e/
├── playwright.config.ts # Global Playwright configuration
├── fixtures/
│ ├── auth.fixture.ts # Authentication state setup
│ └── test-data.fixture.ts # Test data creation/cleanup
├── pages/
│ ├── base.page.ts # Base page object with shared methods
│ ├── login.page.ts # Login page object
│ ├── users.page.ts # Users list page object
│ └── user-detail.page.ts # User detail page object
├── tests/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── users/
│ │ ├── create-user.spec.ts
│ │ ├── edit-user.spec.ts
│ │ └── list-users.spec.ts
│ └── smoke/
│ └── critical-paths.spec.ts
└── utils/
├── api-helpers.ts # Direct API calls for test setup
└── test-constants.ts # Shared constantsNaming conventions:
- Test files:
<feature>.spec.ts - Page objects:
<page-name>.page.ts - Fixtures:
<concern>.fixture.ts - Test names: human-readable sentences describing the user action and expected outcome
e2e/
├── playwright.config.ts # Global Playwright configuration
├── fixtures/
│ ├── auth.fixture.ts # Authentication state setup
│ └── test-data.fixture.ts # Test data creation/cleanup
├── pages/
│ ├── base.page.ts # Base page object with shared methods
│ ├── login.page.ts # Login page object
│ ├── users.page.ts # Users list page object
│ └── user-detail.page.ts # User detail page object
├── tests/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── users/
│ │ ├── create-user.spec.ts
│ │ ├── edit-user.spec.ts
│ │ └── list-users.spec.ts
│ └── smoke/
│ └── critical-paths.spec.ts
└── utils/
├── api-helpers.ts # Direct API calls for test setup
└── test-constants.ts # Shared constants命名规范:
- 测试文件:
<feature>.spec.ts - 页面对象:
<page-name>.page.ts - 夹具:
<concern>.fixture.ts - 测试名称:描述用户操作和预期结果的易读性语句
Page Object Model
页面对象模型
Every page gets a page object class that encapsulates selectors and actions. Tests never interact with selectors directly.
Base page object:
typescript
// e2e/pages/base.page.ts
import { type Page, type Locator } from "@playwright/test";
export abstract class BasePage {
constructor(protected readonly page: Page) {}
/** Navigate to the page's URL. */
abstract goto(): Promise<void>;
/** Wait for the page to be fully loaded. */
async waitForLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
/** Get a toast/notification message. */
get toast(): Locator {
return this.page.getByRole("alert");
}
/** Get the page heading. */
get heading(): Locator {
return this.page.getByRole("heading", { level: 1 });
}
}Concrete page object:
typescript
// e2e/pages/users.page.ts
import { type Page, type Locator } from "@playwright/test";
import { BasePage } from "./base.page";
export class UsersPage extends BasePage {
// ─── Locators ─────────────────────────────────────────
readonly createButton: Locator;
readonly searchInput: Locator;
readonly userTable: Locator;
constructor(page: Page) {
super(page);
this.createButton = page.getByTestId("create-user-btn");
this.searchInput = page.getByRole("searchbox", { name: /search users/i });
this.userTable = page.getByRole("table");
}
// ─── Actions ──────────────────────────────────────────
async goto(): Promise<void> {
await this.page.goto("/users");
await this.waitForLoad();
}
async searchFor(query: string): Promise<void> {
await this.searchInput.fill(query);
// Wait for search results to update (debounced)
await this.page.waitForResponse("**/api/v1/users?*");
}
async clickCreateUser(): Promise<void> {
await this.createButton.click();
}
async getUserRow(email: string): Promise<Locator> {
return this.userTable.getByRole("row").filter({ hasText: email });
}
async getUserCount(): Promise<number> {
// Subtract 1 for header row
return (await this.userTable.getByRole("row").count()) - 1;
}
}Rules for page objects:
- One page object per page or major UI section
- Locators are public readonly properties
- Actions are async methods
- Page objects never contain assertions -- tests assert
- Page objects handle waits internally after actions
每个页面对应一个页面对象类,封装选择器和操作。测试从不直接与选择器交互。
基础页面对象:
typescript
// e2e/pages/base.page.ts
import { type Page, type Locator } from "@playwright/test";
export abstract class BasePage {
constructor(protected readonly page: Page) {}
/** Navigate to the page's URL. */
abstract goto(): Promise<void>;
/** Wait for the page to be fully loaded. */
async waitForLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
/** Get a toast/notification message. */
get toast(): Locator {
return this.page.getByRole("alert");
}
/** Get the page heading. */
get heading(): Locator {
return this.page.getByRole("heading", { level: 1 });
}
}具体页面对象:
typescript
// e2e/pages/users.page.ts
import { type Page, type Locator } from "@playwright/test";
import { BasePage } from "./base.page";
export class UsersPage extends BasePage {
// ─── Locators ─────────────────────────────────────────
readonly createButton: Locator;
readonly searchInput: Locator;
readonly userTable: Locator;
constructor(page: Page) {
super(page);
this.createButton = page.getByTestId("create-user-btn");
this.searchInput = page.getByRole("searchbox", { name: /search users/i });
this.userTable = page.getByRole("table");
}
// ─── Actions ──────────────────────────────────────────
async goto(): Promise<void> {
await this.page.goto("/users");
await this.waitForLoad();
}
async searchFor(query: string): Promise<void> {
await this.searchInput.fill(query);
// Wait for search results to update (debounced)
await this.page.waitForResponse("**/api/v1/users?*");
}
async clickCreateUser(): Promise<void> {
await this.createButton.click();
}
async getUserRow(email: string): Promise<Locator> {
return this.userTable.getByRole("row").filter({ hasText: email });
}
async getUserCount(): Promise<number> {
// Subtract 1 for header row
return (await this.userTable.getByRole("row").count()) - 1;
}
}页面对象规则:
- 每个页面或主要UI区域对应一个页面对象
- 选择器为公共只读属性
- 操作为异步方法
- 页面对象从不包含断言——断言逻辑由测试代码实现
- 页面对象在操作后内部处理等待逻辑
Selector Strategy
选择器策略
Priority order (highest to lowest):
| Priority | Selector | Example | When to Use |
|---|---|---|---|
| 1 | | | Interactive elements, dynamic content |
| 2 | Role | | Buttons, links, headings, inputs |
| 3 | Label | | Form inputs with labels |
| 4 | Placeholder | | Search inputs |
| 5 | Text | | Static text content |
NEVER use:
- CSS selectors (,
.class-name) -- brittle, break on styling changes#id - XPath () -- unreadable, extremely brittle
//div[@class="foo"] - DOM structure selectors () -- break on layout changes
div > span:nth-child(2)
Adding data-testid attributes:
tsx
// In React components -- add data-testid to interactive elements
<button data-testid="create-user-btn" onClick={handleCreate}>
Create User
</button>
// Convention: kebab-case, descriptive
// Pattern: <action>-<entity>-<element-type>
// Examples: create-user-btn, user-email-input, delete-confirm-dialog优先级顺序(从高到低):
| 优先级 | 选择器 | 示例 | 适用场景 |
|---|---|---|---|
| 1 | | | 交互元素、动态内容 |
| 2 | 角色 | | 按钮、链接、标题、输入框 |
| 3 | 标签 | | 带标签的表单输入框 |
| 4 | 占位符 | | 搜索输入框 |
| 5 | 文本 | | 静态文本内容 |
禁止使用:
- CSS选择器(,
.class-name)——易失效,样式变更时会中断#id - XPath()——可读性差,极易失效
//div[@class="foo"] - DOM结构选择器()——布局变更时会中断
div > span:nth-child(2)
添加data-testid属性:
tsx
// In React components -- add data-testid to interactive elements
<button data-testid="create-user-btn" onClick={handleCreate}>
Create User
</button>
// Convention: kebab-case, descriptive
// Pattern: <action>-<entity>-<element-type>
// Examples: create-user-btn, user-email-input, delete-confirm-dialogWait Strategies
等待策略
NEVER use hardcoded waits:
typescript
// BAD: Hardcoded wait -- flaky, slow
await page.waitForTimeout(3000);
// BAD: Sleep
await new Promise((resolve) => setTimeout(resolve, 2000));Use explicit wait conditions:
typescript
// GOOD: Wait for a specific element to appear
await page.getByRole("heading", { name: "Dashboard" }).waitFor();
// GOOD: Wait for navigation
await page.waitForURL("/dashboard");
// GOOD: Wait for API response
await page.waitForResponse(
(response) =>
response.url().includes("/api/v1/users") && response.status() === 200,
);
// GOOD: Wait for network to settle
await page.waitForLoadState("networkidle");
// GOOD: Wait for element state
await page.getByTestId("submit-btn").waitFor({ state: "visible" });
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });Auto-waiting: Playwright auto-waits for elements to be actionable before clicking, filling, etc. Explicit waits are needed only for assertions or complex state transitions.
禁止使用硬编码等待:
typescript
// BAD: Hardcoded wait -- flaky, slow
await page.waitForTimeout(3000);
// BAD: Sleep
await new Promise((resolve) => setTimeout(resolve, 2000));使用显式等待条件:
typescript
// GOOD: Wait for a specific element to appear
await page.getByRole("heading", { name: "Dashboard" }).waitFor();
// GOOD: Wait for navigation
await page.waitForURL("/dashboard");
// GOOD: Wait for API response
await page.waitForResponse(
(response) =>
response.url().includes("/api/v1/users") && response.status() === 200,
);
// GOOD: Wait for network to settle
await page.waitForLoadState("networkidle");
// GOOD: Wait for element state
await page.getByTestId("submit-btn").waitFor({ state: "visible" });
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });自动等待: Playwright会自动等待元素可操作后再执行点击、填充等操作。仅在断言或复杂状态转换时需要显式等待。
Auth State Reuse
认证状态复用
Avoid logging in before every test. Save auth state and reuse it.
Setup auth state once:
typescript
// e2e/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import path from "path";
const AUTH_STATE_PATH = path.resolve("e2e/.auth/user.json");
export const setup = base.extend({});
setup("authenticate", async ({ page }) => {
// Perform real login
await page.goto("/login");
await page.getByLabel("Email").fill("testuser@example.com");
await page.getByLabel("Password").fill("TestPassword123!");
await page.getByRole("button", { name: /sign in/i }).click();
// Wait for auth to complete
await page.waitForURL("/dashboard");
// Save signed-in state
await page.context().storageState({ path: AUTH_STATE_PATH });
});Reuse in tests:
typescript
// playwright.config.ts
export default defineConfig({
projects: [
// Setup project runs first and saves auth state
{ name: "setup", testDir: "./e2e/fixtures", testMatch: "auth.fixture.ts" },
{
name: "chromium",
use: {
storageState: "e2e/.auth/user.json", // Reuse auth state
},
dependencies: ["setup"],
},
],
});避免在每个测试前重复登录。保存认证状态并复用。
一次性设置认证状态:
typescript
// e2e/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import path from "path";
const AUTH_STATE_PATH = path.resolve("e2e/.auth/user.json");
export const setup = base.extend({});
setup("authenticate", async ({ page }) => {
// Perform real login
await page.goto("/login");
await page.getByLabel("Email").fill("testuser@example.com");
await page.getByLabel("Password").fill("TestPassword123!");
await page.getByRole("button", { name: /sign in/i }).click();
// Wait for auth to complete
await page.waitForURL("/dashboard");
// Save signed-in state
await page.context().storageState({ path: AUTH_STATE_PATH });
});在测试中复用:
typescript
// playwright.config.ts
export default defineConfig({
projects: [
// Setup project runs first and saves auth state
{ name: "setup", testDir: "./e2e/fixtures", testMatch: "auth.fixture.ts" },
{
name: "chromium",
use: {
storageState: "e2e/.auth/user.json", // Reuse auth state
},
dependencies: ["setup"],
},
],
});Test Data Management
测试数据管理
Principles:
- Tests create their own data (never depend on pre-existing data)
- Tests clean up after themselves (or use API to reset)
- Use API calls for setup, not UI interactions (faster, more reliable)
API helpers for test data:
typescript
// e2e/utils/api-helpers.ts
import { type APIRequestContext } from "@playwright/test";
export class TestDataAPI {
constructor(private request: APIRequestContext) {}
async createUser(data: { email: string; displayName: string }) {
const response = await this.request.post("/api/v1/users", { data });
return response.json();
}
async deleteUser(userId: number) {
await this.request.delete(`/api/v1/users/${userId}`);
}
async createOrder(userId: number, items: Array<Record<string, unknown>>) {
const response = await this.request.post("/api/v1/orders", {
data: { user_id: userId, items },
});
return response.json();
}
}Usage in tests:
typescript
test("edit user name", async ({ page, request }) => {
const api = new TestDataAPI(request);
// Setup: create user via API (fast)
const user = await api.createUser({
email: "edit-test@example.com",
displayName: "Before Edit",
});
try {
// Test: edit via UI
const usersPage = new UsersPage(page);
await usersPage.goto();
// ... perform edit via UI ...
} finally {
// Cleanup: remove test data
await api.deleteUser(user.id);
}
});原则:
- 测试自行创建数据(绝不依赖预存在的数据)
- 测试完成后清理数据(或通过API重置)
- 使用API调用进行初始化,而非UI交互(更快、更可靠)
用于测试数据的API助手:
typescript
// e2e/utils/api-helpers.ts
import { type APIRequestContext } from "@playwright/test";
export class TestDataAPI {
constructor(private request: APIRequestContext) {}
async createUser(data: { email: string; displayName: string }) {
const response = await this.request.post("/api/v1/users", { data });
return response.json();
}
async deleteUser(userId: number) {
await this.request.delete(`/api/v1/users/${userId}`);
}
async createOrder(userId: number, items: Array<Record<string, unknown>>) {
const response = await this.request.post("/api/v1/orders", {
data: { user_id: userId, items },
});
return response.json();
}
}在测试中使用:
typescript
test("edit user name", async ({ page, request }) => {
const api = new TestDataAPI(request);
// Setup: create user via API (fast)
const user = await api.createUser({
email: "edit-test@example.com",
displayName: "Before Edit",
});
try {
// Test: edit via UI
const usersPage = new UsersPage(page);
await usersPage.goto();
// ... perform edit via UI ...
} finally {
// Cleanup: remove test data
await api.deleteUser(user.id);
}
});Debugging Flaky Tests
调试不稳定测试
1. Use trace viewer for failures:
typescript
// playwright.config.ts
use: {
trace: "on-first-retry", // Capture trace only on retry
}View trace:
npx playwright show-trace trace.zip2. Run in headed mode for debugging:
bash
npx playwright test --headed --debug tests/users/create-user.spec.ts3. Common causes of flaky tests:
| Cause | Fix |
|---|---|
| Hardcoded waits | Use explicit wait conditions |
| Shared test data | Each test creates its own data |
| Animation interference | Set |
| Race conditions | Wait for API responses before assertions |
| Viewport-dependent behavior | Set explicit viewport in config |
| Session leaks between tests | Use |
4. Retry strategy:
typescript
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Retry in CI only
});1. 使用追踪查看器分析失败:
typescript
// playwright.config.ts
use: {
trace: "on-first-retry", // Capture trace only on retry
}查看追踪:
npx playwright show-trace trace.zip2. 使用有头模式调试:
bash
npx playwright test --headed --debug tests/users/create-user.spec.ts3. 不稳定测试的常见原因:
| 原因 | 修复方案 |
|---|---|
| 硬编码等待 | 使用显式等待条件 |
| 共享测试数据 | 每个测试自行创建独立数据 |
| 动画干扰 | 在配置中设置 |
| 竞态条件 | 断言前等待API响应 |
| 视口依赖行为 | 在配置中设置固定视口 |
| 测试间会话泄漏 | 正确使用 |
4. 重试策略:
typescript
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Retry in CI only
});CI Configuration
CI配置
yaml
undefinedyaml
undefined.github/workflows/e2e.yml
.github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Start application
run: |
docker compose up -d
npx wait-on http://localhost:3000 --timeout 60000
- name: Run E2E tests
run: npx playwright test
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 14
- name: Upload traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-traces
path: test-results/
Use `scripts/run-e2e-with-report.sh` to run Playwright with HTML report output locally.name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Start application
run: |
docker compose up -d
npx wait-on http://localhost:3000 --timeout 60000
- name: Run E2E tests
run: npx playwright test
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 14
- name: Upload traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-traces
path: test-results/
使用`scripts/run-e2e-with-report.sh`在本地运行Playwright并生成HTML测试报告。Examples
示例
See for annotated page object class.
See for annotated E2E test.
See for production Playwright config.
references/page-object-template.tsreferences/e2e-test-template.tsreferences/playwright-config-example.ts请查看获取带注释的页面对象类示例。
请查看获取带注释的E2E测试示例。
请查看获取生产环境Playwright配置示例。
references/page-object-template.tsreferences/e2e-test-template.tsreferences/playwright-config-example.ts