Sanity Studio 视觉回归测试的 Storybook 基座:从 Chromatic 快照到 Vercel 部署
【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity
本篇技术指南聚焦 Sanity Studio 官方 monorepo 中的sanity-storybook包(dev/storybook),讲解它如何作为整个仓库的视觉回归测试基座:通过 Storybook 承载组件故事、交给 Chromatic 在每次 PR 上自动做像素级差异对比,并部署到 Vercel 提供稳定的浏览地址。读完本文,你将掌握这个 monorepo 中 Storybook 的命令体系、story 组织约定、浏览器级测试与故事之间的职责划分、Chromatic 工作流配置,以及 Vercel 部署的完整初始化步骤。
为什么 Sanity Studio 需要一套 Storybook
Sanity Studio 是一个大型 pnpm monorepo(pnpm-workspace.yaml),其 UI 层正在经历两场大规模的样式体系迁移:
- styled-components → vanilla-extract:把运行时 CSS-in-JS 替换为编译期提取的样式方案;
@sanity/ui→ui5:升级底层 UI 组件库面。
这类迁移最危险的地方在于:逻辑可能完全正确,但一个像素的间距、颜色或圆角变化就会破坏整个 Studio 的视觉一致性。因此 dev/storybook/README.md 明确写道,sanity-storybook的核心使命就是用自动化的视觉 diff 守护这两场迁移——它被 Chromatic 在每次 PR 上截图快照,并部署到 Vercel 供团队审阅。
整个视觉回归工作流(包括如何新增覆盖)记录在 .agents/skills/sanity-visual-regression/SKILL.md 中,本文则聚焦于该包本身的工程设施。
命令速查:本地开发、构建与手动发布
dev/storybook/package.json 定义了四个脚本,分别对应四类使用场景:
# 从仓库根目录执行 pnpm dev:storybook # Storybook 开发服务器,地址 http://localhost:6006 pnpm build:storybook # 通过 turbo 做静态构建(产物输出到 dev/storybook/storybook-static) # 从 dev/storybook 目录执行 pnpm test # 用 vitest 浏览器模式运行每一个 story(基于 @storybook/addon-vitest) pnpm chromatic # 手动发布并截图快照(需要环境变量 CHROMATIC_PROJECT_TOKEN)细节解读:
dev脚本对应storybook dev --port 6006 --no-open,固定端口 6006,且启动时不自动打开浏览器;build对应storybook build,产物目录为storybook-static;test对应vitest run,其行为由 dev/storybook/vitest.config.mts 定义(下文详解);chromatic对应 Chromatic CLI,可用于手动触发一次快照发布。
从根目录运行pnpm dev:storybook与pnpm build:storybook,依赖的是根 package.json 中的对应 turbo 任务;pnpm build:storybook会经由 dev/storybook/turbo.json 声明dependsOn: ["^build"],保证上游 workspace 包先构建完成,再产出storybook-static/**。
story 的组织约定:包拥有、就近放置
dev/storybook本身不存放任何 CSF 故事文件,它只负责 Storybook、Chromatic 和 addon-vitest 的基础设施。故事的归属原则是:
Stories are package-owned and co-located.每个 story 属于它覆盖的那个 workspace 包。
具体约定(依据 dev/storybook/README.md 与 .agents/skills/sanity-visual-regression/SKILL.md):
- Storybook 自动发现workspace 包
src目录下的*.stories.tsx文件; - story 与组件就近放置:通常把
*.stories.tsx放在组件或 harness 所在的同一个__tests__目录里。真实示例见 packages/sanity/src/ui-components/button/tests/Button.stories.tsx 与 packages/sanity/src/ui-components/dialog/tests/Dialog.stories.tsx; - 使用包内局部导入,不要跨 workspace 边界引用远端模块;
- 不要在
dev/storybook/stories/下新增 CSF 文件。
浏览器测试不重复导出为 story
这是一个容易踩坑的关键分工:vitest 浏览器模式套件(*.browser.test.tsx)本身就是独立的 Chromatic 快照来源。@chromatic-com/vitest会在测试结束时自动归档其最终状态(对应 Chromatic 工作流中的vitest-visualjob)。因此:
- 每个浏览器测试内联保留自己的 harness 组件(
function FooHarness()写在测试文件里),不再为它单独写 story; - 仓库中每一个
*Story.tsx都是被某个*.stories.tsx引用的 Storybook harness; - story 只覆盖浏览器测试没有渲染到的状态;
- 禁止把浏览器测试重新导出成 story,也禁止为覆盖某个已由测试快照的状态而再写一个重复的 story——那会得到同一批像素的重复快照。
Playwright 留在 e2e/,不在 Storybook 里
e2e 套件有自己独立的 Chromatic 项目(e2e/studio-visual-test.ts,由.github/workflows/e2e.yml通过chromaui/action上传)。因此sanity-storybook的playwright依赖仅仅是@storybook/addon-vitest渲染 story 所用的浏览器 runner,该包内不承载任何 spec、fixture 或@chromatic-com/playwright接线。不要为了拿快照而把 e2e spec 改写成 story(或反过来)。
迁移哨兵故事(migration sentinels)
针对前面提到的两场样式迁移,组件本地的 story 专门覆盖测试无法捕获的视觉状态:
ui-components包装组件变体:即@sanity/ui→ui5的迁移表面,优先覆盖 card 与 tone 相关组件(tone 会级联影响所有组件);- 已完成 vanilla-extract 迁移的组件,如 change indicators、
DocumentLayout,作为迁移哨兵。
需要 Studio 上下文(workspace/i18n/layers)的状态,则复用浏览器测试同款 mock studio 包装器TestWrapper(表单输入再加TestForm),它们来自 packages/sanity/test/browser,确定性、无网络,通过 story-only 的*Story.tsxharness 接入。交互后才会出现的浮层(tooltip、菜单等)则用play函数配合storybook/test的userEvent+waitFor/expect(...).toBeVisible()驱动,查询within(document.body)访问 portal 内容——Chromatic 和 addon-vitest 都会先跑play再截图,因此快照中能看到打开的浮层。
每个 story 都可浏览
所有故事都面向"被阅读"而编写,构成组件用法的活文档:不要用tags把 story 从侧边栏(!dev)或文档(!autodocs)隐藏起来。曾经用来隐藏 vitest 派生故事的!dev/!autodocs/vrt-only标签,已随那些故事一起消失——浏览器测试改为就地快照后,这里只保留"给人看"的故事。如果一个状态不值得人看,就不该进 Storybook,而应放进浏览器测试。只想保留可浏览性、不希望被快照的 story,设置parameters: {chromatic: {disableSnapshot: true}}即可。
构建配置:让 story 渲染得和真实 Studio 一模一样
dev/storybook/README.md 特别强调:.storybook/main.ts的 Vite 配置镜像了 packages/sanity/vitest.browser.config.mts,具体包含三部分:
monorepoexports 条件:把 workspace 包解析到 TypeScript 源码,而不是已构建产物;- vanilla-extract 插件:保证迁移后的组件样式正确编译;
- React Compiler transform:与 Studio 运行时保持一致。
这样 story 渲染效果与真实 Studio、与浏览器测试完全一致,快照才有意义。而 dev/storybook/vitest.config.mts 则补充了浏览器测试侧的细节:
- 通过
@storybook/addon-vitest/vitest-plugin的storybookTest()注册,configDir指向.storybook; storybookScript: 'pnpm dev'用于 vitest watch 模式把测试失败关联回 story UI;- 因为 story harness 会启动完整的 Studio 表单构建器,
testTimeout放宽到 30 秒,retry: 1,expect.poll超时 10 秒; - 浏览器用
@vitest/browser-playwright,headless chromium,视口 1280×900——这与 .agents/skills/sanity-visual-regression/SKILL.md 中提到的.storybook/preview.tsx全局桌面模式视口一致,保证 story 与测试的截图尺寸统一。
注意该工程刻意没有注册进根 vitest.config.mts 的多项目运行——因为它需要真实浏览器,只能在pnpm --filter sanity-storybook test下单独运行。
确定性规则
视觉回归最怕随机性。skill 文档明确规定:harness story 靠 mock client/workspace 天然确定、无网络;绝不渲染实时时间戳、随机 id 或未完成的加载态;Chromatic 会自动暂停 CSS 动画。需要微调时使用parameters.chromatic旋钮:delay(截图前等待毫秒数,Portable Text 故事用 300ms 等待编辑器启动)、diffThreshold、disableSnapshot、modes(视口/主题矩阵)。
Chromatic 集成:每次 PR 的自动快照
dev/storybook/chromatic.config.json 只有两个关键字段:
{ "$schema": "https://www.chromatic.com/config-file.schema.json", "buildScriptName": "build", "onlyChanged": true }buildScriptName告诉 Chromatic 用build脚本构建 Storybook;onlyChanged: true即启用TurboSnap——只对本次变更影响的 story 截图,大幅节省快照预算。
工作流:storybook 与 vitest-visual 双 job
.github/workflows/chromatic.yml 在pull_request和push到main时触发,包含两个 job:
storybook(Storybook visual tests):
actions/checkout@v7使用fetch-depth: 0拉取完整 git 历史,这是 Chromatic 找基线构建、追溯 TurboSnap 变更文件的前提;- checkout 用 PR 分支而非 GitHub 的合并提交,保证 Chromatic 能定位它对比的 commits;
- 先用
detect-code-changes判断是否有变更,有变更才执行pnpm build构建全部包,再运行chromaui/action,token 用CHROMATIC_PROJECT_TOKEN_STORYBOOK,workingDir: dev/storybook; exitZeroOnChanges: true:烧录期(burn-in)内检查不阻断合并,差异只作为报告;autoAcceptChanges: main:合并到main时自动接受新基线。
vitest-visual(Vitest browser visual tests):
- 同样拉全量历史,额外做 Playwright 浏览器版本探测、缓存与安装(
playwright install-deps chromium/playwright install chromium); - 设置
CHROMATIC=1与SANITY_VITEST_BROWSER=chromium后运行pnpm --filter sanity test:browser,把每个*.browser.test.tsx的结束态归档; - 校验
packages/sanity/.vitest/chromatic/preview-stats.json存在(TurboSnap 需要它); - 再经
chromaui/action以vitest: true模式上传到独立的 "sanity studio vitest" 项目(token:CHROMATIC_PROJECT_TOKEN_VITEST)。
两个快照来源、两个 Chromatic 项目、各自独立的 token——skill 文档强调"一个集成类型对应一个项目",不要把某一来源的输出上传到另一个项目的 job。此外还有一个受控的第三来源:Playwright e2e 的takeSnapshot(),上传到 "sanity studio playwright" 项目(token:CHROMATIC_PROJECT_TOKEN_E2E),由于 e2e 跑在每 PR 的 staging 数据集上(实时时间戳、presence、并发写入),全局自动快照只会产生纯 diff 噪声,因此 e2e/studio-test.ts 全局禁用自动快照,仅在确定性时刻按 spec 显式takeSnapshot()接入。
浏览器测试内的快照控制
在*.browser.test.tsx内部,@chromatic-com/vitest提供三种粒度:
- 自动快照:每个测试结束自动归档,Chromatic 中的命名是
describe 链 / it 标题 / Snapshot #n; - 按作用域退出:
configure({disableAutoSnapshot: true}),在文件顶层调用作用于整个文件,在describe()内作用于该套件,在test()内仅作用于该测试——适合纯交互检查或仅清理的状态; - 定向快照:
await takeSnapshot('状态名'),抓取测试中途经过但不结束于的状态(如关闭前的菜单、拖拽中途),必须await,未 await 的调用会导致测试失败。
两个 helper 在普通运行中都是 no-op(firefox/webkit 上亦然),只有CHROMATIC=1才真正开启捕获;普通运行只会在被 gitignore 的.vitest/chromatic下留下显式takeSnapshot()的归档。
Vercel 部署:稳定的浏览地址与 PR 预览
Storybook 部署到sanity-sandboxVercel 团队,项目名studio-storybook,生产地址为https://studio-storybook.sanity.dev,并通过 Git 集成自动为每个 PR 生成预览部署。
一次性项目初始化(维护者从仓库根目录执行)
vercel是根 devDependency,初始化分四步(依据 dev/storybook/README.md):
# 1. 认证(一次性) pnpm vercel login # 2. 创建项目 + 设置 Root Directory + 首次预览部署(交互式): # - Set up and deploy? 选 yes # - Scope: sanity-sandbox # - Project name: studio-storybook # - "Code directory?" -> 填 ./dev/storybook # - Vercel 会把框架误判为 Vite;没关系,vercel.json 已钉死 # buildCommand/outputDirectory 并覆盖这一误判 # - 按提示连接检测到的 Git 仓库(origin) # - monorepo 超出 Vercel 1.5 万文件的上传上限,因此必须加 --archive=tgz pnpm vercel --scope sanity-sandbox --archive=tgz # 3. 验证一次生产部署 pnpm vercel --prod --scope sanity-sandbox --archive=tgz # 4. 把生产域名指向项目 pnpm vercel domains add studio-storybook.sanity.dev studio-storybook --scope sanity-sandbox几个关键注意点:
- 首次部署的交互式提示正是把 Root Directory(
dev/storybook)持久化到项目上的时机;setup 阶段连接 Git 仓库即可自动获得 PR 预览与main生产部署,无需再单独执行vercel git connect; - dev/storybook/vercel.json 钉死了构建方式:
cd ../.. && pnpm exec turbo run build --filter=sanity-storybook,输出目录storybook-static,并加了 SPA 路由回退(/(.*)→/index.html)。这样上游 workspace 包会先被构建,Vercel 误判的框架预设也就无关紧要了; - Git 集成构建会从根目录克隆 monorepo 并安装整个 pnpm workspace(保证
workspace:*与catalog:协议可解析),行为与test-studio-preview-iframe项目一致; - 在该包合入
main之前,推送到main触发的生产部署会因缺 Root Directory 而失败——这是 PR 合并前可预期的噪声; .vercel/链接元数据与其他 dev 应用一样被 gitignore;- 可选(仅仪表盘设置):把项目的 Ignored Build Step 设为
npx turbo-ignore sanity-storybook,让不影响 Storybook 的提交跳过部署; - Chromatic 会给每个发布的 Storybook 构建生成永久链接,因此 Vercel 部署承担的是稳定 URL + PR 预览的职责,两者互补。
小结:一整套"人机分流"的视觉回归分工
回看整个设计,sanity-storybook的价值不在于"多一个 Storybook",而在于它用清晰的职责划分把三类快照来源组织成互补体系:
| 状态来源 | 归属 | 快照项目 |
|---|---|---|
仅凭 props/fixtures 或一次play交互可达 | *.stories.tsx(包内就近放置) | sanity studio |
| 需要驱动 UI(输入、拖拽、剪贴板、视口变化) | *.browser.test.tsx(结束态就地快照) | sanity studio vitest |
| 完整 Studio 外观 + 真实部署与数据集 | Playwright spec(e2e/studio-visual-test.ts,只读状态) | sanity studio playwright |
dev/storybook只做基建:stories 全部由各 workspace 包"包拥有"并就近存放,浏览器测试就地快照,Playwright 留在 e2e/,而 Storybook 只保留"给人看"的组件状态文档。配合 Chromatic 的 TurboSnap 与双项目工作流,Sanity Studio 得以在 styled-components → vanilla-extract 和@sanity/ui→ ui5 两场迁移中持续获得像素级的回归保护。
【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考