Storybook 8 Component Testing: The Complete Guide

Storybook 8 Component Testing: The Complete Guide

Storybook 8 made component testing a first-class citizen. What was previously a documentation tool is now a full component testing platform — complete with built-in test runner, Vitest integration, and portable stories that run outside Storybook itself.

This guide covers everything: setting up Storybook 8 for testing, writing stories that double as tests, using play functions for interactions, running tests in CI, and integrating with Vitest.

What Changed in Storybook 8 for Testing

Storybook 8 shipped three major testing improvements:

@storybook/test replaces @storybook/jest and @storybook/testing-library. A single package now provides expect, userEvent, within, and all the utilities you need for interaction testing.

Vitest integration via @storybook/experimental-addon-test. Stories can now run as Vitest tests without a separate Storybook server. This means component tests run in your existing vitest pipeline.

Portable stories are stable. The composeStory and composeStories APIs let you import and render stories in any test environment — Jest, Vitest, or Playwright.

Installation

Start with a fresh Storybook 8 install or upgrade from 7:

# New project
npx storybook@latest init

<span class="hljs-comment"># Upgrade from v7
npx storybook@latest upgrade

Install the test package:

npm install --save-dev @storybook/test

For Vitest integration:

npm install --save-dev @storybook/experimental-addon-test vitest @vitest/browser playwright

Writing Your First Testable Story

A story is a component rendered in a specific state. In Storybook 8, every story is a potential test.

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  args: {
    label: 'Click me',
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    variant: 'primary',
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
  },
};

These stories render your component with specific props and are immediately usable in visual regression tools like Chromatic. But to make them real tests, add a play function.

Play Functions: Interactions as Tests

A play function runs after a story renders. Use it to simulate user interactions and assert outcomes.

import { expect, userEvent, within } from '@storybook/test';

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

    // Find the input and type into it
    const emailInput = canvas.getByLabelText('Email');
    await userEvent.type(emailInput, 'user@example.com');

    // Click submit
    const submitBtn = canvas.getByRole('button', { name: 'Submit' });
    await userEvent.click(submitBtn);

    // Assert the success message appears
    await expect(canvas.getByText('Thank you!')).toBeInTheDocument();
  },
};

The play function receives:

  • canvasElement — the DOM node containing your story
  • args — the story's current args
  • step — helper for grouping assertions into named steps

Using step for Readable Tests

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

    await step('Fill in address', async () => {
      await userEvent.type(canvas.getByLabelText('Street'), '123 Main St');
      await userEvent.type(canvas.getByLabelText('City'), 'Springfield');
    });

    await step('Submit order', async () => {
      await userEvent.click(canvas.getByRole('button', { name: 'Place Order' }));
    });

    await step('Verify confirmation', async () => {
      await expect(canvas.getByText('Order confirmed')).toBeVisible();
    });
  },
};

Steps appear in the Storybook Interactions panel as collapsible sections — much easier to debug than a flat sequence of events.

Running Tests with the Test Runner

Install the official test runner:

npm install --save-dev @storybook/test-runner

Add it to package.json:

{
  "scripts": {
    "test-storybook": "test-storybook"
  }
}

Run it while Storybook is open:

npm run storybook &
npm run test-storybook

The test runner visits every story, executes play functions, and reports failures as standard test output. It uses Playwright under the hood, so it runs in real browsers.

Vitest Integration (Experimental)

The @storybook/experimental-addon-test addon enables running stories as Vitest tests without a running Storybook server.

Add it to .storybook/main.ts:

export default {
  addons: ['@storybook/experimental-addon-test'],
};

Configure Vitest in vitest.config.ts:

import { defineConfig } from 'vitest/config';
import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';

export default defineConfig({
  plugins: [storybookTest()],
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      name: 'chromium',
    },
  },
});

Now vitest run picks up your stories automatically — no separate test-storybook command needed.

Portable Stories

Portable stories let you import and render stories in external test environments. This is useful when you want to test a component story inside a full integration test.

// Button.test.ts (Vitest or Jest)
import { composeStory } from '@storybook/react';
import meta, { Primary } from './Button.stories';

const PrimaryStory = composeStory(Primary, meta);

test('renders primary button', async () => {
  const { getByRole } = await PrimaryStory.run();
  expect(getByRole('button')).toHaveTextContent('Click me');
});

composeStory applies all decorators, loaders, and args from the meta and story — your story runs in exactly the same context as in Storybook.

CI Setup

GitHub Actions with test-storybook

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
      - name: Build Storybook
        run: npm run build-storybook -- --quiet
      - name: Run Storybook Tests
        run: |
          npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
            "npx http-server storybook-static --port 6006 --silent" \
            "npx wait-on tcp:6006 && npm run test-storybook"

GitHub Actions with Vitest

If you're using the Vitest integration, the setup is simpler:

- run: npm ci
- run: npx playwright install chromium
- run: npx vitest run

Coverage

Storybook 8 supports Istanbul-based coverage collection through the Vitest integration. Add @vitest/coverage-istanbul and configure:

test: {
  coverage: {
    provider: 'istanbul',
    include: ['src/**/*.{ts,tsx}'],
  },
}

When Storybook Testing Makes Sense

Storybook component testing is the right choice when:

  • Your components have complex interaction logic (forms, wizards, dropdowns)
  • You need visual documentation alongside tests
  • You want to catch regressions across multiple component states simultaneously
  • Your team uses a design system that needs verified renders

It's less useful for:

  • Pure business logic (use unit tests)
  • Full user journeys across multiple pages (use Playwright E2E)
  • APIs and backend behavior (use integration tests)

The sweet spot is the middle layer: real components, real browser, no full application. Storybook 8 makes that layer fast and easy to maintain.

Read more