PlaywrightのTrace Viewerで、失敗したE2Eテストの原因を調べる

PlaywrightのTrace Viewerで、失敗したE2Eテストの原因を調べる

PlaywrightのE2Eテストがタイムアウトしたとき、待ち時間を延ばす前にTrace Viewerで失敗時の状態を調べます。APIが500を返すサンプルを使い、失敗したexpect、画面の状態、通信の応答を順に確認し、修正後の成功まで追いました。
2026.10.21

どうも!オペ部の西村祐二です!

E2Eテストで「要素が見つからない」というエラーが出ても、それだけでは、セレクターが違うのか、画面の表示が遅いのか、APIが失敗したのか分かりません。CIで失敗した場合は、そのときの画面を手元で見られないこともあります。

PlaywrightのTrace Viewerでは、テスト実行時の操作、DOMのスナップショット、通信などを後から確認できます。

今回は、注文一覧を読み込むAPIが500を返すサンプルを作りました。表示テストのタイムアウトを手がかりにAPIの失敗を突き止め、正常応答に戻してテストが通るところまでを紹介します。CI固有の障害を再現する記事ではなく、CIでも残しておける調査材料の読み方を、ローカルで試す内容です。

まずは失敗した操作から、画面、通信の順に見る

トレースを開いたら、次の順序で確認します。

  1. Errors / Sourceで、失敗した期待値とコードの位置を見る
  2. 失敗した操作を選び、DOMスナップショットで画面の状態を見る
  3. Networkで関連するリクエストのステータスとレスポンス本文を見る

今回の例では、ORDER-001という注文が表示されることを期待していました。しかし、画面には読み込み失敗が表示され、APIは500を返していました。この状態では、expectの待ち時間を延ばしても注文は表示されません。

以下は自作した調査手順の概念図です。公式の構成図ではありません。

自作したTrace Viewerの調査手順の概念図。失敗したexpect、画面の読み込み失敗、APIの500応答の順に確認する。

失敗したテストのトレースを残す

既存のPlaywright Testプロジェクトなら、playwright.config.tsのuse.traceに設定を追加します。今回は初回の失敗を調べたいので、再試行なしでも記録を残せるretain-on-failureを使いました。

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

export default defineConfig({
  use: {
    trace: 'retain-on-failure',
  },
});

トレースの記録設定には、主に次の選択肢があります。

設定 トレースを残すタイミング 使う場面
retain-on-failure 各実行を記録し、成功した実行のトレースを削除する 初回の失敗も含めて調べたいとき
on-first-retry 最初の再試行を記録する 再試行を設定しているCI
on 成否にかかわらず記録する 成功時の操作も含めて確認したいとき

失敗後は、出力に表示されたpnpm exec playwright show-trace <trace.zipのパス>で開けます。既にトレースがある場合は、後述の「Trace Viewerで失敗の前後を確認する」から読み進められます。

on-first-retryは初回実行の記録ではありません。retries: 0のままでは再試行がないため、この設定でトレースは生成されません。

また、この記事ではPlaywright Testの設定を使っています。context.tracingを直接使う方法ではexpectの情報を記録しないため、テストのアサーションも調べる用途にはuse.traceが適しています。

APIが失敗するサンプルを動かす

確認した環境は、macOS 26.6.1(Apple Silicon)、Node.js v24.21.0、pnpm 12.14.0、Playwright Test 1.64.1です。特定バージョンの不具合に依存した例ではありません。

新しいディレクトリで、テストランナーとChromiumを準備します。

mkdir playwright-trace-demo
cd playwright-trace-demo
pnpm add -D -E @playwright/test@1.64.1
pnpm exec playwright install chromium
mkdir tests

注文一覧を読み込むページを作る

server.mjsはページとAPIを同じサーバーで返します。通常はAPIが500を返し、FIXED=1を指定して起動した場合だけ、注文データを200で返すようにしています。外部のサービスやデータベースは使いません。

server.mjs
import http from 'node:http';
const fixed = process.env.FIXED === '1';
http.createServer((req, res) => {
  if (req.url === '/api/orders') {
    res.writeHead(fixed ? 200 : 500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(fixed ? [{ id: 'ORDER-001' }] : { error: 'Order database unavailable' }));
    return;
  }
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.end(`<!doctype html><html lang="en"><title>Orders demo</title>
<style>body{font:20px system-ui;max-width:680px;margin:64px auto}button{font:inherit;padding:10px 20px}#status{margin-top:24px;color:#9c2525}</style>
<h1>Orders</h1><button>Load orders</button><p id="status">Ready</p><ul></ul>
<script>
document.querySelector('button').onclick = async () => {
  const response = await fetch('/api/orders');
  if (!response.ok) {
    document.querySelector('#status').textContent = 'Could not load orders';
    return;
  }
  const orders = await response.json();
  document.querySelector('#status').textContent = 'Loaded';
  document.querySelector('ul').replaceChildren(...orders.map(order => {
    const li = document.createElement('li'); li.textContent = order.id; return li;
  }));
};
</script></html>`);
}).listen(4173, '127.0.0.1');

テストと設定を用意する

テストは、ボタンを押した後に注文番号が表示されることだけを確かめます。

tests/orders.spec.ts
import { test, expect } from '@playwright/test';
test('loads orders', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Load orders' }).click();
  await expect(page.getByRole('listitem')).toHaveText('ORDER-001');
});

