Use Cases (更新: 2026/6/3)

用 Claude Code 做 E2E 测试: Playwright/Cypress 与 CI 实战指南

用 Claude Code 设计 Playwright E2E 测试:CI、常见坑、可运行示例和转化检查。

用 Claude Code 做 E2E 测试: Playwright/Cypress 与 CI 实战指南

先决定 E2E 测试要保护什么

让 Claude Code 生成一个登录测试并通过,并不代表发布已经安全。E2E 测试会在真实浏览器里检查用户操作路径,调试成本比单元测试高,运行时间也更长。如果把所有按钮和样式都放进 E2E,CI 很快会变慢,失败也会被团队忽略。

更好的做法是先选出损失最大的路径:登录、结账、咨询表单、管理后台危险操作,以及文章里的商品或培训 CTA。Masa 在 ClaudeCodeLab 的运营中遇到过一个典型问题:页面能正常显示,但文章末尾通往培训页的链接被改坏了。这样的错误不会被普通截图发现,却会直接影响收入路径。

本文基于 2026 年 6 月确认过的 Playwright 安装文档定位器文档CI 文档Trace Viewer,整理如何向 Claude Code 下达任务、如何写可运行的 Playwright 示例、如何放进 CI,以及 headed/headless 的常见坑。相关内容可以继续读 API 测试自动化测试策略指南

flowchart LR
  A["向 Claude Code 下达任务"] --> B["选择三个关键流程"]
  B --> C["实现 Playwright 测试"]
  C --> D["用 trace 和报告排查"]
  D --> E["作为 CI 门禁"]
  C --> F["检查 CTA 和收入路径"]

给 Claude Code 的提示词模板

第一步要写清楚范围、成功条件、失败条件、可修改文件和验证命令。否则 Claude Code 很容易生成依赖 CSS 类名的脆弱测试。Playwright 官方推荐优先使用 getByRolegetByLabelgetByTextgetByTestId 等定位器。定位器就是“如何找到页面元素”的规则,它能配合 Playwright 的自动等待和可重试断言。

目标: 为登录、结账、newsletter 注册写 E2E 测试
工具: Playwright Test,最后补充 Cypress 适用场景
规则:
- 优先使用 role、label、text、testid,不依赖 CSS 类名
- 不只写成功路径,也要覆盖输入错误和空购物车
- CI 中使用 workers: 1,trace: on-first-retry
- 检查收入 CTA 的 href 没有丢失
可修改范围:
- tests/e2e/**/*.spec.ts
- playwright.config.ts
验证:
- npx playwright test
- 本地排查时使用 npx playwright test --headed

同时也要说明哪些不做。真实扣款、真实邮件发送、广告点击、CRM 写入不应该每次 E2E 都直接触发。它们通常应该使用测试模式、mock,或拆到 API/契约测试里。Webhook 和分析事件可以参考 webhook 实现分析实现

安装与基本命令

新项目可以用下面的命令开始。Playwright 官方说明,每个版本都对应特定浏览器二进制文件,所以升级 Playwright 后也要重新安装浏览器。Linux CI 上还要处理系统依赖,通常使用 --with-deps

npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright show-report

--headed 会打开可见浏览器,适合本地排查。--ui 会打开 Playwright 的交互式界面。CI 通常使用 headless,也就是不显示浏览器窗口。如果本地通过而 CI 失败,优先检查动画、视口、字体、时区、CPU 速度和外部 API 等待。

可直接运行的 Playwright 示例

下面的 spec 使用 page.setContent 创建一个小型页面,因此不需要启动真实应用。安装 Playwright 后,把它复制到 tests/e2e/claude-code-e2e.spec.ts 就能运行。真实项目里,把 renderDemoApp(page) 换成 page.goto('/login'),再调整标签和 data-testid

// tests/e2e/claude-code-e2e.spec.ts
import { test, expect, type Page } from '@playwright/test';

async function renderDemoApp(page: Page) {
  await page.setContent(`
    <!doctype html>
    <main>
      <form id="login-form" aria-label="Login form">
        <label>Email <input id="email" aria-label="Email" /></label>
        <label>Password <input id="password" aria-label="Password" type="password" /></label>
        <button type="submit">Log in</button>
        <p id="login-error" role="alert" hidden></p>
      </form>

      <section id="dashboard" hidden>
        <h1>Dashboard</h1>
        <a data-testid="training-cta" href="/training/">Book Claude Code training</a>
        <button id="add-plan">Add Pro plan to cart</button>
        <span data-testid="cart-count">0</span>
        <button id="buy">Complete purchase</button>
        <p data-testid="order-status" role="status"></p>

        <label>Newsletter email <input id="newsletter-email" aria-label="Newsletter email" /></label>
        <button id="newsletter-button" type="button">Join newsletter</button>
        <p id="newsletter-error" role="alert" hidden></p>
        <p data-testid="newsletter-status" role="status"></p>
      </section>
    </main>

    <script>
      const state = { cart: 0 };
      document.querySelector('#login-form').addEventListener('submit', (event) => {
        event.preventDefault();
        const email = document.querySelector('#email').value;
        const password = document.querySelector('#password').value;
        const error = document.querySelector('#login-error');

        if (email === 'masa@example.com' && password === 'password123') {
          document.querySelector('#login-form').hidden = true;
          document.querySelector('#dashboard').hidden = false;
          error.hidden = true;
          return;
        }

        error.textContent = 'Authentication failed';
        error.hidden = false;
      });

      document.querySelector('#add-plan').addEventListener('click', () => {
        state.cart += 1;
        document.querySelector('[data-testid="cart-count"]').textContent = String(state.cart);
      });

      document.querySelector('#buy').addEventListener('click', () => {
        const status = document.querySelector('[data-testid="order-status"]');
        status.textContent = state.cart === 0 ? 'Cart is empty' : 'Order ORD-1001 completed';
      });

      document.querySelector('#newsletter-button').addEventListener('click', () => {
        const email = document.querySelector('#newsletter-email').value;
        const error = document.querySelector('#newsletter-error');
        const status = document.querySelector('[data-testid="newsletter-status"]');

        if (!email.includes('@')) {
          error.textContent = 'Enter a valid email';
          error.hidden = false;
          status.textContent = '';
          return;
        }

        error.hidden = true;
        status.textContent = 'Thanks, we will send the checklist.';
      });
    </script>
  `);
}

