Claude Media
Claude CodeでPlaywrightのtraceを読み、テスト失敗の原因を切り分ける

Claude CodeでPlaywrightのtraceを読み、テスト失敗の原因を切り分ける

CIで落ちたPlaywrightのtrace.zipをテキストに開き、claude -pに渡してセレクタ変更・待機不足・実バグを切り分ける手順です。

CIで落ちたPlaywrightのテストは、trace.zipの中に原因の手がかりがほぼ残っています。ただしzipのままClaude Codeに渡しても、読めるのは一部だけです。Playwright 1.64.1で意図的に落としたテストからtrace.zipを作って中身を調べると、判定に効く情報は少数のテキストに絞れました。それをパイプでclaude -pに渡せば、セレクタ変更・待機不足・実バグの切り分けまでを非対話で回せます。

テストの自動実行をhookで回す方法はClaude CodeでPlaywrightのE2Eテストをhookで自動実行するにあり、ここで扱うのはその後、CIで落ちた1本を調べる工程です。

traceが残る設定にしておく

調べる前に、失敗した実行のtraceが手元に届く設定になっているか確かめます。記録モードは次のとおりです。

traceの値記録される範囲
on-first-retry記録される範囲失敗したテストの最初のリトライだけ
on-all-retries記録される範囲すべてのリトライ
retain-on-failure記録される範囲全テストで記録し、成功した実行の分は捨てる
retain-on-first-failure記録される範囲最初の実行だけ記録し、失敗したときだけ残す
on記録される範囲全テストで記録(負荷が大きく非推奨)

雛形が生成する設定はon-first-retryで、CIではretries: 2、ローカルではretries: 0です。そのためローカルで1回落ちただけではtraceは残りません。リトライを使わずに失敗分だけ欲しいならretain-on-failureにします。

import { defineConfig } from '@playwright/test';
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
});

on-first-retryのまま運用するときは落とし穴があります。traceは1回目の失敗ではなく、リトライ側の実行を記録したものです。1回目と2回目でエラーの種類が違うなら、その差もそのまま調査材料になります。

zipのまま渡さず、証拠をテキストに開く

trace.zipは名前のとおりzipアーカイブです。Playwright 1.64.1でretain-on-failureにして失敗させた実行では、unzip -lの結果が次の12ファイルでした(長いハッシュ名のファイルは省略)。

