FEATURED · 精选文章

Storybook 交互测试入门:用 play 函数模拟点击并断言组件行为

发布时间 / 2026/9/8 19:48:38
来源 / 创域科博编辑部
栏目 / 资讯中心
Storybook 交互测试入门:用 play 函数模拟点击并断言组件行为 Storybook 交互测试入门用 play 函数模拟点击并断言组件行为【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook本篇技术指南围绕 Storybook 中最简单、最典型的交互测试示例展开为一个“点击按钮打开对话框”的组件编写play函数模拟用户点击、再断言roledialog元素出现在文档中。该示例源自本仓库文档代码片段 docs/_snippets/interaction-test-simple.md被完整引用在 docs/writing-tests/index.mdx 与 docs/writing-tests/interaction-testing.mdx 中是理解 Storybook 组件测试体系的最小闭环。读完本文你将掌握play函数的三大核心 APIcanvas查询、userEvent模拟、expect断言并能在 Angular、React、Vue、Svelte、Web Components 等不同框架下、以 CSF 3、CSF Next、Svelte CSF 等多种文件形态写出等价的交互测试。交互测试的定位把 Story 从“渲染用例”升级为“行为用例”在 Storybook 的测试体系中每个 story 天然就是一个渲染测试render test只要组件能在给定参数和上下文下成功渲染该 story 即通过一旦渲染抛错即失败。但这种测试只能验证组件“静态地”呈现无法验证其交互逻辑。交互测试interaction test正是在此基础上的延伸。它以 story 为骨架将组件放入特定初始状态再通过 story 中定义的play函数模拟用户行为——点击、输入、提交表单等——最后对 DOM 结果或函数调用做出断言详见 docs/writing-tests/interaction-testing.mdx 对“Writing interaction tests”的说明。从 docs/writing-tests/index.mdx 对各类测试类型的定位看交互测试处于承上启下的位置它比渲染测试更深入又是快照、视觉、无障碍等其他测试类型的基础范式。代码片段interaction-test-simple.md展示的正是其中最精简的一课——一个名为Opens的 story点击 “Open Modal” 按钮后断言弹窗出现。最小示例Opens 故事的三个组成部分以 React 风格的 CSF 3 写法为例交互测试的核心逻辑如下取自 docs/_snippets/interaction-test-simple.mdimport type { Meta, StoryObj } from storybook/your-framework; import { expect } from storybook/test; import { Dialog } from ./Dialog; const meta { component: Dialog, } satisfies Metatypeof Dialog; export default meta; type Story StoryObjtypeof meta; export const Opens: Story { play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, };这段代码虽短却完整包含了交互测试的三个核心动作值得逐个拆解。第一步用canvas查询目标元素canvas是play函数 context 中暴露的一个可查询对象代表正在测试的 story 渲染出的界面。你可以把它当作当前 story 的“作用域 DOM”在其上调用 Testing Library 风格的查询方法来定位要交互或断言的元素。查询方法遵循类型主题命名约定。类型决定匹配个数与等待行为详见 docs/writing-tests/interaction-testing.mdx 的查询表格类型0 个匹配1 个匹配1 个匹配是否等待getBy...抛错返回元素抛错否queryBy...返回null返回元素抛错否findBy...抛错返回元素抛错是getAllBy...抛错返回数组返回数组否queryAllBy...返回[]返回数组返回数组否findAllBy...抛错返回数组返回数组是主题部分常用的有ByRole按可访问角色查找如button、dialog、ByLabelText、ByPlaceholderText、ByText、ByDisplayValue、ByAltText、ByTitle、ByTestId。示例中的两处查询都使用了ByRole——这符合 Testing Library 的推荐优先级优先像真实用户那样通过可访问角色与可访问名称定位元素data-testid应留作最后手段。需要留意 CSF 3 与 CSF Next 在查询选项上的细微差异CSF 3 的 Angular 及通用版本用{ name: Open Modal }匹配按钮的 accessible name而 CSF Next 示例中 Angular 版本则用{ text: Open Modal }。此外若组件依赖 Shadow DOM需借助shadow-dom-testing-library在 .storybook/preview 中注册后才能使用。第二步用userEvent模拟用户行为定位到按钮后通过userEvent.click(button)触发点击。userEvent直接取自 Testing Library在play函数内以参数形式提供它的语义是“模拟真实用户的操作序列”而非仅派发一个孤立的 DOM 事件。常用方法包括click、dblClick、hover、unhover、tab、type、keyboard、selectOptions、deselectOptions、clear等完整方法清单可参考user-event文档本仓库的交互测试指南也整理了速查表见 docs/writing-tests/interaction-testing.mdx。第三步用expect对结果断言点击之后测试要验证期望行为是否发生await expect(canvas.getByRole(dialog)).toBeInTheDocument();expect从storybook/test模块导入它合并了两类能力Vitest 自带断言如toHaveBeenCalled、toHaveBeenCalledWith与testing-library/jest-dom的 DOM 断言如toBeInTheDocument、toBeVisible、toHaveAttribute。示例断言的核心含义是dialog 元素已经进入 DOM即“点击按钮后弹窗打开”这一用户可见结果成立。注意userEvent与expect调用在play内都应await。这一要求不仅是语法惯例——只有被 await 的调用才会被逐个记录到 Interactions 面板中供你在 UI 里逐步回放与调试。同一测试多种文件形态interaction-test-simple.md的价值在于它把上面这段逻辑翻译成了 Storybook 支持的每一种主流书写形态覆盖 Angular、通用common、Svelte、Web Components、React、Vue 等渲染器以及 CSF 3、CSF Next、Svelte CSF 三种文件约定。实际项目中应根据自己的框架与 CSF 版本来对号入座。Angular声明式组件与类组件Angular 的 CSF 3 写法将组件元信息置于metaMetaDialog与StoryObjDialog提供了强类型约束import type { Meta, StoryObj } from storybook/angular; import { expect } from storybook/test; import { Dialog } from ./dialog.component; const meta: MetaDialog { component: Dialog, }; export default meta; type Story StoryObjDialog; export const Opens: Story { play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, };CSF Next 的 Angular 形态则显式导入项目级.storybook/preview通过preview.meta(...)创建 meta、用meta.story(...)声明带类型的 story注意它查询按钮时使用的是text选项import { fn, expect } from storybook/test; import preview from ../.storybook/preview; import { Dialog } from ./dialog.component; const meta preview.meta({ component: Dialog, }); export const Opens meta.story({ play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { text: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, });通用框架commonCSF 3 写法对于未单独列出的框架文档给出了“替换your-framework占位符”的通用模板分别提供 TS 与 JS 两种形态// Replace your-framework with the name of your framework (e.g. react-vite, vue3-vite, etc.) import type { Meta, StoryObj } from storybook/your-framework; import { expect } from storybook/test; import { Dialog } from ./Dialog; const meta { component: Dialog, } satisfies Metatypeof Dialog; export default meta; type Story StoryObjtypeof meta; export const Opens: Story { play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, };import { expect } from storybook/test; import { Dialog } from ./Dialog; export default { component: Dialog, }; export const Opens { play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, };对比可见TS 形态借助satisfies Metatypeof Dialog与StoryObjtypeof meta获得组件 props、args 的自动推导JS 形态则更自由、也更贴近无类型项目的速写风格。Svelte.stories.svelte单文件约定Svelte 用户有两种选择。其一是在.stories.ts中沿用标准 CSF 3等价于通用模板导入Dialog.svelte。其二是 Svelte 特有的Svelte CSF把 meta 定义放在script module中由storybook/addon-svelte-csf的defineMeta返回的Story组件来声明每个故事play以内联属性形式传入script module import { defineMeta } from storybook/addon-svelte-csf; import Dialog from ./Dialog.svelte; const { Story } defineMeta({ component: Dialog, }); /script Story nameOpens play{async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }} /与之等价、保持标准 CSF 3 的.stories.ts/.stories.js写法同样可用// Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite import type { Meta, StoryObj } from storybook/your-framework; import { expect } from storybook/test; import Dialog from ./Dialog.svelte; const meta { component: Dialog, } satisfies Metatypeof Dialog; export default meta; type Story StoryObjtypeof meta; export const Opens: Story { play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, };Web Components以自定义元素名充当 componentWeb Components 没有类组件可引用因此 meta 的component字段直接使用自定义元素标签字符串如demo-dialogimport type { Meta, StoryObj } from storybook/web-components-vite; import { expect } from storybook/test; const meta: Meta { component: demo-dialog, }; export default meta; type Story StoryObj; export const Opens: Story { play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, };JS 形态省略了Meta/StoryObj类型标注CSF Next 形态则同样经由preview.meta/meta.story收敛import { fn, expect } from storybook/test; import preview from ../.storybook/preview; const meta preview.meta({ component: demo-dialog, }); export const Opens meta.story({ play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, });CSF NextReact 与 Vue 形态CSF Next实验性写法在 React 与 Vue 中的骨架高度一致差异仅在组件导入路径.tsxvs.vue与文件扩展名。以 React TS 与 Vue JS 为例import { fn, expect } from storybook/test; import preview from ../.storybook/preview; import { Dialog } from ./Dialog; const meta preview.meta({ component: Dialog, }); export const Opens meta.story({ play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, });import { fn, expect } from storybook/test; import preview from ../.storybook/preview; import Dialog from ./Dialog.vue; const meta preview.meta({ component: Dialog, }); export const Opens meta.story({ play: async ({ canvas, userEvent }) { // Click on a button and assert that a dialog appears const button canvas.getByRole(button, { name: Open Modal }); await userEvent.click(button); await expect(canvas.getByRole(dialog)).toBeInTheDocument(); }, });CSF Next 有两处值得注意其一从../.storybook/preview导入preview意味着该示例文件假定位于与.storybook同级的组件目录下其二即使在这个简单示例中它也导入了fn尽管此处未使用这是因为 CSF Next 场景通常紧接着要借助fn对回调参数做 spy从而断言“onClick 被调用且携带了正确参数”这类更深层行为。play 在 Storybook 运行时中是如何被执行的理解 play 的执行时机有助于定位“为什么断言不生效”或“点击还没发生就报错”的问题。在 Storybook 的预览运行时preview-web中story 的渲染与测试生命周期由 StoryRender 驱动。从 StoryRender.ts 的实现可以看到渲染流程会在满足“自动播放开启autoplay且需要重新挂载”的条件下进入play阶段它通过this.runPhase(abortSignal, playing, async () playFunction(context))把 story 切换到playing阶段再以包含canvas、userEvent、args、mount等对象的完整 context 调用playFunction。也就是说组件先以默认 args 渲染进真实浏览器环境随后才执行play中的行为模拟交互测试必须开启自动播放autoplay这是测试与 storybook 预览的默认形态之一play内部抛出的断言错误会被阶段机制捕获最终反映为该 story 的测试失败状态并在 Interactions 面板中精确定位到出错的步骤。同一个执行链路也被组件测试工具与“可移植 stories”portable stories复用因而用同一份 story 编写的交互测试既可以在 Storybook 网页界面运行也可以交给 Vitest/Jest 之类的独立测试运行器执行。运行与调试交互测试interaction-test-simple.md只负责“怎么写”而运行与调试路径则落在 Storybook 的测试运行体系上详见 docs/writing-tests/index.mdx 与 docs/writing-tests/interaction-testing.mdx。在 Storybook UI 中调试Interactions 面板会逐条回放 play 函数中的每个步骤由于步骤被await它们才会被完整记录。面板提供暂停、恢复、回退与单步执行控件失败的断言会直接高亮在对应步骤上配合“以最小复现路径分享 URL”的能力可以快速把失败样例交付给团队。用 Vitest addon 自动化安装并配置 Vitest addon项目需基于 Vite后每个 story 会被自动转换成真实的 Vitest 测试并通过浏览器模式默认基于 Playwright渲染执行。你可以在测试组件中一键运行也可以把它接进编辑器扩展、终端与 CI——例如在package.json中声明test-storybook: vitest --projectstorybook脚本并配置 CI workflow。测试运行器test-runner不使用 Vitest addon 的项目可退而使用基于 Jest Playwright 的 test-runner 在终端与 CI 中执行同一套交互测试。若 play 中需要更复杂的前置能力本仓库的片段库还提供了进阶示例可继续阅读interaction-test-complex.md含fn间谍与网络 mock、login-form-with-play-function.md表单填写与提交断言、mount-basic.md渲染前 mock 时间以及完整指南 docs/writing-tests/interaction-testing.mdx。小结Opens这则示例揭示了 Storybook 交互测试的完整思维模型story 提供组件的状态与语境play 函数在此之上模拟用户并校验结果。无论你使用 Angular、React、Vue、Svelte 还是 Web Components无论项目停留在成熟的 CSF 3、正在尝试 CSF Next还是拥抱 Svelte 专属的 Svelte CSF测试语义都保持一致——getByRole定位、userEvent.click触发、expect(...).toBeInTheDocument()验证。掌握这套最小闭环后即可沿着渲染测试 → 交互测试 → 端到端测试的路径逐步为组件建立真正贴近用户行为的行为保障网。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