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

Storybook 测试运行器

Storybook 故事的测试运行器

在 Github 上查看

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

特性

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

工作原理

在本博客文章中详细了解 Storybook 交互测试的公告,或观看此视频查看其实际效果。

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

测试运行器设计简单——它只是访问正在运行的 Storybook 实例中的每个故事,并确保组件没有失败

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

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

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 [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 (实验性) 只测试匹配指定标签的故事,标签之间用逗号分隔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: ['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'],
};

注意 您不能从另一个文件导入常量并用它们来定义故事中的标签。您的故事或元信息中的标签必须内联定义为字符串数组。这是 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 字段以及您想要的选项来进一步配置 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 模式与 watch 模式不兼容。

在 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 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;

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

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

在所有测试运行前执行一次的异步函数。对于设置 Node 相关的配置很有用,例如扩展 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 Page 实例和一个包含当前故事 idtitlename 的上下文对象。在单个测试中,于故事渲染前执行。对于在故事渲染前配置 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 (已弃用)

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

postVisit

一个异步函数,接收一个 Playwright Page 实例和一个包含当前故事 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 对象,但我们鼓励您尽可能多地在故事的 play 函数中进行测试。

渲染生命周期

为了使用这些钩子可视化测试生命周期,请考虑为您的 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-only']
    exclude: [], // string array, e.g. ['design', 'docs-only']
    skip: [], // string array, e.g. ['design']
  },
};
export default config;

tags 用于过滤您的测试。在此处了解更多信息here

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

在使用钩子运行测试时,您可能想从故事中获取信息,例如传递给它的参数或其 args。测试运行器现在提供了一个 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>
    );
  },
};

考虑到此检查发生在浏览器中,它仅适用于以下场景

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

示例

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

预配置视口大小

您可以使用 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 并将其与测试运行器一起使用,以测试组件的可访问性。如果您使用 @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 无法处理测试您项目中的故事数量。也许您有大量故事,或者您的 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 覆盖率的 1:1 替代品。

未来工作

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

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

贡献

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

分支结构

  • next - npm 上的 next 版本,以及大部分工作发生的开发分支
  • prerelease - npm 上的 prerelease 版本,主分支的最终更改在此处进行测试
  • 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 标签上生成一个发布版本。
作者
  • depressing_utopian
    depressing_utopian
支持框架
    Angular
    Ember
    HTML
    Marko
    Mithril
    Preact
    Rax
    React
    Riot
    Svelte
    Vue
    Web Components
标签