結論先講

視覺回歸測試 = 截圖比對。PR 前截一張,PR 後截一張,pixel by pixel 比較差異。 它能抓到所有其他測試抓不到的問題——CSS 排版跑掉、字體消失、顏色錯誤、元件重疊。但它也是最容易 flaky 的測試類型,導入前要想清楚。

真實場景:同事改了一個全域的 margin-bottom utility class,影響到 40 幾個頁面。功能測試全部通過,但客戶回報「結帳頁的按鈕怎麼被擋住了」。如果有視覺回歸測試,PR 階段就能看到 40 個頁面的截圖變化。


視覺回歸測試的原理

1. 第一次跑:截圖存為 baseline
2. 之後每次跑:截新圖,跟 baseline 比較
3. 如果有差異:標示出不同的 pixel
4. 人工判斷:是 bug 還是預期的變更
5. 如果是預期的:更新 baseline

這聽起來很簡單,但魔鬼在細節裡。


工具比較

功能PercyChromaticPlaywrightBackstopJS
類型SaaSSaaSOSSOSS
費用免費 5000 截圖/月免費 5000 截圖/月免費免費
跨瀏覽器Chrome/Firefox/SafariChrome可設定Chrome/Firefox
Storybook 整合原生(同公司)
CI 整合GitHub/GitLab/等GitHub/GitLab/等任何 CI任何 CI
審核 UI網頁介面,很好用網頁介面,很好用無(只有截圖檔)HTML 報告
防 flaky智慧比對智慧比對需自己處理threshold 設定
適合場景任何專案Storybook 專案已用 Playwright 的專案預算有限的專案

Playwright 視覺測試

如果你的專案已經在用 Playwright,加上視覺回歸測試幾乎是零成本:

// tests/visual/homepage.spec.ts
import { test, expect } from '@playwright/test';
 
test('首頁視覺一致', async ({ page }) => {
  await page.goto('http://localhost:3000');
  // 等待所有圖片載入
  await page.waitForLoadState('networkidle');
  // 截圖比對
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.01,  // 允許 1% 的 pixel 差異
  });
});
 
test('商品卡片元件', async ({ page }) => {
  await page.goto('http://localhost:3000/products');
  const card = page.locator('[data-testid="product-card"]').first();
  await expect(card).toHaveScreenshot('product-card.png');
});
 
test('RWD - 手機版', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('http://localhost:3000');
  await expect(page).toHaveScreenshot('homepage-mobile.png', {
    fullPage: true,
  });
});

處理動態內容

視覺測試最大的敵人是動態內容——日期、隨機圖片、動畫:

test('忽略動態區域', async ({ page }) => {
  await page.goto('http://localhost:3000');
 
  // 方法 1:隱藏特定元素
  await page.evaluate(() => {
    // 隱藏時間戳記和廣告
    document.querySelectorAll('.timestamp, .ad-banner').forEach(el => {
      (el as HTMLElement).style.visibility = 'hidden';
    });
  });
 
  // 方法 2:用 mask 遮蓋
  await expect(page).toHaveScreenshot('homepage-masked.png', {
    mask: [
      page.locator('.dynamic-carousel'),
      page.locator('.user-avatar'),
    ],
  });
});
 
test('等動畫結束再截圖', async ({ page }) => {
  await page.goto('http://localhost:3000');
  // 停用所有動畫
  await page.addStyleTag({
    content: `
      *, *::before, *::after {
        animation-duration: 0s !important;
        transition-duration: 0s !important;
      }
    `,
  });
  await expect(page).toHaveScreenshot('no-animation.png');
});

更新 Baseline

# 第一次跑或需要更新 baseline 時
npx playwright test --update-snapshots
 
# 只更新特定測試的 snapshot
npx playwright test tests/visual/homepage.spec.ts --update-snapshots

Percy 整合

Percy 的優勢是智慧比對(不會因為 anti-aliasing 差異而誤報)和漂亮的審核 UI:

// 安裝:npm install @percy/playwright @percy/cli
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';
 
test('Percy 截圖', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await percySnapshot(page, 'Homepage');
 
  await page.goto('http://localhost:3000/products');
  await percySnapshot(page, 'Product List');
 
  await page.goto('http://localhost:3000/checkout');
  await percySnapshot(page, 'Checkout Page', {
    widths: [375, 768, 1280],  // 同時測三種螢幕寬度
  });
});
# CI 整合
# .github/workflows/visual.yml
visual-test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - run: npm ci
    - run: npm run build && npm run start &
    - run: npx percy exec -- npx playwright test tests/visual/
      env:
        PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

