Docs
Storybook Docs

Storyshots 迁移指南

我们正在积极整合社区反馈,以改进使用 Storybook 进行快照测试的工具和文档。如果你有兴趣参与此过程并帮助我们改进它。请填写此 form 以分享你的反馈。

¥We're actively integrating community feedback to improve the tooling and documentation for snapshot testing with Storybook. If you're interested in participating in this process and helping us improve it. Please fill out this form to share your feedback.

本指南将教你如何将快照测试从 Storyshots 插件迁移到 Storybook 的测试运行器或可移植故事。此外,你将能够了解它们之间的差异,并使用 Storybook 提供的可用工具设置、配置和运行快照测试。

¥This guide will teach you how to migrate your snapshot tests from the Storyshots addon to Storybook's test-runner or portable stories. Also, you will be able to understand the differences between them and set up, configure, and run snapshot tests using the available tooling provided by Storybook.

从 Storyshots 迁移测试

¥Migrating tests from Storyshots

先决条件

¥Prerequisites

在开始迁移过程之前,请确保你已:

¥Before you begin the migration process, ensure that you have:

  • 功能齐全的 Storybook,配置了运行最新稳定版本(即 7.6 或更高版本)的 支持的框架 之一。

    ¥A fully functional Storybook configured with one of the supported frameworks running the latest stable version (i.e., 7.6 or higher).

  • 熟悉你当前的 Storybook 及其测试设置。

    ¥Familiarity with your current Storybook and its testing setup.

使用 test-runner

¥With the test-runner

Storybook 测试运行器将你的所有故事变成可执行测试。由 JestPlaywright 提供支持。它是一个独立的、与框架无关的实用程序,与你的 Storybook 并行运行。它使你能够在多浏览器环境中运行多种测试模式,包括使用 播放函数、DOM 快照和 可访问性测试 进行组件测试。

¥Storybook test-runner turns all of your stories into executable tests. Powered by Jest and Playwright. It's a standalone, framework-agnostic utility that runs parallel to your Storybook. It enables you to run multiple testing patterns in a multi-browser environment, including component testing with the play function, DOM snapshot, and accessibility testing.

设置

¥Setup

要开始从 Storyshots 插件迁移到测试运行器的过程,我们建议你从项目中删除 Storyshots 插件和类似的软件包(即 storybook/addon-storyshots-puppeteer),包括任何相关的配置文件。然后,按照测试运行器的 安装说明 安装、配置和运行它。

¥To get started with the migration process from the Storyshots addon to the test-runner, we recommend that you remove the Storyshots addon and similar packages (i.e., storybook/addon-storyshots-puppeteer ) from your project, including any related configuration files. Then, follow the test-runner's setup instructions to install, configure and run it.

扩展测试覆盖率

¥Extend your test coverage

Storyshots 插件提供了高度可定制的测试解决方案,允许用户以各种方式扩展测试覆盖范围。但是,测试运行器提供了类似的体验,但使用不同的 API。下面,你将找到使用测试运行器实现与使用 Storyshots 实现的结果类似的结果的其他示例。

¥The Storyshots addon offered a highly customizable testing solution, allowing users to extend testing coverage in various ways. However, the test-runner provides a similar experience but with a different API. Below, you will find additional examples of using the test-runner to achieve results similar to those you achieved with Storyshots.

使用测试运行器启用 DOM 快照测试

¥Enable DOM snapshot testing with the test-runner

要使用测试运行器启用 DOM 快照测试,你可以扩展测试运行器的配置文件并使用可用的 hooks,并将它们与 Playwright 的内置 APIs 结合起来,为项目中的每个故事生成 DOM 快照。例如:

¥To enable DOM snapshot testing with the test-runner, you can extend the test-runner's configuration file and use the available hooks and combine them with Playwright's built-in APIs to generate DOM snapshots for each story in your project. For example:

.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;

