結論先講
視覺回歸測試 = 截圖比對。PR 前截一張,PR 後截一張,pixel by pixel 比較差異。 它能抓到所有其他測試抓不到的問題——CSS 排版跑掉、字體消失、顏色錯誤、元件重疊。但它也是最容易 flaky 的測試類型,導入前要想清楚。
真實場景:同事改了一個全域的
margin-bottomutility class,影響到 40 幾個頁面。功能測試全部通過,但客戶回報「結帳頁的按鈕怎麼被擋住了」。如果有視覺回歸測試,PR 階段就能看到 40 個頁面的截圖變化。
視覺回歸測試的原理
1. 第一次跑:截圖存為 baseline
2. 之後每次跑:截新圖,跟 baseline 比較
3. 如果有差異:標示出不同的 pixel
4. 人工判斷:是 bug 還是預期的變更
5. 如果是預期的:更新 baseline
這聽起來很簡單,但魔鬼在細節裡。
工具比較
| 功能 | Percy | Chromatic | Playwright | BackstopJS |
|---|---|---|---|---|
| 類型 | SaaS | SaaS | OSS | OSS |
| 費用 | 免費 5000 截圖/月 | 免費 5000 截圖/月 | 免費 | 免費 |
| 跨瀏覽器 | Chrome/Firefox/Safari | Chrome | 可設定 | 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-snapshotsPercy 整合
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 測試驗功能(點按鈕後頁面跳轉了嗎?),視覺測試驗外觀(頁面長得跟之前一樣嗎?)。一個頁面可以功能正常但排版爆掉,也可以排版正常但功能壞掉。兩個抓不同的問題。