端到端测试Skill e2e-testing

端到端测试是一种软件测试方法,用于模拟真实用户场景,验证Web应用程序从开始到结束的完整流程,确保所有组件正确协作。它涵盖使用自动化工具如Playwright和Cypress的现代测试模式,包括页面对象模型、选择器策略、异步处理、视觉回归测试和不稳定测试预防。关键词:端到端测试,E2E测试,自动化测试,Playwright,Cypress,软件测试,浏览器自动化。

测试 0 次安装 0 次浏览 更新于 3/24/2026

name: e2e-testing description: 使用 Playwright、Cypress、Selenium 和 Puppeteer 为 Web 应用程序进行端到端测试的模式和最佳实践。涵盖页面对象模型、测试夹具、选择器策略、异步处理、视觉回归测试和不稳定测试预防。包括质量保证专业知识,用于验收测试、冒烟测试、跨浏览器测试和测试可靠性。当设置 E2E 测试、调试测试失败、提高测试可靠性或实现浏览器自动化时使用。触发关键词:e2e, e2e testing, end-to-end, end-to-end tests, Playwright, Cypress, Selenium, Puppeteer, Page Object Model, page object, test fixtures, selectors, locator, locators, data-testid, async tests, visual regression, visual testing, screenshot, flaky tests, flakiness, browser testing, browser automation, UI test, UI testing, acceptance test, acceptance testing, smoke test, smoke testing, integration test, wait, waits, assertion, assertions, test data, test isolation.

端到端测试

概述

端到端(E2E)测试通过验证完整的用户流程来确保应用程序所有组件正确协作。此技能涵盖使用 Playwright 和 Cypress 的现代端到端测试模式,包括架构模式、选择器策略和构建可靠、可维护测试套件的技术。

指令

1. 选择您的框架

Playwright 与 Cypress 比较:

特性 Playwright Cypress
多浏览器 Chrome, Firefox, Safari, Edge Chrome, Firefox, Edge
多标签/窗口 有限
网络拦截 强大
并行执行 内置 需要仪表板
语言支持 JS, TS, Python, .NET, Java JS, TS
iframes 完全支持 有限
移动模拟 优秀 基本

Playwright 设置:

npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  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",
  },
  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"] } },
  ],
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

Cypress 设置:

npm install cypress --save-dev
// cypress.config.ts
import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,
    screenshotOnRunFailure: true,
    retries: { runMode: 2, openMode: 0 },
    setupNodeEvents(on, config) {
      // 任务插件
    },
  },
});

2. 实现页面对象模型 (POM)

Playwright 页面对象:

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

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

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.submitButton = page.getByRole("button", { name: "Sign in" });
    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.submitButton.click();
  }

  async getErrorMessage(): Promise<string> {
    return (await this.errorMessage.textContent()) ?? "";
  }
}

Cypress 页面对象:

// cypress/pages/LoginPage.ts
export class LoginPage {
  visit() {
    cy.visit("/login");
    return this;
  }

  getEmailInput() {
    return cy.findByLabelText("Email");
  }

  getPasswordInput() {
    return cy.findByLabelText("Password");
  }

  getSubmitButton() {
    return cy.findByRole("button", { name: "Sign in" });
  }

  login(email: string, password: string) {
    this.getEmailInput().type(email);
    this.getPasswordInput().type(password);
    this.getSubmitButton().click();
    return this;
  }
}

页面对象组合:

// e2e/pages/index.ts
import { Page } from "@playwright/test";
import { LoginPage } from "./LoginPage";
import { DashboardPage } from "./DashboardPage";
import { CheckoutPage } from "./CheckoutPage";

export class App {
  readonly login: LoginPage;
  readonly dashboard: DashboardPage;
  readonly checkout: CheckoutPage;

  constructor(page: Page) {
    this.login = new LoginPage(page);
    this.dashboard = new DashboardPage(page);
    this.checkout = new CheckoutPage(page);
  }
}

