交互测试
在 Storybook 中,交互测试是作为 story 的一部分构建的。该故事会使用必要的 props 和 context 渲染组件,使其处于初始状态。然后,你可以使用 播放函数 来模拟用户行为,例如点击、输入和提交表单,并对最终结果进行断言。
¥In Storybook, interaction tests are built as part of a story. That story renders the component with the necessary props and context to place it in an initial state. You then use a play function to simulate user behavior like clicks, typing, and submitting a form and then assert on the end result.
你可以使用 Storybook UI 中的“交互”面板预览和调试你的交互测试。它们可以使用 Vitest 插件自动化,允许你在 Storybook、终端或 CI 环境中为你的项目运行测试。
¥You can preview and debug your interaction tests using the Interactions panel in the Storybook UI. They can be automated using the Vitest addon, allowing you to run tests for your project in Storybook, terminal, or CI environments.
编写交互测试
¥Writing interaction tests
你编写的每个故事都可以进行渲染测试。渲染测试是交互测试的简化版本,仅测试组件在给定状态下成功渲染的能力。这对于像按钮这样相对简单的静态组件来说非常有效。但对于更复杂、更具交互性的组件,你可以更进一步。
¥Every story you write can be render tested. A render test is a simple version of an interaction test that only tests the ability of a component to render successfully in a given state. That works fine for relatively simple, static components like a Button. But for more complex, interactive components, you can go farther.
你可以使用 play 函数模拟用户行为,并对 DOM 结构或函数调用等功能方面进行断言。测试组件时,会运行 play 函数并验证其中的任何断言。
¥You can simulate user behavior and assert on functional aspects like DOM structure or function calls using the play function. When a component is tested, the play function is ran and any assertions within it are validated.
在此示例中,EmptyForm 故事测试 LoginForm 组件的渲染,FilledForm 故事测试表单提交:
¥In this example, the EmptyForm story tests the render of the LoginForm component and the FilledForm story tests the form submission:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { expect } from 'storybook/test';
import { LoginForm } from './LoginForm';
const meta = {
component: LoginForm,
} satisfies Meta<typeof LoginForm>;
export default meta;
type Story = StoryObj<typeof meta>;
export const EmptyForm: Story = {};
export const FilledForm: Story = {
play: async ({ canvas, userEvent }) => {
// 👇 Simulate interactions with the component
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
// See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'));
// 👇 Assert DOM structure
await expect(
canvas.getByText(
'Everything is perfect. Your account is ready and we should probably get you started!',
),
).toBeInTheDocument();
},
};
该代码示例中包含大量功能,让我们逐一介绍这些 API。
¥There’s a lot going on in that code sample, so let’s walk through the APIs one-by-one.
查询 canvas
¥Querying the canvas
canvas 是一个可查询元素,包含被测故事,作为 play 函数的参数提供。你可以使用 canvas 查找要交互或断言的特定元素。所有查询方法均直接来自测试库,并以 <type><subject> 的形式提供。
¥canvas is a queryable element containing the story under test, which is provided as a parameter of your play function. You can use canvas to find specific elements to interact with or assert on. All query methods come directly from Testing Library and take the form of <type><subject>.
下表总结了可用的类型,并在 测试库文档 中进行了完整记录:
¥The available types are summarized in this table and fully documented in the Testing Library docs:
| 查询类型 | 0 个匹配项 | 1 个匹配项 | 1 个匹配项 | 等待中 |
|---|---|---|---|---|
| 单个元素 | ||||
getBy... | 抛出错误 | 返回元素 | 抛出错误 | 否 |
queryBy... | 返回 null | 返回元素 | 抛出错误 | 否 |
findBy... | 抛出错误 | 返回元素 | 抛出错误 | 是 |
| 多个元素 | ||||
getAllBy... | 抛出错误 | 返回数组 | 返回数组 | 否 |
queryAllBy... | 返回 [] | 返回数组 | 返回数组 | 否 |
findAllBy... | 抛出错误 | 返回数组 | 返回数组 | 是 |
测试主题列在此处,并附有其完整测试库文档的链接:
¥The subjects are listed here, with links to their full Testing Library documentation:
-
ByRole— 根据元素的可访问角色查找元素¥
ByRole— Find elements by their accessible role -
ByLabelText— 根据元素关联的标签文本查找元素¥
ByLabelText— Find elements by their associated label text -
ByPlaceholderText— 根据占位符值查找元素¥
ByPlaceholderText— Find elements by their placeholder value -
ByText— 根据元素包含的文本查找元素¥
ByText— Find elements by the text they contain -
ByDisplayValue— 根据当前值查找input、textarea或select元素¥
ByDisplayValue— Findinput,textarea, orselectelements by their current value -
ByAltText— 查找具有给定alt属性值的元素¥
ByAltText— Find elements with the givenaltattribute value -
ByTitle— 查找具有给定title属性值的元素¥
ByTitle— Find elements with the giventitleattribute value -
ByTestId— 查找具有给定data-testid属性值的元素¥
ByTestId— Find elements with the givendata-testidattribute value
请注意此列表的顺序!我们(以及 测试库)强烈建议你以类似于真人与 UI 交互的方式查询元素。例如,按可访问角色查找元素有助于确保大多数人可以使用你的组件。使用 data-testid 应该是最后的手段,只有在尝试了所有其他方法之后才可以使用。
¥Note the order of this list! We (and Testing Libary) highly encourage you to query for elements in a way that resembles the way a real person would interact with your UI. For example, finding elements by their accessible role helps ensure that the most people can use your component. While using data-testid should be a last resort, only after trying every other approach.
总而言之,一些典型的查询可能如下所示:
¥Putting that altogether, some typical queries might look like:
// Find the first element with a role of button with the accessible name "Submit"
await canvas.findByRole('button', { name: 'Submit' });
// Get the first element with the text "An example heading"
canvas.getByText('An example heading');
// Get all elements with the role of listitem
canvas.getAllByRole('listitem');使用 userEvent 模拟行为
¥Simulating behavior with userEvent
查询元素后,你可能需要与它们交互以测试组件的行为。为此,我们使用 userEvent 实用程序,它是你的 play 函数的参数。此实用程序模拟用户与组件的交互,例如点击按钮、输入内容和选择选项。
¥After querying for elements, you will likely need to interact with them to test your component’s behavior. For this we use the userEvent utility, which is provided as a parameter of your play function. This utility simulates user interactions with the component, such as clicking buttons, typing in inputs, and selecting options.
userEvent 上有许多可用的方法,这些方法在 user-event 文档 中有详细说明。此表将重点介绍一些常用方法。
¥There are many methods available on userEvent, which are detailed in the user-event documentation. This table will highlight some of the commonly-used methods.
| 方法 | 描述 |
|---|---|
click | 点击元素,调用 click() 函数 await userEvent.click(<element>) |
dblClick | 点击元素两次 await userEvent.dblClick(<element>) |
hover | 悬停在元素上 await userEvent.hover(<element>) |
unhover | 鼠标悬停在元素 await userEvent.unhover(<element>) 之外 |
tab | 按下 Tab 键 await userEvent.tab() |
type | 在输入框或文本区域内写入文本 await userEvent.type(<element>, 'Some text'); |
keyboard | 模拟键盘事件 await userEvent.keyboard('{Shift}'); |
selectOptions | 选择选择元素的指定选项 await userEvent.selectOptions(<element>, ['1','2']); |
deselectOptions | 从 select 元素的特定选项中移除选中内容 await userEvent.deselectOptions(<element>, '1'); |
clear | 选择输入框或文本区域内的文本并将其删除 await userEvent.clear(<element>); |
userEvent 方法在 play 函数内应始终为 await。这确保它们能够在交互面板中正确记录和调试。
¥userEvent methods should always be awaited inside of the play function. This ensures they can be properly logged and debugged in the Interactions panel.
使用 expect 断言
¥Asserting with expect
最后,在查询元素并模拟行为后,你可以对结果进行断言,并在运行测试时进行验证。为此,我们使用 expect 实用程序,该实用程序可通过 storybook/test 模块获取:
¥Finally, after querying for elements and simulating behavior, you can make assertions on the result which are validated when running the test. For this we use the expect utility, which is available via the storybook/test module:
import { expect } from 'storybook/test';此处的 expect 实用程序结合了 Vitest 的 expect 和 @testing-library/jest-dom 中的方法(尽管名称不同,但它们也适用于 Vitest 测试)。有许多可用的方法。此表将重点介绍一些常用方法。
¥The expect utility here combines the methods available in Vitest’s expect as well as those from @testing-library/jest-dom (which, despite the name, also work in Vitest tests). There are many, many methods available. This table will highlight some of the commonly-used methods.
| 方法 | 描述 |
|---|---|
toBeInTheDocument() | 检查元素是否在 DOM 中 await expect(<element>).toBeInTheDocument() |
toBeVisible() | 检查元素是否对用户可见 await expect(<element>).toBeVisible() |
toBeDisabled() | 检查元素是否被禁用 await expect(<element>).toBeDisabled() |
toHaveBeenCalled() | 检查是否调用了侦测函数 await expect(<function-spy>).toHaveBeenCalled() |
toHaveBeenCalledWith() | 检查是否使用特定参数调用了侦测函数 await expect(<function-spy>).toHaveBeenCalledWith('example') |
在 play 函数中,expect 调用应始终为 await。这确保它们能够在交互面板中正确记录和调试。
¥expect calls should always be awaited inside of the play function. This ensures they can be properly logged and debugged in the Interactions panel.
使用 fn 监视函数
¥Spying on functions with fn
当你的组件调用某个函数时,你可以使用 Vitest 的 fn 实用程序(可通过 storybook/test 模块获取)监视该函数并对其行为进行断言:
¥When your component calls a function, you can spy on that function to make assertions on its behavior using the fn utility from Vitest, available via the storybook/test module:
import { fn } from 'storybook/test'大多数情况下,你会在编写故事时将 fn 用作 arg 的值,然后在测试中访问该 arg:
¥Most of the time, you will use fn as an arg value when writing your story, then access that arg in your test:
// 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 { fn, expect } from 'storybook/test';
import { LoginForm } from './LoginForm';
const meta = {
component: LoginForm,
args: {
// 👇 Use `fn` to spy on the onSubmit arg
onSubmit: fn(),
},
} satisfies Meta<typeof LoginForm>;
export default meta;
type Story = StoryObj<typeof meta>;
export const FilledForm: Story = {
play: async ({ args, canvas, userEvent }) => {
await userEvent.type(canvas.getByLabelText('Email'), 'email@provider.com');
await userEvent.type(canvas.getByLabelText('Password'), 'a-random-password');
await userEvent.click(canvas.getByRole('button', { name: 'Log in' }));
// 👇 Now we can assert that the onSubmit arg was called
await expect(args.onSubmit).toHaveBeenCalled();
},
};在组件渲染之前运行代码
¥Run code before the component gets rendered
你可以在渲染之前通过使用 play 方法中的 mount 函数执行代码。
¥You can execute code before rendering by using the mount function in the play method.
这是一个使用 mockdate 包模拟 Date 的示例,这是一种使你的故事以一致状态渲染的有用方法。
¥Here's an example of using the mockdate package to mock the Date, a useful way to make your story render in a consistent state.
import MockDate from 'mockdate';
// ...rest of story file
export const ChristmasUI: Story = {
async play({ mount }) {
MockDate.set('2024-12-25');
// 👇 Render the component with the mocked date
await mount();
// ...rest of test
},
};使用 mount 函数有两个要求:
¥There are two requirements to use the mount function:
-
你必须从
context(传递给你的 play 函数的参数)中解构 mount 属性。这可确保 Storybook 不会在播放函数开始之前开始渲染故事。¥You must destructure the mount property from the
context(the argument passed to your play function). This makes sure that Storybook does not start rendering the story before the play function begins. -
你的 Storybook 框架或构建器必须配置为转换为 ES2017 或更新版本。这是因为解构语句和 async/await 用法会被转译掉,从而阻止 Storybook 识别你对
mount的使用。¥Your Storybook framework or builder must be configured to transpile to ES2017 or newer. This is because destructuring statements and async/await usages are otherwise transpiled away, which prevents Storybook from recognizing your usage of
mount.
在 rend 之前创建模拟数据
¥Create mock data before rendering
你还可以使用 mount 创建要传递给组件的模拟数据。为此,请首先在 play 函数中创建数据,然后使用配置了该数据的组件调用 mount 函数。在此示例中,我们创建一个模拟 note 并将其 id 传递给 Page 组件,我们用它调用 mount。
¥You can also use mount to create mock data that you want to pass to the component. To do so, first create your data in the play function and then call the mount function with a component configured with that data. In this example, we create a mock note and pass its id to the Page component, which we call mount with.
// Replace your-framework with the framework you are using, e.g., react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
// 👇 Automocked module resolves to '../lib/__mocks__/db'
import db from '../lib/db';
import { Page } from './Page';
const meta = { component: Page } satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
play: async ({ mount, args, userEvent }) => {
const note = await db.note.create({
data: { title: 'Mount inside of play' },
});
const canvas = await mount(
// 👇 Pass data that is created inside of the play function to the component
// For example, a just-generated UUID
<Page {...args} params={{ id: String(note.id) }} />,
);
await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i }));
},
argTypes: {
// 👇 Make the params prop un-controllable, as the value is always overriden in the play function.
params: { control: { disable: true } },
},
};当你不带参数调用 mount() 时,组件将使用故事的渲染函数进行渲染,无论是 隐式默认值 还是 显式自定义定义。
¥When you call mount() with no arguments, the component is rendered using the story’s render function, whether the implicit default or the explicit custom definition.
当你像上面的示例一样在 mount 函数内安装特定组件时,故事的渲染函数将被忽略。这就是你必须将 args 转发给组件的原因。
¥When you mount a specific component inside the mount function like in the example above, the story’s render function will be ignored. This is why you must forward the args to the component.
在文件中的每个故事之前运行代码
¥Run code before each story in a file
有时你可能需要在文件中的每个故事之前运行相同的代码。例如,你可能需要设置组件或模块的初始状态。你可以通过向组件元数据添加异步 beforeEach 函数来执行此操作。
¥Sometimes you might need to run the same code before each story in a file. For instance, you might need to set up the initial state of the component or modules. You can do this by adding an asynchronous beforeEach function to the component meta.
你可以从 beforeEach 函数返回一个清理函数,该函数将在每个故事之后、故事重新挂载或离开时运行。
¥You can return a cleanup function from the beforeEach function, which will run after each story, when the story is remounted or navigated away from.
通常,你应该重置 预览文件的 beforeAll 或 beforeEach 功能 中的组件和模块状态,以确保它适用于你的整个项目。但是,如果组件的需求特别独特,你可以使用组件元 beforeEach 中返回的清理函数根据需要重置状态。
¥Generally, you should reset component and module state in the preview file's beforeAll or beforeEach functions, to ensure it applies to your entire project. However, if a component's needs are particularly unique, you can use the returned cleanup function in the component meta beforeEach to reset the state as needed.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import MockDate from 'mockdate';
import { Page } from './Page';
const meta = {
component: Page,
// 👇 Set the value of Date for every story in the file
async beforeEach() {
MockDate.set('2024-02-14');
// 👇 Reset the Date after each story
return () => {
MockDate.reset();
};
},
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
async play({ canvas }) {
// ... This will run with the mocked Date
},
};设置或重置所有测试的状态
¥Set up or reset state for all tests
当你 更改组件的状态 时,在渲染另一个故事之前重置该状态以保持测试之间的隔离非常重要。
¥When you alter a component's state, it's important to reset that state before rendering another story to maintain isolation between tests.
重置状态有两种选项,beforeAll 和 beforeEach。
¥There are two options for resetting state, beforeAll and beforeEach.
beforeAll
预览文件(.storybook/preview.js|ts)中的 beforeAll 函数将在项目中的任何故事之前运行一次,并且不会在故事之间重新运行。除了启动测试运行时的初始运行之外,除非更新预览文件,否则它不会再次运行。这是引导项目或运行整个项目所依赖的任何设置的好地方,如下例所示。
¥The beforeAll function in the preview file (.storybook/preview.js|ts) will run once before any stories in the project and will not re-run between stories. Beyond its initial run when kicking off a test run, it will not run again unless the preview file is updated. This is a good place to bootstrap your project or run any setup that your entire project depends on, as in the example below.
你可以从 beforeAll 函数返回一个清理函数,该函数将在重新运行 beforeAll 函数之前或在测试运行器中的拆卸过程中运行。
¥You can return a cleanup function from the beforeAll function, which will run before re-running the beforeAll function or during the teardown process in the test runner.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
import { init } from '../project-bootstrap';
const preview: Preview = {
async beforeAll() {
await init();
},
};
export default preview;beforeEach
与仅运行一次的 beforeAll 不同,预览文件 (.storybook/preview.js|ts) 中的 beforeEach 函数将在项目中的每个故事之前运行。这最适合用于重置所有或大多数故事使用的状态或模块。在下面的例子中,我们使用它来重置模拟日期。
¥Unlike beforeAll, which runs only once, the beforeEach function in the preview file (.storybook/preview.js|ts) will run before each story in the project. This is best used for resetting state or modules that are used by all or most of your stories. In the example below, we use it to reset the mocked Date.
你可以从 beforeEach 函数返回一个清理函数,该函数将在每个故事之后、故事重新挂载或离开时运行。
¥You can return a cleanup function from the beforeEach function, which will run after each story, when the story is remounted or navigated away from.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Preview } from '@storybook/your-framework';
import MockDate from 'mockdate';
const preview: Preview = {
async beforeEach() {
MockDate.reset();
},
};
export default preview;没有必要恢复 fn() 模拟,因为 Storybook 在渲染故事之前已经自动执行了该操作。有关更多信息,请参阅 parameters.test.restoreMocks API。
¥It is not necessary to restore fn() mocks, as Storybook will already do that automatically before rendering a story. See the parameters.test.restoreMocks API for more information.
交互后进行断言
¥Make assertions after interactions
有时,你可能需要在组件渲染并交互后进行断言或运行代码。
¥Sometimes, you may need to make assertions or run code after the component has been rendered and interacted with.
afterEach
afterEach 在故事渲染完成且 play 函数完成后运行。它可在预览文件 (.storybook/preview.js|ts) 中的项目级别、组件元数据中的组件级别或故事定义中的故事级别使用。这对于在组件渲染并与其交互后进行断言很有用,例如对最终渲染的输出运行检查或记录信息。
¥afterEach runs after the story is rendered and the play function has completed. It can be used at the project level in the preview file (.storybook/preview.js|ts), at the component level in the component meta, or at the story level in the story definition. This is useful for making assertions after the component has been rendered and interacted with, such as running checks on the final rendered output or logging information.
与 play 函数一样,afterEach 接收 context 对象,该对象包含 args、canvas 以及其他与故事相关的属性。你可以使用它来在故事渲染后进行断言或运行代码。
¥Like the play function, afterEach receives the context object, which contains the args, canvas, and other properties related to the story. You can use this to make assertions or run code after the story has been rendered.
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Page } from './Page';
const meta = {
component: Page,
// 👇 Runs after each story in this file
async afterEach(context) {
console.log(`✅ Tested ${context.name} story`);
},
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
async play({ canvas }) {
// ...
},
};你不应在测试中使用 afterEach 重置状态。由于它在 Story 运行后运行,因此在此处重置状态可能会导致你无法看到 Story 的正确最终状态。请使用 beforeEach 返回的清理功能 重置状态,该操作仅在故事之间导航时运行以保留最终状态。
¥You should not use afterEach to reset state in your tests. Because it runs after the story, resetting state here could prevent you from seeing the correct end state of your story. Instead, use the beforeEach's returned cleanup function to reset state, which will run only when navigating between stories to preserve the end state.
使用步骤函数对交互进行分组
¥Group interactions with the step function
对于复杂的流程,使用 step 函数将相关的交互组合在一起会很有价值。这允许你提供描述一组交互的自定义标签:
¥For complex flows, it can be worthwhile to group sets of related interactions together using the step function. This allows you to provide a custom label that describes a set of interactions:
// ...rest of story file
export const Submitted: Story = {
play: async ({ args, canvas, step }) => {
await step('Enter email and password', async () => {
await userEvent.type(canvas.getByTestId('email'), 'hi@example.com');
await userEvent.type(canvas.getByTestId('password'), 'supersecret');
});
await step('Submit form', async () => {
await userEvent.click(canvas.getByRole('button'));
});
},
};这将显示嵌套在可折叠组中的交互:
¥This will show your interactions nested in a collapsible group:

