Storybook 测试运行器将所有故事转换为可执行测试。
- 功能
- 工作原理
- Storybook 兼容性
- 入门
- CLI 选项
- 弹出配置
- 过滤测试(实验性)
- 测试报告程序
- 针对已部署的 Storybook 运行
- 在 CI 中运行
- 设置代码覆盖率
- 测试钩子 API
- 食谱
- 故障排除
- 未来工作
- 贡献
功能
- ⚡️ 零配置设置
- 💨 对所有故事进行冒烟测试
- ▶️ 使用播放功能测试故事
- 🏃 在无头浏览器中并行测试你的故事
- 👷 从带有指向故事的直接链接的错误中获得反馈
- 🐛 使用 addon-interactions 在实时浏览器中以可视化和交互的方式调试它们
- 🎭 由 Jest 和 Playwright 提供支持
- 👀 观察模式、过滤器以及你期望的便利性
- 📔 代码覆盖率报告
工作原理
在 这篇博文 中查看关于 Storybook 的交互式测试的详细公告,或观看 这段视频 了解它的实际操作。
Storybook 测试运行器使用 Jest 作为运行器,使用 Playwright 作为测试框架。你的每个 .stories
文件都会转换为一个规范文件,每个故事都会成为一个测试,在无头浏览器中运行。
测试运行器设计简单 - 它只是从运行的 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 |
入门
- 安装测试运行器
yarn add @storybook/test-runner -D
- 在你的 package.json 中添加一个
test-storybook
脚本
{
"scripts": {
"test-storybook": "test-storybook"
}
}
-
可选地,按照 文档 的说明编写交互式测试,并使用 addon-interactions 在 Storybook 中使用交互式调试器可视化交互。
-
运行 Storybook(测试运行器针对正在运行的 Storybook 实例运行)
yarn storybook
- 运行测试运行器
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: ['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 的静态分析限制。
一旦您的故事有了自己的自定义标签,您就可以通过测试运行器配置文件中的标签属性来过滤它们。您也可以使用 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.json
或index.json
中,这是一个所有故事的静态索引。
这对于针对已部署的 Storybook 运行尤其有用,因为index.json
保证与您正在测试的 Storybook 同步。在默认的基于故事文件的模式下,您的本地故事文件可能不同步 - 或者您甚至可能无法访问源代码。
此外,无法直接针对.mdx
故事或使用addon-svelte-csf
编写 Svelte 原生故事时的自定义 CSF 方言运行测试运行器。在这些情况下,必须使用index.json
模式。
要以index.json
模式运行,首先确保您的 Storybook 具有 v4 index.json
文件。您可以在导航到以下位置时找到它:
https://your-storybook-url-here.com/index.json
它应该是一个 JSON 文件,第一个键应该是"v": 4
,后面跟着一个名为"entries"
的键,其中包含一个从故事 ID 到 JSON 对象的映射。
在 Storybok 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 操作中,一旦 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 运行测试,您可能需要使用涉及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
注意 在本地构建 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
命令将遵守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 合并命令合并覆盖率文件。
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 快照等用例,测试运行器导出了可以全局覆盖的测试钩子。这些钩子让您在渲染故事之前和之后访问测试生命周期。
共有三个钩子:setup
、preVisit
和 postVisit
。setup
在所有测试运行之前执行一次。preVisit
和 postVisit
在测试中渲染故事之前和之后执行。
所有三个函数都可以在配置文件 .storybook/test-runner.js
中设置,该文件可以选择性地导出任何这些函数。
注意
preVisit
和postVisit
函数将对所有故事执行。
设置
在所有测试运行之前执行一次的异步函数。对于设置与节点相关的配置很有用,例如扩展 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 页面 和一个包含当前故事的 id
、title
和 name
的上下文对象的异步函数。在渲染故事之前,在测试中执行。对于在故事渲染之前配置页面很有用,例如设置视口大小。
// .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 页面 和一个包含当前故事的 id
、title
和 name
的上下文对象的异步函数。在渲染故事之后,在测试中执行。对于在故事渲染之后断言事物很有用,例如 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
函数,它会在测试故事之前将浏览器置于正确的环境中。您可以覆盖此行为,以防您想要修改浏览器的行为。例如,您可能希望设置一个 cookie,或向访问的 URL 添加查询参数,或在到达 Storybook URL 之前进行一些身份验证。您可以通过覆盖 prepare
函数来做到这一点。
prepare
函数接收一个包含以下内容的对象
browserContext
: Playwright 浏览器上下文 实例page
: Playwright 页面 实例。testRunnerConfig
: 来自.storybook/test-runner.js
的测试运行器配置对象。
作为参考,请使用 默认的 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
属性包含三个选项:include | exclude | skip
,每个选项都接受一个字符串数组
// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
tags: {
include: [], // string array, e.g. ['test-only']
exclude: [], // string array, e.g. ['design', 'docs-only']
skip: [], // string array, e.g. ['design']
},
};
export default config;
tags
用于过滤您的测试。了解更多 信息。
日志级别
当测试失败且在渲染故事期间有浏览器日志时,测试运行器会在错误消息旁边提供日志。logLevel
属性定义了应该显示哪些类型的日志
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>
);
},
};
鉴于此检查正在浏览器中进行,它仅适用于以下场景
- 在故事的渲染/模板函数内
- 在 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
如果是这种情况,则有两个可能的解决方案
- 将
playwright
安装为直接依赖项。您可能需要在之后运行yarn playwright install
,以便安装 Playwright 的浏览器二进制文件。 - 将您的包管理器链接器模式切换为
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 无法处理您项目中测试的故事数量。也许您有很多故事,或者您的 CI 的内存配置很低。
无论哪种方式,要修复它,您应该通过将 --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 覆盖率的一对一替换。
未来工作
未来计划包括添加对以下功能的支持
- 📄 运行附加组件报告
- ⚙️ 通过单个命令在测试运行器中生成 Storybook
贡献
我们欢迎对测试运行器做出贡献!
分支结构
- next - npm 上的
next
版本,也是大部分开发工作所在的开发分支 - prerelease - npm 上的
prerelease
版本,用于测试对main
分支的最终更改 - main - npm 上的
latest
版本,也是大多数用户使用的稳定版本
发布流程
- 所有 PR 应该针对
next
分支,该分支依赖于 Storybook 的next
版本。 - 合并后,此软件包的新版本将在
next
NPM 标签上发布。 - 如果更改包含需要修补到稳定版本的错误修复,请在 PR 说明中注明。
- 标记为
pick
的 PR 将被 cherry-picked 回prerelease
分支,并在prerelease
npm 标签上生成一个版本。 - 验证后,
prerelease
PR 将被合并回main
分支,这将在latest
npm 标签上生成一个版本。