storybook-interactions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Storybook Interaction Tests (Play Functions)

Storybook 交互测试(Play Functions)

Write play functions with consistent structure, accessible queries, and proper async handling.
编写具备一致结构、可访问查询和正确异步处理的play函数。

Required Structure

必填结构

Every play function must follow this pattern:
tsx
export const SearchAndSelect: Story = {
  args: { options: mockData },
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, args, step }) => {
    const canvas = within(canvasElement);

    await step('Search for option', async () => {
      const input = canvas.getByTestId('search-input');
      await userEvent.type(input, 'react');
    });

    await step('Select filtered option', async () => {
      const option = await canvas.findByTestId('option-react');
      await userEvent.click(option);
    });

    await step('Verify selection', async () => {
      await expect(args.onChange).toHaveBeenCalledWith(
        expect.arrayContaining([expect.objectContaining({ value: 'react' })]),
      );
    });
  },
};
每个play函数必须遵循以下模式:
tsx
export const SearchAndSelect: Story = {
  args: { options: mockData },
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, args, step }) => {
    const canvas = within(canvasElement);

    await step('Search for option', async () => {
      const input = canvas.getByTestId('search-input');
      await userEvent.type(input, 'react');
    });

    await step('Select filtered option', async () => {
      const option = await canvas.findByTestId('option-react');
      await userEvent.click(option);
    });

    await step('Verify selection', async () => {
      await expect(args.onChange).toHaveBeenCalledWith(
        expect.arrayContaining([expect.objectContaining({ value: 'react' })]),
      );
    });
  },
};

Key Rules

核心规则

  1. Always destructure
    canvasElement
    ,
    args
    , and
    step
    from the play function argument
  2. Always call
    within(canvasElement)
    as the first line
  3. Wrap logical groups of actions in
    step()
    for test reporting
  4. Always
    await
    user interactions and assertions
  5. Add tags
    ['test', 'interaction']
    to stories with play functions
  1. 始终解构 play函数参数中的
    canvasElement
    args
    step
  2. 始终调用
    within(canvasElement)
    作为第一行代码
  3. 将逻辑操作分组
    step()
    包裹,便于测试报告展示
  4. 始终使用await 处理用户交互和断言
  5. 添加标签 为包含play函数的stories添加
    ['test', 'interaction']
    标签

Query Priority Order

查询优先级顺序

Use queries in this order of preference:
PriorityQueryUse When
1st
getByRole
Element has an accessible role (button, textbox, etc.)
2nd
getByLabelText
Form elements with associated labels
3rd
getByPlaceholderText
Inputs with placeholder text
Last
getByTestId
No accessible query available
tsx
// Preferred - accessible queries
const button = canvas.getByRole('button', { name: /submit/i });
const input = canvas.getByLabelText('Email address');

// Acceptable - when role/label not available
const dropdown = canvas.getByTestId('multiselect-dropdown');

// Never use - fragile selectors
const element = canvas.getByClassName('my-class');  // Breaks on style changes
请按以下优先级使用查询方法:
优先级查询方法适用场景
第一优先级
getByRole
元素具备可访问角色(如按钮、文本框等)
第二优先级
getByLabelText
关联了标签的表单元素
第三优先级
getByPlaceholderText
带有占位文本的输入框
最后选择
getByTestId
没有可用的可访问查询方法时
tsx
// 推荐使用 - 可访问查询
const button = canvas.getByRole('button', { name: /submit/i });
const input = canvas.getByLabelText('Email address');

// 可接受 - 当角色/标签不可用时
const dropdown = canvas.getByTestId('multiselect-dropdown');

// 禁止使用 - 脆弱的选择器
const element = canvas.getByClassName('my-class');  // 样式变更时会失效

Async Rules

异步规则

tsx
// After interactions that render new elements, use findBy (auto-waits)
await userEvent.click(openButton);
const dropdown = await canvas.findByTestId('dropdown');

// For assertions on async state changes, use waitFor
await waitFor(() => {
  expect(canvas.getByText('Loading...')).not.toBeInTheDocument();
});

// Always await userEvent calls
await userEvent.click(button);    // Correct
await userEvent.type(input, 'x'); // Correct
userEvent.click(button);          // WRONG - missing await
tsx
// 在会渲染新元素的交互后,使用findBy(自动等待)
await userEvent.click(openButton);
const dropdown = await canvas.findByTestId('dropdown');

// 针对异步状态变更的断言,使用waitFor
await waitFor(() => {
  expect(canvas.getByText('Loading...')).not.toBeInTheDocument();
});

// 始终await userEvent调用
await userEvent.click(button);    // 正确
await userEvent.type(input, 'x'); // 正确
userEvent.click(button);          // 错误 - 缺少await

Examples

示例

Example 1: Create an interaction test for form submission

示例1:为表单提交创建交互测试

User: "Add a play function to test the login form"
Action:
tsx
export const SubmitLoginForm: Story = {
  args: { onSubmit: fn() },
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, args, step }) => {
    const canvas = within(canvasElement);

    await step('Fill in credentials', async () => {
      await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com');
      await userEvent.type(canvas.getByLabelText('Password'), 'password123');
    });

    await step('Submit the form', async () => {
      await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
    });

    await step('Verify submission', async () => {
      await expect(args.onSubmit).toHaveBeenCalledWith({
        email: 'user@example.com',
        password: 'password123',
      });
    });
  },
};

用户: "为登录表单添加play函数"
实现:
tsx
export const SubmitLoginForm: Story = {
  args: { onSubmit: fn() },
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, args, step }) => {
    const canvas = within(canvasElement);

    await step('填写凭证', async () => {
      await userEvent.type(canvas.getByLabelText('Email'), 'user@example.com');
      await userEvent.type(canvas.getByLabelText('Password'), 'password123');
    });

    await step('提交表单', async () => {
      await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
    });

    await step('验证提交结果', async () => {
      await expect(args.onSubmit).toHaveBeenCalledWith({
        email: 'user@example.com',
        password: 'password123',
      });
    });
  },
};

