---
title: "Playwright Component Testingの使い方｜1.62のstory方式と移行手順"
url: "https://www.issoh.co.jp/tech/details/7392/"
published: 2025-06-24
updated: 2026-09-17
categories: ["自動化"]
publisher: "株式会社一創"
---

# Playwright Component Testingの使い方｜1.62のstory方式と移行手順

Playwright Component Testingは、ReactやVueのコンポーネントを実際のブラウザで単体描画し、Playwrightのテストで操作・検証する仕組みです。2026年7月24日公開のPlaywright 1.62で、テストファイルにJSXを書く従来の `@playwright/experimental-ct-react` 方式から、**story（シナリオ単位のラッパー）とgallery（storyを描画するページ）を使う方式**へ置き換わりました。続く1.63（2026年9月4日）では、旧パッケージの更新終了が告知されています。この記事では、1.63.0で実際に動かした設定・story・テストコードをもとに、新しい書き方と旧方式からの移行手順を解説します。

## まとめ：Playwright Component Testingの現状と要点

- 1.62以降のComponent Testingは `@playwright/test` 本体の組み込みfixture `mount` だけで動きます。専用のnpmパッケージや専用バンドラは使いません。
- テスト対象はstoryファイル（`*.story.tsx`）の名前付きexport単位で用意し、自分の開発サーバーが配信するgalleryページで描画します。
- `@playwright/experimental-ct-react`・`-react17`・`-vue` の最終版は1.62.1（2026年7月30日）です。公式ドキュメントは、移行が済むまでPlaywright 1.62に留まるよう案内しています。
- Svelte用のパッケージは1.59.0（2026年4月1日）で削除済みです。Svelte・Solidは、自前のgalleryを用意すれば新方式でテストできます。
- コールバックの呼び出しは、テスト側では受け取りません。story側で状態を持ち、隠しフォームの値として `toHaveValue()` で検証します。

以下、仕組み、セットアップ、書き方、移行、他ツールとの使い分けの順に説明します。

## Playwright Component Testingの仕組みとE2Eテストとの違い

Playwright自体の概要やSeleniumとの違いは[Playwrightとは｜仕組み・できること・Seleniumとの違いを解説](/tech/details/3167/)で扱っているので、ここではComponent Testingに固有の仕組みに絞ります。

### story・gallery・mountの3要素

公式ドキュメントは、新方式を次の3つの概念で説明しています。

| 要素            | 置き場所                    | 役割                               |
| ------------- | ----------------------- | -------------------------------- |
| story         | コンポーネントの隣の \*.story.tsx | 1シナリオ分のprops・モック・Providerを固定     |
| gallery       | playwright/gallery/     | window.mount() でstoryを #root に描画 |
| mount fixture | @playwright/test に内蔵    | galleryを開きstory idを渡してLocatorを返す |

テストコードはNode.jsで、コンポーネントはブラウザで動きます。実際のクリックやレイアウト計算が走るため、ビジュアル比較もそのまま使えます。コンポーネントが必要とするものはstoryの中（ブラウザ側）で用意し、テストはDOM・URL・ネットワークなどページから観測できるものだけを検証する、という分担が新方式の前提です。

### E2Eテスト・Jest＋Testing Libraryとの守備範囲

Component TestingのテストはE2Eテストと同じ `test` と `expect` で書き、並列実行・リトライ・トレースもそのまま使えます。違うのは、アプリ全体ではなくgalleryページ上の1コンポーネントだけを開く点です。

| 観点        | Playwright CT      | Jest＋Testing Library      | Playwright E2E |
| --------- | ------------------ | ------------------------- | -------------- |
| 描画環境      | 実ブラウザ              | jsdom（testEnvironmentで指定） | 実ブラウザ          |
| テスト対象     | 1コンポーネント           | 1コンポーネント                  | 画面遷移を含むアプリ全体   |
| CSS・レイアウト | 実際に計算される           | 計算されない                    | 実際に計算される       |
| 必要なサーバー   | galleryを配信する開発サーバー | 不要                        | アプリ本体          |

