📘

E2E Testing Patterns

由微软开源的 Playwright 与 Cypress E2E 测试最佳实践指南,提供消除不稳定测试、CI/CD 集成及关键用户路径覆盖的企业级模式。

v1 ra-qm-team curated
🤖

Install 安装

curl -sSL https://updating.cc/skills/cocoloop/qa-e2e-testing-patterns/SKILL.md -o ~/.agents/skills/cocoloop__qa-e2e-testing-patterns/SKILL.md

SKILL.md Preview 技能内容预览

---
name: e2e-testing-patterns
model: standard
category: testing
description: Build reliable, fast E2E test suites with Playwright and Cypress. Critical user journey coverage, flaky test elimination, CI/CD integration.
version: 1.0
keywords: [e2e, end-to-end, playwright, cypress, browser testing, integration tests, test automation, flaky tests, visual regression]
---

# E2E Testing Patterns

> Test what users do, not how code works. E2E tests prove the system works as a whole — they're your confidence to ship.


## Installation

### OpenClaw / Moltbot / Clawbot

```bash
npx clawhub@latest install e2e-testing-patterns
```


---

## WHAT This Skill Does

Provides patterns for building end-to-end test suites that:
- Catch regressions before users do
- Run fast enough for CI/CD
- Remain stable (no flaky failures)
- Cover critical user journeys without over-testing

## WHEN To Use

- **Implementing E2E test automation** for a web application
- **Debugging flaky tests** that fail intermittently
- **Setting up CI/CD test pipelines** with browser tests
- **Testing critical user workflows** (auth, checkout, signup)
- **Choosing what to test with E2E** vs unit/integration tests

---

## Test Pyramid — Know Your Layer

```
        /\
       /E2E\         ← FEW: Critical paths only (this skill)
      /─────\
     /Integr\        ← MORE: Component interactions, API contracts
    /────────\
   /Unit Tests\      ← MANY: Fast, isolated, cover edge cases
  /────────────\
```

### What E2E Tests Are For

| E2E Tests ✓ | NOT E2E Tests ✗ |
|-------------|-----------------|
| Critical user journeys (login → dashboard → action → logout) | Unit-level logic (use unit tests) |
| Multi-step flows (checkout, onboarding wizard) | API contracts (use integration tests) |
| Cross-browser compatibility | Edge cases (too slow, use unit tests) |
| Real API integration | Internal implementation details |
| Authentication flows | Component visual states (use Storybook) |

**Rule of thumb:** If it would devastate your business to break, E2E test it. If it's just inconvenient, test it faster with unit/integration tests.

---

## Core Principles

| Principle | Why | How |
|-----------|-----|-----|
| **Test behavior, not implementation** | Survives refactors | Assert on user-visible outcomes, not DOM structure |
| **Independent tests** | Parallelizable, debuggable | Each test creates its own data, cleans up after |
| **Deterministic waits** | No flakiness | Wait for conditions, not fixed timeouts |
| **Stable selectors** | Survives UI changes | Use `data-testid`, roles, labels — never CSS classes |
| **Fast feedback** | Developers run them | Mock external services, parallelize, shard |

---

## Playwright Patterns

### Configuration

```typescript
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  timeout: 30000,
  expect: { timeout: 5000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [["html"], ["junit", { outputFile: "results.xml" }]],
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
    { name: "mobile", use: { ...devices["iPhone 13"] } },
  ],
});
```

### Pattern: Page Object Model

Encapsulate page logic. Tests read like user stories.

```typescript
// pages/LoginPage.ts
import { Page, Locator } from "@playwright/test";

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.loginButton = page.getByRole("button", { name: "Login" });
    this.errorMessage = page.getByRole("alert");
  }

  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.loginButton.click();
  }
}

// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

test("successful login redirects to dashboard", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login("user@example.com", "password123");

  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});
```

### Pattern: Fixtures for Test Data

Create and clean up test data automatically.

```typescript
// fixtures/test-data.ts
import { test as base } from "@playwright/test";

export const test = base.extend<{ testUser: TestUser }>({
  testUser: async ({}, use) => {
    // Setup: Create user
    const user = await createTestUser({
      email: `test-${Date.now()}@example.com`,
      password: "Test123!@#",
    });

    await use(user);

    // Teardown: Clean up
    await deleteTestUser(user.id);
  },
});

// Usage — testUser is created before, deleted after
test("user can update profile", async ({ page, testUser }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill(testUser.email);
  // ...
});
```

### Pattern: Smart Waiting

Never use fixed timeouts. Wait for specific conditions.

```typescript
// ❌ FLAKY: Fixed timeout
await page.waitForTimeout(3000);

// ✅ STABLE: Wait for conditions
await page.waitForLoadState("networkidle");
await page.waitForURL("/dashboard");

// ✅ BEST: Auto-waiting assertions
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();

// Wait for API response
const responsePromise = page.waitForResponse(
  (r) => r.url().includes("/api/users") && r.status() === 200
);
await page.getByRole("button", { name: "Load" }).click();
await responsePromise;
```

### Pattern: Network Mocking

Isolate tests from real external services.

```typescript
test("shows error when API fails", async ({ page }) => {
  // Mock the API response
  await page.route("**/api/users", (route) => {
    route.fulfill({
      status: 500,
      body: JSON.stringify({ error: "Server Error" }),
    });
  });

  await page.goto("/users");
  await expect(page.getByText("Failed to load users")).toBeVisible();
});

test("handles slow network gracefully", async ({ page }) => {
  await page.route("**/api/data", async (route) => {
    await new Promise((r) => setTimeout(r, 3000)); // Simulate delay
    await route.continue();
  });

  await page.goto("/dashboard");
  await expect(page.getByText("Loading...")).toBeVisible();
});
```

---

## Cypress Patterns

### Custom Commands

```typescript
// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      dataCy(value: string): Chainable<JQuery<HTMLElement>>;
    }
  }
}

Cypress.Commands.add("login", (email, password) => {
  cy.visit("/login");
  cy.get('[data-testid="email"]').type(email);
  cy.get('[data-testid="password"]').type(password);
  cy.get('[data-testid="login-button"]').click();
  cy.url().should("include", "/dashboard");
});

Cypress.Commands.add("dataCy", (value) => {
  return cy.get(`[data-cy="${value}"]`);
});

// Usage
cy.login("user@example.com", "password");
cy.dataCy("submit-button").click();
```

### Network Intercepts

```typescript
// Mock API
cy.intercept("GET", "/api/users", {
  statusCode: 200,
  body: [{ id: 1, name: "John" }],
}).as("getUsers");

cy.visit("/users");
cy.wait("@getUsers");
cy.get('[data-testid="u