如果你在项目中使用测试运行器设置了 DOM 快照测试并通过 CLI 标志启用了 index.json mode,则测试将在项目外部的临时文件夹中生成,并且快照会与它们一起存储。你需要扩展测试运行器的配置并提供自定义快照解析器以允许快照的不同位置。请参阅 故障排除 部分以获取更多信息。

¥If you've set up DOM snapshot tests in your project with the test-runner and enabled the index.json mode via CLI flag, tests are generated in a temporary folder outside your project, and snapshots get stored alongside them. You'll need to extend the test-runner's configuration and provide a custom snapshot resolver to allow a different location for the snapshots. See the Troubleshooting section for more information.

使用测试运行器运行图片快照测试

¥Run image snapshot tests with the test-runner

默认情况下,测试运行器为你提供了以最少配置运行多种测试模式(例如,DOM 快照测试,accessibility)的选项。但是,如果你愿意,你可以扩展它以与其他测试一起运行视觉回归测试。例如:

¥By default, the test-runner provides you with the option to run multiple testing patterns (e.g., DOM snapshot testing, accessibility) with minimal configuration. However, if you want, you can extend it to run visual regression testing alongside your other tests. For example:

.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) {
    // Waits for the page to be ready before taking a screenshot to ensure consistent results
    await waitForPageReady(page);
 
    // To capture a screenshot for for different browsers, add 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;

使用 Portable Stories

¥With Portable Stories

Storybook 提供了一个 composeStories 实用程序,可帮助将故事文件中的故事转换为可渲染元素,这些元素可以在使用 JSDOM 的 Node 测试中重复使用。它还允许你应用你在项目中启用的其他 Storybook 功能(例如,decoratorsargs),从而使你的组件能够正确渲染。这就是所谓的可移植故事。

¥Storybook provides a composeStories utility that helps convert stories from a story file into renderable elements that can be reused in your Node tests with JSDOM. It also allows you to apply other Storybook features that you have enabled in your project (e.g., decorators, args), which allows your component to render correctly. This is what is known as portable stories.

设置

¥Setup

我们建议你关闭当前的故事镜头测试以开始迁移过程。为此,请将配置文件(即 storybook.test.ts|js 或类似文件)重命名为 storybook.test.ts|js.old。这将阻止检测到测试,因为你将创建一个具有相同名称的新测试配置文件。通过这样做,你将能够在从项目中删除 Storyshots 插件之前在过渡到可移植故事时保留现有测试。

¥We recommend you turn off your current storyshots tests to start the migration process. To do this, rename the configuration file (i.e., storybook.test.ts|js or similar) to storybook.test.ts|js.old. This will prevent the tests from being detected, as you'll create a new testing configuration file with the same name. By doing this, you'll be able to preserve your existing tests while transitioning to portable stories before removing the Storyshots addon from your project.

导入来自 Storybook 的项目级注释

¥Import project-level annotations from Storybook

如果你需要在测试中包含的 ./storybook/preview.js|ts 中启用项目级注释(例如 decoratorsargs、样式),请调整测试设置文件以导入注释,如下所示:

¥If you need project-level annotations (e.g., decorators, args, styles) enabled in your ./storybook/preview.js|ts included in your tests, adjust your test set up file to import the annotations as follows:

setupTest.ts
import { beforeAll } from 'vitest';
// 👇 If you're using Next.js, import from @storybook/nextjs
//   If you're using Next.js with Vite, import from @storybook/experimental-nextjs-vite
import { setProjectAnnotations } from '@storybook/react';
// 👇 Import the exported annotations, if any, from the addons you're using; otherwise remove this
import * as addonAnnotations from 'my-addon/preview';
import * as previewAnnotations from './.storybook/preview';
 
const annotations = setProjectAnnotations([previewAnnotations, addonAnnotations]);
 
// Run Storybook's beforeAll hook
beforeAll(annotations.beforeAll);

配置可移植故事的测试框架

¥Configure the testing framework for portable stories

为了帮助你使用 composeStories 辅助程序 API 从 Storyshots 插件迁移到 Storybook 的可移植故事,我们准备了示例来帮助你入门。下面列出了两个最流行的测试框架的示例:JestVitest。我们建议将代码放在新创建的 storybook.test.ts|js 文件中,并根据你的测试框架相应地调整代码。以下两个示例都将:

¥To help you migrate from Storyshots addon to Storybook's portable stories with the composeStories helper API, we've prepared examples to help you get started. Listed below are examples of two of the most popular testing frameworks: Jest and Vitest. We recommend placing the code in a newly created storybook.test.ts|js file and adjusting the code accordingly, depending on your testing framework. Both examples below will:

  • 根据 glob 模式导入所有故事文件

    ¥Import all story files based on a glob pattern

  • 遍历这些文件并在每个模块上使用 composeStories,从而生成每个故事的可渲染组件列表

    ¥Iterate over these files and use composeStories on each of their modules, resulting in a list of renderable components from each story

  • 循环浏览故事、渲染它们并对其进行快照

    ¥Cycle through the stories, render them, and snapshot them

Vitest

如果你使用 Vitest 作为测试框架,则可以参考以下示例,使用 composeStories 辅助 API 开始将快照测试迁移到 Storybook 的可移植故事。你需要按如下方式修改 storybook.test.ts|js 文件中的代码:

¥If you're using Vitest as your testing framework, you can begin migrating your snapshot tests to Storybook's portable stories with the composeStories helper API by referring to the following example. You will need to modify the code in your storybook.test.ts|js file as follows:

storybook.test.ts
// @vitest-environment jsdom
 
// Replace your-framework with one of the supported Storybook frameworks (react, vue3)
import type { Meta, StoryFn } from '@storybook/your-framework';
 
import { describe, expect, test } from 'vitest';
 
// Replace your-renderer with the renderer you are using (e.g., react, vue3, svelte, etc.)
import { composeStories } from '@storybook/your-renderer';
 
type StoryFile = {
  default: Meta;
  [name: string]: StoryFn | Meta;
};
 
const compose = (entry: StoryFile): ReturnType<typeof composeStories<StoryFile>> => {
  try {
    return composeStories(entry);
  } catch (e) {
    throw new Error(
      `There was an issue composing stories for the module: ${JSON.stringify(entry)}, ${e}`
    );
  }
};
 
function getAllStoryFiles() {
  // Place the glob you want to match your story files
  const storyFiles = Object.entries(
    import.meta.glob<StoryFile>('./stories/**/*.(stories|story).@(js|jsx|mjs|ts|tsx)', {
      eager: true,
    })
  );
 
  return storyFiles.map(([filePath, storyFile]) => {
    const storyDir = path.dirname(filePath);
    const componentName = path.basename(filePath).replace(/\.(stories|story)\.[^/.]+$/, '');
    return { filePath, storyFile, componentName, storyDir };
  });
}
 
// Recreate similar options to Storyshots. Place your configuration below
const options = {
  suite: 'Storybook Tests',
  storyKindRegex: /^.*?DontTest$/,
  storyNameRegex: /UNSET/,
  snapshotsDirName: '__snapshots__',
  snapshotExtension: '.storyshot',
};
 
describe(options.suite, () => {
  getAllStoryFiles().forEach(({ storyFile, componentName, storyDir }) => {
    const meta = storyFile.default;
    const title = meta.title || componentName;
 
    if (options.storyKindRegex.test(title) || meta.parameters?.storyshots?.disable) {
      // Skip component tests if they are disabled
      return;
    }
 
    describe(title, () => {
      const stories = Object.entries(compose(storyFile))
        .map(([name, story]) => ({ name, story }))
        .filter(({ name, story }) => {
          // Implements a filtering mechanism to avoid running stories that are disabled via parameters or that match a specific regex mirroring the default behavior of Storyshots.
          return !options.storyNameRegex?.test(name) && !story.parameters.storyshots?.disable;
        });
 
      if (stories.length <= 0) {
        throw new Error(
          `No stories found for this module: ${title}. Make sure there is at least one valid story for this module, without a disable parameter, or add parameters.storyshots.disable in the default export of this file.`
        );
      }
 
      stories.forEach(({ name, story }) => {
        // Instead of not running the test, you can create logic to skip it, flagging it accordingly in the test results.
        const testFn = story.parameters.storyshots?.skip ? test.skip : test;
 
        testFn(name, async () => {
          await story.run();
          // Ensures a consistent snapshot by waiting for the component to render by adding a delay of 1 ms before taking the snapshot.
          await new Promise((resolve) => setTimeout(resolve, 1));
 
          expect(document.body.firstChild).toMatchSnapshot();
        });
      });
    });
  });
});

当你使用 Vitest 执行测试时,它将生成一个包含项目中的所有故事的快照文件(即 storybook.test.ts|js.snap)。但是,如果你想生成单独的快照文件,则可以使用 Vitest 的 toMatchFileSnapshot API。例如:

¥When your test is executed with Vitest, it will generate a single snapshot file (i.e., storybook.test.ts|js.snap) with all the stories in your project. However, if you want to generate individual snapshot files, you can use Vitest's toMatchFileSnapshot API. For example:

storybook.test.js|ts
// ...Code omitted for brevity
 
describe(options.suite, () => {
  // 👇 Add storyDir in the arguments list
  getAllStoryFiles().forEach(({ filePath, storyFile, storyDir }) => {
    // ...Previously existing code
    describe(title, () => {
      // ...Previously existing code
      stories.forEach(({ name, story }) => {
        // ...Previously existing code
        testFn(name, async () => {
          await story.run();
          // Ensures a consistent snapshot by waiting for the component to render by adding a delay of 1 ms before taking the snapshot.
          await new Promise((resolve) => setTimeout(resolve, 1));
 
          // 👇 Define the path to save the snapshot to:
          const snapshotPath = path.join(
            storyDir,
            options.snapshotsDirName,
            `${componentName}${options.snapshotExtension}`
          );
          expect(document.body.firstChild).toMatchFileSnapshot(snapshotPath);
        });
      });
    });
  });
});

Jest

如果你使用 Jest 作为测试框架,你可以参考以下示例,使用 composeStories 辅助 API 开始将快照测试迁移到 Storybook 的可移植故事。你需要按如下方式修改 storybook.test.ts|js 文件中的代码:

¥If you're using Jest as your testing framework, you can begin migrating your snapshot tests to Storybook's portable stories with the composeStories helper API by referring to the following example. You will need to modify the code in your storybook.test.ts|js file as follows:

storybook.test.ts
import path from 'path';
import * as glob from 'glob';
 
// Replace your-framework with one of the supported Storybook frameworks (react, vue3)
import type { Meta, StoryFn } from '@storybook/your-framework';
 
import { describe, test, expect } from '@jest/globals';
 
// Replace your-renderer with the renderer you are using (e.g., react, vue3, svelte, etc.)
import { composeStories } from '@storybook/your-renderer';
 
type StoryFile = {
  default: Meta;
  [name: string]: StoryFn | Meta;
};
 
const compose = (entry: StoryFile): ReturnType<typeof composeStories<StoryFile>> => {
  try {
    return composeStories(entry);
  } catch (e) {
    throw new Error(
      `There was an issue composing stories for the module: ${JSON.stringify(entry)}, ${e}`
    );
  }
};
 
function getAllStoryFiles() {
  // Place the glob you want to match your stories files
  const storyFiles = glob.sync(
    path.join(__dirname, 'stories/**/*.{stories,story}.{js,jsx,mjs,ts,tsx}')
  );
 
  return storyFiles.map((filePath) => {
    const storyFile = require(filePath);
    return { filePath, storyFile };
  });
}
 
// Recreate similar options to Storyshots. Place your configuration below
const options = {
  suite: 'Storybook Tests',
  storyKindRegex: /^.*?DontTest$/,
  storyNameRegex: /UNSET/,
  snapshotsDirName: '__snapshots__',
  snapshotExtension: '.storyshot',
};
 
describe(options.suite, () => {
  getAllStoryFiles().forEach(({ storyFile, componentName }) => {
    const meta = storyFile.default;
    const title = meta.title || componentName;
 
    if (options.storyKindRegex.test(title) || meta.parameters?.storyshots?.disable) {
      // Skip component tests if they are disabled
      return;
    }
 
    describe(title, () => {
      const stories = Object.entries(compose(storyFile))
        .map(([name, story]) => ({ name, story }))
        .filter(({ name, story }) => {
          // Implements a filtering mechanism to avoid running stories that are disabled via parameters or that match a specific regex mirroring the default behavior of Storyshots.
          return !options.storyNameRegex.test(name) && !story.parameters.storyshots?.disable;
        });
 
      if (stories.length <= 0) {
        throw new Error(
          `No stories found for this module: ${title}. Make sure there is at least one valid story for this module, without a disable parameter, or add parameters.storyshots.disable in the default export of this file.`
        );
      }
 
      stories.forEach(({ name, story }) => {
        // Instead of not running the test, you can create logic to skip it, flagging it accordingly in the test results.
        const testFn = story.parameters.storyshots?.skip ? test.skip : test;
 
        testFn(name, async () => {
          await story.run();
          // Ensures a consistent snapshot by waiting for the component to render by adding a delay of 1 ms before taking the snapshot.
          await new Promise((resolve) => setTimeout(resolve, 1));
          expect(document.body.firstChild).toMatchSnapshot();
        });
      });
    });
  });
});

当你使用 Jest 执行测试时,它将生成一个包含项目中的所有故事的快照文件(即 __snapshots__/storybook.test.ts|js.snap)。但是,如果你想生成单独的快照文件,则可以使用 jest-specific-snapshot 包。例如:

¥When your test is executed with Jest, it will generate a single snapshot file (i.e., __snapshots__/storybook.test.ts|js.snap) with all the stories in your project. However, if you want to generate individual snapshot files, you can use the jest-specific-snapshot package. For example:

storybook.test.js|ts
// 👇 Augment expect with jest-specific-snapshot
import 'jest-specific-snapshot';
 
// ...Code omitted for brevity
 
describe(options.suite, () => {
  //👇 Add storyDir in the arguments list
  getAllStoryFiles().forEach(({ filePath, storyFile, storyDir }) => {
    // ...Previously existing code
    describe(title, () => {
      // ...Previously existing code
      stories.forEach(({ name, story }) => {
        // ...Previously existing code
        testFn(name, async () => {
          await story.run();
          // Ensures a consistent snapshot by waiting for the component to render by adding a delay of 1 ms before taking the snapshot.
          await new Promise((resolve) => setTimeout(resolve, 1));
 
          //👇 Define the path to save the snapshot to:
          const snapshotPath = path.join(
            storyDir,
            options.snapshotsDirName,
            `${componentName}${options.snapshotExtension}`
          );
          expect(document.body.firstChild).toMatchSpecificSnapshot(snapshotPath);
        });
      });
    });
  });
});

已知限制

¥Known limitations

如果你选择在测试中使用可移植的故事,那么你将拥有一个可以在 JSDOM 环境中运行的单个测试文件,可以渲染和快照所有的故事。但是,随着项目的发展,你可能会遇到之前使用 Storyshots 时遇到的限制:

¥If you opt to use portable stories in your tests, you'll have a single test file that can run in a JSDOM environment, rendering and snapshotting all your stories. However, as your project grows, you may run into the limitations you had with Storyshots previously:

  • 你不是在针对真实浏览器进行测试。

    ¥You are not testing against a real browser.

  • 你必须模拟许多浏览器实用程序(例如,画布、窗口 API 等)。

    ¥You must mock many browser utilities (e.g., canvas, window APIs, etc).

  • 由于你无法在测试过程中访问浏览器,因此你的调试体验不会那么好。

    ¥Your debugging experience will not be as good, given you can't access the browser as part of your tests.

或者,你可能需要考虑迁移到使用 Storybook 进行快照测试的其他可用选项:test-runner 是一个更强大的解决方案,它使用 Playwright 针对真实浏览器环境运行测试。

¥Alternatively, you may want to consider migrating to the other available option for snapshot testing with Storybook: the test-runner for a more robust solution that runs tests against a real browser environment with Playwright.


故障排除

¥Troubleshooting

由于使用 Storybook 和测试运行器运行快​​照测试可能会导致一些技术限制,从而阻止你成功设置或运行测试,因此我们准备了一组说明来帮助你解决可能遇到的任何问题。

¥As running snapshot tests with Storybook and the test-runner can lead to some technical limitations that may prevent you from setting up or running your tests successfully, we've prepared a set of instructions to help you troubleshoot any issues you may encounter.

测试运行器在运行快照测试时报告错误

¥The test-runner reports an error when running snapshot tests

如果你在使用测试运行器时遇到间歇性测试失败,则在浏览器中运行测试时可能会出现未捕获的错误。如果你之前使用的是 Storyshots 插件,则可能无法发现这些错误。测试运行器默认将这些未捕获的错误视为失败的测试。但是,如果这些错误是预料之中的,你可以通过在故事和测试运行器配置文件中启用自定义故事标签来忽略它们。有关更多信息,请参阅 测试运行器文档

¥If you're experiencing intermittent test failures with the test-runner, uncaught errors may occur when your tests run in the browser. These errors might not have been caught if you were using the Storyshots addons previously. The test-runner will, by default, consider these uncaught errors as failed tests. However, if these errors are expected, you can ignore them by enabling custom story tags in your stories and test-runner configuration files. For more information, please refer to the test-runner documentation.

React docge 速度提高 25-50%

¥The test-runner does not generate snapshot files in the expected directory

如果你已将测试运行器配置为运行快照测试,你可能会注意到快照文件的路径和名称与 Storyshots 插件先前生成的不同。这是因为测试运行器对快照文件使用了不同的命名约定。使用自定义快照解析器,你可以将测试运行器配置为使用你以前使用的相同命名约定。

¥If you've configured the test-runner to run snapshot tests, you may notice that the paths and names of the snapshot files differ from those previously generated by the Storyshots addon. This is because the test-runner uses a different naming convention for snapshot files. Using a custom snapshot resolver, you can configure the test-runner to use the same naming convention you used previously.

运行以下命令为测试运行器生成自定义配置文件,你可以使用该文件来配置 Jest:

¥Run the following command to generate a custom configuration file for the test-runner that you can use to configure Jest:

npm run test-storybook -- --eject

更新文件并启用 snapshotResolver 选项以使用自定义快照解析器:

¥Update the file and enable the snapshotResolver option to use a custom snapshot resolver:

./test-runner-jest.config.js
import { getJestConfig } from '@storybook/test-runner';
 
const defaultConfig = getJestConfig();
 
const config = {
  // The default Jest configuration comes from @storybook/test-runner
  ...defaultConfig,
  snapshotResolver: './snapshot-resolver.js',
};
 
export default config;

最后,创建一个 snapshot-resolver.js 文件来实现自定义快照解析器:

¥Finally, create a snapshot-resolver.js file to implement a custom snapshot resolver:

./snapshot-resolver.js
import path from 'path';
 
export default {
  resolveSnapshotPath: (testPath) => {
    const fileName = path.basename(testPath);
    const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, '');
    const modifiedFileName = `${fileNameWithoutExtension}.storyshot`;
 
    // Configure Jest to generate snapshot files using the following naming convention (__snapshots__/Button.storyshot)
    return path.join(path.dirname(testPath), '__snapshots__', modifiedFileName);
  },
  resolveTestPath: (snapshotFilePath, snapshotExtension) =>
    path.basename(snapshotFilePath, snapshotExtension),
  testPathForConsistencyCheck: 'example.storyshot',
};

