Playwright MCPによるE2Eテストコードの生成 第2回 codegenでSPAのE2Eテストコードを記録する
操作の複雑なSPAでは、自然言語でテスト内容を正確に指示するのが難しくなります。 Playwrightのcodegenでブラウザでの操作をテストコードとして記録する方法と、記録されたコードが抱える課題を解説します。
はじめに
前回は、Playwright MCPでDOMを参照させながら、自然言語の指示でE2Eテストコードを生成する方法を紹介しました。
ただし、この方法は操作対象を言葉で指示できることが前提でした。一方、CMSの管理画面や画像編集ツールのように、1つの画面に多くの部品や操作エリアが並ぶSPA(Single Page Application)では、目的の状態にたどり着くまでの操作が長くなりがちです。また、編集エリアに配置した部品には名前がないことが多く、どの部品を操作するのかを言葉で正確に指すのも難しくなります。
そこで今回からは、こうした画面のE2EテストコードをClaude Codeで作る方法を解説します。手順は次の2段階です。
- Playwrightに付属する
codegenコマンドで、ブラウザでの操作をそのままテストコードとして記録する - 記録されたコードを、Playwright MCPを使ってClaude Codeに補正させる
本記事では前半にあたるcodegenでの記録までを扱い、後半の補正は次回取り上げます。
自然言語による指示の難しさ
筆者が業務で開発しているCMSでは、ページの追加や部品の配置を、管理画面の中で画面遷移なしに行います。
テストコードの生成に入る前に、まずこのCMSを題材にして、Playwright MCPと自然言語の指示だけでどこまで正確に操作できるかを試しました。
大雑把な指示
まずは細かい手順を書かずに、やりたいことだけを伝えてみました。
プロンプト例
テスト用のページを追加し、メインエリアに画像部品を配置してください。
それを直下に複製したら、最後にページを削除してください。
このプロンプトで意図した操作を、簡略化した画面で示すと、次のようになります。
実際にこのプロンプトで操作させたところ、目的の要素にたどり着けないまま違う要素のクリックが続いたため、途中で停止させました。
Claude自身も失敗には気づいていましたが、正解は見つけられず、トークンだけを消費し続けていました。
誤った手順の指示
指示が大雑把すぎたのだと考え、操作手順を書き出すことにしました。ところが、書き出した手順に1か所間違いがありました。
プロンプト例(誤り)
- 左バーの「ページ追加」クリック
- ヘッダーの「部品を追加」クリック
- ヘッダーエリアの「ここに追加」クリック ← 誤り。正しくはメインエリア
- ディレクトリの「画像」クリック
- サブディレクトリの「画像」クリック
- フッターの「この部品を追加する」クリック
- メインエリアの「画像の説明を入力してください」クリック
- 右バーの「複製ボタン」クリック
- プルダウンの「直下に複製」クリック ← 複製機能の実行
- 左バーの最下段の「無題のページ」の設定アイコンをクリック
- サブの左バーの「ページ削除」クリック
- 削除確認のダイアログで「OK」クリック
3行目が間違いです。正しくは『メインエリアの「ここに追加」クリック』で、メインとヘッダーを書き間違えていました。
結果として、先ほどと同じようにClaudeの操作が終わらなかったため、途中で停止させました。
メインエリアには正しく「ここに追加」が表示されていましたが、Claudeはそちらに目を向けることなく、指示されたヘッダーエリアの中で迷い続けました。今回の結果を見る限り、指示の誤りをClaudeが自分で補ってくれるとは限らず、手順の正確さは書く側に委ねられます。そして手順を長く書くほど、こうした間違いは起こりやすくなるでしょう。
正しい手順の指示
3行目を「メインエリア」に修正したところ、意図したとおりに操作が進みました。
プロンプト例
- 左バーの「ページ追加」クリック
- ヘッダーの「部品を追加」クリック
- メインエリアの「ここに追加」クリック ← 修正
(以下同じ)
ただ、この12ステップはCMSの操作としてはもっとも簡単な部類で、実際のテストではさらに長い手順が必要になります。テストのたびにこれを書くのは現実的ではありません。
さらに、言葉では書き下せない操作もあります。たとえば、先ほどの手順で追加した画像部品は、画像・キャプション・リンクなど複数の要素で構成されています。このうち特定の要素をクリックしたい場合、画面を見ればどれを指すかは一瞬でわかりますが、位置と見た目でしか区別できないため、それを指す言葉がありません。
つまり、問題はClaudeが指示を理解できないことではなく、人間の側が操作を言葉で正確に書き下すことの難しさにあります。
codegenによる操作の記録
言葉で書けないのであれば、Claudeに指示するのではなく、操作そのものを記録する方法があります。Playwrightに用意されているcodegenコマンドを使うと、ブラウザでの操作をそのままテストコードに書き起こせます。
codegenは表示されているページを解析したうえで、ロール・テキスト・テストIDを優先してロケーターを選び、同じロケーターに複数の要素が該当する場合は、対象を一意に特定できる形に補正してくれます(Test generator | Playwright)。そのため、言葉で指せない要素であっても、クリックすればロケーター付きのコードとして記録されます。
実際にどのようなコードが記録されるのか、テストコードとして問題なく動作するのかを確かめてみましょう。
codegenの起動
codegenは次のように実行します。
codegenの実行
npx playwright codegen -o tests/tmp.spec.ts http://localhost:9292
-oで出力先のファイルを指定しており、記録したコードはこのファイルに保存されます。
実行すると、ブラウザとInspectorの2つのウインドウが開きます。ブラウザ側でテストしたい操作を行うと、Inspector側にコードがリアルタイムで書き出されていきます。
記録用のツールバー
記録を操作するツールバーはブラウザとInspectorの両方に表示され、記録の開始・停止やアサーションの記録は、どちらから操作してもかまいません。操作しているページから離れずに済むので、ブラウザ側を使うほうが手軽です。
アサーションの記録
codegenは操作だけでなく、アサーションも記録できます。ツールバーからアイコンを選び、ページ上の要素をクリックすると、その要素に対する検証コードが挿入されます。記録中の任意のタイミングで差し込めます。
用意されているのは次の4種類です。
| ツールバーの項目 | 生成されるコード |
|---|---|
| Assert visibility | await expect(locator).toBeVisible(); |
| Assert text | await expect(locator).toHaveText('...'); |
| Assert value | await expect(locator).toHaveValue('...'); |
| Assert snapshot | await expect(locator).toMatchAriaSnapshot(...); |
今回は、部品を追加した直後と複製した直後の2か所で使いました。ブラウザ側のツールバーで目のアイコン(Assert visibility)をクリックし、続けてブラウザ内の対象要素をクリックする、という手順になります。
記録されたコードと実行結果
操作とアサーションをひととおり記録し、ウインドウを閉じると、指定したファイルにテストコードが保存されます。
codegenが記録したコード
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('http://localhost:9292/');
await page.getByRole('button', { name: 'ページ追加' }).click();
await page.getByRole('button', { name: '部品を追加' }).click();
await page.getByText('ここに追加').nth(1).click();
await page.getByText('画像', { exact: true }).click();
await page.getByText('画像', { exact: true }).nth(2).click();
await page.getByRole('button', { name: 'この部品を追加する' }).click();
await page.getByRole('paragraph').filter({ hasText: '画像の説明を入力してください' }).click();
await expect(page.getByRole('img')).toBeVisible();
await page.getByRole('button', { name: '複製' }).click();
await page.getByText('直下に複製').click();
await expect(page.getByText('画像の説明を入力してください').nth(1)).toBeVisible();
await page.locator('li').filter({ hasText: '無題のページ' }).last()
.getByRole('button', { name: '設定' }).click();
await page.getByRole('button', { name: 'ページ削除' }).click();
await expect(page.getByText('このページを削除します')).toBeVisible();
await page.getByRole('button', { name: 'OK' }).click();
});
言葉にできなかった部品内の要素も、クリックするだけで記録されています。ロケーターにはセマンティックロケーターが優先的に採用されており、前回解説した方針とも一致します。
記録したコードを次のコマンドで実行すると、テストは問題なく通りました。
テストの実行
npx playwright test tests/tmp.spec.ts
ここまでは順調です。しかし、生成されたコードをあらためて眺めると、気になる点が出てきます。
codegen生成コードの保守性の問題
テストコードを長く使っていくには、画面の変更で壊れにくいことと、読んで何を確かめているのかがわかることも大切です。その観点で見ると、codegenが記録したコードには2つの問題があります。
コンテナのないロケーター
1つ目は、ロケーターの壊れやすさです。codegenは、記録した時点で要素を一意に特定できれば、それ以上の絞り込みは行いません。たとえば、ダイアログの「OK」ボタンは次のように記録されました。
codegenが記録したロケーター
await page.getByRole('button', { name: 'OK' }).click();
記録した時点では、このボタンは画面内に1つしかないため、これで特定できています。しかし、「OK」のようなラベルは、ダイアログをはじめ画面のあちこちで使われやすいものです。機能の追加などで同じラベルのボタンがほかの場所にも表示されるようになると、ロケーターが複数の要素に一致してしまい、テストは失敗します。
前回は、こうした事態に備えて、対象の要素を含む親要素(コンテナ)でエリアを絞り込んでから要素を探すことを基本にしました(保守性を意識したテストコードの生成)。codegenが記録したロケーターにはこの絞り込みがなく、ページ全体から直接ボタンを探しています。
管理画面のようなアプリケーションは目的別のエリアに区切られていることが多いため、このリスクは小さくありません。
処理のまとまりがないコード
もう1つは、コードの読みにくさです。先ほど記録されたコードから、テストの本体だけを抜き出すと次のようになります。
操作が並ぶだけの状態
await page.getByRole('button', { name: 'ページ追加' }).click();
await page.getByRole('button', { name: '部品を追加' }).click();
await page.getByText('ここに追加').nth(1).click();
await page.getByText('画像', { exact: true }).click();
await page.getByText('画像', { exact: true }).nth(2).click();
await page.getByRole('button', { name: 'この部品を追加する' }).click();
await page.getByRole('paragraph').filter({ hasText: '画像の説明を入力してください' }).click();
await expect(page.getByRole('img')).toBeVisible();
await page.getByRole('button', { name: '複製' }).click();
await page.getByText('直下に複製').click();
await expect(page.getByText('画像の説明を入力してください').nth(1)).toBeVisible();
await page.locator('li').filter({ hasText: '無題のページ' }).last()
.getByRole('button', { name: '設定' }).click();
await page.getByRole('button', { name: 'ページ削除' }).click();
await expect(page.getByText('このページを削除します')).toBeVisible();
await page.getByRole('button', { name: 'OK' }).click();
このコードを読み解くと、次の3つの処理を行っていることがわかります。
- テスト用のページを追加し、画像部品を配置する(1〜8行目)
- 画像部品を複製し、複製されたことを確認する(9〜11行目)
- テスト用のページを削除する(12〜16行目)
このテストで確かめたいのは2つ目の複製機能で、1つ目はそのための準備、3つ目は後片付けです。しかし、codegenは操作を記録した順に並べるだけで、こうした処理のまとまりまでは表してくれません。そのため、コードを1行ずつ追っていかないと、この区切りが見えてきません。
まとめ
今回は、操作の複雑な画面を対象にしたE2Eテストコードの生成について、codegenで操作を記録するところまでを紹介しました。
Claudeに操作を言葉で伝えるのが難しい画面でも、codegenを使えば、ブラウザでの操作をそのままテストコードとして記録できました。ただし、記録されたコードには、コンテナのないロケーターと、処理のまとまりがない構成という、保守性の問題が残りました。
次回は、このコードをPlaywright MCPとClaude Codeで補正し、保守性の高いテストコードに仕上げる方法を紹介します。