T-CREATOR

Playwright Debug モード活用:テストが落ちる原因を 5 分で特定する手順

Playwright Debug モード活用:テストが落ちる原因を 5 分で特定する手順

E2E テストが失敗したとき、ログを眺めるだけでは原因が見えてこないことがありますよね。Playwright には強力な Debug モードが用意されており、テスト実行中の DOM 状態やネットワーク通信を可視化しながら、落ちた箇所を 5 分程度で特定できます。

本記事では、Playwright の Debug モードを使った効率的なデバッグ手順を、実践的なサンプルコードとともに解説します。初めて Debug モードを使う方でも、すぐに実務で活用できるようになるでしょう。

背景

E2E テストのデバッグが難しい理由

E2E テストは複数のレイヤー(フロントエンド、バックエンド、ネットワーク、ブラウザ)が絡み合うため、テストが失敗したときの原因特定が困難です。単純なログ出力だけでは、以下のような問題が見えにくいのが現状です。

  • DOM 要素が存在しているのに見つからない(タイミング問題)
  • API レスポンスの遅延やエラー
  • CSS によって要素が隠されている
  • 期待値と実際の表示内容のズレ

以下の図は、E2E テストで発生しうる問題点の全体像を示しています。

mermaidflowchart TB
  test["テスト実行"] -->|失敗| problem{"問題箇所"}
  problem --> timing["タイミング<br/>問題"]
  problem --> selector["セレクター<br/>問題"]
  problem --> network["ネットワーク<br/>問題"]
  problem --> state["状態管理<br/>問題"]

  timing --> invisible["要素が<br/>まだ表示<br/>されていない"]
  selector --> wrong["セレクターが<br/>間違っている"]
  network --> slow["API が<br/>遅い"]
  state --> data["データが<br/>期待と<br/>異なる"]

このような複雑な問題に対して、Playwright の Debug モードは実行を一時停止しながら状態を確認できるため、原因を素早く特定できます。

Playwright Debug モードとは

Playwright には以下のデバッグ機能が組み込まれています。

#機能説明
1Inspectorテスト実行を一時停止し、ステップ実行できる GUI ツール
2Trace Viewerテスト実行後に、スクリーンショットや DOM を時系列で確認
3UI Modeテストをインタラクティブに実行・デバッグできるブラウザ UI
4VSCode 統合VSCode でブレークポイントを設定してデバッグ

これらを組み合わせることで、テストが落ちた原因を迅速に特定できます。

課題

よくあるデバッグの課題

実際の開発現場では、以下のような課題に直面することが多いでしょう。

課題 1:テストログだけでは原因が見えない

コンソールに出力される Error: Timeout 30000ms exceeded といったメッセージだけでは、なぜタイムアウトしたのかがわかりません。

課題 2:再現性の低いテスト失敗

CI 環境でのみ失敗するテストや、実行タイミングによって結果が変わるテストは、ローカルでの再現が難しく、デバッグに時間がかかります。

課題 3:複数の要因が絡み合う

セレクターの問題なのか、API の問題なのか、それともブラウザの挙動なのか、切り分けが困難です。

以下の図は、テスト失敗時の課題の関係性を示しています。

mermaidflowchart LR
  failed["テスト失敗"] --> log["ログ確認"]
  log --> insufficient["情報不足"]
  insufficient --> retry["手動で<br/>再実行"]
  retry --> notReproduced["再現<br/>しない"]
  notReproduced --> guess["推測で<br/>修正"]
  guess --> failed

  style failed fill:#f88
  style notReproduced fill:#f88
  style insufficient fill:#fa8

このような悪循環を断ち切るために、Debug モードを活用します。

解決策

Debug モードを使った効率的なデバッグフロー

Playwright の Debug モードを活用すると、以下の 5 ステップで原因を特定できます。

ステップ 1:Debug モードでテストを実行

ステップ 2:Inspector で実行を一時停止

ステップ 3:問題箇所の状態を確認

ステップ 4:Trace Viewer で時系列を分析

ステップ 5:修正して再実行

以下の図は、Debug モードを使った効率的なフローを示しています。

