Storybook 覆盖率测试遇上--test优化构建:build.test.disabledAddons让 @storybook/addon-coverage 重新生效
本篇指南聚焦 Storybook 官方文档《Test runner》中“覆盖率插件不支持优化构建(The coverage addon doesn't support optimized builds)”一节给出的典型修复方案。该方案的核心是一条main.js|ts配置代码片段(即仓库中 docs/_snippets/storybook-coverage-addon-optimized-config.md),用于解决:当你用storybook build --test生成面向测试的性能优化构建后,覆盖率插件不再对代码插桩、无法产出覆盖率数据的问题。读完本文,你将理解--test构建的取舍逻辑、build.test.disabledAddons的精确语义与底层过滤实现,并能在 CSF 3 / CSF Next、React / Vue / Angular / Web Components 等多种技术栈下直接落地这一配置。
问题缘起:优化构建为什么会让覆盖率插件“失灵”
Storybook 官方的 Test runner(基于 Playwright 的故事级测试工具)通常把测试跑在一个专门为测试优化过的生产构建上。该构建通过给storybook build传入--test标志生成,目的是把对测试无意义、却拖慢构建与运行时速度的特性剔除掉。
在 docs/writing-tests/integrations/test-runner.mdx 的 Troubleshooting 一节中,官方明确描述了这一现象:
- 你为提升性能执行了带
--test的生产构建; - 同时你又依赖覆盖率插件(
@storybook/addon-coverage)对被测代码做 Istanbul 插桩以统计覆盖率; - 结果会发现覆盖率插件根本没有对代码插桩——因为
--test标志会移除对性能有影响的插件,例如 Docs 与 coverage 插件本身; - 解决办法:修改
.storybook/main.js|ts,给出build.test.disabledAddons配置,让 coverage 插件能继续工作——代价是构建会变慢。
也就是说,--test的“默认禁用名单”是一套覆盖了 Docs、coverage 等性能敏感插件的自动策略;当业务确实需要在这些优化构建上测覆盖率时,就必须显式地覆写这套名单。本节对应的文档原文说明了配置路径是.storybook/main.js|ts(由根目录的 docs/api/main-config/main-config-build.mdx 统一定义其结构与类型)。
修复配置全貌:一份可复制的.storybook/main.js|ts
官方给出的修复片段(即关联文档 docs/_snippets/storybook-coverage-addon-optimized-config.md)在不同语法与框架下有多个等价变体。先看最通用的CSF 3写法:
JavaScript(.storybook/main.js,CSF 3)
export default { // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. framework: '@storybook/your-framework', stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: ['@storybook/addon-docs', '@storybook/addon-vitest', '@storybook/addon-coverage'], build: { test: { disabledAddons: ['@storybook/addon-docs'], }, }, };TypeScript(.storybook/main.ts,CSF 3)
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. import type { StorybookConfig } from '@storybook/your-framework'; const config: StorybookConfig = { framework: '@storybook/your-framework', stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: ['@storybook/addon-docs', '@storybook/addon-vitest', '@storybook/addon-coverage'], build: { test: { disabledAddons: ['@storybook/addon-docs'], }, }, }; export default config;配置只改动了三处关键信息:插件列表补上了@storybook/addon-vitest与@storybook/addon-coverage,同时在build.test.disabledAddons中把@storybook/addon-docs显式列入禁用名单。
各技术栈与 CSF Next 的对应变体
官方片段还提供了CSF Next(试验性,使用defineMain辅助函数)的等价写法,分别覆盖 React、Vue 3、Angular 与 Web Components。各变体与上面 CSF 3 版本的差异仅在于入口导入方式与framework取值,addons与build.test部分完全一致:
| 渲染器 | defineMain的导入来源 | framework示例 | 片段中的源码位置(官方 Tab) |
|---|---|---|---|
| React(React/Vite、Next.js 等) | @storybook/your-framework/node(需替换为实际框架,如 react-vite、nextjs、nextjs-vite) | react-vite/nextjs等 | 见原片段 CSF Next(react)Tab |
| Vue 3(Vite) | @storybook/vue3-vite/node | vue3-vite | 原片段 CSF Next(vue)Tab |
| Angular | @storybook/angular/node | angular | 原片段 CSF Next(angular)Tab |
| Web Components(Vite) | @storybook/web-components-vite/node | web-components-vite | 原片段 CSF Next(web-components)Tab |
例如Vue 3 + Vite的完整写法是:
import { defineMain } from '@storybook/vue3-vite/node'; export default defineMain({ framework: '@storybook/vue3-vite', stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: ['@storybook/addon-docs', '@storybook/addon-vitest', '@storybook/addon-coverage'], build: { test: { disabledAddons: ['@storybook/addon-docs'], }, }, });而React + CSF Next的片段当前以占位符形式给出(源码注释写明需替换为实际框架),导入语句为import { defineMain } from '@storybook/your-framework/node'。对照本仓库源码可确认,defineMain确实是各框架node入口统一导出的类型安全配置辅助函数——例如 Angular 框架导出位于 code/frameworks/angular/src/node/index.ts,React/Vite 框架导出位于 code/frameworks/react-vite/src/node/index.ts,Vue 3 的对应入口则在code/frameworks/vue3-vite下。
逐项拆解:这份配置到底做了什么
理解这份配置的关键,是看懂“同一个插件既出现在addons里、又出现在disabledAddons里”的用意:
addons数组中@storybook/addon-docs、@storybook/addon-vitest、@storybook/addon-coverage三者同时注册,保证普通构建(文档站 / 开发模式)下三者都可用;- 当以
--test生成优化构建时,build.test段生效。disabledAddons: ['@storybook/addon-docs']显式把 Docs 从该构建中排除——Docs 属于“页面级”内容,对跑覆盖率无益且最耗性能,禁用它可以保住--test构建的大部分提速收益; - 由于禁用名单里没有coverage 与 vitest,这两个测试相关插件得以保留在构建中,coverage 插件因此能继续对源码做 Istanbul 插桩,测试结束后即可统计故事覆盖了哪些代码路径。
framework字段需要替换为项目实际使用的 Storybook 框架包(例如react-vite、nextjs、vue3-vite、angular等),stories的 glob 描述的是组件示例与 MDX 文档文件的扫描范围。
配套的执行命令
覆盖率通常要结合测试命令一起使用。Test runner 场景下,官方在 docs/writing-tests/integrations/test-runner.mdx 给出的命令行参数包括:
test-storybook --coverage:让 Test runner 在跑完故事后统计覆盖率;test-storybook --coverage --coverageDirectory coverage/ui/storybook:额外把覆盖率报告输出到指定目录,便于 CI 归档。
如果走的是 Vitest 全家桶路线(@storybook/addon-vitest),则可以参考 docs/writing-tests/in-ci.mdx:为vitest命令追加--coverage标志,或在 CI 配置里按需开启,例如vitest --project=storybook --coverage。本仓库中 Vitest 插件的覆盖率报告实现可见 code/addons/vitest/src/node/coverage-reporter.ts。
build.test与disabledAddons:官方配置项全解析
build.test.disabledAddons只是build.test这一组测试专用构建优化开关中的一员。仓库文档 docs/api/main-config/main-config-build.mdx 将其完整类型定义为:
{ disableBlocks?: boolean; // 从构建中移除 @storybook/addon-docs/blocks(Docs Blocks 自动文档) disabledAddons?: string[]; // 在构建产物中被禁用的插件名单 disableMDXEntries?: boolean; // 移除用户手写的 MDX 文档入口 disableAutoDocs?: boolean; // 禁止 autodocs 自动文档进入构建 disableDocgen?: boolean; // 关闭 argType/组件属性的静态分析推断 disableSourcemaps?: boolean; // 覆盖默认的 sourcemap 生成行为 disableTreeShaking?: boolean; // 关闭 tree shaking }官方文档特别强调:这些选项在storybook build传入--test时会被自动启用,正常情况下不建议改动,仅在“需要为某个项目禁用特定特性”或“排查构建问题”时才应覆写。
针对本文主题,核心是disabledAddons——官方语义为“设置一批会在构建产物中被禁用的插件”。具体的配套示例可见 docs/_snippets/main-config-test-disable-disableaddons.md。关于--test自动移除性能敏感插件(如 Docs、coverage)的行为描述,可在 Test runner 文档的 docs/writing-tests/integrations/test-runner.mdx#L465-L469 找到原文依据。
源码级原理:disabledAddons的过滤是怎么实现的
disabledAddons并非一个“在构建产物中跳过 bundle”的简单开关,它在Storybook 预设(preset)加载阶段就会把对应插件从配置流中剔除。实现位于 code/core/src/common/presets.ts#L218-L228:
- 过滤逻辑只在名单非空、且当前 preset 不是“关键(critical)”preset 时生效;
- 匹配采用**子串包含(
name.includes(n))**规则:插件/预设解析后的名字只要包含disabledAddons里的任意字符串即被过滤。例如禁用@storybook/addon-docs时,其派生的 blocks 等子预设也会一并被排除; - 过滤同时作用于
presets与addons两路输入,然后再递归加载剩余部分。
类型层面,该字段定义在 code/core/src/types/modules/core-common.ts#L396(TestBuildFlags.disabledAddons?: string[]),与build配置结构的顶层类型TestBuildConfig呼应。
仓库还为这一行为提供了完整的单元测试佐证:见 code/core/src/common/presets.test.ts#L722-L754 中 “should filter out disabledAddons” 用例——它构造了一个包含@storybook/addon-docs与addon-bar的addons列表,并传入build.test.disabledAddons: ['@storybook/addon-docs'],断言最终加载结果中addon-docs已被过滤掉,而addon-bar等其他插件保留。这份测试与文档片段(Docs 进disabledAddons、vitest/coverage 保留)给出的行为完全一致,可以作为你调整自定义禁用名单时的“行为参照”。
在优化构建上跑覆盖率:可行方案与限制
综合官方文档(docs/writing-tests/index.mdx)与 Test runner 指南,使用覆盖率时有几点需要明确:
- 默认关闭:覆盖率分析会拖慢测试运行,Storybook 默认关闭;在 Test runner / Vitest 测试中需通过
--coverage显式开启。 - 报告形态:Vite 与 Vitest 插件方案下,覆盖率摘要会显示在测试组件面板中,点击可打开完整的交互式报告;CI 中则更关注“全项目综合覆盖率”(把普通单测与故事测试一起统计)而非仅故事的覆盖率。
- 插桩机制与框架差异:
@storybook/addon-coverage在 Webpack 下通过istanbul-lib-instrument插桩,在 Vite 下通过vite-plugin-istanbul插桩,基本做到零配置开箱即用;其附加配置(Istanbul 的include/exclude/extension/cwd/coverageVariable/cypress等选项)见 docs/_snippets/storybook-coverage-addon-config-options.md 与 Test runner 文档中的参数表。 - 特殊框架注意:
- Vue 3、Svelte 等含专属单文件语法的框架,需要把
.vue/.svelte等扩展名加入 nyc/Istanbul 配置(Test runner 文档的 docs/writing-tests/integrations/test-runner.mdx#L459-L463 提供了 Vue 示例); - 不依赖 Webpack 加载器与 Vite 插件的框架(例如以 Webpack 配置的 Angular)插桩链路不完整,需要额外的接入配置(官方在 Test runner 文档中建议参考社区配方仓库)。
- Vue 3、Svelte 等含专属单文件语法的框架,需要把
回到本文主题,最需要记住的结论是:--test优化构建默认会禁用包括 coverage 在内的性能敏感插件;通过在build.test.disabledAddons中只保留“确实该在测试构建中消失的插件”(如 Docs),并让 coverage/vitest 留在addons中,即可在优化构建上重新获得代码覆盖率能力——这是官方文档当前推荐的唯一标准做法,代价是构建时间会有所回升。若你的项目在覆盖率与构建性能之间需要更细的取舍,可以依据上文build.test全量选项表逐项开关(如disableMDXEntries、disableAutoDocs等),并在跑通后参照仓库的presets.test.ts行为用本地构建验证实际生效的插件集合。
延伸阅读
- 关联配置片段原文:docs/_snippets/storybook-coverage-addon-optimized-config.md
- 问题场景与官方说明:docs/writing-tests/integrations/test-runner.mdx#L459-L473
build.test全量选项:docs/api/main-config/main-config-build.mdxdisabledAddons过滤实现与单元测试:code/core/src/common/presets.ts#L218-L237、code/core/src/common/presets.test.ts#L722-L754、类型定义 code/core/src/types/modules/core-common.ts#L396- Vitest 插件覆盖率报告实现:code/addons/vitest/src/node/coverage-reporter.ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考