測試執行器

透過命令列或 CI 在故事上執行元件測試

在 Github 上檢視

Storybook 測試執行器將您的所有故事轉換為可執行的測試。

功能

  • ⚡️ 零組態設定
  • 💨 冒煙測試所有故事
  • ▶️ 使用 play 函數測試故事
  • 🏃 在無頭瀏覽器中平行測試您的故事
  • 👷 從錯誤中取得回饋,並提供直接連結到故事的連結
  • 🐛 在即時瀏覽器中使用 addon-interactions 以視覺方式互動地除錯它們
  • 🎭 由 JestPlaywright 驅動
  • 👀 監看模式、篩選器以及您期望的便利性
  • 📔 程式碼覆蓋率報告

運作方式

請參閱 這篇部落格文章 中關於 Storybook 互動測試的詳細公告,或觀看 此影片 以了解其運作方式。

Storybook 測試執行器使用 Jest 作為執行器,並使用 Playwright 作為測試框架。您的每個 .stories 檔案都會轉換為規格檔案,且每個故事都會成為在無頭瀏覽器中執行的測試。

測試執行器的設計很簡單 – 它只會從正在執行的 Storybook 執行個體瀏覽每個故事,並確保元件沒有失敗

  • 對於沒有 play 函數的故事,它會驗證故事是否在沒有任何錯誤的情況下呈現。這基本上是冒煙測試。
  • 對於有 play 函數的故事,它也會檢查 play 函數中是否有錯誤,以及所有判斷提示是否通過。這基本上是互動測試

如果發生任何失敗,測試執行器將提供包含錯誤的輸出,以及失敗故事的連結,因此您可以親自查看錯誤並直接在瀏覽器中除錯

Storybook 相容性

根據您使用的 Storybook 版本,使用下表以使用此套件的正確版本

測試執行器版本 Storybook 版本
^0.19.0 ^8.2.0
~0.17.0 ^8.0.0
~0.16.0 ^7.0.0
~0.9.4 ^6.4.0

開始使用

  1. 安裝測試執行器
yarn add @storybook/test-runner -D
  1. test-storybook 指令碼新增至您的 package.json
{
  "scripts": {
    "test-storybook": "test-storybook"
  }
}
  1. 或者,請遵循文件以撰寫互動測試,並使用 addon-interactions 在 Storybook 中使用互動式除錯器來視覺化互動。

  2. 執行 Storybook (測試執行器會針對正在執行的 Storybook 執行個體執行)

yarn storybook
  1. 執行測試執行器
yarn test-storybook

注意 執行器假設您的 Storybook 在連接埠 6006 上執行。如果您在另一個連接埠中執行 Storybook,請使用 --url 或在執行命令前設定 TARGET_URL,例如

yarn test-storybook --url http://127.0.0.1:9009
or
TARGET_URL=http://127.0.0.1:9009 yarn test-storybook

CLI 選項

Usage: test-storybook [options]
選項 說明
--help 輸出使用資訊 test-storybook --help
-i--index-json 在 index json 模式下執行。自動偵測 (需要相容的 Storybook) test-storybook --index-json
--no-index-json 停用 index json 模式 test-storybook --no-index-json
-c--config-dir [dir-name] 從中載入 Storybook 組態的目錄 test-storybook -c .storybook
--watch 監看檔案變更,並重新執行與變更檔案相關的測試。test-storybook --watch
--watchAll 監看檔案變更,並在發生變更時重新執行所有測試。test-storybook --watchAll
--coverage 表示應收集測試覆蓋率資訊並在輸出中報告 test-storybook --coverage
--coverageDirectory 寫入覆蓋率報告輸出的目錄 test-storybook --coverage --coverageDirectory coverage/ui/storybook
--url 定義執行測試的 URL。適用於自訂 Storybook URL test-storybook --url http://the-storybook-url-here.com
--browsers 定義執行測試的瀏覽器。以下之一或多個:chromium、firefox、webkit test-storybook --browsers firefox chromium
--maxWorkers [數量] 指定 worker 集區將產生用於執行測試的工作者最大數量 test-storybook --maxWorkers=2
--testTimeout [數字] 此選項設定測試案例的預設逾時 test-storybook --testTimeout=15_000
--no-cache 停用快取 test-storybook --no-cache
--clearCache 刪除 Jest 快取目錄,然後在不執行測試的情況下結束 test-storybook --clearCache
--verbose 使用測試套件階層顯示個別測試結果 test-storybook --verbose
-u--updateSnapshot 使用此旗標以重新錄製在此測試執行期間失敗的每個快照 test-storybook -u
--eject 建立本機組態檔以覆寫測試執行器的預設值 test-storybook --eject
--json 以 JSON 格式列印測試結果。此模式會將所有其他測試輸出和使用者訊息傳送到 stderr。test-storybook --json
--outputFile 同時指定 --json 選項時,將測試結果寫入檔案。test-storybook --json --outputFile results.json
--junit 表示應在 junit 檔案中報告測試資訊。test-storybook --**junit**
--ci 它不會自動儲存新的快照,而是會使測試失敗,且需要使用 --updateSnapshot 來執行 Jest。test-storybook --ci
--shard [shardIndex/shardCount] 在不同機器之間分割測試套件,以便在 CI 中執行。test-storybook --shard=1/3
--failOnConsole 在瀏覽器主控台發生錯誤時使測試失敗test-storybook --failOnConsole
--includeTags (實驗性) 只測試符合指定標籤的故事 (以逗號分隔)test-storybook --includeTags="test-only"
--excludeTags (實驗性) 不測試符合指定標籤的故事 (以逗號分隔)test-storybook --excludeTags="broken-story,todo"
--skipTags (實驗性) 不測試符合指定標籤的故事,並在 CLI 輸出中將它們標示為已略過 (以逗號分隔)test-storybook --skipTags="design"