mermaidflowchart TD
  start["テスト失敗"] --> debug["Debug モードで<br/>実行"]
  debug --> pause["Inspector で<br/>一時停止"]
  pause --> check["DOM/Network<br/>確認"]
  check --> trace["Trace Viewer で<br/>時系列分析"]
  trace --> identify["原因特定"]
  identify --> fix["修正"]
  fix --> verify["再実行して<br/>検証"]
  verify --> done["完了"]

  style start fill:#f88
  style identify fill:#8f8
  style done fill:#8f8

それぞれのステップを具体的に見ていきましょう。

Debug モードの起動方法

Playwright には複数の Debug モード起動方法があります。用途に応じて使い分けましょう。

方法 1:Inspector を使う(最も基本)

コマンドラインから --debug フラグを付けて実行します。

bashyarn playwright test --debug

これにより、Playwright Inspector が起動し、テスト実行を 1 ステップずつ確認できます。

方法 2:UI Mode を使う(推奨)

Playwright v1.32 以降では、UI Mode が利用できます。

bashyarn playwright test --ui

UI Mode では、ブラウザ上でテストの実行状況を確認しながら、インタラクティブにデバッグできます。複数のテストケースを切り替えながら確認できるため、最も効率的です。

方法 3:特定のテストだけをデバッグ

ファイル名やテスト名を指定して、特定のテストだけを Debug モードで実行できます。

bashyarn playwright test login.spec.ts --debug

テストファイルが多数ある場合は、この方法で対象を絞ると時間を節約できます。

Inspector の基本操作

Inspector が起動すると、以下のような GUI が表示されます。

主要な操作ボタン

#ボタン機能ショートカット
1Resume次のブレークポイントまで実行F8
2Step over次のステップへ進むF10
3Step into関数内にステップインF11
4Step out関数から抜けるShift+F11

これらのボタンを使って、テストを 1 行ずつ実行しながら問題箇所を特定します。

Inspector での確認ポイント

Inspector では、以下の情報をリアルタイムで確認できます。

DOM の状態

現在の DOM ツリーを確認し、要素が存在するか、表示されているかをチェックできます。

実行中のコード

どの行が実行されているかが強調表示され、変数の値も確認できます。

ロケーター情報

Playwright がどのようにして要素を特定しようとしているかが表示されます。

ブレークポイントの設定

コード内に page.pause() を挿入することで、その位置で実行を一時停止できます。

typescriptimport { test, expect } from '@playwright/test';

test('ログインフロー', async ({ page }) => {
  // ログインページに移動
  await page.goto('https://example.com/login');

  // ここで一時停止してフォームの状態を確認
  await page.pause();

  // ログイン情報を入力
  await page.fill(
    'input[name="email"]',
    'test@example.com'
  );
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
});

page.pause() を挿入すると、そこで実行が止まり、Inspector で DOM の状態やネットワーク通信を確認できます。

問題が起きそうな箇所の直前に page.pause() を配置することで、効率的にデバッグできるでしょう。

Trace Viewer でテスト全体を可視化

Trace Viewer は、テスト実行後に時系列で動作を振り返ることができる強力なツールです。

Trace の有効化

テスト実行時に Trace を記録するには、設定ファイルで有効化します。

typescript// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // 失敗時のみ Trace を記録(推奨)
    trace: 'on-first-retry',

    // または、常に記録する場合
    // trace: 'on',
  },
});

trace: 'on-first-retry' を設定すると、テストが失敗したときだけ Trace が記録されるため、ストレージ容量を節約できます。

Trace Viewer の起動

テスト実行後、以下のコマンドで Trace Viewer を開きます。

bashyarn playwright show-trace trace.zip

または、テスト実行時に --trace on を指定することもできます。

bashyarn playwright test --trace on

Trace Viewer で確認できる情報

Trace Viewer では、以下の情報をタイムライン形式で確認できます。

#情報説明
1スクリーンショット各アクションごとのブラウザ画面
2DOM スナップショットその時点での完全な DOM 構造
3ネットワークリクエストAPI 呼び出しやリソース読み込み
4コンソールログブラウザコンソールに出力されたログ
5アクション実行時間各操作にかかった時間

