Visual Regression Testing — 完整對比
「UI 改 padding 改死人」、「按鈕從 red 變 dark-red 沒人發現」 — 這些功能性測試抓不到的、Visual Regression 抓得到。這篇給你完整工具地圖 + 實戰 setup。
為什麼功能測試不夠
flowchart LR
F[功能測試] -->|抓| F1["按鈕能點"]
F -->|抓不到| F2["按鈕變小變色"]
F -->|抓不到| F3["排版跑掉"]
F -->|抓不到| F4["字體換錯"]
F -->|抓不到| F5["圖片消失"]
F -->|抓不到| F6["RWD 在 iPad 爆"]
V[Visual Regression] -->|抓| V1[像素級變化]
V -->|抓| V2[版面位移]
V -->|抓| V3[顏色 / 字體]
style F2 fill:#ef4444,color:#fff
style F3 fill:#ef4444,color:#fff
style F4 fill:#ef4444,color:#fff
style V1 fill:#10b981,color:#fff
Visual Regression 工作流
flowchart LR
PR[Dev push PR] --> Build[Build 新版]
Build --> Snap[拍 N 張 screenshot]
Snap --> Diff{對比 baseline}
Diff -->|無變化| Pass[✓ Pass]
Diff -->|有變化| Review[人類 review]
Review -->|預期內| Accept[更新 baseline]
Review -->|是 bug| Reject[退回 PR]
style Pass fill:#10b981,color:#fff
style Review fill:#a855f7,color:#fff
style Reject fill:#ef4444,color:#fff
工具對比
| 工具 | 起跳價 | 強項 | 弱項 |
|---|---|---|---|
| Playwright Snapshot | 免費 | 內建、簡單 | 沒 UI / approval workflow |
| Percy | $39/月 | BrowserStack 整合、UI 強 | 貴 |
| Chromatic | 免費起跳 | Storybook 親兒子 | 限 Storybook 友善 |
| Applitools | $1500/年 | AI 視覺、跨平台 | 企業向、起價高 |
| BackstopJS | 免費 | Open source、CLI | 自架、UX 普通 |
| Loki | 免費 | Storybook 整合 | 維護慢 |
Playwright Snapshot — 起步首選
基本用法
import { test, expect } from '@playwright/test';
test('homepage looks right', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png');
});
第一次跑 → 建立 baseline homepage.png。
之後跑 → 比對 baseline、有差 → fail。
配置(playwright.config.ts)
export default defineConfig({
use: {
// 關掉動畫避免 flaky
actionTimeout: 0,
},
expect: {
toHaveScreenshot: {
// 允許 0.1% 像素差異(防 anti-aliasing flaky)
maxDiffPixelRatio: 0.001,
// 動畫 disabled
animations: 'disabled',
// 截圖時隱藏 cursor
caret: 'hide',
},
},
});
進階:mask 動態元素
test('avoid time / random masking', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.locator('.timestamp'),
page.locator('.random-banner'),
page.locator('time'),
],
});
});
跨裝置
test.describe('responsive', () => {
for (const device of ['Desktop Chrome', 'iPhone 13', 'iPad Pro']) {
test(`looks right on ${device}`, async ({ browser }) => {
const context = await browser.newContext({ ...devices[device] });
const page = await context.newPage();
await page.goto('/');
await expect(page).toHaveScreenshot(`home-${device}.png`);
});
}
});
更新 baseline
npx playwright test --update-snapshots
code review 時帶上 baseline diff 截圖、reviewer 才看得出變化是預期還是 bug。
Percy — 企業選擇
Setup
npm install --save-dev @percy/cli @percy/playwright
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';
test('homepage', async ({ page }) => {
await page.goto('https://example.com');
await percySnapshot(page, 'homepage');
});
跑:
PERCY_TOKEN=xxx npx percy exec -- npx playwright test
Percy 強項
flowchart LR
Pcy[Percy] --> S1[Web UI 看 diff]
Pcy --> S2[一鍵 approve / reject]
Pcy --> S3[並列前後對比]
Pcy --> S4[Slack / GitHub 通知]
Pcy --> S5[跨瀏覽器 baseline]
Pcy --> S6[Responsive width 一次拍多寬]
style Pcy fill:#a855f7,color:#fff
Chromatic — Storybook 神配
npm install --save-dev chromatic
npx chromatic --project-token=xxx
Chromatic 抓你所有 Storybook stories、自動拍 + 比對。Component 層級的視覺穩定 → 整個 UI 穩。
優勢: - Component-level snapshot(粒度比 page 細) - 設計師 friendly UI - 跟 Figma 對齊
Applitools — AI 視覺比對
不是 pixel diff、是 「語意一致」比對: - 字體大小改 1px → AI 知道是同字體 - 顏色 hex 改 1 號 → AI 知道是同顏色 - 版面微調 → AI 容忍
減少 false positive 5-10 倍。但貴。
import { Eyes } from '@applitools/eyes-playwright';
test('with Applitools', async ({ page }) => {
const eyes = new Eyes();
await eyes.open(page, 'My App', 'Homepage Test');
await page.goto('/');
await eyes.check('Homepage');
await eyes.close();
});
CI 整合範例
Playwright + GitHub Actions
name: Visual Tests
on: [pull_request]
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --grep @visual
- if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diff
path: test-results/
PR 中失敗 → 下載 artifact 看 diff PNG。
反 flaky 5 招
flowchart TD
Flaky[Visual flaky 來源] --> F1[字體 anti-aliasing]
Flaky --> F2[動畫]
Flaky --> F3[時間 / 隨機資料]
Flaky --> F4[網路 timing]
Flaky --> F5[跨 OS 字體差]
F1 --> S1[maxDiffPixelRatio: 0.001]
F2 --> S2["animations: 'disabled'"]
F3 --> S3[mask 動態元素]
F4 --> S4[等狀態再截]
F5 --> S5[同 docker / 同 OS 跑]
style S1 fill:#10b981,color:#fff
style S2 fill:#10b981,color:#fff
style S3 fill:#10b981,color:#fff
反模式
flowchart TD
Anti[Visual 反模式] --> A1["拍整頁 → diff 太多沒人看"]
Anti --> A2["不關動畫"]
Anti --> A3["跨 OS 跑(字體差)"]
Anti --> A4["每 PR 強制 0 diff(變 flaky 之源)"]
Anti --> A5["沒 mask 時間 / 動態"]
Anti --> A6["跑 prod data"]
style A1 fill:#ef4444,color:#fff
style A2 fill:#ef4444,color:#fff
style A3 fill:#ef4444,color:#fff
style A4 fill:#ef4444,color:#fff
style A5 fill:#ef4444,color:#fff
style A6 fill:#ef4444,color:#fff
給 QA 的 5 句
- 拍小區塊比拍整頁穩
- 動態元素 mask、動畫 disable
- 同 docker 跑 = 跨平台一致性
- 新案先 Playwright snapshot、長大再 Percy
- Code review 時帶 diff 圖、reviewer 才能判斷
最後
Visual Regression 是 「功能測試抓不到、但使用者一眼看出來」的最後防線。從 Playwright 內建 snapshot 起步、設好 threshold + mask、CI 自動跑、PR 帶 diff — 半年後 UI 退步 bug 砍 80%。
延伸: - Page Object Model 實戰 - Cross-browser Testing 策略 - Accessibility (a11y) Testing