跳至主要内容
小龙虾小龙虾AI
🤖

E2E Testing Patterns

Build reliable, fast E2E test suites with Playwright and Cypress. Critical user journey coverage, flaky test elimination, CI/CD integration.

下载2.4k
星标2
版本1.0.0
开发工具
安全通过
⚙️脚本

技能说明


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

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 compatibilityEdge cases (too slow, use unit tests)
Real API integrationInternal implementation details
Authentication flowsComponent 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

PrincipleWhyHow
Test behavior, not implementationSurvives refactorsAssert on user-visible outcomes, not DOM structure
Independent testsParallelizable, debuggableEach test creates its own data, cleans up after
Deterministic waitsNo flakinessWait for conditions, not fixed timeouts
Stable selectorsSurvives UI changesUse data-testid, roles, labels — never CSS classes
Fast feedbackDevelopers run themMock external services, parallelize, shard

Playwright Patterns

Configuration

// 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.

// 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.

// 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.

// ❌ 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.

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

// 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

// 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="user-list"]').children().should("have.length", 1);

Selector Strategy

PrioritySelector TypeExampleWhy
1Role + namegetByRole("button", { name: "Submit" })Accessible, user-facing
2LabelgetByLabel("Email address")Accessible, semantic
3data-testidgetByTestId("checkout-form")Stable, explicit for testing
4Text contentgetByText("Welcome back")User-facing
CSS classes.btn-primaryBreaks on styling changes
DOM structurediv > form > input:nth-child(2)Breaks on any restructure
// ❌ BAD: Brittle selectors
cy.get(".btn.btn-primary.submit-button").click();
cy.get("div > form > div:nth-child(2) > input").type("text");

// ✅ GOOD: Stable selectors
page.getByRole("button", { name: "Submit" }).click();
page.getByLabel("Email address").fill("user@example.com");
page.getByTestId("email-input").fill("user@example.com");

Visual Regression Testing

// Playwright visual comparisons
test("homepage looks correct", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("homepage.png", {
    fullPage: true,
    maxDiffPixels: 100,
  });
});

test("button states", async ({ page }) => {
  const button = page.getByRole("button", { name: "Submit" });

  await expect(button).toHaveScreenshot("button-default.png");

  await button.hover();
  await expect(button).toHaveScreenshot("button-hover.png");
});

Accessibility Testing

// npm install @axe-core/playwright
import AxeBuilder from "@axe-core/playwright";

test("page has no accessibility violations", async ({ page }) => {
  await page.goto("/");

  const results = await new AxeBuilder({ page })
    .exclude("#third-party-widget")  // Exclude things you can't control
    .analyze();

  expect(results.violations).toEqual([]);
});

Debugging Failed Tests

# Run in headed mode (see the browser)
npx playwright test --headed

# Debug mode (step through)
npx playwright test --debug

# Show trace viewer for failed tests
npx playwright show-report
// Add test steps for better failure reports
test("checkout flow", async ({ page }) => {
  await test.step("Add item to cart", async () => {
    await page.goto("/products");
    await page.getByRole("button", { name: "Add to Cart" }).click();
  });

  await test.step("Complete checkout", async () => {
    await page.goto("/checkout");
    // ... if this fails, you know which step
  });
});

// Pause for manual inspection
await page.pause();

Flaky Test Checklist

When a test fails intermittently, check:

IssueFix
Fixed waitForTimeout() callsReplace with waitForSelector() or expect assertions
Race conditions on page loadWait for networkidle or specific elements
Test data pollutionEnsure tests create/clean their own data
Animation timingWait for animations to complete or disable them
Viewport inconsistencySet explicit viewport in config
Random test order issuesTests must be independent
Third-party service flakinessMock external APIs

CI/CD Integration

# GitHub Actions example
name: E2E Tests
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npm run start & npx wait-on http://localhost:3000
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

NEVER Do

  1. NEVER use fixed waitForTimeout() or cy.wait(ms) — they cause flaky tests and slow down suites
  2. NEVER rely on CSS classes or DOM structure for selectors — use roles, labels, or data-testid
  3. NEVER share state between tests — each test must be completely independent
  4. NEVER test implementation details — test what users see and do, not internal structure
  5. NEVER skip cleanup — always delete test data you created, even on failure
  6. NEVER test everything with E2E — reserve for critical paths; use faster tests for edge cases
  7. NEVER ignore flaky tests — fix them immediately or delete them; a flaky test is worse than no test
  8. NEVER hardcode test data in selectors — use dynamic waits for content that varies

Quick Reference

Playwright Commands

// Navigation
await page.goto("/path");
await page.goBack();
await page.reload();

// Interactions
await page.click("selector");
await page.fill("selector", "text");
await page.type("selector", "text");  // Types character by character
await page.selectOption("select", "value");
await page.check("checkbox");

// Assertions
await expect(page).toHaveURL("/expected");
await expect(locator).toBeVisible();
await expect(locator).toHaveText("expected");
await expect(locator).toBeEnabled();
await expect(locator).toHaveCount(3);

Cypress Commands

// Navigation
cy.visit("/path");
cy.go("back");
cy.reload();

// Interactions
cy.get("selector").click();
cy.get("selector").type("text");
cy.get("selector").clear().type("text");
cy.get("select").select("value");
cy.get("checkbox").check();

// Assertions
cy.url().should("include", "/expected");
cy.get("selector").should("be.visible");
cy.get("selector").should("have.text", "expected");
cy.get("selector").should("have.length", 3);

如何使用「E2E Testing Patterns」?

  1. 打开小龙虾AI(Web 或 iOS App)
  2. 点击上方「立即使用」按钮,或在对话框中输入任务描述
  3. 小龙虾AI 会自动匹配并调用「E2E Testing Patterns」技能完成任务
  4. 结果即时呈现,支持继续对话优化

相关技能