これらの情報を組み合わせることで、テストが失敗した正確なタイミングと原因を特定できます。

以下の図は、Trace Viewer で分析できる情報の関係性を示しています。

mermaidflowchart LR
  timeline["タイムライン"] --> action["アクション"]
  action --> screenshot["スクリーン<br/>ショット"]
  action --> dom["DOM<br/>スナップショット"]
  action --> network["ネットワーク<br/>リクエスト"]
  action --> console["コンソール<br/>ログ"]

  screenshot --> compare["Before/After<br/>比較"]
  dom --> inspect["要素の<br/>検査"]
  network --> timing["レスポンス<br/>時間"]
  console --> error["エラー<br/>メッセージ"]

UI Mode の活用

UI Mode は、テストをインタラクティブに実行・デバッグできる最も便利な方法です。

bashyarn playwright test --ui

UI Mode を起動すると、ブラウザで以下の機能が利用できます。

テスト一覧の表示

すべてのテストファイルとテストケースが一覧表示され、クリックで個別に実行できます。

Watch モード

コードを変更すると自動的にテストが再実行されるため、修正と確認のサイクルが高速化されます。

タイムトラベル

テスト実行の任意の時点に戻って、その時の DOM やネットワーク状態を確認できます。

ロケーター生成

画面上の要素をクリックすると、その要素に対する適切なロケーターコードを自動生成してくれます。

UI Mode は、特に以下のシーンで威力を発揮します。

  • 新しいテストを書くとき(ロケーター生成機能)
  • テストを修正しながら確認したいとき(Watch モード)
  • 複数のテストケースを比較したいとき(テスト一覧)

具体例

例 1:タイムアウトエラーの原因特定

よくある「Timeout 30000ms exceeded」エラーを Debug モードで解決してみましょう。

問題のテストコード

以下のテストが失敗します。

typescriptimport { test, expect } from '@playwright/test';

test('商品一覧ページの表示', async ({ page }) => {
  await page.goto('https://example.com/products');

  // 商品カードが表示されるまで待機(ここでタイムアウト)
  await page.waitForSelector('.product-card');

  const products = await page
    .locator('.product-card')
    .count();
  expect(products).toBeGreaterThan(0);
});

このテストを実行すると、waitForSelector でタイムアウトが発生します。

Debug モードで実行

bashyarn playwright test products.spec.ts --debug

Inspector が起動したら、waitForSelector の直前で一時停止し、以下を確認します。

確認 1:DOM を調べる

Inspector の要素タブで、.product-card という要素が存在するかを確認します。もし存在しない場合は、セレクターが間違っている可能性があります。

確認 2:ネットワークタブを見る

商品データを取得する API が実行されているか、レスポンスが返ってきているかを確認します。API が遅い、またはエラーになっている可能性があります。

確認 3:コンソールログを見る

JavaScript エラーが出ていないかを確認します。エラーがあると、商品カードが正しくレンダリングされない場合があります。

原因の特定と修正

Debug モードで確認した結果、以下のことがわかりました。

  • .product-card ではなく .product-item というクラス名が使われていた
  • API レスポンスは正常に返っていた

セレクターを修正します。

typescriptimport { test, expect } from '@playwright/test';

test('商品一覧ページの表示', async ({ page }) => {
  await page.goto('https://example.com/products');

  // 正しいセレクターに修正
  await page.waitForSelector('.product-item');

  const products = await page
    .locator('.product-item')
    .count();
  expect(products).toBeGreaterThan(0);
});

この修正により、テストが正常に通過するようになりました。

このように、Debug モードを使うことで、推測ではなく事実に基づいた修正ができるのです。

例 2:ボタンクリックが反応しない問題

ボタンをクリックしても画面遷移しないケースを見ていきましょう。

問題のテストコード

typescriptimport { test, expect } from '@playwright/test';

