测试运行器
Storybook 测试运行器将你的所有故事都转换为可执行的测试。它由 Jest 和 Playwright 提供支持。
这些测试在真实的浏览器中运行,可以通过 命令行 或你的 CI 服务器 执行。
设置
测试运行器是一个独立的、与框架无关的实用程序,与你的 Storybook 并行运行。你需要采取一些额外的步骤来正确设置它。下面详细介绍了我们建议的配置和执行方法。
运行以下命令进行安装。
npm install @storybook/test-runner --save-dev
更新你的 package.json
脚本并启用测试运行器。
{
"scripts": {
"test-storybook": "test-storybook"
}
}
使用以下命令启动你的 Storybook
npm run storybook
Storybook 的测试运行器需要一个本地运行的 Storybook 实例或一个已发布的 Storybook 才能运行所有现有的测试。
最后,打开一个新的终端窗口并使用以下命令运行测试运行器
npm run test-storybook
配置
测试运行器为 Storybook 提供了零配置支持。但是,你可以运行 test-storybook --eject
以获得更细粒度的控制。它会在项目的根目录生成一个 test-runner-jest.config.js
文件,你可以修改它。此外,你可以扩展生成的配置文件并提供 testEnvironmentOptions,因为测试运行器也在内部使用了 jest-playwright。
CLI 选项
测试运行器由 Jest 提供支持,并接受其 CLI 选项 的一个子集(例如,--watch
、--maxWorkers
)。如果你已经在项目中使用了任何这些标志,则应该能够将它们迁移到 Storybook 的测试运行器中,而不会出现任何问题。下面列出了所有可用的标志以及使用它们的示例。
选项 | 描述 |
---|---|
--help | 输出使用信息test-storybook --help |
-s , --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 URLtest-storybook --url http://the-storybook-url-here.com |
--browsers | 定义运行测试的浏览器。可以是以下一个或多个:chromium、firefox、webkittest-storybook --browsers firefox chromium |
--maxWorkers [amount] | 指定工作池为运行测试而生成的 worker 的最大数量test-storybook --maxWorkers=2 |
--testTimeout [amount] | 定义测试在自动标记为失败之前可以运行的最长时间(以毫秒为单位)。适用于长时间运行的测试test-storybook --testTimeout=60000 |
--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 [index/count] | 需要 CI。将测试套件执行拆分为多台机器test-storybook --shard=1/8 |
--failOnConsole | 使测试在浏览器控制台错误时失败test-storybook --failOnConsole |
--includeTags | 实验性功能 如果启用 标签 与其匹配,则定义要测试的故事的子集。 test-storybook --includeTags="test-only, pages" |
--excludeTags | 实验性功能 如果启用 标签 与其匹配,则阻止故事被测试。 test-storybook --excludeTags="no-tests, tokens" |
--skipTags | 实验性功能 配置测试运行器,跳过与提供的标签匹配的故事的测试。 test-storybook --skipTags="skip-test, layout" |
npm run test-storybook -- --watch
针对已部署的 Storybook 运行测试
默认情况下,测试运行器假设您正在端口 6006
上针对本地服务 Storybook 运行它。如果您想定义要针对已部署的 Storybook 运行的目标 URL,则可以使用 --url
标志。
npm run test-storybook -- --url https://the-storybook-url-here.com
或者,您可以设置 TARGET_URL
环境变量并运行测试运行器。
TARGET_URL=https://the-storybook-url-here.com yarn test-storybook
设置 CI 以运行测试
您还可以配置测试运行器以在 CI 环境中运行测试。下面记录了一些食谱,以帮助您入门。
通过 Github Actions 部署针对已部署的 Storybook 运行测试
如果您使用 Vercel 或 Netlify 等服务发布您的 Storybook,它们会在 GitHub Actions 中发出 deployment_status
事件。您可以使用它并将 deployment_status.target_url
设置为 TARGET_URL
环境变量。方法如下:
# .github/workflows/storybook-tests.yml
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-file: '.nvmrc'
- name: Install dependencies
run: yarn
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Storybook tests
run: yarn test-storybook
env:
TARGET_URL: '${{ github.event.deployment_status.target_url }}'
要使此示例正常工作,已发布的 Storybook 必须可公开访问。如果需要身份验证,我们建议使用下面的食谱运行测试服务器。
针对未部署的 Storybook 运行测试
您可以使用您的 CI 提供商(例如 GitHub Actions、GitLab Pipelines、CircleCI)来构建并针对您构建的 Storybook 运行测试运行器。这是一个依赖于第三方库的食谱,也就是说,concurrently、http-server 和 wait-on 用于构建 Storybook 并使用测试运行器运行测试。
# .github/workflows/storybook-tests.yml
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-file: '.nvmrc'
- name: Install dependencies
run: yarn
- name: Install Playwright
run: npx playwright install --with-deps
- name: Build Storybook
run: yarn build-storybook --quiet
- name: Serve Storybook and run tests
run: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:127.0.0.1:6006 && yarn test-storybook"
默认情况下,Storybook 将 构建 输出到 storybook-static
目录。如果您使用的是不同的构建目录,则需要相应地调整食谱。
Chromatic 和测试运行器之间有什么区别?
测试运行器是一个通用的测试工具,可以在本地或 CI 上运行,并可以配置或扩展以运行各种测试。
Chromatic 是一种基于云的服务,它运行视觉和组件测试(以及即将推出的可访问性测试),无需设置测试运行器。它还可以与您的 Git 提供商同步并管理私有项目的访问控制。
但是,在某些情况下,您可能希望将测试运行器和 Chromatic 配对使用。
- 在本地使用它,并在您的 CI 上使用 Chromatic。
- 使用 Chromatic 进行视觉和组件测试,并使用测试运行器运行其他自定义测试。
高级配置
测试钩子 API
测试运行器渲染一个故事,如果存在,则执行其播放函数。但是,某些行为无法通过在浏览器中执行的播放函数来实现。例如,如果您希望测试运行器为您拍摄视觉快照,这可以通过 Playwright/Jest 实现,但必须在 Node 中执行。
测试运行器导出可以全局覆盖的测试钩子,以启用视觉或 DOM 快照等用例。这些钩子允许您在渲染故事之前和之后访问测试生命周期。下面列出了可用的钩子和如何使用它们的概述。
钩子 | 描述 |
---|---|
prepare | 为测试准备浏览器async prepare({ page, browserContext, testRunnerConfig }) {} |
setup | 在所有测试运行之前执行一次setup() {} |
preVisit | 在最初访问和渲染浏览器中的故事之前执行async preVisit(page, context) {} |
postVisit | 在访问并完全渲染故事后执行async postVisit(page, context) {} |
这些测试钩子是实验性的,可能会发生重大更改。我们建议您在故事的播放函数中尽可能多地进行测试。
要启用钩子 API,您需要在 Storybook 目录中添加一个新的配置文件并按如下方式设置它们。
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
// Hook that is executed before the test runner starts running tests
setup() {
// Add your configuration here.
},
/* Hook to execute before a story is initially visited before being rendered in the browser.
* The page argument is the Playwright's page object for the story.
* The context argument is a Storybook object containing the story's id, title, and name.
*/
async preVisit(page, context) {
// Add your configuration here.
},
/* Hook to execute after a story is visited and fully rendered.
* The page argument is the Playwright's page object for the story
* The context argument is a Storybook object containing the story's id, title, and name.
*/
async postVisit(page, context) {
// Add your configuration here.
},
};
export default config;
除了 setup
函数之外,所有其他函数都异步运行。preVisit
和 postVisit
函数都包含两个额外的参数,一个Playwright 页面和一个上下文对象,其中包含故事的 id
、title
和 name
。
当测试运行器执行时,您现有的测试将经历以下生命周期。
- 在所有测试运行之前执行
setup
函数。 - 生成包含所需信息的上下文对象。
- Playwright 导航到故事的页面。
- 执行
preVisit
函数。 - 渲染故事,并执行任何现有的
play
函数。 - 执行
postVisit
函数。
(实验性) 过滤测试
当您在 Storybook 上运行测试运行器时,它会默认测试每个故事。但是,如果您想过滤测试,可以使用 tags
配置选项。Storybook 最初引入此功能是为了为故事生成自动文档。但它可以进一步扩展以配置测试运行器以根据提供的标签运行测试,使用类似的配置选项或通过 CLI 标志(例如 --includeTags
、--excludeTags
、--skipTags
),仅在最新的稳定版本(0.15
或更高版本)中可用。下面列出了可用的选项和如何使用它们的概述。
选项 | 描述 |
---|---|
exclude | 如果故事与提供的标签匹配,则阻止测试。 |
include | 仅定义要测试的故事子集,如果它们与启用的标签匹配。 |
skip | 如果故事与提供的标签匹配,则跳过测试。 |
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
tags: {
include: ['test-only', 'pages'],
exclude: ['no-tests', 'tokens'],
skip: ['skip-test', 'layout'],
},
};
export default config;
使用 CLI 标志运行测试优先于配置文件中提供的选项,并将覆盖配置文件中可用的选项。
禁用测试
如果要阻止测试运行器测试特定的故事,您可以使用自定义标签配置您的故事,在测试运行器配置文件中启用它,或者使用--excludeTags
CLI标志运行测试运行器,并将它们排除在测试之外。当您想要排除尚未准备好进行测试或与您的测试无关的故事时,这很有用。例如
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
tags: ['no-tests'], // 👈 Provides the `no-tests` tag to all stories in this file
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const ExcludeStory: Story = {
//👇 Adds the `no-tests` tag to this story to exclude it from the tests when enabled in the test-runner configuration
tags: ['no-tests'],
};
运行部分故事的测试
要允许测试运行器仅对特定故事或部分故事运行测试,您可以使用自定义标签配置故事,在测试运行器配置文件中启用它,或者使用--includeTags
CLI标志运行测试运行器,并将它们包含在您的测试中。例如,如果您想根据test-only
标签运行测试,您可以按如下方式调整您的配置
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
tags: ['test-only'], // 👈 Provides the `test-only` tag to all stories in this file
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const IncludeStory: Story = {
//👇 Adds the `test-only` tag to this story to be included in the tests when enabled in the test-runner configuration
tags: ['test-only'],
};
组件故事的标签应用应该在组件级别(使用meta
)或故事级别进行。在 Storybook 中不支持跨故事导入标签,并且不会按预期工作。
跳过测试
如果您想跳过对特定故事或部分故事运行测试,您可以使用自定义标签配置您的故事,在测试运行器配置文件中启用它,或使用--skipTags
CLI标志运行测试运行器。使用此选项运行测试将导致测试运行器忽略并相应地在测试结果中标记它们,表明测试已暂时禁用。例如
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
tags: ['skip-test'], // 👈 Provides the `skip-test` tag to all stories in this file
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const SkipStory: Story = {
//👇 Adds the `skip-test` tag to this story to allow it to be skipped in the tests when enabled in the test-runner configuration
tags: ['skip-test'],
};
已部署 Storybook 的身份验证
如果您使用需要身份验证来托管您的 Storybook 的安全托管提供商,您可能需要设置 HTTP 标头。这主要是由于测试运行器如何通过 fetch 请求和 Playwright 检查实例的状态及其故事的索引。为此,您可以修改测试运行器配置文件以包含getHttpHeaders
函数。此函数以 fetch 调用和页面访问的 URL 作为输入,并返回一个包含需要设置的标头的对象。
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
getHttpHeaders: async (url) => {
const token = url.includes('prod') ? 'prod-token' : 'dev-token';
return {
Authorization: `Bearer ${token}`,
};
},
};
export default config;
助手
测试运行器导出了一些助手,可用于通过访问 Storybook 的内部组件(例如 args
、parameters
)使您的测试更具可读性和可维护性。下面列出了可用的助手以及如何使用它们的概述。
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext, waitForPageReady } from '@storybook/test-runner';
const config: TestRunnerConfig = {
// Hook that is executed before the test runner starts running tests
setup() {
// Add your configuration here.
},
/* Hook to execute before a story is initially visited before being rendered in the browser.
* The page argument is the Playwright's page object for the story.
* The context argument is a Storybook object containing the story's id, title, and name.
*/
async preVisit(page, context) {
// Add your configuration here.
},
/* Hook to execute after a story is visited and fully rendered.
* The page argument is the Playwright's page object for the story
* The context argument is a Storybook object containing the story's id, title, and name.
*/
async postVisit(page, context) {
// Get the entire context of a story, including parameters, args, argTypes, etc.
const storyContext = await getStoryContext(page, context);
// This utility function is designed for image snapshot testing. It will wait for the page to be fully loaded, including all the async items (e.g., images, fonts, etc.).
await waitForPageReady(page);
// Add your configuration here.
},
};
export default config;
使用测试运行器访问故事信息
如果您需要访问有关故事的信息,例如其参数,测试运行器包含一个名为getStoryContext
的辅助函数,您可以使用它来检索它。然后,您可以根据需要使用它进一步自定义您的测试。例如,如果您需要配置 Playwright 的页面视口大小以使用故事参数中定义的视口大小,您可以按如下方式操作
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
const { MINIMAL_VIEWPORTS } = require('@storybook/addon-viewport');
const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 };
const config: TestRunnerConfig = {
async preVisit(page, story) {
// Accesses the story's parameters and retrieves the viewport used to render it
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,
// Converts the viewport size from percentages to numbers
[screen]: parseInt(size),
}),
{},
);
// Configures the Playwright page to use the viewport size
page.setViewportSize(viewportSize);
} else {
page.setViewportSize(DEFAULT_VIEWPORT_SIZE);
}
},
};
export default config;
使用资源
如果您正在运行一组特定的测试(例如,图像快照测试),测试运行器提供了一个名为waitForPageReady
的辅助函数,您可以使用它来确保页面完全加载并准备好在运行测试之前。例如
import type { TestRunnerConfig } from '@storybook/test-runner';
import { 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) {
// Awaits for the page to be loaded and available including assets (e.g., fonts)
await waitForPageReady(page);
// Generates a snapshot file based on the story identifier
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotsDir,
customSnapshotIdentifier: context.id,
});
},
};
export default config;
Index.json 模式
测试运行器在测试本地 Storybook 时会将您的故事文件转换为测试。对于远程 Storybook,它使用 Storybook 的index.json(以前为stories.json
)文件(所有故事的静态索引)来运行测试。
为什么?
假设您遇到本地和远程 Storybook 出现不同步的情况,或者您可能甚至无法访问代码。在这种情况下,index.json
文件保证是您正在测试的已部署 Storybook 的最准确表示。要使用此功能测试本地 Storybook,请按如下方式使用--index-json
标志
npm run test-storybook -- --index-json
index.json
模式与监视模式不兼容。
如果您需要禁用它,请使用--no-index-json
标志
npm run test-storybook -- --no-index-json
如何检查我的 Storybook 是否有index.json
文件?
Index.json 模式需要一个index.json
文件。打开一个浏览器窗口并导航到您的已部署 Storybook 实例(例如 https://your-storybook-url-here.com/index.json
)。您应该会看到一个以"v": 3
键开头,紧随其后是另一个名为“stories”的键的 JSON 文件,该键包含故事 ID 到 JSON 对象的映射。如果是这种情况,您的 Storybook 支持index.json 模式。
故障排除
测试运行器似乎不稳定并且不断超时
如果您的测试超时并显示以下消息
Timeout - Async callback was not invoked within the 15000 ms timeout specified by jest.setTimeout
这可能是 Playwright 无法处理您项目中故事数量的测试。也许您有大量的故事,或者您的 CI 环境的 RAM 配置非常低。在这种情况下,您应该通过如下调整命令来限制并行运行的工作程序数量
{
"scripts": {
"test-storybook:ci": "yarn test-storybook --maxWorkers=2"
}
}