PlaywrightVisual Regression TestingE2ECI/CDTesting

Playwright 視覺回歸測試教學:toHaveScreenshot 實務指南

用 Playwright toHaveScreenshot 落地視覺回歸測試:baseline 管理、mask/stylePath 降噪與 CI PR diff 審查流程一次掌握。

· 5 分鐘閱讀

這篇會帶你把 Playwright 視覺回歸測試落地到真實專案,從 baseline 策略到 CI/PR 流程一次建好。


前言

很多團隊其實已經有 E2E 測試,但 UI 回歸還是常常在上線後才被發現。原因很簡單:functional test 通常只能驗證「流程走得通」,卻不會告訴你「畫面是不是壞了」。

例如 checkout 流程全部綠燈,不代表按鈕樣式、版面間距、字體 fallback、或某個 locale 下的排版沒有跑掉。這些問題常常不是 JavaScript error,而是視覺層的小偏移,卻直接影響轉換率與使用者信任。

Playwright 的 expect(page).toHaveScreenshot() 提供一個很實際的 guardrail:用 baseline 比對把變動顯性化,讓 reviewer 在 PR 階段就能看到「這次改動到底影響了哪些畫面」。

為什麼單靠 Functional Test 不夠

功能測試擅長驗證「事件有沒有發生」,視覺回歸測試擅長驗證「結果看起來對不對」。兩者不是替代關係,而是互補關係。

常見缺口:

  • CSS token 被改動,流程還能點,但按鈕對比度或 spacing 已經不符合設計規範。
  • 第三方元件升版後 DOM 結構改變,導致某些 breakpoint 下 layout 崩壞。
  • 動畫、時間、隨機資料導致頁面不穩定,肉眼很難在 PR 中完整檢查。

所以在測試策略上,functional test 負責「能不能用」,visual test 負責「看起來是不是還對」。

toHaveScreenshot() 的正確心智模型

先掌握三個重點:

  1. toHaveScreenshot() 是 Playwright Test 內建能力,不需要額外安裝 pixelmatchcanvas
  2. 第一次跑測試時會建立 baseline,後續執行才做比對。
  3. Playwright 會先等到兩次連續截圖一致,再拿去和 baseline 比,降低瞬間閃動造成的誤報。

以下是 page 等級的基礎範例:

import { test, expect } from '@playwright/test';

test('product list visual regression', async ({ page }) => {
  await page.goto('/products');

  await expect(page).toHaveScreenshot('products-page.png', {
    fullPage: true,
    maxDiffPixels: 120, // 允許最多 120 個像素差異
    maxDiffPixelRatio: 0.001, // 允許 0.1% 像素差異
    threshold: 0.2, // YIQ 色差容忍度,0 越嚴格,1 越寬鬆
  });
});

如果你只關心某個關鍵元件,建議用 locator screenshot,噪音更低:

import { test, expect } from '@playwright/test';

test('checkout summary card visual regression', async ({ page }) => {
  await page.goto('/checkout');

  const summaryCard = page.getByTestId('checkout-summary');
  await expect(summaryCard).toHaveScreenshot('checkout-summary.png', {
    maxDiffPixels: 40,
    threshold: 0.2,
  });
});

Baseline 與 Snapshot 管理策略

預設情況下,example.spec.ts 的快照會放在 example.spec.ts-snapshots 目錄。請把這些 baseline 納入版本控制,因為它們就是你團隊共同認可的 UI 參考線。

更新 baseline 時,請只在「預期的 UI 變更」下執行:

# 全量更新(通常在明確 UI 改版時才用)
npx playwright test --update-snapshots

# 只更新單一 spec,降低誤更新風險
npx playwright test tests/visual/checkout.spec.ts --update-snapshots

若要客製儲存路徑,再使用 snapshotPathTemplateexpect.toHaveScreenshot.pathTemplate

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  snapshotPathTemplate:
    '{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}',
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 80,
      threshold: 0.2,
    },
  },
});