test('ログイン処理', async ({ page }) => {
  await page.goto('https://example.com/login');

  // フォームに入力
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'password123');

  // ログインボタンをクリック(ここで問題発生)
  await page.click('#login-button');

  // ダッシュボードページに遷移することを期待
  await expect(page).toHaveURL(
    'https://example.com/dashboard'
  );
});

このテストでは、ログインボタンをクリックしても画面が遷移せず、最後の expect で失敗します。

UI Mode で原因を調査

bashyarn playwright test login.spec.ts --ui

UI Mode でテストを実行し、ボタンクリックの前後を観察します。

タイムトラベルで確認

ボタンクリックの直前に戻って、以下を確認します。

  • ボタンが表示されているか
  • ボタンがクリック可能な状態か(disabled になっていないか)
  • JavaScript のイベントリスナーが設定されているか

ネットワークタブで確認

ボタンクリック後に、ログイン API が呼ばれているかを確認します。

原因の特定

UI Mode で調査した結果、以下のことがわかりました。

  • ボタンは表示されている
  • ボタンはクリックされている(Playwright のログに記録あり)
  • しかし、ログイン API が呼ばれていない

コンソールログを見ると、以下のエラーが出ていました。

textTypeError: Cannot read property 'value' of null

これは、フォームのバリデーションが失敗して、API 呼び出しが実行されていないことを示しています。

修正方法

実際のページを見ると、フォームには隠しフィールド csrf_token があることがわかりました。この値が設定されていないため、バリデーションエラーになっていたのです。

修正後のコード:

typescriptimport { test, expect } from '@playwright/test';

test('ログイン処理', async ({ page }) => {
  await page.goto('https://example.com/login');

  // CSRF トークンを取得して設定
  const csrfToken = await page.inputValue('#csrf_token');

  // フォームに入力
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'password123');

  // CSRF トークンが正しく設定されていることを確認
  await expect(page.locator('#csrf_token')).toHaveValue(
    csrfToken
  );

  // ログインボタンをクリック
  await page.click('#login-button');

  // ダッシュボードページに遷移することを期待
  await expect(page).toHaveURL(
    'https://example.com/dashboard'
  );
});

これにより、テストが正常に動作するようになりました。

例 3:CI 環境でのみ失敗するテストの調査

ローカルでは成功するのに、CI 環境でのみ失敗するテストは、Trace を使って調査します。

CI で Trace を有効化

GitHub Actions の例:

yaml# .github/workflows/test.yml
name: E2E Tests

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18

      # 依存関係のインストール
      - name: Install dependencies
        run: yarn install

      # Playwright のインストール
      - name: Install Playwright browsers
        run: yarn playwright install --with-deps

      # テスト実行(Trace を有効化)
      - name: Run tests
        run: yarn playwright test --trace on

      # Trace ファイルをアーティファクトとして保存
      - uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: playwright-traces
          path: test-results/

この設定により、テストが失敗した場合に Trace ファイルが保存されます。

Trace のダウンロードと分析

GitHub Actions の Artifacts から Trace ファイルをダウンロードし、ローカルで開きます。

bash# ダウンロードした trace.zip を開く
yarn playwright show-trace ~/Downloads/playwright-traces/trace.zip

Trace Viewer で、以下を確認します。

タイミングの違い

CI 環境では API レスポンスが遅い可能性があります。各アクションの実行時間を確認しましょう。

画面サイズの違い

CI 環境ではヘッドレスモードで実行されるため、画面サイズが異なる場合があります。レスポンシブデザインの問題かもしれません。

リソース読み込みの問題

CI 環境ではネットワークが遅い場合があります。画像や CSS の読み込み状況を確認しましょう。

よくある CI 特有の問題と対策

以下は、CI 環境でよく発生する問題と、その対策です。

#問題対策
1タイムアウトが発生しやすいtimeout オプションを増やす
2フォントが異なるスクリーンショット比較では許容範囲を設定
3並列実行での競合workers 設定で並列数を調整
4ネットワークが不安定リトライ設定を有効化

タイムアウト設定の例:

typescript// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // CI 環境では長めのタイムアウトを設定
  timeout: process.env.CI ? 60000 : 30000,

  use: {
    // アクションごとのタイムアウト
    actionTimeout: process.env.CI ? 15000 : 10000,
  },

  // 失敗時のリトライ
  retries: process.env.CI ? 2 : 0,
});

