Storybook
03 / 03

Testing & Documentation

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 run

MSW 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