Claude Code で Vue 3 + Vuetify 3 プロジェクトに Playwright E2E テストを構築した話

はじめに

Miyagi です。前回の記事では Vitest による単体テストの整備について書きました。今回はその続きで、Playwright による E2E テストの話です。

単体テストでロジック層のカバレッジは確保できましたが、「ブラウザ上で実際に画面が表示されるか」「フォームのバリデーションがユーザー操作で正しく動くか」は E2E テストでしか確認できません。特に Vue 2 → Vue 3 移行プロジェクトでは、移行前と同じ動作をしているかのノンデグレード確認が最重要テスト観点でした。

Note: ノンデグレード確認とは、システムの変更後に既存の機能が壊れていないことを確認するテストのことです。リグレッションテストとも呼ばれます。

ただ、Vuetify 3 の E2E テストは思っていたよりクセがありました。page.click() が効かない、オーバーレイが操作を吸収する、日付ピッカーの DOM 構造が全然違う…。この記事では、そのあたりのハマりどころと解決策も含めて共有します。

本記事では以下について説明します:

 

概要

先に結論を書いておくと、E2E テストで一番大事だったのは「Vuetify 3 の操作ヘルパーを先に作る」ことでした。 Vuetify 3 のコンポーネントは内部 DOM が複雑で、Playwright の標準的な操作だけでは安定したテストが書けません。ヘルパーを整備してからテストを書き始めたことで、各テストファイルがシンプルに保てました。

テストの全体像はこんな感じです。

テスト種別 内容 テストケース数
コンソールチェック 全画面巡回してエラー/ワーニングがないことを確認 約 30 画面
画面表示テスト 全画面が正常に表示されることを確認 約 30 画面
バリデーションテスト フォーム入力・日付操作・画像アップロード等のバリデーション動作確認 約 50 ケース

テストは Playwright Test@playwright/test パッケージ)の CLI で実行します。npx playwright test を叩くだけで、設定ファイルに基づいてテストが走ります。実行環境はローカル(Mock API サーバー + 開発サーバー)で、認証はスキップした状態、ブラウザは Chromium のみ。実行は数十秒で完了します。

 

テスト基盤の設計

ディレクトリ構成
tests/e2e/
├── playwright.config.js          # メイン設定
├── helpers/                      # 共通ヘルパー
│   ├── auth.js                   # ログイン処理
│   ├── pages.js                  # テスト対象ページ定義
│   ├── selectors.js              # セレクタ切り替え
│   ├── vuetify.js                # Vuetify 3 操作ヘルパー
│   └── console-capture.js        # コンソールログキャプチャ
├── selectors/                    # Vue 2/3 セレクタ定義
│   ├── vue3.js
│   └── vue2.js
└── tests/                        # テストスクリプト
    ├── console-check.spec.js
    ├── page-display.spec.js
    ├── validation-search.spec.js
    ├── validation-edit.spec.js
    ├── validation-advanced.spec.js
    └── validation-date.spec.js

ポイントは helpers/selectors/ の分離です。テストスクリプト自体には Vuetify の内部 DOM に依存するコードを書かず、ヘルパーとセレクタに集約しています。

Playwright Test CLI の設定

Playwright には AI エージェント向けの Playwright MCP@playwright/mcp)もありますが、今回は従来の Playwright Test CLI@playwright/test)を使用しました。選定理由は後述します。

Note: Playwright Test CLI はデフォルトでヘッドレスモードで動作します。ヘッドレスとは、ブラウザの GUI(ウィンドウ)を表示せずにバックグラウンドで実行する方式のことです。画面描画のオーバーヘッドがないため高速で、CI 環境など画面のないサーバーでも実行できます。デバッグ時に実際のブラウザ画面を見たい場合は --headed オプションや UI モード(--ui)で切り替えられます。

export default defineConfig({
  testDir: './tests',
  timeout: 60000,
  expect: { timeout: 10000 },
  fullyParallel: false,
  retries: 0,
  workers: 1,
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:8080',
    browserName: 'chromium',
    viewport: { width: 1280, height: 800 },
    launchOptions: {
      args: ['--disable-web-security', '--disable-site-isolation-trials'],
    },
  },
})

並列実行は無効にしています。テスト間で Cookie やストアの状態が干渉するのを避けるためです。ワーカー数も 1 に固定。E2E テストでは安定性を最優先にしました。

--disable-web-security は、ローカルの Mock API サーバーへの CORS 問題を回避するためです。本番環境のテストではない(ローカル開発専用)ので、この割り切りは許容範囲と判断しました。

ログイン処理の共通化

認証スキップ環境でも Cookie のセットは必要なので、helpers/auth.js に共通化しました。

export async function login(page) {
  // Cookie セット + ログイン画面操作 + ネットワークアイドル待機
}