彈射組態

測試執行器以 Jest 為基礎,且會接受 Jest 的大多數 CLI 選項,例如 --watch--watchAll--maxWorkers--testTimeout 等。它可立即運作,但如果您想要更好地控制其組態,您可以執行 test-storybook --eject 以彈射其組態,以在專案的根資料夾中建立本機 test-runner-jest.config.js 檔案。測試執行器將會使用此檔案。

注意 test-runner-jest.config.js 檔案也可以放置在您的 Storybook 組態目錄中。如果您傳遞 --config-dir 選項,測試執行器也會在那裡尋找組態檔。

組態檔將接受兩個執行器的選項

Jest-playwright 選項

測試執行器使用 jest-playwright,您可以傳遞 testEnvironmentOptions 以進一步設定它。

Jest 選項

Storybook 測試執行器隨附的 Jest 已安裝為內部相依性。您可以根據測試執行器隨附的 Jest 版本傳遞 Jest 選項。

測試執行器版本 Jest 版本
^0.6.2 ^26.6.3 或 ^27.0.0
^0.7.0 ^28.0.0
^0.14.0 ^29.0.0

如果您已在使用相容版本的 Jest,測試執行器將會使用它,而不是在您的 node_modules 資料夾中安裝重複的版本。

以下是彈射檔案的範例,用於從 Jest 延伸測試逾時

// ./test-runner-jest.config.js
const { getJestConfig } = require('@storybook/test-runner');

const testRunnerConfig = getJestConfig();

/**
 * @type {import('@jest/types').Config.InitialOptions}
 */
module.exports = {
  // The default Jest configuration comes from @storybook/test-runner
  ...testRunnerConfig,
  /** Add your own overrides below
   * @see https://jest.dev.org.tw/docs/configuration
   */
  testTimeout: 20000, // default timeout is 15s
};

篩選測試 (實驗性)

您可能想要在測試執行器中略過某些故事,只針對故事的子集執行測試,或完全從測試中排除某些故事。這可以透過 tags 註解來實現。依預設,測試執行器會包含每個具有 'test' 標籤的故事。依預設,Storybook 8 會為所有故事包含此標籤,除非使用者透過標籤否定另行告知。

此註解可以是故事的一部分,因此僅適用於該故事,或元件 meta (預設匯出),它適用於檔案中的所有故事

const meta = {
  component: Button,
  tags: ['atom'],
};
export default meta;

// will inherit tags from project and meta to be ['dev', 'test', 'atom']
export const Primary = {};

export const Secondary = {
  // will combine with project and meta tags to be ['dev', 'test', 'atom', 'design']
  tags: ['design'],
};

export const Tertiary = {
  // will combine with project and meta tags to be ['dev', 'atom']
  tags: ['!test'],
};

注意 您無法從另一個檔案匯入常數,並使用它們來定義故事中的標籤。您故事或 meta 中的標籤必須以字串陣列的形式內嵌定義。這是由於 Storybook 的靜態分析所致。

如需關於標籤如何組合 (以及可以選擇性移除) 的詳細資訊,請參閱官方文件

一旦您的故事擁有您自己的自訂標籤,您就可以透過測試執行器組態檔中的標籤屬性來篩選它們。您也可以將 CLI 旗標 --includeTags--excludeTags--skipTags 用於相同目的。CLI 旗標的優先順序會高於測試執行器組態中的標籤,因此會覆寫它們。

--skipTags--excludeTags 都會防止測試故事。不同之處在於,略過的測試在 CLI 輸出中會顯示為「略過」,而排除的測試則完全不會出現。略過的測試可用於指示暫時停用的測試。

測試報告器

測試執行器使用預設的 Jest 報告器,但您可以透過彈射組態 (如上所述) 並覆寫 (或與之合併) reporters 屬性,來新增其他報告器。

此外,如果你傳遞 --junittest-storybook,測試執行器會將 jest-junit 加入回報器列表,並產生 JUnit XML 格式的測試報告。你可以進一步設定 jest-junit 的行為,方法是設定特定的 JEST_JUNIT_* 環境變數,或是在你的 package.json 中定義一個帶有你想要選項的 jest-junit 欄位,這些選項會在產生報告時被採用。你可以在這裡查看所有可用的選項:https://github.com/jest-community/jest-junit#configuration

