Component Testing August 15, 2026 14 min read

Playwright Component Testing: Test React, Vue & Svelte Components (2026)

Unit tests are too shallow. E2E tests are too slow. Playwright Component Testing fills the gap — mount real components in real browsers and test them in isolation with the same API you already know. This guide covers setup, code examples, and best practices for React, Vue, and Svelte.

Modern frontend applications are built from components. A Button, a DataTable, a SearchFilter, a Modal — each is a self-contained piece of UI with its own props, state, events, and rendering logic. Yet most teams test these components at the wrong level: either with shallow unit tests that mock the DOM away, or with full E2E tests that require a running backend and take minutes to execute.

Playwright Component Testing (CT) gives you a middle ground. It mounts your actual component — React, Vue, or Svelte — inside a real browser (Chromium, Firefox, or WebKit), lets you interact with it using Playwright's locator API, and runs fast enough to use in your development loop. No simulated DOM. No mocked browser APIs. Real rendering, real events, real cross-browser coverage.

This guide walks through everything you need: what component testing is, how to set up Playwright CT, and how to write tests for React, Vue, and Svelte components with full code examples. If you are new to Playwright's core API, start with our Playwright locators guide first, then come back here.


What Is Component Testing?

To understand where component testing fits, visualize the testing pyramid:

  1. Unit tests (base) — Test pure functions, utilities, and business logic in isolation. Fast, cheap, but cannot test rendering or user interactions.
  2. Component tests (middle) — Mount a single component with its real DOM, apply props, simulate user interactions, and assert on the rendered output. No backend, no routing, no full page — just the component.
  3. E2E tests (top) — Drive a full application through a browser. Cover complete user flows across multiple pages, API calls, and auth. Slowest and most expensive to maintain.

The gap between unit tests and E2E tests is where most bugs hide. A unit test for a form validation function might pass, but the form component itself might not display the error message correctly, or a CSS rule might hide the submit button on certain viewports. These are rendering and interaction bugs that only surface when the component is mounted in a real browser.

Component testing fills that gap. You get browser-level fidelity (real CSS, real layout, real events) at unit-test-like speed (no server, no database, no network). A typical component test suite runs in 2–5 seconds.

Key insight: Component tests are not a replacement for E2E tests. They complement them. Use component tests for interaction-heavy UI components (forms, tables, modals) and E2E tests for full user journeys (login, checkout, onboarding). See our Playwright best practices guide for more on test strategy.


Playwright Component Testing (CT) Overview

Playwright CT is an official Playwright feature that lets you mount individual components inside a real browser and test them using the full Playwright API — locators, assertions, screenshots, and more.

How it works under the hood

  1. You write a test file that imports a component and calls mount().
  2. Playwright CT uses Vite to bundle just that component (and its dependencies) into a minimal HTML page.
  3. It launches a real browser, navigates to that page, and gives you a Playwright Locator pointing at the mounted component.
  4. You interact with the component using the standard Playwright API — click(), fill(), getByRole(), expect().
conceptual flow
// 1. Import the component
import { Button } from './Button';

// 2. Mount it in a real browser
const component = await mount(<Button label="Click me" />);

// 3. Interact with it using Playwright API
await component.click();

// 4. Assert on the result
await expect(component).toContainText('Clicked!');

Because CT uses real browsers, you get features that simulated DOM environments (jsdom, happy-dom) cannot provide:

  • Real CSS rendering — test that styles actually apply, that layouts don't break, that responsive breakpoints work.
  • Native browser events — real clicks, real keyboard events, real focus management.
  • Cross-browser coverage — run the same tests in Chromium, Firefox, and WebKit.
  • Visual regression — take screenshots and compare them across runs.

Setting Up Playwright CT for React

Let's set up a React project with Playwright Component Testing from scratch. The process takes about two minutes.

Step 1: Initialize the project

terminal
# Create a new React + Vite project (or use your existing one)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

# Install Playwright CT
npm init playwright@latest -- --ct

The --ct flag tells Playwright to scaffold the component testing configuration instead of the standard E2E setup. It creates three key files:

Step 2: Configure ct.config.ts

