ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

掌握 Storybook 的 step 函数:为交互测试分组、命名与精确调试

掌握 Storybook 的 step 函数:为交互测试分组、命名与精确调试 掌握 Storybook 的 step 函数为交互测试分组、命名与精确调试step 函数是 Storybook play 函数中用于对一组相关交互进行分组的关键 API它为复杂流程提供自定义标签并让 Interactions 面板以可折叠分组的形式呈现每步交互。本文将基于 Storybook 官方文档与仓库源码讲解 step 函数的用法、CSF 3 与 CSF Next 两种写法、底层执行机制及调试技巧。原文素材取自仓库文档 interaction-testing.mdx 引用的代码片段 storybook-interactions-step-function.md并结合交互测试内核源码展开。step 函数解决的问题复杂交互流程的可读性与可调试性在交互测试Interaction tests中每个 story 的play函数负责模拟真实用户行为点击、输入、提交表单随后对结果进行断言。对于登录、下单这类流程较长的组件play函数里往往会混入多组不同类型的操作比如“先填表单、再提交、再等待响应”。此时仅从代码或 Interactions 面板的日志里很难一眼分辨哪些操作属于同一个业务阶段。step函数正是为此设计的它接收一个描述性标签和一个回调内部继续执行若干交互从而把一组相关的userEvent操作打包成一个带名字的分组。在 Storybook UI 的 Interactions 面板中这些分组会嵌套展示为可折叠的组运行失败时也能直接定位到具体的步骤。在 play 函数中使用 step基础写法step与args、canvas、userEvent一样从 play 函数的 context 参数中解构获得。以下示例来自关联片段它把“填写邮箱与密码”和“提交表单”拆成两个命名步骤// ...story file 其余内容 export const Submitted { play: async ({ args, canvas, step, userEvent }) { await step(Enter email and password, async () { await userEvent.type(canvas.getByTestId(email), hiexample.com); await userEvent.type(canvas.getByTestId(password), supersecret); }); await step(Submit form, async () { await userEvent.click(canvas.getByRole(button)); }); }, };// ...story file 其余内容 export const Submitted: Story { play: async ({ args, canvas, step, userEvent }) { await step(Enter email and password, async () { await userEvent.type(canvas.getByTestId(email), hiexample.com); await userEvent.type(canvas.getByTestId(password), supersecret); }); await step(Submit form, async () { await userEvent.click(canvas.getByRole(button)); }); }, };对上述示例逐点拆解step(label, play)第一个参数是StepLabel即 string 类型标签第二个参数是与 play 函数签名一致的异步回调内部照常使用canvas上的 Testing Library 查询与userEvent模拟操作查询优先按真实用户习惯示例中用getByTestId定位输入框、用getByRole(button)定位按钮。官方推荐优先使用ByRole、ByLabelText这类贴近无障碍语义的查询data-testid应作为兜底手段详见 querying the canvas务必await每一步userEvent与step都应被await这样 Interactions 面板才能完整记录并逐帧回放每一个交互。运行后Interactions 面板会把Enter email and password、Submit form呈现为带层级、可折叠的组交互之间可以暂停、续播、回退、单步执行。在 CSF Next 中编写带 step 的 storyCSF Nextpreview.meta/meta.story结构是片段中出现的另一种 story 组织方式。与 CSF 3 的export const风格不同CSF Next 从.storybook/preview引入preview先声明meta绑定被测组件再通过meta.story({...})定义单个 storyimport preview from ../.storybook/preview; import MyComponent from ./MyComponent; const meta preview.meta({ component: MyComponent, }); export const Submitted meta.story({ play: async ({ args, canvas, step, userEvent }) { await step(Enter email and password, async () { await userEvent.type(canvas.getByTestId(email), hiexample.com); await userEvent.type(canvas.getByTestId(password), supersecret); }); await step(Submit form, async () { await userEvent.click(canvas.getByRole(button)); }); }, });片段同时为多个渲染器提供了对应变体差异集中在如何绑定组件以及文件命名上渲染器组件绑定写法建议文件名Angularcomponent: MyComponent导入./my-component.componentMyComponent.stories.tsReactcomponent: MyComponent默认导出组件MyComponent.stories.ts/.stories.jsVue 3component: MyComponent导入./MyComponent.vueMyComponent.stories.ts/.stories.jsWeb Componentscomponent: my-component标签名字符串MyComponent.stories.ts/.stories.js例如 Web Components 变体将 meta 的组件绑定为标签名import preview from ../.storybook/preview; const meta preview.meta({ component: my-component, }); export const Submitted meta.story({ play: async ({ args, canvas, step, userEvent }) { await step(Enter email and password, async () { await userEvent.type(canvas.getByTestId(email), hiexample.com); await userEvent.type(canvas.getByTestId(password), supersecret); }); await step(Submit form, async () { await userEvent.click(canvas.getByRole(button)); }); }, });无论哪种渲染器与 story 组织方式step(..., async () {...})的核心用法完全一致标签只是为交互分组服务不改变事件执行顺序也不改变 context 内容。step 的源码实现与底层原理从源码角度step是 story 运行时 context 的一个正式成员。在 story.ts 的StoryContext接口中可以看到它与其他调试 API 并列export interface StoryContextTRenderer, TArgs ... { canvas: Canvas; userEvent: ReturnTypetypeof userEvent.setup; mount: TRenderer[mount]; step: StepFunctionTRenderer, TArgs; ... }对应的类型定义在同一文件 story.tsStepLabel即普通字符串StepFunction (label: StepLabel, play: PlayFunction) Promisevoid | void——这就是你在 play 函数里调用的step的签名StepRunner (label, play, context) Promisevoid是各插件尤其是 interactions 插件对“如何运行一个 step”的底层扩展点。step的执行并非魔法而是由 step runner 编排的。仓库中 stepRunners.ts 提供了composeStepRunners把多个插件注册的 step runner 像装饰器一样依次组合最内层才真正执行用户传入的play(context)。默认情况下没有任何插件注册 step runner组合结果等价于async (label, play, context) play(context)即 step 退化为普通函数调用。而典型的 step runner 实现来自 interactions 插件——它会对 step 内部的所有被插桩代码附加标签信息这正是 Interactions 面板能渲染成带标题分组的原因。这一组合在 composeConfigs.ts 中完成runStep: composeStepRunnersTRenderer(stepRunners),其行为也有对应测试验证见 stepRunners.test.ts其中既有多个 step runner 依序嵌套的场景也有空 runner 数组退化为直通执行的场景。由此可以推断两点用法约束不要在 step 回调里省略awaitstep runner 的职责之一是把回调内被插桩的交互完整记录异步时序被await打断会破坏面板的步骤还原step 可以嵌套使用因为 step 的内层回调收到的仍是完整StoryContext其中包含step本身你可以在一个大的step(Submit form)内部再细分step(Fill email)、step(Click submit)等层级从而获得更清晰的树状交互日志。调试与运行带 step 的交互测试在 Storybook UI 中打开某个 story 的 Interactions 面板就能看到 play 函数按 step 分组后的完整流程面板提供暂停、恢复、回退与逐条执行的控件。如果某个断言失败错误会直接挂在对应的交互/分组上配合 permalink基于 URL 的复现链接即可把失败现场分享给协作者无需额外环境即可复现参见 interaction-testing.mdx 调试章节。自动执行方面这些交互测试可通过 Vitest 插件在 Storybook UI、编辑器、终端或 CI 中运行也可以使用 test-runner具体方式见 运行交互测试 与 CI 章节。最佳实践小结按业务阶段而不是按单个操作建组一个 step 内包含一组“用户视角下连贯的动作”标签采用“动词 对象”的祈使句如Enter email and password、Submit form查询顺序遵循 Testing Library 推荐优先级能用getByRole/getByLabelText就不用getByTestId始终awaitstep 与其内部的 userEvent/expect保证 Interactions 面板日志的完整性与可调试性善用 Interactions 面板做回归验证把步骤标题当作测试文档失败信息会让后续维护者第一时间理解组件预期行为若需要断言、mock 模块或在渲染前后执行逻辑可与fn、mount、beforeEach/afterEach等 API 组合使用完整参考 interaction-testing.mdx。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表