針對已部署的 Storybook 執行

預設情況下,測試執行器會假設你正在針對本地伺服器上 6006 埠的 Storybook 執行測試。如果你想要定義目標網址,以便針對已部署的 Storybook 執行測試,你可以透過傳遞 TARGET_URL 環境變數來達成。

TARGET_URL=https://the-storybook-url-here.com yarn test-storybook

或是使用 --url 旗標。

yarn test-storybook --url https://the-storybook-url-here.com

Index.json 模式

預設情況下,測試執行器會將你的 Story 檔案轉換為測試。它也支援第二種「index.json 模式」,可以直接針對你的 Storybook 的索引資料執行測試,這些資料依你的 Storybook 版本而定,位於 stories.jsonindex.json 中,是一個包含所有 Story 的靜態索引。

這對於針對已部署的 Storybook 執行測試特別有用,因為 index.json 必定會與你正在測試的 Storybook 同步。在預設的、基於 Story 檔案的模式中,你的本地 Story 檔案可能會不同步,或者你甚至可能無法存取原始碼。

此外,無法直接針對 .mdx Story 或自訂 CSF 方言(例如使用 addon-svelte-csf 編寫 Svelte 原生 Story 時)執行測試執行器。在這些情況下,必須使用 index.json 模式。

若要以 index.json 模式執行,首先請確保你的 Storybook 有一個 v4 的 index.json 檔案。你可以在導覽至下列位置時找到它:

https://your-storybook-url-here.com/index.json

它應該是一個 JSON 檔案,且第一個鍵應該是 "v": 4,後面接著一個名為 "entries" 的鍵,其中包含 Story ID 到 JSON 物件的對應。

在 Storybook 7.0 中,index.json 預設為啟用,除非你正在使用 storiesOf() 語法,否則不支援。

在 Storybook 6.4 和 6.5 上,若要以 index.json 模式執行,首先請確保你的 Storybook 有一個名為 stories.json 的檔案,其中包含 "v": 3,位於

https://your-storybook-url-here.com/stories.json

如果你的 Storybook 沒有 stories.json 檔案,你可以產生一個,前提是

  • 你正在執行 Storybook 6.4 或更高版本
  • 你沒有使用 storiesOf Story

若要在你的 Storybook 中啟用 stories.json,請在 .storybook/main.js 中設定 buildStoriesJson 功能旗標。

// .storybook/main.ts
const config = {
  // ... rest of the config
  features: { buildStoriesJson: true },
};
export default config;

一旦你擁有有效的 stories.json 檔案,你的 Storybook 將會與「index.json 模式」相容。

預設情況下,測試執行器會偵測你的 Storybook URL 是本地還是遠端,如果是遠端,它會自動以「index.json 模式」執行。若要停用它,你可以傳遞 --no-index-json 旗標。

yarn test-storybook --no-index-json

如果你正在針對本地 Storybook 執行測試,但由於某些原因想要以「index.json 模式」執行,你可以傳遞 --index-json 旗標。

yarn test-storybook --index-json

注意 index.json 模式與監看模式不相容。

在 CI 中執行

如果你想要將測試執行器新增到 CI,有幾種方法可以做到。

1. 在 Github Actions 部署上針對已部署的 Storybook 執行

在 Github Actions 上,一旦像 Vercel、Netlify 等服務完成部署執行,它們會遵循一個模式,發出一個包含新產生 URL 的 deployment_status 事件,位於 deployment_status.target_url 下。你可以使用該 URL 並將其設定為測試執行器的 TARGET_URL

以下是一個根據該設定執行測試的 Action 範例:

name: Storybook Tests
on: deployment_status
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    if: github.event.deployment_status.state == 'success'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18.x'
      - name: Install dependencies
        run: yarn
      - name: Run Storybook tests
        run: yarn test-storybook
        env:
          TARGET_URL: '${{ github.event.deployment_status.target_url }}'

注意 如果你正在針對遠端部署的 Storybook (例如 Chromatic) 的 TARGET_URL 執行測試執行器,請確保該 URL 會載入公開可用的 Storybook。當在瀏覽器的無痕模式中開啟時,是否能正確載入?如果你的已部署 Storybook 是私有的且具有驗證層,測試執行器將會遇到它們,因此無法存取你的 Story。如果發生這種情況,請改用下一個選項。

2. 在 CI 中針對本機建置的 Storybook 執行

為了在 CI 中建置並針對你的 Storybook 執行測試,你可能需要結合使用涉及 concurrentlyhttp-serverwait-on 程式庫的指令。以下是一個執行以下操作的配方:Storybook 會在本地建置並提供服務,一旦準備就緒,測試執行器將會針對它執行。

{
  "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook\""
}

然後你基本上可以在你的 CI 中執行 test-storybook:ci

name: Storybook Tests
on: push
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18.x'
      - name: Install dependencies
        run: yarn
      - name: Run Storybook tests
        run: yarn test-storybook:ci