各テストファイルの beforeEach でこの関数を呼ぶだけで済むので、認証周りの変更があっても修正箇所が 1 ファイルで閉じます。

 

なぜ Playwright Test CLI を選んだか

Playwright には 2 つの使い方があります。テストコードを書いて CLI で実行する Playwright Test@playwright/test)と、AI エージェントが MCP プロトコル経由でブラウザを直接操作する Playwright MCP@playwright/mcp)です。

Note: MCP(Model Context Protocol)は Anthropic が開発したオープンプロトコルで、AI モデルが外部ツールと連携するための標準インターフェースです。claude mcp add playwright npx @playwright/mcp@latest で Claude Code に Playwright MCP を登録すると、自然言語でブラウザ操作を指示できるようになります。

今回 CLI を選んだ理由は、テストを資産として残し、繰り返し実行することが目的だったからです。

観点 Playwright Test CLI Playwright MCP
テストの再現性 同じコード → 同じ結果(決定的) 同じ指示でも毎回挙動が変わりうる
CI/CD 連携 そのまま組み込める 組み込みが難しい
テスト資産 コードとして残り、チームで共有・レビュー可能 その場の実行で終わり、資産として残りにくい
保守コスト DOM 変更時にセレクタ修正が必要 AI が自律的に対応できる可能性がある
成熟度 エコシステムが充実 まだ v0.0.x で安定性は未知数

移行プロジェクトのノンデグレード確認では「同じテストを何度も回して、前回と結果が変わらないことを保証する」のが重要です。MCP は「この画面を開いてざっと確認して」のような探索的な用途には便利そうですが、再現性が求められるテストスイートの運用には CLI のほうが向いています。

とはいえ、MCP は発展途上のツールなので、今後バージョンが上がってテスト資産の管理や CI 連携が充実してくれば、選択肢として有力になるかもしれません。

 

Vuetify 3 特有のハマりどころと対策

Vuetify 3 の E2E テストで一番苦労したのがここです。Vuetify 2 と DOM 構造が大きく変わっているので、セレクタの書き直しだけでは済みませんでした。

page.click() が効かない問題

Vuetify 3 のボタンやフォーム要素には、内部にオーバーレイ要素が重なっている場合があります。Playwright の page.click() はデフォルトで「要素がクリック可能か」をチェックするので、オーバーレイに遮られてタイムアウトすることがあります。

対策として、page.evaluate() で直接 DOM を操作するヘルパーを作りました。

// helpers/vuetify.js
export async function clickButton(page, text) {
  await page.evaluate((buttonText) => {
    const buttons = [...document.querySelectorAll('button.v-btn')]
    const target = buttons.find(btn => btn.textContent.includes(buttonText))
    if (target) target.click()
  }, text)
}

見た目は泥臭いですが、これが一番安定しました。Playwright の force: true オプションでも回避できますが、「実際にクリック可能かどうか」のチェックを丸ごとスキップしてしまうので、テストの信頼性が下がります。テキスト検索でボタンを特定する方式なら、少なくとも「そのボタンが DOM に存在すること」は保証されます。

オーバーレイの待機

ダイアログやメニューを開いた後、Vuetify 3 はアニメーション付きのオーバーレイを表示します。アニメーション完了前に次の操作をすると不安定になるので、オーバーレイの状態を待機するヘルパーも用意しました。

セレクタの Vue 2/3 切り替え

Vue 2 との比較テストも実行できるように、セレクタを Vue バージョンごとに定義ファイルを分けています。

// selectors/vue3.js
export default {
  textFieldInput: '.v-field__input input',
  errorMessage: '.v-messages__message',
  datePickerDayCell: 'button.v-date-picker-month__day-btn',
  snackbar: '.v-snackbar',
}

// selectors/vue2.js
export default {
  textFieldInput: '.v-text-field input',
  errorMessage: '.v-messages__message',
  datePickerDayCell: '.v-date-picker-table td button',
  snackbar: '.v-snack',
}

helpers/selectors.js で環境変数に応じて切り替えるだけなので、テストスクリプト側は Vue バージョンを意識しません。移行プロジェクト特有の工夫ですが、Vuetify のメジャーバージョンアップが将来また来た時にも使えるパターンだと思います。

 

3種類のテストと段階的な作成

E2E テストは 3 種類に分けて、段階的に作成しました。

1. コンソールチェック

全画面を順番に巡回して、コンソールにエラーやワーニングが出ていないことを確認するテストです。地味ですが、移行後の品質保証としてはこれが一番重要でした。

ポイントはログのフィルタリングです。ブラウザのコンソールには Vite の HMR(Hot Module Replacement)ログやネットワークエラーなど、アプリ由来ではないログも大量に流れます。これらを除外して「アプリケーションコードが出力したワーニング/エラー」だけを判定対象にする仕組みを console-capture.js に実装しました。

