首页
/ Storybook 沙箱 Docgen 基线测试:基于真实构建产物的组件文档快照校验机制

Storybook 沙箱 Docgen 基线测试:基于真实构建产物的组件文档快照校验机制

2026-09-06 18:05:20作者:余洋婵Anita

导读

docgen-harness 是 Storybook 仓库内一套面向组件文档(docgen)生成器的测试框架,而 sandbox-baselines(沙箱基线)是其"整条链路"维度的核心防线:对每一个启用了实验性 Docgen Server 的沙箱模板,build-storybook 构建后每个组件都会落盘一份 DocgenPayload 快照,这套工具把这些快照以"每个组件一个 JSON"的形式固化到仓库中,并提供精确到组件字段的比对与重录流程。本文基于仓库内 code/lib/docgen-harness/src/sandbox-baselines/README.md 展开,结合同目录源码与测试,说明基线数据从哪来、覆盖哪些模板、如何更新、失败如何分级解读,帮助贡献者在改动文档提供方(provider)、预设链(preset chain)或索引逻辑时第一时间看到可审查的差异,而不是靠人工在构建产物里"找变化"。

一、沙箱基线要解决什么问题:单组件 fixture 之外的另一半

在进入实操之前,先理解这套机制在测试体系中的位置。

docgen-harness 中紧邻 sandbox-baselines 的 fixture 套件(src/angular/__testfixtures__)用来证明"提取器"(extractor)能正确处理各种刻意编写的组件写法——它们输入的是针对测试编写的组件源码,验证的是提取算法本身。

而 sandbox baselines 覆盖的是另一面:对于一个完整的沙箱工程,文档提供方在真实环境里到底产出了什么。这个产出是经过完整路径解析的:

  • 真实 preset 链(real preset chain);
  • 真实的 story 索引(story index);
  • 真实的 Compodoc 运行(对 Angular 而言)。

README 的表述看,组件重名(name collisions)、无法解析的 import、tsconfig 覆盖缺口(tsconfig coverage gaps),这类问题只会在"整个沙箱"的尺度上暴露出来,任何单组件 fixture 都无法复现。因此,基线机制本质上是对 fixture 单元测试的互补:前者验证"提取器能不能读懂组件",后者验证"提供方/预设/索引在真实工程中能否把组件找全并正确文档化"。

二、数据从哪来:一次构建,每组件一份快照

2.1 产物目录约定

沙箱基线不是对内存中对象的录制,而是读取 build-storybook 的真实静态产物:

  • 当沙箱 main 配置同时开启 features.experimentalDocgenServerfeatures.componentsManifest 时,build-storybook 会按组件把 docgen 快照写入 storybook-static/services/core/docgen/ 目录,一个组件一份 JSON。

read-static-docgen.ts 中,该相对目录被定义为常量 DOCGEN_SNAPSHOT_DIR = join('services', 'core', 'docgen'),实际路径为 storybook-static/services/core/docgen/readStaticDocgen() 会:

  1. 读取该目录下所有 .json 文件;
  2. 对每个文件按 { components: Record<string, DocgenPayload> } 结构解析;
  3. 逐条把组件 payload 转成基线条目;
  4. 按组件 id 做 key 排序后返回(见 read-static-docgen.ts),保证重录时 diff 可读。

2.2 只保留可移植字段:把引擎私有数据挡在仓库外

DocgenPayload 带有索引签名,例如 Angular 提供方会把原始 Compodoc 条目挂在 payload 上——在普通沙箱里这份原始文本可达 约 117KB。基线机制采用 allow-list(白名单)而非 exclude-list,只保留契约内的可移植字段(见 read-static-docgen.ts):

const PORTABLE_FIELDS = [
  'id',
  'name',
  'path',
  'description',
  'summary',
  'jsDocTags',
  'argTypes',
  'subcomponents',
  'error',
] as const satisfies readonly (keyof DocgenPayload)[];

基线条目类型 SandboxBaselinePick<DocgenPayload, ...>(仅含上表字段)。这样,依赖引擎的扩展数据(大量 sourceCode 等)不会进入仓库,基线只守护真正跨引擎的文档契约。

以 Angular 沙箱中的一个枚举组件基线为例(示例基线文件),其 argTypes 携带了 descriptiontable.categorytable.type.summarytype.nametype.value(枚举取值列表)等结构化信息——这些正是后续比对时"一个 arg 消失"或"默认值丢失"可以精确报警的前提。

2.3 路径归一化:让基线与机器无关

沙箱在不同开发机与不同 CI 上路径各不相同,因此基线录制时会把错误消息等文本中的绝对沙箱路径统一改写为 <sandbox> 令牌。实现上 normalizePaths 同时处理 原生拼写与 POSIX 拼写\/ 两种写法都做替换),并在 normalizeDeep 中对字符串、数组、对象递归完成归一化——从而一份在某平台录制的基线可以在另一平台正常校验。

2.4 通过全局引用的组件会被跳过

