Storybook 可移植故事在 Jest 中按测试覆盖 Globals:用 composeStory 实现多语言等场景断言

发布时间:2026/9/18 9:24:36
Storybook 可移植故事在 Jest 中按测试覆盖 Globals:用 composeStory 实现多语言等场景断言
Storybook 可移植故事在 Jest 中按测试覆盖 Globals用 composeStory 实现多语言等场景断言可移植故事Portable Stories允许你把 Storybook 中精心编排好的故事直接复用到 Jest 等外部测试环境。但当故事的行为依赖globals如locale语言环境时如何在每一个测试里分别注入不同的全局值、完成“同一故事、多种环境”的断言是本文要解决的核心问题。读完本文你将掌握在 React 与 Vue 项目中通过composeStory第三个参数按测试覆盖 globals 的完整写法并理解其背后的故事管线story pipeline与源码依据。背景Globals 与 Args 的区别以及为什么需要覆盖在 Storybook 中globals是作用于整个 Storybook 项目的“工具条级”全局状态例如界面语言locale、主题theme、数据方向direction等。它与单个故事的args不同args描述组件接收的属性属于某一个故事实例globals描述的是渲染环境本身会被preview、装饰器、loaders 以及故事 render 逻辑共同消费。一个典型场景是国际化组件Button在不同locale下渲染 “Hello” / “Hola”而故事本身并不感知语言语言由全局的locale决定。在 Storybook 里开发者通过工具栏切换locale预览把故事移植到 Jest 之后你就需要在每个测试里通过覆盖 globals 来模拟这种切换。前置准备在 Jest 的 setup 文件中注册项目注解要让composeStory/composeStories能正确处理preview中定义的装饰器、loaders、beforeAll以及globalTypes需要先在 Jest 的 setup 文件对应 jest 配置的setupFiles中调用一次setProjectAnnotations例如 portable-stories-jest-set-project-annotations.md 中给出的结构import { beforeAll } from jest/globals; // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import { setProjectAnnotations } from storybook/your-framework; // Import the exported annotations, if any, from the addons youre using; otherwise remove this import * as addonAnnotations from my-addon/preview; import * as previewAnnotations from ./.storybook/preview; const annotations setProjectAnnotations([previewAnnotations, addonAnnotations]); // Supports beforeAll hook from Storybook beforeAll(annotations.beforeAll);在这之后每个被composeStory/composeStories组合出来的故事都会自动带上.storybook/preview的项目级注解——其中就包含你在preview里声明的globalTypes与initialGlobals。核心写法composeStory 的第三个参数按测试覆盖 globals当你希望同一个故事在「英语环境」与「西班牙语环境」下分别渲染并被断言时不需要复制两份故事文件只需在组合故事时通过projectAnnotations覆盖对应的 globals。这正是本仓库文档片段 portable-stories-jest-override-globals.md 演示的用法。React 示例import { test } from jest/globals; // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import { composeStory } from storybook/your-framework; import meta, { Primary as PrimaryStory } from ./Button.stories; test(renders in English, async () { const Primary composeStory( PrimaryStory, meta, { globals: { locale: en } }, // Project annotations to override the locale ); await Primary.run(); }); test(renders in Spanish, async () { const Primary composeStory(PrimaryStory, meta, { globals: { locale: es } }); await Primary.run(); });Vue 示例import { test } from jest/globals; import { render } from testing-library/vue; import { composeStory } from storybook/vue3-vite; import meta, { Primary as PrimaryStory } from ./Button.stories; test(renders in English, async () { const Primary composeStory( PrimaryStory, meta, { globals: { locale: en } }, // Project annotations to override the locale ); await Primary.run(); }); test(renders in Spanish, async () { const Primary composeStory(PrimaryStory, meta, { globals: { locale: es } }); await Primary.run(); });把上面的写法拆开看有三个关键点composeStory(story, componentAnnotations, projectAnnotations?)第三个参数是本次组合专用的项目注解。文档明确说明它被用于覆盖通过setProjectAnnotations设置的全局项目注解——也就是“仅作用于当前这个组合出来的故事”不会污染其他测试。{ globals: { locale: en } }把locale这个全局变量覆盖成en。只要你项目的preview或其globalTypes声明过locale被覆盖的值就会进入故事的渲染上下文让依赖globals的 render 逻辑、play 函数按对应语言执行。await Primary.run()run()会挂载组件并完整执行故事生命周期包括 loaders、beforeEach以及 play 函数详见下文“故事管线”。在断言型 play 函数里若断言失败测试即失败。与 setProjectAnnotations 的覆盖关系局部优先在 portable-stories-jest.mdx 的 “Overriding globals” 小节中官方推荐的语义是setProjectAnnotations在 setup 阶段设置全局项目注解作用于所有组合出来的故事composeStory以及composeStories的projectAnnotations参数提供“按需覆盖”适合每个测试内部单独调整 globals 这类全局面量。因此你的测试文件中可以出现这样的分层preview里定义了默认的locale例如en而某一个特定测试通过{ globals: { locale: es } }临时把语言切到西班牙语其余测试继续使用默认语言。这种“默认统一、单测可覆盖”的模型正好对应 Storybook 中“项目级默认 故事级覆盖”的注解层级思想。源码依据composeStory 中的 globals 是如何被计算的在仓库的核心实现 portable-stories.ts 中可以找到 globals 覆盖机制的底层逻辑。composeStory的实现位于 portable-stories.ts其签名是export function composeStoryTRenderer extends Renderer Renderer, TArgs extends Args Args( storyAnnotations: LegacyStoryAnnotationsOrFnTRenderer, componentAnnotations: ComponentAnnotationsTRenderer, TArgs, projectAnnotations?: ProjectAnnotationsTRenderer, defaultConfig?: ProjectAnnotationsTRenderer, exportsName?: string ): ComposedStoryFnTRenderer, PartialTArgs注意该实现保留了第五个参数exportsName便于在无法使用composeStories它天然知道每个 export 的名字时仍能保证测试内的故事名唯一。在函数体内第三个参数projectAnnotations会与全局默认注解合并portable-stories.tsconst normalizedProjectAnnotations normalizeProjectAnnotationsTRenderer( composeConfigs([ defaultConfig ?? globalThis.globalProjectAnnotations ?? {}, projectAnnotations ?? {}, ]) );组合后的上下文 globals 则由以下三部分叠加而成portable-stories.tsconst globalsFromGlobalTypes getValuesFromGlobalTypes(normalizedProjectAnnotations.globalTypes); const globals { ...globalsFromGlobalTypes, ...normalizedProjectAnnotations.initialGlobals, ...story.storyGlobals, };其中getValuesFromGlobalTypes会把globalTypes里各字段声明的defaultValue提取为初始 globals见 getValuesFromGlobalTypes.tsinitialGlobals来自项目注解story.storyGlobals则来自组件/故事级注解。由此可以推断一旦在composeStory的项目注解中覆盖了globals.locale该值就会成为这个组合故事上下文里的最终渲染环境。当测试调用run()时执行的是 runStory它先清理上次运行残留的卸载回调接着把故事挂载到文档中然后依次applyLoaders、applyBeforeEach、执行play函数最后applyAfterEach并处理动画等待/暂停。这也是为什么上面两个测试分别await Primary.run()就能获得完整的“渲染 交互断言”能力。不同框架只是把这份核心逻辑重新导出React 侧的实现见 renderers/react/src/portable-stories.tsxVue 侧见 renderers/vue3/src/portable-stories.ts。因此示例中 React 项目从storybook/your-framework如storybook/react-vite、storybook/nextjs导入Vue 项目从storybook/vue3-vite导入。补充要点与限制版本前提上述.run()风格 API 需要 Storybook8.2.7及以上版本官方文档建议用npx storybooklatest upgrade升级到最新版以使用本 API。旧版本请改用.play()其余参数与写法一致见 portable-stories-jest.mdx。Next.js 项目需要为 Jest 配置next/jest.jstransformer、从storybook/nextjs导入composeStories/composeStory并正确设置内部模块别名以支持 mock 与断言详见 portable-stories-jest.mdx 中 React 分支的提示。故事管线需要手动执行在 Storybook 内部项目注解注入、loader 数据准备、渲染与 play 由故事管线自动完成而可移植故事要求你通过setProjectAnnotations管线第 1 步、composeStories/composeStory第 2 步组合与组合故事的run()第 3 步运行自行还原这条管线。按测试覆盖 globals本质上就是在第 2 步组合时为不同测试注入了不同的项目级注解。覆盖范围是“本次组合”composeStory的第三个参数只影响它返回的那一个组合故事。若你的多个测试需要不同的 globals应在各测试内分别composeStory一次如上方英语/西班牙语两个测试所示若要让一批故事统一使用同组 globals可考虑把它们放在composeStories的公共projectAnnotations参数里。小结覆盖 globals 是让同一份故事资产在 Jest 中“一鱼多吃”的关键技巧以composeStory(PrimaryStory, meta, { globals: { locale: en } })的方式按测试注入语言等全局面量再通过run()驱动完整故事管线即可用最少的重复代码覆盖多语言、多主题等渲染分支。这一写法的权威定义位于 portable-stories-jest-override-globals.md其底层语义与 globals 合成逻辑可在 portable-stories.ts 中进一步查阅。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考