注意 在本地建置 Storybook 可以簡單地測試可能在遠端可用,但在驗證層下的 Storybook。如果你也將你的 Storybook 部署到某個地方 (例如 Chromatic、Vercel 等),Storybook URL 仍然可以與測試執行器一起使用。你可以在執行 test-storybook 指令時將其傳遞給 REFERENCE_URL 環境變數,如果 Story 失敗,測試執行器將會提供一個有用的訊息,其中包含你的已發佈 Storybook 中 Story 的連結。

設定程式碼覆蓋率

測試執行器支援使用 --coverage 旗標或 STORYBOOK_COLLECT_COVERAGE 環境變數進行程式碼涵蓋率。先決條件是你的元件已使用 istanbul 進行檢測。

1 - 檢測程式碼

檢測程式碼是一個重要的步驟,可讓 Storybook 追蹤程式碼行數。這通常是透過使用檢測程式庫來實現的,例如 Istanbul Babel 外掛程式,或其 Vite 對應程式。在 Storybook 中,你可以透過兩種不同的方式設定檢測:

使用 @storybook/addon-coverage

對於選定的框架 (React、Preact、HTML、Web 元件、Svelte 和 Vue),你可以使用 @storybook/addon-coverage 外掛程式,它會自動為你設定外掛程式。

安裝 @storybook/addon-coverage

yarn add -D @storybook/addon-coverage

並將其註冊在你的 .storybook/main.js 檔案中。

// .storybook/main.ts
const config = {
  // ...rest of your code here
  addons: ['@storybook/addon-coverage'],
};
export default config;

外掛程式具有可能足夠你的專案的預設選項,並且它接受一個 用於專案特定設定的選項物件

手動設定 istanbul

如果你的框架未使用 Babel 或 Vite,例如 Angular,你將必須手動設定你的專案可能需要的任何 Istanbul 風格 (Webpack 加載器等)。此外,如果你的專案使用 Vue 或 Svelte,你將需要為 nyc 新增一個額外的設定。

你可以在 此儲存庫 中找到包含許多不同設定和關於如何在每個設定中設定涵蓋率步驟的配方。

2 - 使用 --coverage 旗標執行測試

設定檢測之後,請執行 Storybook,然後使用 --coverage 執行測試執行器。

yarn test-storybook --coverage

測試執行器將會在 CLI 中報告結果,並產生一個 coverage/storybook/coverage-storybook.json 檔案,nyc 可以使用該檔案。

注意 如果你的元件未顯示在報告中,並且你正在使用 Vue 或 Svelte,可能是因為你缺少一個 .nycrc.json 檔案來指定檔案副檔名。請使用配方作為如何設定的參考。

如果你想要使用 不同的回報器產生涵蓋率報告,你可以使用 nyc 並將其指向包含 Storybook 涵蓋率檔案的資料夾。nyc 是測試執行器的依賴項,因此你的專案中已經有它。

以下是一個產生 lcov 報告的範例:

npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook

這會產生更詳細、互動式的涵蓋率摘要,你可以在 coverage/storybook/index.html 檔案中存取,你可以瀏覽它,並詳細顯示涵蓋率。

如果你的專案中有 nyc,則 nyc 指令將會採用 nyc 設定檔案

如果你希望刻意忽略程式碼的某些部分,你可以使用 istanbul 解析提示

3 - 將程式碼覆蓋率與其他工具的覆蓋率合併

測試執行器會報告與 coverage/storybook/coverage-storybook.json 檔案相關的涵蓋率。這是設計好的,會顯示在執行 Storybook 時測試的涵蓋率。

現在,你可能會有其他測試 (例如單元測試) 在 Storybook 中未涵蓋,但在使用 Jest 執行測試時會涵蓋,你也可以從中產生涵蓋率檔案。在這種情況下,如果你使用像 Codecov 之類的工具來自動執行報告,涵蓋率檔案將會被自動偵測到,如果涵蓋率資料夾中有許多檔案,它們將會自動合併。

或者,如果你想合併其他工具的涵蓋率,你應該

1 - 移動或複製 coverage/storybook/coverage-storybook.jsoncoverage/coverage-storybook.json; 2 - 針對 coverage 資料夾執行 nyc report

以下是如何實現的範例:

{
  "scripts": {
    "test:coverage": "jest --coverage",
    "test-storybook:coverage": "test-storybook --coverage",
    "coverage-report": "cp coverage/storybook/coverage-storybook.json coverage/coverage-storybook.json && nyc report --reporter=html -t coverage --report-dir coverage"
  }
}

注意 如果你的其他測試 (例如 Jest) 使用與 babel 不同的 coverageProvider,合併涵蓋率檔案時會遇到問題。此處提供更多資訊

4 - 使用 --shard 旗標執行測試

測試執行器會將所有涵蓋率收集在一個檔案 coverage/storybook/coverage-storybook.json 中。若要分割涵蓋率檔案,你應該使用 shard-index 重新命名它。若要報告涵蓋率,你應該使用 nyc merge 指令合併涵蓋率檔案。

