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 句

  1. 拍小區塊比拍整頁穩
  2. 動態元素 mask、動畫 disable
  3. 同 docker 跑 = 跨平台一致性
  4. 新案先 Playwright snapshot、長大再 Percy
  5. 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