Knowledge
Playwright の page.route は Server Action 内の外部 API 呼び出しを傍受できない
page.route はブラウザ発リクエストのみ対象のため、Server Action 内で行う Gemini API 等の呼び出しはモック不可。UI 状態テストへの限定とローカルモックサーバーの2つの対処法を解説します。
1. 発生した現象
Markdown Formatter の E2E テストで、MD変換ボタンをクリックした際の Gemini API 呼び出しをモックしようとして以下のコードを書きました。
await page.route('https://generativelanguage.googleapis.com/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ candidates: [{ content: { parts: [{ text: '# Mock Result' }] } }] }),
})
})しかし、MD変換ボタンをクリックしてもモックレスポンスが返らず、 実際の Gemini API への呼び出しが走り続けました。page.route によるインターセプトが一切機能していませんでした。
2. 原因:page.route はブラウザ発リクエストのみ傍受できる
Playwright の page.route は、ブラウザ(クライアントサイド)から発信される HTTP リクエストのみを傍受します。
MD変換の Gemini API 呼び出しは app/tools/markdown-formatter/_actions/gemini-format.ts の Server Action('use server')内で行われます。 Server Action は Next.js サーバー(本プロジェクトでは Cloudflare Workers)上で実行されるため、 HTTP リクエストはサーバーサイドから Gemini API に直接送信されます。
ブラウザから見ると「MD変換ボタンをクリック → Next.js サーバーへ POST → サーバー内部で Gemini API 呼び出し」 という流れになります。ブラウザは Gemini API への直接リクエストを送信しないため、page.route には届かないのです。
// ブラウザから Next.js サーバーへのリクエスト(page.route で傍受できる)
POST /tools/markdown-formatter ← ここは傍受できる
// Next.js サーバーから Gemini API へのリクエスト(page.route で傍受できない)
POST https://generativelanguage.googleapis.com/... ← ここは傍受できないなお、POST /tools/markdown-formatter(Server Action の呼び出し自体)はpage.route で傍受できます。 レスポンスを遅延させてローディング状態を観察するテストには有効です。
3. 対処法 A:テスト範囲を UI 状態に限定する
Gemini API のレスポンス内容に依存しないテストシナリオに絞ることで、 外部 API のモックなしに E2E テストを成立させます。
- ローディング状態のテスト:
page.routeでPOST /tools/markdown-formatterの応答を遅延させ、 その間にボタンが「MD変換中…」に変わり非活性になることを確認する。await page.route('**/tools/markdown-formatter', async (route) => { if (route.request().method() === 'POST') { await new Promise<void>((resolve) => setTimeout(resolve, 3000)) await route.continue() } else { await route.continue() } }) - デフォルト状態のテスト: ページ初期表示時にエラーセクションが非表示であること、 ボタンが「上限到達」ではなく「MD変換」ラベルであることを確認する。
- 入力に応じたボタン活性化テスト: テキスト入力後に「MD変換」ボタンが有効になることを確認する。
Gemini API のレスポンス内容(変換結果の正確さ等)は E2E テストの責任範囲外として割り切り、 Server Action の単体テストや手動テストで確認します。
4. 対処法 B:ローカル HTTP サーバーでモックする
サーバーサイドの外部 API 呼び出しをモックしたい場合は、 E2E テスト側でローカル HTTP サーバーを起動し、 Server Action が呼び出す URL をテスト用のサーバーに向ける方法があります。
本プロジェクトの SEO Analyzer(e2e/seo-analyzer.spec.ts)がこのパターンを実装しています。test.beforeAll で http.createServer を使ったモックサーバーを起動し、 Server Action の fetch 先として環境変数経由でモック URL を渡します。
- メリット: サーバーサイドの HTTP レスポンス内容を完全にコントロールできる。 エラーレスポンス(429・503 等)を意図的に返すテストが書ける。
- デメリット: テストコードの複雑さが増す。 Server Action がモック可能なエンドポイントを参照するよう 実装側にも配慮が必要(ハードコードされた URL の場合は差し替えが困難)。
5. 使い分けの判断基準
2つの対処法の使い分けは以下の観点で判断します。
- 検証したい内容がレスポンス内容に依存するか: 「変換結果の文字列が正しいか」「エラーコードに応じた UI 変化が正しいか」を E2E で確認したい場合は、対処法 B(ローカルモックサーバー)が必要です。 一方、ローディング状態・ボタン活性状態・デフォルト表示のような レスポンス内容によらない UI 確認は対処法 A で十分です。
- 外部 API の呼び出し URL が差し替え可能か: Server Action がハードコードされた URL を参照している場合は 環境変数経由での差し替えが必要です。 差し替え機構のない実装に後からモックサーバーを当てるのは改修コストがかかります。
- テストの保守コスト: 外部 API のレスポンス仕様が変わるたびにモックも更新が必要です。 レスポンス内容の検証は単体テストに任せ、 E2E は UI フローの確認に集中させる設計が保守しやすいです。
本プロジェクトでは Markdown Formatter の MD変換テストに対処法 A を採用しました。 AI の変換結果は毎回異なるため E2E での内容検証は不適切であり、 UI 状態(ローディング・ボタン活性・エラー表示なし)の確認に限定することが テスト設計として自然な選択でした。