storybook-interactions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStorybook 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
核心规则
- Always destructure ,
canvasElement, andargsfrom the play function argumentstep - Always call as the first line
within(canvasElement) - Wrap logical groups of actions in for test reporting
step() - Always user interactions and assertions
await - Add tags to stories with play functions
['test', 'interaction']
- 始终解构 play函数参数中的、
canvasElement和argsstep - 始终调用 作为第一行代码
within(canvasElement) - 将逻辑操作分组 用包裹,便于测试报告展示
step() - 始终使用await 处理用户交互和断言
- 添加标签 为包含play函数的stories添加标签
['test', 'interaction']
Query Priority Order
查询优先级顺序
Use queries in this order of preference:
| Priority | Query | Use When |
|---|---|---|
| 1st | | Element has an accessible role (button, textbox, etc.) |
| 2nd | | Form elements with associated labels |
| 3rd | | Inputs with placeholder text |
| Last | | 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请按以下优先级使用查询方法:
| 优先级 | 查询方法 | 适用场景 |
|---|---|---|
| 第一优先级 | | 元素具备可访问角色(如按钮、文本框等) |
| 第二优先级 | | 关联了标签的表单元素 |
| 第三优先级 | | 带有占位文本的输入框 |
| 最后选择 | | 没有可用的可访问查询方法时 |
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 awaittsx
// 在会渲染新元素的交互后,使用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); // 错误 - 缺少awaitExamples
示例
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:
| Check | What to Look For |
|---|---|
| Structure | Uses |
| Queries | Prefers |
| Async | All |
| Async elements | Uses |
| Tags | Story has |
| Assertions | Uses |
| Canvas | Uses |
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函数"
实现: 对照以下标准检查:
| 检查项 | 检查内容 |
|---|---|
| 结构 | 使用 |
| 查询方法 | 优先使用 |
| 异步处理 | 所有 |
| 异步元素 | 对交互后才出现的元素使用 |
| 标签 | Story添加了 |
| 断言 | 使用 |
| Canvas | 使用 |
评审中常见的问题:
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函数代码评审检查清单
- 故障排除指南