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. 在您的 package.json 中新增 test-storybook 指令碼
{
  "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 [amount] 指定工作池將產生的最大工作數量,以執行測試 test-storybook --maxWorkers=2
--testTimeout [number] 此選項會設定測試案例的預設逾時 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 的一部分(因此僅適用於該 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 擁有您自己的自訂標籤,您就可以透過測試執行器設定檔中的 tags 屬性來篩選它們。您也可以使用 CLI 標記 --includeTags--excludeTags--skipTags 來達到相同的目的。CLI 標記的優先順序高於測試執行器設定中的標籤,因此會覆寫它們。

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

測試報告器

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

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

針對已部署的 Storybook 執行測試

預設情況下,測試執行器會假設您正在針對本機伺服器上的 Storybook(埠號為 6006)執行測試。如果您想要定義目標 URL,以便針對已部署的 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 部署上針對已部署的 Storybooks 執行

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

以下是一個根據該 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 中針對本機建置的 Storybooks 執行

為了在 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 Components、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 loader 等)。此外,如果您的專案使用 Vue 或 Svelte,您還需要為 nyc 新增一個額外的設定。

您可以在 此儲存庫 中找到包含許多不同設定的範例,以及如何在每個設定中設定覆蓋率的步驟。

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

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

yarn test-storybook --coverage

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

注意:如果您的元件未顯示在報告中,並且您正在使用 Vue 或 Svelte,這很可能是因為您缺少一個 .nycrc.json 檔案來指定檔案副檔名。請參閱 範例,了解如何設定。

如果您想使用 不同的 reporters 產生覆蓋率報告,您可以使用 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

測試執行器會呈現 story 並執行其 play function (如果存在)。但是,有些行為無法透過在瀏覽器中執行的 play function 來達成。例如,如果您希望測試執行器為您拍攝視覺快照,這是可以透過 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 (已棄用)

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

preVisit

非同步函式,接收一個 Playwright Page 和一個包含目前 story 的 idtitlename 的 context 物件。會在 story 渲染前的測試中執行。適用於在 story 渲染前設定 Page,例如設定 viewport 大小。

// .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 Page 和一個包含目前 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

測試執行器有一個預設的 prepare 函式,它會在測試 story 前將瀏覽器置於正確的環境中。您可以覆寫此行為,以防您可能想要駭入瀏覽器的行為。例如,您可能想要設定 cookie,或將查詢參數新增至訪問的 URL,或在到達 Storybook URL 前進行一些身份驗證。您可以透過覆寫 prepare 函式來做到這一點。

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

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

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

getHttpHeaders

測試執行器會進行幾個 fetch 呼叫,以檢查 Storybook 實例的狀態,並取得 Storybook story 的索引。此外,它還會使用 Playwright 訪問頁面。在所有這些情況下,根據您的 Storybook 託管位置,您可能需要設定一些 HTTP header。例如,如果您的 Storybook 託管在基本驗證之後,您可能需要設定 Authorization header。您可以透過將 getHttpHeaders 函式傳遞至您的測試執行器組態來做到這一點。該函式會接收 fetch 呼叫和頁面訪問的 url,並且應傳回一個包含要設定之 header 的物件。

// .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 渲染期間有瀏覽器記錄時,測試執行器會將記錄與錯誤訊息一起提供。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 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

當您使用測試執行器執行影像快照測試時,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 是否在測試執行器的 context 中渲染。如果您想要在測試執行器中執行時停用 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 (Plug n' Play) 支援

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',以便報告與預期一致並正確合併。

如需更多 context,這裡有一些解釋,說明為什麼 v8 不是 Babel/Istanbul 覆蓋率的一對一替代品。

未來工作

未來的計劃包括增加對以下功能的支援

  • 📄 執行外掛程式報告
  • ⚙️ 在單一命令中透過測試執行器產生 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 標籤上產生一個發佈版本。
作者
  • depressing_utopian
    depressing_utopian
適用於
    Angular
    Ember
    HTML
    Marko
    Mithril
    Preact
    Rax
    React
    Riot
    Svelte
    Vue
    Web Components
標籤