---
title: Visual Regression Testing — Percy / Chromatic / Playwright Snapshot 完整對比
description: Visual regression 測試完整指南。Pixel diff vs DOM diff、Playwright snapshot 免費版、Percy / Applitools / Chromatic 比較、AI 視覺比對、跨平台一致性、CI 整合。
category: automation
tags: [visual-regression, snapshot, percy, chromatic, applitools]
date: 2026-06-17
faq:
  - q: Playwright 內建 snapshot 夠用嗎？
    a: 小團隊 / 起步階段夠。跨瀏覽器穩定度、視覺比對 UI、approval workflow 不如 Percy/Chromatic。100 個 case 以下用 Playwright、上去後考慮付費。
  - q: 視覺迴歸常 flaky 怎辦？
    a: "用 mask 動態元素（時間 / 動畫 / 隨機 ID）、設 threshold（0.1% 容忍）、跑同瀏覽器同尺寸。CSS animation 用 `animations: 'disabled'` 關掉。"
  - q: AI 視覺比對（Applitools）跟 pixel diff 差別？
    a: Pixel diff = 嚴格、容易 false positive。AI 比對 = 容忍語意相同的小差異（字微調但版面對）。AI 強但貴、新團隊先 pixel + threshold。
  - q: Storybook 該整合視覺測試嗎？
    a: 強烈建議。Chromatic 跟 Storybook 同公司、整合最佳。Component 層級的視覺穩定 → 上層 E2E 也穩。
---

# Visual Regression Testing — 完整對比

「UI 改 padding 改死人」、「按鈕從 red 變 dark-red 沒人發現」 — 這些功能性測試抓不到的、Visual Regression 抓得到。這篇給你完整工具地圖 + 實戰 setup。

## 為什麼功能測試不夠

```mermaid
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 工作流

```mermaid
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 — 起步首選

### 基本用法

```typescript
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）

```typescript
export default defineConfig({
  use: {
    // 關掉動畫避免 flaky
    actionTimeout: 0,
  },
  expect: {
    toHaveScreenshot: {
      // 允許 0.1% 像素差異（防 anti-aliasing flaky）
      maxDiffPixelRatio: 0.001,
      // 動畫 disabled
      animations: 'disabled',
      // 截圖時隱藏 cursor
      caret: 'hide',
    },
  },
});
```

### 進階：mask 動態元素

```typescript
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'),
    ],
  });
});
```

### 跨裝置

```typescript
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

```bash
npx playwright test --update-snapshots
```

**code review 時帶上 baseline diff 截圖**、reviewer 才看得出變化是預期還是 bug。

## Percy — 企業選擇

### Setup

```bash
npm install --save-dev @percy/cli @percy/playwright
```

```typescript
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';

test('homepage', async ({ page }) => {
  await page.goto('https://example.com');
  await percySnapshot(page, 'homepage');
});
```

跑：

```bash
PERCY_TOKEN=xxx npx percy exec -- npx playwright test
```

### Percy 強項

```mermaid
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 神配

```bash
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 倍**。但貴。

```typescript
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

```yaml
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 招

```mermaid
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
```

## 反模式

```mermaid
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 實戰](/automation/page-object-model.html)
- [Cross-browser Testing 策略](/automation/cross-browser-testing.html)
- [Accessibility (a11y) Testing](/manual/accessibility-testing.html)
