Storybook Test Runner 自定义快照序列化器配置指南:通过 `snapshotSerializers` 让 HTML 快照稳定可复现
2026/9/11 7:28:09 网站建设 项目流程

Storybook Test Runner 自定义快照序列化器配置指南:通过snapshotSerializers让 HTML 快照稳定可复现

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

导读

Storybook 的 Test Runner(@storybook/test-runner)可以把你的每一个 Story 变成可执行的 Jest 测试,其中基于 DOM 的快照测试依赖默认的jest-serializer-html序列化器。然而一旦组件引入 CSS-in-JS(如 Emotion)、Angular 的ng属性或会生成哈希类名/动态 ID 的 UI 库(如 React Aria),快照内容就会随每次构建而变化,导致快照频繁失效。本篇文章以 test-runner-config-serializer.md 为核心,完整讲解如何在test-runner-jest.config.js中通过getJestConfig()snapshotSerializers选项注入自定义快照序列化器,配合 test-runner-custom-snapshot-serializer.md 中的序列化器实现,让快照在不同测试运行之间保持一致、可复现。

一、为什么需要自定义快照序列化器

1.1 Test Runner 的快照测试机制

在 Storybook Test Runner 中启用 DOM 快照测试,只需要在 Storybook 目录下新增一个.storybook/test-runner.js(或.ts)配置文件,通过postVisit钩子读取渲染结果并断言:

// .storybook/test-runner.js module.exports = { async postVisit(page, context) { // 在 Storybook 6.x 中,包裹 Story 的根节点选择器是 #root const elementHandler = await page.$('#storybook-root'); const innerHTML = await elementHandler.innerHTML(); expect(innerHTML).toMatchSnapshot(); }, };

完整示例可参考 test-runner-dom-snapshot-testing.md(含 TypeScript 版本)。执行yarn test-storybook后,Test Runner 会遍历所有 Story,为每个 Story 在__snapshots__目录下生成对应的快照文件。

1.2 默认序列化器与快照不稳定的根源

Test Runner 默认使用jest-serializer-html来序列化 HTML 快照(该包作为@storybook/test-runner的依赖随包安装,无需单独引入)。正如 test-runner.mdx 中的 "Customize snapshot serialization" 小节所说明的,这种默认行为在以下场景会引发问题:

  • CSS-in-JS 库(如 Emotion):会为 CSS 类名生成基于哈希的标识符,每次编译都可能产生不同值;
  • Angular 的ng属性:框架注入的ng-*属性内容可能包含动态信息;
  • 生成动态 ID 的组件库:例如 React Aria 等无障碍组件库,会生成类似react-aria970235672-:rl:的动态 ID 与for属性。

这些动态内容一旦进入快照,就会导致同样的组件在不同运行中产生不同的快照文本,破坏快照的稳定性。解决思路是在快照被写入之前,用一个自定义序列化器对 HTML 做预处理——把动态生成的属性替换为静态、固定的值。

二、核心配置:test-runner-jest.config.jssnapshotSerializers

2.1 配置文件从哪来

Test Runner 支持零配置开箱即用,但当你需要更细粒度的控制时,有两种方式获得可编辑的配置文件:

  1. 运行test-storybook --eject,会在项目根目录生成一个test-runner-jest.config.js文件;
  2. 直接在项目根目录手动创建test-runner-jest.config.js

2.2 关联文档中的完整配置

test-runner-config-serializer.md 给出了启用自定义快照序列化器的标准配置:

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

这段配置值得逐行拆解:

代码片段作用说明
import { getJestConfig } from '@storybook/test-runner'@storybook/test-runner包中导入工厂函数,用于获取 Test Runner 内置的默认 Jest 配置(包括默认的snapshotSerializers、测试环境、匹配器等)
const defaultConfig = getJestConfig()调用函数得到默认配置对象,这是"零配置"能力的基础
...defaultConfig通过展开运算符继承全部默认配置,确保自定义文件只做增量覆盖,不破坏 Test Runner 的默认行为
snapshotSerializers: ['./snapshot-serializer.js', ...defaultConfig.snapshotSerializers]覆盖snapshotSerializers数组:把自定义序列化器放在数组最前面,再拼接回默认序列化器列表

2.3 为什么自定义序列化器要放在数组最前面

snapshotSerializers是 Jest 标准的配置项,用于注册快照序列化器。Jest 在序列化值时,会按照数组顺序依次调用每个序列化器的test(val)方法做匹配检测,第一个返回true的序列化器将负责处理该值

因此把'./snapshot-serializer.js'放在...defaultConfig.snapshotSerializers之前,可以确保自定义序列化器优先于默认的jest-serializer-html被命中,从而在 HTML 被默认序列化器处理之前完成动态内容的替换。这与文档注释 "Sets up the custom serializer to preprocess the HTML before it's passed onto the test-runner" 的意图完全一致。

三、配套实现:自定义序列化器snapshot-serializer.js

3.1 实现代码

配置引用的./snapshot-serializer.js需要你自行创建在项目根目录。完整的参考实现来自 test-runner-custom-snapshot-serializer.md:

// ./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); }, };

3.2 序列化器接口契约:serializetest

