文档
Storybook 文档

测试运行器

Storybook 测试运行器将你的所有故事都转换为可执行的测试。它由 JestPlaywright 提供支持。

  • 对于那些 没有播放函数 的故事:它会验证故事是否渲染且没有错误。
  • 对于那些 有播放函数 的故事:它还会检查播放函数中是否存在错误,以及所有断言是否都通过了。

这些测试在真实的浏览器中运行,可以通过 命令行 或你的 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 URL
test-storybook --url http://the-storybook-url-here.com
--browsers定义运行测试的浏览器。可以是以下一个或多个:chromium、firefox、webkit
test-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 运行测试运行器。这是一个依赖于第三方库的食谱,也就是说,concurrentlyhttp-serverwait-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 目录中添加一个新的配置文件并按如下方式设置它们。

.storybook/test-runner.ts
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 函数之外,所有其他函数都异步运行。preVisitpostVisit 函数都包含两个额外的参数,一个Playwright 页面和一个上下文对象,其中包含故事的 idtitlename

当测试运行器执行时,您现有的测试将经历以下生命周期。

  • 在所有测试运行之前执行 setup 函数。
  • 生成包含所需信息的上下文对象。
  • Playwright 导航到故事的页面。
  • 执行 preVisit 函数。
  • 渲染故事,并执行任何现有的 play 函数。
  • 执行 postVisit 函数。

(实验性) 过滤测试

当您在 Storybook 上运行测试运行器时,它会默认测试每个故事。但是,如果您想过滤测试,可以使用 tags 配置选项。Storybook 最初引入此功能是为了为故事生成自动文档。但它可以进一步扩展以配置测试运行器以根据提供的标签运行测试,使用类似的配置选项或通过 CLI 标志(例如 --includeTags--excludeTags--skipTags),仅在最新的稳定版本(0.15 或更高版本)中可用。下面列出了可用的选项和如何使用它们的概述。

选项描述
exclude如果故事与提供的标签匹配,则阻止测试。
include仅定义要测试的故事子集,如果它们与启用的标签匹配。
skip如果故事与提供的标签匹配,则跳过测试。
.storybook/test-runner.ts
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标志运行测试运行器,并将它们排除在测试之外。当您想要排除尚未准备好进行测试或与您的测试无关的故事时,这很有用。例如

MyComponent.stories.ts|tsx
// 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标签运行测试,您可以按如下方式调整您的配置

MyComponent.stories.ts|tsx
// 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标志运行测试运行器。使用此选项运行测试将导致测试运行器忽略并相应地在测试结果中标记它们,表明测试已暂时禁用。例如

MyComponent.stories.ts|tsx
// 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 作为输入,并返回一个包含需要设置的标头的对象。

.storybook/test-runner.ts
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 的内部组件(例如 argsparameters)使您的测试更具可读性和可维护性。下面列出了可用的助手以及如何使用它们的概述。

.storybook/test-runner.ts
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 的页面视口大小以使用故事参数中定义的视口大小,您可以按如下方式操作

.storybook/test-runner.js
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的辅助函数,您可以使用它来确保页面完全加载并准备好在运行测试之前。例如

.storybook/test-runner.ts
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"
  }
}

CLI 中的错误输出太短默认情况下,测试运行器会将错误输出截断为 1000 个字符,您可以在浏览器中的 Storybook 中直接查看完整输出。但是,如果要更改此限制,可以通过将 DEBUG_PRINT_LIMIT 环境变量设置为任意数字来实现,例如,DEBUG_PRINT_LIMIT=5000 yarn test-storybook

在其他 CI 环境中运行测试运行器

由于测试运行器基于 Playwright,因此您可能需要根据您的 CI 设置使用特定的 Docker 镜像或其他配置。在这种情况下,您可以参考 Playwright CI 文档 获取更多信息。

按标签过滤的测试执行不正确

如果您启用了使用标签过滤测试并在 includeexclude 列表中提供了类似的标签,则测试运行器将根据 exclude 列表执行测试并忽略 include 列表。为避免这种情况,请确保提供给 includeexclude 列表的标签有所不同。

测试运行器本身不支持 Yarn PnP

如果您在启用了较新版本的 Yarn 和 Plug'n'Play (PnP) 的项目中启用了测试运行器,则测试运行器可能无法按预期工作,并且在运行测试时可能会生成以下错误

PlaywrightError: jest-playwright-preset: Cannot find playwright package to use chromium

这是因为测试运行器使用了社区维护的包 jest-playwright-preset,该包仍需要支持此功能。要解决此问题,您可以将 nodeLinker 设置切换到 node-modules,或者将 Playwright 作为直接依赖项安装到您的项目中,然后通过 install 命令添加浏览器二进制文件。

了解其他 UI 测试