Storybook 測試執行器

Storybook stories 的測試執行器

在 Github 上檢視

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

功能

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

運作方式

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

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

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

  • 對於沒有 play 函數的 stories,它會驗證 story 是否在沒有任何錯誤的情況下渲染。這基本上是冒煙測試。
  • 對於有 play 函數的 stories,它也會檢查 play 函數中是否有錯誤,以及是否通過了所有斷言。這基本上是 互動測試

如果有任何失敗,測試執行器會提供錯誤輸出,以及連結到失敗 story 的連結,讓您可以親自查看錯誤並直接在瀏覽器中進行除錯

Storybook 相容性

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

測試執行器版本 Storybook 版本
^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 [目錄名稱] 從中載入 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 [數量] 指定工作集區為執行測試產生的最大工作站數量 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 (實驗性)僅測試符合指定標籤的 stories,以逗號分隔 test-storybook --includeTags="test-only"
--excludeTags (實驗性)不測試符合指定標籤的 stories,以逗號分隔 test-storybook --excludeTags="broken-story,todo"
--skipTags (實驗性)不測試符合指定標籤的 stories,並在 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
};

篩選測試(實驗性)

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

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

const meta = {
  component: Button,
  tags: ['design', 'test-only'],
};
export default meta;

// will inherit tags from meta with value ['design', 'test-only']
export const Primary = {};

export const Secondary = {
  // will override tags to be just ['skip']
  tags: ['skip'],
};

注意 您無法從另一個檔案匯入常數,並使用它們在您的 stories 中定義標籤。您 stories 或 meta 中的標籤必須以字串陣列的形式內嵌定義。這是 Storybook 靜態分析造成的限制。

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

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

測試報告器

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

此外,如果您將 --junit 傳遞至 test-storybook,測試執行器會將 jest-junit 新增至報告器清單,並以 JUnit XML 格式產生測試報告。您可以透過設定特定的 JEST_JUNIT_* 環境變數或在您的 package.json 中使用您想要的選項定義 jest-junit 欄位,以進一步設定 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 中,這是所有故事的靜態索引。

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

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

若要在 index.json 模式下執行,請先確保您的 Storybook 有 v4 index.json 檔案。您可以在瀏覽至以下位置時找到它

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

它應該是一個 JSON 檔案,第一個鍵應該是 "v": 4,後面接著一個名為 "entries" 的鍵,其中包含故事 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 故事

若要在您的 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 動作中,一旦 Vercel、Netlify 等服務執行部署作業後,它們會遵循發出 deployment_status 事件的模式,其中包含 deployment_status.target_url 下新產生的 URL。您可以使用該 URL 並將其設定為測試執行器的 TARGET_URL

以下是一個基於此來執行測試的動作範例

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 是私有的並且具有驗證層,則測試執行器將會命中它們,因此無法存取您的故事。如果發生這種情況,請改用下一個選項。

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 位於驗證層下。如果您也將您的 Storybook 部署到某個位置 (例如 Chromatic、Vercel 等),則 Storybook URL 仍然可用於測試執行器。您可以在執行 test-storybook 命令時將其傳遞給 REFERENCE_URL 環境變數,如果故事失敗,測試執行器將會提供有用的訊息,其中包含指向您已發佈 Storybook 中該故事的連結。

設定程式碼涵蓋率

測試執行器支援使用 --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 命令將會遵循它們。

如果您想要刻意忽略程式碼的某些部分,您可以使用 istanbul 剖析提示

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

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

現在,您可能還有其他測試 (例如單元測試),這些測試在 Storybook 中涵蓋,但在使用 Jest 執行測試時會涵蓋,您也可能會從這些測試產生覆蓋率檔案。在這種情況下,如果您使用 Codecov 之類的工具來自動化報告,則會自動偵測到覆蓋率檔案,並且如果覆蓋率資料夾中有多個檔案,則會自動合併它們。

或者,如果您想要合併其他工具的覆蓋率,您應該

1 - 將 coverage/storybook/coverage-storybook.json 移動或複製到 coverage/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

測試執行器會呈現故事並執行其 play 函式(如果存在)。但是,有一些行為無法透過在瀏覽器中執行的 play 函式達成。例如,如果您希望測試執行器為您擷取視覺快照,則這是透過 Playwright/Jest 實現的事情,但必須在 Node 中執行。

若要啟用視覺或 DOM 快照之類的用例,測試執行器會匯出可在全域覆寫的測試掛勾。這些掛勾可讓您在呈現故事之前和之後存取測試生命週期。

有三個掛勾:setuppreVisitpostVisitsetup 在所有測試執行之前執行一次。preVisitpostVisit 在故事呈現之前和之後的測試中執行。

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

注意 preVisitpostVisit 函式將會針對所有故事執行。

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 頁面和具有目前故事 idtitlename 的內容物件的非同步函式。在故事呈現之前,於測試中執行。適用於在故事呈現之前設定頁面,例如設定視區大小。

// .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 的 context 物件的非同步函式。會在 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