Jest 快照序列化器遵循一个约定俗成的双方法接口,本实现同样遵守:

  • test(val):判断当前值是否应由本序列化器处理。这里直接复用jestSerializerHtml.test(val)的判定逻辑——即仅当传入值是需要按 HTML 序列化的字符串时返回true,从而与默认序列化器的能力边界保持一致;
  • serialize(val):当test返回true时被调用,执行实际的序列化。这里的处理分两步:
    1. 先用正则/"react-aria-\d+(\.\d+)?"/g匹配并替换所有react-aria前缀的动态 ID(含可选的小数部分),统一替换为静态占位符mocked_id
    2. 再把替换后的字符串交给jestSerializerHtml.print(withFixedIds),复用默认序列化器的格式化输出能力。

从代码注释可以看到预期效果:形如<label id="react-aria970235672-:rl:" for="react-aria970235672-:rk:">Favorite color</label>的 HTML,会被规整为<label id="react-mocked_id" for="react-mocked_id">Favorite color</label>——属性值稳定了,快照也就稳定了。

3.3 如何适配你自己的组件库

正则DYNAMIC_ID_PATTERN是针对 React Aria 动态 ID 编写的示例。实际项目中,你应该根据自己组件库生成动态内容的格式来调整匹配模式,例如:

  • 匹配 Emotion 等 CSS-in-JS 生成的哈希类名:/css-\w+/g
  • 匹配 Angular 注入的ng属性:把ng-reflect-*ng-version等属性从 HTML 中剔除;
  • 匹配其他库的动态 ID:把对应的前缀与编号结构写进正则即可。

原则是:只替换真正动态的部分,保留静态内容,避免过度规整导致快照失去回归检测的意义。

四、完整配置流程与验证

将以上两部分组合起来,完整的启用步骤如下:

  1. 确保已安装并配置 Test Runner:安装@storybook/test-runner,并在package.jsonscripts中添加"test-storybook": "test-storybook"(参考 test-runner-install.md);
  2. 创建自定义序列化器:在项目根目录新建snapshot-serializer.js,内容见本文第三节;
  3. 创建/修改 Jest 配置文件:在项目根目录新建(或通过test-storybook --eject生成后修改)test-runner-jest.config.js,内容见本文第二节;
  4. 启动 Storybook:先在一个终端运行yarn storybook(或你的启动脚本),因为 Test Runner 需要连接一个正在运行的 Storybook 实例(本地或已发布版本);
  5. 运行测试:新开终端执行yarn test-storybook。首次运行会生成__snapshots__目录下的快照文件,之后每次运行都会与既有快照比对。

可以验证的一点:打开生成的快照文件,如果组件包含react-aria动态 ID,其中存储的应当是统一的mocked_id而非每次变化的随机 ID。若快照内容仍包含动态值,应检查test()方法是否正确返回true、正则是否覆盖了实际的 ID 格式。

五、深入理解:与默认配置的关系及同类配置

5.1getJestConfig()是零配置的基石

Test Runner 的"零配置"体验依赖getJestConfig()工厂函数——它集中封装了测试环境、快照目录约定、序列化器等默认设置。在自定义配置中始终先const defaultConfig = getJestConfig()再基于它做展开覆盖,是官方推荐的增量扩展方式,可以避免手写全部配置带来的维护成本和与 Test Runner 内部约定脱节的风险。

5.2 与snapshotResolver的区分

在 test-runner.mdx 的 "Override the default snapshot directory" 小节中,还存在一个容易混淆的配置项snapshotResolver。两者的职责边界清晰:

配置项解决的问题配置示例
snapshotResolver控制快照文件存到哪里(自定义快照目录/命名)snapshotResolver: './snapshot-resolver.js',完整示例见 test-runner-config-snapshot-resolver.md
snapshotSerializers控制快照内容如何生成(序列化前预处理 HTML)snapshotSerializers: ['./snapshot-serializer.js', ...defaultConfig.snapshotSerializers],即本文核心配置

如果你的组件存在动态 ID 问题,需要的是snapshotSerializers;如果只是想把快照文件从默认的__snapshots__目录挪到别处,需要的是snapshotResolver。两者也可以在同一份test-runner-jest.config.js中同时启用。

5.3 适用前提与注意事项

  • 本配置方案面向 Webpack 构建的 Storybook Test Runner(该文档位于 test-runner.mdx 中,标题即标注了 "Test runner (Webpack)");若你使用 Vite 驱动的 Storybook 框架,官方推荐改用功能更现代的 Vitest addon(以docs/writing-tests/integrations/目录下的 vitest-addon 文档为准);
  • 自定义序列化器会按数组顺序参与所有快照的序列化,因此test(val)的判定务必精确,避免误伤非 HTML 类型的快照值;
  • 序列化器文件路径是相对项目根目录解析的(如'./snapshot-serializer.js'),路径写错会导致 Jest 加载失败;
  • 修改序列化器逻辑后,如需重新生成基线快照,可配合test-storybook -u--updateSnapshot)更新既有快照。

六、小结

围绕 test-runner-config-serializer.md 这一核心配置片段,本文完整还原了 Storybook Test Runner 自定义快照序列化器的整套方案:先用getJestConfig()获取默认配置,再通过snapshotSerializers数组将自定义序列化器置于默认序列化器之前,最后在序列化器中用正则规整动态 ID 并复用jest-serializer-html的输出能力。这套"预处理 + 委托默认序列化器"的模式,是让含动态属性(CSS-in-JS 哈希、ng属性、无障碍组件动态 ID)的组件快照保持稳定、可复现、适合纳入 CI 回归检测的通用解法。配套的序列化器实现与快照目录自定义示例,可分别继续阅读 test-runner-custom-snapshot-serializer.md 与 test-runner-config-snapshot-resolver.md 获取。

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询