用 Claude Code 和 Playwright 落地 E2E 测试
用 Claude Code 设计 Playwright E2E:移动截图、认证状态、Trace Viewer、选择器和 CI 重试。
只对 Claude Code 说“帮我写 Playwright 测试”,通常得不到能支撑发布的 E2E 套件。它可能生成能跑一次的代码,但选择器依赖 CSS 类名,登录每个用例都走 UI,手机截图没有留下,CI 失败后也没有 Trace Viewer 可以排查。
更有效的做法,是把 Claude Code 当成测试设计伙伴。你负责说明业务风险、页面路径、选择器规则和验证命令;Claude Code 负责阅读代码库、补齐 Playwright 配置和测试文件。本文会把 Claude Code + Playwright 的实用流程整理成可复制的示例,覆盖移动端截图、代码块布局 QA、Trace Viewer、认证状态、CI retry,以及如何减少 flaky test。
官方资料建议以 Claude Code overview、Claude Code common workflows、Playwright 的 Locators、Authentication、Screenshots、Trace Viewer、Retries 和 CI 为准。ClaudeCodeLab 内部可继续阅读测试策略、CI/CD 设置和响应式设计。
先选值得保护的流程
E2E 测试会启动真实浏览器,比单元测试慢,所以不要试图覆盖所有细节。优先选择只有浏览器才能证明的关键路径。
| 用例 | 保护什么 | Playwright 检查点 |
|---|---|---|
| 文章到产品页 | 读者能从内容进入 /products/ | CTA 链接、URL、手机可点击性 |
| 登录后的后台 | 已认证用户能进入受保护页面 | storageState、重定向、权限 |
| 技术文章布局 | 代码块和表格不会撑破手机宽度 | 移动截图、无横向溢出、Trace |
flowchart LR
A["收入或注册路径"] --> B["Playwright E2E"]
C["移动布局风险"] --> B
D["纯计算和校验"] --> E["单元测试"]
F["API 或组件边界"] --> G["集成测试"]
这三个用例适合内容站、SaaS 和电商。CTA 在手机上被遮住、代码块让页面变宽、付费用户页面没有进 CI,看起来都是小问题,但会直接影响转化和信任。
给 Claude Code 明确边界
提示词要写清目标路由、允许的选择器、验证命令和可修改文件。这样可以避免 Claude Code 为了补测试而顺手重构 UI。
请读取现有 Astro 网站,并添加 Playwright E2E 测试。
目标:
- 验证 `/zh/blog/claude-code-playwright-testing/` 可以进入 `/products/` 和 `/training/`
- 在 390px 手机宽度检查文章、表格、代码块是否横向溢出
- 需要登录的测试使用 `storageState`,不要每个测试都从 UI 登录
- CI 中 retry 设为 2,trace 使用 `on-first-retry`
限制:
- 不使用 `page.waitForTimeout()`
- 优先使用 role、label、text、test id,避免 CSS 类名链
- 只修改 `playwright.config.ts` 和 `tests/e2e/**`
- 执行 `npx playwright test`,失败时用 Trace Viewer 说明原因
评审 Claude Code 的输出时,不要只看文件是否生成,而要看失败时能不能解释、选择器是否稳定、测试数据是否可控。
可复制的最小配置
下面的配置可直接放进应用。不同框架只需要调整 BASE_URL 和 preview 命令。
cd site
npm i -D @playwright/test
npx playwright install
mkdir tests/e2e
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const baseURL = process.env.BASE_URL ?? 'http://127.0.0.1:4321';
const hasAuth = Boolean(process.env.TEST_EMAIL && process.env.TEST_PASSWORD);
const authFile = 'playwright/.auth/user.json';
export default defineConfig({
testDir: './tests/e2e',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI ? [['html'], ['github']] : 'html',
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
...(process.env.PLAYWRIGHT_WEB_SERVER === '1'
? {
webServer: {
command: 'npm run preview -- --host 127.0.0.1 --port 4321',
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
}
: {}),
projects: [
...(hasAuth
? [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
]
: []),
{
name: 'desktop-chrome',
use: {
...devices['Desktop Chrome'],
storageState: hasAuth ? authFile : undefined,
},
dependencies: hasAuth ? ['setup'] : [],
},
{
name: 'mobile-safari',
use: {
...devices['iPhone 13'],
storageState: hasAuth ? authFile : undefined,
},
dependencies: hasAuth ? ['setup'] : [],
},
],
});
本地不启用 retry,便于尽快暴露问题;CI 才启用 retry,因为共享 runner 会有偶发波动。关键是 retry 必须和 trace、截图、HTML report 一起保存。
用 storageState 保存登录状态
认证相关测试如果每次都从登录页开始,会慢,而且容易受二次验证、限流、UI 改版影响。storageState 可以把登录后的浏览器状态保存到文件,后续项目直接复用。注意:playwright/.auth 可能包含 Cookie 和 header,必须加入 .gitignore。
// tests/e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
const authFile = path.resolve('playwright/.auth/user.json');
const email = process.env.TEST_EMAIL;
const password = process.env.TEST_PASSWORD;
setup('save signed-in browser state', async ({ page }) => {
setup.skip(!email || !password, 'Set TEST_EMAIL and TEST_PASSWORD to record auth state.');
await page.goto('/login');
await page.getByLabel(/email|メール|e-mail/i).fill(email!);
await page.getByLabel(/password|パスワード/i).fill(password!);
await page.getByRole('button', { name: /log in|sign in|ログイン/i }).click();
await expect(page).toHaveURL(/dashboard|account|admin/);
await expect(page.locator('body')).toBeVisible();
fs.mkdirSync(path.dirname(authFile), { recursive: true });
await page.context().storageState({ path: authFile });
});
请让 Claude Code 把“登录流程本身的测试”和“登录后功能的测试”分开。否则登录按钮文案一变,整套 E2E 都会红。
检查移动截图和代码块布局
技术文章最常见的问题不是页面完全打不开,而是长代码行、表格或图片把手机页面撑宽。下面的测试会检查 CTA、保存移动截图,并用数字断言横向溢出。
// tests/e2e/article-quality.spec.ts
import { test, expect } from '@playwright/test';
const articlePath = process.env.ARTICLE_PATH ?? '/zh/blog/claude-code-playwright-testing/';
test.describe('article quality checks', () => {
test('article has monetization CTAs', async ({ page }) => {
await page.goto(articlePath);
await expect(page.getByRole('heading', { level: 1 })).toContainText(/Playwright|E2E|Claude Code/i);
await expect(page.locator('a[href="/products/"], a[href="/products"]').first()).toBeVisible();
await expect(page.locator('a[href="/training/"], a[href="/training"]').first()).toBeVisible();
});
test('mobile layout has no horizontal overflow', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(articlePath);
await expect(page.locator('main, article').first()).toBeVisible();
const overflow = await page.evaluate(() => ({
viewport: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
offenders: Array.from(document.querySelectorAll('pre, table, img, iframe, .prose'))
.filter((node) => {
const rect = node.getBoundingClientRect();
return rect.left < -1 || rect.right > window.innerWidth + 1;
})
.map((node) => {
const rect = node.getBoundingClientRect();
return `${node.tagName.toLowerCase()} ${Math.round(rect.left)}-${Math.round(rect.right)}`;
}),
}));
expect(overflow.documentWidth, JSON.stringify(overflow)).toBeLessThanOrEqual(overflow.viewport + 2);
expect(overflow.offenders).toEqual([]);
await page.screenshot({ path: testInfo.outputPath('article-mobile.png'), fullPage: true });
});
test('code examples are present and copyable', async ({ page }) => {
await page.goto(articlePath);
const blocks = page.locator('pre code');
await expect(blocks.first()).toBeVisible();
expect(await blocks.count()).toBeGreaterThanOrEqual(3);
await expect(blocks.nth(0)).toContainText(/playwright|defineConfig|test/i);
});
});
截图用于人工 review,横向溢出断言用于 CI。两者组合,比单纯依赖视觉快照更稳定。
把变现路径放进 E2E
内容站不只要文章能读,还要读者能继续进入产品、培训或咨询页面。下面的测试保护 /products/ 和 /training/ 两个关键 CTA。
// tests/e2e/revenue-flows.spec.ts
import { test, expect } from '@playwright/test';
const articlePath = process.env.ARTICLE_PATH ?? '/zh/blog/claude-code-playwright-testing/';
test.describe('revenue and learning flows', () => {
test('reader can move from article to products', async ({ page }) => {
await page.goto(articlePath);
await page.locator('a[href="/products/"], a[href="/products"]').first().click();
await expect(page).toHaveURL(/\/products\/?$/);
await expect(page.locator('main').first()).toBeVisible();
});
test('training CTA is reachable on mobile', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(articlePath);
await page.locator('a[href="/training/"], a[href="/training"]').first().click();
await expect(page).toHaveURL(/\/training\/?$/);
await expect(page.locator('main').first()).toBeVisible();
});
test('main navigation can open the blog index', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation').first()).toBeVisible();
await page.getByRole('link', { name: /blog|記事|articles/i }).first().click();
await expect(page).toHaveURL(/blog/);
});
});
如果页面是多语言的,CTA 文案可能变,但 href 不该乱变。对于 checkout、logout、拖拽操作等关键动作,可以让 Claude Code 添加稳定的 data-testid。
减少 flaky test
选择器优先级建议如下:
| 优先级 | 选择器 | 示例 | 原因 |
|---|---|---|---|
| 高 | role + name | page.getByRole('button', { name: /save/i }) | 接近真实用户和辅助技术 |
| 高 | label | page.getByLabel(/email/i) | 同时检查表单语义 |
| 中 | text | page.getByText(/Start trial/) | 清楚,但会受文案影响 |
| 中 | test id | page.getByTestId('checkout-submit') | 适合稳定业务动作 |
| 低 | CSS 结构 | .card:nth-child(3) | 布局一变就坏 |
也不要用 page.waitForTimeout() 掩盖问题。优先使用 toBeVisible()、toHaveURL()、toContainText() 等 web-first assertion,让 Playwright 自动等待真实条件。
用 Trace Viewer 排查
CI 失败时,Trace Viewer 能看到每一步的 DOM、截图、网络和 console。配置 trace: 'on-first-retry',可以只在失败重试时留下证据。
npx playwright test --trace on
npx playwright show-report
npx playwright show-trace test-results/path-to-trace/trace.zip
把失败交给 Claude Code 时,请附上测试名、trace 中看到的画面状态、以及期望的用户行为。这样它更可能修选择器、补数据准备或指出 UI 缺陷,而不是简单加等待时间。
CI 配置
# .github/workflows/playwright.yml
name: Playwright E2E
on:
pull_request:
push:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: site
env:
BASE_URL: http://127.0.0.1:4321
PLAYWRIGHT_WEB_SERVER: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: site/package-lock.json
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: site/playwright-report
retention-days: 7
retry 不是治好 flaky test,而是把它分类并留下证据。HTML report、trace zip、截图 artifact 都要保留。
常见坑
第一个坑是把所有测试都写成 E2E。价格计算、权限边界、字段校验应该优先放在单元或集成测试,Playwright 只负责浏览器层面的关键路径。
第二个坑是把认证状态提交到仓库。storageState 方便,但包含敏感信息,必须使用专门测试账号并排除出 git。
第三个坑是用 retry 盖住不稳定。只要测试经常靠 retry 才通过,就要看 Trace Viewer,排查网络等待、随机数据、动画、时间依赖或弱选择器。
第四个坑是让 Claude Code 修改范围过大。更可靠的流程是:先补会失败的测试,再看 trace,最后做最小产品修复。
如果你想自助落地,可从 ClaudeCodeLab 的 products 模板开始;如果团队需要统一 review 规则、CI 策略和培训流程,可以看 training。
我用这套流程检查了一个本地 ClaudeCodeLab 风格文章页:390px 截图、代码块横向溢出、/products/ 与 /training/ CTA、以及 CI retry 配置。最先暴露的问题不是 Playwright,而是过长代码行和不够明确的链接名称。修好这些之后,Trace Viewer 才真正变成可以交给 Claude Code 继续修复的证据。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。