🤖

【Playwright 】Claude Code × Playwright MCPでPageObjectの自動生成を試してみた

に公開

冒頭には自分に起きた時事ネタを書くことが多いです。
実は・・・ついにClaudeに課金しました(笑
なので、元を取る使い倒すという意味で、ここからの記事はClaudeを使用したものが多くなりそうです。

と雑談?はこのあたりにして、方向修正をしましょう(笑
さて、Playwrightは優秀です。
PlaywrightはUIがあり非エンジニアでもある程度触ることができます。
PlaywrightはGitHub ActionsでE2E自動化を回せます。

「あれ?PlaywrightはAI(MCP)と絡ませないでも充分では・・?」

MCP(Model Context Protocol)って何?

MCPは、AIモデルと外部のツール・データソースをつなぐためのオープンな標準規格です。Anthropicが2024年11月に発表しました。

それまでは「このAIとこのツールを繋ぐには専用の連携コードが必要」という状態で、AIモデルの数 × ツールの数だけ個別対応が発生していました(いわゆるM×N問題)。MCPはここを標準化して、1つのプロトコルに対応してさえいれば、どのAIからどのツールにも繋げられるようにしたものです。

技術的にはクライアント・サーバー方式で、JSON-RPC 2.0でやり取りします。Claude CodeやClaude Desktopのようなホストアプリが、MCPサーバー(GitHub・Slack・DBなど)に接続して機能を呼び出す、という構成です。

普及はかなり早くて、2026年時点でSDKの月間ダウンロード数が1億近くまで伸びていて、主要なAIベンダーもほぼ対応済みです。2025年12月にはAnthropicからLinux Foundation傘下の団体(Agentic AI Foundation)に寄贈され、特定ベンダーに依存しない標準という位置づけになっています。

この記事で使っている Playwright MCP も、数あるMCPサーバーの1つです。ブラウザ操作という「ツール」をMCP経由でClaude Codeから呼べるようにしたもの、と考えると分かりやすいと思います。

「いや、きっともっと有効に使うところがあるはず」と考えて、今回試したのが「ブラウザを巡回して要素を抽出し、PageObjectを自動生成する」という仕組みです。

結論から書くと、PageObjectの生成は成功しましたが、全てAI任せにする方向にはしませんでした。
あくまでAIには、人がメンテしやすいようにベース作りをしてもらって、スタートしやすくなるようにしています。
で、最終的には人間がレビューするフローを想定しています。本記事ではそのあたりの経緯と、実際に使ったツール・コードをまとめます。

事前準備・前提

  • Claude Code(VSCode拡張 or Desktopアプリ)
    • 上記は無料版ではできませんので、各自課金をお願いします。
  • Playwright MCP
  • 対象は、以下のWEBサイトで実施

https://sandbox-automation.pages.dev/

XPathまわりの考え方は↓の記事や公式ページとだいたい同じなので、あわせて読んでもらえると分かりやすいかもしれません。
https://zenn.dev/beeeegle/articles/e9847874fdd034
https://playwright.dev/docs/pom

アプローチの選定

PageObject生成のために、HTML要素をどう取得してAIに渡すかで3案を比較しました。
一番気にしたところはトークン消費量です。(貧乏性)

内容 課題
案1 フルHTMLを取得してLLMに渡す 【AIが算出した推定】
トークン消費が8,000〜20,000と多くなることが予想され、WEBサイトやページによって消費量のブレが大きい
案2 Chrome拡張機能で要素を取得する 節約・精度は期待できるが開発・維持コストが高い。後述の既存ツール(playwright-pom-generator)も微妙だった
案3(採用) JS/CSSを排除した構造化抽出 data-testid / role / tag に絞ったJSON構造をLLMに渡す。【AIが算出した推定】トークン消費を200〜500に圧縮を試みた

案2でやりたかったこと(トークン節約・精度向上)は、実は拡張機能を作らなくても、渡す情報を絞り込むだけで達成できました。これが案3を採用した理由です。

案1 フルHTMLを取得してLLMに渡す

正直怖くて実施していないので、近いうちに1ページだけでも生成してみようと考えています。

案2 Chrome拡張機能で要素を取得する

計画当初はAIでChrome拡張機能をを自作しようと考えていたのですが、既存ツールの playwright-pom-generator(Chrome拡張)というのがありせっかくなので試しました。
ただ、全体的に公式の記述に準拠していなかったので、期待していた結果になりませんでした。

  • 変数名にテキストボックスに入力したテストデータの値がそのまま使われた
    • 例: aaaaaAaaaCoJp(イケてない)
  • ヘッダー・フッターなど共通要素が、全ページのファイルに記載される(イケてない)
  • 全要素に対して isXxxVisible() が羅列される(イケてない)
  • LocatorそれぞれにGetterが生成される(イケてない)

単純に要素を拾うだけだと、レビューにも保守にも耐えるPageObjectにはならないんだな、というのが正直な感想でした。
ただ、Chrome拡張機能にしてもよかったのですが、今後何かしら配布するなどとなった時に、メンテナンス性などのコスト面から「不採用」としました。

案3 JS/CSSを排除した構造化抽出

先述しましたが、この案を採用しました。
案2に近いですが、これであればGithubのPublicリポジトリで、パッケージライクな配布が容易ですし、バージョン管理もしやすいなど考えました。
それでは、お待たせしましたが、以下構成や検証結果を記載します。

構成・アーキテクチャ

ツール構成

  • Claude Code(VSCode拡張 or Desktopアプリ): 作業の司令塔
  • Playwright MCP: ブラウザのリアルタイム操作
  • extract-elements.ts: 要素の構造化抽出スクリプト
  • CLAUDE.md: Claudeへの指示書(規約・手順)

プロジェクト構成

project-root/
├── CLAUDE.md
├── extract-elements.ts
├── .env.test(Git管理外)
├── helpers/
├── pages/
│   ├── layouts/(Header/Footerなど共通要素)
│   └── auth/(URLの階層構造に対応)
├── data/(テストデータ・手動定義)
├── docs/
│   └── target_screens.md(対象画面一覧)
└── tests/

特にpagesフォルダ配下は、URLの階層構造をそのままフォルダ構造にしています。/auth/confirm なら pages/auth/ConfirmPage.ts に置く、というシンプルなルールです。

CLAUDE.mdに書いたこと

CLAUDE.mdは、Claudeに何をどう作らせるかをまとめた指示書です。今回の検証で一番時間を使ったのは、たぶんここです。

大きく分けるとこんな感じの内容が入っています。

実行ポリシー
ファイル作成やMCPツールの実行は確認なしで進めてOK、エラーが出ても処理を止めずに docs/result.md に記録して次の画面に進む、という運用にしています。複数画面を巡回する処理なので、途中で止まらないようにするためです。

生成手順(Step 0〜5)
MCPの登録確認 → 対象画面の確認 → 認証情報の読み込み → 要素抽出 → 配置先の決定 → PageObject生成、という流れです。

命名規則
テストデータの値をそのまま変数名にしない、というルールを明記しています。

// ✅ 正しい例
get lastNameInput(): Locator { return this.page.getByTestId('confirm-lastName'); }
get submitButton(): Locator { return this.page.getByTestId('confirm-submit-button'); }

// ❌ 間違い例(テストデータの値で命名している)
get aaaaaAaaaCoJp(): Locator { ... }
get item09012345678(): Locator { ... }

これはplaywright-pom-generatorで実際に困ったところそのものです。「何を生成しないか」を先に指示書に書いておくと、同じ失敗を繰り返さずに済みます。

出力フォーマット
生成するPageObjectは公式のPOMスタイルに合わせています。Locatorはgetterじゃなくてconstructorで初期化して、readonlyで定義する形です。

example.ts
import { Page, Locator, expect } from '@playwright/test';

export class ExamplePage {
  readonly page: Page;
  readonly someButton: Locator;
  readonly someInput: Locator;

  constructor(page: Page) {
    this.page = page;
    this.someButton = page.getByTestId('some-button');
    this.someInput = page.getByTestId('some-input');
  }

  async clickSomeButton(): Promise<void> {
    await this.someButton.click();
  }

  async fillSomeInput(value: string): Promise<void> {
    await this.someInput.fill(value);
  }

  async expectPageLoaded(): Promise<void> {
    await expect(this.page).toHaveURL(new RegExp(this.CONFIG.PAGE_PATH));
  }

  async goto(baseUrl?: string): Promise<void> {
    const url = baseUrl ?? process.env.TEST_BASE_URL ?? '';
    await this.page.goto(`${url}${this.CONFIG.PAGE_PATH}`);
    await this.page.waitForLoadState('domcontentloaded');
  }
}

あと「不要なメソッドを作らせない」ことも書いています。全要素に isXxxVisible() を並べたり、clickButton(buttonName) みたいな汎用マップを作ったりするのは、指示書段階で禁止しました。

要素抽出(extract-elements.ts)

Playwright MCP経由でブラウザに注入して、data-testid / role / タグ種別に絞って要素を構造化抽出するスクリプトです。CSSやスタイル情報は含めず、テキストも50文字で切っているので、LLMに渡すトークン量はかなり小さく収まります。

export interface ExtractedElement {
  tag: string;
  testId: string | null;
  role: string | null;
  type: string | null;
  name: string | null;
  label: string | null;
  text: string | null;
  placeholder: string | null;
  href: string | null;
}

export const extractElements = (): ExtractedResult => {
  const TARGET_SELECTORS = [
    '[data-testid]', 'button', 'input', 'select', 'textarea',
    'a[href]', 'form',
    '[role="button"]', '[role="link"]', '[role="checkbox"]',
    '[role="radio"]', '[role="textbox"]',
  ];

  const seen = new Set<Element>();
  const elements: ExtractedElement[] = [];

  TARGET_SELECTORS.forEach((selector) => {
    document.querySelectorAll(selector).forEach((el) => {
      if (seen.has(el)) return;
      seen.add(el);
      // labelの紐付けなど、詳細は割愛
      elements.push({ /* ... */ });
    });
  });

  return { url: window.location.pathname, title: document.title, elements };
};

今回は、操作可能な要素だけに絞っているのもポイントです。というもの装飾用のdivspanpHxを含めるとノイズが多く入りトークン消費量も増えると推測したので、ますzはそれらを対象外にして、ノイズを減らしています。

認証情報

ログインが必要な画面の認証情報は .env.test に分離して、Git管理外にしています。CLAUDE.mdやtarget_screens.mdには書かないようにしています。

XPathは使わない

要素取得・Locator生成にXPathは使いませんでした。理由は以前の記事とほぼ同じです。

Playwright公式ドキュメントでも、XPathやCSSのようにDOM構造に依存するセレクタは、DOM構造が変わると壊れやすいと書かれています。

よく「XPathはSeleniumでは使えるけどPlaywrightでは使えない」と誤解されがちですが、これは正確じゃないですね。壊れやすさの原因はツールではなくて、フロントエンドの構造(Vue/React/Nuxtみたいなコンポーネントベースの作り)にあります。Seleniumで書いても、同じ理由で同じように壊れます。

判断理由をざっとまとめると

  • DOM構造の変更に弱い(//div[3]/button[2] みたいな指定はdivの並びが変わると即壊れる)
  • パフォーマンスが他のLocatorより劣る
  • 可読性が低い(何を指しているか一見でわからない)
  • getByRole だとa11yの問題も副産物的に見つけられるが、XPathにはこの効果がない

というところです。Locatorの優先順位はこう決めました。

優先度 Locator 用途
1 getByTestId data-testid が付与されている要素
2 getByRole ボタン・リンク・入力欄など
3 getByLabel フォームのラベルと紐づく要素
4 CSSセレクタ 上記で取得できない場合のみ
XPath 採用しない

ただ、フロントエンドの構造が長期間変わらない枯れたシステムとか、Seleniumの既存資産がすでに大量にある環境だと、XPathが有効なケースもあるとは思います。あくまで今回のプロジェクト(Vue/Nuxt)の前提での判断です。

検証結果

気になるトークンの消費量ですが、APIを実行していないので取得できない模様です。
なので、WEBブラウザベースで一旦記載すると、17画面で、18%の消費となりました。
大雑把に1画面1%くらいでしょうか。

できたこと

  • Playwright MCPによるブラウザ巡回と要素抽出
  • CLAUDE.mdの規約に沿ったPageObjectの生成
  • 公式スタイル(constructorでのLocator初期化・readonly定義)への準拠
  • URLの階層構造に対応したフォルダへの自動配置

PageObjectの生成レポート

以下のようなレポートを出力するようにしています。
これによって、どこ画面の生成に成功したか失敗したかなどがチェックできるようになってます。


実際のコード

以下のログイン画面から、出来上がったコードの一部画像です。

できなかったこと・課題

  • MCPが未インストールの場合、curlによるフォールバックが発生することがあった
    • プロジェクトにPlaywrightはMCPがInstallされていることが前提になるのですが、自分自身で忘れてました。しかしAIが良しなにCurlでHTMLを取得してPageObjectを生成してくれました。
  • 動的パス(例: /shop/items/[id])は事前にサンプルURLを用意しておく必要がある

今後やりたいこと

今回はテスト実施の工程(PageObject自動生成)だけをやりました。次はテスト実施の工程も試したいと思っています。

  • flaky判定: 失敗したテストをN回リトライして、成功率でflakyかバグかを自動分類する
  • セレクタ自動修復: DOM変更時にPlaywright MCPが実際のDOMを見て修復案を出す
  • GitHub Issues自動報告: AIの分類結果からissueの草案を作って、承認後に登録する

これができると、テストの前工程・後工程を通した半自動化パイプラインになりそうです。

あと、今回はPlaywright(TypeScript)向けに作りましたが、この仕組み自体は出力言語に依存しないはずです。Python × Seleniumでも同じアプローチが使えると思うので、次のフェーズで試してみたいですね。

あとがき

今回やってみて、細かいところはさて置き設計書がなくても「たたき台を作ってもらえる」ってことがわかったのが自分的にはよかったです。
人が開発者ツールなどを使って、ロケーターを調べるのは意外と手間がかかるので・・・笑

一方で、Curlで良しなにPageObject作ってくれてて、成功ってなっているのはビックリしましたね・・・笑
良くも悪くもAIすげぇーってなりました。

正直、冒頭にも記載しましたが、Playwrightが標準でも高機能なので、正直MCPっていらないのでは?と思っていましたが、AIがブラウザを操作できるってのは、使えますね。

関連記事

https://zenn.dev/beeeegle/articles/e9847874fdd034)

参考

https://sandbox-automation.pages.dev/

Discussion