StorybookVitestComponent TestingFrontend

Storybook 測試指南:Vitest Addon、play function 與 CI 實作

整理 Storybook 測試主線,說明如何用 @storybook/addon-vitest 與 storybook/test 建立可驗證的元件規格,並落地到 CI 流程。

· 5 分鐘閱讀

你會學到怎麼把 Storybook 從「展示櫥窗」升級成「可執行規格」,讓文件與測試共享同一份來源。


前言

很多團隊都有同一個痛點:元件文件在一處、測試在另一處,久了就會分歧。文件寫「按鈕可 disabled」,測試卻沒驗;或測試涵蓋了關鍵互動,但文件根本沒寫到。最終結果是,大家都以為自己有規格,實際上沒有單一真相來源。

Storybook 最有價值的地方,不只是「看得到元件」,而是讓每個 story 同時承擔三件事:展示、文件、可執行測試。這樣規格不是只存在於文字,而是能被 CI 驗證。

這篇會以 2026-04-02 可驗證的主線 寫法為基準:storybook + @storybook/addon-vitest + storybook/test。同時也會標註舊做法 @storybook/test-runner 的適用情境,避免版本混線。

先釐清版本邊界:目前建議做法 vs 舊版做法

截至 2026-04-02(npm dist-tags):

  • storybook latest:10.3.3
  • @storybook/addon-vitest latest:10.3.3
  • vitest latest:4.1.2
  • playwright latest:1.59.1
  • @storybook/test-runner latest:0.24.3(舊路線,仍可用)

在 Storybook 官方文件中,Vite-based framework 已明確建議優先使用 Vitest addon;test-runner 則偏向舊路線或非 Vite 情境。

你可以用下面的簡單判斷:

  1. 已用 Vite(React/Vue/Next.js Vite framework)→ 優先 @storybook/addon-vitest
  2. 歷史包袱重、仍綁 Jest 流程 → 可暫留 @storybook/test-runner
  3. 新專案、希望文件和測試完全同軌 → 直接走 Vitest addon

play function 的正確定位:互動腳本,不是測試執行器

play function 常被誤解。它的角色是「描述這個 story 在畫面上要怎麼互動與斷言」,例如點擊、輸入、檢查文字。它不是 Playwright runner,也不是 Vitest runner 本身。

真正執行測試的是測試框架(這條主線是 Vitest),而 Storybook addon 會把 stories 轉成 Vitest 可跑的測試。你可以把責任拆成:

  • story:元件在特定狀態下的規格
  • play:該規格下的互動腳本與斷言
  • @storybook/addon-vitest:把 stories 轉成可執行測試
  • Vitest browser mode + Playwright:在真實瀏覽器環境執行

這樣分層清楚,遇到失敗時你才知道該修 story、修測試腳本,還是修執行環境。

實作步驟:把 Storybook 文件與測試整合到同一條主線

1) 安裝與初始化

# 初始化或升級 Storybook
npx storybook@latest init

# 安裝並自動設定 Vitest addon
npx storybook@latest add @storybook/addon-vitest

# 安裝 Playwright browser binaries(CI 與本機都建議明確執行)
npx playwright install --with-deps chromium

add 指令會協助你註冊 addon、調整 Vitest 設定,並把 Storybook 測試拉進 browser mode。

2) 設定 .storybook/main.ts

// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'],
  addons: [
    '@storybook/addon-docs',
    '@storybook/addon-a11y',
    '@storybook/addon-vitest', // 讓 story 可直接轉成 Vitest 測試
  ],
};

export default config;

重點是 framework 要走 Vite 版本(例如 @storybook/react-vite@storybook/nextjs-vite),否則無法套用這條主線。

3) 在 story 同時寫文件與互動測試

// src/components/Button.stories.tsx
import type { Meta, StoryObj } from 'storybook/react';
import { expect, fn, userEvent, within } from 'storybook/test';
import { Button } from './Button';

