codegenで録画したテストが、思ったより壊れやすかった話
Playwrightを触り始めてまずやることといえば、npx playwright codegenでブラウザ操作を録画してテストコードを作ることだと思います。私も初めて動かしたときは「クリックするだけでコードが出てくる」ことに素直に感動しました。
ただ、しばらく使っていると気づきます。録画したテストの一部が、ちょっとしたマークアップの変更ですぐ落ちるのです。原因を見てみると、こんなロケータが混ざっていました。
await page.locator('div.card:nth-child(3) > button.btn-primary').click();
nth-childで位置を数えているだけのロケータです。デザイナーがカードを1枚追加しただけで、3番目の意味が変わってテストが落ちます。CSSセレクタでテストを書いていれば誰もが一度は経験することだと思います。
codegenは元々role系ロケータを優先している
ここで正直に訂正しておきたいのですが、最近のPlaywrightのcodegenは何も考えずにCSSセレクタを吐いているわけではありません。クリックした要素にroleやアクセシブルな名前が取れれば、getByRoleやgetByTextを優先して生成してくれます。
// アクセシブルな名前が取れる要素はこう出る
await page.getByRole('button', { name: 'ログイン' }).click();
問題は、対象の要素にroleもラベルもテキストも紐付いていない場合です。divにonclickだけ付けたようなカスタムUIや、アイコンだけのボタンだと、codegenはブラウザから意味を読み取れません。結果としてnth-childを含むCSSセレクタにフォールバックします。ここが今回の本題です。
ここからが本番です。AIに書き換えてもらいましょう
意味の取れない要素をロケータにするしかない、という状況そのものはcodegenの限界です。ここで効くのが、Claude CodeのようなAIエージェントに「録画したテストを意味ベースのロケータへ書き換えてもらう」というやり方です。Playwright公式も1.56からGenerator/Planner/Healerという3つのAIエージェント定義を配布していて、次のコマンドで自分のプロジェクトに入れられます。
npx playwright init-agents --loop=claude
実行すると.claude/agents/にPlanner・Generator・Healerのエージェント定義と、Playwright MCPサーバーに繋ぐ.mcp.jsonが生成されます。Generatorはこの中の1つで、テスト計画やseedのテストファイルを読み込んで、実際にブラウザを動かしながらロケータを検証してテストコードを書いてくれる係です。
正直なところ、ここまで大掛かりな仕組みを組まなくても、手元のClaude Codeに録画したテストファイルをそのまま渡して「意味ベースのロケータに書き換えて」と頼むだけでも十分実用的です。私も普段はこちらのやり方が多いです。
実際に書き換えさせてみる
先ほどのnth-childのコードを、AIエージェントに渡してみます。プロンプトはシンプルで大丈夫です。
このPlaywrightのテストコードのロケータを、getByRole/getByLabel/getByTextなど意味ベースのロケータに書き換えてください。
対象のHTMLも一緒に読んで、aria-labelやroleが無い要素にはHTML側にも最小限のaria-labelを提案してください。
返ってきたコードはこんな形でした。
// before(codegenの録画そのまま)
await page.locator('div.card:nth-child(3) > button.btn-primary').click();
// after(AIによる書き換え)
await page.getByRole('button', { name: 'カートに追加' }).click();
あわせて、元のHTML側にaria-label="カートに追加"を足す提案も出てきました。ボタンの中身がアイコンだけで、そのままではrole取得の名前が空になっていたためです。ここはAIまかせにせず、実際のボタンの文脈と合っているか自分の目で確認しました。
生成物をそのまま信じないための検証
AIが書き換えたロケータは、そのまま鵜呑みにしないようにしています。私が必ずやっているのは次の3つです。
- 書き換え後のテストを実際に一度実行して、緑になることを確認する
- 同じ
nameを持つ要素が画面内に複数ないか、getByRoleの結果件数を確認する(複数ヒットするとstrict modeでエラーになります) - 提案されたaria-labelが、実際にそのボタンの意味と一致しているか読み直す
特に2つ目は見落としがちなので注意です。同じ「削除」ボタンが1画面に5個並んでいるようなリスト画面では、getByRole('button', { name: '削除' })だけでは足りません。行の中身までスコープを絞る必要があります。
// 複数ヒットしてしまう例
await page.getByRole('button', { name: '削除' }).click();
// 行ごとにスコープを絞る
await page.getByRole('row', { name: '田中太郎' }).getByRole('button', { name: '削除' }).click();
AIに書き換えを頼むときも、このスコープ絞りまで一発で当ててくれるとは限りません(試した範囲では、複数行あるテーブルだと見落とすことがそこそこありました)。ここは人がレビューして直す前提でいた方が安全です。
演習:手元の録画テストを1本書き換えてみる
手元にcodegenで録画したテストがあれば、1本選んで同じようにAIエージェントに書き換えを頼んでみてください。ヒント:まずnth-childや.classを含む行だけを検索して、そこだけ渡すと結果が読みやすくなります。
まとめ
というわけで、codegenはもう何も考えずにCSSセレクタを吐くツールではありません。role系ロケータを優先してくれますが、アクセシブルな情報を持たない要素にはどうしても限界があります。その先の書き換えをAIエージェントに任せると、手作業でnth-childを1個ずつ潰していくよりずっと速く進みます。ただし生成されたロケータが複数ヒットしていないか、aria-labelの提案が意味的に正しいかは、必ず自分の目で確認してから使ってください。この検証の手間を惜しむと、壊れにくいはずのロケータでまた同じ目に遭います。
役割ベースのロケータがなぜ壊れにくいのか、仕組みからもう少し追いたい方はSeleniumからPlaywrightへの移行ガイドのセレクタの節もあわせてどうぞ。落ちたテストをAIに直させる話は壊れたテストをAIが直す:自己修復(Healer)を試す、アサーションの書き方はPlaywrightのexpectマッチャー完全リファレンス、AIとブラウザを繋ぐ入口はPlaywright MCP入門にまとめています。

コメント