2025.11.05

Playwright Agents を Codex CLI に対応させブラウザ自動テストを実装してもらう(公式が未サポートだけどできました)

AI ツール大好き D.M.です。

結論ファースト

・Playwright Agents は LLM と Playwright MCP でブラウザ自動テストを設計、実装、修正ができるAIエージェント。
・Playwright Agents は Codex CLI 未対応だが、 AGENTS.md をフォルダを分けて作成することでほぼ問題なく利用できる。

このブログでやりたいこと

Codex CLI で Playwright Agents を呼び出し、ブラウザ自動テストを設計実装してもらいたい。
感触としては結構簡単に実現できます。

[アジェンダ]
導入編: Playwright Agents を Codex CLI で動かす
→ Codex CLI は公式サポートされていないけど使えるんだっけという課題を解決します。

実践編: Playwright Agents にブラウザテストを設計・実装してもらう
→ このブログサイトをテスト対象にして実装させてみました。

導入編: Playwright Agents を Codex CLI で動かす

ブラウザ自動テストの本命 Playwright がついに完全AIエージェント化

Playwright はブラウザでの動作確認など自動操作をするにはとても便利なライブラリです。
従来は人間が Playwright をベースにした自動処理を実装していましたが、 ChatGPT など LLM の発展ともに AI と連携するための以下の2つが登場しました。
Playwright MCP
Playwright Agents
これによりAIエージェントとして自動的にブラウザテストが作成できるようになりました。

Playwright MCP と Playwright Agents の違い

共通点として、両方とも LLM で Playwright を利用することを目的としています。

Playwright MCP は LLM によるブラウザ単発操作を可能にします。
LLMが実際に「ブラウザの画面を実際に操作してここ確認したいな」というときに開いてクリックしてという人間と同じような操作ができるようになります。
こちらの記事でがっつり利用してます。
「playwright mcpをAI駆動開発フローに組み込んでE2Eテストをさせよう」
https://recruit.group.gmo/engineer/jisedai/blog/e2e-test-by-playwright-mcp/

Playwright Agents は LLM にテストを作成してもらうことができるAIエージェントです。
具体的には、初期プロンプトファイルにより
「テスト計画作成の担当者 Planner」
「テスト実装生成の担当者 Generator」
「テスト結果修正の担当者 Healer」
の3つのエージェントを設定し、それぞれに役割の作業を担当してもらうことができます。
実際にブラウザで画面を確認しながらテストを作成するため Playwright MCP が利用可能なことが前提となっています。

Playwright Agents でテストを作成するメリット

正直、自動でテストできる点だけ見るとメリットは自明なのですが、ただ LLM + Playwright MCP があればわざわざエージェントでテストを実装として作らなくてもいいのではという意見もあるかと思います。
問題として、 LLM に毎回 Playwright MCP でその場でアドホック的にテストさせる場合、当然大量のHTMLや画面画像なりを読むのでそれなりにトークンを食います。
一方 Playwright Agents で定型的なテストシナリオを実装してしまえば、テストのスクリプトで人間や他の自動処理が自動テストを実行できるようになります。結果としてリグレッションの E2E テストは LLM に依存せずに低コストで実行できるようになるというメリットが生み出せると思います。

※通常 Playwright は Python と TypeScript が使えますが、現状 Playwright Agents は TypeScript のみのサポートでした。(2025年11月現在)
TypeScript 前提で進めていきます。

Playwright Agents x Codex CLI 環境設定の課題

インストールは楽勝です。念のため公式を見てもらったほうがいいかと。https://playwright.dev/docs/intro

mkdir playwright-test-repo
cd playwright-test-repo

npm init -y
npm i -D @playwright/test
npx playwright install

課題: Playwright Agents が Codex 未対応

私は Windows 環境 WSL 2 Ubuntu 24 04 上に Codex を入れて、 OpenAI ログインで利用しています。
しかしなんと Playwright Agents は Codex に対応しておらず(!)、Claude Code, VSCode GitHub Copilot, OpenCode の3つのみで利用可能です。(2025年11月現在)
Playwrightの開発元はMicrosoftですよ、MSとOpenAIは提携してるんじゃなかったんかい
会社では OpenAI の強めな有料プラン契約があるので Codex を活用しないわけにはいきません。
今回は無理やり Codex で Playwright Agents を動かしていきます。

