参加直播:美国东部时间周四上午 11 点,Storybook 9 发布和 AMA

测试运行器

通过命令行或 CI 在 stories 上运行组件测试

在 Github 上查看

Storybook 测试运行器将您的所有 stories 转化为可执行的测试。

特性

  • ⚡️ 零配置设置
  • 💨 对所有 stories 进行冒烟测试
  • ▶️ 使用 play 函数测试 stories
  • 🏃 在无头浏览器中并行测试您的 stories
  • 👷 通过直接链接到 story 获取错误反馈
  • 🐛 使用 addon-interactions 在实时浏览器中以可视化和交互方式调试它们
  • 🎭 由 JestPlaywright 提供支持
  • 👀 监听模式、过滤器以及您期望的便利功能
  • 📔 代码覆盖率报告

工作原理

在这篇博客文章中详细了解 Storybook 交互测试的发布,或观看此视频了解其实际应用。

Storybook 测试运行器使用 Jest 作为运行器,Playwright 作为测试框架。您的每个 .stories 文件都会被转换为一个 spec 文件,每个 story 都会变成一个测试,在无头浏览器中运行。

测试运行器的设计很简单——它只需访问正在运行的 Storybook 实例中的每个 story,并确保组件没有出现故障

  • 对于没有 play 函数的 stories,它会验证 story 是否渲染时没有错误。这本质上是一个冒烟测试。
  • 对于有 play 函数的 stories,它还会检查 play 函数中的错误以及所有断言是否通过。这本质上是一个交互测试

如果出现任何故障,测试运行器将提供包含错误信息的输出,并附带指向失败 story 的链接,以便您可以自己查看错误并在浏览器中直接调试它

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

[!Note] 运行器假定您的 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**
--listTests 列出所有将要运行的测试文件,然后退出test-storybook --listTests
--ci 它不会自动保存新快照的常规行为,而是会使测试失败并要求使用 --updateSnapshot 运行 Jest。test-storybook --ci
--shard [分片索引/分片总数] 将您的测试套件拆分到不同的机器上以便在 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 文件来弹出其配置。测试运行器将使用此文件。

[!Note] 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.node.org.cn/docs/configuration
   */
  testTimeout: 20000, // default timeout is 15s
};

过滤测试 (实验性)

您可能希望在测试运行器中跳过某些 stories,仅对 stories 的子集运行测试,或者完全从测试中排除某些 stories。这可以通过 tags 标注实现。默认情况下,测试运行器包含所有带有 'test' 标签的 stories。此标签在 Storybook 8 中默认包含在所有 stories 中,除非用户通过标签否定另有说明。

此标注可以是 story 的一部分,因此仅适用于该 story,也可以是组件 meta (默认导出),适用于文件中的所有 stories

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'],
};

[!Note] 您不能从另一个文件导入常量并使用它们来定义 stories 中的 tags。您的 stories 或 meta 中的 tags 必须以内联方式定义为字符串数组。这是由于 Storybook 的静态分析导致的限制。

有关如何组合 tags (以及如何选择性移除) 的更多信息,请参阅官方文档

一旦您的 stories 有了自己的自定义 tags,您就可以通过tags 属性在测试运行器配置文件中过滤它们。您也可以使用 CLI 标志 --includeTags--excludeTags--skipTags 来达到同样的目的。CLI 标志将优先于测试运行器配置中的 tags,因此会覆盖它们。

--skipTags--excludeTags 都会阻止 story 被测试。区别在于,跳过的测试在 CLI 输出中显示为“skipped”,而被排除的测试则完全不显示。跳过的测试可用于指示临时禁用的测试。

测试报告器

测试运行器使用默认的 Jest 报告器,但您可以通过如上所述弹出配置并覆盖 (或合并) reporters 属性来添加额外的报告器。

此外,如果您将 --junit 传递给 test-storybook,测试运行器会将 jest-junit 添加到 reporters 列表中,并生成 JUnit XML 格式的测试报告。您可以通过设置特定的 JEST_JUNIT_* 环境变量或在 package.json 中定义一个包含所需选项的 jest-junit 字段来进一步配置 jest-junit 的行为,这些选项在生成报告时会生效。您可以在此处查看所有可用选项:https://github.com/jest-community/jest-junit#configuration

