文档
Storybook Docs

Play function

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

使用 play 函数编写故事

Storybook 的 play 函数是在故事渲染完成后运行的小代码片段。借助 交互面板,您可以构建组件交互并测试那些在没有用户干预的情况下不可能实现的场景。例如,如果您正在处理一个注册表单并想对其进行验证,您可以编写如下的 play 函数故事

RegistrationForm.stories.ts|tsx
// 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 { RegistrationForm } from './RegistrationForm';
 
const meta = {
  component: RegistrationForm,
} satisfies Meta<typeof RegistrationForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
/*
 * See https://storybook.org.cn/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvas to query the DOM
 */
export const FilledForm: Story = {
  play: async ({ canvas, userEvent }) => {
    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,
    });
 
    const submitButton = canvas.getByRole('button');
    await userEvent.click(submitButton);
  },
};

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

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

处理画布

传递给 play 函数的上下文的一部分是 canvas 对象。此对象允许您查询已渲染故事的 DOM。它提供了 Testing Library 查询的范围版本,因此您可以像在常规测试中一样使用它们。

MyComponent.stories.ts|tsx
// 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 { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const ExampleStory: Story = {
  play: async ({ canvas, userEvent }) => {
    // Starts querying from the component's root element
    await userEvent.type(canvas.getByTestId('example-element'), 'something');
    await userEvent.click(canvas.getByRole('button'));
  },
};

如果您需要查询画布外部的内容(例如,测试出现在故事根目录之外的对话框),您可以使用 storybook/test 中提供的 screen 对象。

Dialog.stories.ts|tsx
// 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 { screen } from 'storybook/test';
 
import { Dialog } from './Dialog';
 
const meta = {
  component: Dialog,
} satisfies Meta<typeof Dialog>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Open: Story = {
  play: async ({ canvas, userEvent }) => {
    await userEvent.click(canvas.getByRole('button', { name: 'Open dialog' }));
 
    // Starts querying from the document
    const dialog = screen.getByRole('dialog');
    await expect(dialog).toBeVisible();
  },
};

组合故事

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

MyComponent.stories.ts|tsx
// 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 { MyComponent } from './MyComponent';
 
const meta = {
  component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
/*
 * See https://storybook.org.cn/docs/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvas to query the DOM
 */
export const FirstStory: Story = {
  play: async ({ canvas, userEvent }) => {
    await userEvent.type(canvas.getByTestId('an-element'), 'example-value');
  },
};
 
export const SecondStory: Story = {
  play: async ({ canvas, userEvent }) => {
    await userEvent.type(canvas.getByTestId('other-element'), 'another value');
  },
};
 
export const CombinedStories: Story = {
  play: async ({ context, canvas, userEvent }) => {
    // 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');
  },
};

通过组合这些故事,您将重现整个组件工作流程,并在减少所需的样板代码的同时发现潜在问题。