加入直播会话:周四,美国东部时间上午 11 点,Storybook 9 发布 & 问答

Storybook 测试运行器

用于 Storybook 故事的测试运行器

在 Github 上查看

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

特性

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

工作原理

这篇博客文章中详细了解 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 输出中显示为“skipped”,而排除的测试则根本不会出现。跳过的测试可用于指示暂时禁用的测试。

测试报告器

测试运行器使用默认的 Jest 报告器,但您可以通过如上所述弹出配置并覆盖(或合并)reporters 属性来添加其他报告器。

此外,如果您向 test-storybook 传递 --junit,测试运行器会将 jest-junit 添加到报告器列表中,并生成 JUnit XML 格式的测试报告。您可以通过设置特定的 JEST_JUNIT_* 环境变量或在 package.json 中定义带有您所需选项的 jest-junit 字段来进一步配置 jest-junit 的行为,这些配置在生成报告时将生效。您可以在此处查看所有可用选项:https://github.com/jest-community/jest-junit#configuration

针对已部署的 Storybook 运行

默认情况下,测试运行器假定您正在针对本地服务的 Storybook(端口 6006)运行它。如果您想定义目标 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 模式与监视模式不兼容。

在 CI 中运行

如果您想将测试运行器添加到 CI,有几种方法可以实现

1. 在 Github Actions 部署中针对已部署的 Storybook 运行

在 Github Actions 上,一旦 Vercel、Netlify 等服务进行部署运行,它们会遵循一种模式,即发出包含在 deployment_status.target_url 下新生成 URL 的 deployment_status 事件。您可以使用该 URL 并将其设置为测试运行器的 TARGET_URL

这是一个基于此运行测试的 Action 示例

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 插件或其 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 使用。

注意 如果您的组件未在报告中显示,并且您使用的是 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)使用的 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

测试运行器渲染一个故事并执行其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 的上下文对象。在故事渲染后,在单个测试内部执行。适用于在故事渲染后断言(asserting)某些事物,例如 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 属性包含三个选项: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

在使用钩子运行测试时,您可能希望获取故事的信息,例如传递给它的参数或其 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 用户代理

测试运行器会在浏览器的 user agent 中添加一个 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>
    );
  },
};

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

  • 故事的 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;

无障碍性测试

您可以安装 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 (Plug n' Play) 支持

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 目前存在一个错误,这意味着测试不能位于项目所在驱动器之外的单独驱动器上。要解决此问题,您需要将 TEMP 环境变量设置为项目所在驱动器上的临时文件夹。在 GitHub Actions 上看起来是这样的

env:
  # Workaround for https://github.com/facebook/jest/issues/8536
  TEMP: ${{ runner.temp }}

将测试运行器添加到其他 CI 环境

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

合并测试覆盖率结果导致覆盖率错误

合并来自测试运行器的测试覆盖率报告与来自其他工具(例如 Jest)的报告后,如果最终结果与您预期的不同。原因如下:

测试运行器使用 babel 作为 coverage provider,它在评估代码覆盖率时有特定的行为方式。如果您的其他报告使用的 coverage provider 与 babel 不同,例如 v8,它们将以不同的方式评估覆盖率。合并后,结果很可能是错误的。

示例:在 v8 中,import 和 export 行被计为可覆盖的代码片段,而在 babel 中则不是。这会影响覆盖率百分比的计算。

虽然测试运行器不提供 v8 作为 coverage provider 选项,但如果可能,建议您将应用程序的 Jest 配置设置为使用 coverageProvider: 'babel',以便报告符合预期并正确合并。

有关更多背景信息,此处有一些解释,说明为何 v8 不能完全替代 Babel/Istanbul 覆盖率。

未来工作

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

  • 📄 运行插件报告
  • ⚙️ 通过单个命令使用测试运行器启动 Storybook

贡献

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

分支结构

  • next - npm 上的 next 版本,以及大部分工作发生的开发分支
  • prerelease - npm 上的 prerelease 版本,用于测试对 main 的最终更改
  • main - npm 上的 latest 版本以及大多数用户使用的稳定版本

发布流程

  1. 所有 PR 应针对 next 分支,该分支依赖于 Storybook 的 next 版本。
  2. 合并后,此包的新版本将发布到 next NPM 标签。
  3. 如果更改包含需要回补到稳定版本的错误修复,请在 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
标签