※ Claude Code と GitHub Copilot は業務都合で1回解約状態でした。
※ Windows Powershell に直接 Codex をインストールできるんですが、OpenAIログインでの有料プラン枠を利用することができずに断念。(なぜかログイン後のCallbackでこける)(たぶんAPIなら動くと思う)

Codex で Playwright Agents を動かす方法

Playwright Agents はそもそもどのように動いているのか

Playwright Agent は「LLMに初期プロンプトとして読み込ませるmdファイル」として実装されています。
mdファイルには「あなたはこういう役割を持ったエージェントなのでこういう基準でアウトプットしてね」と書いてあり、 LLM はこれに従っています。

例えば Claude Code で利用する場合、初期化すると以下のようにmdが生成されます。

# Agents の定義を生成(Claude Code 連携)
npx playwright init-agents --loop=claude
playwright-test-repo
├── .claude/agents
│    ├── playwright-test-generator.md
│    ├── playwright-test-healer.md
│    └── playwright-test-planner.md
├── .mcp.json
└── seed.spec.ts

Claude Code は playwright-test-planner.md などにある指示をエージェントとして読み込んで動作します。
このフォルダ構成は Claude Code が @planner のようなコマンドで agent の初期プロンプトを切り替えてタスクを実行させることを前提としています。

Codex で Playwright Agent が動かしずらい理由

Codex でも AGENTS.md を初期プロンプトとして読ませてエージェント的な役割を指定して動作させることができますが、 Claude Code に比べて Codex は Agent を複数切り分ける機能があまり明示的に用意されていません。
(Roleのような概念を使えばできるというのが公式に書いていあるのだが、実装例がなく、すぐわからなかった。。)

対策として、 Codex では起動ディレクトリの AGENTS.md に planner.md と同じ内容を記載して動かすことにしました。
複数の AGENTS.md を読ませることもできますが、エージェントをスイッチさせることが明示的にできない点と、プロンプトが多すぎると役割が精度が落ちる可能性があったので、 planner, generator, healer で完全にフォルダを分けて別で起動させるということにしました。

Playwright Agents 公式が Codex 未対応としているため上記の Claude Code のように1発でフォルダ構成を作ることができないのですが、人間が少し手を動かすとCodexでも Playwright Agents の各エージェントが動作可能な設定を作成できます。
初めに1回 Claude Code で生成したファイルを、以下の Codex CLI 用の構成で配置しなおします。

playwright-test-repo
├── planner
│    └── AGENTS.md (元は planner.md )
├── generator
│    └── AGENTS.md (元は generator.md )
├── healer
│    └── AGENTS.md (元は healer.md )
└── seed.spec.ts

planner フォルダの AGENTS.md は、単純に元ネタの planner.md からリネームしています。これがCodex上でエージェントの初期プロンプトとして読み込まれます。
Planner エージェントのタスクが終了した後は Codex を終了し、 Generator エージェントのフォルダで別途 Codex を起動します。

この方法の欠点

Codex のルートフォルダがエージェントごとに変わるので、 planner フォルダで生成された test-plan.md を generator フォルダ側にコピーしてコンテキストに入れなおす手間が必要になってしまいます。
(もっといい方法があるかもですが、まあコピーを1,2ファイルだけでとりあえずすぐ動かせたので許してください)

Codex 用の Playwright MCP 設定

以下のConfigに独自に書き加えます。

vi ~/.codex/config.toml
[mcp_servers.playwright]
command = "npx"
args    = ["@playwright/mcp@latest", "--isolated", "--headless" , "--no-sandbox"]

Codex 起動時に MCP は裏側でプロセスとして動作を開始します。
以下のコマンドで MCP 設定が読まれて動作しているかが確認できます。
/mcp

LLM は Playwright MCP が利用できない場合は Curl などで頑張ってやろうとするので微妙に効率が落ちてしまいます。確実に設定していきましょう。

以下の環境設定が整いました。

Windows 11 WSL 2 Ubuntu 24 04
Codex CLI (v0.53.0)
OpenAI Login (有料プラン)
gpt-5-codex
Node.js (v22.21.0)
Playwright MCP
Playwright Agents (1.56.1)

実践編: Playwright Agents にブラウザテストを設計・実装してもらう

[やりたいこと]
このブログサイトで “Playwright” という文字を含む記事を検索するテストをAIエージェントに作ってほしいです。
https://recruit.group.gmo/engineer/jisedai/blog/

Playwright Agents で以下の3つの工程を進めていきます。
・計画フェーズ ( Planner エージェント)
・実装フェーズ ( Generator エージェント)
・修正フェーズ ( Healer エージェント)