Github CI 範例

test:
  name: Running Test-storybook (${{ matrix.shard }})
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - name: Testing storybook
      run: yarn test-storybook --coverage --shard=${{ matrix.shard }}/${{ strategy.job-total }}
    - name: Renaming coverage file
      run: mv coverage/storybook/coverage-storybook.json coverage/storybook/coverage-storybook-${matrix.shard}.json
report-coverage:
  name: Reporting storybook coverage
  steps:
    - name: Merging coverage
      run: yarn nyc merge coverage/storybook merged-output/merged-coverage.json
    - name: Report coverage
      run: yarn nyc report --reporter=text -t merged-output --report-dir merged-output

Circle CI 範例

test:
  parallelism: 4
  steps:
    - run:
        command: yarn test-storybook --coverage --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL
        command: mv coverage/storybook/coverage-storybook.json coverage/storybook/coverage-storybook-${CIRCLE_NODE_INDEX + 1}.json
report-coverage:
  steps:
    - run:
        command: yarn nyc merge coverage/storybook merged-output/merged-coverage.json
        command: yarn nyc report --reporter=text -t merged-output --report-dir merged-output

Gitlab CI 範例

test:
  parallel: 4
  script:
    - yarn test-storybook --coverage --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
    - mv coverage/storybook/coverage-storybook.json coverage/storybook/coverage-storybook-${CI_NODE_INDEX}.json
report-coverage:
  script:
    - yarn nyc merge coverage/storybook merged-output/merged-coverage.json
    - yarn nyc report --reporter=text -t merged-output --report-dir merged-output

測試掛鉤 API

測試執行器會渲染一個 Story 並執行它的 play 函數 (如果存在)。但是,有些行為無法透過在瀏覽器中執行的 play 函數達成。例如,如果你希望測試執行器為你擷取視覺快照,這可以透過 Playwright/Jest 達成,但必須在 Node 中執行。

若要啟用視覺或 DOM 快照等使用案例,測試執行器會匯出可以在全域覆寫的測試掛勾。這些掛勾讓你可以在 Story 渲染前後存取測試生命週期。

有三個掛勾:setuppreVisitpostVisitsetup 會在所有測試執行之前執行一次。preVisitpostVisit 會在測試中,在 Story 渲染前後執行。

所有三個函數都可以在設定檔 .storybook/test-runner.js 中設定,該檔案可以選擇性地匯出這些函數中的任何一個。

注意 preVisitpostVisit 函數將會針對所有 Story 執行。

setup

會在所有測試執行之前執行一次的非同步函數。適用於設定與節點相關的組態,例如擴展 Jest 全域 expect 以進行可存取性比對。

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async setup() {
    // execute whatever you like, in Node, once before all tests run
  },
};
export default config;

preRender (已棄用)

注意 這個掛勾已棄用。它已重新命名為 preVisit,請改用它。

preVisit

接收 Playwright 頁面 和內容物件 (包含目前 Story 的 idtitlename) 的非同步函數。會在測試中 Story 渲染之前執行。適用於在 Story 渲染之前設定頁面,例如設定視窗大小。

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async preVisit(page, context) {
    // execute whatever you like, before the story renders
  },
};
export default config;

postRender (已棄用)

注意:此 Hook 已被棄用。它已重新命名為 postVisit,請改用它。

postVisit

接收 Playwright 頁面和包含目前 Story 的 idtitlename 的內容物件的非同步函式。在 Story 渲染後,會在測試中執行。適用於在 Story 渲染後斷言事項,例如 DOM 和影像快照。

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // execute whatever you like, after the story renders
  },
};
export default config;

注意:雖然您可以使用 Playwright 的 Page 物件,但在某些 Hook 中,我們鼓勵您盡可能在 Story 的 play 函式中進行測試。

渲染生命週期

為了視覺化這些 Hook 的測試生命週期,請考慮針對 Storybook 中每個 Story 自動產生的簡化版測試程式碼

// executed once, before the tests
await setup();

it('button--basic', async () => {
  // filled in with data for the current story
  const context = { id: 'button--basic', title: 'Button', name: 'Basic' };

  // playwright page https://playwright.dev.org.tw/docs/pages
  await page.goto(STORYBOOK_URL);

  // pre-visit hook
  if (preVisit) await preVisit(page, context);

  // render the story and watch its play function (if applicable)
  await page.execute('render', context);

  // post-visit hook
  if (postVisit) await postVisit(page, context);
});

這些 Hook 對於各種使用案例非常有用,這些案例將在下面的食譜章節中說明。

除了這些 Hook 之外,您還可以在 .storybook/test-runner.js 中設定其他屬性

prepare

測試執行器具有預設的 prepare 函式,該函式會在測試 Story 之前,將瀏覽器設定到正確的環境中。如果您想要修改瀏覽器的行為,可以覆寫此行為。例如,您可能想要在到達 Storybook URL 之前設定 Cookie、將查詢參數新增到瀏覽 URL,或執行一些身分驗證。您可以透過覆寫 prepare 函式來完成。

