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本体の組み込みfixturemountだけで動きます。専用の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との違いを解説で扱っているので、ここでは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とは?使い方・クエリ・ベストプラクティスを実務目線で解説の方式で十分です。ブラウザも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のリリースノートで確認した、パッケージごとの状況は次のとおりです(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/[email protected] [email protected] @vitejs/[email protected]
npm i [email protected] [email protected]
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対応を参照してください。
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 が提供するアクセシビリティ検証機能とはで紹介している 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からの移行手順
公式の移行ガイドにある対応表のうち、影響の大きい項目を抜き出しました。
| 旧パッケージ | 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');
});
公式が勧める進め方は段階移行です。
@playwright/testと旧パッケージを1.62系に固定する。- galleryと
componentsプロジェクトを、既存のCTプロジェクトと並べて追加する。 - specを1本ずつstory方式へ書き換える。
- すべて移したら、
@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の導入・セットアップ方法を徹底解説、jsdomでの書き方はVitestの使い方|React Testing Libraryでコンポーネントテストを書く実践ガイド【2026年版】にまとめています。 - すでにPlaywrightでE2Eテストを運用しているチーム:設定・レポーター・トレース・CIをそのまま共有できる点で、Playwright CTの新方式が有利です。storyがコンポーネント状態の一覧としても読めます。
- StorybookのPlaywright CT連携を使っていたチーム:Storybookの移行ガイド(10.5.xから10.6.0への節)で、旧
@playwright/experimental-ct-*を土台にした実験的APIcreatePlaywrightTestの削除が告知されています。Storybook側は開発サーバーとしての連携を検討中としていますが、文書化された統合はまだありません。
採用を見送るべき場面もあります。値の違いだけならpropsで同じstoryを使い回せます。しかし、テストごとに異なるJSXツリーを組み立てるテストや、テスト側から関数を渡して呼び出し引数を細かく検証するテストが中心なら、話が変わります。構成の数だけstoryのexportを作り、コールバックの結果を隠しフォームへ記録し直す必要があり、移行の負担が大きくなります。そうした場合は、テスト内でコンポーネントを組み立てられるVitest Browser Modeのほうが素直です。テスト全体の配分をどうするかは、Testing Trophy(テスティングトロフィー)とは?テストピラミッドとの違いと理想のテスト配分を解説、E2E側の設計はE2E(エンドツーエンド)テストのベストプラクティス|結合テストとの違いとツール選定を参考にしてください。
よくある質問
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を分けることを勧めています。