April 14, 2025

前端測試指南 - 策略與實踐 (Chapter 5 - 視覺測試)


Chapter 5 視覺測試

5-1 視覺測試

「視覺測試」(Visual Testing)是指一種測試技術, 主要用來檢查應用程式 UI 的外觀是否與預期一致, 它不只是測試功能是否正常, 更關注畫面是否正確顯示。

在什麼樣的情況下會進行視覺測試?

為什麼我只有改邏輯, 沒動 UI, 但還是要做視覺測試?

不是要驗證你「有改變」了什麼,而是要確認你「沒意外改變」什麼。

「欸靠, 我明明沒有動那邊, 為什麼那邊壞掉了?, 跟我沒關係喔!!!」 結果看 PR 問題就是這就是你的鍋, 手賤動到 config, 或是不小心刪到東西…

所以視覺測試透過畫面比對協助發覺肉眼不可見或是人為忽略的問題, 確保在重構/更新路上走得安穩… 「視覺測試」並不是沒用, 而是告訴你「你沒不小心搞壞東西」,這就是視覺測試最安心的地方.

(( 但我還是覺得這就是一個大大增加成本然後吃力不討好的工作 XD

Cypress Installation & Configuration

npm create vite@latest
pnpm add -D cypress
"scripts": {
  // ...
  "cy:open": "cypress open",
},

clipboard.png

會出現這個問題, 請把 cypress.config.ts -> cypress.config.js, 或是繼續維持 ts, 但是需要針對 TS 的部分做額外配置, 詳情可以參考這個 issue cypress-issue/23552


Percy (https://percy.io/)

Percy 是一套自動化的「視覺回歸測試工具」,可以幫助你在開發或部署過程中,自動截圖比對網頁或元件的畫面,確保 UI 沒有意外變化(像是顏色、字型、排版等視覺差異)。

它常與 Cypress、Playwright、Selenium、Storybook 等工具整合使用,並支援與 CI/CD(例如 GitHub Actions)串接,讓設計和工程團隊能快速發現、審核並追蹤畫面上的變更。

Percy vs 其他視覺回歸測試工具比較 (ChatGPT)

工具優點缺點
Percy✅ 易於整合(支援 Cypress、Playwright、Selenium)
✅ Percy UI 清楚
✅ 可與 CI/CD 整合良好
❌ 免費快照數量有限(5,000/月)
❌ 商業版價格偏高
Chromatic✅ 專為 Storybook 打造
✅ 非常適合 component 層級測試
❌ 僅支援 Storybook,不適合整頁 E2E 視覺測試
Applitools✅ AI 智能比對、抗小變動干擾能力強
✅ 支援多平台
❌ 費用高
❌ 學習曲線稍高
Loki✅ 開源、輕量
✅ 可自訂 CI/CD 整合方式
❌ 功能簡單、社群不大
❌ 缺乏視覺化儀表板

Installation & Configuration

安裝 @percy/cli + @percy/cypress

pnpm add @percy/cli -D
pnpm add -D @percy/cypress

由於 視覺測試 他是截圖上傳到 percy 專案內, 所以需要登入官網建立 project, 把對應 project 的 token 註記在環境中

clipboard.png

clipboard.png

關於 percy in Cypress 配置, 可以參考 Integrate your Cypress tests with Percy

# .env
PERCY_TOKEN=web_d5ba0ca8e24422c41fb9ca18eb22cc89859326242a008838c141e4e534b7616a
"scripts": {
  // ...
  "cy:open": "cypress open",
  "cy:percy": "source .env && npx percy exec -- npx cypress run"
},

第一個視覺測試檔案

// cypress/e2e/mixtini.cy.js
describe("Index Page", () => {
  it("should update snapshot to Percy correctly", () => {
    cy.visit("https://mixtini-co.web.app/");
    cy.percySnapshot("index");
  });
});

[!NOTE] > cy.percySnapshot 目前尚未被 cypress 引用, 所以透過 /cypress/support/commands.ts 進行 cypress 的全域註冊 plugin/擴充套件, 當然也可以在 要測試的檔案裡面直接 import "@percy/cypress"

// cypress/support/commands.ts

/// <reference types="cypress" />
import "@percy/cypress";

// 或是

// cypress/e2e/mixtini.cy.ts
import "@percy/cypress";
describe("Index Page", () => {
  // ...
});

clipboard.png clipboard.png

clipboard.png

此串網址就是執行 Percy build 後的網址, 可以看到內部有兩張圖

clipboard.png

[!NOTE] 如果是第一次執行, 只有一張, 我理圖片裡的並不是第一次, 再跑一次就好

點擊上放的 </> icon, 可以看到內部的比對內容, 實際上不應該有不一樣, 可能是網頁在渲染的過程 被 Percy 捕捉到差異顯示 0.02% diff clipboard.png

快照比對原理


實際情境範例

Percy 比對 baseline 的流程:

  (1) 有沒有同分支前一次成功快照?
       └─> 有 ➜ 拿它當 baseline
       └─> 沒有 ➜ 看有沒有 target branch(如 main)
              └─> 有 ➜ 用它
              └─> 沒有 ➜ 無 baseline,Percy 無法比對

Percy 的 baseline 是「分支感知」的:

條件比對對象
PR 建立時PR 的 base branch 上最新 snapshot
多次更新同一 branch該 branch 上上一次 snapshot
無 baseline 時fallback 到 default branch 或共同 ancestor

結論


5-2 驗證畫面正確性

pnpm add -D storybook
npx sb init

npx sb init 協助建立 storybook 相關設置檔案與初始化, 可以看到 .storybook 以及 src/storybook 兩個資料夾與內部的檔案, 可以先刪除 src/storybook, 自己寫一個實際案例

// src/components/Button.tsx
import React from "react";

type ButtonProps = {
  label: string,
  onClick?: () => void,
};

export const Button = ({ label, onClick }: ButtonProps) => {
  return <button onClick={onClick}>{label}</button>;
};
pnpm add -D @storybook/test
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within, expect } from "@storybook/test";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Example/Button",
  component: Button,
};
export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    label: "Click Me",
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = await canvas.getByRole("button");

    await userEvent.click(button);

    // 你可以根據點擊後的狀態去驗證
    expect(button).toHaveTextContent("Clicked"); // 假設點擊後文字會改變
  },
};