沙箱中的共享模板故事(monorepo 的共享 template stories)往往通过 globalThis.__TEMPLATE_COMPONENTS__.* 引用组件,此时解析器既没有 import 可跟随,也没有可扫描的文件,这类组件按构造必然报错(error payload),它们反映的是"模板故事装载机制",与 docgen 本身无关。

isGloballyReferenced 通过 payload.name.startsWith('globalThis') 识别并跳过这类条目(见 read-static-docgen.ts)。跳过它们的量化效果:

  • Angular 沙箱原始记录组件 111 个 → 37 个
  • 其中 30 个被正常文档化,7 个是声明在故事文件中的 Angular 类(inline class)。

2.5 空录制的保护

若 docgen 目录不存在、或读不到任何快照、或所有组件都属全局引用,readStaticDocgen 会直接抛错("No docgen snapshots at ..." / "refusing to record an empty run"),绝不静默记录一个空集——见 read-static-docgen.ts

三、覆盖哪些模板:由模板定义本身推导,无需人工同步

基线覆盖范围不是在 harness 里维护一份清单,而是从模板定义本身读取——凡是 main 配置同时打开 experimentalDocgenServercomponentsManifest 的沙箱模板都会被纳入。

sandbox-templates.ts 中两个特性被定义为常量:

const DOCGEN_SERVER_FEATURES = ['experimentalDocgenServer', 'componentsManifest'] as const;

docgenServerTemplates() 遍历全部模板,用 enablesDocgenServer 过滤出两个特性均为 true 的模板集合。也就是说:为某个模板打开这两个 flag,就足以把它纳入基线覆盖,且没有任何副本需要保持同步

反过来也有一条硬性约束:一个已打上 flag 却还没有任何基线记录的模板,校验运行会失败而不是悄悄跳过——见 run.ts 中"no baselines committed ... Record them with ..."的错误提示。

当前仓库 __baselines__ 目录下实际固化了两套沙箱基线(可当作该机制的现状示例):

  • angular-vite-docgen-server-ts(37 个组件级 JSON,含 example-buttonexample-headerexample-page 及大量 stories-frameworks-angular-vite-* 条目);
  • vue3-vite-docgen-server-ts(含 defineModeldefineSlots、Option API、TS 引用类型等 vue 组件元数据场景)。

四、如何更新基线:verify 与 update 两条命令

4.1 工作流命令

从 README 与 run.ts 的文件头注释可得到完整流程:

# 1. 构建沙箱(产出 docgen 快照)
yarn task build --template <template> --start-from auto

# 2. 进入 docgen-harness 包
cd code/lib/docgen-harness

# 3. 校验每一个 server-docgen 模板
yarn baselines:sandbox

# 4. 审阅 diff 后重录
yarn baselines:sandbox --update

其中 baselines:sandbox 脚本定义于 package.json

"baselines:sandbox": "node --import jiti/register ./src/sandbox-baselines/run.ts"

4.2 命令行选项

入口通过 zod schema 校验参数(见 options.ts),支持三个选项:

选项 类型 说明
--template <key> string 只处理某一个模板;缺省时处理所有开启 server-docgen 的模板
--sandbox <dir> string 指向一个非默认位置的沙箱目录
--update / -u boolean 重录模式(否则为校验模式)

实现上 parseBaselineRunOptions 对空字符串的 --template= 做了显式拒绝——调用方点名了模板却收到空值,会被当作"未指定"而把运行范围悄悄放大到全部模板,因此必须报错而不是当作缺省。

沙箱目录与基线目录的命名约定见 run.ts:二者都把模板 key 中的 / 替换为 -,分别落到沙箱目录 SANDBOX_DIRECTORY/<key 改写>__baselines__/<key 改写>

CI 会在逐个构建完每个被覆盖的沙箱后执行校验形态(verify)的命令。

4.3 原子化的写入策略

重录时 write 采用"先写暂存、再整体替换、旧版备份"的三段式策略,避免已提交基线出现"只换了一半"的中间态:

  1. 新集合先写入同级目录 <baselineDir>.staging
  2. 旧基线目录整体改名(renameSync)为 .backup 而非直接删除;
  3. 再把 staging 改名为正式目录;若换名本身失败,会把 backup 恢复回来;
  4. finally 中清理 staging 与 backup。

此外,每条基线以 key 排序后的 JSON 落盘: stable-stringify.ts 递归地对对象键做 localeCompare 排序后再 JSON.stringify(缩进 2),使"重录产生的 diff 反映内容变化,而不是生产者恰好输出的键顺序";配套测试也专门验证"忽略键顺序,因为生产者可能在不改变语义的情况下改变顺序"(见 compare-baselines.test.ts)。

五、如何阅读一次失败:精确匹配门禁与两级严重度

5.1 门禁语义:精确匹配,不做"更好就放行"

基线的比对门禁是精确匹配:候选产物与已提交基线有任何差异都会导致运行失败——即使是明确无误的改进(unambiguous improvement)也一样

原因在于(README 的原话精神):沙箱基线是"整条提供方链路产出的录像",链路上每一步移动都值得 reviewer 的目光审视,因此不存在"当前值或更好即放行"的豁免通道。该语义对应 run.ts:有 findings 就报错退出并提示"一旦理解 diff,用 --update 重录"。