この設定により、CI 環境でのテストの安定性が向上します。

例 4:複雑なユーザー操作のステップ実行

複数のステップを踏むテスト(例:カート追加 → チェックアウト → 決済)では、どこで問題が起きているかを特定するのが難しい場合があります。

問題のテストコード

typescriptimport { test, expect } from '@playwright/test';

test('購入フローのテスト', async ({ page }) => {
  // 1. 商品ページに移動
  await page.goto('https://example.com/products/12345');

  // 2. カートに追加
  await page.click('button:has-text("カートに追加")');

  // 3. カートページへ移動
  await page.click('a[href="/cart"]');

  // 4. チェックアウト
  await page.click('button:has-text("購入手続きへ")');

  // 5. 配送先情報を入力
  await page.fill('#address', '東京都渋谷区...');
  await page.fill('#phone', '090-1234-5678');

  // 6. 決済方法を選択
  await page.click('input[value="credit-card"]');

  // 7. 注文確定(ここで失敗)
  await page.click('button:has-text("注文を確定する")');

  await expect(page).toHaveURL(/\/order\/complete/);
});

このテストはステップ 7 で失敗しますが、どこに原因があるかが不明です。

page.pause() を使った段階的デバッグ

各ステップの間に page.pause() を挿入して、状態を確認します。

typescriptimport { test, expect } from '@playwright/test';

test('購入フローのテスト', async ({ page }) => {
  await page.goto('https://example.com/products/12345');

  await page.click('button:has-text("カートに追加")');
  await page.pause(); // ← カート追加後の状態を確認

  await page.click('a[href="/cart"]');
  await page.pause(); // ← カートページの表示を確認

  await page.click('button:has-text("購入手続きへ")');
  await page.pause(); // ← チェックアウトページの表示を確認

  await page.fill('#address', '東京都渋谷区...');
  await page.fill('#phone', '090-1234-5678');
  await page.pause(); // ← 入力内容を確認

  await page.click('input[value="credit-card"]');
  await page.pause(); // ← 決済方法の選択を確認

  await page.click('button:has-text("注文を確定する")');

  await expect(page).toHaveURL(/\/order\/complete/);
});

Debug モードで実行します。

bashyarn playwright test purchase.spec.ts --debug

page.pause() で停止し、以下を確認します。

  • DOM の状態(必要な要素が存在するか)
  • フォームの入力値(正しく入力されているか)
  • JavaScript のエラー(コンソールに出ていないか)
  • ネットワークリクエスト(API が正しく呼ばれているか)

原因の特定

調査の結果、配送先情報の入力で以下の問題が見つかりました。

  • #phone フィールドに電話番号を入力したが、バリデーションエラーになっていた
  • フォームには「ハイフンなしで入力してください」という注記があった

修正後のコード:

typescript// ハイフンなしで入力
await page.fill('#phone', '09012345678');

このように、page.pause() を使うことで、複雑なフローでも 1 ステップずつ確認しながらデバッグできます。

デバッグが完了したら、page.pause() を削除してテストを実行しましょう。

例 5:ロケーター生成機能の活用

新しいテストを書くとき、正しいセレクターを見つけるのは意外と時間がかかります。UI Mode のロケーター生成機能を使うと、この作業が大幅に効率化されます。

UI Mode でロケーターを生成

bashyarn playwright test --ui

UI Mode を起動したら、以下の手順でロケーターを生成します。

手順 1:テストを一時停止

任意のテストを実行し、page.pause() の位置で停止します(または UI Mode の一時停止ボタンをクリック)。

手順 2:Pick Locator ボタンをクリック

UI Mode の右上にある「Pick Locator」ボタンをクリックします。

手順 3:要素を選択

ブラウザ画面上で、操作したい要素をクリックします。

手順 4:ロケーターコードをコピー

UI Mode に、その要素に対する最適なロケーターコードが表示されます。