模拟模块
¥Mocked modules
如果你的组件依赖于导入到组件文件中的模块,你可以模拟这些模块以控制和断言它们的行为。这在 模拟模块指南 中有详细说明。然后,你可以将模拟模块(具有 Vitest 模拟函数 的所有有用方法)导入到你的故事中,并使用它来断言组件的行为:
¥If your component depends on modules that are imported into the component file, you can mock those modules to control and assert on their behavior. This is detailed in the mocking modules guide. You can then import the mocked module (which has all of the helpful methods of a Vitest mocked function) into your story and use it to assert on the behavior of your component:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { expect } from 'storybook/test';
// 👇 Automocked module resolves to '../app/__mocks__/actions'
import { saveNote } from '../app/actions';
import { createNotes } from '../app/mocks/notes';
import NoteUI from './note-ui';
const meta = { component: NoteUI } satisfies Meta<typeof NoteUI>;
export default meta;
type Story = StoryObj<typeof meta>;
const notes = createNotes();
export const SaveFlow: Story = {
name: 'Save Flow ▶',
args: {
isEditing: true,
note: notes[0],
},
play: async ({ canvas, userEvent }) => {
const saveButton = canvas.getByRole('menuitem', { name: /done/i });
await userEvent.click(saveButton);
// 👇 This is the mock function, so you can assert its behavior
await expect(saveNote).toHaveBeenCalled();
},
};运行交互测试
¥Running interaction tests
如果你使用的是 Vitest 插件,则可以通过以下方式运行交互测试:
¥If you're using the Vitest addon, you can run your interaction tests in these ways:
在 Storybook UI 中,你可以通过点击侧边栏中扩展测试小部件中的“运行组件测试”按钮,或者打开故事或文件夹上的上下文菜单(三个点)并选择“运行组件测试”来运行交互测试。
¥In the Storybook UI, you can run interaction tests by clicking the Run component tests button in the expanded testing widget in the sidebar or by opening the context menu (three dots) on a story or folder and selecting Run component tests.