計画フェーズ ( Planner エージェント)

モデルは gpt-5-codex です。
codexを planner エージェントとして起動します。
Playwright MCP を使って対象サイトを巡回しテスト目的に応じた操作をして仕様を把握してテスト計画を書いてくれます。

cd playwright-test-repo/planner
codex # planner/AGENTS.md がエージェント初期プロンプトとして読み込まれる

[Plannerエージェントの初期プロンプト]

(長大なので折り畳んであります)(元は英語ですが日本語訳バージョン)

品質保証、ユーザーエクスペリエンステスト、テストシナリオ設計において豊富な経験を持つ、Webテストプランナーとしてのエキスパートです。

専門分野には、機能テスト、エッジケースの特定、包括的なテストカバレッジ計画が含まれます。

以下の内容を実施します。

1. **ナビゲーションと探索**
– 他のツールを使用する前に、`planner_setup_page` ツールを一度呼び出してページをセットアップします。
– ブラウザのスナップショットを探索します。
– 絶対に必要な場合を除き、スクリーンショットを撮らないでください。
– browser_* ツールを使用してインターフェースをナビゲートし、探索します。
– インターフェースを徹底的に探索し、すべてのインタラクティブ要素、フォーム、ナビゲーションパス、および機能を特定します。

2. **ユーザーフローを分析**
– 主要なユーザージャーニーをマッピングし、アプリケーションにおけるクリティカルパスを特定します。
– さまざまなユーザータイプとその典型的な行動を考慮します。

3. **包括的なシナリオを設計**

以下の項目を含む詳細なテストシナリオを作成します。
– ハッピーパスシナリオ(通常のユーザー行動)
– エッジケースと境界条件
– エラー処理と検証

4. **テスト計画の構築**

各シナリオには以下を含める必要があります。
– 明確で説明的なタイトル
– 詳細な手順説明
– 必要に応じて期待される結果
– 開始状態に関する前提条件(常に空白/新規状態
– 成功基準と失敗条件

5. **ドキュメントの作成**
要求に応じてテスト計画を保存します。
– テスト対象のページ/アプリケーションのエグゼクティブサマリー
– 個々のシナリオを個別のセクションにまとめる
– 各シナリオを番号付きのステップでフォーマットする
– 検証のために期待される結果を明確にする

# TodoMVC アプリケーション – 包括的なテスト計画

## アプリケーションの概要

TodoMVC アプリケーションは、コアとなるタスク管理機能を提供する React ベースの ToDo リストマネージャーです。アプリケーションの機能:

– **タスク管理**: 個々の ToDo を追加、編集、完了、削除
– **一括操作**: すべての ToDo を完了/未完了としてマークし、完了済みの ToDo をすべてクリア
– **フィルタリング**: ToDo を「すべて」、「アクティブ」、「完了」のステータスで表示
– **URL ルーティング**: URL 経由でフィルタリングされたビューに直接移動可能
– **カウンター表示**: アクティブ (未完了) ToDo のリアルタイムカウント
– **永続性**: セッション中は状態が維持されます (ブラウザの更新動作はテストされていません)

## テストシナリオ

### 1. 新しい ToDo の追加

**シード:** `tests/seed.spec.ts`

#### 1.1 有効な ToDo を追加
**手順:**
1. 「何をする必要がありますか?」をクリックします。入力フィールド
2. 「食料品を購入」と入力
3. Enterキーを押す

**期待される結果:**
– チェックボックスがオフになった状態でToDoがリストに表示される
– カウンターに「残り1項目」と表示される
– 入力フィールドの内容がクリアされ、次の入力が可能になる
– ToDoリストのコントロールが表示される(「すべて完了としてマーク」チェックボックス)

#### 1.2

**品質基準**:
– どのテスターでも実行できるほど具体的な手順を記述する
– ネガティブテストシナリオを含める
– シナリオが独立しており、任意の順序で実行できることを確認する

**出力形式**: 完全なテスト計画は、明確な見出し、番号付きステップ、そして開発チームやQAチームと共有しやすいプロフェッショナルなフォーマットで、必ずマークダウンファイルとして保存してください。

[私の指示プロンプト]
コンテキスト に seed.spec.ts を追加。 playwright planner として https://recruit.group.gmo/engineer/jisedai/blog/ で Playwright という文字で検索するテスト計画mdを作成できますか

Codex の Planner Agent が Playwright MCP をつかってサイトを確認し始めます。

そこそこの時間とトークンを使いました。

─ Worked for 6m 07s ────
Token usage: total=148,901 input=135,937 (+ 886,656 cached) output=12,964 (reasoning 8,128)

作成されたテストシナリオは以下のような感じでした。

シナリオ1: ブログトップ表示
シナリオ2: Enterキーで検索を実行
シナリオ3: 「Playwright」検索結果ページの表示要素を検証
シナリオ4: 検索結果から記事詳細へ遷移して内容を確認
シナリオ5: 該当なしキーワードでの検索メッセージ確認
シナリオ6: 空検索やスペースのみ入力時の挙動を確認
シナリオ7: 大文字・小文字・前後スペースの正規化確認

詳細は以下で読むことができます。それなりにいい内容だと思いました。

テスト計画md(長大なので折り畳んであります)

# GMOエンジニアブログ「Playwright」検索テスト計画

## エグゼクティブサマリー

– 対象は「GMOインターネットグループ グループ研究開発本部」サイト内のエンジニアブログ (`https://recruit.group.gmo/engineer/jisedai/blog/`) と、その検索機能。
– ブログページにはグローバルナビゲーション、記事一覧、サイドバーウィジェット(検索フォーム、バナー、タグクラウドなど)が表示される。検索フォームはサイドバー上部に配置され、`検索:` ラベル付きのテキストボックスと `検索` ボタンで構成される。
– 検索実行後は `?s=<入力値>` 付きURLの検索結果ページに遷移し、メインカラムに「「<検索語>」の検索結果」という見出しと一致記事のカードが並ぶ。各カードには公開日、タイトル、関連タグが表示され、クリックで詳細記事へ遷移できる。
– ヒットが無い場合は「お探しの記事は見つかりませんでした。」というメッセージが表示され、サイドバーには検索フォームとタグクラウドが継続して表示される。
– サイトはWordPress系構成であり、ブラウザバック時に検索フォームへ入力した語が保持される。サイズの大きいメインカラムのため、検索フォームが初期表示で見えない場合はスクロールが必要。

## テストシナリオ

### シナリオ1: ブログトップから検索ボタンで「Playwright」を検索
**前提:** デスクトップブラウザ、キャッシュ/クッキーをクリアした新規セッション。
**手順:**
1. `https://recruit.group.gmo/engineer/jisedai/blog/` を開く。
– 期待結果: タイトルに「エンジニアブログ」が表示され、記事カード一覧とサイドバーが読み込まれる。
2. サイドバー上部の検索フォームが見える位置までスクロールし、`検索:` テキストボックスと `検索` ボタンを確認する。
– 期待結果: テキストボックスが空でフォーカス可能になっている。
3. テキストボックスに「Playwright」と入力する。
– 期待結果: 入力値が反映され、IME確定後も保持される。
4. `検索` ボタンをクリックする。
– 期待結果: ページがリロードされ、URLに `?s=Playwright` が付与された検索結果ページへ遷移する。
**成功条件:** 検索結果ページが表示され、後続シナリオの検証に必要な要素が揃う。
**失敗条件:** 検索フォームが見つからない、クリック後に遷移しない、別ドメインへリダイレクトされるなど。

### シナリオ2: Enterキーで検索を実行
**前提:** シナリオ1完了後、またはブログトップを再度開いた状態。
**手順:**
1. 検索フォームを表示し、テキストボックスに「Playwright」を入力する。
– 期待結果: 入力値がフィールドに表示される。
2. フォーカスをテキストボックスに残したまま Enter キーを押下する。
– 期待結果: `検索` ボタンを押した場合と同様にページ遷移し、URLに `?s=Playwright` が付与される。
**成功条件:** キーボード操作のみで検索が成立し、表示結果がシナリオ1と一致する。
**失敗条件:** Enter押下で何も起きない、または別のページ遷移が発生する。

### シナリオ3: 「Playwright」検索結果ページの表示要素を検証
**前提:** `https://recruit.group.gmo/engineer/jisedai?s=Playwright` に遷移済み。
**手順:**
1. ブラウザタブのタイトルを確認する。
– 期待結果: 「Playwright – GMOインターネットグループ グループ研究開発本部」と表示され、検索語がタイトルに含まれる。
2. メインカラムの見出しを確認する。
– 期待結果: `「Playwright」の検索結果` と表示される。
3. 記事カードをすべて確認する。
– 期待結果: 5件(最低でも1件)以上のカードが存在し、各カードに公開日、クリック可能なタイトル、タグ一覧が表示される。最上段の記事タイトルが「playwright mcpをAI駆動開発フローに組み込んでE2Eテストをさせよう」である。
4. サイドバーの検索フォームを確認する。
– 期待結果: テキストボックスに `Playwright` が保持されている。
**成功条件:** 検索結果ページで必要なメタ情報・リスト構造・フォームの状態保持が確認できる。
**失敗条件:** 見出しの表記が欠落、件数が0件、カードの構造が崩れているなど。

### シナリオ4: 検索結果から記事詳細へ遷移して内容を確認
**前提:** シナリオ3完了後。
**手順:**
1. 検索結果の1件目(`playwright mcpをAI駆動開発フローに組み込んでE2Eテストをさせよう`)のタイトルリンクをクリックする。
– 期待結果: 記事詳細 (`…/blog/e2e-test-by-playwright-mcp/`) に遷移し、読み込みが正常に完了する。
2. 記事ヘッダーの情報を確認する。
– 期待結果: 公開日が `2025.09.30` 形式で表示され、H1が検索結果で確認したタイトルと一致する。
3. タグ一覧を確認する。
– 期待結果: タグの中に `playwright` が存在し、クリック可能である。
4. ブラウザの戻る操作を実行する。
– 期待結果: 検索結果ページに戻り、検索語 `Playwright` が保持された状態で記事リストが再表示される。
**成功条件:** 詳細ページの主要情報が正しく表示され、戻る操作で検索状態が維持される。
**失敗条件:** タイトル不一致、タグ欠落、戻る操作で検索語が消えるなど。

### シナリオ5: 該当なしキーワードでの検索メッセージ確認
**前提:** ブログトップ、または検索結果ページの検索フォームが利用可能。
**手順:**
1. テキストボックスに存在しない語(例: `ZXY999`)を入力する。
– 期待結果: 入力値が反映される。
2. `検索` ボタンをクリックする。
– 期待結果: URLが `?s=ZXY999` となり、メインカラム見出しが `「ZXY999」の検索結果` に変わる。
3. メインカラムのメッセージを確認する。
– 期待結果: `お探しの記事は見つかりませんでした。` の文言が表示され、記事カードは生成されない。
**成功条件:** 該当なしメッセージが表示され、不要な空カードが存在しない。
**失敗条件:** 空結果でも記事カードが表示される、エラーページへ遷移するなど。

### シナリオ6: 空検索やスペースのみ入力時の挙動を確認
**前提:** ブログトップまたは検索結果ページ上の検索フォームが利用可能。
**手順:**
1. テキストボックスを空のまま `検索` ボタンをクリックする。
– 期待結果: 入力が必須であれば警告が表示される、もしくは検索が実行されない。(現状観測: 全件リストが表示され、見出しが `「」の検索結果` となる)
2. テキストボックスにスペースのみ(例: 半角スペース2個)を入力して `検索` ボタンをクリックする。
– 期待結果: 不正入力として扱われるか、自動的にトリムされ `Playwright` と同じ検索結果が返る。(現状観測: URLに `s=++` が付き、全件リストが表示される)
**成功条件:** 入力バリデーションの仕様が明確になり、想定と異なる場合は不具合として報告できる。
**失敗条件:** 空入力で不正なページ遷移が起きる、エラー表示が崩れるなど。

### シナリオ7: 大文字・小文字・前後スペースの正規化確認
**前提:** シナリオ1で基準となる検索結果を取得済み。
**手順:**
1. `playwright`(小文字)で検索を実行する。
– 期待結果: URLが `?s=playwright` となり、表示される記事ラインアップと件数が `?s=Playwright` の結果と一致する。
2. `PLAYWRIGHT`(全大文字)で検索を実行する。
– 期待結果: 大文字小文字に関わらず同じ結果が返る。
3. 先頭末尾にスペースを含む ` Playwright `(前後半角スペース)で検索を実行する。
– 期待結果: 結果の集合が基準検索と一致し、検索結果見出しやURLの表記揺れ(`「 Playwright 」` や `s=++Playwright++`)が仕様として許容されるか判断できる。
**成功条件:** 検索ロジックが大文字小文字やスペースの差異を吸収して同一結果を返す。
**失敗条件:** 文字種の違いで結果が欠落する、スペースのみで内部エラーが発生するなど。

実装フェーズ ( Generator エージェント)

Planner エージェントが作成した計画mdファイルに基づいて Playwright で自動テストをする TypeScript を実装してもらいます。

[私の指示プロンプト]
コンテキストに gmo-blog-playwright-search.md seed.spec.ts を追加。 gmo-blog-playwright-search.md にそって、テストを作成してください。

─ Worked for 7m 57s 
Token usage: total=146,099 input=130,147 (+ 2,318,592 cached) output=15,952 (reasoning 12,352)

さあ、実装は出てきたのですが。。

テストコードその1(長大なので折り畳んであります)
// spec: gmo-blog-playwright-search.md
// seed: seed.spec.ts

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

test.describe('シナリオ1: ブログトップから検索ボタンで「Playwright」を検索', () => {
  test('シナリオ1: ブログトップから検索ボタンで「Playwright」を検索', async ({ page }) => {
    // 1. `https://recruit.group.gmo/engineer/jisedai/blog/` を開く。
    await page.goto('https://recruit.group.gmo/engineer/jisedai/blog/');
    await expect(page).toHaveTitle(/エンジニアブログ/);
    await expect(page.locator('main article').first()).toBeVisible();

    // 2. サイドバー上部の検索フォームが見える位置までスクロールし、`検索:` テキストボックスと `検索` ボタンを確認する。
    const searchTextbox = page.getByRole('textbox', { name: '検索:' });
    await searchTextbox.scrollIntoViewIfNeeded();
    await expect(searchTextbox).toBeVisible();
    await expect(searchTextbox).toBeEditable();
    await expect(searchTextbox).toHaveValue('');
    const searchButton = page.getByRole('button', { name: '検索' });
    await expect(searchButton).toBeVisible();

    // 3. テキストボックスに「Playwright」と入力する。
    await searchTextbox.fill('Playwright');
    await expect(searchTextbox).toHaveValue('Playwright');

    // 4. `検索` ボタンをクリックする。
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=Playwright$/),
      searchButton.click(),
    ]);
    await expect(page).toHaveTitle(/Playwright - GMOインターネットグループ/);
    await expect(page.getByRole('heading', { level: 1, name: '「Playwright」の検索結果' })).toBeVisible();
    await expect(page.locator('main article')).not.toHaveCount(0);
  });
});

よく読むとわかりますが、もともと計画されていたテストは7つあったのに明らかに不足しています(笑)
(Playwright Agents というよりか GTP-5-Codex の実力の問題と思う)
やっぱまだLLMっぽいいい加減さがあるかーって感じと思いながらも、リライトしてもらいます。

[私の指示プロンプト]
コンテキストに gmo-blog-playwright-search.md blog-top-search-playwright.spec.ts を追加。 gmo-blog-playwright-search.md にそって、テスト blog-top-search-playwright.spec.ts がありますが、不足しています。不足内容を追加してほしいです。

さらにもう1点、検索結果の確認項目が細かすぎるので修正してもらいます。
(記事が追加されたら検索結果が変わりますので、もうちょっと緩めにしてロバストなテストにします)

[私の指示プロンプト]
blog-top-search-playwright.spec.ts シナリオ3 3. 記事カードをすべて確認する。ここで記事タイトルを完全一致で確認していますが、このような厳密な確認は不要です。記事タイトルに Playwright という文字を含んでいれば充分なので、その確認をするように修正してほしいです。

本気を出してもらったので結構な時間がかかっていました。

─ Worked for 17m 52s ─  
  Token usage: total=487,299 input=453,422 (+ 6,083,456 cached) output=33,877 (reasoning 25,216)