async function loginAsDemoUser(page: Page) {
  await page.getByLabel('Email').fill('masa@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Log in' }).click();
}

test.describe('Claude Code E2E starter', () => {
  test('use case 1: login shows dashboard and keeps the training CTA', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await expect(page.getByTestId('training-cta')).toHaveAttribute('href', '/training/');
  });

  test('use case 2: checkout flow creates an order number', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await page.getByRole('button', { name: 'Add Pro plan to cart' }).click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
    await page.getByRole('button', { name: 'Complete purchase' }).click();
    await expect(page.getByTestId('order-status')).toContainText('ORD-1001');
  });

  test('use case 3: newsletter validation blocks invalid leads', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await page.getByRole('button', { name: 'Join newsletter' }).click();
    await expect(page.getByRole('alert')).toContainText('Enter a valid email');

    await page.getByLabel('Newsletter email').fill('reader@example.com');
    await page.getByRole('button', { name: 'Join newsletter' }).click();
    await expect(page.getByTestId('newsletter-status')).toContainText('Thanks');
  });
});

这个文件覆盖了三个具体用例:登录、结账、线索表单。它还检查了培训 CTA 的 href,这正是很多薄弱 E2E 测试容易漏掉的地方。

实务用 Playwright 配置

配置文件要让失败可追踪。Playwright 的 CI 文档建议在 CI 中使用 workers: 1 来提高稳定性和可复现性。等测试稳定后,再考虑并行或分片。

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

trace: 'on-first-retry' 会在第一次失败后保留操作记录。把 trace 和 HTML 报告交给 Claude Code,比只贴一行超时错误更容易定位问题。

放进 GitHub Actions

CI 上常见失败不是测试代码,而是浏览器或 Linux 依赖缺失。先用简单配置,不要一开始就复杂缓存。

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run E2E tests
        run: npx playwright test
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

如果测试预览环境,把 BASE_URL 指向预览地址。如果要在 CI 里启动本地应用,再补充 Playwright 的 webServer 配置。

值得写 E2E 的用例

用例为什么适合 E2E关键期待值
登录到控制台认证、跳转、权限会连在一起标题、用户状态、退出、错误提示
结账或咨询表单直接影响收入和线索订单号、确认页、防重复提交
文章到免费资料或培训页保护非广告收入路径CTA 的 href、事件、移动端可见
管理后台危险操作防止误删和越权确认弹窗、审计日志、角色检查
多语言页面翻译时最容易断链接locale URL、自然文案、外部文档

把这张表交给 Claude Code,可以避免它只“点击按钮”却没有验证真正重要的结果。

常见失败案例

不要使用 waitForTimeout(3000)。固定等待在本机可能通过,在 CI 中经常失败。应该等待具体状态,例如元素可见、URL 改变、状态文本出现。

不要依赖 .btn-primary:nth-child(2) 这类 CSS 选择器。设计改版会立即破坏测试。优先使用 role、label、text 和稳定的 data-testid

不要让测试共享状态。前一个测试创建的购物车、Cookie 或数据库记录,不应该成为下一个测试的前提。并行和重试会放大这个问题。

不要忽视 headed 与 headless 的差异。字体、GPU、滚动、剪贴板、文件上传、视口都可能在 CI 和本地不同。CI 保持 headless,排查时再用 --headed--ui

不要每次都调用真实支付、邮件、广告或 CRM。使用测试模式、mock 或单独的 API 合约测试更稳定。

Playwright 与 Cypress 的取舍

维度PlaywrightCypress
浏览器Chromium、Firefox、WebKitChrome 系、Firefox、Edge
并行Playwright Test 内建支持通常结合 Dashboard 设计
多标签和上下文支持较强限制较多
调试Trace Viewer、UI mode、HTML report交互式 GUI 友好
适合场景CI、多浏览器、认证状态隔离已有 Cypress 流程的前端团队

Cypress 仍然是好工具。如果团队已有 Cypress 资产,不必为了追新而迁移。可以先给一个关键流程补 Playwright,比较 flake 率、trace 质量和 CI 时间。最终判断要以 PlaywrightCypress 官方文档为准。

变现 CTA 与实际验证结果

E2E 也在保护收入。咨询链接、商品卡、免费资料表单、培训 CTA 坏掉,可能比一个视觉问题更贵。个人练习可以从免费 Claude Code 速查表开始,需要复用提示词时看产品与模板。团队要把 CI 门禁、CLAUDE.md、评审规则和 E2E 责任边界落地,可以从Claude Code 培训与咨询开始。

本文的示例已按 TypeScript 代码片段抽出并做了语法验证。最有用的习惯是把业务期待写进测试名,例如“保留培训 CTA”。真实项目建议先写三条:登录、结账或线索表单、文章 CTA。等它们在 CI 中稳定后,再扩展到后台和多语言页面。

#Claude Code #E2E 测试 #Playwright #Cypress #CI
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。