Storybook Test Runner 自定义快照目录:用 snapshotResolver 重定向快照文件位置
2026/9/18 23:37:20 网站建设 项目流程

Storybook Test Runner 自定义快照目录:用 snapshotResolver 重定向快照文件位置

本指南讲解如何在 Storybook Test Runner 中通过自定义快照解析器(snapshot resolver)将快照测试生成的.snap文件重定向到指定目录,摆脱默认的__snapshots__存放位置。读完本文,你将掌握resolveSnapshotPathresolveTestPathtestPathForConsistencyCheck三个核心字段的完整语义,能够为多项目、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.jsButton.stories.js
  • fileName.replace(/\.[^/.]+$/, ''):用正则去掉最后一个扩展名,Button.stories.jsButton.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', };

与集中式版本相比,仅有两处差异:

  1. path.join(path.dirname(testPath), ...):用path.dirname保留测试文件的原目录,快照就近生成在src/components/__snapshots__/Button.storyshot
  2. 一致性样例改为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 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),仅供参考

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

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

立即咨询