動態內容穩定化:先降噪,再談門檻

視覺測試最常失敗,不是因為 UI 真的壞,而是測試環境太「會抖」。建議先做 deterministic 控制,再調整 diff 門檻。

1. 用 stylePath 去除易變動區塊

/* tests/visual/screenshot.css */
[data-test-clock],
[data-test-rotating-banner] {
  visibility: hidden !important;
}
import { defineConfig } from '@playwright/test';

export default defineConfig({
  expect: {
    toHaveScreenshot: {
      stylePath: './tests/visual/screenshot.css',
      animations: 'disabled', // 官方支援做法,停用動畫干擾
    },
  },
});

2. 對特定元素使用 mask

await expect(page).toHaveScreenshot('home.png', {
  mask: [
    page.getByTestId('ad-slot'),
    page.getByTestId('live-visitors-counter'),
  ],
});

3. Mock 動態 API,固定資料來源

import { test, expect } from '@playwright/test';

test('dashboard visual with stable fixture', async ({ page }) => {
  await page.route('**/api/dashboard/summary', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        activeUsers: 128,
        revenue: 52300,
        trend: 'up',
      }),
    });
  });

  await page.goto('/dashboard');
  await expect(page).toHaveScreenshot('dashboard.png', {
    fullPage: true,
  });
});

CI / PR Review Flow:把視覺差異變成可審查資產

推薦流程是「PR 跑 visual tests,失敗就上傳 diff artifacts,讓 reviewer 直接看圖」。

name: visual-regression

on:
  pull_request:

jobs:
  visual-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npx playwright install --with-deps

      - name: Run visual tests
        run: npx playwright test tests/visual --reporter=html,line

      - name: Upload Playwright artifacts on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-visual-artifacts
          path: |
            test-results/
            playwright-report/
          retention-days: 7

PR 審查時的決策原則可以固定成三步:

  1. 差異是預期改動嗎?是的話更新 baseline。
  2. 差異是非預期回歸嗎?先修再重跑。
  3. 無法判定時,要求設計或 feature owner 一起看 diff 圖。

何時用 Playwright,何時用 Percy / Chromatic

Playwright 適合:

  • 你已經有 Playwright E2E 基礎,想快速補上 visual guardrail。
  • 重點是核心流程頁(checkout、signup、dashboard)而不是全站像素級覆蓋。
  • 團隊可接受以工程規則(mask/mock/stylePath)維持穩定度。

Percy / Chromatic 這類專門服務較適合:

  • 你需要設計系統層級的大規模視覺審查與歷史追蹤。
  • 希望用雲端工作流降低 baseline 維護成本。
  • 需要更成熟的跨分支 UI review governance。

很多團隊會採混合策略:產品主流程用 Playwright,設計系統元件庫交給 Percy/Chromatic。

常見問題 / 注意事項

Q1:視覺測試是不是門檻設高一點就好? 不是。maxDiffPixels/maxDiffPixelRatio 設太寬鬆會讓真正回歸漏掉。優先順序應該是先穩定資料與畫面,再調門檻。

Q2:可以跨 OS 共用同一套 baseline 嗎? 通常不建議。字型與渲染差異會讓誤報增加。實務上多半固定在同一種 CI runner(例如 Ubuntu)產生與比對 baseline。

Q3:snapshot 目錄可不可以不 commit? 不建議。baseline 是視覺規格的一部分,不進版控就很難在 PR 中追蹤何時、為何改變。

總結

Playwright 的視覺回歸測試重點,不是把每個頁面都截圖,而是把高風險 UI 路徑做成可審查、可回溯、可自動化的 guardrail。

先建立正確心智模型,再落地三件事:

  • 明確 baseline 管理規範(何時更新、誰可更新)
  • 穩定化策略(mock、mask、stylePath、固定環境)
  • CI/PR 的 diff 審查流程

這三步做完,你的團隊才會真正把 Visual Regression Testing 用在「減少上線事故」,而不是增加 CI 噪音。