前几天我们组发生了件极度丢脸的事。一个老手改动了全局导航栏里的公共下拉菜单组件,结果因为 CSS 样式污染和 JS 事件冒泡冲突,导致购物车页面的“去结算”按钮直接无法点击,或者一点击就报错白屏。最糟糕的是,测试人员在预发布环境只点了首页和商品详情页,谁也没去测试完整的结账流程。直到线上用户开始疯狂吐槽订单提交失败,老板的电话被打爆,我们才紧急排查故障回滚代码。这一波直接让当天的转化率跌到了谷底。

这次事故让我们彻底清醒了:指望人工点击去覆盖所有的交互分支,无异于在代码里埋地雷。前端项目的复杂度一旦上来,每次重构都像是在走钢丝。必须通过工具在 CI/CD 管道里把核心链路的回归测试(E2E Testing)完全自动化。

我们对比了 Cypress、Selenium 和 Playwright。最后决定选用 Playwright,主要是因为它能极速并发启动多浏览器实例(Chromium/Firefox/Webkit)、支持强大的 API 请求 Mock,并且极其稳定,几乎没有 Cypress 那种玄学的死锁等待问题。

为了解决支付流程在 CI 环境中无法真实调用的痛点,我们在 Playwright 测试中对外部 API 进行了拦截 Mock。下面是我们落地的一段核心测试用例代码,展示了如何模拟登录、添加购物车、拦截底层支付 API 并校验购买成功提示的完整测试闭环:

import { test, expect } from "@playwright/test";

test.describe("E2E Checkout Flow Regression Suite", () => {
  test.beforeEach(async ({ page }) => {
    // 拦截所有的后端支付请求,防止真实的第三方扣款扣费
    await page.route("**/api/v1/payment/charge", async (route) => {
      const json = {
        success: true,
        transactionId: "tx_mock_88992211",
        timestamp: new Date().toISOString(),
      };
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(json),
      });
    });
  });

  test("should successfully complete the checkout flow", async ({ page }) => {
    // 1. 打开首页并校验页面是否白屏
    await page.goto("http://localhost:3000/");
    await expect(page.locator("h1")).toContainText("商品推荐");

    // 2. 将首个商品添加至购物车
    const firstProductCard = page.locator(".product-card").first();
    await expect(firstProductCard).toBeVisible();
    
    const addToCartButton = firstProductCard.locator("button.add-to-cart");
    await addToCartButton.click();

    // 3. 点击导航栏的购物车图标,进入结算页
    const cartIcon = page.locator("#nav-cart-btn");
    await expect(cartIcon).toContainText("1");
    await cartIcon.click();

    // 4. 在结算页填写表单
    await expect(page).toHaveURL(/.*\/cart/);
    await page.fill('input[name="shipping_name"]', "张三");
    await page.fill('input[name="shipping_phone"]', "13800138000");
    await page.fill('input[name="shipping_address"]', "北京市海淀区中关村大街 1 号");

    // 5. 点击“去结算”,验证是否弹出支付成功气泡
    const checkoutButton = page.locator("button#submit-checkout-btn");
    await expect(checkoutButton).toBeEnabled();
    await checkoutButton.click();

    const successToast = page.locator(".toast-message-success");
    await expect(successToast).toBeVisible();
    await expect(successToast).toContainText("支付成功");
  });
});

把这个脚本写完之后,我们直接在 GitHub Actions 的 CI 配置文件 .github/workflows/e2e.yml 里增加了一步。每次有提交或者 PR 时,系统会自动跑 npm run test:e2e。如果有人在改动代码时弄坏了任何结账页的 DOM 元素,导致 Playwright 定位不到节点或者按钮断言失败,整个 CI 管道就会立刻飘红,彻底拦截有缺陷的代码被合入主分支。

搞定这套自动化拦截网之后,上线发布的心慌感减轻了九成。现在哪怕是深夜一键合入代码,我们也觉得心安理得。

你们在写 E2E 测试的时候,是用 data-testid 属性这种对代码有侵入式的元素定位方式,还是直接用 Playwright 推荐的 Accessibility 角色(例如 getByRole('button'))来进行语义化查找?