// フィルタリングの例
const IGNORE_PATTERNS = [
  /\[vite\]/,              // Vite HMR ログ
  /net::ERR_/,             // ネットワークエラー
  // ... 認証スキップ環境特有のワーニング等
]

各画面の結果は .log ファイルとして出力し、エビデンスとして残します。ヘッダーに実行日・結果(OK/NG)・エラー件数を記録するので、後から見返した時にもすぐ分かります。

2. 画面表示テスト

全画面が正常に表示されるかの確認です。ログインが必要な画面は認証を通してからアクセスし、主要な要素が表示されていることを expect で検証します。

コンソールチェックと画面表示テストは yarn test:e2e で一緒に実行されるようにしました。この 2 つは「全画面を対象に、基本的な品質を担保する」という共通の目的があるので、分けて実行する意味がありません。

3. バリデーションテスト

フォームのバリデーション動作を確認するテストで、ケース数としてはここが一番多くなりました。テストファイルは対象の種類ごとに分割しています。

ファイル 対象
validation-search.spec.js 一覧画面の検索フォーム
validation-edit.spec.js 編集画面のフォーム
validation-advanced.spec.js モーダル・画像アップロード・テンプレート選択等
validation-date.spec.js 日付ピッカー操作・日付比較ルール

バリデーションテストでは、境界値テストを重点的に書きました。「最大文字数ちょうどはOK、1文字超えたらエラー」のようなパターンです。単体テストでもバリデーションルール自体はテスト済みですが、E2E では「ブラウザ上でエラーメッセージが実際に表示されるか」まで確認できます。

日付ピッカーのテストは特に苦労しました。Vuetify 3 の日付ピッカーは DOM 構造が Vuetify 2 と全く違い、月の移動ボタンやカレンダーセルのセレクタを一から調べ直す必要がありました。「前月の日付を選択 → 過去日エラーが出る」「翌月に移動 → 未来日を選択 → エラーが消える」のようなシナリオは、DOM 操作が複雑なぶんヘルパーの恩恵が大きかったです。

API エラーのモック

バリデーションテストの中には、API がエラーを返した時の画面の振る舞いを確認するケースもあります。ここでは Mock API サーバーを変更するのではなく、Playwright の route.fulfill() でレスポンスを差し替える方式を採りました。

await page.route('**/api/endpoint', (route) => {
  route.fulfill({
    status: 500,
    contentType: 'application/json',
    body: JSON.stringify({ errorCode: '500000' }),
  })
})

テストスクリプト内でモックを完結させられるので、Mock API サーバーとの依存関係がなく、テストの独立性が保たれます。400 / 500 / 503 / タイムアウト / 404 の各パターンをこの方式でテストしました。

 

Claude Code に渡したプロンプトの設計

単体テストの記事でも書きましたが、E2E テストでもプロンプトの事前設計が効きました。ただし、E2E 特有のポイントがいくつかあります。

セッション分割

E2E テストは単体テストよりスコープが広いので、Claude Code のセッションを 2 つに分けました。

  • セッション 1: 全画面表示確認 + コンソールチェック + ヘルパー整備
  • セッション 2: バリデーションテスト(検索フォーム → 編集フォーム → 高度な操作 → 日付)

セッション 1 でヘルパー(auth.js, vuetify.js, console-capture.js)を作り込み、セッション 2 ではそのヘルパーを前提にテストを書かせる流れです。

プロンプトに含めた情報

E2E テストのプロンプトでは、単体テストとは違う情報が必要でした。

# E2E バリデーションテスト作成

## 環境
- Playwright + Chromium
- ローカル開発サーバー: http://localhost:8080
- Mock API サーバー: http://localhost:4000
- 認証: Cookie 手動セット(VITE_LOCAL=1 環境)

## 参照すべきファイル
- tests/e2e/helpers/ 配下のヘルパー(使い方を理解してから書くこと)
- tests/e2e/selectors/vue3.js(セレクタ定義)
- src/plugins/validationRules.js(バリデーションルール定義)

## テスト観点
1. Vue 2 → Vue 3 移行のノンデグレード確認(最重要)
2. バリデーションエラー表示の確認
3. 境界値テスト(max-1 / max / max+1)
4. 日付比較ルールの動作確認

特に「ヘルパーを理解してから書くこと」という指示が重要でした。E2E テストではヘルパーの使い方を知らないと、セレクタを直接書いたり page.click() を使ったりして不安定なテストが生成されます。

ガイドラインの要点

E2E テスト用のガイドラインも用意しました。単体テストのガイドラインとは別物で、E2E 特有の以下を定義しています。

