加入直播会话:周四,美国东部时间上午 11 点,Storybook 9 版本发布 & AMA
文档
Storybook Docs

Play function

Play function 是 story 渲染后执行的一小段代码片段。它们使你能够与组件交互,并测试否则需要用户介入的场景。

使用 play function 编写 stories

Storybook 的 play function 是 story 渲染完成后运行的一小段代码片段。借助交互面板,它使你能够构建组件交互和测试在没有用户介入的情况下无法实现的场景。例如,如果你正在处理一个注册表单并想验证它,你可以使用 play function 编写以下 story

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: Meta<typeof RegistrationForm> = {
  component: RegistrationForm,
};
export default meta;
 
type Story = StoryObj<typeof RegistrationForm>;
 
/*
 * 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 完成 story 渲染后,它会执行 play function 中定义的步骤,与组件交互并填充表单信息。这一切都无需用户介入。如果你查看你的 Interactions 面板,你会看到分步流程。

使用 canvas

传递给 play function 的一部分上下文是一个 canvas 对象。此对象允许你查询渲染的 story 的 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'));
  },
};

如果你需要在 canvas 之外进行查询(例如,测试出现在 story 根目录之外的对话框),可以使用 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();
  },
};

组合 stories

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

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');
  },
};

通过组合这些 stories,你正在重新创建整个组件工作流程,可以在减少所需的样板代码的同时发现潜在问题。