jpskill.com
🛠️ 開発・MCP コミュニティ

component-testing

Playwrightを活用し、React、Vue、SvelteのUIコンポーネントを個別にテストすることで、コンポーネント間の連携やテストスイート構築を効率化し、品質向上に貢献するSkill。

📜 元の英語説明(参考)

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.

🇯🇵 日本人クリエイター向け解説

一言でいうと

Playwrightを活用し、React、Vue、SvelteのUIコンポーネントを個別にテストすることで、コンポーネント間の連携やテストスイート構築を効率化し、品質向上に貢献するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o component-testing.zip https://jpskill.com/download/9406.zip && unzip -o component-testing.zip && rm component-testing.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/9406.zip -OutFile "$d\component-testing.zip"; Expand-Archive "$d\component-testing.zip" -DestinationPath $d -Force; ri "$d\component-testing.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して component-testing.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → component-testing フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

Playwright を使用したコンポーネントテスト

Playwright の実験的なコンポーネントテスト機能を使用して、UI コンポーネントを分離してテストします。React、Vue、Svelte、および Solid をサポートしています。

クイックスタート

// 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);
});

インストール

React

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

または手動で:

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

Vue

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

Svelte

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: '**/*.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 コンポーネントテスト

基本的なマウント

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();
});

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/);
});

イベントハンドラーを使用する

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',
  });
});

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/);
});

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 コンポーネントテスト

// 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();
});

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();
});

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 コンポーネントテスト

// 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);
});

テストパターン

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('but
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

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