typescript// 自動生成された例
page.getByRole('button', { name: '送信' });
page.getByLabel('メールアドレス');
page.getByTestId('submit-button');

これらのコードをコピーして、テストコードに貼り付けるだけです。

ロケーター生成の利点

手動でセレクターを書く場合と比較して、以下の利点があります。

#手動セレクターロケーター生成機能
1CSS セレクターを調べる手間自動で最適なセレクターを提案
2セレクターが壊れやすいアクセシビリティ重視で安定
3テストコードが読みにくい意味が明確で保守しやすい

特に、getByRolegetByLabel は、アクセシビリティを考慮したセレクターであり、HTML 構造が変わってもテストが壊れにくいメリットがあります。

ロケーターの優先順位

Playwright は、以下の優先順位でロケーターを生成します。

1. Role ベース(推奨)

typescriptpage.getByRole('button', { name: 'ログイン' });

ボタン、リンク、フォーム要素などは、ロールで指定するのが最も安定します。

2. Label ベース

typescriptpage.getByLabel('メールアドレス');

フォーム要素は、対応する <label> から取得できます。

3. Test ID ベース

typescriptpage.getByTestId('user-profile');

特定の要素には、data-testid 属性を付けることで、確実に取得できます。

4. CSS セレクター(最後の手段)

typescriptpage.locator('.user-profile > .username');

CSS セレクターは、HTML 構造が変わると壊れやすいため、最後の手段として使います。

ロケーター生成機能を使うことで、これらのベストプラクティスを自然に守ることができます。

デバッグ時のベストプラクティス

ここまでの例を踏まえて、効率的にデバッグするためのベストプラクティスをまとめます。

1. 小さい範囲から始める

テスト全体をデバッグするのではなく、失敗した箇所の前後だけを集中的に調査しましょう。

typescript// 失敗した箇所だけを test.only で実行
test.only('問題の箇所', async ({ page }) => {
  // ...
});

2. 複数のツールを組み合わせる

  • Inspector:リアルタイムでステップ実行
  • Trace Viewer:事後分析で全体像を把握
  • UI Mode:修正と確認のサイクルを高速化

これらを状況に応じて使い分けることで、デバッグ時間を最小化できます。

3. スクリーンショットを活用

問題の箇所でスクリーンショットを撮ることで、後から見返したり、チームで共有したりできます。

typescript// 特定の箇所でスクリーンショットを保存
await page.screenshot({ path: 'debug-screenshot.png' });

4. ログを積極的に出力

コンソールログや Playwright の console.log を使って、テストの進行状況を記録しましょう。

typescriptconsole.log('現在のURL:', page.url());
console.log(
  '要素の数:',
  await page.locator('.item').count()
);

5. タイムアウトを適切に設定

デバッグ中はタイムアウトを長めに設定し、焦らずに確認できるようにします。

typescript// デバッグ用に長いタイムアウトを設定
test.setTimeout(120000); // 2 分

まとめ

Playwright の Debug モードを活用することで、E2E テストが失敗した原因を 5 分程度で特定できるようになります。本記事で紹介した内容を振り返りましょう。

Debug モードの起動方法

  • --debug フラグで Inspector を起動
  • --ui フラグで UI Mode を起動(推奨)
  • --trace on で Trace を記録

効率的なデバッグフロー

  1. Debug モードでテストを実行
  2. Inspector で実行を一時停止
  3. DOM、ネットワーク、コンソールを確認
  4. Trace Viewer で時系列を分析
  5. 原因を特定して修正

実践的なテクニック

  • page.pause() でブレークポイントを設定
  • UI Mode のロケーター生成機能を活用
  • CI 環境では Trace を有効化してアーティファクトを保存
  • 複数のツールを組み合わせて効率化

これらの手法を使うことで、テストの失敗原因を推測ではなく事実に基づいて特定できるようになります。デバッグ時間が短縮されることで、より多くの時間をテストの充実や機能開発に充てられるでしょう。

Playwright の Debug モードは、E2E テストの信頼性を高めるための必須ツールです。ぜひ日々の開発フローに取り入れて、快適なテスト駆動開発を実現してください。

関連リンク