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.js与snapshotSerializers
2.1 配置文件从哪来
Test Runner 支持零配置开箱即用,但当你需要更细粒度的控制时,有两种方式获得可编辑的配置文件:
- 运行
test-storybook --eject,会在项目根目录生成一个test-runner-jest.config.js文件; - 直接在项目根目录手动创建
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 序列化器接口契约:serialize与test
Jest 快照序列化器遵循一个约定俗成的双方法接口,本实现同样遵守:
test(val):判断当前值是否应由本序列化器处理。这里直接复用jestSerializerHtml.test(val)的判定逻辑——即仅当传入值是需要按 HTML 序列化的字符串时返回true,从而与默认序列化器的能力边界保持一致;serialize(val):当test返回true时被调用,执行实际的序列化。这里的处理分两步:- 先用正则
/"react-aria-\d+(\.\d+)?"/g匹配并替换所有react-aria前缀的动态 ID(含可选的小数部分),统一替换为静态占位符mocked_id; - 再把替换后的字符串交给
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:把对应的前缀与编号结构写进正则即可。
原则是:只替换真正动态的部分,保留静态内容,避免过度规整导致快照失去回归检测的意义。
四、完整配置流程与验证
将以上两部分组合起来,完整的启用步骤如下:
- 确保已安装并配置 Test Runner:安装
@storybook/test-runner,并在package.json的scripts中添加"test-storybook": "test-storybook"(参考 test-runner-install.md); - 创建自定义序列化器:在项目根目录新建
snapshot-serializer.js,内容见本文第三节; - 创建/修改 Jest 配置文件:在项目根目录新建(或通过
test-storybook --eject生成后修改)test-runner-jest.config.js,内容见本文第二节; - 启动 Storybook:先在一个终端运行
yarn storybook(或你的启动脚本),因为 Test Runner 需要连接一个正在运行的 Storybook 实例(本地或已发布版本); - 运行测试:新开终端执行
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),仅供参考