Skip to main content

Playwright Component Testing vs Storybook Testing: Component Tests 2026

·PkgPulse Team

TL;DR

Use Playwright component testing for fast, isolated component tests that run in real browsers. Use Storybook for visual development and design system documentation — and add Storybook test runner as a bonus. They serve different primary purposes: Playwright CT is a testing tool that can show visuals; Storybook is a development/documentation tool that can run tests. In 2026, many teams use both: Storybook for development + Playwright CT for CI testing.

Key Takeaways

  • Playwright CT: Real browser testing, 3x faster than Cypress CT, integrates with existing Playwright setup
  • Storybook: Visual development sandbox, test runner via @storybook/test-runner, interaction tests
  • Speed: Playwright CT (parallel, real browsers) vs Storybook runner (serial by default)
  • Coverage: Playwright CT has better code coverage support; Storybook adds visual regression
  • DX: Storybook has better visual feedback; Playwright CT has better test authoring
  • 2026 choice: Playwright CT for teams that already use Playwright; Storybook if you need visual docs

Downloads

PackageWeekly DownloadsTrend
@playwright/test~5M↑ Growing
@storybook/react~3M→ Stable
@storybook/test-runner~500K↑ Growing

Playwright Component Testing

npm install -D @playwright/experimental-ct-react
# Or for other frameworks:
npm install -D @playwright/experimental-ct-vue
npm install -D @playwright/experimental-ct-svelte
// playwright-ct.config.ts:
import { defineConfig, devices } from '@playwright/experimental-ct-react';

export default defineConfig({
  testDir: './src',
  testMatch: '**/*.ct.{ts,tsx}',
  use: {
    ctPort: 3100,
    ctViteConfig: {
      // Your Vite config here
    },
  },
  projects: [
    { name: 'chromium', use: devices['Desktop Chrome'] },
    { name: 'firefox', use: devices['Desktop Firefox'] },
  ],
});
// Button.ct.tsx — Playwright component test:
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('renders with correct text', async ({ mount }) => {
  const component = await mount(<Button variant="default">Click Me</Button>);
  await expect(component).toContainText('Click Me');
  await expect(component).toHaveClass(/bg-primary/);
});

test('calls onClick when clicked', async ({ mount }) => {
  let clicked = false;
  const component = await mount(
    <Button onClick={() => { clicked = true; }}>Click Me</Button>
  );
  await component.click();
  expect(clicked).toBe(true);
});

test('disabled button is not clickable', async ({ mount }) => {
  const component = await mount(<Button disabled>Disabled</Button>);
  await expect(component).toBeDisabled();
  await expect(component).toHaveAttribute('disabled');
});

test('visual snapshot', async ({ mount, page }) => {
  const component = await mount(
    <div className="p-4 bg-white">
      <Button variant="default">Default</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="destructive">Delete</Button>
    </div>
  );
  await expect(component).toHaveScreenshot('buttons.png');
});
// Testing with context providers:
test('modal with auth context', async ({ mount }) => {
  const component = await mount(
    <AuthProvider user={{ id: '1', name: 'John' }}>
      <DeleteConfirmDialog onDelete={() => {}} />
    </AuthProvider>
  );
  
  // Trigger the dialog:
  await component.getByRole('button', { name: 'Delete' }).click();
  
  // Verify dialog appeared:
  await expect(component.getByRole('dialog')).toBeVisible();
  await expect(component.getByText('Are you sure?')).toBeVisible();
});

Storybook: Visual Development + Testing

npx storybook@latest init  # Detects framework automatically
npm install -D @storybook/test-runner
// Button.stories.tsx — story definition:
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { expect, fn, within } from '@storybook/test';

const meta: Meta<typeof Button> = {
  title: 'UI/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: { control: 'select', options: ['default', 'outline', 'destructive', 'ghost'] },
    size: { control: 'select', options: ['default', 'sm', 'lg', 'icon'] },
  },
};
export default meta;
type Story = StoryObj<typeof Button>;

// Basic stories (visual documentation):
export const Default: Story = {
  args: { children: 'Button', variant: 'default' },
};

export const Outline: Story = {
  args: { children: 'Outline', variant: 'outline' },
};

export const Destructive: Story = {
  args: { children: 'Delete', variant: 'destructive' },
};

// Interaction test (runs in test runner AND browser):
export const ClickTest: Story = {
  args: {
    children: 'Click Me',
    onClick: fn(),
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button', { name: 'Click Me' });
    
    await button.click();
    await expect(args.onClick).toHaveBeenCalledOnce();
    await expect(button).toHaveFocus();
  },
};

// Accessibility test:
export const AccessibilityTest: Story = {
  args: { children: 'Accessible Button' },
  play: async ({ canvasElement }) => {
    // Storybook's a11y addon runs checks automatically
    // Additional manual checks:
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    await expect(button).toBeInTheDocument();
  },
};
# Run Storybook test runner:
npx storybook@latest dev -p 6006  # Start Storybook
npx test-storybook                # Run all play functions as tests

# Or in CI (headless):
npx concurrently -k -s first -n "SB,TEST" \
  "npx storybook dev --ci" \
  "npx wait-on tcp:6006 && npx test-storybook"

Speed Comparison

Component count: 50 components, 3 stories/tests each

Playwright CT:
  → Parallel execution (4 workers)
  → Total: 45s
  → Per component: ~300ms average

Storybook test runner:
  → Sequential by default
  → Total: 2m 30s
  → Per component: ~1s average

Playwright CT with visual snapshots:
  → Total: 1m 30s (snapshot generation is slower)

Storybook with Chromatic (cloud):
  → Parallel cloud runners
  → Total: ~30s (but paid, network dependent)

Feature Comparison

FeaturePlaywright CTStorybook Testing
Real browser✅ Chrome/FF/Safari✅ Via test runner
Visual snapshotstoHaveScreenshot✅ Chromatic
Interaction testsplay() functions
Visual development✅ Primary feature
Design system docs✅ Autodocs
A11y testingVia axe-playwright@storybook/addon-a11y
Code coverage✅ NativeLimited
CI speedFast (parallel)Slower (sequential)
Chromatic integration✅ First-class
Setup complexityLowMedium

Decision Guide

Use Playwright Component Testing if:
  → Team already uses Playwright for E2E
  → Want fast isolated component tests in CI
  → Don't need visual development sandbox
  → Code coverage is important

Use Storybook if:
  → Building a design system or component library
  → Designers need to interact with components
  → Need documentation for component API
  → Want visual regression with Chromatic

Use both (recommended for teams):
  → Storybook for development + documentation
  → Playwright CT for isolated unit tests in CI
  → Storybook test runner for integration tests
  → Saves writing tests twice for documented components

Skip component testing entirely if:
  → Small project, E2E tests cover enough
  → Solo developer, unit tests + E2E sufficient
  → Moving fast, will add later

Compare Playwright and Storybook download trends on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.