サーバーはPlaywrightのwebServerで起動します。ここで使う設定ファイルの全体は次のとおりです。先ほどのトレース設定だけの例を、この内容に置き換えます。

playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  retries: 0,
  reporter: [['list'], ['html', { open: 'never' }]],
  use: { baseURL: 'http://127.0.0.1:4173', trace: 'retain-on-failure' },
  webServer: {
    command: 'node server.mjs',
    url: 'http://127.0.0.1:4173',
    reuseExistingServer: false,
  },
});
pnpm exec playwright test

結果は1件の失敗でした。主なエラーは次のとおりです。

Error: expect(locator).toHaveText(expected) failed

Locator: getByRole('listitem')
Expected: "ORDER-001"
Timeout: 5000ms
Error: element(s) not found

この出力だけで分かるのは、「5秒待っても期待する要素が見つからなかった」ことです。APIの失敗が原因かどうかは、まだ分かりません。

Trace Viewerで失敗の前後を確認する

失敗時の出力にはtrace.zipの保存先と、開くコマンドが表示されます。今回のテスト名では次のパスでした。

pnpm exec playwright show-trace test-results/orders-loads-orders/trace.zip

保存先はテスト名やproject設定で変わるため、実際にはテスト結果に表示されたパスを使います。

ErrorsとSourceで失敗したexpectを確認する

左側の操作一覧で、赤く表示されたExpect "toHaveText"を選び、下部のErrorsを開きます。今回はgetByRole('listitem')に対するexpectが失敗していました。Sourceでは対応するテストコードを確認できます。

次の画像では、左側に失敗したexpect、中央に当時の画面、下部にエラーの詳細が表示されています。

Trace Viewerで失敗したexpectを選び、注文一覧の読み込み失敗とErrorsの詳細を確認した画面。

スナップショットで画面の状態を確認する

画面には注文番号の代わりにCould not load ordersと表示されていました。テストが探している注文要素が存在しないことと、アプリが読み込み失敗を表示していることが分かります。

ここで表示されるのは、記録されたDOMのスナップショットです。録画の静止画とは異なり、操作前後の状態を切り替えて確認できます。今回は失敗したexpectを選んだ状態で、読み込み失敗の表示を確認しました。

Networkで500応答と本文を確認する

Networkを開き、/api/ordersのリクエストを選びます。Headersではステータスが500 Internal Server Errorであることを、Responseでは次の本文を確認できました。

{"error":"Order database unavailable"}

以下はResponseを表示した画面です。注文の読み込みに失敗した画面と、APIのエラー本文を一緒に確認できます。

Trace ViewerのNetworkで注文APIのResponseを開き、Order database unavailableというエラー本文を確認した画面。

サンプルでは、APIが500を返すと画面にエラーを表示し、注文の要素を作らない実装にしています。expectのタイムアウトは、その結果として発生していました。

実際のアプリなら、この後はAPI側のログや接続先を調べることになります。Trace Viewerで500応答を確認できても、データベースの障害原因まで分かるわけではありません。今回のOrder database unavailableは、サンプルが固定で返すメッセージです。

通信が多い場合は、NetworkのフィルターでURLを絞れます。また、操作をダブルクリックすると、その操作中の通信に絞り込めます。通信が見つからないときは、時間範囲も確認する必要があります。

正常応答に戻して、同じテストを再実行する

サンプルのAPIを200応答へ切り替えます。webServerの子プロセスにも環境変数が渡るので、テストコードは変更しません。

FIXED=1 pnpm exec playwright test

再実行では、ORDER-001が表示されて1件成功しました。

1 passed

retain-on-failureの設定では、成功した実行のトレースは残りませんでした。今回の切り替えは調査手順を試すための仕組みで、実際のアプリでは、切り分けたAPI側の問題を修正してから同じテストを実行する流れになります。

CIでは再試行と成果物の保存を組み合わせる

公式ドキュメントでは、CI向けにon-first-retryが推奨されています。再試行を1回にする場合の変更部分は次のとおりです。既存のbaseURLやwebServerなどの設定は残し、retriesとuse.traceに反映します。

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

export default defineConfig({
  retries: process.env.CI ? 1 : 0,
  use: {
    trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure',
  },
});

最初の失敗が再試行で再現しないこともあるため、初回の失敗そのものを見たい用途ではretain-on-failureが候補になります。全実行を残すonは記録の負荷があるため、必要な期間に絞って使うのがよさそうです。

CIジョブの終了後にも確認するには、HTMLレポートやトレースを成果物として保存する設定も必要です。HTMLレポートを使う場合は、レポート全体を取得してshow-reportで開き、対象テストのトレースから調べられます。

この例の失敗と成功はローカルで確認しました。CIへの成果物の保存は、公式ドキュメントに基づく運用例です。

まとめ

Playwrightのテストがタイムアウトしたときは、Trace Viewerで「失敗したexpect → 画面の状態 → 通信」の順に追うと、次に調べる場所を絞れます。

今回の例では、注文の要素が見つからないというエラーから、画面の読み込み失敗とAPIの500応答を確認できました。初回の失敗を残すならretain-on-failure、再試行時の記録を中心にするならon-first-retryという違いも、設定時に意識したいところです。

参考リンク

この記事をシェアする

関連記事