Skip to main content

Overview

Playwright Component Testing allows you to test UI components in isolation within a real browser environment. Test React, Vue, Svelte, and other framework components with the same Playwright APIs you use for end-to-end testing.

Getting Started

Installation

Install Playwright Component Testing for your framework:
npm init playwright@latest -- --ct
This creates a playwright-ct.config.ts file and installs the necessary dependencies.

Project Structure

my-app/
├── src/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── Button.test.tsx
│   └── ...
├── playwright-ct.config.ts
└── package.json

React Component Testing

Basic Component Test

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

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

Test Component Props

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

test('display user information', async ({ mount }) => {
  const component = await mount(
    <UserCard 
      name="John Doe"
      email="[email protected]"
      role="Admin"
    />
  );
  
  await expect(component.getByText('John Doe')).toBeVisible();
  await expect(component.getByText('[email protected]')).toBeVisible();
  await expect(component.getByText('Admin')).toBeVisible();
});

Test Component State

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

test('counter increments', async ({ mount }) => {
  const component = await mount(<Counter initialValue={0} />);
  
  await expect(component.getByText('Count: 0')).toBeVisible();
  
  await component.getByRole('button', { name: 'Increment' }).click();
  await expect(component.getByText('Count: 1')).toBeVisible();
  
  await component.getByRole('button', { name: 'Increment' }).click();
  await expect(component.getByText('Count: 2')).toBeVisible();
});

Vue Component Testing

Basic Vue Component Test

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

test('todo item', async ({ mount }) => {
  const component = await mount(TodoItem, {
    props: {
      text: 'Buy groceries',
      completed: false,
    },
  });
  
  await expect(component.getByText('Buy groceries')).toBeVisible();
  await expect(component.locator('.completed')).not.toBeVisible();
  
  // Mark as completed
  await component.getByRole('checkbox').check();
  await expect(component.locator('.completed')).toBeVisible();
});

Test Vue Events

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

test('search event', async ({ mount }) => {
  let searchQuery = '';
  
  const component = await mount(SearchBox, {
    on: {
      search: (query: string) => {
        searchQuery = query;
      },
    },
  });
  
  await component.getByRole('textbox').fill('playwright');
  await component.getByRole('button', { name: 'Search' }).click();
  
  expect(searchQuery).toBe('playwright');
});

Advanced Testing Patterns

Test with Context

Provide context/store to components:
import { test, expect } from '@playwright/experimental-ct-react';
import { ThemeProvider } from './ThemeContext';
import Button from './Button';

test('themed button', async ({ mount }) => {
  const component = await mount(
    <ThemeProvider theme="dark">
      <Button>Click me</Button>
    </ThemeProvider>
  );
  
  await expect(component).toHaveClass(/dark-theme/);
});

Test Hooks and Lifecycle

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

test('fetch data on mount', async ({ mount, page }) => {
  // Mock API response
  await page.route('**/api/data', (route) => {
    route.fulfill({
      status: 200,
      body: JSON.stringify({ message: 'Hello from API' }),
    });
  });
  
  const component = await mount(<DataFetcher />);
  
  // Wait for loading to complete
  await expect(component.getByText('Loading...')).toBeVisible();
  await expect(component.getByText('Hello from API')).toBeVisible();
});

Test Component Interactions

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

test('form validation', async ({ mount }) => {
  const component = await mount(<Form />);
  
  // Submit empty form
  await component.getByRole('button', { name: 'Submit' }).click();
  await expect(component.getByText('Name is required')).toBeVisible();
  
  // Fill valid data
  await component.getByLabel('Name').fill('John Doe');
  await component.getByLabel('Email').fill('[email protected]');
  await component.getByRole('button', { name: 'Submit' }).click();
  
  // Verify success
  await expect(component.getByText('Form submitted')).toBeVisible();
});

Configuration

Component Testing Config

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

export default defineConfig({
  testDir: './src',
  testMatch: '**/*.test.{ts,tsx}',
  
  use: {
    ctPort: 3100,
    ctViteConfig: {
      // Vite configuration
    },
  },
  
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Run Component Tests

1

Run all component tests

npm run test:ct
2

Run in UI mode

npm run test:ct -- --ui
3

Run specific test file

npm run test:ct Button.test.tsx
4

Debug component tests

npm run test:ct -- --debug

Best Practices

Test User Behavior: Focus on testing how users interact with components rather than implementation details.
  • Test in isolation: Component tests should focus on a single component
  • Mock external dependencies: Use route handlers to mock API calls
  • Test accessibility: Verify components are accessible with screen readers
  • Test edge cases: Verify components handle empty states and errors
  • Keep tests fast: Component tests should run quickly
  • Use realistic data: Test with data that matches production scenarios
Component testing runs in a real browser, providing higher confidence than unit tests while remaining faster than full E2E tests.

Comparison with E2E Testing

AspectComponent TestingE2E Testing
ScopeSingle componentFull application
SpeedFastSlower
SetupMinimalComplex
DependenciesMockedReal
Use CaseUnit/IntegrationUser flows

CI/CD Integration

Run component tests in CI:
name: Component Tests

on: [push, pull_request]

jobs:
  component-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:ct

Troubleshooting

Component not rendering

Verify the component import and check console for errors:
test('debug render', async ({ mount, page }) => {
  page.on('console', msg => console.log(msg.text()));
  const component = await mount(<MyComponent />);
  // Check if component mounted
  await expect(component).toBeVisible();
});

Vite configuration issues

Customize Vite config in playwright-ct.config.ts:
use: {
  ctViteConfig: {
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
      },
    },
  },
},
Component testing is experimental. The API may change in future versions.

Build docs developers (and LLMs) love