component-testing

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "component-testing" with this command: npx skills add adaptationio/skrillz/adaptationio-skrillz-component-testing

Component Testing with Playwright

Test UI components in isolation using Playwright's experimental component testing feature. Supports React, Vue, Svelte, and Solid.

Quick Start

// 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(
    <Button onClick={() => clicked = true}>Click me</Button>
  );

  await component.click();
  expect(clicked).toBe(true);
});

Installation

React

npm init playwright@latest -- --ct
# Select React when prompted

Or manually:

npm install -D @playwright/experimental-ct-react

Vue

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

Svelte

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

Configuration

playwright-ct.config.ts:

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

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

test('displays user info', async ({ mount }) => {
  const component = await mount(
    <UserCard
      name="John Doe"
      email="john@example.com"
    />
  );

  await expect(component.getByText('John Doe')).toBeVisible();
  await expect(component.getByText('john@example.com')).toBeVisible();
});

With Props

test('button variants', async ({ mount }) => {
  // Primary variant
  const primary = await mount(<Button variant="primary">Save</Button>);
  await expect(primary).toHaveClass(/btn-primary/);

  // Secondary variant
  const secondary = await mount(<Button variant="secondary">Cancel</Button>);
  await expect(secondary).toHaveClass(/btn-secondary/);
});

With Event Handlers

test('form submission', async ({ mount }) => {
  const submittedData: any[] = [];

  const component = await mount(
    <ContactForm onSubmit={(data) => 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

// Create wrapper for providers
import { ThemeProvider } from './ThemeContext';

test('themed component', async ({ mount }) => {
  const component = await mount(
    <ThemeProvider theme="dark">
      <ThemedButton>Click</ThemedButton>
    </ThemeProvider>
  );

  await expect(component).toHaveClass(/dark-theme/);
});

With Slots/Children

test('card with custom content', async ({ mount }) => {
  const component = await mount(
    <Card>
      <CardHeader>Title</CardHeader>
      <CardBody>Content here</CardBody>
      <CardFooter>
        <Button>Action</Button>
      </CardFooter>
    </Card>
  );

  await expect(component.getByText('Title')).toBeVisible();
  await expect(component.getByText('Content here')).toBeVisible();
  await expect(component.getByRole('button')).toBeVisible();
});

Vue Component Testing

// 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

test('card with slots', async ({ mount }) => {
  const component = await mount(Card, {
    slots: {
      default: '<p>Card content</p>',
      header: '<h2>Card Title</h2>',
    },
  });

  await expect(component.getByText('Card Title')).toBeVisible();
  await expect(component.getByText('Card content')).toBeVisible();
});

With Vuex/Pinia

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

// 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

test('button visual states', async ({ mount }) => {
  const component = await mount(<Button>Click</Button>);

  // 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

import AxeBuilder from '@axe-core/playwright';

test('button is accessible', async ({ mount, page }) => {
  await mount(<Button>Submit</Button>);

  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Responsive Behavior

test('responsive navigation', async ({ mount, page }) => {
  const component = await mount(<Navigation />);

  // 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

test('async component states', async ({ mount }) => {
  const component = await mount(<DataTable dataUrl="/api/data" />);

  // 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

test('error handling', async ({ mount, page }) => {
  // Mock failed API
  await page.route('**/api/data', route => {
    route.fulfill({ status: 500 });
  });

  const component = await mount(<DataTable dataUrl="/api/data" />);

  await expect(component.getByText(/error/i)).toBeVisible();
  await expect(component.getByRole('button', { name: 'Retry' })).toBeVisible();
});

Hooks and Fixtures

Before Each Test

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(<ApiComponent />);
  // API calls are automatically mocked
});

Custom Mount

const test = base.extend({
  mountWithProviders: async ({ mount }, use) => {
    const wrappedMount = async (component: JSX.Element) => {
      return mount(
        <ThemeProvider>
          <AuthProvider>
            {component}
          </AuthProvider>
        </ThemeProvider>
      );
    };
    await use(wrappedMount);
  },
});

test('with providers', async ({ mountWithProviders }) => {
  const component = await mountWithProviders(<Dashboard />);
  // Component has access to theme and auth contexts
});

Running Tests

# 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

finnhub-api

No summary provided by upstream source.

Repository SourceNeeds Review
General

auto-updater

No summary provided by upstream source.

Repository SourceNeeds Review
General

todo-management

No summary provided by upstream source.

Repository SourceNeeds Review