playwright-ct.config.ts
import { defineConfig, devices } from '@playwright/experimental-ct-react';

export default defineConfig({
  testDir: './src',
  testMatch: '**/*.ct.{ts,tsx}',

  /* Maximum time one test can run */
  timeout: 10_000,

  /* Run tests in parallel */
  fullyParallel: true,

  /* Reporter */
  reporter: 'html',

  /* Shared settings for all projects */
  use: {
    ctPort: 3100,
    ctViteConfig: {
      /* Pass Vite config options here */
      resolve: {
        alias: {
          '@': './src',
        },
      },
    },
  },

  /* Test against multiple browsers */
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

Step 3: Create the index file

Playwright CT needs an entry point to render components. The scaffolding creates playwright/index.html and playwright/index.tsx:

playwright/index.tsx
// This file is the component test entry point.
// Import global styles, providers, or context here.

import '../src/index.css';

// If your components use a theme provider or router:
// import { ThemeProvider } from './ThemeProvider';
// import { beforeMount } from '@playwright/experimental-ct-react/hooks';
//
// beforeMount(async ({ App }) => {
//   return <ThemeProvider><App /></ThemeProvider>;
// });

Vite integration: Playwright CT uses Vite under the hood. This means hot module replacement (HMR) during development, fast bundling, and support for TypeScript, JSX, CSS modules, and Tailwind CSS out of the box. If your project already uses Vite, CT will respect your existing vite.config.ts settings.


Writing Your First Component Test

Let's start with a simple Button component and write a complete test for it.

The component

src/components/Button.tsx
import { useState } from 'react';

interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary';
  onClick?: () => void;
}

export function Button({ label, variant = 'primary', onClick }: ButtonProps) {
  const [clicked, setClicked] = useState(false);

  const handleClick = () => {
    setClicked(true);
    onClick?.();
  };

  return (
    <button
      className={`btn btn-${variant}`}
      onClick={handleClick}
    >
      {clicked ? 'Clicked!' : label}
    </button>
  );
}

The test

src/components/Button.ct.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';

test('renders with the correct label', async ({ mount }) => {
  const component = await mount(<Button label="Submit" />);
  await expect(component).toContainText('Submit');
});

test('changes text on click', async ({ mount }) => {
  const component = await mount(<Button label="Submit" />);

  // Before click
  await expect(component).toContainText('Submit');

  // Click the button
  await component.click();

  // After click
  await expect(component).toContainText('Clicked!');
});

test('calls onClick callback', async ({ mount }) => {
  let callCount = 0;
  const component = await mount(
    <Button label="Submit" onClick={() => callCount++} />
  );

  await component.click();
  expect(callCount).toBe(1);
});

test('applies variant class', async ({ mount }) => {
  const component = await mount(
    <Button label="Cancel" variant="secondary" />
  );
  await expect(component.locator('button')).toHaveClass(/btn-secondary/);
});

Run the tests

terminal
npx playwright test --config playwright-ct.config.ts

That's it. Four tests, real browser execution, and you never had to start a dev server or mock the DOM.


Testing React Components

React components introduce complexity through state, props, context, and event handlers. Let's cover the most common patterns.

Testing forms with validation

LoginForm.ct.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { LoginForm } from './LoginForm';

test('shows validation errors for empty fields', async ({ mount, page }) => {
  const component = await mount(<LoginForm />);

  // Submit without filling fields
  await component.getByRole('button', { name: 'Sign In' }).click();

  // Validation errors should appear
  await expect(component.getByText('Email is required')).toBeVisible();
  await expect(component.getByText('Password is required')).toBeVisible();
});

test('submits valid form data', async ({ mount }) => {
  let submittedData: any = null;
  const component = await mount(
    <LoginForm onSubmit={(data) => submittedData = data} />
  );

  // Fill in the form
  await component.getByLabel('Email').fill('user@example.com');
  await component.getByLabel('Password').fill('securePass123');

  // Submit
  await component.getByRole('button', { name: 'Sign In' }).click();

  // Verify callback received correct data
  expect(submittedData).toEqual({
    email: 'user@example.com',
    password: 'securePass123',
  });
});

Testing with context providers

Many React components depend on context (theme, auth, i18n). Use the beforeMount hook to wrap components in providers:

playwright/index.tsx — global provider wrapping
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import { ThemeProvider } from '../src/context/ThemeProvider';
import { I18nProvider } from '../src/context/I18nProvider';
import '../src/index.css';

beforeMount(async ({ App }) => {
  return (
    <ThemeProvider theme="light">
      <I18nProvider locale="en">
        <App />
      </I18nProvider>
    </ThemeProvider>
  );
});

Testing state changes and re-renders

Counter.ct.tsx
test('increments and decrements counter', async ({ mount }) => {
  const component = await mount(<Counter initialValue={5} />);

  // Verify initial state
  await expect(component.getByTestId('count')).toHaveText('5');

  // Increment three times
  await component.getByRole('button', { name: '+' }).click();
  await component.getByRole('button', { name: '+' }).click();
  await component.getByRole('button', { name: '+' }).click();
  await expect(component.getByTestId('count')).toHaveText('8');

  // Decrement once
  await component.getByRole('button', { name: '-' }).click();
  await expect(component.getByTestId('count')).toHaveText('7');
});

Testing Vue Components

Playwright CT supports Vue 3 with the @playwright/experimental-ct-vue package. The API is nearly identical to the React version — the only difference is how you pass props and listen to events.

Setup for Vue

terminal
npm install -D @playwright/experimental-ct-vue

Testing a Vue SFC with props and emits

SearchFilter.ct.ts
import { test, expect } from '@playwright/experimental-ct-vue';
import SearchFilter from './SearchFilter.vue';

test('renders with placeholder text', async ({ mount }) => {
  const component = await mount(SearchFilter, {
    props: {
      placeholder: 'Search products...',
      debounceMs: 0,
    },
  });

  await expect(
    component.getByRole('textbox')
  ).toHaveAttribute('placeholder', 'Search products...');
});

test('emits search event on input', async ({ mount }) => {
  const emitted: string[] = [];
  const component = await mount(SearchFilter, {
    props: { placeholder: 'Search...', debounceMs: 0 },
    on: {
      search(value: string) {
        emitted.push(value);
      },
    },
  });

  await component.getByRole('textbox').fill('laptop');
  expect(emitted).toContain('laptop');
});

test('renders slot content', async ({ mount }) => {
  const component = await mount(SearchFilter, {
    props: { placeholder: 'Search...' },
    slots: {
      prefix: '<span data-testid="icon">🔍</span>',
    },
  });

  await expect(component.getByTestId('icon')).toBeVisible();
});

Notice how Vue CT uses an object syntax for mount() instead of JSX. You pass props, on (event listeners), and slots as separate keys. This mirrors Vue's component API and makes it easy to test emits without wrapping the component in a parent.


Testing Svelte Components

Svelte support is available through @playwright/experimental-ct-svelte. Svelte's reactive model and event dispatching translate cleanly into Playwright CT.

terminal
npm install -D @playwright/experimental-ct-svelte

Testing a Svelte component with reactive state

Toggle.ct.ts
import { test, expect } from '@playwright/experimental-ct-svelte';
import Toggle from './Toggle.svelte';

test('renders in off state by default', async ({ mount }) => {
  const component = await mount(Toggle, {
    props: { label: 'Dark mode' },
  });

  await expect(component.getByRole('switch')).not.toBeChecked();
  await expect(component.getByText('Off')).toBeVisible();
});

test('toggles on click', async ({ mount }) => {
  const component = await mount(Toggle, {
    props: { label: 'Dark mode' },
  });

  await component.getByRole('switch').click();

  await expect(component.getByRole('switch')).toBeChecked();
  await expect(component.getByText('On')).toBeVisible();
});

test('dispatches change event', async ({ mount }) => {
  let newValue: boolean | null = null;
  const component = await mount(Toggle, {
    props: { label: 'Dark mode' },
    on: {
      change(value: boolean) {
        newValue = value;
      },
    },
  });

  await component.getByRole('switch').click();
  expect(newValue).toBe(true);
});

Svelte's event dispatching maps directly to the on option in mount(), just like Vue. The Playwright locator API works identically across all three frameworks — once you learn the pattern, switching frameworks in your test files is trivial.


Component Testing vs Vitest/Jest

The most common question teams ask: "Why not just use Vitest with Testing Library?" The answer comes down to what you are testing and how much fidelity you need.

Criteria Playwright CT Vitest / Jest + Testing Library
DOM environment Real browser (Chromium, Firefox, WebKit) Simulated (jsdom or happy-dom)
CSS rendering Full CSS support, real layout No CSS rendering at all
Visual regression Built-in screenshot comparison Not possible
Cross-browser Chromium + Firefox + WebKit Single simulated environment
Speed per test ~50–200ms (browser overhead) ~5–20ms (in-process)
Native events Real mouse, keyboard, touch events Simulated via fireEvent / userEvent
API familiarity Same as Playwright E2E tests Testing Library API (different from E2E)
Setup complexity Moderate (Vite + browser install) Low (npm install + config)

Practical guidance: Use Vitest for testing pure logic (utility functions, hooks, stores) and Playwright CT for testing interactive UI components (forms, tables, modals, dropdowns). The two tools complement each other — you do not have to choose one or the other. For a deeper look at structuring tests, see our Playwright fixtures and hooks guide.


Mocking Dependencies in Component Tests

Components rarely exist in isolation. They call APIs, import child components, and read from context. Playwright CT gives you several strategies for mocking these dependencies.

Mocking API calls with route interception

UserProfile.ct.tsx — mocking fetch
import { test, expect } from '@playwright/experimental-ct-react';
import { UserProfile } from './UserProfile';

test('displays user data from API', async ({ mount, page }) => {
  // Intercept the API call before mounting
  await page.route('**/api/user/42', async (route) => {
    await route.fulfill({
      json: {
        id: 42,
        name: 'Jane Doe',
        email: 'jane@example.com',
        role: 'Admin',
      },
    });
  });

  const component = await mount(<UserProfile userId={42} />);

  await expect(component.getByText('Jane Doe')).toBeVisible();
  await expect(component.getByText('Admin')).toBeVisible();
});

test('shows error state on API failure', async ({ mount, page }) => {
  await page.route('**/api/user/42', async (route) => {
    await route.fulfill({ status: 500 });
  });

  const component = await mount(<UserProfile userId={42} />);

  await expect(
    component.getByText('Failed to load user')
  ).toBeVisible();
});

Because Playwright CT runs in a real browser, you can use page.route() to intercept any network request the component makes — exactly like you would in an E2E test. This is far more realistic than mocking fetch at the module level.

Wrapping with custom providers

For components that depend on React Context, Redux, Zustand, or other state management:

CartBadge.ct.tsx — custom wrapper
import { test, expect } from '@playwright/experimental-ct-react';
import { CartBadge } from './CartBadge';
import { CartProvider } from '../context/CartContext';

test('shows item count from cart context', async ({ mount }) => {
  const component = await mount(
    <CartProvider initialItems={[
      { id: 1, name: 'Widget', qty: 3 },
      { id: 2, name: 'Gadget', qty: 1 },
    ]}>
      <CartBadge />
    </CartProvider>
  );

  await expect(component.getByTestId('cart-count')).toHaveText('4');
});

Component Testing Best Practices

After testing hundreds of components across React, Vue, and Svelte projects, these patterns consistently produce fast, reliable test suites.

Do

Test one component per file. Keep tests focused on a single component's behavior. If a test needs multiple components, it might be an E2E test.

Don't

Don't test implementation details. Never assert on internal state variables, ref values, or hook return values. Test what the user sees and interacts with.

Do

Use semantic locators: getByRole, getByLabel, getByText. These mirror real user behavior and validate accessibility. See our locators guide.

Don't

Don't use CSS class selectors or DOM structure. locator('.card > .header > span.count') breaks when markup changes.

Do

Mock external dependencies (API calls, timers) to keep tests fast and deterministic. Use page.route() for network mocking.

Don't

Don't let component tests hit real APIs. Flaky network responses cause flaky tests. Reserve real API calls for E2E tests.

What to test at the component level

Good candidates for component tests

  • Form validation and error messages
  • Conditional rendering based on props
  • User interactions (click, type, select)
  • Keyboard navigation and focus management
  • Loading and error states
  • Responsive layout at different viewports
  • Accessibility (ARIA attributes, roles)
  • Visual regression (screenshot comparison)

What to leave for E2E tests

  • Multi-page user flows (login, checkout, onboarding)
  • Real API integrations and database state
  • Authentication and authorization flows
  • Browser navigation (back/forward, deep links)
  • Third-party integrations (payment gateways, OAuth)

Speed tip: Run component tests in parallel with fullyParallel: true in your config. Since each test mounts its own isolated component, there are no shared state conflicts. A suite of 100 component tests should complete in under 30 seconds. For more on test assertions, see our Playwright assertions guide.


Component Testing with Claude AI

Writing component tests is repetitive. You look at the component code, identify the props, figure out the states, and write tests for each combination. This is exactly the kind of task that Claude AI excels at.

In the Playwright + Claude AI & MCP Server course, you learn how to use Claude AI to:

  • Generate component tests from component code: Paste a React, Vue, or Svelte component into Claude and get a complete test file with all edge cases covered.
  • Identify missing test cases: Claude analyzes your existing tests and suggests gaps — error states you forgot, keyboard interactions you skipped, accessibility checks you missed.
  • Convert between frameworks: Have React component tests? Claude can generate the equivalent Vue or Svelte tests from your component code.
  • Debug failing tests: Paste a test failure and the component code, and Claude explains why it fails and how to fix it.
  • Generate mock data: Claude creates realistic mock API responses for your page.route() interceptors.

Frequently Asked Questions

Is Playwright Component Testing production-ready in 2026?

Playwright CT has matured significantly since its experimental launch. As of 2026, it is stable for React, Vue, and Svelte projects using Vite. Many teams use it in production CI pipelines alongside their E2E Playwright tests. Always check the official Playwright release notes for the latest status on your specific framework and bundler combination.

Can I use Playwright Component Testing with Next.js?

Playwright CT works best with Vite-based setups. For Next.js projects (which use Webpack or Turbopack), you can still test individual React components by extracting them into a Vite-compatible test environment, or by using Playwright CT's experimental Webpack support. Many teams test shared UI components with Playwright CT and reserve Next.js-specific pages for full E2E tests.

How is Playwright CT different from Vitest or Jest?

Vitest and Jest run component tests in a simulated DOM (jsdom or happy-dom), which cannot replicate real browser behavior like CSS rendering, viewport changes, or native events. Playwright CT mounts your component inside a real browser, giving you pixel-accurate rendering, real user interactions, and cross-browser coverage. The trade-off is slightly slower execution per test.

Can I run component tests and E2E tests in the same project?

Yes. Playwright supports separate configuration files for component tests (playwright-ct.config.ts) and E2E tests (playwright.config.ts). You can run them independently using different npm scripts, or run both in your CI pipeline. They share the same assertion library, locator API, and test runner.

What components should I test with Playwright CT vs E2E tests?

Use Playwright CT for isolated UI components with complex interaction logic: forms with validation, data tables with sorting, modals, dropdowns, and date pickers. Use E2E tests for full user flows that span multiple pages, involve API calls, authentication, or routing. If you can test it without a running backend, it is a good candidate for component testing.


Component Testing Checklist

  • Install @playwright/experimental-ct-react (or vue/svelte)
  • Create playwright-ct.config.ts with Vite integration
  • Set up playwright/index.tsx with global providers
  • Write tests using mount() + Playwright locators
  • Mock API calls with page.route()
  • Use semantic locators (getByRole, getByLabel)
  • Run in parallel with fullyParallel: true
  • Add visual regression with screenshot assertions

Playwright Component Testing bridges the gap between shallow unit tests and slow E2E tests. You get real browser fidelity at near-unit-test speed, with an API you already know from your E2E tests. Start with the components that have the most user interaction — forms, tables, modals — and expand from there. For a comprehensive testing strategy that combines component tests with E2E tests, check our Playwright best practices guide and the fixtures and hooks guide.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn