Playwrightから新しくAgents機能がリリースされました。テストプランの作成・テストコードの作成・エラーとなったテストの修正を行なう、3つのエージェント機能を備えています。今回は、Claude CodeとAgentsを使ってLaravelプロジェクトにE2Eテストを追加してみます。
テスト対象のサイト
テスト対象のサイトについて簡単にご紹介します。よくある「英語日記」のような機能で、ログイン後のダッシュボードでは日記一覧の表示・音声再生・詳細画面への遷移ができます。
新規作成は3段階の画面遷移になっており、「日本語で入力 → 英語で入力 → 最後にAIが文章を翻訳・添削してアドバイスを返す」という流れになっています。
準備・セットアップ
ではさっそくAgentsを使う準備から進めてゆきます。
Playwrightのインストールがまだの方は、セットアップの記事をご覧ください。
私の環境ではすでにPlaywrightがインストール済みですが、Agentsは新しい機能のため、バージョンアップが必要かもしれません。プロジェクトのルートディレクトリで以下のコマンドを実行します。
$ npm install -D @playwright/test@latest $ npx playwright --version Version 1.56.0
執筆時点で最新の1.56.0に更新されました。
次に、以下のコマンドを実行します。この記事ではClaude Codeを使用するので、オプションでclaudeを指定します。
$ npx playwright init-agents --loop=claude
Claude Code以外を使用する方はドキュメントからコマンドをご確認ください。
コマンド実行後、新規に以下の5ファイルが作成されました。
Writing file: .claude/agents/playwright-test-generator.md Writing file: .claude/agents/playwright-test-healer.md Writing file: .claude/agents/playwright-test-planner.md Writing file: .mcp.json Writing file: e2e/seed.spec.ts
.claude/agents/ディレクトリには3種類のエージェントに関するファイルが生成され、それぞれのエージェントは以下のような役割になっています。
- playwright-test-planner.md : テスト計画を作成するエージェント
- playwright-test-generator.md : テストコードを生成するエージェント
- playwright-test-healer.md : テストを修復するエージェント
また、テストディレクトリにインストールされたe2e/seed.spec.tsを見てみると、初期状態では以下のように特に内容のないファイルとなっています。
import { test, expect } from '@playwright/test';
test.describe('Test group', () => {
test('seed', async ({ page }) => {
// generate code here.
});
});
こちらはグローバルセットアップや依存関係、またはエージェントへの指示を記述するファイルで、Playwrightの各エージェントから参照されます。実際の運用時には重要となるファイルですが、今回は初期状態のまま進めます。
plannerでテストプランを作成
ここからClaude Codeを使ってゆきます。Claude Codeを起動すると、次のようなメッセージが表示されました。
New MCP server found in .mcp.json: playwright-test │ │ │ │ MCP servers may execute code or access system resources. All tool calls require approval. Learn more in the MCP documentation │ │ (https://docs.claude.com/s/claude-code-mcp). │ │ │ │ ❯ 1. Use this and all future MCP servers in this project │ │ 2. Use this MCP server │ │ 3. Continue without using this MCP server
先ほど作成された.mcp.jsonに記載のMCPサーバー(playwright-test)は、コード実行やシステムリソースへのアクセスを行うため、セキュリティ確認が求められているようです。今回は2番の「このMCPサーバーを使用」を選択しました。
ここからやっとプランの作成に入りますが、その前にテスト対象サイトにアクセスできる状態かを確認します。
今回はローカルにあるLaravelプロジェクトを対象としているため、サーバーを起動させておきます。
$ php artisan serve
プラン作成用エージェントを実行すると、ブラウザが起動してPlaywrightが実際にサイトを操作しながらテストプランを作成します。そのため、あらかじめ起動しておく必要があります。
Claude Codeへのプロンプトは以下のように記述しました。@agent-playwright-test-plannerのように”@”付きで明示的にエージェントを起動してもいいですし、以下のように文章のみで渡しても問題なく認識されました。
$ playwright-test-plannerエージェントを使って次のテストプランを作成ください。ログイン後のダッシュボード画面での日記一覧表示・新規作成・翻訳機能
すると、以下のようにplaywright-test-plannerが起動しました。

しばらく待つとプラン作成完了のメッセージが表示されました。メッセージが長いので冒頭のみですが、以下のような形で作成したプランの概要を知らせてくれます。
⏺ テストプランの作成が完了しました!
作成されたファイル
1. 詳細テストプラン (e2e/test-plan-dashboard.md)
600行以上の詳細なドキュメントで、以下を含みます:
- 70以上のテストシナリオ
- 12のテストカテゴリ:
- 認証とログイン
- 日記一覧表示
- 日記詳細表示
- 日記新規作成(3ステップ)
- 翻訳機能
- セキュリティとバリデーション
- UI/UX
- エッジケースとエラー処理など
2. 要約版 (e2e/test-plan-summary.md)
エグゼクティブサマリーで、以下を含みます:
- 発見された主要機能
- テストシナリオの優先度
- 技術スタック分析
- 推奨される次のステップ
・・・・・
実際のテストプランはtest-plan-dashboard.mdに出力されており、テスト概要 → シナリオ → 成功基準 という構成で書かれていて読みやすいです。こちらも一部を抜粋します。
#### 1.1 ログイン後の正常なページロード **手順:** 1. `/user/diary`に遷移 2. ページが完全にロードされるまで待機 **期待される結果:** - ページがエラーなく正常にロード - URLが`/user/diary` - ナビゲーションバーに「テスト用ユーザー」ボタンが表示 - 左上に紫色の作成ボタンが表示 - 日記一覧セクションが表示 - 上部にページネーションコントロールが表示 #### 1.2 日記カードの表示確認 **手順:** 1. `/user/diary`に遷移 2. 一覧内の日記カードを確認 **期待される結果:** - 最低1枚の日記カードが表示(シードデータから5件) - 各カードに以下が表示: - 日付ボックス上部に月番号 - 日付ボックス下部に日番号 - 翻訳コンテンツのテキスト(英語) - 母語コンテンツのテキスト(日本語) - 音声プレーヤーまたは「音声変換中...」メッセージ - カードがレスポンシブグリッドで配置(デスクトップで3列) - カードが作成日降順で表示(最新が最初) #### 1.3 レスポンシブレイアウトの確認 **手順:** 1. デスクトップビューポート(1920x1080)で`/user/diary`をロード 2. グリッドレイアウトが3列になっていることを確認 3. タブレットビューポート(768x1024)にリサイズ 4. グリッドレイアウトが2列になっていることを確認 5. モバイルビューポート(375x667)にリサイズ 6. グリッドレイアウトが1列になっていることを確認 **期待される結果:** - デスクトップ: 3列グリッドレイアウト(`lg:w-1/3`) - タブレット: 2列グリッドレイアウト(`md:w-1/2`) - モバイル: 1列レイアウト(`w-full`) - すべてのコンテンツが読みやすく適切にフォーマット - 横スクロール不要
ページネーションやエッジケースのテストは以下のような感じです。
#### 3.1 ページネーションコントロールの表示 **手順:** 1. テストデータベースに50件以上の日記エントリーを用意 2. `/user/diary`に遷移 3. 一覧上部のページネーションコントロールを見つける **期待される結果:** - 50件以上のエントリーがある場合、ページネーションコントロールが表示 - 現在のページ番号がハイライト - 「次へ」「前へ」リンクが表示(該当する場合) - ページ番号がクリック可能 #### 3.2 次ページへの遷移 **手順:** 1. データベースに51件以上の日記エントリーがある状態で`/user/diary`に遷移 2. 「次へ」リンクまたはページ2のリンクをクリック **期待される結果:** - URLがページネーションパラメータを含むように更新(例: `/user/diary?page=2`) - 異なる日記エントリーのセットでページがロード - 表示される日記エントリーがページ1と異なる - ページネーションコントロールが現在のページを反映 - 「前へ」リンクがアクティブに
#### 6.1 日記エントリーがない場合の表示 **手順:** 1. テストユーザーのすべての日記エントリーをクリア 2. `/user/diary`に遷移 **期待される結果:** - ページがエラーなくロード - 作成ボタンが表示され機能する - 空の一覧セクションが表示 - 日記カードが表示されない - ページネーションコントロールが表示されない - エラーメッセージが表示されない #### 6.2 空状態から最初の日記作成 **手順:** 1. エントリーがない状態で`/user/diary`に遷移 2. 作成ボタンをクリック 3. 日記作成プロセスを完了 4. `/user/diary`に戻る **期待される結果:** - 空状態から作成ページへのナビゲーションが機能 - 日記作成後、一覧に新しいエントリーが表示 - 日記カードがすべての期待されるフィールドを表示 - 音声生成が自動的に開始 #### 11.1 存在しない日記へのナビゲーション処理 **手順:** 1. `/user/diary/99999`(存在しないID)に遷移 **期待される結果:** - 404エラーページまたは適切なエラーメッセージ - ユーザーが一覧に戻ることが可能 - アプリケーションクラッシュなし - エラーが適切にログ記録
パフォーマンス関連のテストもあります。
#### 13.1 ページロードパフォーマンス **手順:** 1. 50件の日記エントリーがある状態で`/user/diary`に遷移 2. ブラウザDevToolsでページロード時間を測定 **期待される結果:** - 初期ページロードが2秒以内に完了 - Livewireコンポーネントが迅速に初期化 - 画像と音声参照が非同期でロード - ブロッキングリソースなし #### 13.2 ページネーションパフォーマンス **手順:** 1. 50件以上のエントリーがある複数ページ間をナビゲート 2. ページ遷移時間を測定 **期待される結果:** - ページ遷移が1秒以内に完了 - ページ間のスムーズなナビゲーション - 目に見える遅延やラグなし - フラッシュなしでコンテンツが更新
ユーザー認証関連も。
#### 14.2 ユーザー固有データ表示確認 **手順:** 1. テストユーザー(test@example.com)としてログイン 2. `/user/diary`に遷移 3. テストユーザーの日記のみが表示されることを確認 **期待される結果:** - 認証済みユーザーに属する日記のみ表示 - 日記数がテストユーザーのエントリー数と一致(シードから5件) - 他のユーザーのデータが表示されない - データ分離が維持
以下は表示関連のテストですが、先ほどのページネーションのテストと少し被っている部分もあります。
#### 15.1 単一日記エントリーの表示 **手順:** 1. テストユーザーがちょうど1件の日記エントリーを持つように設定 2. `/user/diary`に遷移 **期待される結果:** - 単一の日記カードが正しく表示 - ページネーションコントロール非表示 - 作成ボタンが引き続き機能 - レイアウトが適切な構造を維持 #### 15.2 ちょうど50件の日記エントリー(ページネーション境界) **手順:** 1. テストユーザーがちょうど50件の日記エントリーを持つように設定 2. `/user/diary`に遷移 **期待される結果:** - すべての50件のエントリーがページ1に表示 - ページネーションコントロール非表示 - ページが効率的にロード - すべてのカードが正しくレンダリング #### 15.3 51件の日記エントリー(ページネーショントリガー) **手順:** 1. テストユーザーがちょうど51件の日記エントリーを持つように設定 2. `/user/diary`に遷移 **期待される結果:** - 最初の50件のエントリーがページ1に表示 - ページネーションコントロールが表示 - ページ2のリンクが利用可能 - ページ2をクリックすると51件目のエントリーが表示 #### 15.4 非常に長い日記コンテンツ **手順:** 1. 500文字の母語コンテンツで日記を作成 2. 500文字の翻訳コンテンツで日記を作成 3. `/user/diary`に遷移 **期待される結果:** - コンテンツが300文字で適切に省略 - レイアウト崩れなし - カードが一貫した高さを維持 - 詳細ページで完全なコンテンツが利用可能 #### 15.5 コンテンツ内の特殊文字 **手順:** 1. 日本語特殊文字(々、〜など)で日記を作成 2. 絵文字文字で日記を作成 3. HTML特殊文字(<、>、&など)で日記を作成 4. `/user/diary`に遷移 **期待される結果:** - すべての特殊文字が正しく表示 - HTMLインジェクションやXSS脆弱性なし - 文字が適切にエンコード - レイアウトやレンダリング問題なし
基本的な画面表示からパフォーマンス測定、エッジケースまで網羅的にカバーされています。 ページネーションや表示関連ではもう少しケースをまとめられるような気がするので、作成されたテストプランは人間の確認が現時点では必要そうです。
generator・healerでE2Eテストを作成
テストプランができたので、こちらを元にテストコードを作成します。提案されたテストケース数が多いため、今回は一部のテストのみを指定して以下のように生成を依頼しました。
このテストプランをもとに、2.1 日記一覧の表示 のテストを作成してください
@agent-playwright-test-generatorとエージェントを明示しなくても、文脈から判断して自動でテスト作成用エージェントを起動してくれました。

テスト作成完了後、テストの動作確認を依頼したところエラーが発生しました。すると自動でplaywright-test-healerを起動して修正してくれます。
エラーの原因は、複数のLink要素が画面上に存在しており、一意に指定できなかったためでした。今回はfirst()を追加してエラーが修正されました。
// 修正前
await expect(page.getByRole('main').getByRole('link')).toBeVisible();
// 修正後
await expect(page.getByRole('main').getByRole('link').first()).toBeVisible();
このように検証対象の要素が一意に特定しにくいHTMLとなっていると、テスト作成に時間がかかったり、セレクタ指定が冗長になるということがあります。
そのため、HTMLにdata-testid属性を付与したり、aria-labelを付与してgetByRole()で取得しやすくしておくと、よりスムーズにテスト作成ができるようになると思います。
Playwrightの推奨ロケーターは複数ありますので、ドキュメントをご参照ください。
実際に作成されたテストコードと、気になった点
修正も完了し、実際に作成されたテストコードが以下となります。
バリデーション関連のテスト
test('Empty Form Submission', async ({ page }) => {
// 1. Login with test@example.com / password
await page.goto('http://127.0.0.1:8000/login');
await page.getByRole('textbox', { name: 'メールアドレス' }).fill('test@example.com');
await page.getByRole('textbox', { name: 'パスワード' }).fill('password');
await page.getByRole('button', { name: 'ログイン' }).click();
// 2. Navigate to diary creation page (/user/diary/create)
await page.goto('http://127.0.0.1:8000/user/diary/create');
// 3. Leave the textarea empty (do not enter any text)
// (No action needed - textarea is already empty)
// 4. Click "次へ!" (Next!) button
await page.getByRole('button', { name: '次へ!' }).click();
// Verify validation error is displayed
await expect(page.getByText('母語のテキストは必ず指定してください。')).toBeVisible();
// Verify user remains on Step 1 (did not proceed to Step 2)
await expect(page.getByRole('heading', { name: 'Step 1: まずは日本語で書いてみよう' })).toBeVisible();
});
日記一覧画面表示のテスト
test('日記一覧の表示と基本機能', async ({ page }) => {
// ログインページにアクセス
await page.goto('http://127.0.0.1:8000/login');
// メールアドレスを入力
await page.getByRole('textbox', { name: 'メールアドレス' }).fill('test@example.com');
// パスワードを入力
await page.getByRole('textbox', { name: 'パスワード' }).fill('password');
// ログインボタンをクリック
await page.getByRole('button', { name: 'ログイン' }).click();
// ログイン後、ダッシュボード (/user/diary) にアクセスされることを確認
await expect(page).toHaveURL('http://127.0.0.1:8000/user/diary');
// ナビゲーションバーにユーザー名ボタンが表示されることを確認
await expect(page.getByRole('button', { name: 'テスト用ユーザー' })).toBeVisible();
// 新規作成ボタン(ペンアイコン)が表示されることを確認
await expect(page.getByRole('main').getByRole('link').first()).toBeVisible();
// 日記エントリーの英訳された内容(見出し)が表示されることを確認
await expect(page.getByText('The weather was really nice today. I took a walk in the park this morning and saw beautiful cherry blossoms. I was happy to feel the arrival of spring.').first()).toBeVisible();
// 日記エントリーの日本語原文が表示されることを確認
await expect(page.getByText('今日はとても良い天気でした。朝から公園を散歩して、美しい桜を見ました。春の訪れを感じることができて嬉しかったです。').first()).toBeVisible();
// 日付(月)が表示されることを確認
await expect(page.getByText('10').first()).toBeVisible();
// 日付(日)が表示されることを確認
await expect(page.getByText('18').first()).toBeVisible();
// 任意の日記カードをクリック
await page.getByRole('heading', { name: 'The weather was really nice' }).first().click();
// 日記詳細ページに遷移することを確認(URL に /user/diary/{id} が含まれる)
await expect(page.url()).toMatch(/\/user\/diary\/\d+/);
// 詳細ページに年が表示されることを確認
await expect(page.getByText('2025')).toBeVisible();
// 詳細ページに日付が表示されることを確認
await expect(page.getByText('10-18')).toBeVisible();
// 日記一覧に戻るボタンが表示されることを確認
await expect(page.getByRole('link', { name: '日記一覧' })).toBeVisible();
// 日記一覧に戻るボタンをクリック
await page.getByRole('link', { name: '日記一覧' }).click();
// ダッシュボードに戻ることを確認
await expect(page).toHaveURL('http://127.0.0.1:8000/user/diary');
});
作成されたテストケースは各要素を細かくアサートしてくれており、コメントも十分なほど付いているのでコードを詳しく読まなくても何をテストしているのかがよくわかります。
これらをほぼ自動で作ってくれるという点でとても凄いAgents機能ですが、一方、現時点で気になった点もありましたので以下にまとめます。
・テストごとにログイン処理のコードが記述されている
バリデーションテスト・一覧表示確認のテストそれぞれにログイン処理のコードが含まれていました。ログイン後の画面で実施するテストであれば、テストに直接関係しないログイン動作は省略するのが理想的です。
テストプラン作成〜テストコード作成の流れの中では、Playwright側から「ログイン動作をまとめる」という提案は特にありませんでした。本来、認証関連はseed.spec.tsなどにまとめるべきなので、プラン作成前の段階であらかじめ方針を決めておく必要がありそうです。
・テストのたびに異なる値を固定値でチェックしている
日記カードの表示確認テストでは、日記タイトルや内容を固定の文字列で比較して存在確認を行うコードになっていました。しかし、これらの値は実行のたびに変化する可能性があるため、固定値での比較は望ましくありません。
他のテスト項目でも同様に、固定値でチェックする形になっている箇所が複数あり気になりました。こちらもあらかじめルールを設けておく必要がありそうです。
・外部APIのモック化
日記の新規作成時には、AI翻訳のため外部APIとの通信が発生する仕組みです。テスト実行のたびにこのAPI通信が発生し課金も伴うため、できればテスト環境ではモック化を検討して欲しかったです。
この点も、プラン作成前に「外部通信をどう扱うか」を明確にしておく必要がありそうです。
まとめ
今回はClaude Codeへの指示文もシンプルで、ほぼAI任せの状態で作成したことを考えると、事前の準備次第ではもっと精度の高いテストが作成できると思います。
このAgents機能はリリースされてから間もないため、今後どんどんアップデートされることを踏まえると、近いうちに人間不在でのE2Eテスト作成〜実行が現実味を帯びてきたなと感じます。
メルマガ購読の申し込みはこちらから。