针对已部署的 Storybook 运行

默认情况下,测试运行器假定您正在针对在端口 6006 上本地服务的 Storybook 运行它。如果您想定义一个目标 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 中,这是一个包含所有 stories 的静态索引。

这对于针对已部署的 Storybook 运行特别有用,因为 index.json 保证与您正在测试的 Storybook 同步。在默认的基于 story 文件模式下,您的本地 story 文件可能不同步——或者您甚至可能无法访问源代码。

此外,无法直接针对 .mdx stories 或自定义 CSF 方言运行测试运行器,例如使用 addon-svelte-csf 编写 Svelte 原生 stories 时。在这些情况下,必须使用 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 stories

要在您的 Storybook 中启用 stories.json,请在 .storybook/main.js 中设置 buildStoriesJson feature flag

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

[!Note] index.json 模式与 watch 模式不兼容。

在 CI 中运行

如果您想将测试运行器添加到 CI,有几种方法可以实现

1. 在 Github Actions 部署中针对已部署的 Storybook 运行

在 Github actions 上,像 Vercel、Netlify 等服务进行部署运行时,它们会遵循一个模式,即发出一个包含在新生成的 URL 在 deployment_status.target_url 下的 deployment_status 事件。您可以使用该 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 }}'

[!Note] 如果您正在针对远程部署的 Storybook (例如 Chromatic) 的 TARGET_URL 运行测试运行器,请确保该 URL 加载的是公开可用的 Storybook。在浏览器中以隐身模式打开时是否正确加载?如果您的已部署 Storybook 是私有的并具有身份验证层,测试运行器将无法访问您的 stories。如果是这种情况,请改用下一个选项。

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

[!Note] 在本地构建 Storybook 使得测试远程可用但位于身份验证层下的 Storybook 变得简单。如果您也在某个地方 (例如 Chromatic、Vercel 等) 部署了您的 Storybook,Storybook URL 仍然可以与测试运行器一起使用。在运行 test-storybook 命令时,您可以将其传递给 REFERENCE_URL 环境变量,如果某个 story 失败,测试运行器将提供一个带有指向您的已发布 Storybook 中该 story 链接的有用消息。

设置代码覆盖率

测试运行器支持使用 --coverage 标志或 STORYBOOK_COLLECT_COVERAGE 环境变量进行代码覆盖率测试。前提是您的组件已使用 istanbul 进行插桩。

1 - 插桩代码

对代码进行插桩是一个重要步骤,它允许 Storybook 跟踪代码行。这通常通过使用插桩库来实现,例如 Istanbul Babel plugin 或其 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;

该插件有默认选项,可能足以满足您的项目需求,它接受一个 options 对象用于项目特定配置

手动配置 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 使用。

[!Note] 如果您的组件未显示在报告中,并且您正在使用 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 等工具自动化报告,覆盖率文件将自动检测到,并且如果在 coverage 文件夹中有多个文件,它们将自动合并。

或者,如果您想合并来自其他工具的覆盖率,您应该

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"
  }
}

[!Note] 如果您的其他测试 (例如 Jest) 使用的 coverageProvider 与 babel 不同,则在合并覆盖率文件时会出现问题。更多信息请见此处

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 中设置,该文件可以选择性地导出这些函数中的任何一个。

[!Note] preVisitpostVisit 函数将对所有 stories 执行。

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 (已废弃)

[!Note] 此钩子已废弃。它已更名为 preVisit,请改用它。

preVisit

接收一个 Playwright Page 和一个包含当前 story 的 idtitlename 的上下文对象的异步函数。在测试中,于 story 渲染前执行。对于在 story 渲染前配置 Page 很有用,例如设置视口大小。

// .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 (已废弃)

[!Note] 此钩子已废弃。它已更名为 postVisit,请改用它。

postVisit

接收一个 Playwright Page 和一个包含当前 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;

[!Note] 虽然您可以在其中一些钩子中访问 Playwright 的 Page 对象,但我们鼓励您尽可能多地在 story 的 play 函数中进行测试。

渲染生命周期

为了用这些钩子可视化测试生命周期,请考虑为您的 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.net.cn/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);
});

这些钩子对于各种用例非常有用,下方示例部分对此进行了描述。

除了这些钩子之外,您还可以在 .storybook/test-runner.js 中设置其他属性