// 在测试中使用
test("用户可以完成购买", async ({ page }) => {
  const app = new App(page);
  await app.login.goto();
  await app.login.login("user@example.com", "password");
  await app.dashboard.selectProduct("Widget");
  await app.checkout.completePayment();
});

3. 管理测试夹具和数据

Playwright 夹具:

// e2e/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

type AuthFixtures = {
  authenticatedPage: Page;
  loginPage: LoginPage;
};

export const test = base.extend<AuthFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  authenticatedPage: async ({ page }, use) => {
    // 设置认证状态
    await page.goto("/login");
    await page.getByLabel("Email").fill("test@example.com");
    await page.getByLabel("Password").fill("password123");
    await page.getByRole("button", { name: "Sign in" }).click();
    await page.waitForURL("/dashboard");

    await use(page);
  },
});

// 或使用存储状态以加速认证
export const test = base.extend<AuthFixtures>({
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: "e2e/.auth/user.json",
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

测试数据工厂:

// e2e/fixtures/factories.ts
import { faker } from "@faker-js/faker";

export const UserFactory = {
  create(overrides = {}) {
    return {
      email: faker.internet.email(),
      password: faker.internet.password({ length: 12 }),
      firstName: faker.person.firstName(),
      lastName: faker.person.lastName(),
      ...overrides,
    };
  },

  createAdmin(overrides = {}) {
    return this.create({ role: "admin", ...overrides });
  },
};

export const ProductFactory = {
  create(overrides = {}) {
    return {
      name: faker.commerce.productName(),
      price: parseFloat(faker.commerce.price()),
      description: faker.commerce.productDescription(),
      sku: faker.string.alphanumeric(8).toUpperCase(),
      ...overrides,
    };
  },
};

数据库种子:

// e2e/fixtures/database.ts
import { test as base } from "@playwright/test";
import { prisma } from "../../src/lib/prisma";
import { UserFactory, ProductFactory } from "./factories";

export const test = base.extend({
  testUser: async ({}, use) => {
    const userData = UserFactory.create();
    const user = await prisma.user.create({ data: userData });

    await use(user);

    // 测试后清理
    await prisma.user.delete({ where: { id: user.id } });
  },

  seededProducts: async ({}, use) => {
    const products = await Promise.all(
      Array.from({ length: 5 }, () =>
        prisma.product.create({ data: ProductFactory.create() }),
      ),
    );

    await use(products);

    await prisma.product.deleteMany({
      where: { id: { in: products.map((p) => p.id) } },
    });
  },
});

4. 应用选择器策略

选择器优先级 (从好到差):

  1. 无障碍角色和标签
  2. data-testid 属性
  3. 文本内容
  4. CSS 选择器
  5. XPath (避免使用)

Playwright 选择器示例:

// 首选: 基于无障碍的选择器
page.getByRole("button", { name: "Submit" });
page.getByRole("textbox", { name: "Email" });
page.getByRole("link", { name: "Learn more" });
page.getByLabel("Password");
page.getByPlaceholder("Enter your email");
page.getByText("Welcome back");

// 好: 测试 ID 用于复杂元素
page.getByTestId("user-avatar");
page.getByTestId("product-card-123");

// 可接受: CSS 用于结构选择
page.locator("table tbody tr:first-child");
page.locator(".modal-content");

// 链接定位器
page
  .getByTestId("product-list")
  .getByRole("listitem")
  .filter({ hasText: "Widget" })
  .getByRole("button", { name: "Add to cart" });

向组件添加测试 ID:

// 带测试 ID 的 React 组件
function ProductCard({ product }: { product: Product }) {
  return (
    <div data-testid={`product-card-${product.id}`}>
      <h3 data-testid="product-name">{product.name}</h3>
      <span data-testid="product-price">${product.price}</span>
      <button data-testid="add-to-cart-btn">Add to Cart</button>
    </div>
  );
}

// 在生产中移除测试 ID
// babel.config.js
module.exports = {
  env: {
    production: {
      plugins: [["react-remove-properties", { properties: ["data-testid"] }]],
    },
  },
};

5. 处理异步操作和等待

Playwright 中的自动等待:

// Playwright 自动等待操作可行性
await page.getByRole("button").click(); // 等待可见、启用、稳定

// 需要时的显式等待
await page.waitForURL("/dashboard");
await page.waitForResponse("/api/users");
await page.waitForLoadState("networkidle");

// 等待特定条件
await expect(page.getByTestId("loading")).toBeHidden();
await expect(page.getByRole("table")).toBeVisible();

网络请求处理:

// 等待 API 响应
const responsePromise = page.waitForResponse("/api/products");
await page.getByRole("button", { name: "Load Products" }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);

// 模拟 API 响应
await page.route("/api/products", async (route) => {
  await route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify([{ id: 1, name: "Mocked Product" }]),
  });
});

