测试运行器

通过命令行或 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. 在你的 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 在索引 json 模式下运行。自动检测(需要兼容的 Storybook) test-storybook --index-json
--no-index-json 禁用索引 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 它不会像常规行为那样自动存储新快照,而是会使测试失败,并要求 Jest 使用 --updateSnapshot 运行。 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.node.org.cn/docs/configuration
   */
  testTimeout: 20000, // default timeout is 15s
};

过滤测试(实验性)

你可能希望在测试运行器中跳过某些故事,仅针对故事子集运行测试,或完全从测试中排除某些故事。这可以通过 tags 注释实现。默认情况下,测试运行器包含所有带有 'test' 标签的故事。除非用户通过 标签否定 告知,否则 Storybook 8 默认情况下会为所有故事包含此标签。

此注释可以是故事的一部分,因此仅应用于该故事,也可以是组件元数据(默认导出),它应用于文件中的所有故事。

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

注意 你无法从另一个文件中导入常量并使用它们来在你的故事中定义标签。你故事或元数据中的标签**必须**在内联中定义,作为字符串数组。这是由于 Storybook 的静态分析所致的限制。

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

一旦你的故事拥有自己的自定义标签,你就可以通过测试运行器配置文件中的 tags 属性 来过滤它们。你也可以使用 CLI 标志 --includeTags--excludeTags--skipTags 来实现相同目的。CLI 标志将优先于测试运行器配置中的标签,因此会覆盖它们。

--skipTags--excludeTags 都将阻止测试故事。区别在于跳过的测试将在 CLI 输出中显示为“跳过”,而排除的测试根本不会出现。跳过的测试可能有助于指示暂时禁用的测试。

测试报告器

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

此外,如果你将 --junit 传递给 test-storybook,测试运行器会将 jest-junit 添加到报告器列表中,并在 JUnit XML 格式中生成测试报告。你还可以通过设置特定的 JEST_JUNIT_* 环境变量,或者在你的 package.json 中定义一个 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 模式

默认情况下,测试运行器会将您的故事文件转换为测试。它还支持二级“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 Actions 上,一旦像 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 部署到某个地方(例如 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)使用的 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

测试运行器呈现一个故事并执行其 播放函数(如果存在)。但是,某些行为无法通过在浏览器中执行的播放函数来实现。例如,如果您希望测试运行器为您拍摄视觉快照,这可以通过 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(已弃用)

注意此钩子已弃用。它已被重命名为 postVisit,请改为使用它。

postVisit

接收 Playwright 页面 和包含当前故事 idtitlename 的上下文对象的异步函数。在故事呈现之后在测试中执行。对于在故事呈现之后断言某些内容(例如 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 对象,但在某些挂钩中,我们鼓励您尽可能地在故事的播放函数中进行测试。

渲染生命周期

要可视化使用这些挂钩的测试生命周期,请考虑一个简化版本的测试代码,该代码是为 Storybook 中的每个故事自动生成的。

// 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 函数,该函数在测试故事之前将浏览器置于正确的环境中。您可以覆盖此行为,以防您可能想要修改浏览器的行为。例如,您可能想要设置 cookie,或向访问的 URL 添加查询参数,或在到达 Storybook URL 之前进行一些身份验证。您可以通过覆盖 prepare 函数来做到这一点。

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

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

注意 如果你覆盖了默认的 prepare 行为,虽然这很强大,但你要负责正确地准备浏览器。未来对默认 prepare 函数的更改将不会包含在你的项目中,所以你必须注意即将发布的版本中的更改。

getHttpHeaders

测试运行器会发出一些 fetch 调用来检查 Storybook 实例的状态,并获取 Storybook 故事的索引。此外,它还会使用 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

当测试失败并且在渲染故事期间存在浏览器日志时,测试运行器会在错误消息旁边提供日志。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

在使用挂钩运行测试时,您可能想要从故事中获取信息,例如传递给它的参数或它的参数。测试运行器现在提供了一个 getStoryContext 实用函数,该函数获取当前故事的故事上下文。

假设你的故事是这样的。

// ./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 条目。您可以使用它来确定故事是否在测试运行器的上下文中呈现。如果您希望在测试运行器中运行时禁用故事中的某些功能,这可能很有用,尽管这很可能是边缘情况。

// 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>
    );
  },
};

鉴于此检查是在浏览器中进行的,它仅适用于以下情况

  • 在故事的渲染/模板函数内部
  • 在播放函数内部
  • 在 preview.js 内部
  • 在浏览器中执行的任何其他代码内部

食谱

您将在下面找到使用挂钩和实用程序函数来使用测试运行器实现不同事情的食谱。

预配置视窗大小

您可以使用 Playwright 的页面视窗实用程序 以编程方式更改测试的视窗大小。如果您使用 @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 无法处理测试项目中故事的数量。您可能拥有大量的 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 中目前存在一个 错误,这意味着测试不能与项目位于不同的驱动器上。要解决此问题,您需要将 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 中,导入和导出行被计为可覆盖的代码段,但在 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
标签