
eslint-plugin-storybook context-in-play-function 规则详解跨 Story 调用 play 函数时必须传递完整 Context【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook导读在 Storybook 的组件测试与交互测试中一条 Story 的play函数常常需要直接调用另一条 Story 的play函数来复用既有交互逻辑。这种做法虽然方便但稍不注意就会漏传context参数导致被调用的交互无法依赖 Storybook 注入的内部能力而静默失败。本文围绕 eslint-plugin-storybook 的storybook/context-in-play-function规则展开先讲清楚为什么必须传完整 context再给出违规与合规代码示例、规则在源码层面的判定逻辑、它在 recommended / addon-interactions 等预置配置中的启用位置以及如何在只对故事文件生效的前提下按需关闭或改写规则帮助你写出可被 ESLint 自动兜底的健壮 play 函数组合。规则背景为什么跨 Story 调用 play 必须传递完整 context在 CSFComponent Story Format中Storybook 在执行每条 Story 时都会为它的play函数注入一个context对象。这个 context 承载着 Storybook 运行 play 函数所必需的内部能力例如用于定位组件的canvasElement、画布 API、各类与渲染和交互通道挂钩的内部方法等。当你在一条 Story 的 play 函数中通过MyOtherStory.play(...)这种方式手动复用另一条 Story 的交互时Storybook 不会再自动帮你注入 context——它只会把你显式传入的参数交给目标 play 函数。此时如果只传了一个普通对象、甚至什么都不传目标 play 函数在执行within(canvasElement)这类依赖 context 内部能力的操作时就会拿不到正确数据交互测试便无法按预期工作。context-in-play-function规则的职责就是在静态分析层面识别SomeStory.play()形式的外部调用点要求调用方把完整 context 原样传给目标 play 函数。从规则源码看它的 meta 被定义为type: problem、severity: error、不可自动修复、且没有任何可配置项schema: []触发后报告Pass a context when invoking play function of another story相关实现见 code/lib/eslint-plugin/src/rules/context-in-play-function.ts。这条规则归属于 eslint-plugin-storybook 对 play 交互代码的静态检查体系与await-interactions、use-storybook-expect、use-storybook-testing-library一起构成面向交互测试的规则组。违规写法Incorrect规则文档给出了两类典型的违规代码。情况一完全不传任何参数import { within, userEvent } from storybook/testing-library MyStory.play ({ canvasElement }) { const canvas within(canvasElement) // not passing any context await MyOtherStory.play() userEvent.click(canvas.getByRole(button)) }MyOtherStory.play()调用没有携带任何实参目标 play 函数内部一旦依赖 context例如再次调用within(context.canvasElement)就会因为缺少上下文而出错。这是最直接的违规形态。情况二只传了不完整的 context 片段import { within, userEvent } from storybook/testing-library MyStory.play ({ canvasElement }) { const canvas within(canvasElement) // not passing the full context await MyOtherStory.play({ canvasElement }) userEvent.click(canvas.getByRole(button)) }这里看似传了参数但实际上只挑选了canvasElement这一个字段拼成了新对象。Storybook 为 play 注入的 context 中还有大量对交互正确运行至关重要的内部功能{ canvasElement }这个残缺对象无法满足目标 play 的完整需求。规则的单元测试 context-in-play-function.test.ts 把这两种场景都固化为 invalid 用例例如// invalid 用例 1对象模式只传部分字段 export const SecondStory { play: async ({ canvasElement }) { await FirstStory.play({ canvasElement }) } } // invalid 用例 2完全无参 export const SecondStory { play: async () { await FirstStory.play() } }CSF 2 风格的Template.bind({})写法同样会被检查测试中的 invalid 用例覆盖了SecondStory.play async ({ canvasElement }) { await FirstStory.play({ canvasElement }) }与完全无参两种形态。合规写法Correct文档给出了两种推荐形态。方式一接收完整 context 并原样转发import { within, userEvent } from storybook/testing-library MyStory.play (context) { const canvas within(context.canvasElement) // passing full context await MyOtherStory.play(context) await userEvent.click(canvas.getByRole(button)) }最稳妥的做法play 函数不急着解构而是先把完整context接住再把它作为唯一实参整体传给目标 play。这样 context 里所有内部能力都被原样保留。方式二借助 context 自引用属性转发import { within, userEvent } from storybook/testing-library MyStory.play ({ context, canvasElement }) { const canvas within(canvasElement) // passing self referencing context property await MyOtherStory.play(context) await userEvent.click(canvas.getByRole(button)) }如果你希望本层直接使用canvasElement又要把完整上下文转发给别的 Story可以解构出 context 的context自引用属性——Storybook 注入的 context 对象自身带有指向完整上下文的引用字段——然后把它传给目标 play。源码视角规则究竟如何判定上下文是否被完整传递context-in-play-function并非简单的字符串匹配而是基于typescript-eslint的 AST 分析。结合 规则实现 可以把它的判定逻辑拆解成三步。第一步识别来自另一条 Story 的 play 调用规则遍历 AST 中的CallExpression通过isPlayFunctionFromAnotherStory判断本次调用是否属于外部 Story 的 play若被调用方是成员表达式且属性名为play如MyOtherStory.play(...)、Template.play(...)即为目标同时兼容了 TS 非空断言形态即isTSNonNullExpression包裹下的成员表达式如MyStory.play!(...)也会被识别这样设计是为了避开当前文件内正在被定义的同名 play 赋值与普通函数调用只针对调用他处 play这一行为。第二步向上解析当前 play 函数拿到了哪个 context 变量getParentParameterName会沿着 AST 向上回溯找出当前所处箭头函数的第一个参数并判断它如何接收上下文参数是普通标识符如(context) ...、(ctx) ...规则记住该变量名参数是对象解构模式如({ canvasElement, context }) ...若解构列表里显式包含context字段则视为拿到了完整引用否则若存在 rest 剩余元素如({ canvasElement, ...context }) ...则以剩余元素绑定的变量为准若箭头函数没有参数或解构中既没有context字段也没有 rest 元素则判定当前函数手上根本没有可用于转发的完整 context此时任何外部 play 调用都会直接违规。第三步判断实参是否原样携带了那个 context 变量isNotPassingContextCorrectly只认可两种传参形态调用恰好只有一个参数且该参数就是步骤二中解析出的 context 变量本身await FirstStory.play(context)第一个参数是对象字面量且该字面量以展开spread方式把 context 变量整体打散进去await FirstStory.play({ canvasElement, ...context })。其余任何形态——包括无参调用、只传单个字段的{ canvasElement }、传了其他无关变量——都会被判定为没有正确传递完整 context并报告错误。需要注意的是规则把逐字段显式复制 context如play({ a: context.a, b: context.b })也排除在合规之外只有整体引用或整体展开才能保证未来新增的 context 内部字段不会被遗漏。测试用例如何印证这一判定模型从 context-in-play-function.test.ts 的 valid 集合可以看到规则明确接受的完整矩阵// 标识符整体传递普通箭头函数 / CSF2 bind 均合规 play: async (context) { await FirstStory.play(context) } play: async (ctx) { await FirstStory.play(ctx) } // rest 展开后整体 spread 回传即使剥离出个别字段也不违规 play: async ({ canvasElement, ...context }) { await FirstStory.play({ canvasElement, ...context }) } // 解构出 context 自引用字段再整体传递 play: async ({ context, canvasElement }) { await FirstStory.play(context) }这些用例分别对应上文方式一方式二以及解构时使用 rest 剩余元素保留剩余上下文的变体说明规则刻意允许你在解构中取出个别字段单独使用只要最终转发时 context 被整体保留即可。规则默认启用情况与配置方式context-in-play-function不需要你手动开启它被收纳进 ESLint 插件 Storybook 的四套预置配置中。文档头部标注以及各预置配置源码例如 code/lib/eslint-plugin/src/configs/recommended.ts均显示该规则以error级别默认开启于recommended传统.eslintrc配置flat/recommendedESLint v9 平铺配置addon-interactionsflat/addon-interactions只要你在.eslintrc中extends: [plugin:storybook/recommended]或在 flat config 中展开storybook.configs[flat/recommended]这条规则即随之上线。预置配置还限定了规则的生效文件范围——只对**/*.stories.(ts|tsx|js|jsx|mjs|cjs)与**/*.story.(ts|tsx|js|jsx|mjs|cjs)两种故事文件模式生效。When Not To Use It避免误伤测试文件规则文档特别提醒不要把这套 Storybook 规则应用到测试文件上。测试文件例如 vitest、jest 编写的*.test.ts中经常借助composeStories或直接调用.play来驱动组件其执行环境与 Storybook 的 play context 注入机制不同context-in-play-function的假设并不适用。因此请确保只在故事文件上启用 Storybook 相关规则。如果你确实在某些非故事文件上需要关闭或改写它可以通过overrides精准圈定文件范围在.eslintrc.*中按故事文件 glob 追加 overrides 并把规则置为off或在 flat config 中新增同样限定files的配置块覆盖默认值。通用的覆写示例此处以同属该插件、风格一致的规则演示写法可见 code/lib/eslint-plugin/README.md 的 Overriding/disabling rules 小节需要注意的是该插件本身不处理 MDX 文件因此基于 MDX 编写的故事不受本规则约束。实战建议把 play 组合写成全量转发风格综合规则文档与源码判定逻辑可以沉淀出几条可直接落地的工程建议优先整体接收、整体转发写play (context) { ...; await OtherStory.play(context) }这是语义最清晰、对未来 context 结构变化最健壮的写法需要取用局部字段时使用 rest 保留上下文({ canvasElement, ...ctx }) OtherStory.play({ canvasElement, ...ctx })既能在本层方便地使用canvasElement又能保证其余上下文完整转发保持await习惯调用其他 Story 的 play 属于异步交互应与本规则的姊妹规则await-interactions配合使用避免遗漏对交互 Promise 的等待让静态检查尽早兜底把eslint-plugin-storybook接入 CI配合*.stories.*的文件匹配这类漏传 context的问题在提交前就能被 error 级提示拦截而不必等到运行 Storybook 交互测试时才暴露。小结storybook/context-in-play-function用一条简单但精准的静态规则守护了 Storybook 中跨 Story 复用 play 函数的正确姿势Storybook 注入的 context 包含交互运行所需的内部能力因此任何MyOtherStory.play(...)调用都必须以完整引用或整体展开的方式把 context 原样带上。本文既给出了可直接对照的合规与违规代码也结合 规则源码 与 单元测试 还原了其三步判定逻辑同时说明了它在recommended、addon-interactions等预置配置中的默认启用状态以及通过 overrides 限定在故事文件范围内使用的注意事项。掌握这条规则能让你在组件交互高度复用的场景里少踩一个隐蔽的运行时坑。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考