5.2 两级严重度:regression 与 change

README 明确指出:severity 告诉你看到的是哪一类失败,而不是是否阻塞。其语义在 compare-baselines.ts 中被建模为 BaselineFinding.severity 的联合类型:

regression —— docgen 被明确证实的变差,这类结果要的是修复而不是重录。对应场景(见 compare-baselines.ts 及测试 compare-baselines.test.ts):

  • 组件或某个 arg 消失(component-removedargtypes 的 lost-arg);
  • 组件不再被文档化(docgen-lost:原本记录为"已文档化"的组件现在报错或产不出 argTypes);
  • 记录的默认值(default)消失。

change —— 其余一切差异,属于中性漂移,审阅 diff 后用 --update 采纳即可:

  • 新增组件、组件由未文档变为已文档(docgen-gained);
  • 新增 arg、某个类型被更精确地解析、某段 prose 被改写;
  • 其它字段级差异(field-changed)。

compare-baselines.ts 中,判断 argTypes 差异是否为 regression 借助了 compareArgTypes(来自 ../compare/argtypes.ts),并把 lost-arglost-default 映射为回归措辞;compare-baselines.test.ts 中还特别覆盖了两个边界:新增 arg 被当作 change 而不是需要修复的失败(L115-L122),以及类型获得更高保真度被判为 change 而非 regression(L126-L136)——因为把改进称为 regression,会误导 reviewer 去找一个并不存在的 bug。

5.3 输出格式:回归在前

formatFindings['regression', 'change'] 顺序分组输出,回归永远排在前面;同组内每行以 - <component> [<kind>] <message> 呈现,例如"was documented with N arg(s), now errors: ..."。配套测试 it('lists regressions before changes') 也验证了这一排序约定。相比笼统的"something moved",每条 finding 都指明组件、差异类别与前后内容摘要,重录时能直接生成可审查的 diff。

六、运行流程的实现骨架(源码印证)

整个 CLI 的入口逻辑集中在 run.tsmain() 中:

const { template, sandboxDir, update } = parseBaselineRunOptions(process.argv.slice(2));
const templates = template ? [template] : docgenServerTemplates();
  • docgenServerTemplates() 返回空集,会打印"没有任何沙箱模板启用 server docgen……"并让 process.exitCode = 1——注释明确道出缘由:在此处静默会"看起来像全数通过,实际什么都没查";
  • 逐个模板执行 runTemplate,其输出先汇报读到的组件总数与其中已文档化的数量(read N component(s) from ... (M documented));
  • runTemplate 在 update 模式下直接调用 write() 重录;在校验模式下读取已提交基线并与候选比对,无差异输出 baselines match.,有差异输出格式化的 findings 并返回 false;
  • 任何模板失败都会让整个进程以非零码退出。

校验/重录两条路径、三级模块(读静态产物 → 比对 → 稳定序列化落盘)、两级严重度与按字段报告的 finding 结构,构成了一个"构建产物级、可读 diff、可自动回归检测"的闭环。

七、何时应该重录、何时应该修复

结合 README 与 run.ts 的错误提示,可提炼出明确的处置原则:

  1. 输出中若含 regression(组件消失、文档化丢失、arg 或默认值消失)——先别急着 --update,这通常意味着提供方/索引/preset 链路出了真问题,优先修复代码;
  2. 输出只有 change(新增组件、新增 arg、类型保真度提升、描述文字变化)——在 diff 里逐条确认符合预期后,用 yarn baselines:sandbox --template <key> --update 采纳;
  3. 新模板想纳入覆盖——在模板定义的 mainConfig.features 里同时打开 experimentalDocgenServercomponentsManifest,然后先构建、再以 --update 录制首版基线,此后该模板自动进入 CI 校验范围。
# 单个模板重录的完整示例
yarn task build --template angular-vite/docgen-server-ts --start-from auto
cd code/lib/docgen-harness
yarn baselines:sandbox --template angular-vite/docgen-server-ts --update

(模板 key 的实际拼写以 sandbox-templates.ts 中定义的 key 为准;--template--sandbox 也可组合使用,后者用于指向非默认路径的已构建沙箱。)

总结

docgen-harness 的 sandbox baselines 用"每个组件一份 JSON 快照 + 精确匹配比对"的方式,把文档提供方在真实沙箱里的全部产出变成版本可追踪、失败可分级、diff 可审查的回归防线。它从 storybook-static/services/core/docgen/ 读取 build-storybook 的落盘产物,只保留 DocgenPayload 的可移植字段,做沙箱路径归一化、跳过全局引用组件、拒绝空录制,覆盖范围直接由模板的 experimentalDocgenServercomponentsManifest 两个特性 flag 推导。理解 regressionchange 的分野——前者是 bug 要修复、后者是漂移可 --update 采纳——是在这套机制下高效工作的关键。相关源码与测试均可从 sandbox-baselines 目录 出发继续深入研读。

登录后查看全文
热门项目推荐
相关项目推荐