测试组件交互
你拨动开关,灯却没有亮。可能是灯泡烧坏了,也可能是线路故障。开关和灯泡通过墙内的电线连接在一起。
应用程序也是如此。表面上是用户看到和交互的 UI。在底层,UI 被连接起来以促进数据和事件的流动。
随着你构建更复杂的 UI(如页面),组件不仅负责渲染 UI。它们还会获取数据并管理状态。本章将教你如何使用计算机来模拟和验证用户交互。

那个组件真的有用吗?
组件的主要任务是根据一组 props 渲染 UI 的一部分。更复杂的组件还会跟踪应用程序状态并将行为传递给组件树。
例如,组件将从初始状态开始。当用户在输入字段中键入内容或单击按钮时,它会触发应用程序内的事件。组件会根据此事件更新状态。这些状态更改随后会更新渲染的 UI。这就是交互的完整周期。
在 InboxScreen 中,用户可以单击星形图标来固定任务。或者单击复选框将其归档。视觉测试可确保组件在所有这些状态下看起来都正确。我们还需要确保 UI 对这些交互做出正确的响应。

Storybook 中的组件测试如何工作?
测试交互是验证用户行为的广泛模式。你提供模拟数据来设置测试场景,使用 Testing Library 模拟用户交互,并检查生成的 DOM 结构。

在 Storybook 中,这种熟悉的开发流程发生在你的浏览器中。这使得调试失败更加容易,因为你在与开发组件相同的环境中运行测试——浏览器。
我们将从编写一个 **story** 开始,以设置组件的初始状态。然后使用 **play function** 模拟用户行为,如点击和表单输入。最后,使用 Storybook **test runner** 检查 UI 和组件状态是否正确更新。
设置测试运行器
运行以下命令进行安装
yarn add --dev @storybook/test-runner
然后将测试任务添加到你的项目的 package.json 中
{
"scripts": {
"test-storybook": "test-storybook"
}
}
最后,启动你的 Storybook(测试运行器将针对你的本地 Storybook 实例运行)
yarn storybook
将 stories 重用为组件测试用例
在上一章中,我们在 InboxScreen.stories.jsx 文件中列出了 InboxScreen 组件的所有用例。这使我们能够在开发过程中进行抽查外观,并通过视觉测试捕获回归。这些 stories 现在也将支持我们的组件测试。
import { http, HttpResponse } from 'msw';
import InboxScreen from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
export default {
component: InboxScreen,
title: 'InboxScreen',
};
export const Default = {
parameters: {
msw: {
handlers: [
http.get('/tasks', () => {
return HttpResponse.json(TaskListDefault.args);
}),
],
},
},
};
export const Error = {
args: {
error: 'Something',
},
parameters: {
msw: {
handlers: [
http.get('/tasks', () => {
return HttpResponse.json([]);
}),
],
},
},
};
使用 play function 编写组件测试
Testing Library 提供了一个方便的 API 来模拟用户交互——点击、拖动、触摸、输入等。而 Vitest 提供断言实用程序。我们将使用这两个工具的 Storybook 增强版本来编写测试。因此,你可以获得一个熟悉的、对开发者友好的与 DOM 交互的语法,同时还具有额外的遥测功能以帮助调试。
测试本身将包含在 play function 中。这段代码会附加到一个 story 上,并在 story 渲染后运行。
让我们添加第一个组件测试,以验证用户可以固定一个任务
import { http, HttpResponse } from 'msw';
import InboxScreen from './InboxScreen';
import { Default as TaskListDefault } from './components/TaskList.stories';
import { expect, userEvent, findByRole, within } from '@storybook/test';
// ... code omitted for brevity ...
export const PinTask = {
parameters: {
...Default.parameters,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (id) => canvas.findByRole('listitem', { name: id });
const itemToPin = await getTask('task-4');
// Find the pin button
const pinButton = await findByRole(itemToPin, 'button', { name: 'pin' });
// Click the pin button
await userEvent.click(pinButton);
// Check that the pin button is now a unpin button
const unpinButton = within(itemToPin).getByRole('button', {
name: 'unpin',
});
await expect(unpinButton).toBeInTheDocument();
},
};
💡 @storybook/test 包取代了 @storybook/jest 和 @storybook/testing-library 测试包,提供了更小的打包体积和基于 Vitest 包的更简单的 API。
每个 play function 都接收 Canvas 元素——story 的顶级容器。你可以将查询范围限定在此元素内,从而更容易找到 DOM 节点。
在本例中,我们正在查找“Export logo”任务。然后找到其中的固定按钮并单击它。最后,我们检查按钮是否已更新为未固定状态。
当 Storybook 完成渲染 story 时,它会执行 play function 中定义的步骤,与组件交互并固定任务——类似于用户操作的方式。如果你查看 interactions panel,你将看到分步流程。它还提供了一组方便的 UI 控件来暂停、恢复、倒带和逐个交互。
使用测试运行器执行测试
既然我们有了第一个测试,让我们继续为归档和编辑任务功能添加测试。
// ... code omitted for brevity ...
export const ArchiveTask = {
parameters: {
...Default.parameters,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (id) => canvas.findByRole('listitem', { name: id });
const itemToArchive = await getTask('task-2');
const archiveButton = await findByRole(itemToArchive, 'button', {
name: 'archiveButton-2',
});
await userEvent.click(archiveButton);
},
};
export const EditTask = {
parameters: {
...Default.parameters,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const getTask = (id) => canvas.findByRole('listitem', { name: id });
const itemToEdit = await getTask('task-5');
const taskInput = await findByRole(itemToEdit, 'textbox');
await userEvent.type(taskInput, ' and disabled state');
await expect(taskInput.value).toBe(
'Fix bug in input error state and disabled state'
);
},
};
现在你应该可以看到这些场景的 stories。Storybook 只在你查看 story 时运行组件测试。因此,你需要查看每个 story 来运行所有检查。
手动审查整个 Storybook 并不现实。Storybook test runner 自动化了这个过程。它是一个独立的实用程序——由 Playwright 提供支持——可以运行所有测试并捕获损坏的 stories。

启动测试运行器(在单独的终端窗口中)
yarn test-storybook --watch

它将验证所有 stories 是否都无错误地渲染,并且所有断言都通过。如果测试失败,你将获得一个链接,该链接会在浏览器中打开失败的 story。

总之,设置代码和测试都位于 stories 文件中。使用 play function,我们像用户一样与 UI 进行了交互。Storybook 组件测试将实时浏览器的直观调试环境与无头浏览器的性能和脚本化能力相结合。
捕获可用性问题
当你确保你的 UI 对每个用户都可用时,你就会影响公司的财务状况并满足 法律要求。这是双赢。下一章将演示如何利用 stories 的可移植性来简化可访问性测试。