让 agent 改样式,最常见的翻车不是接口报错,而是「功能没坏、页面坏了」:按钮被挤到屏幕外、中文标题撑破卡片、暗色模式下的灰字对比度掉到看不见。单元测试看不见这些,人工点一遍又容易漏。这个教程用 Playwright 的三张固定截图和像素阈值,把视觉回归做成一条命令,跑在合并之前。
先定要守的三张图
不要一上来给全站截图。先选三条最贵的路径:首页、列表页、详情页。固定三件事——视口 1440×900、deviceScaleFactor 为 1、关闭动画;数据用固定 fixture 或固定 seed,别让「今天有几条内容」影响截图。这样基线的差异才只来自样式。
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/visual',
use: {
baseURL: 'http://localhost:3000',
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
reducedMotion: 'reduce',
},
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.01,
animations: 'disabled',
},
},
});写第一个截图测试
截图前等两件事:字体加载完成、网络空闲。中文字体晚到几百毫秒,截图就会整体位移,这种失败最容易被误判成「样式改坏了」。
// tests/visual/pages.spec.ts
import { test, expect } from '@playwright/test';
const pages = [
{ name: 'home', path: '/' },
{ name: 'articles', path: '/articles.html' },
{ name: 'detail', path: '/article.html?id=aa545' },
];
for (const p of pages) {
test(`${p.name} 视觉基线`, async ({ page }) => {
await page.goto(p.path, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot(`${p.name}.png`, {
fullPage: true,
mask: [page.locator('[data-visual-dynamic]')],
});
});
}生成基线,但不要闭眼更新
第一次运行会生成基线图,放在 tests/visual/pages.spec.ts-snapshots/ 下,和代码一起提交。之后每次失败,Playwright 会输出 actual、expected、diff 三张图;先看 diff,再决定是修代码还是更新基线。把 --update-snapshots 当成「确认这次变化是对的」,不是「让测试变绿」。
npx playwright install chromium
npx playwright test # 首次生成基线
npx playwright test --update-snapshots # 只在确认变化正确时用
npx playwright show-report # 看 actual / expected / diff阈值和遮罩怎么定
maxDiffPixelRatio 建议从 0.01 起步:它容忍抗锯齿和字体渲染的细微差异,但挡得住布局位移。时间、广告位、随机推荐、用户头像这类每次都变的内容,用 data-visual-dynamic 标记后加进 mask;不要用 CSS 隐藏它们,否则你测的就不是真实页面。
放进 CI
CI 里最关键的是环境一致:同一个浏览器版本、同一套字体、同一个视口。用 Playwright 官方镜像或 --with-deps 安装依赖,把报告作为 artifact 上传,失败时能直接点开 diff。
# .github/workflows/visual.yml
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/三个常见坑
- 字体没加载完就截图:先 await document.fonts.ready,再等 networkidle。
- 动画和轮播导致随机差异:配置 reducedMotion: 'reduce',并在测试里禁用 CSS animation。
- 基线图太大、review 不动:只截三条关键路径,必要时用 clip 截关键区域,不要全站铺开。
和 agent 的工作流怎么接
把「跑视觉测试」写进 agent 的完成条件,但不要把「更新基线」交给它自动做。正确顺序是:agent 改代码 → 跑测试 → 失败时把 diff 图交给人 → 人确认是预期变化 → 再让 agent 执行 --update-snapshots。这样基线才是人审过的,而不是模型自己给自己盖章。
视觉回归不是让截图永远不变,而是让每一次变化都有人看过。