Percy 會在 PR 上留 comment,展示所有截圖差異,reviewers 可以直接在 Percy 的介面上 approve 或 reject。


Chromatic + Storybook

如果你的專案用 Storybook 管理元件,Chromatic 是最無痛的選擇(Chromatic 就是 Storybook 的公司做的):

# 安裝
npm install -D chromatic
 
# 跑視覺測試(會自動截所有 story 的圖)
npx chromatic --project-token=YOUR_TOKEN

就這樣。它會自動對每個 Story 截圖,跟上一次的 baseline 比對。不需要額外寫任何測試程式碼。

# CI
chromatic:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0  # Chromatic 需要完整 git history
    - run: npm ci
    - uses: chromaui/action@latest
      with:
        projectToken: ${{ secrets.CHROMATIC_TOKEN }}

什麼時候值得做

值得做

  • 設計系統 / 元件庫——元件被很多頁面使用,改一個影響全部
  • Marketing 頁面——設計稿要 pixel perfect,不能跑版
  • 跨瀏覽器支援——同一個頁面在不同瀏覽器要一致
  • CSS 重構——用視覺測試確保重構前後一樣

不值得做

  • 快速迭代的早期產品——UI 天天在改,baseline 天天要更新
  • 後台管理介面——沒人在意它長什麼樣,功能對就好
  • 資料密集型頁面——內容每次不同,截圖也每次不同
  • 團隊沒有 design review 文化——截圖差異沒人看,等於白做

處理 Flaky Visual Tests

視覺測試是所有測試類型中最容易 flaky 的,因為太多因素會影響截圖結果:

問題解法
字體渲染差異(OS 不同)CI 統一用 Docker,或設定 threshold
動畫 / 過場效果CSS 停用動畫,或等動畫完成
隨機內容(廣告、推薦)mask 遮蓋或隱藏
圖片載入時機waitForLoadState('networkidle')
anti-aliasing 差異提高 maxDiffPixelRatio
時間相關顯示固定系統時間或隱藏
Web font 載入等 font 載入完成再截圖
// 完整的防 flaky 設定
test('穩定的視覺測試', async ({ page }) => {
  // 固定時間
  await page.clock.setFixedTime(new Date('2026-01-01T12:00:00'));
 
  // 停用動畫
  await page.addStyleTag({
    content: '*, *::before, *::after { animation: none !important; transition: none !important; }',
  });
 
  await page.goto('http://localhost:3000');
 
  // 等所有資源載入
  await page.waitForLoadState('networkidle');
 
  // 等字體載入
  await page.evaluate(() => document.fonts.ready);
 
  // 截圖,允許小量差異
  await expect(page).toHaveScreenshot('stable-test.png', {
    maxDiffPixelRatio: 0.005,
    mask: [page.locator('.ad-slot'), page.locator('.random-recommendation')],
  });
});

常見問題

視覺回歸測試能取代人眼 review 嗎?

不能。它能抓到「跟之前不一樣」,但不能判斷「這樣好不好看」。它是輔助工具,讓 reviewer 不需要在每個頁面上人工檢查,而是看差異報告就好。

截圖存在 Git 裡嗎?

看工具。Playwright 存在 repo 裡(__snapshots__ 目錄),會佔空間但離線可用。Percy 和 Chromatic 存在雲端,不佔 repo 空間。如果截圖很多(>100),建議用 SaaS 方案。

免費方案夠用嗎?

Percy 和 Chromatic 都有每月 5000 次免費截圖。小專案夠用。如果超過,BackstopJS 完全免費,但要自己處理 flaky 問題和審核流程。

跟 E2E 測試有什麼不同?

E2E 測試驗功能(點按鈕後頁面跳轉了嗎?),視覺測試驗外觀(頁面長得跟之前一樣嗎?)。一個頁面可以功能正常但排版爆掉,也可以排版正常但功能壞掉。兩個抓不同的問題。

本系列文章

  1. 測試策略(一):測試金字塔
  2. 測試策略(二):Unit vs Integration
  3. 測試策略(三):E2E + 壓力測試
  4. CD 整合
  5. API 契約測試
  6. 安全測試
  7. Smoke + 回歸測試
  8. 測試資料管理
  9. 視覺回歸測試(本篇)
  10. AI 輔助測試
  11. ISO 29119)
  12. 測試相關憑證與學習路徑