prepare 函式接收包含下列內容的物件

作為參考,請使用預設的 prepare函式作為起點。

注意:如果您覆寫預設的 prepare 行為(即使它功能強大),您也必須負責正確準備瀏覽器。未來對預設 prepare 函式的變更將不會包含在您的專案中,因此您必須留意即將發佈的版本中的變更。

getHttpHeaders

測試執行器會進行一些 fetch 呼叫,以檢查 Storybook 執行個體的狀態,並取得 Storybook 的 Story 索引。此外,它會使用 Playwright 造訪頁面。在所有這些情況下,根據您的 Storybook 的託管位置,您可能需要設定一些 HTTP 標頭。例如,如果您的 Storybook 託管在基本身分驗證之後,您可能需要設定 Authorization 標頭。您可以透過將 getHttpHeaders 函式傳遞給您的測試執行器組態來完成。該函式會接收 fetch 呼叫和頁面造訪的 url,並且應該傳回具有要設定的標頭的物件。

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  getHttpHeaders: async (url) => {
    const token = url.includes('prod') ? 'XYZ' : 'ABC';
    return {
      Authorization: `Bearer ${token}`,
    };
  },
};
export default config;

標籤 (實驗性)

tags 屬性包含三個選項:include | exclude | skip,每個選項都接受字串陣列

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  tags: {
    include: [], // string array, e.g. ['test', 'design'] - by default, the value will be ['test']
    exclude: [], // string array, e.g. ['design', 'docs-only']
    skip: [], // string array, e.g. ['design']
  },
};
export default config;

tags 用於篩選您的測試。請在這裡瞭解更多資訊。

logLevel

當測試失敗且在渲染 Story 期間有瀏覽器記錄時,測試執行器會連同錯誤訊息一起提供記錄。logLevel 屬性定義應該顯示哪種類型的記錄

  • info(預設):顯示主控台記錄、警告和錯誤。
  • warn僅顯示警告和錯誤。
  • error僅顯示錯誤訊息。
  • verbose包含所有主控台輸出,包括偵錯資訊和堆疊追蹤。
  • none抑制所有記錄輸出。
// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  logLevel: 'verbose',
};
export default config;

errorMessageFormatter

errorMessageFormatter 屬性定義一個函式,該函式會在錯誤訊息於 CLI 中回報之前預先格式化錯誤訊息

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  errorMessageFormatter: (message) => {
    // manipulate the error message as you like
    return message;
  },
};
export default config;

實用函數

對於更特定的使用案例,測試執行器提供實用函式,這些函式可能對您有用。

getStoryContext

在使用 Hook 執行測試時,您可能想要從 Story 取得資訊,例如傳遞給它的參數或其 args。測試執行器現在提供 getStoryContext 實用函式,該函式會擷取目前 Story 的 Story 內容

假設您的 Story 看起來像這樣

// ./Button.stories.ts

export const Primary = {
  parameters: {
    theme: 'dark',
  },
};

您可以像這樣在測試 Hook 中存取其內容

// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // Get entire context of a story, including parameters, args, argTypes, etc.
    const storyContext = await getStoryContext(page, context);
    if (storyContext.parameters.theme === 'dark') {
      // do something
    } else {
      // do something else
    }
  },
};
export default config;

它對於略過或加強影像快照測試無障礙功能測試等使用案例非常有用。

waitForPageReady

當您使用測試執行器執行影像快照測試時,waitForPageReady 實用工具非常有用。它封裝了一些斷言,以確保瀏覽器已完成下載資產。

// .storybook/test-runner.ts
import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // use the test-runner utility to wait for fonts to load, etc.
    await waitForPageReady(page);

    // by now, we know that the page is fully loaded
  },
};
export default config;

StorybookTestRunner 使用者代理

測試執行器會將 StorybookTestRunner 項目新增至瀏覽器的使用者代理程式。您可以使用它來判斷 Story 是否在測試執行器的內容中渲染。如果您想要在測試執行器中執行時停用 Story 中的某些功能,這可能很有用,儘管這可能是一個邊緣案例。

// At the render level, useful for dynamically rendering something based on the test-runner
export const MyStory = {
  render: () => {
    const isTestRunner = window.navigator.userAgent.match(/StorybookTestRunner/);
    return (
      <div>
        <p>Is this story running in the test runner?</p>
        <p>{isTestRunner ? 'Yes' : 'No'}</p>
      </div>
    );
  },
};

由於此檢查是在瀏覽器中進行,因此它僅適用於以下情況

  • 在 Story 的渲染/範本函式內
  • 在 play 函式內
  • 在 preview.js 內
  • 在瀏覽器中執行的任何其他程式碼內

範例

您將在下面找到使用 Hook 和實用函式來使用測試執行器達成不同目標的食譜。

預先設定視窗大小

