Storybook 测试运行器

Storybook 故事的测试运行器

在 Github 上查看

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

特性

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

工作原理

这篇博文 中详细了解使用 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

入门

  1. 安装测试运行器
yarn add @storybook/test-runner -D
  1. 在您的 package.json 中添加一个 test-storybook 脚本
{
  "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] 指定工作池将为运行测试而生成的 worker 的最大数量 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 的静态分析造成的限制。

一旦您的故事拥有了自己的自定义标签,您就可以通过测试运行器配置文件中的 tags 属性 对其进行过滤。您也可以使用 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.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 事件的模式,该事件包含 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 的测试,您可能需要使用涉及 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 组件、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 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 函数将对所有故事执行。

设置

异步函数,在所有测试运行之前执行一次。用于设置与节点相关的配置,例如扩展 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。在渲染故事之前在测试中执行。用于在渲染故事之前配置页面,例如设置视口大小。

// .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。在渲染故事之后在测试中执行。用于在渲染故事后断言内容,例如 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 函数接收一个包含以下内容的对象:

作为参考,请使用默认的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

当测试失败并且在渲染故事期间存在浏览器日志时,测试运行器会将日志与错误消息一起提供。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>
    );
  },
};

鉴于此检查是在浏览器中发生的,因此它仅适用于以下场景

  • 在故事的渲染/模板函数内部
  • 在播放函数内部
  • 在preview.js内部
  • 在浏览器中执行的任何其他代码内部

食谱

您将在下面找到使用钩子和实用程序函数来使用测试运行器实现不同目标的示例。

预配置视口大小

您可以使用Playwright的页面视口实用程序以编程方式更改测试的视口大小。如果您使用@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

如果是这种情况,则有两种可能的解决方案

  1. playwright作为直接依赖项安装。您可能需要运行yarn playwright install,以便安装Playwright的浏览器二进制文件。
  2. 将包管理器的链接器模式切换到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无法处理项目中故事的数量。也许您有大量的story,或者您的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作为覆盖率提供程序,在评估代码覆盖率时其行为方式有所不同。如果您的其他报告碰巧使用与babel不同的覆盖率提供程序(例如v8),它们将以不同的方式评估覆盖率。合并后,结果可能会出错。

示例:在v8中,导入和导出行被计为可覆盖的代码段,但在babel中,它们不被计为可覆盖的代码段。这会影响覆盖率百分比的计算。

虽然测试运行器没有提供v8作为覆盖率提供程序的选项,但建议您(如果可以)将应用程序的Jest配置设置为使用coverageProvider: 'babel',以便报告按预期排列并正确合并。

有关更多上下文,此处提供了一些说明,说明为什么v8不是Babel/Istanbul覆盖率的1:1替代。

未来工作

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

  • 📄 运行插件报告
  • ⚙️ 通过测试运行器在一个命令中生成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-picked回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
标签