Storybook Test Runner 自定义快照目录:用 snapshotResolver 重定向快照文件位置
本指南讲解如何在 Storybook Test Runner 中通过自定义快照解析器(snapshot resolver)将快照测试生成的.snap文件重定向到指定目录,摆脱默认的__snapshots__存放位置。读完本文,你将掌握resolveSnapshotPath、resolveTestPath与testPathForConsistencyCheck三个核心字段的完整语义,能够为多项目、Monorepo 或规范严格的团队定制快照文件的命名与存放规则。
快照测试在 Test Runner 中的定位
Storybook Test Runner 会把你的所有 stories 变成可执行的测试:没有play函数的 story 会验证其能否无错渲染;带有play函数的 story 还会检查交互断言是否全部通过。这些测试运行在真实浏览器中(基于 Jest 与 Playwright),既可通过 CLI 运行,也可接入 CI。
在运行快照测试场景下,Test Runner 依赖一个postVisithook 在每次访问 story 后抓取渲染结果并调用toMatchSnapshot()。官方推荐在 Storybook 配置目录下新建.storybook/test-runner.js:
module.exports = { 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(); }, };使用 TypeScript 时,可从@storybook/test-runner导入TestRunnerConfig类型获得完整的类型提示:
import type { TestRunnerConfig } from '@storybook/test-runner'; const config: TestRunnerConfig = { async postVisit(page, context) { const elementHandler = await page.$('#storybook-root'); const innerHTML = await elementHandler.innerHTML(); expect(innerHTML).toMatchSnapshot(); }, }; export default config;执行yarn test-storybook后,Test Runner 会遍历所有 stories 并运行快照测试,为每个 story 生成快照文件。默认情况下,这些文件存放在__snapshots__目录中,且 Test Runner 内置了一套默认的命名约定与路径规则。
为什么需要自定义快照目录
默认的__snapshots__机制开箱即用,覆盖了大多数使用场景。但以下情况需要自定义:
- 统一归档:希望所有快照集中在一个专门的目录(如
./src/test/__snapshots__),与测试代码物理隔离,便于 CI 缓存与清理; - 命名规范:默认扩展名与命名约定不满足团队规范,需要改成
.storyshot等自定义扩展名; - Monorepo 多包结构:每个子包需要把快照输出到各自约定的位置,避免互相污染;
- Git 策略:某些团队希望快照文件存放在独立目录,以便配置不同的
.gitignore或 codeowner。
Test Runner 默认配置由@storybook/test-runner内部提供,但你可以在项目根目录创建test-runner-jest.config.js覆盖它,其中就包含 Jest 的snapshotResolver选项——这正是自定义快照目录的入口。
快照解析器的三个核心字段
Jest 的snapshotResolver约定一个包含三个字段的模块,Test Runner 直接沿用这套机制。理解这三个字段是定制的基础:
| 字段 | 作用 | 在本场景中的实现 |
|---|---|---|
resolveSnapshotPath(testPath) | 给定测试文件路径,返回快照文件应写入的完整路径 | 取出测试文件名、去掉扩展名后拼上.snap,再拼到自定义目录 |
resolveTestPath(snapshotFilePath, snapshotExtension) | 反向映射:给定快照文件路径,反推出对应的测试文件路径 | 用path.basename去掉快照扩展名得到测试文件名 |
testPathForConsistencyCheck | 一致性校验的样例路径,Jest 用它验证上述两个函数互为逆运算 | 填一个示例测试文件名(如example) |
resolveTestPath中的snapshotExtension参数由 Jest 传入(默认即.snap),testPathForConsistencyCheck必须保证resolveTestPath(resolveSnapshotPath(example), extension) === example,否则 Jest 会直接抛错拒绝加载配置。
实战:将快照输出到自定义目录
第一步:创建自定义快照解析器
在项目根目录创建snapshot-resolver.js,将快照文件统一输出到./src/test/__snapshots__/目录:
import path from 'path'; export default { resolveSnapshotPath: (testPath) => { const fileName = path.basename(testPath); const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, ''); // Defines the file extension for the snapshot file const modifiedFileName = `${fileNameWithoutExtension}.snap`; // Configure Jest to generate snapshot files using the following convention (./src/test/__snapshots__/Button.stories.snap) return path.join('./src/test/__snapshots__', modifiedFileName); }, resolveTestPath: (snapshotFilePath, snapshotExtension) => path.basename(snapshotFilePath, snapshotExtension), testPathForConsistencyCheck: 'example', };逐行拆解这个实现:
path.basename(testPath):只取测试文件名部分,忽略其原目录。例如./src/components/Button.stories.js→Button.stories.js;fileName.replace(/\.[^/.]+$/, ''):用正则去掉最后一个扩展名,Button.stories.js→Button.stories;- 拼接
${fileNameWithoutExtension}.snap得到快照文件名Button.stories.snap,即保持testPathForConsistencyCheck中的一致性命名的逆运算; path.join('./src/test/__snapshots__', modifiedFileName):最终生成./src/test/__snapshots__/Button.stories.snap。
这里的关键是:目录层级被拍平了——resolveSnapshotPath只保留测试文件名,因此无论测试文件分散在多少个目录,快照都会集中到同一个目录,不会产生子目录嵌套。
第二步:在 Jest 配置中启用 resolver
项目根目录创建test-runner-jest.config.js,从@storybook/test-runner导入默认配置,展开后用snapshotResolver覆盖:
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;要点:
- 必须通过展开运算符继承
getJestConfig()的默认配置,否则会丢失 Test Runner 的整套预设(测试环境、transform、Playwright 集成等); snapshotResolver指向第一步创建的snapshot-resolver.js,路径相对于项目根目录;- 该文件也可以由
test-storybook --eject生成后手动修改,两种方式等价。
完成上述两步后重新执行yarn test-storybook,Test Runner 会遍历所有 stories 并运行快照测试,将每个 story 的快照文件生成到你指定的自定义目录中。
变体:仅修改命名约定,保持就近存放
如果你只想把快照扩展名从默认的.snap改为.storyshot,同时仍然存放在测试文件旁边的__snapshots__目录,可以使用配套的另一个实现:
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', };与集中式版本相比,仅有两处差异:
path.join(path.dirname(testPath), ...):用path.dirname保留测试文件的原目录,快照就近生成在src/components/__snapshots__/Button.storyshot;- 一致性样例改为
example.storyshot:因为自定义扩展名参与双向映射,校验样例也必须使用新扩展名,否则 Jest 一致性检查会失败。
其余(test-runner-jest.config.js的配置方式)与集中式版本完全一致,只需复用同一个snapshotResolver指向即可。
运行时行为与快照更新
自定义 resolver 生效后,以下 Test Runner 的 CLI 行为依旧适用(详见官方 CLI 选项):
| 选项 | 作用 | 示例 |
|---|---|---|
-u,--updateSnapshot | 重新记录本次运行中所有失败的快照 | yarn test-storybook -u |
--ci | CI 模式下不自动保存新快照,而是让测试失败,强制配合-u使用 | yarn test-storybook --ci |
--watch/--watchAll | 监听模式,文件变化时重跑全部测试 | yarn test-storybook --watch |
--no-cache/--clearCache | 禁用/清空 Jest 缓存目录 | yarn test-storybook --clearCache |
其中-u与--ci与快照目录定制直接相关:无论快照被重定向到哪里,-u都会在自定义目录中重写快照,而--ci在 CI 中防止旧快照被静默覆盖——这两者结合可以保证"本地用-u更新、CI 只校验不写入"的规范工作流。
原理小结与常见问题
从源码结构看,这套机制完全复用 Jest 的snapshotResolver协议:resolveSnapshotPath决定快照"写到哪里",resolveTestPath决定快照"属于哪个测试",testPathForConsistencyCheck确保两条路径互为逆运算。Test Runner 只是把默认配置通过getJestConfig()暴露出来,让你以最小代价覆盖这一环。
常见问题排查:
- 启动报错 "Consistency check failed":
testPathForConsistencyCheck与两个 resolve 函数不匹配。逐一验证resolveTestPath(resolveSnapshotPath(sample), ext) === sample; - 快照仍然生成在旧目录:确认
test-runner-jest.config.js位于项目根目录,且没有其他 Jest 配置文件(如jest.config.js)优先级更高地覆盖了它; - 扩展名变化后旧快照不匹配:改变扩展名意味着产生一套全新快照,首次运行需配合
-u生成基线; - 拍平目录后出现同名快照冲突:集中式方案中,不同目录下同名的测试文件会映射到同一个
.snap文件,建议在resolveSnapshotPath中加入额外路径段(如组件分类)以避免覆盖。
另外需要注意:Test Runner 本身已被 Vitest addon 逐步取代,官方建议 Vite 驱动的 Storybook 框架优先使用 Vitest addon。但无论使用哪种测试底座,"将快照文件与测试文件解耦、通过解析器控制存放位置"的思路完全一致,本文的 resolver 设计与 Jest 协议同样适用于基于 Jest 的既有项目迁移场景。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考