
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.jsmodule.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 用它验证上述两个函数互为逆运算填一个示例测试文件名如exampleresolveTestPath中的snapshotExtension参数由 Jest 传入默认即.snaptestPathForConsistencyCheck必须保证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.jsfileName.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-storybookTest 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--ciCI 模式下不自动保存新快照而是让测试失败强制配合-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 failedtestPathForConsistencyCheck与两个 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),仅供参考