prepare

测试运行器有一个默认的 prepare 函数,用于在测试 stories 之前让浏览器进入正确的环境。您可以覆盖此行为,以防您想改变浏览器的行为。例如,您可能想设置一个 cookie,或向访问 URL 添加查询参数,或在到达 Storybook URL 之前进行一些身份验证。您可以通过覆盖 prepare 函数来实现。

prepare 函数接收一个包含以下内容的对象

供参考,请使用默认的 prepare 函数作为起点。

[!Note] 如果您覆盖默认的 prepare 行为,尽管这很强大,您将负责正确准备浏览器。默认 prepare 函数未来的更改将不会包含在您的项目中,因此您需要关注未来版本中的更改。

getHttpHeaders

测试运行器会进行几次 fetch 调用来检查 Storybook 实例的状态,并获取 Storybook stories 的索引。此外,它还使用 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 (实验性)

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

在使用钩子运行测试时,您可能希望获取有关 story 的信息,例如传递给它的参数或其 args。测试运行器现在提供了一个 getStoryContext 工具函数,用于获取当前 story 的上下文

假设您的 story 如下所示

// ./Button.stories.ts

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

您可以在测试钩子中像这样访问其上下文

// .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 是否在测试运行器的上下文中渲染。如果您想在测试运行器中运行时禁用 stories 中的某些功能,这可能会很有用,尽管这可能是一个边缘情况。

// 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 的 render/template 函数内部
  • 在 play 函数内部
  • 在 preview.js 内部
  • 在浏览器中执行的任何其他代码内部

示例

下方您将找到使用钩子和工具函数通过测试运行器实现不同功能的示例。

预配置视口大小

您可以使用 Playwright 的 Page 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;

可访问性测试

使用 Storybook 9

在 Storybook 9 中,可访问性插件增强了自动化报告功能,测试运行器对此提供了开箱即用的支持。如果您已安装 @storybook/addon-a11y,只要通过 parameters 启用它们,您就会获得对每个 story 的 a11y 检查

// .storybook/preview.ts

const preview = {
  parameters: {
    a11y: {
      // 'error' will cause a11y violations to fail tests
      test: 'error', // or 'todo' or 'off'
    },
  },
};

export default preview;

如果您之前为 Storybook 8 设置了 a11y 测试 (参考下方示例),您可以卸载 axe-playwright 并移除测试运行器钩子中的所有代码,因为它们不再需要了。

使用 Storybook 8

[!TIP] 如果您升级到 Storybook 9,则对 a11y 测试提供了开箱即用的支持,您不必遵循这样的示例。

您可以安装 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 Addon,则可以针对使用该插件生成的基于 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 数量。也许您有大量的 stories,或者您的 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 目前存在一个bug,意味着测试不能位于与项目不同的驱动器上。要解决此问题,您需要将 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 覆盖率的一些解释

未来工作

未来的计划包括增加对以下功能的支持

  • 📄 运行插件报告
  • ⚙️ 通过测试运行器以单个命令启动 Storybook

贡献

我们欢迎对测试运行器的贡献!

分支结构

  • next - npm 上的 next 版本,也是大部分工作发生的开发分支
  • prerelease - npm 上的 prerelease 版本,用于测试对 main 的最终更改
  • main - npm 上的 latest 版本,也是大多数用户使用的稳定版本

发布流程

  1. 所有 PR 应指向 next 分支,该分支依赖于 Storybook 的 next 版本。
  2. 合并后,此包的新版本将在 next NPM 标签上发布。
  3. 如果更改包含需要回补到稳定版本的 bugfix,请在 PR 描述中注明。
  4. 标记为 pick 的 PR 将被 cherry-pick 回 prerelease 分支,并在 prerelease npm 标签上生成发布。
  5. 验证后,prerelease PR 将被合并回 main 分支,这将在 latest npm 标签上生成发布。
作者
  • ndelangen
    ndelangen
  • shilman
    shilman
  • tmeasday
    tmeasday
  • ghengeveld
    ghengeveld
  • winkervsbecks
    winkervsbecks
  • yannbf
    yannbf
支持
    Angular
    Ember
    HTML
    Marko
    Mithril
    Preact
    Rax
    React
    Riot
    Svelte
    Vue
    Web Components
标签