Skip to main content
Technology & EngineeringTesting Services202 lines

Storybook Test

Storybook Test: component testing via play functions, interaction testing, accessibility checks, visual regression, test-runner, portable stories

Quick Summary9 lines
You are an expert in using Storybook's testing capabilities for component-level interaction testing, accessibility audits, and visual regression.

## Key Points

- Write play functions for every interactive story so the test runner catches regressions automatically; stories without play functions still verify the component renders without crashing.
- Use `step()` inside play functions to group related interactions, making the Interactions panel readable and failures easier to locate.
- Leverage `composeStories` to reuse story definitions in unit tests so you maintain a single source of truth for component states and avoid duplicating setup logic.
skilldb get testing-services-skills/Storybook TestFull skill: 202 lines
Paste into your CLAUDE.md or agent config

Storybook Test — Testing

You are an expert in using Storybook's testing capabilities for component-level interaction testing, accessibility audits, and visual regression.

Core Philosophy

Overview

Storybook Test turns your stories into executable tests. Each story already renders a component in isolation with specific props and state; play functions add user interactions and assertions on top. The @storybook/test package re-exports Testing Library and Vitest utilities (expect, fn, userEvent) so you write interactions directly inside stories. The @storybook/test-runner then executes every story in a real browser via Playwright, catching rendering errors, interaction failures, and (optionally) accessibility violations automatically. This means your component documentation and your tests are the same artifact.

Setup & Configuration

Installation

# Core testing utilities (included in Storybook 8+)
npm install -D @storybook/test

# Test runner for CI execution
npm install -D @storybook/test-runner

# Accessibility addon for a11y checks
npm install -D @storybook/addon-a11y

Storybook config (.storybook/main.ts)

import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(ts|tsx)"],
  addons: [
    "@storybook/addon-essentials",
    "@storybook/addon-a11y",
    "@storybook/addon-interactions",
  ],
  framework: "@storybook/react-vite",
};

export default config;

Test runner config (test-runner.ts)

import type { TestRunnerConfig } from "@storybook/test-runner";
import { checkA11yRules } from "@storybook/addon-a11y/test-utils";

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // Run axe accessibility checks on every story
    await checkA11yRules(page, context);
  },
};

export default config;

package.json scripts

{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "test-storybook": "test-storybook --url http://127.0.0.1:6006"
  }
}

Core Patterns

Interaction testing with play functions

// src/components/LoginForm.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn, userEvent, within } 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 FilledAndSubmitted: Story = {
  play: async ({ args, canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step("Fill in credentials", async () => {
      await userEvent.type(canvas.getByLabelText("Email"), "user@test.com");
      await userEvent.type(canvas.getByLabelText("Password"), "secret123");
    });

    await step("Submit the form", async () => {
      await userEvent.click(canvas.getByRole("button", { name: "Log in" }));
    });

    await expect(args.onSubmit).toHaveBeenCalledWith({
      email: "user@test.com",
      password: "secret123",
    });
  },
};

export const ValidationError: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Submit without filling fields
    await userEvent.click(canvas.getByRole("button", { name: "Log in" }));

    await expect(canvas.getByText("Email is required")).toBeInTheDocument();
  },
};

Portable stories (using stories in Vitest/Jest)

// src/components/LoginForm.test.tsx
import { composeStories } from "@storybook/react";
import { render, screen } from "@testing-library/react";
import * as stories from "./LoginForm.stories";

const { FilledAndSubmitted, ValidationError } = composeStories(stories);

test("submits credentials", async () => {
  const { container } = render(<FilledAndSubmitted />);
  await FilledAndSubmitted.play({ canvasElement: container });
  // assertions already ran inside the play function
});

test("shows validation errors", async () => {
  const { container } = render(<ValidationError />);
  await ValidationError.play({ canvasElement: container });
  expect(screen.getByText("Email is required")).toBeInTheDocument();
});

Mock modules and network requests

// src/components/UserProfile.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { expect, within } from "@storybook/test";
import { UserProfile } from "./UserProfile";

const meta: Meta<typeof UserProfile> = {
  component: UserProfile,
  // Use Storybook loaders to fetch/mock data before render
  loaders: [
    async () => ({
      user: { id: "1", name: "Alice", role: "admin" },
    }),
  ],
  render: (args, { loaded: { user } }) => <UserProfile user={user} {...args} />,
};
export default meta;
type Story = StoryObj<typeof UserProfile>;

export const AdminUser: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await expect(canvas.getByText("Alice")).toBeInTheDocument();
    await expect(canvas.getByText("admin")).toBeInTheDocument();
  },
};

Best Practices

  • Write play functions for every interactive story so the test runner catches regressions automatically; stories without play functions still verify the component renders without crashing.
  • Use step() inside play functions to group related interactions, making the Interactions panel readable and failures easier to locate.
  • Leverage composeStories to reuse story definitions in unit tests so you maintain a single source of truth for component states and avoid duplicating setup logic.

Common Pitfalls

  • Running test-storybook against a Storybook that is not already serving; the test runner requires a live Storybook instance (start it first or use --url with a CI workflow that builds a static Storybook).
  • Forgetting to await interactions and assertions inside play functions; every userEvent and expect call is async, and skipping await causes assertions to run before the interaction completes, producing false passes or flaky failures.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add testing-services-skills

Get CLI access →