visual-regression-gen
⚙️ 執行前先讀
modules/config-loader.md。 啟用條件:config.platforms.web.enabled = true且config.visual_regression.enabled = true。
適用場景
- ✅ Web App 有重要的視覺品質要求(品牌頁 / Marketing site / Dashboard)
- ✅ 已有 E2E 但沒有「視覺改變」守門
- ✅ 想在 PR 看到 "視覺 diff" 自動標出
- ✅ 多瀏覽器要對齊(Chrome / Safari 渲染差異)
不適用場景
- ❌ 純 API 後端 — 用
tc-to-pytest - ❌ UI 變動頻繁的早期 MVP — baseline 維護成本高
- ❌ 大量動態內容(隨機推薦 / 廣告) — 即使 mask 也 flaky
工具選擇
| 工具 | 適合 | 雲服務 | 成本 |
|---|---|---|---|
| Playwright snapshot(推薦) | 自託管、開源、最簡 | ❌ self-host | 免費 |
| Percy | 雲端 baseline 管理、PR 整合 | ✅ | $$ |
| Chromatic | Storybook 元件、視覺 review workflow | ✅ | $$ |
| BackstopJS | Legacy 專案、需要 hot reload baseline | ❌ self-host | 免費 |
預設 {{VR_TOOL}}(從 config 讀),可用 --tool= 覆寫。
執行流程
Phase 1: 偵測既有設定
# 找配置檔
ls playwright.config.* .percy.* .chromatic.* backstop.json 2>/dev/null
若已有對應配置 → 直接擴充,不從零建。
Phase 2: 識別關鍵 viewport
從 config.platforms.web.viewport_sizes 讀(預設 desktop / tablet / mobile):
| Viewport | 用途 |
|---|---|
| 1920×1080 desktop | 主流桌機 |
| 1366×768 desktop | 老舊 / 低解析度 |
| 768×1024 tablet | iPad 直 |
| 375×667 mobile | iPhone SE/小螢幕 |
Phase 3: 動態元素 masking 策略
自動偵測該 mask 掉的元素(避免 false-positive flaky):
| 類型 | locator | 處理 |
|---|---|---|
| 時間戳 | [data-testid*="time"] / time |
mask |
| 動畫 GIF / video | img[src*=".gif"], video |
mask |
| 廣告 | iframe[src*="ads"], [class*="ad-banner"] |
mask |
| 即時數字(unread count) | [data-testid*="badge"] |
mask |
| 隨機推薦 | [data-testid*="recommendation"] |
mask |
讓使用者覆核,可自加自定義 mask。
Phase 4: 生成腳本
Playwright snapshot 範例
// tests/visual/home.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Home page visual regression', () => {
for (const viewport of [
{ name: 'desktop', width: 1920, height: 1080 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'mobile', width: 375, height: 667 },
]) {
test(`TC: VR-HOME-001 home page @ ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/');
await page.waitForLoadState('networkidle');
// Mask 動態元素
await expect(page).toHaveScreenshot(`home-${viewport.name}.png`, {
mask: [
page.locator('[data-testid="current-time"]'),
page.locator('iframe[src*="ads"]'),
page.locator('[class*="ad-banner"]'),
],
maxDiffPixelRatio: 0.02, // 2% 容忍
animations: 'disabled', // 凍結動畫
});
});
}
});
Percy 範例
const percySnapshot = require('@percy/playwright');
test('TC: VR-HOME-001 home page', async ({ page }) => {
await page.goto('/');
await percySnapshot(page, 'Home page', {
widths: [1920, 1366, 768, 375],
percyCSS: `
[data-testid="current-time"] { visibility: hidden; }
iframe[src*="ads"] { display: none; }
`,
});
});
Chromatic(Storybook 整合)範例
// Button.stories.tsx
export const Primary = {
args: { variant: 'primary', children: 'Click me' },
parameters: { chromatic: { viewports: [320, 768, 1200] } },
};
Phase 5: Baseline 管理
| 工具 | Baseline 存哪 |
|---|---|
| Playwright snapshot | git 內 tests/visual/__screenshots__/ |
| Percy | Percy cloud |
| Chromatic | Chromatic cloud |
| BackstopJS | backstop_data/ (git 或 S3) |
git 內 baseline 注意:
- ✅ Commit 進 git(小團隊好做 diff review)
- ⚠️ 大團隊建議 Git LFS(baseline 累積會肥)
- ❌ 不該 in CI cache(每次 CI re-generate 失去 baseline 意義)
Phase 6: 處理 false positive
當 PR 改動觸發視覺 diff 但實際 UI 正確:
- 誤判 → 加 mask / 提高
maxDiffPixelRatio - 真的改了 UI →
npx playwright test --update-snapshots更新 baseline,commit 進 PR
Phase 7: CI 整合
# .github/workflows/visual-regression.yml
on:
pull_request:
paths: ['src/**', 'public/**', 'styles/**']
jobs:
visual-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npx playwright install
- run: npx playwright test --grep @visual
- uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-diff
path: test-results/
PR 內若有 diff,自動上傳 diff 影像。
⚠️ 安全護欄
- ❌ 不主動
--update-snapshots(需要使用者明確同意) - ✅ Diff > threshold → CI fail(守門)
- ✅ Baseline commit 必須通過 review
- ⚠️ baseline 規模 > 50 個 / 100 MB → 提示用 Git LFS
設定依賴
| 設定 Key | 用途 | 缺值時行為 |
|---|---|---|
visual_regression.enabled |
啟用此 skill | skill 不啟用 |
visual_regression.tool |
預設工具 | playwright |
visual_regression.max_diff_ratio |
預設容忍度 | 0.02 (2%) |
platforms.web.viewport_sizes |
跑哪些 viewport | 3 個預設 |
範例
詳見 examples.md