昨天把元件的 API 用意圖命名與行為分層整理好之後,今天要面對一個殘酷的現實:再乾淨的 prop 介面,也擋不住一次 CSS 全域改動把 Button 的 padding 悄悄縮掉兩像素。API 保護的是「怎麼呼叫」,但沒人在保護「畫出來長怎樣」。這道缺口就是視覺回歸測試(Visual Regression Testing,VRT)要補的:把每個元件的每種狀態當成一張基準圖,之後每次改動都拿新截圖和基準逐像素比對,顏色跑掉、間距位移、字重變了,CI 直接紅燈。
📖 學
視覺回歸測試的核心是「基準(baseline)+ 差異(diff)」這組概念。第一次跑,工具會把元件渲染成一張參考截圖存起來;之後每次跑,它渲染出新截圖,和參考圖做像素比對,把不同的地方標紅給你看。你要做的判斷只有兩種:這個變化是我要的(接受新基準),還是我不要的(當成 bug 修掉)。整條測試的價值不在「抓 bug」,而在「讓每一個非預期的視覺改動都必須經過一個人點頭」。這對設計系統特別關鍵,因為一個底層元件被幾十個產品畫面消費,你改一行 token,影響面根本不是靠肉眼掃得完的。
放進設計系統的脈絡,VRT 幾乎是接在 token 和元件 API 後面的第三根支柱。前兩天談的 design token 決定了「一個顏色、一段間距的單一事實來源」,元件 API 決定了「外部怎麼組合這些值」;但 token 換算成實際畫面的那一哩路,仍然可能被 flex 對齊、line-height、border-box 這些細節扭曲。VRT 就是守在輸出端的哨兵。實務上最順的接法是讓它吃 Storybook 的 story:你為元件寫的每一個 story(預設、hover、disabled、loading、長文字溢出)本來就是一份狀態清單,VRT 工具直接把每個 story 截一張圖,等於白拿了一整套測試案例。這也是為什麼 Storybook 官方文件會把 visual test 當成一等公民,@chromatic-com/storybook 這個 addon 就是把「跑測試、看 diff、核可」直接搬進 Storybook 面板裡。
工具上今天有兩條主流路線,選哪條取決於你願不願意把截圖這件事交給雲端。第一條是 Chromatic,它由 Storybook 維護團隊做,把截圖、跨瀏覽器渲染、diff 儲存、團隊核可全放雲端,每個 commit 自動跑、平行執行,設計師和工程師能在同一個 diff 上留言討論。它的好處是你完全不用管「截圖環境不一致」這個惡夢——字體渲染、抗鋸齒、螢幕 DPI 在不同機器上會產生假差異,而雲端把渲染環境鎖死了。代價是它是付費 SaaS,截圖數量會算進帳單。第二條是自己用 Playwright 的 toHaveScreenshot(),完全免費、截圖存在你自己的 repo 裡,await expect(page).toHaveScreenshot() 第一次跑產生基準、之後自動比對,還內建等頁面穩定後才截圖的 auto-retry,比舊的 toMatchSnapshot() 可靠。代價是你得自己處理環境一致性——最常見的做法是把測試跑在 Docker 容器裡,確保本機和 CI 用同一套字體與渲染引擎,否則 diff 會被雜訊淹沒。
真正會讓 VRT 專案失敗的不是工具選錯,而是「不穩定的測試(flaky test)」。只要有一個測試三不五時無理由變紅,團隊很快就會養成「反正是雜訊,全部核可」的習慣,那一刻整套測試就等於廢了。所以導入 VRT 的功夫有一半花在消除非決定性來源:關掉 CSS 動畫與過場(Playwright 可設 animations: 'disabled')、把會變動的內容(日期、頭像、隨機資料)用 mask 遮掉、等字體載入完成再截圖、對允許的微小差異設 maxDiffPixels 或 threshold 給一點容忍度但不要開太大。這些設定的目標只有一個:讓「紅燈」永遠等於「真的有東西變了」,而不是「機器今天心情不好」。
🧠 記
- VRT 的本質是「基準圖 + 逐像素 diff + 人工核可」,價值在讓每個非預期視覺變化都必須有人點頭,不是自動抓 bug。
- 在設計系統裡,VRT 是接在 token(單一事實來源)、元件 API(組合方式)之後的輸出端哨兵,守住「token 到畫面」那一哩路。
- 最省力的接法是吃 Storybook 的 story:每個狀態 story 就是一個現成的視覺測試案例。
- 兩條路線:Chromatic(雲端、環境鎖死、付費、含團隊核可流程)vs Playwright
toHaveScreenshot()(自架、免費、要自己搞定環境一致性)。 - flaky test 是頭號殺手:關動畫、mask 動態內容、等字體載入、設合理容忍度,讓紅燈永遠代表真的有變化。
✍️ 實踐
今天挑一個你設計系統裡「被消費最多」的底層元件(通常是 Button),用 Playwright 為它加上第一個視覺回歸測試,今天做得完:
- 確認專案有 Playwright,沒有就
npm init playwright@latest。 - 寫一個 spec,導向 Storybook 裡
Button的預設 story(或直接渲染元件的頁面),加一行await expect(page).toHaveScreenshot('button-default.png')。 - 在同一支測試裡再截兩到三個狀態:hover、disabled、長文字。用
page.getByRole觸發狀態後各截一張。 - 第一次跑產生基準圖,打開
*-snapshots/資料夾用肉眼確認這幾張圖確實是你要的樣子。 - 故意改一下 Button 的 padding 或顏色,再跑一次,看它變紅並產出 diff 圖——親眼確認守門機制有效,再把改動還原。
- 在測試設定裡加上
animations: 'disabled',並對含動態內容的元件試用一次mask,體會消除 flaky 的手感。
做完你就有了一張可以往其他元件複製的模板,以及一次「看著 diff 變紅」的肌肉記憶。
🔗 延伸學習
- Visual tests | Storybook docs — Storybook 官方講如何把 story 變成視覺測試,含
@chromatic-com/storybookaddon。 - Visual comparisons | Playwright —
toHaveScreenshot()官方文件,基準產生、比對、mask、threshold 全在這。 - Visual Tests addon for Storybook • Chromatic docs — Chromatic 官方,把雲端視覺測試與核可流程接進 Storybook 面板。
- SnapshotAssertions | Playwright —
toHaveScreenshot完整參數表,maxDiffPixels、animations、mask逐項說明。
💬 問 AI
我在為公司的設計系統導入視覺回歸測試,底層元件已有 Storybook stories,token 與元件 API 已整理好。
請幫我:
1. 比較「Chromatic」與「自架 Playwright toHaveScreenshot」兩條路線,列出在成本、環境一致性、團隊核可流程、跨瀏覽器上的取捨,並針對「5 人團隊、約 40 個元件」給出建議。
2. 給我一份 Playwright 視覺測試的最小可用設定,包含關閉動畫、mask 動態內容、等字體載入、maxDiffPixels 容忍度,並說明每一項是在消除哪一種 flaky 來源。
3. 設計一套 CI 流程:PR 觸發視覺測試、diff 如何呈現給 reviewer、基準圖更新的核可與版本控管策略。
請用繁體中文、台灣用語回答,並在關鍵設定附上簡短程式碼範例。