您可以使用 Playwright 的 Page viewport 實用工具,以程式方式變更測試的 viewport 大小。如果您使用 @storybook/addon-viewports,您可以重複使用其參數,並確保測試在組態中一致。

// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';

const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 };

const config: TestRunnerConfig = {
  async preVisit(page, story) {
    const context = await getStoryContext(page, story);
    const viewportName = context.parameters?.viewport?.defaultViewport;
    const viewportParameter = MINIMAL_VIEWPORTS[viewportName];

    if (viewportParameter) {
      const viewportSize = Object.entries(viewportParameter.styles).reduce(
        (acc, [screen, size]) => ({
          ...acc,
          // make sure your viewport config in Storybook only uses numbers, not percentages
          [screen]: parseInt(size),
        }),
        {}
      );

      page.setViewportSize(viewportSize);
    } else {
      page.setViewportSize(DEFAULT_VIEWPORT_SIZE);
    }
  },
};
export default config;

可存取性測試

您可以安裝 axe-playwright,並將其與測試執行器搭配使用,以測試元件的無障礙功能。如果您使用 @storybook/addon-a11y,您可以重複使用其參數,並確保測試在無障礙功能附加元件面板和測試執行器中都符合組態。

// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
import { injectAxe, checkA11y, configureAxe } from 'axe-playwright';

const config: TestRunnerConfig = {
  async preVisit(page, context) {
    // Inject Axe utilities in the page before the story renders
    await injectAxe(page);
  },
  async postVisit(page, context) {
    // Get entire context of a story, including parameters, args, argTypes, etc.
    const storyContext = await getStoryContext(page, context);

    // Do not test a11y for stories that disable a11y
    if (storyContext.parameters?.a11y?.disable) {
      return;
    }

    // Apply story-level a11y rules
    await configureAxe(page, {
      rules: storyContext.parameters?.a11y?.config?.rules,
    });

    // in Storybook 6.x, the selector is #root
    await checkA11y(page, '#storybook-root', {
      detailedReport: true,
      detailedReportOptions: {
        html: true,
      },
      // pass axe options defined in @storybook/addon-a11y
      axeOptions: storyContext.parameters?.a11y?.options,
    });
  },
};
export default config;

DOM 快照 (HTML)

您可以使用 Playwright 的內建 API 進行 DOM 快照測試

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root
    const elementHandler = await page.$('#storybook-root');
    const innerHTML = await elementHandler.innerHTML();
    expect(innerHTML).toMatchSnapshot();
  },
};
export default config;

當使用 --stories-json 執行時,測試會在臨時資料夾中產生,快照會儲存在旁邊。您將需要 --eject 並設定自訂的 snapshotResolver,將它們儲存在其他位置,例如您的工作目錄中

// ./test-runner-jest.config.js
const { getJestConfig } = require('@storybook/test-runner');

const testRunnerConfig = getJestConfig();

/**
 * @type {import('@jest/types').Config.InitialOptions}
 */
module.exports = {
  // The default Jest configuration comes from @storybook/test-runner
  ...testRunnerConfig,
  snapshotResolver: './snapshot-resolver.js',
};
// ./snapshot-resolver.js
const path = require('path');

// 👉 process.env.TEST_ROOT will only be available in --index-json or --stories-json mode.
// if you run this code without these flags, you will have to override it the test root, else it will break.
// e.g. process.env.TEST_ROOT = process.cwd()

module.exports = {
  resolveSnapshotPath: (testPath, snapshotExtension) =>
    path.join(process.cwd(), '__snapshots__', path.basename(testPath) + snapshotExtension),
  resolveTestPath: (snapshotFilePath, snapshotExtension) =>
    path.join(process.env.TEST_ROOT, path.basename(snapshotFilePath, snapshotExtension)),
  testPathForConsistencyCheck: path.join(process.env.TEST_ROOT, 'example.test.js'),
};

影像快照

以下是影像快照測試的略有不同的食譜

// .storybook/test-runner.ts
import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner';
import { toMatchImageSnapshot } from 'jest-image-snapshot';

const customSnapshotsDir = `${process.cwd()}/__snapshots__`;

const config: TestRunnerConfig = {
  setup() {
    expect.extend({ toMatchImageSnapshot });
  },
  async postVisit(page, context) {
    // use the test-runner utility to wait for fonts to load, etc.
    await waitForPageReady(page);

    // If you want to take screenshot of multiple browsers, use
    // page.context().browser().browserType().name() to get the browser name to prefix the file name
    const image = await page.screenshot();
    expect(image).toMatchImageSnapshot({
      customSnapshotsDir,
      customSnapshotIdentifier: context.id,
    });
  },
};
export default config;

疑難排解

Yarn PnP (隨插即用) 支援

Storybook 測試執行器依賴於一個名為 jest-playwright-preset 的程式庫,該程式庫似乎不支援 PnP。因此,測試執行器無法直接與 PnP 搭配使用,您可能會遇到以下錯誤

PlaywrightError: jest-playwright-preset: Cannot find playwright package to use chromium