// 拦截和修改
await page.route("/api/user", async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.isAdmin = true;
  await route.fulfill({ response, json });
});

处理加载状态:

// 等待加载完成
async function waitForDataLoad(page: Page) {
  // 选项 1: 等待加载指示器消失
  await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });

  // 选项 2: 等待数据出现
  await expect(page.getByRole("table")).toHaveCount(1);

  // 选项 3: 等待网络空闲
  await page.waitForLoadState("networkidle");
}

6. 实现视觉回归测试

Playwright 视觉比较:

// 基本截图比较
test("主页视觉", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("homepage.png");
});

// 组件截图
test("按钮状态", async ({ page }) => {
  await page.goto("/components/button");

  const button = page.getByRole("button", { name: "Click me" });
  await expect(button).toHaveScreenshot("button-default.png");

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

// 带选项的完整页面
test("完整页面视觉", async ({ page }) => {
  await page.goto("/dashboard");
  await expect(page).toHaveScreenshot("dashboard.png", {
    fullPage: true,
    mask: [page.getByTestId("dynamic-timestamp")],
    maxDiffPixelRatio: 0.01,
  });
});

视觉测试配置:

// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 100,
      maxDiffPixelRatio: 0.01,
      threshold: 0.2,
      animations: "disabled",
    },
  },
  use: {
    // 视觉测试的一致视口
    viewport: { width: 1280, height: 720 },
  },
});

处理动态内容:

// 遮罩动态元素
await expect(page).toHaveScreenshot({
  mask: [
    page.getByTestId("current-date"),
    page.getByTestId("user-avatar"),
    page.locator(".advertisement"),
  ],
});

// 冻结动画和时间
await page.emulateMedia({ reducedMotion: "reduce" });
await page.clock.setFixedTime(new Date("2024-01-15T10:00:00"));

7. 防止不稳定测试

常见不稳定原因和解决方案:

// 坏: 带时间的竞态条件
await page.click("#submit");
await page.waitForTimeout(2000); // 任意等待
expect(await page.textContent(".result")).toBe("Success");

// 好: 等待实际条件
await page.click("#submit");
await expect(page.getByText("Success")).toBeVisible();
// 坏: 依赖元素顺序
const items = await page.locator(".list-item").all();
await items[2].click(); // 索引可能改变

// 好: 按内容选择
await page.getByRole("listitem").filter({ hasText: "Target Item" }).click();
// 坏: 未等待导航
await page.click('a[href="/dashboard"]');
await expect(page.locator(".dashboard")).toBeVisible();

// 好: 显式导航等待
await page.click('a[href="/dashboard"]');
await page.waitForURL("/dashboard");
await expect(page.locator(".dashboard")).toBeVisible();

测试隔离:

// 每个测试应重新开始
test.beforeEach(async ({ page }) => {
  // 清除存储
  await page.context().clearCookies();
  await page.evaluate(() => localStorage.clear());

  // 重置到已知状态
  await page.goto("/");
});

