Docs
Storybook Docs

播放函数

Watch a video tutorial

Play 函数是故事渲染后执行的小代码片段。使你能够与组件进行交互并测试需要用户干预的场景。

¥Play functions are small snippets of code executed after the story renders. Enabling you to interact with your components and test scenarios that otherwise required user intervention.

设置交互插件

¥Setup the interactions addon

我们建议在你开始使用 play 功能编写故事之前安装 Storybook 的 addon-interactions。它是它的完美补充,包括一组方便的 UI 控件,可让你控制执行流程。你可以随时暂停、恢复、倒带和逐步完成每个交互。还为你提供了一个易于使用的调试器,用于解决潜在问题。

¥We recommend installing Storybook's addon-interactions before you start writing stories with the play function. It's the perfect complement for it, including a handy set of UI controls to allow you command over the execution flow. At any time, you can pause, resume, rewind, and step through each interaction. Also providing you with an easy-to-use debugger for potential issues.

运行以下命令来安装插件和所需的依赖。

¥Run the following command to install the addon and the required dependencies.

npm install @storybook/test @storybook/addon-interactions --save-dev

更新你的 Storybook 配置(在 .storybook/main.js|ts 中)以包含交互插件。

¥Update your Storybook configuration (in .storybook/main.js|ts) to include the interactions addon.

.storybook/main.ts
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
 
const config: StorybookConfig = {
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    // Other Storybook addons
    '@storybook/addon-interactions', // 👈 Register the addon
  ],
};
 
export default config;

使用 play 函数编写故事

¥Writing stories with the play function

Storybook 的 play 函数是故事完成渲染后运行的小代码片段。在 addon-interactions 的帮助下,它允许你构建组件交互和测试场景,而这些在没有用户干预的情况下是不可能实现的。例如,如果你正在开发注册表单并想要验证它,你可以使用 play 函数编写以下故事:

¥Storybook's play functions are small code snippets that run once the story finishes rendering. Aided by the addon-interactions, it allows you to build component interactions and test scenarios that were impossible without user intervention. For example, if you were working on a registration form and wanted to validate it, you could write the following story with the play function:

RegistrationForm.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { RegistrationForm } from './RegistrationForm';
 
const meta: Meta<typeof RegistrationForm> = {
  component: RegistrationForm,
};
 
export default meta;
type Story = StoryObj<typeof RegistrationForm>;
 