如果你使用的是 test-runner,则可以在终端或 CI 环境中运行交互测试。
¥If you're using the test-runner, you can run your interaction tests in the terminal or in CI environments.
调试交互测试
¥Debugging interaction tests
如果你查看“交互”面板,你将看到在 play 函数中为每个故事定义的分步流程。它还提供了一组方便的 UI 控件来暂停、恢复、倒带和逐步完成每个交互。
¥If you check the Interactions panel, you'll see the step-by-step flow defined in your play function for each story. It also offers a handy set of UI controls to pause, resume, rewind, and step through each interaction.
任何测试失败也会显示在此处,方便快速查明故障点。在此示例中,缺少按下“登录”按钮后设置 submitted 状态的逻辑。
¥Any test failures will also show up here, making it easy to quickly pinpoint the exact point of failure. In this example, the logic is missing to set the submitted state after pressing the Log in button.
复制品的永久链接
¥Permalinks for reproductions
由于 Storybook 是一个 Web 应用,任何拥有 URL 的人都可以使用相同的详细信息重现故障,而无需任何额外的环境配置或工具。
¥Because Storybook is a webapp, anyone with the URL can reproduce the failure with the same detailed information without any additional environment configuration or tooling required.

通过在拉取请求中自动执行 发布 Storybook,进一步简化交互测试。这为团队提供了一个测试和调试故事的通用参考点。
¥Streamline interaction testing further by automatically publishing Storybook in pull requests. That gives teams a universal reference point to test and debug stories.
使用 CI 自动化
¥Automate with CI
使用 Vitest 插件运行测试时,自动化测试就像在 CI 环境中运行测试一样简单。更多信息,请参阅 CI 指南中的测试。
¥When you run your tests with the Vitest addon, automating those tests is as simple as running your tests in your CI environment. Please see the testing in CI guide for more information.
如果你无法使用 Vitest 插件,你仍然可以使用 test-runner 在 CI 中运行你的测试。
¥If you cannot use the Vitest addon, you can still run your tests in CI using the test-runner.
故障排除
¥Troubleshooting
交互测试和可视化测试有什么区别?
¥What’s the difference between interaction tests and visual tests?
如果将交互测试批量应用于每个组件,维护成本可能会很高。我们建议将它们与其他方法(如视觉测试)结合起来,以减少维护工作,实现全面覆盖。
¥Interaction tests can be expensive to maintain when applied wholesale to every component. We recommend combining them with other methods like visual testing for comprehensive coverage with less maintenance work.
交互测试和单独使用 Vitest + 测试库有什么区别?
¥What's the difference between interaction tests and using Vitest + Testing Library alone?
交互测试将 Vitest 和测试库集成到 Storybook 中。最大的好处是能够在真实浏览器中查看你正在测试的组件。这可以帮助你直观地进行调试,而不是在命令行中获取(假)DOM 的转储或达到 JSDOM 模拟浏览器功能的限制。将故事和测试放在一个文件中也比将它们分散在多个文件中更方便。
¥Interaction tests integrate Vitest and Testing Library into Storybook. The biggest benefit is the ability to view the component you're testing in a real browser. That helps you debug visually, instead of getting a dump of the (fake) DOM in the command line or hitting the limitations of how JSDOM mocks browser functionality. It's also more convenient to keep stories and tests together in one file than having them spread across files.
更多测试资源
¥More testing resources
-
Vitest 插件 用于在 Storybook 中运行测试
¥Vitest addon for running tests in Storybook
-
无障碍测试 用于可访问性
¥Accessibility testing for accessibility
-
可视化测试 用于外观
¥Visual testing for appearance
-
快照测试 提供有关渲染错误和警告
¥Snapshot testing for rendering errors and warnings
-
测试覆盖率 用于测量代码覆盖率
¥Test coverage for measuring code coverage
-
CI 用于在你的 CI/CD 流水线中运行测试
¥CI for running tests in your CI/CD pipeline
-
端到端测试 提供有关模拟真实用户场景
¥End-to-end testing for simulating real user scenarios
-
单元测试 用于功能性
¥Unit testing for functionality
-
测试运行器 用于自动化测试执行
¥Test runner to automate test execution