unzip -l test-results/tests-login-submit-is-clickable/trace.zip
src/<hash>.ts            テストのソース
attachments/<hash>       添付(スクリーンショットなど)
test.trace               テストランナー側のステップ
0-trace.trace            ブラウザコンテキストの記録
1-trace.trace            同上(ページ操作とログ)
0-trace.network          ネットワークの記録
1-trace.network          同上
0-trace.stacks           スタック情報
1-trace.stacks           同上
screencast/*.jpeg        画面のフィルムストリップ

*.traceは1行1JSONの形式でした。ここからtypeがlogの行を取り出すと、Playwrightが待機中に残した内部ログがそのまま読めます。Trace ViewerのLogタブに出る内容と同じ種類の情報です。

失敗の周辺には、ほかにも渡せるテキストがあります。

  • 失敗時にテスト別ディレクトリへ出るerror-context.md。この実行では「Explain why, be concise」というAI向けの指示文、エラー詳細、ページのアクセシビリティスナップショット、ソースの抜粋が1ファイルに入っていました
  • --reporter=jsonのJSON。PLAYWRIGHT_JSON_OUTPUT_NAME=results.jsonで出力先を指定します
  • test-failed-1.pngのようなスクリーンショット。画像のパスをプロンプトに書けば、Claude Codeは画像も分析できます

生のzipをclaude -pに流し込む必要はありません。標準入力は10MBが上限で、超えるとエラーで終了します。大きい入力は、ファイルに書いてパスをプロンプトに書く方式が案内されています。

ログの形で失敗を4つに分ける

3本の意図的な失敗を作ると、Callログの形がはっきり分かれました。切り分けの軸はこの形です。

ログの特徴読み方次に見る場所
waiting for locator('#send')の後にlocator resolved toが出ない読み方セレクタに一致する要素がない次に見る場所error-context.mdのページスナップショット
locator resolved to <button … disabled>の後にelement is not enabledが繰り返される読み方要素はあるが操作できる状態にならない次に見る場所有効化を起こす処理のネットワークとコンソール
ExpectedとReceivedが並び、unexpected valueが繰り返される読み方ロケーターは正しく、値が食い違う次に見る場所APIの応答と、期待値が古くなっていないか
browserType.launch: Executable doesn't exist読み方ブラウザが入っていない環境の問題次に見る場所CIのインストール手順

1行目の例では、テストが#sendを探し、DOMにはid="send-v2"のボタンがありました。error-context.mdのスナップショットにはbutton "送信"が1つだけ出ており、役割と名前から取り直せます。これがセレクタ変更の典型です。

2行目は慎重に読みます。element is not enabledが増えるのは、待機時間が足りないときも、有効化するはずのAPI呼び出しが失敗しているときもあるからです。タイムアウトを延ばして通るなら待機不足、延ばしても通らないなら実バグを疑う、という順で進めます。

3行目のExpectedとReceivedは、Playwrightが要素を見つけたうえで値を比べた結果です。セレクタの問題はここで除外できます。残る疑いは、アプリの不具合かテストの期待値の古さの2つです。

traceの証拠を抜き出してclaude -pに渡す

判定に使う部分だけをテキストにするシェルスクリプトです。jqが必要で、ここではunzipとmktempを使います。

#!/bin/bash
# triage-trace.sh <test-results配下のテスト別ディレクトリ>
set -euo pipefail
DIR="$1"
WORK=$(mktemp -d)
unzip -q "$DIR/trace.zip" -d "$WORK"
 
{
  cat "$DIR/error-context.md"
  echo "--- trace log (last 40 lines) ---"
  jq -r 'select(.type=="log") | .message' "$WORK"/*.trace | tail -n 40
} | claude -p \
    "このPlaywrightテストの失敗原因を次の4つから1つ選んでください。
     1 セレクタ変更 2 待機不足 3 アプリの不具合 4 環境
     根拠にしたログの行をそのまま引用し、断定できなければ不明と答えてください。
     修正案は書かないでください。" \
    --allowedTools "Read" \
    --output-format json | jq -r '.result'

ポイントは3つあります。

  1. 分類だけを頼み、修正は頼まない。原因を絞る段階とコードを直す段階を分けると、誤った分類のまま直しに進む事故を防げます
  2. 引用を必須にする。ログの行を引用させれば、人が表の4分類と照らして確かめられます
  3. ツールはReadだけ。 claude -pでは、承認が要る操作が許可されていなければ拒否されます。調査段階で編集やコマンド実行をさせない構成にできます

CIの中で動かすなら--bareを足すと、ホストのhookやCLAUDE.mdを読まずに毎回同じ条件で起動します。--bareはサブスクリプションのログインを使わないため、ANTHROPIC_API_KEYの設定が前提です。認証まわりの失敗はclaude -pがOAuth session expiredで失敗する原因と回避策が詳しいです。

スクリーンショットも見せたいときは、プロンプトに画像のパスを書き足します。

claude -p "test-results/tests-login-submit-is-clickable/test-failed-1.png を見て、\
ボタンが画面に出ているか答えてください" --allowedTools "Read"

失敗が複数あるときは構造化出力で集計する

1本ずつ文章で返ってきても、20本落ちた夜には読み切れません。--output-format jsonに--json-schemaを添えると、分類を決まった形で受け取れます。応答のstructured_outputフィールドに入るため、jqで拾って集計できます。

まずJSONレポーターで結果を書き出します。PLAYWRIGHT_JSON_OUTPUT_NAMEで出力先を指定する方式です。

PLAYWRIGHT_JSON_OUTPUT_NAME=results.json \
  npx playwright test --reporter=json

JSONの各テストには、結果ごとにattachmentsの配列があります。Playwright 1.64.1で2本落としたとき、どちらの結果にもscreenshot・error-context・traceの3つが入り、それぞれにファイルの絶対パスがありました。traceのパスからディレクトリを取り出せば、失敗したテストを順に回せます。

jq -r '.. | .specs? // empty | .[] | .title as $t
  | .tests[].results[] | select(.status=="failed")
  | .attachments[] | select(.name=="trace")
  | [$t, (.path | split("/") | .[:-1] | join("/"))] | @tsv' results.json |
while IFS=$'\t' read -r title dir; do
  { cat "$dir/error-context.md"
    unzip -p "$dir/trace.zip" '*.trace' \
      | jq -r 'select(.type=="log") | .message' | tail -n 40
  } | claude -p "失敗原因を分類してください。根拠のログ行を引用すること。" \
      --output-format json \
      --json-schema '{"type":"object","properties":{"category":{"type":"string","enum":["selector","timing","app-bug","environment","unknown"]},"evidence":{"type":"string"}},"required":["category","evidence"]}' \
    | jq -c --arg t "$title" '{test:$t} + .structured_output'
done

.last-run.jsonも役に立ちます。失敗したテストのIDを持つファイルで、npx playwright test --last-failedは失敗分だけを再実行します。分類の後に直して確かめるときの最短経路です。

スキーマに合う出力が得られなかった実行では、structured_outputフィールド自体がありません。jqの後段で存在を確かめてから使います。unknownを選択肢に残しておけば、Claudeに無理な断定を迫らずに済みます。

分類のあとに頼む修正の指示

分類が出たら、型ごとに修正の頼み方を変えます。同じ「直して」では、対象のずれた修正が混ざります。

  • セレクタ変更: error-context.mdのスナップショットにある役割と名前を使い、getByRoleに取り直すよう頼みます。IDやCSSクラスへの依存を残さない、という条件も添えます
  • 待機不足: waitForTimeoutの追加は頼みません。待つべき状態をexpectで明示する形に直すよう頼みます。要素の操作は、見えて・有効で・安定するまでPlaywrightが自動で待つので、足りないのは「何を待つか」の表明という場合があるからです
  • アプリの不具合: テストは変えず、再現手順と該当のAPI応答をまとめる依頼にとどめます。テスト側を緩めて通すと、不具合が見えなくなります
  • 環境: アプリ側の変更ではなく、ワークフローのインストール手順を確かめる対象です

この4つをCLAUDE.mdに書いておけば、--bareを付けないclaude -pでも対話セッションでも、同じ基準が読み込まれます。

CIでtraceを持ち帰る

GitHub Actionsでは、テスト実行の後に成果物をアップロードします。ひな形はHTMLレポートのplaywright-report/だけを上げますが、スクリプトで使うのはtest-results/側のtrace.zipとerror-context.mdです。失敗時に両方を上げておくと、ローカルに落として上のスクリプトを回せます。

- uses: actions/upload-artifact@v5
  if: ${{ !cancelled() }}
  with:
    name: playwright-results
    path: |
      playwright-report/
      test-results/
    retention-days: 30

ワークフロー自体にClaudeを組み込む方法はGitHub Actionsのワークフロー作成をClaude Codeで進める手順にまとめています。

HTMLレポートはnpx playwright show-reportで開けます。リモートにあるtrace.zipは、npx playwright show-traceにURLを渡せば手元にダウンロードせず開けます。trace.playwright.devにzipをドロップする方法もあり、こちらはブラウザ内で読み込まれ、データは外部に送られません。

分類を人が確かめるときの見どころ

Claudeの分類が怪しいと感じたら、Trace Viewerで次の順に見ます。

  • Actions: 失敗したアクションを選び、BeforeとAfterのスナップショットを見比べます。クリック位置もここで分かります
  • Log: 要素が見えて・有効で・安定するのを待つ過程が出ます。上の表の元データです
  • Errors: タイムライン上に赤い線で失敗位置が出ます
  • Network: アクションを選んで絞り込むと、その操作中のリクエストだけが並びます。ステータスコードを比べると、3行目の型で実バグを疑う根拠になります
  • Console: ブラウザ側とテスト側のログが区別して並びます

画面の見た目を比べる必要があるときは、Attachmentsタブでスクリーンショットの差分を重ねて確かめられます。

判定を誤りやすい場面

ローカルで通りCIだけ落ちる場合は、error-context.mdのスナップショットがローカルと違う画面を指していないかを先に見ます。CIだけ描画が遅いのか、そもそも別の画面なのかを、そこで分けられます。

リトライで通ったテストは失敗扱いになりませんが、on-first-retryならtraceは残ります。通ったtraceにも同じelement is not enabledの繰り返しがあれば、待機不足の予備軍です。

リトライをまたいで原因が変わるときは、JSONレポーターのresults配列が手がかりです。1テストの結果がretryの番号つきで並ぶので(最初の実行は0)、番号ごとにerror.messageを取り出して比べます。1回目はセレクタ不一致、2回目はタイムアウトなら、実行ごとに別の原因で落ちていると分かるので、traceは1つの失敗に絞らず全回分を見ます。

Claudeの判定が不明で返ってきたときは、渡したログの行数を増やす前に、error-context.mdが入っているか確かめます。ログだけでは「どの要素を探したか」は分かっても、画面に何があったかは分かりません。

まとめ

traceの調査を速くするのは、ログの形で原因を絞る型を先に持っておくことです。locator resolvedが出るかどうかで、セレクタの問題とそれ以外を最初に分けられます。Claudeには分類と引用だけを任せ、修正はその後に別の指示で頼む。この2段に分けるだけで、見当違いの修正を減らせます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. playwright.dev/docs/trace-viewer
  3. playwright.dev/docs/trace-viewer-intro
  4. playwright.dev/docs/test-reporters
この記事を共有:XはてブLinkedIn