Example 2: Create a keyboard navigation test

示例2:创建键盘导航测试

User: "Add a story that tests keyboard navigation on the dropdown"
Action:
tsx
export const KeyboardNavigation: Story = {
  args: { options: mockOptions },
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Open dropdown with keyboard', async () => {
      const combobox = canvas.getByRole('combobox');
      combobox.focus();
      await userEvent.keyboard('{ArrowDown}');
    });

    await step('Navigate and select option', async () => {
      await userEvent.keyboard('{ArrowDown}');
      await userEvent.keyboard('{ArrowDown}');
      await userEvent.keyboard('{Enter}');
    });

    await step('Verify selection is displayed', async () => {
      const selected = canvas.getByRole('combobox');
      await expect(selected).toHaveTextContent('Option 2');
    });
  },
};

用户: "添加一个测试下拉框键盘导航的story"
实现:
tsx
export const KeyboardNavigation: Story = {
  args: { options: mockOptions },
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('用键盘打开下拉框', async () => {
      const combobox = canvas.getByRole('combobox');
      combobox.focus();
      await userEvent.keyboard('{ArrowDown}');
    });

    await step('导航并选择选项', async () => {
      await userEvent.keyboard('{ArrowDown}');
      await userEvent.keyboard('{ArrowDown}');
      await userEvent.keyboard('{Enter}');
    });

    await step('验证选中项已显示', async () => {
      const selected = canvas.getByRole('combobox');
      await expect(selected).toHaveTextContent('Option 2');
    });
  },
};

Example 3: Test error handling

示例3:测试错误处理

User: "Add a story for invalid email validation"
Action:
tsx
export const InvalidEmail: Story = {
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Enter invalid email', async () => {
      await userEvent.type(canvas.getByLabelText('Email'), 'not-an-email');
    });

    await step('Submit and verify error', async () => {
      await userEvent.click(canvas.getByRole('button', { name: /submit/i }));
      const errorMsg = await canvas.findByRole('alert');
      await expect(errorMsg).toHaveTextContent(/invalid email/i);
    });
  },
};

用户: "添加一个无效邮箱验证的story"
实现:
tsx
export const InvalidEmail: Story = {
  tags: ['test', 'interaction'],
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('输入无效邮箱', async () => {
      await userEvent.type(canvas.getByLabelText('Email'), 'not-an-email');
    });

    await step('提交并验证错误提示', async () => {
      await userEvent.click(canvas.getByRole('button', { name: /submit/i }));
      const errorMsg = await canvas.findByRole('alert');
      await expect(errorMsg).toHaveTextContent(/invalid email/i);
    });
  },
};

Example 4: Review a play function for best practices

示例4:评审play函数的最佳实践

User: "Review this play function"
Action: Check against these criteria:
CheckWhat to Look For
StructureUses
step()
to group logical actions
QueriesPrefers
getByRole
/
getByLabelText
over
getByTestId
AsyncAll
userEvent
calls and assertions are awaited
Async elementsUses
findBy
for elements that appear after interaction
TagsStory has
['test', 'interaction']
tags
AssertionsUses
expect
to verify outcomes, not just interactions
CanvasUses
within(canvasElement)
, not global queries
Common issues found in reviews:
tsx
// Missing await
userEvent.click(button);                    // Fix: await userEvent.click(button);

// Missing step() grouping
play: async ({ canvasElement }) => {        // Fix: wrap in step() calls
  const canvas = within(canvasElement);
  await userEvent.click(canvas.getByRole('button'));
  await expect(...).toBe(...);
};

// Using getBy for async-rendered elements
await userEvent.click(openBtn);
const menu = canvas.getByTestId('menu');    // Fix: await canvas.findByTestId('menu');

// Missing tags
export const MyTest: Story = {              // Fix: add tags: ['test', 'interaction']
  play: async ({ canvasElement }) => { ... },
};
用户: "评审这个play函数"
实现: 对照以下标准检查:
检查项检查内容
结构使用
step()
对逻辑操作进行分组
查询方法优先使用
getByRole
/
getByLabelText
而非
getByTestId
异步处理所有
userEvent
调用和断言都使用了await
异步元素对交互后才出现的元素使用
findBy
标签Story添加了
['test', 'interaction']
标签
断言使用
expect
验证结果,而非仅执行交互
Canvas使用
within(canvasElement)
,而非全局查询
评审中常见的问题:
tsx
// 缺少await
userEvent.click(button);                    // 修复:await userEvent.click(button);

// 缺少step()分组
play: async ({ canvasElement }) => {        // 修复:用step()调用包裹
  const canvas = within(canvasElement);
  await userEvent.click(canvas.getByRole('button'));
  await expect(...).toBe(...);
};

// 对异步渲染的元素使用getBy
await userEvent.click(openBtn);
const menu = canvas.getByTestId('menu');    // 修复:await canvas.findByTestId('menu');

// 缺少标签
export const MyTest: Story = {              // 修复:添加tags: ['test', 'interaction']
  play: async ({ canvasElement }) => { ... },
};

More Information

更多信息

See REFERENCE.md for detailed documentation including:
  • Complete query reference with examples
  • All common interaction patterns
  • Step function best practices
  • Code review checklist for play functions
  • Troubleshooting guide
请查看REFERENCE.md获取详细文档,包括:
  • 包含示例的完整查询参考
  • 所有常见交互模式
  • Step函数最佳实践
  • Play函数代码评审检查清单
  • 故障排除指南