前几天我们组发生了件极度丢脸的事。一个老手改动了全局导航栏里的公共下拉菜单组件,结果因为 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'))来进行语义化查找?