/*
 * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const FilledForm: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    const emailInput = canvas.getByLabelText('email', {
      selector: 'input',
    });
 
    await userEvent.type(emailInput, 'example-email@email.com', {
      delay: 100,
    });
 
    const passwordInput = canvas.getByLabelText('password', {
      selector: 'input',
    });
 
    await userEvent.type(passwordInput, 'ExamplePassword', {
      delay: 100,
    });
    // See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    const submitButton = canvas.getByRole('button');
 
    await userEvent.click(submitButton);
  },
};

有关可用 API 事件的概述,请参阅 组件测试文档

¥See the component testing documentation for an overview of the available API events.

当 Storybook 完成故事渲染时,它会执行 play 函数中定义的步骤,与组件交互并填写表单的信息。所有这些都无需用户干预。如果你检查 Interactions 面板,你将看到分步流程。

¥When Storybook finishes rendering the story, it executes the steps defined within the play function, interacting with the component and filling the form's information. All of this without the need for user intervention. If you check your Interactions panel, you'll see the step-by-step flow.

编写故事

¥Composing stories

得益于 组件故事格式(一种基于 ES6 模块的文件格式),你还可以组合 play 功能,类似于其他现有的 Storybook 功能(例如 args)。例如,如果你想验证组件的特定工作流程,你可以编写以下故事:

¥Thanks to the Component Story Format, an ES6 module based file format, you can also combine your play functions, similar to other existing Storybook features (e.g., args). For example, if you wanted to verify a specific workflow for your component, you could write the following stories:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
/*
 * See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const FirstStory: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    await userEvent.type(canvas.getByTestId('an-element'), 'example-value');
  },
};
 
export const SecondStory: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    await userEvent.type(canvas.getByTestId('other-element'), 'another value');
  },
};
 
export const CombinedStories: Story = {
  play: async ({ context, canvasElement }) => {
    const canvas = within(canvasElement);
 
    // Runs the FirstStory and Second story play function before running this story's play function
    await FirstStory.play(context);
    await SecondStory.play(context);
    await userEvent.type(canvas.getByTestId('another-element'), 'random value');
  },
};

通过组合故事,你可以重新创建整个组件工作流程,并可以发现潜在问题,同时减少你需要编写的样板代码。

¥By combining the stories, you're recreating the entire component workflow and can spot potential issues while reducing the boilerplate code you need to write.

使用事件

¥Working with events

大多数现代 UI 都是以交互为重点构建的(例如,单击按钮、选择选项、勾选复选框),为终端用户提供丰富的体验。使用 play 函数,你可以将相同级别的交互纳入你的故事中。

¥Most modern UIs are built focusing on interaction (e.g., clicking a button, selecting options, ticking checkboxes), providing rich experiences to the end-user. With the play function, you can incorporate the same level of interaction into your stories.

一种常见的组件交互类型是按钮单击。如果你需要在故事中重现它,则可以按如下方式定义故事的 play 函数:

¥A common type of component interaction is a button click. If you need to reproduce it in your story, you can define your story's play function as the following:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { fireEvent, userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
/* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const ClickExample: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // 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'));
  },
};
 
export const FireEventExample: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await fireEvent.click(canvas.getByTestId('data-testid'));
  },
};

当 Storybook 加载故事并执行函数时,它会与组件交互并触发按钮单击,类似于用户会执行的操作。

¥When Storybook loads the story and the function executes, it interacts with the component and triggers the button click, similar to what a user would do.

除了单击事件之外,你还可以使用 play 函数编写其他事件脚本。例如,如果你的组件包含具有各种选项的选择,你可以编写以下故事并测试每个场景:

¥Asides from click events, you can also script additional events with the play function. For example, if your component includes a select with various options, you can write the following story and test each scenario:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
// Function to emulate pausing between interactions
function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
/* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const ExampleChangeEvent: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    const select = canvas.getByRole('listbox');
 
    await userEvent.selectOptions(select, ['One Item']);
    await sleep(2000);
 
    await userEvent.selectOptions(select, ['Another Item']);
    await sleep(2000);
 
    await userEvent.selectOptions(select, ['Yet another item']);
  },
};

除了事件之外,你还可以基于其他类型的异步方法创建与 play 函数的交互。例如,假设你正在使用一个实现了验证逻辑的组件(例如,电子邮件验证、密码强度)。在这种情况下,你可以在 play 函数中引入延迟来模拟用户交互并断言提供的值是否有效:

¥In addition to events, you can also create interactions with the play function based on other types of asynchronous methods. For instance, let's assume that you're working with a component with validation logic implemented (e.g., email validation, password strength). In that case, you can introduce delays within your play function to emulate user interaction and assert if the values provided are valid or not:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
/* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const DelayedStory: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    const exampleElement = canvas.getByLabelText('example-element');
 
    // The delay option sets the amount of milliseconds between characters being typed
    await userEvent.type(exampleElement, 'random string', {
      delay: 100,
    });
 
    const AnotherExampleElement = canvas.getByLabelText('another-example-element');
    await userEvent.type(AnotherExampleElement, 'another random string', {
      delay: 100,
    });
  },
};

当 Storybook 加载故事时,它会与组件交互,填写其输入并触发定义的任何验证逻辑。

¥When Storybook loads the story, it interacts with the component, filling in its inputs and triggering any validation logic defined.

你还可以使用 play 函数根据特定交互验证元素的存在。例如,如果你正在处理一个组件并想检查如果用户输入错误信息会发生什么。在这种情况下,你可以编写以下故事:

¥You can also use the play function to verify the existence of an element based on a specific interaction. For instance, if you're working on a component and want to check what happens if a user introduces the wrong information. In that case, you could write the following story:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, waitFor, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
/* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const ExampleAsyncStory: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    const Input = canvas.getByLabelText('Username', {
      selector: 'input',
    });
 
    await userEvent.type(Input, 'WrongInput', {
      delay: 100,
    });
 
    // See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    const Submit = canvas.getByRole('button');
    await userEvent.click(Submit);
 
    await waitFor(async () => {
      await userEvent.hover(canvas.getByTestId('error'));
    });
  },
};

查询元素

¥Querying elements

如果需要,你还可以调整 play 函数以根据查询(例如角色、文本内容)查找元素。例如:

¥If you need, you can also adjust your play function to find elements based on queries (e.g., role, text content). For example:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
/* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const ExampleWithRole: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // 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', { name: / button label/i }));
  },
};

你可以在 测试库文档 中阅读有关查询元素的更多信息。

¥You can read more about the querying elements in the Testing Library documentation.

当 Storybook 加载故事时,play 函数开始执行并查询 DOM 树,期望元素在故事渲染时可用。如果你的测试失败,你将能够快速验证其根本原因。

¥When Storybook loads the story, the play function starts its execution and queries the DOM tree expecting the element to be available when the story renders. In case there's a failure in your test, you'll be able to verify its root cause quickly.

否则,如果组件不是立即可用的,例如,由于 play 函数中定义的上一步或某些异步行为,你可以调整故事并等待 DOM 树的更改发生,然后再查询元素。例如:

¥Otherwise, if the component is not immediately available, for instance, due to a previous step defined inside your play function or some asynchronous behavior, you can adjust your story and wait for the change to the DOM tree to happen before querying the element. For example:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
/* See https://storybook.js.org/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const AsyncExample: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
 
    // Other steps
 
    // Waits for the component to be rendered before querying the element
    await canvas.findByRole('button', { name: / button label/i });
  },
};

使用 Canvas

¥Working with the Canvas

默认情况下,你在 play 函数中编写的每个交互都将从 Canvas 的顶层元素开始执行。这对于较小的组件(例如按钮、复选框、文本输入)是可以接受的,但对于复杂的组件(例如表单、页面)或多个故事来说效率低下。为了适应这一点,你可以调整交互以从组件的根目录开始执行。例如:

¥By default, each interaction you write inside your play function will be executed starting from the top-level element of the Canvas. This is acceptable for smaller components (e.g., buttons, checkboxes, text inputs), but can be inefficient for complex components (e.g., forms, pages), or for multiple stories. To accommodate this, you can adjust your interactions to start execution from the component's root. For example:

MyComponent.stories.ts|tsx
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { userEvent, within } from '@storybook/test';
 
import { MyComponent } from './MyComponent';
 
const meta: Meta<typeof MyComponent> = {
  component: MyComponent,
};
 
export default meta;
type Story = StoryObj<typeof MyComponent>;
 
export const ExampleStory: Story = {
  play: async ({ canvasElement }) => {
    // Assigns canvas to the component root element
    const canvas = within(canvasElement);
 
    // Starts querying from the component's root element
    await userEvent.type(canvas.getByTestId('example-element'), 'something');
    await userEvent.click(canvas.getByRole('another-element'));
  },
};

将这些更改应用于你的故事可以提高性能并改善 addon-interactions 的错误处理。

¥Applying these changes to your stories can provide a performance boost and improved error handling with addon-interactions.