操作の安定性:

  • Vuetify 3 のボタンクリックは helpers/vuetify.js のヘルパーを使う
  • ダイアログ操作後はオーバーレイの消滅を待機する
  • ネットワークアイドル待機を適切に挟む

セレクタ方針:

  • selectors/vue3.js に定義済みのセレクタを使う
  • テストスクリプト内にマジックストリングのセレクタを書かない
  • 新しいセレクタが必要な場合はセレクタ定義ファイルに追加する

API モック方針:

  • エラーケースは route.fulfill() でテスト内に閉じる
  • Mock API サーバーは変更しない

 

テスト結果の管理

E2E テストの結果は tests/_reports/e2e/ にエビデンスとして保存しています。

_reports/e2e/
├── page-list.md                # 画面一覧・表示テスト結果
├── validation-test-plan.md     # バリデーションテスト計画・結果
└── src/                        # コンソールチェックログ
    ├── login/
    │   └── index.log
    ├── users/
    │   ├── index.log
    │   └── edit/index.log
    └── messages/
        └── ...

コンソールチェックのログは src/views/ のディレクトリ構造をミラーしているので、「この画面のログを見たい」と思った時にパスから直感的にたどれます。ログファイルのヘッダーには実行日・結果・エラー件数を記録しているので、CI で自動実行した場合にも結果がすぐ分かります。

バリデーションテストの計画と結果は validation-test-plan.md に集約しています。テスト観点(ノンデグレード確認、エラー表示、境界値、日付比較…)とテストケースの一覧、各ケースの PASS/FAIL を記録するフォーマットです。

 

振り返り

うまくいったこと:

ヘルパーを先に作ったのは正解でした。特に vuetify.js(クリック・オーバーレイ待機)と console-capture.js(ログフィルタリング)は、これがないとテストスクリプトが Vuetify の内部実装に依存するコードだらけになっていたと思います。ヘルパーに閉じ込めたことで、Vuetify のマイナーバージョンアップで DOM 構造が変わっても影響範囲が限定されます。

セレクタの Vue 2/3 切り替えも便利でした。同じテストスクリプトで Vue 2 版と Vue 3 版の両方を動かして比較できるので、ノンデグレード確認が効率的に進みました。

API エラーのモックに route.fulfill() を使う判断も、振り返ってみて良い選択だったと感じています。今回の E2E テストの目的は「エラー時にフロントエンドが正しくダイアログやリダイレクトを出すか」の確認であって、リクエストの正しさは単体テスト(Vitest)でカバー済みでした。この役割分担があったからこそ、E2E 側は route.fulfill() でレスポンスを差し替えるだけのシンプルな方式で済んだのだと思います。もし単体テストがなかったら、Mock API サーバーにもっと検証ロジックを持たせる必要があったはずで、テスト全体の設計としてうまく噛み合っていました。

改善したいこと:

テストの実行が直列なので、ケース数が増えると実行時間も伸びます。現状は数十秒で済んでいますが、今後テストを追加していく場合は並列化の検討が必要です。ただ、Vuetify のオーバーレイやアニメーションが並列実行で干渉するリスクもあるので、単純に fullyParallel: true にすれば良いという話ではありません。

あと、日付ピッカーのテストは今でも一番壊れやすいです。カレンダーの「今日」の日付によってテストの挙動が変わるので、vi.useFakeTimers() のような時刻固定の仕組みが Playwright にもあると良いのですが…。現状は「今月の15日を選ぶ」のようにある程度安全な日付を選ぶことで回避しています。

※ 記事公開後に、Playwright には page.clock API(v1.45+)で時刻を固定できる仕組みがあることを知りました。次回テストを追加・改善する際に試してみたいと思います。

 

まとめ

Vue 3 + Vuetify 3 の E2E テストは、Vuetify の内部 DOM にどう向き合うかが最大の課題でした。page.click() が効かない、オーバーレイが邪魔する、セレクタが全部変わる。これらを一つずつ解決してヘルパーに閉じ込めることで、テストスクリプト自体はシンプルに保てました。

ポイントをまとめると:

  1. Vuetify 3 操作ヘルパーを先に作るpage.evaluate() による直接 DOM 操作、オーバーレイ待機をヘルパーに集約
  2. セレクタをバージョン別に管理する ― Vue 2/3 の切り替えを透過的にし、ノンデグレード確認を効率化
  3. コンソールチェックはフィルタリングが命 ― Vite ログやネットワークエラーを除外し、アプリ由来の問題だけを検出
  4. API エラーは route.fulfill() でモック ― Mock API サーバーを変更せず、テスト内に閉じる
  5. Claude Code にはヘルパーを先に理解させる ― 不安定な操作コードの生成を防ぐ
vrmonkey

 

最後まで読んでいただき、ありがとうございました!
この記事が少しでもお役に立てれば嬉しいです。良い開発ライフを!

 

参考リンク