const meta = {
  title: 'Form/Button',
  component: Button,
  tags: ['autodocs'],
  parameters: {
    docs: {
      description: {
        // component 層級描述,放在 meta 才是正確位置
        component: '通用按鈕元件,支援主要操作、次要操作與 disabled 狀態。',
      },
    },
  },
  args: {
    onClick: fn(),
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    children: '送出',
    disabled: false,
  },
};

export const Disabled: Story = {
  args: {
    children: '送出',
    disabled: true,
  },
};

export const Clickable: Story = {
  args: {
    children: '送出',
    disabled: false,
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: '送出' });

    await userEvent.click(button);

    // 在 story 規格中完成互動斷言
    await expect(args.onClick).toHaveBeenCalledTimes(1);
    await expect(button).toBeEnabled();
  },
};

這個範例有兩個關鍵:

  1. parameters.docs.description.component 放在 meta 層級,而不是單一 story
  2. play 是互動規格,搭配 storybook/test 的查找與斷言 API

4) CLI 與 CI:讓 story 在 pipeline 可被驗證

package.json 可以分開一般測試與 Storybook 測試:

{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build",
    "test": "vitest",
    "test-storybook": "vitest --project=storybook --run"
  }
}

GitHub Actions 範例:

name: Storybook Component Tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  storybook-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci

      - name: Install Playwright Chromium
        run: npx playwright install --with-deps chromium

      - name: Run Storybook tests via Vitest project
        run: npm run test-storybook

實務上,build-storybook 不一定每次都要跑;如果你的目標是先驗證互動規格,vitest --project=storybook 就能先擋掉大部分回歸。發佈流程再補 build,可讓 PR 回饋更快。

什麼時候該上 Storybook 測試?給團隊的決策框架

以下是我建議的落地順序:

  1. 先上文件(Stories + Autodocs):先讓可見性建立起來
  2. 補關鍵互動的 play:像表單送出、狀態切換、權限按鈕
  3. 進 CI,但只跑高價值 story:避免一次把所有故事都變阻塞點
  4. 最後再擴 coverage:依事故或 bug 熱點擴充,不要盲目追全覆蓋

如果你的元件是靜態展示型(純排版、無事件),只寫 story 文件通常就夠。反過來,若元件有狀態轉換與副作用(Modal、Toast、Form Wizard),建議一定要有 play 與 CI 驗證。

常見問題 / 注意事項

Q1:@storybook/test-runner 還能用嗎?
可以,尤其在既有 Jest 流程或非 Vite 架構下仍有價值;但若你是 Vite-based Storybook,官方主線已偏向 Vitest addon。

Q2:play 能不能等同 E2E 測試?
不能。play 比較像 component interaction spec,適合元件級互動。跨頁導覽、後端整合、權限流等情境,仍建議保留 Playwright/Cypress 的完整 E2E。

Q3:Autodocs 會自動幫我產生每個 story 截圖嗎?
不會。Autodocs 主要負責把 stories、args、型別資訊整理成文件頁。若要做視覺回歸或快照比對,要另外導入對應測試方案。

Q4:Next.js 專案可以用這條主線嗎?
可以,但要使用 Vite-based 的 Storybook framework(例如 @storybook/nextjs-vite),再搭配 Vitest addon。

總結

Storybook 的進階價值,不是「把元件秀出來」,而是讓元件規格可執行、可回歸、可進 CI。當 stories 既是文件又是測試入口,團隊就能把規格維護成本降到最低。

這篇重點可以收斂成三句話:

  1. 把版本基線定清楚:Vite framework + @storybook/addon-vitest + storybook/test
  2. 把責任分層:play 是互動腳本,Vitest/Playwright 才是執行環境。
  3. 把 CI 策略做漸進:先跑高價值 story,再擴大覆蓋。

照這個節奏做,你的 Storybook 就不會停在「UI 展示」,而會成為真正可驗證的元件規格系統。