// 每个测试使用唯一数据
test("创建用户", async ({ page }) => {
  const uniqueEmail = `test-${Date.now()}@example.com`;
  // ...
});

重试策略:

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: "on-first-retry", // 重试时捕获跟踪
  },
});

// 特定测试重试
test("可能不稳定的测试", async ({ page }) => {
  test.info().annotations.push({ type: "retries", description: "3" });
  // ...
});

调试不稳定测试:

// 启用跟踪
await context.tracing.start({ screenshots: true, snapshots: true });
// ... 运行测试
await context.tracing.stop({ path: "trace.zip" });

// 查看跟踪
// npx playwright show-trace trace.zip

// 添加调试暂停
await page.pause(); // 打开检查器

8. 实现 Playwright 特定模式

Playwright 高级特性:

// 多上下文 (并行会话)
test("多用户", async ({ browser }) => {
  const userContext = await browser.newContext();
  const adminContext = await browser.newContext();

  const userPage = await userContext.newPage();
  const adminPage = await adminContext.newPage();

  await userPage.goto("/");
  await adminPage.goto("/admin");

  // 测试用户间交互
  await adminPage.getByRole("button", { name: "Broadcast" }).click();
  await expect(userPage.getByRole("alert")).toBeVisible();

  await userContext.close();
  await adminContext.close();
});

// 移动模拟
test("移动导航", async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto("/");

  // 移动菜单应可见
  await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
});

// 地理位置测试
test("基于位置的功能", async ({ context, page }) => {
  await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
  await context.grantPermissions(["geolocation"]);

  await page.goto("/");
  await expect(page.getByText("San Francisco")).toBeVisible();
});

// 网络离线模式
test("离线行为", async ({ context, page }) => {
  await page.goto("/");
  await context.setOffline(true);

  await page.reload();
  await expect(page.getByText("You are offline")).toBeVisible();
});

Playwright API 请求上下文:

// API 级认证以加速设置
test.beforeAll(async ({ request }) => {
  // 通过 API 创建用户
  const response = await request.post("/api/users", {
    data: { email: "test@example.com", password: "secure123" },
  });
  expect(response.ok()).toBeTruthy();
});

// 混合 API + UI 测试
test("订单创建", async ({ page, request }) => {
  // 通过 API 设置 (快速)
  await request.post("/api/cart/add", {
    data: { productId: "123", quantity: 2 },
  });

  // 通过 UI 验证 (面向用户)
  await page.goto("/cart");
  await expect(page.getByTestId("cart-item")).toHaveCount(1);
  await expect(page.getByTestId("quantity")).toHaveText("2");
});

9. 应用质量保证最佳实践

测试金字塔策略:

         端到端测试 (5-10%)      ← 冒烟测试, 关键路径
       集成测试 (20-30%) ← 组件集成
      单元测试 (60-75%)  ← 业务逻辑, 工具

冒烟测试套件 (发布前必须通过):

// e2e/smoke/critical-paths.spec.ts
test.describe("冒烟测试", () => {
  test("主页加载", async ({ page }) => {
    await page.goto("/");
    await expect(page).toHaveTitle(/Home/);
    await expect(page.getByRole("navigation")).toBeVisible();
  });

  test("用户可以登录", async ({ page }) => {
    await page.goto("/login");
    await page.getByLabel("Email").fill("user@example.com");
    await page.getByLabel("Password").fill("password123");
    await page.getByRole("button", { name: "Sign in" }).click();
    await expect(page).toHaveURL("/dashboard");
  });

  test("关键 API 端点响应", async ({ request }) => {
    const endpoints = ["/api/health", "/api/products", "/api/user"];
    for (const endpoint of endpoints) {
      const response = await request.get(endpoint);
      expect(response.status()).toBeLessThan(500);
    }
  });
});

验收测试模式:

// e2e/acceptance/user-stories.spec.ts
test.describe("用户故事: 购买流程", () => {
  test("作为一名顾客,我想购买一个产品以便我能在家收到它", async ({
    page,
  }) => {
    // 给定我在产品页面
    await page.goto("/products/widget-123");

    // 当我将产品添加到购物车
    await page.getByRole("button", { name: "Add to Cart" }).click();

    // 然后我继续结账
    await page.getByRole("link", { name: "Checkout" }).click();

    // 然后我填写我的配送详情
    await page.getByLabel("Address").fill("123 Main St");
    await page.getByLabel("City").fill("Anytown");

    // 然后我完成支付
    await page.getByLabel("Card number").fill("4242424242424242");
    await page.getByRole("button", { name: "Place Order" }).click();

    // 那么我应该看到订单确认
    await expect(page.getByText("Thank you for your order")).toBeVisible();
    await expect(page.getByTestId("order-number")).toBeVisible();
  });
});

跨浏览器测试策略:

// playwright.config.ts
export default defineConfig({
  projects: [
    // 桌面浏览器
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },

    // 移动浏览器
    { name: "mobile-chrome", use: { ...devices["Pixel 5"] } },
    { name: "mobile-safari", use: { ...devices["iPhone 13"] } },

    // 品牌浏览器 (如果需要)
    { name: "edge", use: { ...devices["Desktop Edge"], channel: "msedge" } },
    {
      name: "chrome",
      use: { ...devices["Desktop Chrome"], channel: "chrome" },
    },
  ],
});

// 在所有浏览器上运行关键测试,其他仅在 Chrome 上运行
test.describe("关键流程", () => {
  test("结账工作", async ({ page, browserName }) => {
    // 在所有浏览器上运行
  });
});

test.describe("管理面板", () => {
  test.skip(({ browserName }) => browserName !== "chromium");

  test("批量操作", async ({ page }) => {
    // 仅在 Chrome 上运行以加速
  });
});

测试可观察性和报告:

// 持续集成的自定义测试报告器
// playwright.config.ts
export default defineConfig({
  reporter: [
    ["html", { outputFolder: "test-results/html" }],
    ["junit", { outputFile: "test-results/junit.xml" }],
    ["json", { outputFile: "test-results/results.json" }],
    ["./custom-reporter.ts"], // 自定义 Slack/Teams 通知
  ],

  use: {
    trace: "retain-on-failure", // 为失败测试保留跟踪
    video: "retain-on-failure",
    screenshot: "only-on-failure",
  },
});

// 自定义报告器示例
class CustomReporter {
  onTestEnd(test, result) {
    if (result.status === "failed") {
      // 发送通知到 Slack/Teams
      // 附加跟踪 URL, 截图
    }
  }

  onEnd(result) {
    const passRate = (result.passed / result.total) * 100;
    // 发送摘要仪表板
  }
}

最佳实践

  1. 保持测试独立

    • 测试间没有共享状态
    • 每个测试设置和拆卸其自己的数据
    • 测试可以以任何顺序运行
    • 使用数据库事务或隔离的测试数据库
  2. 使用描述性测试名称

    // 好 - 描述用户行为和预期结果
    test('用户提交空表单时看到错误消息', ...);
    test('管理员可以从管理面板删除用户', ...);
    
    // 坏 - 太模糊
    test('表单验证', ...);
    test('删除用户', ...);
    
  3. 遵循 AAA 模式 (安排-行动-断言)

    test("产品添加到购物车", async ({ page }) => {
      // 安排 - 设置初始状态
      await page.goto("/products");
    
      // 行动 - 执行用户操作
      await page
        .getByTestId("product-1")
        .getByRole("button", { name: "Add" })
        .click();
    
      // 断言 - 验证预期结果
      await expect(page.getByTestId("cart-count")).toHaveText("1");
    });
    
  4. 最小化测试范围

    • 每个测试测试一个用户流程
    • 将复杂流程分解为更小的测试
    • 对常见设置使用夹具
    • 避免在一个测试中测试多个场景
  5. 主动处理不稳定性

    • 立即审查和修复不稳定测试 (破窗理论)
    • 使用适当的等待,从不使用任意超时
    • 隔离测试与外部依赖
    • 模拟不稳定的第三方服务
    • 仅将自动重试用作临时措施
  6. 维护测试数据

    • 对一致测试数据使用工厂
    • 测试后清理 (避免污染数据库)
    • 避免硬编码 ID 或值
    • 需要时使用唯一标识符 (时间戳, UUID)
  7. 优先测试维护

    • 代码更改时重构测试
    • 移除过时的测试
    • 保持页面对象与 UI 更改同步
    • 在持续集成中立即审查测试失败
  8. 优化测试执行速度

    • 可能时并行运行测试
    • 对测试数据使用 API 设置而不是 UI
    • 跳过测试间不必要的导航
    • 对认证使用存储状态
    • 分组类似测试以共享设置

