用途
sanity-visual-coverage 回答的核心问题是:如果某个文件的渲染发生变化,Chromatic 快照能否捕获到?这是一个关于仓库的静态事实,因此由脚本计算得出,不应通过阅读 Chromatic 检查结果或凭记忆回答。
主要能力
该技能提供命令行检查,支持多种模式:
pnpm visual-coverage --changed:检查相对于 origin/main 的变更文件,包括未提交的更改。pnpm visual-coverage --changed --prs:同时标记开放 PR 即将覆盖的文件。pnpm visual-coverage <path> [<path>...]:检查指定文件。pnpm visual-coverage:检查整棵代码树,每个区域一行。pnpm visual-coverage --uncovered:检查整棵树并列出所有未覆盖文件。pnpm visual-coverage --format json ...:输出机器可读格式,支持相同模式。pnpm visual-coverage --help:查看帮助。
对于不在本地的 PR,可执行 gh pr checkout <number> && pnpm visual-coverage --changed --prs。每个触及 packages/**/src/**/*.tsx 或 *.css.ts 的同仓库 PR 还会收到来自 .github/workflows/visual-coverage.yml 的粘性评论,由同一脚本以 --format markdown --prs 生成。审查时应先阅读该评论,其计数清晰可见,逐文件表格位于折叠的 Per-file coverage 开关之后。
脚本位于 scripts/visualCoverage.ts,无依赖,可在 tsx 或纯 Node 22.18+ 下运行(node scripts/visualCoverage.ts)。
覆盖判定标准
Chromatic 快照的是 story,而不是组件。当某个 story 渲染了组件时,该组件即被视为已覆盖。脚本通过直接导入来建模:
- story:
packages/**/src/**/*.stories.tsx,对应 Chromatic 项目 "sanity studio",状态为 covered。 - browser-test:
packages/**/src/**/*.browser.test.tsx,对应 Chromatic 项目 "sanity studio vitest",状态为 covered。 - pending:由开放 PR 添加的
*.stories.tsx(使用--prs),尚无对应项目,状态为 pending,由该 PR 声明。
两个项目都会在每个 PR 上生成快照:story 由 Storybook 构建捕获,浏览器测试的最终状态由 CHROMATIC=1 捕获运行(加上其调用的任何 takeSnapshot())捕获。Playwright 项目("sanity studio playwright")是精选的 opt-in,不建模为覆盖。
当 story 或浏览器测试直接导入某文件,或导入一个导入该文件的 *Story.tsx 测试装置时,该文件即被覆盖。.css.ts 文件继承导入它的 .tsx 文件的覆盖。更深的依赖不计入。TestWrapper 导入整个 sanity 包,因此传递图会将所有内容标记为已覆盖,Chromatic 的 TurboSnap 也有同样的盲点。这就是为什么 PR 上的绿色 Chromatic 检查只表示“受影响的 story 仍然匹配”,而从不表示“你的组件被某个 story 渲染”。
Story 如何映射到 Studio UI
dev/storybook 是宿主。其 dev/storybook/.storybook/main.ts 中的 stories glob 会查找每个工作区包 src 下的 *.stories.tsx。Story 位于组件旁边的 __tests__ 目录中。dev/storybook/stories 下没有任何内容。
有两种 story 形态。对于 packages/sanity/src/ui-components 包装器,使用普通变体网格(例如 Button.stories.tsx 导入 ../Button)。对于需要工作区、i18n 或图层的任何内容,使用测试装置 story。测试装置是 <Name>Story.tsx,包装 TestWrapper,而 <Name>.stories.tsx 是一个薄 CSF 文件,其组件是测试装置。
浏览器测试内联定义其测试装置组件(<Name>.browser.test.tsx 内的 function <Name>Harness()),因此每个 *Story.tsx 都属于一个 story。Vitest Chromatic 集成会就地快照测试的最终状态;不要将测试的测试装置提取到 *Story.tsx 中为其放置 story。它们的覆盖显示为 browser-test 证据。
"ui5 sentinel" 和 "box sentinel" 是同一回事。添加 story 是为了让 @sanity/ui 到 ui5 的 Box/Flex/Card 迁移在交换落地前获得快照。测试装置渲染最可能漂移的状态(色调、间距、截断、空状态),仅使用固定文案。命名遵循上述测试装置模式。title 为 Area/Component。Sentinel 是普通 story——可浏览,带有 JSDoc 描述说明其固定内容;没有隐藏它们的标签。可参考 packages/sanity/src/core/field/diff/components/__tests__ 下的 FieldDiffChromeStory.tsx 和 FieldDiffChrome.stories.tsx 作为参考对。
一个 story 恰好覆盖其测试装置导入的组件。DocumentLayout story 也会绘制按钮和卡片,但只有 Button story 是 Button 的 sentinel。
决策流程
在分支上运行 pnpm visual-coverage --changed --prs,然后逐文件处理:
- covered:完成。如果更改添加了 story 未渲染的状态(新色调、空状态、截断情况),扩展现有 story 或测试装置。不要为同一组件添加第二个 story。当唯一证据是 browser-test 时,状态由 Vitest 集成快照;扩展该测试或其测试装置,而不是为其添加 story。
- pending:不要添加 story。报告中有 PR 编号。审查该 PR,或在其上评论说明缺少你需要的变体。
- uncovered,且文件绘制了某些内容(布局、色调、间距、文本):按照
.agents/skills/sanity-visual-regression/SKILL.md添加覆盖,使用其 "Which source owns a state" 表选择来源:当状态可从 props 或一个 play 步骤到达时使用 story,当到达状态意味着驱动 UI 时使用*.browser.test.tsx。对于 story,在创建新测试装置前,先复用同一目录中现有的*Story.tsx测试装置;绝不要用浏览器测试的测试装置构建 story。 - uncovered,且文件是 provider、hook 包装器、context 或仅渲染 children:无需快照。在 PR 中说明,而不是添加 story。
对于迁移规划,pnpm visual-coverage --uncovered --prs 列出缺口。从中选择,不要手动调查。
避免重复覆盖 PR
ui5 迁移的 sentinel 覆盖以一系列标题为 test(storybook): add ui5 ... sentinels ... 的 PR 落地(原始堆栈 #14056 至 #14511 已合并;较新的通常以相同方式堆叠)。迁移本身落在 chore/ui-v5-* 分支上。
--prs 已考虑每个添加 *.stories.tsx 的开放 PR。报告为 pending 的文件已被声明。
查看开放内容:gh pr list --state open --search "test(storybook) in:title" --json number,title,headRefName,baseRefName。
不要 rebase、重写或推送到这些分支。新的覆盖 PR 在其依赖开放 PR 中添加的测试装置时,应基于该开放 PR;当其文件与所有开放 PR 不相交时,应基于 main。
在打开覆盖 PR 前,再次使用 --prs 运行检查。如果你添加的任何内容现在在其他地方处于 pending,则放弃它。
适用场景
- 在 PR 审查中判断变更文件是否已被视觉回归覆盖。
- 规划 ui5 迁移的 sentinel 覆盖缺口。
- 避免重复添加已由其他开放 PR 声明的 story。
限制
- 覆盖判定仅基于直接导入,不追踪传递依赖。
- Playwright 项目不建模为覆盖。
- 脚本无依赖,需在 tsx 或 Node 22.18+ 下运行。
- 不提供收费、价格或平台信息(资料未提供)。