Loading...
Loading...
Use when creating Storybook play functions, writing interaction tests in stories, or reviewing play function code in pull requests. Ensures consistent structure, proper query priorities, correct async handling, and best practices for Storybook interaction testing.
npx skill4agent add peterknezek/skills storybook-interactionsexport 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' })]),
);
});
},
};canvasElementargsstepwithin(canvasElement)step()await['test', 'interaction']| 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 |
// 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// 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 awaitexport 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',
});
});
},
};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');
});
},
};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);
});
},
};| Check | What to Look For |
|---|---|
| Structure | Uses |
| Queries | Prefers |
| Async | All |
| Async elements | Uses |
| Tags | Story has |
| Assertions | Uses |
| Canvas | Uses |
// 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 }) => { ... },
};