Use this skill when the user asks to "set up testing", "configure tests for Storybook", "add interaction tests", "add accessibility tests", "set up a11y testing", "configure Vitest", "configure Playwright", mentions "play functions", "test-storybook", "component testing", or wants to add comprehensive testing to their Storybook setup. This skill provides guidance on modern Storybook 9 testing with Vitest, Playwright, and axe-core.
/plugin marketplace add flight505/storybook-assistant-plugin/plugin install flight505-storybook-assistant@flight505/storybook-assistant-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Set up and configure comprehensive testing for Storybook 9 components, including interaction tests with play functions, accessibility testing with axe-core, and visual regression testing with Playwright.
This skill provides guidance on implementing modern component testing patterns using Storybook 9's integrated testing capabilities.
Configure the right testing setup based on:
Set up and write interaction tests using:
Configure accessibility validation with:
Set up visual regression tests with:
Stories with args and controls only (no automated tests).
Best for:
Setup:
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button',
},
};
Stories with play functions that test user interactions.
Best for:
Setup:
import { expect, userEvent, within } from '@storybook/test';
export const WithInteraction: Story = {
args: { children: 'Click me' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await expect(button).toBeInTheDocument();
await userEvent.click(button);
await expect(button).toHaveFocus();
},
};
Stories with axe-core validation rules.
Best for:
Setup:
export const AccessibilityValidation: Story = {
args: { children: 'Button' },
parameters: {
a11y: {
config: {
rules: [
{ id: 'button-name', enabled: true },
{ id: 'color-contrast', enabled: true },
{ id: 'focus-visible', enabled: true },
],
},
},
},
};
Combination of interaction tests, accessibility tests, and visual regression.
Best for:
Storybook 9 uses Vitest as the default test runner:
Benefits:
Setup:
Add to package.json:
{
"scripts": {
"test-storybook": "test-storybook"
}
}
Real browser testing with Playwright:
Benefits:
Setup:
Storybook 9 includes Playwright by default. Configure in .storybook/test-runner-jest.config.js:
export default {
browsers: ['chromium', 'firefox', 'webkit'],
screenshot: 'only-on-failure',
video: 'retain-on-failure',
};
Query components using accessible selectors:
Recommended queries (in order of preference):
getByRole() - Semantic HTML rolesgetByLabelText() - Form labelsgetByPlaceholderText() - Input placeholdersgetByText() - Visible text contentgetByTestId() - Last resort onlyExample:
const button = canvas.getByRole('button', { name: /submit/i });
const input = canvas.getByLabelText('Email address');
const heading = canvas.getByRole('heading', { level: 1 });
export const ButtonInteraction: Story = {
args: {
onClick: fn(),
children: 'Click me',
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
// Test rendering
await expect(button).toBeInTheDocument();
// Test click
await userEvent.click(button);
await expect(args.onClick).toHaveBeenCalledTimes(1);
// Test disabled state
await expect(button).not.toBeDisabled();
},
};
export const InputInteraction: Story = {
args: {
label: 'Username',
onChange: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText('Username');
// Test typing
await userEvent.type(input, 'john.doe');
await expect(input).toHaveValue('john.doe');
// Test change handler
await expect(args.onChange).toHaveBeenCalled();
// Test validation
await userEvent.clear(input);
await expect(input).toHaveValue('');
},
};
export const ModalInteraction: Story = {
args: {
isOpen: true,
onClose: fn(),
title: 'Confirm Action',
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const dialog = canvas.getByRole('dialog');
// Test ARIA attributes
await expect(dialog).toHaveAttribute('aria-modal', 'true');
// Test focus trap
const firstButton = canvas.getAllByRole('button')[0];
firstButton.focus();
await expect(firstButton).toHaveFocus();
// Test ESC key
await userEvent.keyboard('{Escape}');
await expect(args.onClose).toHaveBeenCalled();
},
};
export const FormInteraction: Story = {
args: {
onSubmit: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
// Fill form
await userEvent.type(canvas.getByLabelText('Email'), 'test@example.com');
await userEvent.type(canvas.getByLabelText('Password'), 'password123');
// Submit
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));
// Verify submission
await expect(args.onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
},
};
export const WCAG_AA_Compliance: Story = {
parameters: {
a11y: {
config: {
rules: [
// Perceivable
{ id: 'color-contrast', enabled: true }, // 1.4.3
{ id: 'image-alt', enabled: true }, // 1.1.1
// Operable
{ id: 'button-name', enabled: true }, // 4.1.2
{ id: 'link-name', enabled: true }, // 4.1.2
{ id: 'focus-visible', enabled: true }, // 2.4.7
// Understandable
{ id: 'label', enabled: true }, // 3.3.2
{ id: 'valid-lang', enabled: true }, // 3.1.1
// Robust
{ id: 'aria-valid-attr', enabled: true }, // 4.1.2
{ id: 'aria-hidden-focus', enabled: true }, // 4.1.2
],
},
},
},
};
export const KeyboardNavigation: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Tab through interactive elements
await userEvent.tab();
const firstButton = canvas.getAllByRole('button')[0];
await expect(firstButton).toHaveFocus();
// Activate with Enter
await userEvent.keyboard('{Enter}');
// Activate with Space
await userEvent.keyboard(' ');
// Navigate with arrows (for radio/tabs)
await userEvent.keyboard('{ArrowRight}');
},
};
# Run all tests
npm run test-storybook
# Run specific story
npm run test-storybook -- --stories "Button/WithInteraction"
# Watch mode
npm run test-storybook -- --watch
# Debug mode
npm run test-storybook -- --debug
Add to GitHub Actions:
name: Storybook Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build-storybook
- run: npm run test-storybook
Storybook 9 includes V8 coverage (faster than Istanbul):
{
"scripts": {
"test-storybook": "test-storybook --coverage",
"test-storybook:ci": "test-storybook --coverage --coverageReporters=lcov"
}
}
View coverage report:
npm run test-storybook -- --coverage
# Coverage report: coverage/lcov-report/index.html
getByRole() for semantic queriesgetByTestId() unless necessaryIncrease timeout in story:
export const SlowLoading: Story = {
parameters: {
test: {
timeout: 10000, // 10 seconds
},
},
};
Use findBy queries for async elements:
const button = await canvas.findByRole('button'); // Waits up to 1s
Add explicit waits:
await waitFor(() => {
expect(canvas.getByText('Loaded')).toBeInTheDocument();
});