---
name: component-testing
description: Isolated component testing for React, Vue, and Svelte with Playwright. Use when testing UI components in isolation, testing component interactions, or building component test suites.
version: 1.0.0
tags: [testing, components, react, vue, svelte, playwright, isolation]
---
# Component Testing with Playwright
Test UI components in isolation using Playwright's experimental component testing feature. Supports React, Vue, Svelte, and Solid.
## Quick Start
```typescript
// Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('button click triggers callback', async ({ mount }) => {
let clicked = false;
const component = await mount(
);
await component.click();
expect(clicked).toBe(true);
});
```
## Installation
### React
```bash
npm init playwright@latest -- --ct
# Select React when prompted
```
Or manually:
```bash
npm install -D @playwright/experimental-ct-react
```
### Vue
```bash
npm install -D @playwright/experimental-ct-vue
```
### Svelte
```bash
npm install -D @playwright/experimental-ct-svelte
```
## Configuration
**playwright-ct.config.ts:**
```typescript
import { defineConfig, devices } from '@playwright/experimental-ct-react';
export default defineConfig({
testDir: './src',
testMatch: '**/*.spec.tsx',
use: {
ctPort: 3100,
ctViteConfig: {
// Custom Vite config for component tests
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
```
## React Component Testing
### Basic Mount
```typescript
import { test, expect } from '@playwright/experimental-ct-react';
import { UserCard } from './UserCard';
test('displays user info', async ({ mount }) => {
const component = await mount(
);
await expect(component.getByText('John Doe')).toBeVisible();
await expect(component.getByText('john@example.com')).toBeVisible();
});
```
### With Props
```typescript
test('button variants', async ({ mount }) => {
// Primary variant
const primary = await mount();
await expect(primary).toHaveClass(/btn-primary/);
// Secondary variant
const secondary = await mount();
await expect(secondary).toHaveClass(/btn-secondary/);
});
```
### With Event Handlers
```typescript
test('form submission', async ({ mount }) => {
const submittedData: any[] = [];
const component = await mount(
submittedData.push(data)} />
);
await component.getByLabel('Name').fill('John');
await component.getByLabel('Email').fill('john@example.com');
await component.getByRole('button', { name: 'Submit' }).click();
expect(submittedData).toHaveLength(1);
expect(submittedData[0]).toEqual({
name: 'John',
email: 'john@example.com',
});
});
```
### With Context Providers
```typescript
// Create wrapper for providers
import { ThemeProvider } from './ThemeContext';
test('themed component', async ({ mount }) => {
const component = await mount(
Click
);
await expect(component).toHaveClass(/dark-theme/);
});
```
### With Slots/Children
```typescript
test('card with custom content', async ({ mount }) => {
const component = await mount(
Title
Content here
);
await expect(component.getByText('Title')).toBeVisible();
await expect(component.getByText('Content here')).toBeVisible();
await expect(component.getByRole('button')).toBeVisible();
});
```
## Vue Component Testing
```typescript
// Counter.spec.ts
import { test, expect } from '@playwright/experimental-ct-vue';
import Counter from './Counter.vue';
test('counter increments', async ({ mount }) => {
const component = await mount(Counter, {
props: {
initialCount: 0,
},
});
await expect(component.getByText('Count: 0')).toBeVisible();
await component.getByRole('button', { name: '+' }).click();
await expect(component.getByText('Count: 1')).toBeVisible();
});
```
### With Slots
```typescript
test('card with slots', async ({ mount }) => {
const component = await mount(Card, {
slots: {
default: 'Card content
',
header: 'Card Title
',
},
});
await expect(component.getByText('Card Title')).toBeVisible();
await expect(component.getByText('Card content')).toBeVisible();
});
```
### With Vuex/Pinia
```typescript
import { test, expect } from '@playwright/experimental-ct-vue';
import { createTestingPinia } from '@pinia/testing';
import UserProfile from './UserProfile.vue';
test('displays user from store', async ({ mount }) => {
const component = await mount(UserProfile, {
global: {
plugins: [
createTestingPinia({
initialState: {
user: { name: 'John', email: 'john@example.com' },
},
}),
],
},
});
await expect(component.getByText('John')).toBeVisible();
});
```
## Svelte Component Testing
```typescript
// Button.spec.ts
import { test, expect } from '@playwright/experimental-ct-svelte';
import Button from './Button.svelte';
test('button emits click', async ({ mount }) => {
let clicked = false;
const component = await mount(Button, {
props: {
label: 'Click me',
},
on: {
click: () => clicked = true,
},
});
await component.click();
expect(clicked).toBe(true);
});
```
## Testing Patterns
### Visual Regression
```typescript
test('button visual states', async ({ mount }) => {
const component = await mount();
// Default state
await expect(component).toHaveScreenshot('button-default.png');
// Hover state
await component.hover();
await expect(component).toHaveScreenshot('button-hover.png');
// Focus state
await component.focus();
await expect(component).toHaveScreenshot('button-focus.png');
});
```
### Accessibility
```typescript
import AxeBuilder from '@axe-core/playwright';
test('button is accessible', async ({ mount, page }) => {
await mount();
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
```
### Responsive Behavior
```typescript
test('responsive navigation', async ({ mount, page }) => {
const component = await mount();
// Desktop - horizontal nav
await page.setViewportSize({ width: 1280, height: 720 });
await expect(component.locator('.nav-horizontal')).toBeVisible();
// Mobile - hamburger menu
await page.setViewportSize({ width: 375, height: 667 });
await expect(component.locator('.hamburger-menu')).toBeVisible();
});
```
### Loading States
```typescript
test('async component states', async ({ mount }) => {
const component = await mount();
// Loading state
await expect(component.getByText('Loading...')).toBeVisible();
// Wait for data
await expect(component.getByRole('table')).toBeVisible();
await expect(component.getByText('Loading...')).not.toBeVisible();
});
```
### Error States
```typescript
test('error handling', async ({ mount, page }) => {
// Mock failed API
await page.route('**/api/data', route => {
route.fulfill({ status: 500 });
});
const component = await mount();
await expect(component.getByText(/error/i)).toBeVisible();
await expect(component.getByRole('button', { name: 'Retry' })).toBeVisible();
});
```
## Hooks and Fixtures
### Before Each Test
```typescript
import { test as base, expect } from '@playwright/experimental-ct-react';
const test = base.extend({
autoMockApi: async ({ page }, use) => {
await page.route('**/api/**', route => {
route.fulfill({ status: 200, body: '{}' });
});
await use();
},
});
test('component with mocked api', async ({ mount, autoMockApi }) => {
const component = await mount();
// API calls are automatically mocked
});
```
### Custom Mount
```typescript
const test = base.extend({
mountWithProviders: async ({ mount }, use) => {
const wrappedMount = async (component: JSX.Element) => {
return mount(
{component}
);
};
await use(wrappedMount);
},
});
test('with providers', async ({ mountWithProviders }) => {
const component = await mountWithProviders();
// Component has access to theme and auth contexts
});
```
## Running Tests
```bash
# Run all component tests
npx playwright test -c playwright-ct.config.ts
# Run specific test file
npx playwright test Button.spec.tsx -c playwright-ct.config.ts
# Run with UI mode
npx playwright test -c playwright-ct.config.ts --ui
# Update snapshots
npx playwright test -c playwright-ct.config.ts --update-snapshots
```
## Best Practices
1. **Test behavior, not implementation** - Focus on user interactions
2. **Keep components isolated** - Mock external dependencies
3. **Test all states** - Default, loading, error, empty, success
4. **Use semantic queries** - `getByRole`, `getByLabel` over CSS selectors
5. **Combine with E2E** - Component tests for logic, E2E for integration
## References
- `references/react-patterns.md` - React-specific testing patterns
- `references/vue-patterns.md` - Vue-specific testing patterns