Testing & Documentation
Storybook integrates interaction testing, accessibility auditing, and auto-generated docs. Stories double as tests — play functions simulate user interactions, and the test runner asserts on outcomes.
Play Functions — Interaction Testing
// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within, waitFor } from '@storybook/test';
import { LoginForm } from './LoginForm';
const meta: Meta<typeof LoginForm> = {
component: LoginForm,
args: { onSubmit: fn() },
};
export default meta;
type Story = StoryObj<typeof LoginForm>;
export const SuccessfulSubmit: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Interact with form elements
await userEvent.type(
canvas.getByLabelText('Email'),
'alice@example.com',
{ delay: 50 } // simulate real typing speed
);
await userEvent.type(
canvas.getByLabelText('Password'),
'password123'
);
await userEvent.click(
canvas.getByRole('button', { name: /sign in/i })
);
// Assert
await waitFor(() => expect(args.onSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'password123',
}));
},
};
export const ValidationErrors: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Submit without filling in the form
await userEvent.click(canvas.getByRole('button', { name: /sign in/i }));
// Expect validation messages
await expect(canvas.getByText('Email is required')).toBeInTheDocument();
await expect(canvas.getByText('Password is required')).toBeInTheDocument();
},
};Storybook Test Runner & CI
# Install test runner
npm install --save-dev @storybook/test-runner
# Run all play functions as tests
npx storybook test
# Run specific story
npx storybook test --stories="**/Button.stories.*"
# With coverage
npx storybook test --coverage
# In CI — start storybook, then test
# package.json scripts:
# test:storybook: storybook dev -p 6006 --ci & wait-on tcp:6006 && storybook test
# Or use concurrently
# concurrently -k -s=first 'storybook dev -p 6006 --ci' 'wait-on tcp:6006 && storybook test'Accessibility Testing
// @storybook/addon-a11y — runs axe-core on every story
// Install: npm install --save-dev @storybook/addon-a11y
// Add to .storybook/main.ts addons array: '@storybook/addon-a11y'
// Configure per story
export const InaccessibleButton: Story = {
args: { children: 'Click', 'aria-label': undefined },
parameters: {
a11y: {
// Override axe rules
config: {
rules: [
{ id: 'button-name', enabled: true },
{ id: 'color-contrast', enabled: true },
],
},
// Expected violations (use sparingly)
options: { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] } },
},
},
};
// Disable a11y check for a known issue
export const KnownIssue: Story = {
parameters: {
a11y: { disable: true },
},
};
// Run a11y tests in CI
// storybook test --includeStories='.*'
// All a11y violations will fail the test runMSW API Mocking & Mock Providers
// Mock Service Worker in Storybook — msw-storybook-addon
// Install: npm install --save-dev msw msw-storybook-addon
// .storybook/preview.tsx — initialize MSW
import { initialize, mswLoader } from 'msw-storybook-addon';
initialize({ onUnhandledRequest: 'bypass' });
const preview: Preview = {
loaders: [mswLoader],
// ...
};
// UserProfile.stories.tsx — mock API per story
import { http, HttpResponse, delay } from 'msw';
export const LoadedProfile: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users/:id', async ({ params }) => {
await delay(300); // simulate network latency
return HttpResponse.json({
id: params.id,
name: 'Alice Smith',
email: 'alice@example.com',
role: 'admin',
avatar: 'https://i.pravatar.cc/150?u=alice',
});
}),
],
},
},
};
export const ErrorState: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/users/:id', () =>
HttpResponse.json({ message: 'Not found' }, { status: 404 })
),
],
},
},
};Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free