资讯动态

用 CSF Next 工厂函数故事的 `.run()` 在 Vitest 中复用 Storybook 组件测试

发布时间:2026/9/9 12:50:57 来源:尧图企业网站定制
用 CSF Next 工厂函数故事的.run()在 Vitest 中复用 Storybook 组件测试【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook本篇技术指南聚焦 Storybook 仓库中 docs/_snippets/portable-stories-csf-factory-run.md 所演示的实践当项目采用新一代CSF NextCSF 工厂函数语法编写 story 后如何在 Vitest Testing Library 测试文件中直接调用故事对象上的.run()一次性完成挂载组件 执行 Storybook 全生命周期钩子的渲染与断言。读完本文你将理解.run()与.Component两种复用方式的取舍、composed/input属性与参数覆盖规则以及 CSF Next 下 portable stories 测试脚本与旧版composeStories流程的差异。1. 先看清这段片段所在的位置CSF Next 的 portable stories 测试portable-stories-csf-factory-run.md本质是 docs/api/csf/csf-next.mdx 文档中在测试文件里复用 story环节使用的真实代码片段。在该文档第 5 步中官方给出了新老写法的对比import { test, expect } from vitest; import { screen } from testing-library/react; - import { composeStories } from storybook/your-framework; // Import all stories from the stories file import * as stories from ./Button.stories; const { Primary } stories; - const { Primary } composeStories(stories); test(renders primary button with default args, async () { // The run function will mount the component and run all of Storybooks lifecycle hooks await Primary.run(); const buttonElement screen.getByText(Text coming from args in stories file!); expect(buttonElement).not.toBeNull(); });这意味着在CSF Next 下story 模块导出本身就是开箱即用的可组合对象不再需要composeStories二次包装。这是理解后面所有.run()细节的前提。使用前需要明确三个适用范围限制CSF Next 目前是preview预览特性API 在未来版本可能变化仅官方支持 React、Vue、Angular 与 Web Components 四个渲染器项目见 docs/api/csf/csf-next.mdxCSF Next 采用工厂函数链defineMain → definePreview → preview.meta → meta.story每一环都带类型推导story 的类型含 args自动从组件与 meta 推断不再需要手动给 meta/story 标注 props 类型。2. 核心示例在 Vitest 测试中调用.run()渲染并断言portable-stories-csf-factory-run.md提供的完整可运行示例React Vitest Testing Library如下import { test, expect } from vitest; import { screen } from testing-library/react; // Import all stories from the stories file import * as stories from ./Button.stories; const { Primary, Secondary } stories; test(renders primary button with default args, async () { // The run function will mount the component and run all of Storybooks lifecycle hooks await Primary.run(); const buttonElement screen.getByText(Text coming from args in stories file!); expect(buttonElement).not.toBeNull(); }); test(renders primary button with overridden props, async () { // You can override props by passing them in the context argument of the run function await Primary.run({ args: { ...Primary.composed.args, children: Hello world } }); const buttonElement screen.getByText(/Hello world/i); expect(buttonElement).not.toBeNull(); });拆解这段代码它覆盖了三个核心点直接解构故事const { Primary, Secondary } stories把 stories 文件中的具名 story 导出当作已组合好的测试单元Secondary虽未使用但揭示整个模块中所有 story 都具备同样能力。默认参数渲染await Primary.run()等价于让 Storybook 用该 story 的默认 args 完整渲染一次组件随后用screen.getByText(...)在真实 DOM 中做断言。覆盖参数渲染await Primary.run({ args: { ...Primary.composed.args, children: Hello world } })通过 run 函数的 context 参数覆盖 args。为什么覆盖 args 时要先展开Primary.composed.args在 code/core/src/preview-api/modules/store/csf/portable-stories.ts 中run的实现如下const run (extraContext?: PartialStoryContextTRenderer, PartialTArgs) { const context initializeContext(); Object.assign(context, extraContext); return runStory(story, context); };extraContext会通过Object.assign整体覆盖到 story context 上如果你只写{ args: { children: Hello world } }那么 context.args 会被这个仅含children的对象整体替换story 在 meta / story 里定义的其余默认 args 都会丢失。因此示例特意用{ ...Primary.composed.args, children: Hello world }先展开已合并的默认 args、再覆盖单个字段——这是一个保证在默认参数基础上做局部覆盖的关键细节。3..run()底层做了什么从类型定义到渲染管线.run()并非测试库提供的能力而是composed story 对象接口的一部分。仓库类型文件 code/core/src/types/modules/composedStory.ts 定义了ComposedStoryFnexport type ComposedStoryFn TRenderer extends Renderer Renderer, TArgs Args, PartialArgsStoryFnTRenderer, TArgs { args: TArgs; id: StoryId; play?: (context?: PartialStoryContextTRenderer, PartialTArgs) Promisevoid; run: (context?: PartialStoryContextTRenderer, PartialTArgs) Promisevoid; load: () Promisevoid; storyName: string; parameters: Parameters; argTypes: StrictArgTypesTArgs; reporting: ReporterAPI; tags: Tag[]; globals: Globals; };可见 story 对象身上同时挂载了run、play、load、args、parameters、globals、reporting等完整测试面。其中run的职责注释与实现对应关系为run走完整渲染管线runStory挂载组件并执行 Storybook 的加载、渲染、play、清理等完整生命周期portable-stories.ts中与片段注释一致play仅执行故事中定义的 play 函数体不负责完整挂载流程load仅加载 loaders 产出的异步数据若上下文尚未加载则先加载再执行。从执行流程看run内部通过initializeContext()构造携带new HooksContext()的StoryContext再把canvasElement缺省指向document.body因此它天然与 Testing Library 的screen查询配合run把组件渲染进真实 DOM测试随后用screen.getByText/getByRole等断言交互与结构。与.Component渲染方式的关系同一个 story 对象还暴露.Component属性便于你脱离.run()用自己的渲染方式例如直接render(Primary.Component /)——这正是同目录配套片段 portable-stories-csf-factory-render.md 展示的用法const { Primary, Secondary } stories; test(renders primary button with default args, async () { // Access the storys component via the .Component property render(Primary.Component /); const buttonElement screen.getByText(Text coming from args in stories file!); expect(buttonElement).not.toBeNull(); }); test(renders primary button with overridden props, async () { // You can override props by passing them directly to the storys component render(Primary.ComponentHello world/Primary.Component); const buttonElement screen.getByText(/Hello world/i); expect(buttonElement).not.toBeNull(); });两者的取舍很清晰复用方式触发的能力适用场景await Primary.run({ ... })完整挂载 Storybook 生命周期钩子loaders、decorators、play、清理等希望最大程度复刻 Storybook 内渲染行为、含 play 函数与装饰器的测试render(Primary.Component .../)组件与默认 args 由 story 提供但由你自己掌控渲染想用 Testing Library 的render灵活组合或对自定义渲染有强需求官方建议story 的args、parameters等属性统一通过.composed访问见 docs/api/csf/csf-next.mdx不要直接访问Story.args这类旧式字段在 CSF Next 中已弃用。4.composed与input合并值 vs 原始输入为什么片段中要写Primary.composed.args而不是Primary.args因为 CSF Next 为合并语义引入了专门命名composed由story、component meta、preview项目级三层配置合并后的结果是渲染与测试真正使用的最终值。属性名正是取自由各层 compose 而来的含义参见 docs/api/csf/csf-next.mdx。input你在 story / meta 定义里直接写入的原始输入未经合并。这一设计的证据也体现在源码侧工具函数getCsfFactoryAnnotationscode/core/src/preview-api/modules/store/csf/csf-factory-utils.ts在把工厂 story 转回普通注解时会分别取return isStory(story) ? { story: story.input, meta: story.meta.input, preview: story.meta.preview.composed, } : { story, meta: isMeta(meta) ? meta.input : meta, preview: projectAnnotations };即story/meta 保留原始输入而preview项目级注解直接取composed合并结果。这正好解释了为什么 CSF Next 下即使不用setProjectAnnotationsstory 仍随身携带项目级配置——story 对象通过story.meta.preview.composed把 preview 层装饰器、参数与钩子绑定在自己身上。这也意味着.run()在测试里可以开箱即用地享受到.storybook/preview中的全局装饰器与参数例如主题装饰器设置data-theme而不必在测试侧再手动应用一遍。5. 配套的 Vitest setup 文件该怎样写要让.run()在 Vitest 中完整可用一般还需要在 setup 文件中启动 Storybook 全局的beforeAll/afterEach生命周期。CSF Next 下的推荐写法与旧版存在明显差异见 docs/api/csf/csf-next.mdximport { beforeAll } from vitest; // No longer necessary - // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import { setProjectAnnotations } from storybook/your-framework; - import * as addonAnnotations from my-addon/preview; import preview from ./.storybook/preview; - import * as previewAnnotations from ./.storybook/preview; // No longer necessary - const annotations setProjectAnnotations([previewAnnotations, addonAnnotations]); // Run Storybooks beforeAll hook beforeAll(preview.composed.beforeAll); - beforeAll(annotations.beforeAll);两个要点值得注意addons 声明位置前移在 CSF Next 中addon 通过definePreview({ addons: [addonA11y()] })声明见 docs/api/csf/csf-next.mdx其注解随preview.composed一起可用因此 setup 里不再需要单独import * as addonAnnotations。安装 addon 时官方也建议直接使用npx storybook add addon-name或运行storybook dev让配置自动更新。新旧混合需双 setup 文件官方明确提示——只有全部故事都采用 CSF Next 时才适用上述简化若测试里同时混用 CSF 1/2/3 与 CSF Next则必须维护两套独立的 setup 文件portable-stories.ts的setProjectAnnotations仍会被旧格式组合路径使用。如果无法使用 Storybook Testvitest addon而要在普通测试文件中复用 storyCSF Next 的这个免composeStories特性把整条链路从组合注解 → 渲染进一步收敛成了直接run。这套完整方法论的更多背景可继续阅读Portable stories 单元测试整体说明docs/writing-tests/integrations/stories-in-unit-tests.mdxCSF Next 完整 API 与迁移指南docs/api/csf/csf-next.mdx.Component渲染配套片段docs/_snippets/portable-stories-csf-factory-render.md组合逻辑与run实现code/core/src/preview-api/modules/store/csf/portable-stories.tscomposed story 类型接口code/core/src/types/modules/composedStory.ts6. 常见注意点小结务必await.run()返回 Promise内部执行含异步生命周期loaders、play、清理不 await 会出现断言先于渲染完成导致的偶发失败。args 覆盖要带展开run({ args })的 args 是整包替换语义局部覆盖请始终{ ...Story.composed.args, ...你想要的字段 }。读默认值看composed只有Story.composed.args/Story.composed.parameters才是合并 story meta preview 三层后的最终渲染值。预览特性注意升级CSF Next 与.run()目前在仓库文档中标记为 previewAPI 仍可能演进升级请参考 docs/api/csf/csf-next.mdx 中的迁移章节。渲染器限制.run()目前仅在 React、Vue、Angular、Web Components 渲染器下获得官方支持与文档背书。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价