jsdomで足りるロジック中心のテストは、Jestや[React Testing Libraryとは？使い方・クエリ・ベストプラクティスを実務目線で解説](/tech/details/3521/)の方式で十分です。ブラウザもgallery用のサーバーも起動せずに済みます。フォーカス移動、スクロール、CSSによる表示切り替えなど、実ブラウザでしか再現しない振る舞いを確かめたい箇所にPlaywright CTを使うのが効率的です。

## 1.62で変わったComponent Testingの提供形態

Component Testingは、Playwright 1.22でReact・Vue向けのプレビュー機能として登場しました。以来 `@playwright/experimental-ct-*` という実験的パッケージのまま提供されてきたため、2025年以前の日本語記事の多くは、`mount(<Button />)` とJSXを直接渡す旧方式を前提にしています。旧パッケージは更新が止まったため、2026年9月時点で新たに導入するならstory方式を選ぶべきです。

### experimental-ct-\*パッケージの終了時期

npmレジストリと[GitHubのリリースノート](https://github.com/microsoft/playwright/releases)で確認した、パッケージごとの状況は次のとおりです（2026年9月17日時点）。

| パッケージ                               | 最終版                | 状況             |
| ----------------------------------- | ------------------ | -------------- |
| @playwright/experimental-ct-react   | 1.62.1（2026-07-30） | 1.63で更新終了を告知   |
| @playwright/experimental-ct-react17 | 1.62.1（2026-07-30） | 1.63で更新終了を告知   |
| @playwright/experimental-ct-vue     | 1.62.1（2026-07-30） | 1.63で更新終了を告知   |
| @playwright/experimental-ct-svelte  | 1.58.2（2026-02-06） | 1.59.0で削除      |
| @playwright/experimental-ct-solid   | 1.48.2（2024-10-25） | npmでdeprecated |
| @playwright/experimental-ct-vue2    | 1.48.2（2024-10-25） | npmでdeprecated |

1.63.0のリリースノートは、この3パッケージについて「will no longer be updated」と告知し、1.62で導入したstory方式への移行ガイドを案内しています。playwright.devの現行ドキュメントはさらに踏み込んで、「have been removed and are no longer published」と書いています。旧パッケージを使い続けている場合は、`@playwright/test` を1.62系に固定したうえで移行を進めるのが公式の手順です。

### JSXをテストに書く方式をやめた理由

公式ドキュメントは、旧方式が実験的な扱いから抜けられなかった理由を2つ挙げています。

- **自前のビルド構成と合わない**：旧方式はPlaywright自身がViteを内蔵してバンドルしていたため、パスエイリアス・プラグイン・CSSの設定を `ctViteConfig` に手で写す必要がありました。webpackやNext.jsの構成はそのままでは使えず、フレームワークごとに専用パッケージも要りました。
- **Node.jsとブラウザの境界で情報が落ちる**：テスト内のJSXをNode.jsでコンパイルしてブラウザで組み立て直すため、生きたオブジェクトは渡せず、コールバックは中途半端にしか動かず、モジュールのモックも黙って効きませんでした。

新方式ではgalleryを自分の開発サーバーが配信するので、アプリと同じプラグイン・エイリアス・CSSがそのまま効きます。フレームワークに依存する部分はgalleryの数十行だけになりました。

## React＋Viteでのセットアップ手順（1.63.0で実行確認）

以下のファイル一式は、@playwright/test 1.63.0、Vite 8.3.0、@vitejs/plugin-react 6.1.1、React 19.3.0、Node.js 26.5.0の組み合わせで、このまま動かして確認したものです。

```
project/
├─ playwright.config.ts
├─ vite.config.ts
├─ playwright/gallery/
│  ├─ index.html
│  └─ main.tsx
├─ src/components/
│  ├─ Counter.tsx
│  └─ Counter.story.tsx
└─ tests/components/
   └─ counter.spec.ts
```

### インストールとinit-skillsコマンド

React＋Viteのプロジェクトに追加する前提です。検証した版に固定する場合は次のとおりです。

```
npm i -D @playwright/test@1.63.0 vite@8.3.0 @vitejs/plugin-react@6.1.1
npm i react@19.3.0 react-dom@19.3.0
npx playwright install chromium
npx playwright init-skills
```

`npx playwright init-skills` は、1.63.0に同梱されているエージェント向けスキル（playwright-component-testingなど）をプロジェクトに展開するコマンドです。公式ドキュメントは、Claude CodeやGitHub Copilotなどのコーディングエージェントにこのスキルを読ませ、galleryの実装を任せる方法を最初の手順として挙げています。`--loop` オプションには `claude`（既定）か `agents` を指定します。手で書く場合も、仕様は下の2ファイル程度に収まります。Playwright本体の導入でつまずいた場合は[Playwrightのインストール方法とできない時の対処｜バージョン確認・Windows対応](/tech/details/3563/)を参照してください。

### galleryページ（index.htmlとmain.tsx）

galleryは `window.mount({ story, props })` と `window.unmount()` を公開する1枚のページです。Viteはプロジェクト配下の `.html` を配信するので、既存の開発サーバーで `/playwright/gallery/index.html` が開けます。

```
<!DOCTYPE html>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
```

```
import { StrictMode } from 'react';
import { flushSync } from 'react-dom';
import { createRoot, type Root } from 'react-dom/client';

const stories = import.meta.glob('../../src/**/*.story.{tsx,jsx}');
const id = (f: string) => f.replace(/^(\.\.\/)+src\//, '').replace(/\.story\.\w+$/, '');

async function resolve(storyId: string) {
  const sep = storyId.lastIndexOf('/');
  const [path, name] = [storyId.slice(0, sep), storyId.slice(sep + 1)];
  const files = Object.keys(stories);
  const exact = files.filter(f => id(f) === path);
  const matched = exact.length ? exact : files.filter(f => id(f).endsWith('/' + path));
  if (matched.length > 1) throw new Error(`Ambiguous story: ${storyId}`);
  const file = matched[0];
  const mod = (file && await stories[file]()) as Record<string, any> | undefined;
  return mod?.[name] ?? mod?.default;
}

const rootEl = document.getElementById('root')!;
let root: Root | undefined;
let renderError: unknown;

(window as any).mount = async ({ story, props }: { story: string; props?: Record<string, any> }) => {
  const Story = await resolve(story);
  if (!Story) throw new Error(`Unknown story: ${story}`);
  root ??= createRoot(rootEl, { onUncaughtError: error => { renderError = error; } });
  renderError = undefined;
  flushSync(() => root!.render(<StrictMode><Story {...props} /></StrictMode>));
  if (renderError) {
    const error = renderError;
    root.unmount();
    root = undefined;
    throw error;
  }
};

(window as any).unmount = async () => {
  root?.unmount();
  root = undefined;
};
```

このmain.tsxは、同梱スキルのgallery仕様にある作例をもとに、StrictModeで包み、エラー処理を補ったものです。要点は次の4つです。

- `import.meta.glob` でstoryファイルを列挙し、`src/` からの相対パスと export名を組み合わせてstory idにします。この処理はViteの静的解析に依存するため、共通パッケージには切り出せません。
- 2回目以降の `mount` でも `createRoot` を作り直さずに再利用します。作り直すと、後述の `update()` でコンポーネントの状態が消えます。
- 描画中の例外をテスト側の失敗として返します。React 19では描画中のエラーが再スローされないため、`flushSync` だけでは `mount` が成功扱いになりました（同梱スキルの作例のままでは、5件目のテストは失敗します）。そこで `createRoot` の `onUncaughtError` でエラーを受け取り、`window.mount` から投げ直しています。
- 短縮idが複数のstoryファイルに一致した場合は、先頭のファイルを選ばずに `Ambiguous story` として失敗させます。完全一致があればそちらを優先します。

アプリ全体で使うCSSやテーマの初期化も、このmain.tsxに書きます。旧方式の `beforeMount`／`afterMount` フックに当たる処理の置き場所も、この `window.mount` の中です。

### playwright.config.tsのcomponentsプロジェクト

```
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'components',
      testDir: './tests/components',
      use: {
        ...devices['Desktop Chrome'],
        baseURL: 'http://localhost:5173/playwright/gallery/index.html',
        serviceWorkers: 'block',
        reuseContext: true,
      },
    },
  ],
  webServer: {
    command: 'npx vite',
    url: 'http://localhost:5173/playwright/gallery/index.html',
    reuseExistingServer: !process.env.CI,
  },
});
```

```
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()], server: { port: 5173, strictPort: true } });
```

`mount` は `baseURL` へ遷移するので、`baseURL` はgalleryのURLにします。`serviceWorkers: 'block'` は、アプリのService Workerがキャッシュを返して `page.route()` のモックを素通りさせるのを防ぐ設定です。`reuseContext: true` は、同じワーカー内のテスト間でブラウザコンテキストを使い回します。旧パッケージが暗黙に行っていた高速化を、明示的に指定する形になりました。なお、package.jsonに `"type": "module"` がないと、Vite 8.3.0はvite.config.tsの読み込み時に警告を出します。

## storyとテストコードの書き方

### storyの命名とstory id

テスト対象のCounterと、そのstoryを次のように用意しました。storyファイルはコンポーネントと同じディレクトリに置き、名前付きexportを1つのシナリオとして扱います。

```
import { useState } from 'react';

type Props = { step?: number; onChange?: (value: number) => void };

export function Counter({ step = 1, onChange }: Props) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <output aria-label="count">{count}</output>
      <button onClick={() => { const next = count + step; setCount(next); onChange?.(next); }}>
        +{step}
      </button>
    </div>
  );
}
```

```
import { useState } from 'react';
import { Counter } from './Counter';

export const Default = ({ step = 1 }: { step?: number }) => <Counter step={step} />;

export const RecordsChange = () => {
  const [last, setLast] = useState<number | null>(null);
  return <>
    <Counter step={2} onChange={setLast} />
    <form hidden><input data-testid="last" readOnly value={String(last)} /></form>
  </>;
};

export const Broken = () => { throw new Error('render failed'); };
```

story idは「`src/` 配下のパスから `.story.*` を除いたもの＋export名」です。この例では `components/Counter/Default` になり、一意に決まる末尾部分だけの `Counter/Default` でも解決できます。この例のように文字列でidを書く場合、storyの名前変更や移動は実行時まで検出されません。1.63では、Vite用の小さなプラグインで `stories.d.ts` を生成し、`@playwright/test` の `Stories` インターフェースに登録すると、id自体も型検査と補完の対象にできます。

### propsの受け渡しとupdate()

```
import { test, expect } from '@playwright/test';
import type { Default } from '../../src/components/Counter.story';

test('step の値ずつ増える', async ({ mount }) => {
  const component = await mount<typeof Default>('components/Counter/Default', { step: 5 });
  await component.getByRole('button', { name: '+5' }).click();
  await expect(component.getByLabel('count')).toHaveText('5');
});

test('update() は状態を保ったまま props だけ差し替える', async ({ mount }) => {
  const component = await mount<typeof Default>('Counter/Default', { step: 1 });
  await component.getByRole('button').click();
  await component.update({ step: 10 });
  await component.getByRole('button', { name: '+10' }).click();
  await expect(component.getByLabel('count')).toHaveText('11');
});

test('onChange の結果は story 側の隠しフォームで検証する', async ({ mount }) => {
  const component = await mount('Counter/RecordsChange');
  await component.getByRole('button').click();
  await expect(component.getByTestId('last')).toHaveValue('2');
});

test('存在しない story は mount が例外になる', async ({ mount }) => {
  await expect(mount('Counter/Missing')).rejects.toThrow('Unknown story');
});

test('描画中の例外も mount の失敗として受け取れる', async ({ mount }) => {
  await expect(mount('Counter/Broken')).rejects.toThrow('render failed');
});
```

```
npx playwright test --project=components --reporter=list
```

```
Running 5 tests using 1 worker

  ✓  1 [components] › tests/components/counter.spec.ts:4:1 › step の値ずつ増える (627ms)
  ✓  2 [components] › tests/components/counter.spec.ts:10:1 › update() は状態を保ったまま props だけ差し替える (898ms)
  ✓  3 [components] › tests/components/counter.spec.ts:18:1 › onChange の結果は story 側の隠しフォームで検証する (589ms)
  ✓  4 [components] › tests/components/counter.spec.ts:24:1 › 存在しない story は mount が例外になる (348ms)
  ✓  5 [components] › tests/components/counter.spec.ts:28:1 › 描画中の例外も mount の失敗として受け取れる (362ms)

  5 passed (6.5s)
```

2件目のテストは、1回クリックして値を1にしたあと、`update({ step: 10 })` でpropsだけを差し替えています。続けて+10を押した結果が11になることから、再マウントされずに状態が引き継がれたことがわかります。`mount<typeof Default>` のように型引数でstoryの型を渡すと、propsが型検査されます。`step` に文字列を渡すコードは、tsc 7.0.2でTS2322のエラーになりました。

propsで渡すのは、シリアライズできる素のデータだけにします。同梱スキルも公式ドキュメントも、コールバックはstoryの中に置くよう定めています。

### コールバックは隠しフォームで検証

3件目のテストが、新方式の中心となる書き方です。旧方式のように、テスト側で関数を渡して呼び出し回数を数えることはしません。story側で `useState` を持ってコールバックに接続し、その値を `hidden` 属性付きのフォームに書き出します。テストは `toHaveValue()` で値を確認します。`toHaveValue()` は値が反映されるまで自動で再試行するので、待機処理を書く必要がありません。検証したい値が複数ある場合は、値ごとに `data-testid` 付きのinputを分けます。オブジェクトは `JSON.stringify` で文字列にして書き出します。

手動で確認したいときは、galleryのURLをブラウザで開き、DevToolsのコンソールで `await window.mount({ story: 'components/Counter/RecordsChange' })` を実行します。これは `mount` fixtureが内部で行っている処理と同じです。開発中はstoryの `hidden` を外しておくと、クリックに応じて記録値が変わる様子を画面で確認できます。

### ネットワークのモックとスクリーンショット比較

```
test('APIが500を返したらエラー表示になる', async ({ page, mount }) => {
  await page.route('**/api/items', route => route.fulfill({ status: 500 }));
  const component = await mount('components/ItemList/Default');
  await expect(component.getByRole('alert')).toContainText('Something went wrong');
});

test('ボタンの見た目を状態ごとに比較する', async ({ mount }) => {
  await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png');
  await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');
});
```

上の2つは、公式ドキュメントの例をもとにテスト名を変えたものです（ItemListとButtonのstoryは別途必要です）。`mount` はページ遷移を伴うので、`page.route()` は必ず `mount` より前に登録します。スクリーンショットはページ全体ではなく、`mount` が返すルートのLocatorを撮ると、galleryに置いた余分な要素が比較に混ざりません。1.62からは、スナップショット名を `.webp` にするとWebP形式で保存できます。アクセシビリティツリーをスナップショットで比べたい場合は、[Playwright の Aria Snapshots が提供するアクセシビリティ検証機能とは](/tech/details/6230/)で紹介している `toMatchAriaSnapshot()` も、同じLocatorに対して使えます。

## Vue・Svelteなど他フレームワークでの扱い

新方式で必要なフレームワーク固有のコードはgalleryだけなので、公式ドキュメントは「React、Vue、Svelte、Solidなど、開発サーバーが描画できるものなら何でもテストできる」としています。ただし、同梱スキルに作例があるのはReactとVueだけです。

Vueでは `createApp(...).mount()` を呼ぶたびに新しいインスタンスができるため、gallery仕様は、小さなホストを1度だけマウントし、storyとpropsを `shallowRef` で差し替える実装を示しています。

```
const story = shallowRef<Component | null>(null);
const props = shallowRef<Record<string, any>>({});
const host = { render: () => (story.value ? h(story.value, props.value) : null) };
let app: App | undefined;

(window as any).mount = async ({ story: id, props: next }) => {
  const resolved = await resolve(id);
  if (!resolved)
    throw new Error(`Unknown story: ${id}`);
  story.value = resolved;
  props.value = next ?? {};
  if (!app) {
    app = createApp(host);
    app.mount('#root');
  }
};
```

スロットを多用するシナリオは、`.story.vue` の単一ファイルコンポーネントを1つのstoryとして扱えます（idはパスのみで、default exportを使います）。Svelteは専用パッケージが1.59.0で削除されたため、現行のPlaywright Component Testingで扱うには、新方式の自前galleryを書くことになります。その場合も、story idを解決して `#root` に描画し、`update()` のためにルートを使い回すという契約を満たせば、`mount` fixture側の変更は不要です。

## 旧experimental-ct-reactからの移行手順

[公式の移行ガイド](https://playwright.dev/docs/test-components#migration-from-the-experimental-packages)にある対応表のうち、影響の大きい項目を抜き出しました。

| 旧パッケージ                            | story方式                           |
| --------------------------------- | --------------------------------- |
| mount(JSX)                        | storyのexport＋mount(‘id’)          |
| テストからのデータprops                    | mount(id, props)                  |
| JSXのchildren・スロット                 | 組み合わせごとにstoryを1つ                  |
| component.update(JSX)             | component.update(props)           |
| beforeMount・afterMount            | galleryのwindow.mount・storyのデコレーター |
| hooksConfig                       | storyが解釈するprops                   |
| routerフィクスチャ                      | page.route() またはstory内のMSW        |
| playwright/index.html             | galleryのindex.htmlとエントリーモジュール     |
| ctViteConfig・ctPort・ctTemplateDir | 廃止（webServerとbaseURLで指定）          |

クリック回数を数える典型的なテストは、次のように書き換えます。

移行前（button.spec.tsx）：

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

test('counts clicks', async ({ mount }) => {
  let clicks = 0;
  const component = await mount(<Button title='Submit' onClick={() => ++clicks} />);
  await component.getByRole('button').click();
  expect(clicks).toBe(1);
});
```

移行後のstory（src/components/Button.story.tsx）：

```
import { useState } from 'react';
import { Button } from './Button';

export const CountsClicks = () => {
  const [clicks, setClicks] = useState(0);
  return <>
    <Button title='Submit' onClick={() => setClicks(count => count + 1)} />
    <form hidden><input data-testid='click-count' readOnly value={String(clicks)} /></form>
  </>;
};
```

移行後のテスト（tests/components/button.spec.ts）：

```
import { test, expect } from '@playwright/test';

test('counts clicks', async ({ mount }) => {
  const component = await mount('components/Button/CountsClicks');
  await component.getByRole('button').click();
  await expect(component.getByTestId('click-count')).toHaveValue('1');
});
```

公式が勧める進め方は段階移行です。

1. `@playwright/test` と旧パッケージを1.62系に固定する。
2. galleryと `components` プロジェクトを、既存のCTプロジェクトと並べて追加する。
3. specを1本ずつstory方式へ書き換える。
4. すべて移したら、`@playwright/experimental-ct-*`、`playwright/index.html`、`playwright/index.ts`、`playwright/.cache` を削除し、Playwrightを1.63以降へ上げる。

テストごとに異なるJSXを組み立てていたspecは、組み合わせの数だけstoryのexportが増えます。公式はこれを、テストする価値のある組み合わせには名前を付けてレビュー対象にすべきだ、という設計意図として説明しています。

## Vitest Browser Mode・Storybookとの使い分け

移行を機に、コンポーネントテストの基盤ごと見直すチームもあるはずです。判断材料を整理します。

- **Vitestで単体テストを書いているチーム**：Vitest 5.0.1（2026年9月15日）のBrowser Modeも、`@vitest/browser-playwright` 経由でPlaywrightのブラウザを使って実ブラウザ上でテストできます。こちらはテストファイルにコンポーネントを直接書けるので、story方式への書き換えは不要です。導入手順は[Vitest Browser Modeの導入・セットアップ方法を徹底解説](/tech/details/7394/)、jsdomでの書き方は[Vitestの使い方｜React Testing Libraryでコンポーネントテストを書く実践ガイド【2026年版】](/tech/details/2867/)にまとめています。
- **すでにPlaywrightでE2Eテストを運用しているチーム**：設定・レポーター・トレース・CIをそのまま共有できる点で、Playwright CTの新方式が有利です。storyがコンポーネント状態の一覧としても読めます。
- **StorybookのPlaywright CT連携を使っていたチーム**：[Storybookの移行ガイド](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#experimental-playwright-ct-integration-removed)（10.5.xから10.6.0への節）で、旧 `@playwright/experimental-ct-*` を土台にした実験的API `createPlaywrightTest` の削除が告知されています。Storybook側は開発サーバーとしての連携を検討中としていますが、文書化された統合はまだありません。

採用を見送るべき場面もあります。値の違いだけならpropsで同じstoryを使い回せます。しかし、テストごとに異なるJSXツリーを組み立てるテストや、テスト側から関数を渡して呼び出し引数を細かく検証するテストが中心なら、話が変わります。構成の数だけstoryのexportを作り、コールバックの結果を隠しフォームへ記録し直す必要があり、移行の負担が大きくなります。そうした場合は、テスト内でコンポーネントを組み立てられるVitest Browser Modeのほうが素直です。テスト全体の配分をどうするかは、[Testing Trophy（テスティングトロフィー）とは？テストピラミッドとの違いと理想のテスト配分を解説](/tech/details/8765/)、E2E側の設計は[E2E（エンドツーエンド）テストのベストプラクティス｜結合テストとの違いとツール選定](/tech/details/5470/)を参考にしてください。

## よくある質問

### Playwright Component TestingとJestはどちらを使えばよいですか？

役割が違います。Jestの既定のテスト環境はnodeで、DOMを扱うテストでは環境にjsdomを指定します。jsdomはCSSのレイアウトを計算しません。実ブラウザでの表示や操作を確かめたいコンポーネントにPlaywright CTを使い、ロジック中心のテストはJestやVitestで書く、という分担が一般的です。

### @playwright/experimental-ct-reactは今も使えますか？

1.62.1（2026年7月30日）が最終版で、1.63では更新終了が告知されています。既存のテストは、Playwrightを1.62系に固定すれば動き続けます。ただし、1.63以降の新機能やブラウザ更新は受けられないため、公式の移行ガイドに沿ってstory方式へ移すことが前提です。

### テストファイルの中でコンポーネントを組み立てられますか？

1.62以降の方式ではできません。`mount` に渡せるのはstory idとシリアライズできるpropsだけなので、組み合わせごとにstoryのexportを作ります。テスト内でJSXを書きたい場合は、Vitest Browser Modeが選択肢になります。

### Playwright Component TestingでUIモードは使えますか？

使えます。新方式のテストは通常のPlaywrightテストなので、`npx playwright test --project=components --ui` のように、E2Eテストと同じUIモードやトレースビューアで実行・デバッグできます。

### test.eachのように同じテストを複数の値で回せますか？

Playwrightには `test.each` がないため、公式ドキュメントのとおり、値の配列を `forEach` で回して `test()` を複数定義します（テスト名は一意にします）。境界値の網羅のような本当にパラメーター化が必要なケースでは、`mount(id, props)` のpropsに値を渡します。状態ごとの見た目の違いを確かめたいだけなら、公式はstoryのexportを分けることを勧めています。

## 関連記事

- [Playwrightとは｜仕組み・できること・Seleniumとの違いを解説](/tech/details/3167/)
- [Vitest Browser Modeの導入・セットアップ方法を徹底解説](/tech/details/7394/)
- [React Testing Libraryとは？使い方・クエリ・ベストプラクティスを実務目線で解説](/tech/details/3521/)
- [Testing Trophy（テスティングトロフィー）とは？テストピラミッドとの違いと理想のテスト配分を解説](/tech/details/8765/)
- [E2E（エンドツーエンド）テストのベストプラクティス｜結合テストとの違いとツール選定](/tech/details/5470/)

---

出典: [Playwright Component Testingの使い方｜1.62のstory方式と移行手順](<https://www.issoh.co.jp/tech/details/7392/>)（株式会社一創）