如果遇到這種情況,有兩種可能的解決方案

  1. playwright 安裝為直接相依性。您可能需要在那之後執行 yarn playwright install,以便安裝 Playwright 的瀏覽器二進位檔。
  2. 將您的套件管理員的連結器模式切換為 node-modules

React Native 支援

測試執行器是基於 Web 的,因此無法直接與 @storybook/react-native 搭配使用。但是,如果您使用 React Native Web Storybook 附加元件,則可以針對使用該附加元件產生的基於 Web 的 Storybook 執行測試執行器。在這種情況下,事情的運作方式會相同。

CLI 中的錯誤輸出太短

根據預設,測試執行器會將錯誤輸出截斷為 1000 個字元,您可以在瀏覽器中直接在 Storybook 中檢查完整輸出。但是,如果您想要變更該限制,可以將 DEBUG_PRINT_LIMIT 環境變數設定為您選擇的數字,例如,DEBUG_PRINT_LIMIT=5000 yarn test-storybook

測試執行器似乎不穩定且一直逾時

如果您的測試因 Timeout - Async callback was not invoked within the 15000 ms timeout specified by jest.setTimeout 而逾時,可能是 playwright 無法處理測試您專案中 Story 的數量。也許您有大量的 Story,或者您的 CI 的 RAM 組態非常低。

無論如何,為了修正此問題,您應該透過將 --maxWorkers 選項傳遞給您的命令,來限制並行執行的 Worker 數量

{
  "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2\""
}

另一種選擇是嘗試透過將 --testTimeout 選項傳遞給您的命令來增加測試逾時時間(新增 --testTimeout=60_000 將測試逾時時間增加到 1 分鐘)

"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2 --testTimeout=60_000\""

測試執行器回報在 Windows CI 上執行時「找不到測試」

Jest 中目前有一個 錯誤,這表示測試不能與專案位於不同的磁碟機上。為了解決此問題,您需要將 TEMP 環境變數設定為與您的專案位於相同磁碟機上的臨時資料夾。以下是在 GitHub Actions 上的外觀範例

env:
  # Workaround for https://github.com/facebook/jest/issues/8536
  TEMP: ${{ runner.temp }}

將測試執行器新增至其他 CI 環境

由於測試執行器是基於 playwright,因此根據您的 CI 設定,您可能需要使用特定的 Docker 映像或其他組態。在這種情況下,您可以參考 Playwright CI 文件以取得更多資訊。

合併測試覆蓋率結果導致錯誤覆蓋率

在合併來自測試執行器的測試覆蓋率報告與來自其他工具(例如 Jest)的報告之後,如果最終結果並非您所預期的。原因如下

測試執行器使用 babel 作為覆蓋率提供者,當評估程式碼覆蓋率時,該提供者的行為方式會有所不同。如果您的其他報告碰巧使用與 babel 不同的覆蓋率提供者,例如 v8,則它們會以不同的方式評估覆蓋率。合併後,結果可能會不正確。

範例:在 v8 中,import 和 export 行會被計算為可覆蓋的程式碼片段,但在 babel 中,則不會。這會影響覆蓋率計算的百分比。

雖然測試執行器不提供 v8 作為覆蓋率提供者的選項,但建議您將應用程式的 Jest 組態設定為使用 coverageProvider: 'babel'(如果可以),以便報告能如預期地對齊並正確合併。

如需更多內容,此處有一些說明,說明為什麼 v8 不是 Babel/Istanbul 覆蓋率的 1:1 取代方案。

未來工作

未來的計畫包括新增對以下功能的支持

  • 📄 執行附加元件報告
  • ⚙️ 通過單一命令在測試執行器中衍生 Storybook

貢獻

我們歡迎您為測試執行器做出貢獻!

分支結構

  • next - npm 上的 next 版本,也是大多數開發工作進行的開發分支
  • prerelease - npm 上的 prerelease 版本,用於測試最終會合併到 main 的變更
  • main - npm 上的 latest 版本,也是大多數使用者使用的穩定版本

發布流程

  1. 所有的 PR 都應該以 next 分支為目標,該分支依賴於 Storybook 的 next 版本。
  2. 當合併後,此套件的新版本將會發佈到 next NPM 標籤。
  3. 如果變更包含需要回溯修補到穩定版本的錯誤修正,請在 PR 描述中註明。
  4. 標記為 pick 的 PR 將會被 cherry-pick 回到 prerelease 分支,並將在 prerelease npm 標籤上產生發佈。
  5. 一旦驗證通過,prerelease PR 將會被合併回 main 分支,這將會在 latest npm 標籤上產生發佈。
由以下人員製作
  • domyen
    domyen
  • kasperpeulen
    kasperpeulen
  • valentinpalkovic
    valentinpalkovic
  • jreinhold
    jreinhold
  • kylegach
    kylegach
  • ndelangen
    ndelangen
適用於
    Angular
    Ember
    HTML
    Marko
    Mithril
    Preact
    Rax
    React
    Riot
    Svelte
    Vue
    Web Components
標籤