这是「AI 时代的前端设计与实现」系列的一篇补充。前面我们聊了角色、设计系统、rules 模板和 Figma MCP;这一篇聚焦工程化落地里最常被低估的环节——端到端测试。
为什么 AI 生成的前端特别需要 E2E
AI 写前端的速度很快,但它有两个天然短板:
- 没有全局视角:改 A 页面时很容易把 B 页面的样式或交互带崩。
- 对边界状态不敏感:loading、error、empty、无权限这些状态经常被忽略。
更麻烦的是,AI 生成的代码往往看起来没问题,一跑就报错。TypeScript 和 ESLint 能拦住类型和语法错误,但拦不住这些问题:
- 按钮点了没反应
- 表单提交后没跳转
- 某个弹窗在特定分辨率下错位
- 改完配色后整个页面气质变了
E2E 测试就是针对这些问题的最后一道防线。它不只是测功能,还能帮你做视觉验收和回归防护。
E2E 要测什么
不要试图把所有东西都用 E2E 测。抓住三类高价值场景:
1. 核心用户流程(Happy Path)
用户最常用的路径必须稳定。例如电商站的「搜索 → 加购 → 结算 → 支付」。
import { test, expect } from '@playwright/test';
test('用户完成购买流程', async ({ page }) => {
await page.goto('/products');
await page.click('[data-testid="product-card"]:first-child');
await page.click('[data-testid="add-to-cart"]');
await page.click('[data-testid="checkout-button"]');
await page.fill('[name="email"]', 'user@example.com');
await page.click('[data-testid="submit-order"]');
await expect(page.locator('text=订单已创建')).toBeVisible();
});
2. 错误和边界状态
AI 最容易漏的恰恰是这些:
- 网络失败时的 fallback UI
- 搜索无结果
- 表单校验失败
- 未登录访问需要权限的页面
test('搜索无结果展示空状态', async ({ page }) => {
await page.goto('/search');
await page.fill('[name="q"]', 'xyznotfound123');
await page.press('[name="q"]', 'Enter');
await expect(page.locator('[data-testid="empty-state"]')).toBeVisible();
await expect(page.locator('text=没有找到相关结果')).toBeVisible();
});
3. 视觉回归
这是 AI 前端最关键的能力。每次 AI 改完代码,自动截图和基准图对比,像素级变化都能发现。
test('首页视觉回归', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
});
});
工具链选择
| 工具 | 适用场景 | 推荐度 |
|---|---|---|
| Playwright | 功能测试、截图回归、多浏览器、移动端模拟、trace 调试 | ⭐⭐⭐ |
| Cypress | 社区成熟,但多标签页、iframe 支持弱 | ⭐⭐ |
| Selenium | 老项目维护,新项目不推荐 | ⭐ |
| Chromatic / Percy | 专业 UI 回归 SaaS,适合组件级视觉对比 | ⭐⭐⭐ |
| axe-core | 无障碍扫描 | ⭐⭐⭐ |
建议: 新项目直接用 Playwright。它一个工具就能覆盖功能测试、视觉回归、响应式、多浏览器,失败时还能看 trace 回放。
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'], ['list']],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
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 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
关键配置解读:
trace: 'on-first-retry':失败时保留 trace,可以逐帧回放。screenshot: 'only-on-failure':失败自动截图。retries: 2:CI 里重试 2 次,排除 flaky。webServer:自动起本地服务。
视觉回归怎么做
基础截图对比
test('登录页视觉回归', async ({ page }) => {
await page.goto('/login');
await page.waitForSelector('[data-testid="login-form"]');
await expect(page).toHaveScreenshot('login-page.png', {
maxDiffPixels: 100,
threshold: 0.2,
});
});
组件级视觉回归
如果你想测某个组件在不同状态下的样子:
test('按钮各状态截图', async ({ page }) => {
await page.goto('/button-demos');
await page.click('[data-testid="hover-trigger"]');
await expect(page.locator('[data-testid="button-primary"]')).toHaveScreenshot('button-hover.png');
await page.click('[data-testid="focus-trigger"]');
await expect(page.locator('[data-testid="button-primary"]')).toHaveScreenshot('button-focus.png');
await page.click('[data-testid="loading-trigger"]');
await expect(page.locator('[data-testid="button-primary"]')).toHaveScreenshot('button-loading.png');
});
让视觉回归更稳定
视觉回归最怕 flaky。几个工程化技巧:
- 固定字体加载:截图前确保字体已加载。
await page.waitForFunction(() => document.fonts.ready); - 屏蔽动态内容:时间、随机数、动画元素用 CSS 隐藏或 mock。
await page.addStyleTag({ content: '[data-testid="current-time"] { visibility: hidden !important; }' }); - 统一 viewport 和缩放:在
playwright.config.ts里统一设备配置。 - 动画禁用:
await page.addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' });
响应式测试
AI 生成的页面常常在桌面端好看,移动端就崩。Playwright 可以很方便地多分辨率截图:
test.describe('响应式首页', () => {
test.use({ viewport: { width: 375, height: 667 } });
test('移动端', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
});
test.describe('响应式首页桌面端', () => {
test.use({ viewport: { width: 1440, height: 900 } });
test('桌面端', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-desktop.png');
});
});
无障碍测试
AI 生成的代码容易忽略 alt、aria-label、对比度等。用 axe-core 跑一遍:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('首页无严重无障碍问题', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
让 AI 参与测试工作流
E2E 不只是写测试,还可以让 AI 帮你生成和维护测试。
1. 从用户故事生成测试
把 PRD 或用户故事喂给 AI:
用户搜索商品,选择第一个结果加入购物车,进入结算页填写邮箱,提交订单后看到成功提示。
AI 输出 Playwright 测试代码,你只需微调选择器。
2. 从 Bug 报告生成回归测试
遇到一个 bug 后,先让 AI 写一个能复现它的 E2E 测试。修完 bug 再跑这个测试,确保不会回归。
3. 自动分析失败原因
Playwright 失败时会生成 trace。你可以把 trace 截图或错误日志丢给 AI,让它给出修复建议,比如:
- 某个选择器不稳定,建议加
data-testid - 页面还没加载完,建议加
waitForSelector - 某个 API 响应慢,建议 mock
4. AI 视觉验收闭环
结合 Chrome MCP / Playwright,可以建立这样的 workflow:
AI 生成代码
↓
Playwright 跑 E2E + 截图
↓
AI 对比截图 vs Figma 设计稿
↓
AI 提出样式修复建议
↓
AI 再次生成代码
↓
重新跑 E2E
这个闭环能大幅减少人工 Design QA 的工作量。
推荐的目录结构
e2e/
├── fixtures/
│ ├── users.ts
│ └── products.ts
├── page-objects/
│ ├── LoginPage.ts
│ ├── ProductPage.ts
│ └── CheckoutPage.ts
├── specs/
│ ├── auth.spec.ts
│ ├── product.spec.ts
│ ├── checkout.spec.ts
│ └── visual/
│ ├── homepage.visual.spec.ts
│ └── login.visual.spec.ts
├── utils/
│ ├── test-helpers.ts
│ └── mock-api.ts
└── snapshots/
└── baseline/
Page Object 模式很重要。把选择器封装到 Page Object 里,AI 改了 DOM 结构时,只需要改一处:
// e2e/page-objects/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('[data-testid="email-input"]', email);
await this.page.fill('[data-testid="password-input"]', password);
await this.page.click('[data-testid="login-button"]');
}
}
CI/CD 集成
在 GitHub Actions 里跑:
name: E2E Tests
on:
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
test-results/
视觉回归基准图管理
视觉回归最怕 baseline 不同步。建议:
- 本地开发改 UI 时,跑
npx playwright test --update-snapshots更新自己负责的快照。 - PR 里如果改了 UI,必须在 PR 描述里说明更新了哪些截图。
- CI 里不允许自动更新快照,防止 silently 通过视觉回归。
给 AI 的 Rules 建议
把 E2E 要求写进 .cursorrules 或 .cursor/rules/*.mdc:
## E2E 规范
- 新增页面或核心交互后,必须在 `e2e/specs/` 下补充对应测试
- 优先使用 Playwright 和 `data-testid` 选择器,避免依赖 CSS 类名
- 每个核心流程测试必须覆盖:正常流程、空状态、错误状态
- 视觉改动必须更新 Playwright baseline 截图
- 提交前运行 `npm run test:e2e`,确保通过
- 不要写死等待时间,优先用 `waitForSelector` / `toBeVisible` 等断言
总结
E2E 测试对 AI 生成的前端有三层价值:
- 功能兜底:核心流程不崩溃、边界状态有处理。
- 视觉验收:每次改动自动截图对比,防止 UI 漂移。
- 回归保险:后续 AI 迭代时,不会把已有的功能改坏。
它和 design token、组件库、TypeScript、rules 组合起来,才能把「AI 生成前端」从 demo 变成可维护、可上线的产品。