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 upgradeInstall the test package:
npm install --save-dev @storybook/testFor Vitest integration:
npm install --save-dev @storybook/experimental-addon-test vitest @vitest/browser playwrightWriting 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 storyargs— the story's current argsstep— 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-runnerAdd it to package.json:
{
"scripts": {
"test-storybook": "test-storybook"
}
}Run it while Storybook is open:
npm run storybook &
npm run test-storybookThe 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 runCoverage
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.