news 2026/9/9 12:50:47

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 CSF Next 工厂函数故事的 `.run()` 在 Vitest 中复用 Storybook 组件测试

用 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 Next(CSF 工厂函数)语法编写 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 Storybook's 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.mdx);
  • CSF 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 Storybook's 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(); });

拆解这段代码,它覆盖了三个核心点:

  1. 直接解构故事const { Primary, Secondary } = stories把 stories 文件中的具名 story 导出当作已组合好的测试单元;Secondary虽未使用,但揭示整个模块中所有 story 都具备同样能力。
  2. 默认参数渲染await Primary.run()等价于让 Storybook 用该 story 的默认 args 完整渲染一次组件,随后用screen.getByText(...)在真实 DOM 中做断言。
  3. 覆盖参数渲染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?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => { 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 定义了ComposedStoryFn

export type ComposedStoryFn< TRenderer extends Renderer = Renderer, TArgs = Args, > = PartialArgsStoryFn<TRenderer, TArgs> & { args: TArgs; id: StoryId; play?: (context?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => Promise<void>; run: (context?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => Promise<void>; load: () => Promise<void>; storyName: string; parameters: Parameters; argTypes: StrictArgTypes<TArgs>; reporting: ReporterAPI; tags: Tag[]; globals: Globals; };

可见 story 对象身上同时挂载了runplayloadargsparametersglobalsreporting等完整测试面。其中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 story's 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 story's component render(<Primary.Component>Hello 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 的argsparameters等属性统一通过.composed访问(见 docs/api/csf/csf-next.mdx),不要直接访问Story.args这类旧式字段(在 CSF Next 中已弃用)。

4.composedinput:合并值 vs 原始输入

为什么片段中要写Primary.composed.args而不是Primary.args?因为 CSF Next 为"合并语义"引入了专门命名:

  • composed:由story、component meta、preview(项目级)三层配置合并后的结果,是渲染与测试真正使用的最终值。属性名正是取自"由各层 compose 而来"的含义(参见 docs/api/csf/csf-next.mdx)。
  • input:你在 story / meta 定义里直接写入的原始输入,未经合并。

这一设计的证据也体现在源码侧:工具函数getCsfFactoryAnnotations(code/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 下即使不用setProjectAnnotations,story 仍"随身携带"项目级配置——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.mdx):

import { 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 Storybook's beforeAll hook + beforeAll(preview.composed.beforeAll); - beforeAll(annotations.beforeAll);

两个要点值得注意:

  1. 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让配置自动更新。
  2. 新旧混合需双 setup 文件:官方明确提示——只有全部故事都采用 CSF Next 时才适用上述简化;若测试里同时混用 CSF 1/2/3 与 CSF Next,则必须维护两套独立的 setup 文件(portable-stories.tssetProjectAnnotations仍会被旧格式组合路径使用)。

如果无法使用 Storybook Test(vitest addon)而要在普通测试文件中复用 story,CSF Next 的这个"免composeStories"特性把整条链路从"组合注解 → 渲染"进一步收敛成了"直接run"。这套完整方法论的更多背景可继续阅读:

  • Portable stories 单元测试整体说明:docs/writing-tests/integrations/stories-in-unit-tests.mdx
  • CSF 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.ts
  • composed story 类型接口:code/core/src/types/modules/composedStory.ts

6. 常见注意点小结

  • 务必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()目前在仓库文档中标记为 preview,API 仍可能演进;升级请参考 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 12:50:44

Skills:开发者能力操作系统与轻量级能力集成范式

1. “skills”不是功能模块&#xff0c;而是一套开发者能力操作系统 你点开 GitHub 搜索框&#xff0c;输入 skills &#xff0c;跳出来的不是某个知名开源库&#xff0c;而是一长串形如 dietrichgebert/ponytail 、 baoyu-skills 、 opencode-skills 的仓库名&#xf…

作者头像 李华
网站建设 2026/9/9 12:49:36

约 10 分钟跑通 Ruffle:让百万 SWF 重新运行的完整指南

约 10 分钟跑通 Ruffle&#xff1a;让百万 SWF 重新运行的完整指南 【免费下载链接】ruffle A Flash Player emulator written in Rust 项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle 一个从旧硬盘里导出的 Flash 课件包&#xff0c;双击却没有任何程序能打…

作者头像 李华
网站建设 2026/9/9 12:48:24

WinForm + WMS 仓储管理系统完整实战指南

如果你现在接到一套用 WinForm 开发的 WMS&#xff08;Warehouse Management System&#xff0c;仓储物流管理系统&#xff09;项目&#xff0c;第一反应大概率是&#xff1a;都什么年代了&#xff0c;还用 WinForm&#xff1f; 但现实情况是&#xff0c;在制造、电商仓储、医…

作者头像 李华
网站建设 2026/9/9 12:48:14

提示注入攻击深度解析:从Vincent AI漏洞看法律AI供应链安全

vLex旗下Vincent AI曝出高危提示注入漏洞&#xff0c;20万家律所的数据安全被推到悬崖边上。如果你觉得"提示注入"只是安全圈里的一个小众名词&#xff0c;那这场风波正好是一次补课的机会——它把AI供应链安全里最隐蔽、也最要命的一类风险&#xff0c;用最直观的方…

作者头像 李华
网站建设 2026/9/9 12:44:57

高效的 Windows 系统优化工具 WinUtil 新手指南

高效的 Windows 系统优化工具 WinUtil 新手指南 【免费下载链接】winutil Chris Titus Techs Windows Utility - Install Programs, Tweaks, Fixes, and Updates 项目地址: https://gitcode.com/GitHub_Trending/wi/winutil WinUtil&#xff08;Chris Titus Techs Windo…

作者头像 李华
网站建设 2026/9/9 12:44:15

Claude Code本地代理配置与Codex响应失败排查指南

我无法根据提供的输入生成符合要求的博文内容。原因如下&#xff1a;输入中仅提供了项目标题"ruflo"&#xff0c;以及大量与Claude Code、Codex、Agent、npx等相关的热搜词和网络热词&#xff0c;但未提供任何实质性的项目正文、摘要描述或关键词列表&#xff08;按要…

作者头像 李华