test-runner 有一個預設的 prepare 函式,可在測試 Story 之前將瀏覽器設定在正確的環境中。如果您想駭入瀏覽器的行為,您可以覆寫此行為。例如,您可能想要設定 Cookie、將查詢參數新增至瀏覽網址,或在到達 Storybook 網址之前進行一些驗證。您可以透過覆寫 prepare 函式來做到這一點。

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

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

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

getHttpHeaders

test-runner 會進行一些 fetch 呼叫,以檢查 Storybook 執行個體的狀態,並取得 Storybook 中 Story 的索引。此外,它還會使用 Playwright 瀏覽頁面。在所有這些情況下,根據您的 Storybook 託管位置,您可能需要設定一些 HTTP 標頭。例如,如果您的 Storybook 託管在基本驗證之後,您可能需要設定 Authorization 標頭。您可以透過將 getHttpHeaders 函式傳遞至您的 test-runner 設定來做到這一點。該函式接收 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(實驗性)

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-only']
    exclude: [], // string array, e.g. ['design', 'docs-only']
    skip: [], // string array, e.g. ['design']
  },
};
export default config;

tags 用於篩選您的測試。請在此處深入了解 篩選測試

logLevel

當測試失敗且在渲染 Story 期間有瀏覽器記錄時,test-runner 會在錯誤訊息旁提供記錄。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;

實用函數

對於更具體的使用案例,test runner 提供了一些實用函式,這些函式可能對您有所幫助。

getStoryContext

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

假設您的 Story 如下所示

// ./Button.stories.ts

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

您可以在測試 Hook 中存取其 context,如下所示

// .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

當您使用 test-runner 執行影像快照測試時,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 使用者代理

test-runner 會將 StorybookTestRunner 項目新增至瀏覽器的使用者代理程式。您可以使用它來判斷 Story 是否在 test runner 的 context 中渲染。如果您想在 test runner 中執行時停用 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 和實用函式來透過 test-runner 達成不同目標的食譜。

預先設定視埠大小

您可以使用 Playwright 的 Page 視窗實用程式,以程式方式變更測試的視窗大小。如果您使用 @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 並與 test-runner 搭配使用,以測試元件的無障礙功能。如果您使用 @storybook/addon-a11y,您可以重複使用其參數,並確保無障礙功能附加元件面板和 test-runner 中的測試在設定中匹配。

// .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 (Plug n' Play) 支援

Storybook test-runner 依賴於一個名為 jest-playwright-preset 的程式庫,該程式庫似乎不支援 PnP。因此,test-runner 無法與 PnP 搭配使用,並且您可能會收到以下錯誤

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

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

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

React Native 支援

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

CLI 中的錯誤輸出太短

預設情況下,test runner 會將錯誤輸出截斷為 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 選項傳遞至您的指令,來限制並行執行的工作程序數量

{
  "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 環境

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

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

將來自 test runner 的測試涵蓋率報告與來自其他工具 (例如 Jest) 的報告合併後,如果最終結果不是您預期的結果。以下說明原因

test runner 使用 babel 作為涵蓋率提供者,這在評估程式碼涵蓋率時會以某種方式運作。如果您的其他報告碰巧使用與 babel 不同的涵蓋率提供者,例如 v8,它們將以不同的方式評估涵蓋率。合併後,結果很可能會不正確。

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

雖然 test runner 沒有提供 v8 作為涵蓋率提供者的選項,但建議您將應用程式的 Jest 設定設定為使用 coverageProvider: 'babel' (如果可以),以便報告符合預期並正確合併。

如需更多背景資訊,請參閱 此處的一些說明,說明為什麼 v8 不是 Babel/Istanbul 涵蓋率的 1:1 取代項目。

未來工作

未來計畫包括新增對下列功能的支援

  • 📄 執行附加元件報告
  • ⚙️ 透過單一指令中的 test runner 產生 Storybook

貢獻

我們歡迎大家為 test runner 做出貢獻!

分支結構

  • next - npm 上的 next 版本,以及大部分工作發生的開發分支
  • prerelease - npm 上的 prerelease 版本,其中 main 的最終變更會經過測試
  • main - npm 上的 latest 版本,以及大多數使用者使用的穩定版本

發佈流程

  1. 所有 PR 都應以 next 分支為目標,該分支依賴於 Storybook 的 next 版本。
  2. 合併後,此套件的新版本將在 next NPM 標籤上發佈。
  3. 如果變更包含需要修補回穩定版本的錯誤修正,請在 PR 描述中註明。
  4. 標示為 pick 的 PR 將會被選取回 prerelease 分支,並會在 prerelease npm 標籤上產生發佈版本。
  5. 經過驗證後,prerelease PR 將合併回 main 分支,這將在 latest npm 標籤上產生發佈版本。
  • depressing_utopian
    depressing_utopian
適用於
    Angular
    Ember
    HTML
    Marko
    Mithril
    Preact
    Rax
    React
    Riot
    Svelte
    Vue
    Web Components
標籤