快照文件的格式与 Storyshots 插件生成的文件格式不同

¥The format of the snapshot files is different from the ones generated by the Storyshots addon

默认情况下,测试运行器使用 jest-serializer-html 序列化 HTML 快照。这可能会导致与你现有的快照相比格式不同,即使你使用的是特定的 CSS-in-JS 库(如 情感、Angular 的 ng 属性或其他类似的为 CSS 类生成基于哈希的标识符的库)。但是,你可以配置测试运行器以使用自定义快照序列化器来解决此问题,方法是用静态名称覆盖随机类名,该静态名称对于每次测试运行都相同。

¥By default, the test-runner uses jest-serializer-html to serialize HTML snapshots. This may cause differences in formatting compared to your existing snapshots, even if you're using specific CSS-in-JS libraries like Emotion, Angular's ng attributes, or other similar libraries that generate hash-based identifiers for CSS classes. However, you can configure the test-runner to use a custom snapshot serializer to solve this issue by overriding the random class names with a static one that will be the same for each test run.

运行以下命令为测试运行器生成自定义配置文件,你可以使用该文件来提供其他配置选项。

¥Run the following command to generate a custom configuration file for the test-runner that you can use to provide additional configuration options.

npm run test-storybook -- --eject

更新文件并启用 snapshotSerializers 选项以使用自定义快照解析器。例如:

