Storybook 测试运行器将您的所有 stories 转化为可执行的测试。
- 特性
- 工作原理
- Storybook 兼容性
- 快速开始
- CLI 选项
- 弹出配置
- 过滤测试 (实验性)
- 测试报告器
- 针对已部署的 Storybook 运行
- 在 CI 中运行
- 设置代码覆盖率
- 测试钩子 API
- 示例
- 故障排除
- 未来工作
- 贡献
特性
- ⚡️ 零配置设置
- 💨 对所有 stories 进行冒烟测试
- ▶️ 使用 play 函数测试 stories
- 🏃 在无头浏览器中并行测试您的 stories
- 👷 通过直接链接到 story 获取错误反馈
- 🐛 使用 addon-interactions 在实时浏览器中以可视化和交互方式调试它们
- 🎭 由 Jest 和 Playwright 提供支持
- 👀 监听模式、过滤器以及您期望的便利功能
- 📔 代码覆盖率报告
工作原理
在这篇博客文章中详细了解 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 |
快速开始
- 安装测试运行器
yarn add @storybook/test-runner -D
- 将
test-storybook
脚本添加到 package.json
{
"scripts": {
"test-storybook": "test-storybook"
}
}
-
或者,遵循文档编写交互测试并使用addon-interactions在 Storybook 中通过交互式调试器可视化交互。
-
运行 Storybook (测试运行器针对正在运行的 Storybook 实例运行)
yarn storybook
- 运行测试运行器
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.json
或 index.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 运行测试,您可能需要结合使用 concurrently、http-server 和 wait-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 渲染前后访问测试生命周期。
有三个钩子:setup
、preVisit
和 postVisit
。setup
在所有测试运行前执行一次。preVisit
和 postVisit
在测试中,于 story 渲染前后执行。
这三个函数都可以在配置文件 .storybook/test-runner.js
中设置,该文件可以选择性地导出这些函数中的任何一个。
[!Note]
preVisit
和postVisit
函数将对所有 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 的 id
、title
和 name
的上下文对象的异步函数。在测试中,于 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 的 id
、title
和 name
的上下文对象的异步函数。在测试中,于 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
函数接收一个包含以下内容的对象
browserContext
: 一个 Playwright Browser Context 实例page
: 一个 Playwright Page 实例。testRunnerConfig
: 测试运行器配置对象,来自.storybook/test-runner.js
。
供参考,请使用默认的 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;
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
如果是这种情况,有两种潜在的解决方案
- 将
playwright
作为直接依赖安装。之后您可能需要运行yarn playwright install
,以便安装 Playwright 的浏览器二进制文件。 - 将您的包管理器的链接器模式切换到
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
版本,也是大多数用户使用的稳定版本
发布流程
- 所有 PR 应指向
next
分支,该分支依赖于 Storybook 的next
版本。 - 合并后,此包的新版本将在
next
NPM 标签上发布。 - 如果更改包含需要回补到稳定版本的 bugfix,请在 PR 描述中注明。
- 标记为
pick
的 PR 将被 cherry-pick 回prerelease
分支,并在prerelease
npm 标签上生成发布。 - 验证后,
prerelease
PR 将被合并回main
分支,这将在latest
npm 标签上生成发布。