示例

示例: 完整的端到端测试套件

// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
import { App } from "./pages";
import { UserFactory, ProductFactory } from "./fixtures/factories";

test.describe("结账流程", () => {
  let app: App;

  test.beforeEach(async ({ page }) => {
    app = new App(page);
  });

  test("访客用户可以完成结账", async ({ page }) => {
    // 导航到产品
    await page.goto("/products");
    await page
      .getByTestId("product-card")
      .first()
      .getByRole("button", { name: "Add to Cart" })
      .click();

    // 验证购物车已更新
    await expect(page.getByTestId("cart-count")).toHaveText("1");

    // 去结账
    await page.getByRole("link", { name: "Checkout" }).click();
    await page.waitForURL("/checkout");

    // 填写配送信息
    await page.getByLabel("Email").fill("guest@example.com");
    await page.getByLabel("Address").fill("123 Test St");
    await page.getByLabel("City").fill("Test City");
    await page.getByRole("button", { name: "Continue" }).click();

    // 填写支付 (测试模式)
    await page.getByLabel("Card number").fill("4242424242424242");
    await page.getByLabel("Expiry").fill("12/25");
    await page.getByLabel("CVC").fill("123");

    // 完成订单
    await page.getByRole("button", { name: "Place Order" }).click();

    // 验证成功
    await expect(
      page.getByRole("heading", { name: "Order Confirmed" }),
    ).toBeVisible();
    await expect(page.getByTestId("order-number")).toBeVisible();
  });

  test("显示无效支付的验证错误", async ({ page }) => {
    // 设置: 添加项目并去支付
    await page.goto("/checkout?items=product-1");
    await page.getByLabel("Email").fill("test@example.com");
    await page.getByRole("button", { name: "Continue" }).click();

    // 输入无效卡号
    await page.getByLabel("Card number").fill("1234567890123456");
    await page.getByRole("button", { name: "Place Order" }).click();

    // 验证错误
    await expect(page.getByRole("alert")).toContainText("Invalid card number");
  });
});

示例: 边缘情况的 API 模拟

// e2e/error-handling.spec.ts
import { test, expect } from "@playwright/test";

test.describe("错误处理", () => {
  test("API 失败时显示友好错误", async ({ page }) => {
    // 模拟 API 失败
    await page.route("/api/products", (route) =>
      route.fulfill({ status: 500, body: "Internal Server Error" }),
    );

    await page.goto("/products");

    await expect(page.getByRole("alert")).toContainText(
      "Unable to load products. Please try again.",
    );
    await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
  });

  test("优雅处理网络超时", async ({ page }) => {
    // 模拟慢网络
    await page.route("/api/products", async (route) => {
      await new Promise((resolve) => setTimeout(resolve, 30000));
      await route.continue();
    });

    await page.goto("/products");

    await expect(page.getByText("Loading...")).toBeVisible();
    // 在合理等待后验证超时处理
  });
});