¥Update the file and enable the snapshotSerializers option to use a custom snapshot resolver. For example:

./test-runner-jest.config.js
import { getJestConfig } from '@storybook/test-runner';
 
const defaultConfig = getJestConfig();
 
const config = {
  ...defaultConfig,
  snapshotSerializers: [
    // Sets up the custom serializer to preprocess the HTML before it's passed onto the test-runner
    './snapshot-serializer.js',
    ...defaultConfig.snapshotSerializers,
  ],
};
 
export default config;

最后,创建一个 snapshot-serializer.js 文件来实现自定义快照序列化器:

¥Finally, create a snapshot-serializer.js file to implement a custom snapshot serializer:

./snapshot-serializer.js
// The jest-serializer-html package is available as a dependency of the test-runner
const jestSerializerHtml = require('jest-serializer-html');
 
const DYNAMIC_ID_PATTERN = /"react-aria-\d+(\.\d+)?"/g;
 
module.exports = {
  /*
   * The test-runner calls the serialize function when the test reaches the expect(SomeHTMLElement).toMatchSnapshot().
   * It will replace all dynamic IDs with a static ID so that the snapshot is consistent.
   * For instance, from <label id="react-aria970235672-:rl:" for="react-aria970235672-:rk:">Favorite color</label> to <label id="react-mocked_id" for="react-mocked_id">Favorite color</label>
   */
  serialize(val) {
    const withFixedIds = val.replace(DYNAMIC_ID_PATTERN, 'mocked_id');
    return jestSerializerHtml.print(withFixedIds);
  },
  test(val) {
    return jestSerializerHtml.test(val);
  },
};

当测试运行器执行你的测试时,它将自省生成的 HTML,并在快照组件之前将任何动态生成的属性替换为正则表达式提供的静态属性。

¥When the test-runner executes your tests, it will introspect the resulting HTML and replace any dynamically generated attributes with the static ones provided by the regex expression before snapshotting the component.