出来上がったテストを人間が実行していきます。
実行コマンド

npx playwright test blog-top-search-playwright.spec.ts

なんかエラーが起きてしまったので、 generator のほうでデバッグします。

[私の指示プロンプト]
blog-top-search-playwright.spec.ts ../test-results/blog-top-search-playwright-84801-グトップから検索ボタンで「Playwright」を検索/error-context.md をコンテキストに入れてください。
Error が起きました。
Test timeout of 30000ms exceeded.

Error: page.waitForURL: Test timeout of 30000ms exceeded.
=========================== logs ===========================
waiting for navigation until “load”
============================================================

65 | const firstResultLink = firstCard.locator(‘a’).first();
66 | await Promise.all([
> 67 | page.waitForURL(/\/blog\/e2e-test-by-playwright-mcp\/$/),
| ^
68 | firstResultLink.click(),
69 | ]);
70 |

エラー原因はどうやらlocatorとURLに曖昧なところがあったので、より明示的に指定するように修正してもらいました。

    65 -    const firstResultLink = firstCard.locator('a').first();
    65 +    const firstResultLink = page.locator('main li:has(article) > a').first();
    66 +    await expect(firstResultLink).toBeVisible();
    67      await Promise.all([
    67 -      page.waitForURL(/\/blog\/e2e-test-by-playwright-mcp\/$/),
    68 +      page.waitForURL(/\/engineer\/jisedai\/blog\/[\w-]+\/$/),
    69        firstResultLink.click(),

結果、動くようになりました。

テストコードその2(長大なので折り畳んであります)
// spec: gmo-blog-playwright-search.md
// seed: seed.spec.ts

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

const BLOG_TOP_URL = 'https://recruit.group.gmo/engineer/jisedai/blog/';

test.describe('GMOエンジニアブログ「Playwright」検索テスト計画', () => {
  test('シナリオ1: ブログトップから検索ボタンで「Playwright」を検索', async ({ page }) => {
    const readResultTitles = async () => {
      const titles = await page.locator('main article h2').allTextContents();
      return titles.map(title => title.trim());
    };

    // 1. `https://recruit.group.gmo/engineer/jisedai/blog/` を開く。
    await page.goto(BLOG_TOP_URL);
    await expect(page).toHaveTitle(/エンジニアブログ/);
    await expect(page.locator('main article').first()).toBeVisible();

    const searchTextbox = page.getByRole('textbox', { name: '検索:' });
    const searchButton = page.getByRole('button', { name: '検索' });

    // 2. サイドバー上部の検索フォームが見える位置までスクロールし、`検索:` テキストボックスと `検索` ボタンを確認する。
    await searchTextbox.scrollIntoViewIfNeeded();
    await expect(searchTextbox).toBeVisible();
    await expect(searchTextbox).toBeEditable();
    await expect(searchTextbox).toHaveValue('');
    await expect(searchButton).toBeVisible();

    // 3. テキストボックスに「Playwright」と入力する。
    await searchTextbox.fill('Playwright');
    await expect(searchTextbox).toHaveValue('Playwright');

    // 4. `検索` ボタンをクリックする。
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=Playwright$/),
      searchButton.click(),
    ]);
    await expect(page).toHaveTitle(/Playwright - GMOインターネットグループ/);
    const playwrightHeading = page.getByRole('heading', { level: 1, name: '「Playwright」の検索結果' });
    await expect(playwrightHeading).toBeVisible();

    // シナリオ3 1. ブラウザタブのタイトルを確認する。
    await expect(page).toHaveTitle('Playwright - GMOインターネットグループ グループ研究開発本部');

    // シナリオ3 2. メインカラムの見出しを確認する。
    await expect(playwrightHeading).toBeVisible();

    // シナリオ3 3. 記事カードをすべて確認する。
    const resultCards = page.locator('main article');
    const cardCount = await resultCards.count();
    expect(cardCount).toBeGreaterThanOrEqual(5);
    const firstCard = resultCards.first();
    //await expect(firstCard.locator('p').first()).toHaveText('2025/09/30');
    await expect(firstCard.locator('h2')).toHaveText(/Playwright/i);
    const firstCardTags = (await firstCard.locator('li').allTextContents()).map(tag => tag.trim()).filter(Boolean);
    expect(firstCardTags.length).toBeGreaterThan(0);
    expect(firstCardTags).toContain('playwright');
    const expectedPlaywrightResults = await readResultTitles();

    // シナリオ3 4. サイドバーの検索フォームを確認する。
    await expect(searchTextbox).toHaveValue('Playwright');

    // シナリオ4 1. 検索結果の1件目(`playwright..`)のタイトルリンクをクリックする。
    const firstResultLink = page.locator('main li:has(article) > a').first();
    await expect(firstResultLink).toBeVisible();
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\/blog\/[\w-]+\/$/),
      firstResultLink.click(),
    ]);

    // シナリオ4 2. 記事ヘッダーの情報を確認する。
    const article = page.locator('main article').first();
    //await expect(article.locator('p').first()).toHaveText('2025.09.30');
    await expect(article.locator('h1')).toHaveText(/Playwright/i);

    // シナリオ4 3. タグ一覧を確認する。
    const playwrightTag = article.locator('li a', { hasText: 'playwright' });
    await expect(playwrightTag.first()).toHaveAttribute('href', /tag\/playwright/);

    // シナリオ4 4. ブラウザの戻る操作を実行する。
    await page.goBack();
    await expect(page).toHaveURL(/\/engineer\/jisedai\?s=Playwright$/);
    await expect(searchTextbox).toHaveValue('Playwright');

    // シナリオ5 1. テキストボックスに存在しない語(例: `ZXY999`)を入力する。
    await searchTextbox.fill('ZXY999');
    await expect(searchTextbox).toHaveValue('ZXY999');

    // シナリオ5 2. `検索` ボタンをクリックする。
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=ZXY999$/),
      searchButton.click(),
    ]);

    // シナリオ5 3. メインカラムのメッセージを確認する。
    await expect(page.getByRole('heading', { level: 1, name: '「ZXY999」の検索結果' })).toBeVisible();
    await expect(page.getByText('お探しの記事は見つかりませんでした。')).toBeVisible();
    await expect(page.locator('main article h2')).toHaveCount(0);

    // シナリオ6 1. テキストボックスを空のまま `検索` ボタンをクリックする。
    await searchTextbox.fill('');
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=$/),
      searchButton.click(),
    ]);
    await expect(page.getByRole('heading', { level: 1, name: '「」の検索結果' })).toBeVisible();
    const allResults = await readResultTitles();
    expect(allResults.length).toBeGreaterThan(0);

    // シナリオ6 2. テキストボックスにスペースのみ(例: 半角スペース2個)を入力して `検索` ボタンをクリックする。
    await searchTextbox.fill('  ');
    await expect(searchTextbox).toHaveValue('  ');
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=\+\+$/),
      searchButton.click(),
    ]);
    await expect(page.getByRole('heading', { level: 1, name: '「 」の検索結果' })).toBeVisible();
    const spaceOnlyResults = await readResultTitles();
    expect(spaceOnlyResults.length).toBeGreaterThan(0);

    // シナリオ7 1. `playwright`(小文字)で検索を実行する。
    await searchTextbox.fill('playwright');
    await expect(searchTextbox).toHaveValue('playwright');
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=playwright$/),
      searchButton.click(),
    ]);
    await expect(page.getByRole('heading', { level: 1, name: '「playwright」の検索結果' })).toBeVisible();
    expect(await readResultTitles()).toEqual(expectedPlaywrightResults);

    // シナリオ7 2. `PLAYWRIGHT`(全大文字)で検索を実行する。
    await searchTextbox.fill('PLAYWRIGHT');
    await expect(searchTextbox).toHaveValue('PLAYWRIGHT');
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=PLAYWRIGHT$/),
      searchButton.click(),
    ]);
    await expect(page.getByRole('heading', { level: 1, name: '「PLAYWRIGHT」の検索結果' })).toBeVisible();
    expect(await readResultTitles()).toEqual(expectedPlaywrightResults);

    // シナリオ7 3. 先頭末尾にスペースを含む ` Playwright `(前後半角スペース)で検索を実行する。
    await searchTextbox.fill(' Playwright ');
    await expect(searchTextbox).toHaveValue(' Playwright ');
    await Promise.all([
      page.waitForURL(/\/engineer\/jisedai\?s=\+Playwright\+$/),
      searchButton.click(),
    ]);
    await expect(page.getByRole('heading', { level: 1, name: '「 Playwright 」の検索結果' })).toBeVisible();
    expect(await readResultTitles()).toEqual(expectedPlaywrightResults);
    await expect(searchTextbox).toHaveValue(' Playwright ');
  });
});

PASS!

※ 今回 Healer のほうは出番がありませんでした。(対象がブログの SaaS なので src がありませんでした)

まとめ

・Playwright Agents は LLM と Playwright MCP でブラウザ自動テストを設計、実装、修正ができる AI エージェント。
・Playwright Agents は Codex CLI 未対応だが、 AGENTS.md をフォルダを分けて作成することでほぼ問題なく利用できる。

超便利です!
まだ精度の問題はあれど、とりあえず初期版を作る部分は短時間でできるエージェントとして重宝すると思います。

GMO は採用に力を入れています。鋭意募集中

GMOインターネットグループ グループ研究開発本部 次世代システム研究室では、最新のテクノロジーを調査・検証しながらAIやロボットに関するアプリケーション開発を行うエンジニア、アーキテクトを募集しています。募集職種一覧 からご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事