
PlaywrightのTrace Viewerで、失敗したE2Eテストの原因を調べる
どうも!オペ部の西村祐二です!
E2Eテストで「要素が見つからない」というエラーが出ても、それだけでは、セレクターが違うのか、画面の表示が遅いのか、APIが失敗したのか分かりません。CIで失敗した場合は、そのときの画面を手元で見られないこともあります。
PlaywrightのTrace Viewerでは、テスト実行時の操作、DOMのスナップショット、通信などを後から確認できます。
今回は、注文一覧を読み込むAPIが500を返すサンプルを作りました。表示テストのタイムアウトを手がかりにAPIの失敗を突き止め、正常応答に戻してテストが通るところまでを紹介します。CI固有の障害を再現する記事ではなく、CIでも残しておける調査材料の読み方を、ローカルで試す内容です。
まずは失敗した操作から、画面、通信の順に見る
トレースを開いたら、次の順序で確認します。
- Errors / Sourceで、失敗した期待値とコードの位置を見る
- 失敗した操作を選び、DOMスナップショットで画面の状態を見る
- Networkで関連するリクエストのステータスとレスポンス本文を見る
今回の例では、ORDER-001という注文が表示されることを期待していました。しかし、画面には読み込み失敗が表示され、APIは500を返していました。この状態では、expectの待ち時間を延ばしても注文は表示されません。
以下は自作した調査手順の概念図です。公式の構成図ではありません。

失敗したテストのトレースを残す
既存のPlaywright Testプロジェクトなら、playwright.config.tsのuse.traceに設定を追加します。今回は初回の失敗を調べたいので、再試行なしでも記録を残せるretain-on-failureを使いました。
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で返すようにしています。外部のサービスやデータベースは使いません。
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');
テストと設定を用意する
テストは、ボタンを押した後に注文番号が表示されることだけを確かめます。
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で起動します。ここで使う設定ファイルの全体は次のとおりです。先ほどのトレース設定だけの例を、この内容に置き換えます。
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、中央に当時の画面、下部にエラーの詳細が表示されています。

スナップショットで画面の状態を確認する
画面には注文番号の代わりにCould not load ordersと表示されていました。テストが探している注文要素が存在しないことと、アプリが読み込み失敗を表示していることが分かります。
ここで表示されるのは、記録されたDOMのスナップショットです。録画の静止画とは異なり、操作前後の状態を切り替えて確認できます。今回は失敗したexpectを選んだ状態で、読み込み失敗の表示を確認しました。
Networkで500応答と本文を確認する
Networkを開き、/api/ordersのリクエストを選びます。Headersではステータスが500 Internal Server Errorであることを、Responseでは次の本文を確認できました。
{"error":"Order database unavailable"}
以下はResponseを表示した画面です。注文の読み込みに失敗した画面と、APIのエラー本文を一緒に確認できます。

サンプルでは、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に反映します。
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という違いも、設定時に意識したいところです。







