Storybook `tags` 配置实战:让 CSF Next 的 `.test` 用例默认不占用侧边栏
本文讲解 Storybook 官方 tags 配置中的一个典型场景:当你在项目中使用 CSF Factories 的实验性 .test 方法编写随附测试时,这些测试默认会以条目形式出现在 Storybook 侧边栏,干扰组件开发时的浏览体验。通过在主配置文件(.storybook/main.js 或 .storybook/main.ts)中为内建 _test 标签声明 defaultFilterSelection: 'exclude',即可让测试用例默认从侧边栏隐藏,同时又完全保留其“可运行、可筛选、可临时调出”的能力。读完本文你将掌握 tags 配置项的字段语义、CSF 3 与 CSF Next 两种写法、以及它在 Storybook 管理器源码中的执行链路。
背景:这个配置要解决的问题
本配置片段出自 Storybook 官方文档「Tags」指南的 Recipes 章节,小节名为 Test cases that don't clutter the sidebar(docs/writing-stories/tags.mdx)。其描述的场景是:
- 你使用了 CSF Factories 的实验性
.test方法 为某个 story 挂载随附测试; - 这类测试会被自动打上系统标签(内部名为
_test,在侧边栏 URL 中的简写为$test); - 默认情况下它们会与普通 story 一同出现在侧边栏中,使左侧导航被大量“测试条目”占据;
- 通过在主配置中调整
_test标签的默认筛选行为,你可以让它们默认不可见,只在需要时通过侧边栏筛选器临时调出。
这种“同一批故事素材、多种使用视角”正是 Storybook tags 体系的核心价值:标签不仅用于给故事分类,还能控制哪些条目默认出现在侧边栏、哪些默认被排除,从而支撑一个 sidebar/dev 模式与测试模式并行的工作流。
⚠️ 需要特别说明:该配置对所有标签(含自定义标签)都适用,但其中内置的
_test标签本身仍处于实验阶段。
前置知识:tags 体系与 _test 标签
在深入配置之前,先厘清 Storybook 的标签机制。官方内置标签如下(详见 tags.mdx 中的 Built-in tags 表格):
| 标签 | 默认是否自动应用 | 作用 |
|---|---|---|
dev |
是 | 带 dev 标签的 story 会渲染在 Storybook 侧边栏 |
manifest |
是 | 纳入 component / docs 清单输出 |
test |
是 | 纳入测试运行(test runner、Vitest addon)范围 |
autodocs |
否 | 控制是否生成 docs 页面 |
play-fn |
否 | 自动应用到定义了 play function 的 story |
test-fn |
否 | 自动应用到用实验性 .test 方法定义的测试 |
其中,由 .test 方法产生的测试条目在侧边栏过滤语境中对应内部系统标签 _test(从 manager-api 侧边栏模块 的 BUILT_IN_URL_TAG_MAP 可以看到 $test: '_test' 这一映射关系)。本文涉及的配置正是围绕这个 _test 标签展开。
区分两个易混淆的用法:在主配置 .storybook/main.* 的 tags 字段里,你声明的是“标签的默认行为”(即本段代码做的事情);而真正给 story、组件或项目打标签,则是在 CSF 文件的 meta/story 或 .storybook/preview.* 中通过 tags: [...] 数组完成。
前提:启用实验性 .test 语法
_test 标签所对应的测试条目的来源,是 CSF Factories(CSF Next)中新增的 <Story>.test(...) 方法。这是一个实验性 API,需通过 experimentalTestSyntax feature flag 开启:
// .storybook/main.ts(片段,仅示意 feature 开关)
export default {
features: {
experimentalTestSyntax: true,
},
};
该 flag 的完整说明见 main-config-features.mdx。启用后,你可以在 CSF Factory 的 story 上这样挂测试(示例见 csf-next.mdx):
export const PrimaryDisabled = Primary.extend({ args: { disabled: true } });
// .test 方法:为 story 挂载随附测试
PrimaryDisabled.test('should be disabled', async ({ canvas, userEvent, args }) => {
const button = await canvas.findByRole('button');
await userEvent.click(button);
await expect(button).toHaveAttribute('aria-disabled', 'true');
await expect(args.onClick).not.toHaveBeenCalled();
});
启用并编写了这类测试之后,即可通过下一节的 tags 配置把它们默认请出侧边栏。
核心配置:为 _test 声明默认排除
本配置片段(docs/_snippets/main-config-tags-test-fn-exclude.md)的本质,是在主配置的 tags 对象里,给 _test 设置 defaultFilterSelection: 'exclude'。下面是两种配置形态下的完整写法。
CSF 3:.storybook/main.js
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)'],
tags: {
// 👇 Adjust the default configuration of this tag
_test: {
defaultFilterSelection: 'exclude',
},
},
};
CSF 3:.storybook/main.ts(类型化写法)
// 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)'],
tags: {
// 👇 Adjust the default configuration of this tag
_test: {
defaultFilterSelection: 'exclude',
},
},
};
export default config;
占位符
@storybook/your-framework需替换为你实际使用的框架包,例如@storybook/react-vite、@storybook/nextjs、@storybook/vue3-vite、@storybook/angular等。
CSF Next:.storybook/main.ts(defineMain 写法)
如果你的项目已经切换到 CSF Next(CSF Factories),主配置文件需要使用 defineMain 并从 @storybook/<framework>/node 导入。以 React 为例:
// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite)
import { defineMain } from '@storybook/your-framework/node';
export default defineMain({
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
tags: {
// 👇 Adjust the default configuration of this tag
_test: {
defaultFilterSelection: 'exclude',
},
},
});
其他框架的 CSF Next 写法与此完全同构,唯一的差别是 defineMain 的导入来源与 framework 字段:
| 框架 | 导入 | framework 取值 |
|---|---|---|
| Vue 3 | import { defineMain } from '@storybook/vue3-vite/node' |
@storybook/vue3-vite |
| Angular | import { defineMain } from '@storybook/angular/node' |
@storybook/angular |
| Web Components | import { defineMain } from '@storybook/web-components-vite/node' |
@storybook/web-components-vite |
片段中的 Vue、Angular、Web Components 用例代码如下(与 React 版除导入来源外一致):
import { defineMain } from '@storybook/vue3-vite/node';
export default defineMain({
framework: '@storybook/vue3-vite',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
tags: {
_test: {
defaultFilterSelection: 'exclude',
},
},
});
import { defineMain } from '@storybook/angular/node';
export default defineMain({
framework: '@storybook/angular',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
tags: {
_test: {
defaultFilterSelection: 'exclude',
},
},
});
import { defineMain } from '@storybook/web-components-vite/node';
export default defineMain({
framework: '@storybook/web-components-vite',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
tags: {
_test: {
defaultFilterSelection: 'exclude',
},
},
});
在 CSF Next 下同样支持 .storybook/main.js 的 defineMain 写法(import { defineMain } from '@storybook/your-framework/node'),JavaScript 版本只是省略类型标注,配置结构完全一致。
defaultFilterSelection 的三种取值语义
tags 配置项的类型为 { [tagName: string]: { defaultFilterSelection?: 'include' | 'exclude' } }(见 main-config-tags.mdx)。defaultFilterSelection 决定某个标签在侧边栏筛选菜单中的初始选中状态:
'exclude':带该标签的条目在侧边栏默认被排除(筛选菜单中该标签默认处于“排除”状态)。需要查看时,可在侧边栏筛选菜单中显式“包含”该标签,把它们临时调回;'include':带该标签的条目默认被选中包含在侧边栏中;- 未设置(
undefined):标签没有默认筛选状态,Storybook 不做任何额外处理。
与纯配置层面的字段说明相比,从源码可以看得更具体:TagOptions 类型定义中除了 defaultFilterSelection,还存在 excludeFromSidebar 与 excludeFromDocsStories 两个布尔字段——后者用于彻底阻止带该标签的条目进入侧边栏渲染,而 defaultFilterSelection: 'exclude' 只是设定一个“默认过滤条件”,条目本身仍然可以被用户手动包含回来。这也是本配置方案区别于“硬隐藏”的关键:它保证测试随时可以按需显示,而不是被永久抹掉。
将 exclude 配置到 _test 标签后的实际效果是:打开 Storybook 时侧边栏默认不再展示那些 .test 用例条目,组件列表保持干净;而运行测试(如 Vitest addon 或 test runner)时这些用例照常执行,不受侧边栏可见性影响。
源码视角:这段配置是如何生效的
在 Storybook 管理器(manager)侧,标签的默认筛选逻辑集中在 code/core/src/manager-api/modules/tags.ts。与本节配置直接相关的调用链可以梳理为:
-
解析预设默认筛选:
getDefaultTagsFromPreset(presets)遍历主配置传入的tags(TagsOptions),凡是option.defaultFilterSelection === 'include'的标签进入默认included集合,=== 'exclude'的进入默认excluded集合。也就是说,本节在_test上写的exclude,最终会变成侧边栏初始化时一组“默认排除”的标签集合。 -
生成实际过滤函数:
computeTagsFilterFn(includedTagFilters, excludedTagFilters)把标签集合编译成对索引条目(API_PreparedIndexEntry)的过滤判定。值得注意的是,它会区分内建过滤器(BUILT_IN_FILTERS)与用户标签过滤器(USER_TAG_FILTER),并按分组进行“包含组任一命中、排除组全部命中”的组合判断,从而支持侧边栏里 include/exclude 混用的交互(详见 tags.mdx 中 “Filtering the sidebar by tags” 一节)。 -
URL 状态同步:
parseTagsParam与serializeTagsParam负责把$test(_test)这类标签简写在 URL 参数中序列化/反序列化,使得用户当前的筛选状态可被收藏、可被分享。 -
UI 层交互:侧边栏筛选菜单的组件行为可以在 Filter.stories.tsx 中看到对应的组件级 Story 用例。
配置字段 defaultFilterSelection 的类型声明位置为 core-common.ts,而 Tag/TagsOptions 的字段含义与“未设置即无默认筛选”的行为说明可对照 main-config-tags.mdx。
与自定义标签配置的同构性
需要指出的是,本节做法并不局限于 _test。同一份 tags 配置对象可以用于任意自定义标签,例如定义一个默认排除的 experimental 标签(官方示例见 docs/_snippets/main-config-tags.md):
export default {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
tags: {
// 👇 Define a custom tag named "experimental"
experimental: {
defaultFilterSelection: 'exclude', // Or 'include'
},
},
};
这类配置同样有 CSF 3(export default / 类型化 StorybookConfig)与 CSF Next(defineMain)两套写法,模式与上节完全一致。因此,掌握本文的 _test 配置,就等于掌握了在 Storybook 中“让任何一类标签默认不占侧边栏”的通用方法。
排除后如何按需查看与配合测试
配置 defaultFilterSelection: 'exclude' 之后,当你想临时审查某个 .test 用例时:
- 打开侧边栏顶部的标签筛选菜单,在筛选器中选择
_test(界面中常以$test或对应语义名称呈现)并将它切换为“包含”,相应条目即会重新出现; - 筛选器支持多标签组合:同时选择多个标签会显示“包含任意一个标签”的条目;多选“排除”则排除命中任一标签的条目;也可以 include/exclude 混用;
- 未做任何选择时,侧边栏展示全部条目;
- 标签筛选会先于搜索生效——当你在侧边栏输入关键词搜索时,结果已经被当前筛选限定,这在大项目中能够显著缩小匹配范围。
下方截图展示了侧边栏标签筛选器的交互形态(筛选菜单中同时存在“包含”与“排除”两类标签时,侧边栏只显示“被包含且未被排除”的条目):
需要留意的是,这套配置只影响侧边栏的展示/默认筛选,不影响 .test 用例是否参与测试运行。_test 条目的执行由测试链路(Vitest addon、test runner 等)依据标签与配置驱动;本方案真正的收益是让“写测试用的故事集”与“开发浏览用的故事集”在侧边栏各得其所,互不干扰。
注意事项与适用边界
- 实验性 API:
_test标签与.test方法、CSF Next 相关能力均处于实验阶段,接口细节可能随版本演进变化,建议关注官方 CHANGELOG 与本仓库的文档更新; - 格式前提:主配置中的
tags字段是对标签“默认行为的声明”,要正确展示.test条目还需保证experimentalTestSyntaxfeature 已开启,并在.test方法被调用的 CSF Factory 文件中使用了该语法; - 标签应用层面:若你想改变某个 story、组件或整个项目的具体标签集合(例如去掉
dev实现“仅文档”故事),应到.storybook/preview.*、meta 或 story 的tags数组里操作,与本文所讲的主配置声明并不冲突; - 脚本替换注意:将本文示例直接复制进项目前,务必把
@storybook/your-framework替换为真实的框架包名,并确认storiesglob 与你项目实际的索引位置一致。
延伸阅读
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