執行 pnpm percy, 就可以看到剛剛新增的 Button 組件納入 Percy snapshot

clipboard.png

訪問連結網站可以看到 component 的比對測試, 內容皆與頁面測試相同.

clipboard.png

頁面可找到對應的寬度, 瀏覽器切換

clipboard.png

clipboard.png

{
 "version": 2
 "snapshot": {
   "widths": [375, 768, 1280],
   "min-height": 1024
 }
}

clipboard.png

[!WARNING]

  • 書上說是 percy.json, 但事實上是 .percy.json, 有一個 . 前綴
  • 需要 "version": 2

clipboard.png


特定流程後取的 snapshot

修改 mixtini.cy.ts

import "@percy/cypress";

describe("Index Page", () => {
  it("should update snapshot to Percy correctly", () => {
    cy.visit("https://mixtini-co.web.app/cocktails/search");
    cy.percySnapshot("index", {
      // widths: [375, 768, 1440],
    });
  });

  it("should search for 愛爾蘭咖啡 and take Percy snapshot", () => {
    cy.visit("https://mixtini-co.web.app/cocktails/search");

    // 等待頁面加載完成,包含 React 渲染
    cy.wait(1000);

    // 輸入「愛爾蘭咖啡」
    cy.get('input[placeholder="請輸入調酒名稱或材料"]').type("愛爾蘭咖啡", {
      delay: 100,
    });

    // 點擊搜尋按鈕
    cy.get('button[data-testid="SEARCH_BUTTON"]').click();

    // 等待搜尋結果載入
    cy.wait(3000);

    // Percy 拍照
    cy.percySnapshot("搜尋結果 - 愛爾蘭咖啡");
  });
});

分別執行 pnpm cy:open pnpm cy:percy 可以看到對應的畫面皆有按照測試案例去做顯示與截圖.

clipboard.png

clipboard.png

配合 CI workflow 的整合, 結合 CI tools 做測試

name: Regular Visual Testing
on:
  schedule:
    - cron: "0 0 * * 1" # Every Monday at midnight UTC
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Install dependencies
        run: npm install

      - name: Run visual testing
        run: npx percy exec -- npx cypress run
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

      - name: Run Component visual Testing
        run: npm run percy

[!WARNING] 好奇怪 XD, visual testing 沒有跑出連結 Run Component visual Testing

clipboard.png


5-3 工具評比

歸納總共有幾個面向:

表格一:比較 Percy、Chromatic、Cypress 與 Jest 的 toMatchSnapshot

#PercyChromaticCypressJest 的 toMatchSnapshot
快照的類型與結構畫面截圖畫面截圖畫面截圖以文字格式儲存 DOM 結構
比對工具平台工具平台工具搭配套件 cypress-image-diff-js搭配版控工具或測試斷言
比對原始碼平台工具搭配版控工具搭配版控工具
依照 PR 或 branch 檢視快照平台工具平台工具搭配版控工具搭配版控工具
Merge Checks搭配 percy/exec-action搭配 chromaui/action搭配版控工具
元件測試搭配 Storybook搭配 Storybook內建指令

表格二:各工具更多比較資訊

#PercyChromaticCypressJest 的 toMatchSnapshot
頁面測試搭配 e2e testing 框架內建指令
瀏覽器支援度Chrome、Firefox 和 EdgeChromeChrome、Firefox 和 Edge快照與瀏覽器無關
特色兼顧元件和頁面測試專注在元件測試且功能完善整合多種測試方式實作過程簡單易懂
儲存空間平台提供平台提供開發者自行處理開發者自行處理

本書推薦做法

5-4 本章回顧與總結