Claude CodeでPlaywrightスクリーンショット比較を読み、画面の崩れを切り分ける
toHaveScreenshotが落ちたとき、expected・actual・diffの3枚と呼び出しログをClaudeに渡して崩れか意図した変更かを分け、ベースライン更新を人の判断に残す手順です。
PlaywrightのtoHaveScreenshot()が落ちると、test-resultsに基準画像・今回の画像・差分画像の3枚が出ます。この3枚と呼び出しログをClaude Codeに渡せば、「意図した変更」「回帰」「環境差」のどれかを絞り込めます。ただし--update-snapshotsで基準を差し替える判断は、人に残したほうが安全です。崩れた画面がそのまま新しい正解になるからです。
Playwright 1.64.1で意図的に画面を変えて落とした実行をもとに、何が残るか、何を渡すか、基準更新をどう止めるか、CIで画像をどう届けるかを順に書きます。落ちた理由がセレクタや待機にある場合は、Claude CodeでPlaywrightのtraceを読み、テスト失敗の原因を切り分けるの領分です。
差分が出たとき、手元に残るファイル
1枚のスクリーンショットを撮るテストを用意し、基準を作ってからボタンの色とカードの幅を変えて実行しました。
import { test, expect } from '@playwright/test';
test('card', async ({ page }) => {
await page.goto('index.html');
await expect(page).toHaveScreenshot('card.png');
});基準がまだ無い最初の実行は、失敗として終わります。Playwrightが現在の画像をcard.spec.ts-snapshots/に書き出すためで、2回目から比較が始まります。ファイル名はこの環境でcard-darwin.pngになりました。ブラウザ名とOSが名前に入る仕組みなので、macOSで作った基準はLinuxのCIでは使われません。
画面を変えて再実行すると、test-results/card-card/に次のファイルが出ました。
| ファイル | 中身 |
|---|---|
card-expected.png | 中身比較に使った基準画像(スナップショットの複製) |
card-actual.png | 中身今回撮った画像 |
card-diff.png | 中身差分を示した画像 |
error-context.md | 中身「Explain why, be concise」で始まるAI向けの指示、エラー詳細、呼び出しログ |
エラー本文には「2842 pixels (ratio 0.01 of all image pixels) are different.」と出ます。呼び出しログを読むと、Playwrightは1回撮って差を見つけたあと、100ms待ってもう一度撮り、安定した画像で改めて比較していました。1回目の揺れで落ちたのではなく、撮り直しても差が残ったことが分かります。
card-diff.pngを読ませると、変わったボタンの領域が赤で塗られ、変わっていない見出しや枠は薄いグレーに落ちていました。差分画像は「どこが変わったか」を示すだけで、「それが正しいか」までは教えてくれません。この点を、次の切り分けで補います。
3枚と数字を渡して、4つに分類させる
Claude Codeはプロンプトに画像のパスを書けば画像を読めます。非対話モードのclaude -pでも同じで、Readだけを許可しておけば調査中にファイルを書き換えることはありません。
#!/bin/bash
# triage-visual.sh <test-results配下のテスト別ディレクトリ>
set -euo pipefail
DIR="$1"
FILES=$(ls "$DIR"/*.png "$DIR"/error-context.md)
claude --bare -p "Playwrightのスクリーンショット比較が失敗しました。
次のファイルを読んでください。-expectedが基準、-actualが今回、-diffが差分です。
$FILES
差分の原因を4つから1つ選んでください。
1 意図した変更の可能性 2 回帰(崩れ) 3 環境差(フォント・OS) 4 動的な内容の揺れ
画像のどこがどう変わったかを1文で述べ、ログの差分ピクセル数を引用してください。
基準の更新や修正案は書かないでください。" \
--allowedTools "Read"シェルのダブルクォート内ではワイルドカードが展開されないため、lsで実在するファイル名に解決してからプロンプトに埋めています。1つのテストが複数枚のスクリーンショットを撮る場合も、ファイルの一覧がそのまま渡ります。
--bareは、ホストのhookやCLAUDE.mdを読まずに起動するモードです。公式のヘッドレス解説では、スクリプトやSDKからの呼び出しに勧められています。ログイン認証は使えないので、ANTHROPIC_API_KEYなどを環境変数に置いて動かします。認証まわりでつまずいたら、claude -pがOAuth session expiredで失敗する原因と回避策が参考になります。
プロンプトで効くのは次の3点です。
- 分類だけを頼む。修正案まで書かせると、崩れを直すつもりで基準画像の側に手を入れる流れになりやすくなります
- ピクセル数の引用を求める。「2842ピクセル」が画像のどの範囲に当たるかを、ボタン1個分なのか画面全体なのかで答えさせると、根拠が人にも見えます
- 選択肢に「環境差」と「動的な内容」を入れる。回帰か意図した変更かの二択にすると、フォント差のような誤検知の置き場がなくなります
複数のテストが落ちた夜は、分類を構造化して集計する
デザイン変更の翌日は、十数本のスクリーンショット比較がまとめて落ちます。1本ずつ文章で返されても読み切れないので、--output-format jsonに--json-schemaを添えて、返答の形を決めます。スキーマに適合した結果は応答のstructured_outputに入ります。
claude --bare -p "$PROMPT" --allowedTools "Read" \
--output-format json \
--json-schema '{"type":"object","properties":{"category":{"enum":["intended","regression","environment","dynamic"]},"evidence":{"type":"string"}},"required":["category","evidence"]}' \
| jq -r '.structured_output | "\(.category)\t\(.evidence)"'公式は、有効な構造化出力が得られなかった実行ではstructured_outputが付かないと説明しています。スクリプトではjqに渡す前に、このフィールドがあるかを確かめます。
categoryがregressionのテストだけを先に人が見る、という並べ替えができます。intendedが大半で、同じコンポーネントを指していれば、デザイン変更のまとめ更新として1回確認すれば足ります。
差分の見え方から、ベースラインを更新するか決める
分類結果を、基準を更新するかどうかの判断に結び付けます。Claudeの答えは材料であり、決めるのは人です。
| 差分の見え方 | 分類 | 次の動き |
|---|---|---|
| 変更したコンポーネントの領域だけが塗られ、デザイン変更のチケットと一致する | 分類意図した変更 | 次の動き差分を目で確かめ、基準を更新する |
| 関係ない場所のレイアウトがずれている、要素が重なる、欠ける | 分類回帰 | 次の動き基準は更新せず、CSSとテンプレートを調べる |
| 文字の輪郭全体が薄く塗られ、画面全体に細かい差分が散る | 分類環境差 | 次の動き基準を作った環境と比べる(下の節) |
| 日付・広告・アニメーション・埋め込みの場所だけが塗られる | 分類動的な内容 | 次の動き撮影前に隠すか固定する(その次の節) |
画面全体のレイアウトが動いた回帰の追い方は、Claude CodeでCLS(レイアウトシフト)の原因を探して直す手順の考え方がそのまま使えます。
基準を更新する側のコマンドはnpx playwright test --update-snapshotsです。CLIのリファレンスでは--update-snapshots [mode]の値にall・changed・missing・none・defaultがあり、値を付けずに指定するとchangedとして動くと書かれています。更新後は、*-snapshotsディレクトリの変更をgitの差分として人が確認します。公式の説明も、このディレクトリはバージョン管理に入れて変更をレビューするよう求めています。
落とし穴
Claudeに更新まで任せると、崩れが新しい正解になる
テストが落ちたとき、一番手っ取り早い「直し方」は--update-snapshotsを実行することです。エージェントに「テストを通して」と頼むと、この近道が選択肢に入ります。差分が回帰だったとしても、更新したあとのテストは緑になります。更新は1コマンドで終わるため、気づくのはレビューのときです。
基準の更新だけをhookで止める
「分類は任せるが更新は止める」を、PreToolUseのhookで実装します。Bashの呼び出し前に、コマンドに--update-snapshotsか-uが含まれていれば、終了コード2でブロックします。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-update-snapshots.sh"
}
]
}
]
}
}#!/bin/bash
# .claude/hooks/block-update-snapshots.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -Eq 'playwright test.*(--update-snapshots|[[:space:]]-u([[:space:]]|$))'; then
echo "ベースライン更新は人が差分画像を確認してから実行します。差分の分類だけ報告してください。" >&2
exit 2
fi
exit 0chmod +xで実行権を付けて使います。公式のhookリファレンスでは、終了コード2は呼び出しのブロックで、標準エラー出力の文字列がClaudeへの理由として渡されます。手元で標準入力にJSONを流して確かめたところ、npx playwright test card --update-snapshotsとnpx playwright test -uは終了コード2、npx playwright test cardは0でした。
このhookに弱点があります。npm run test:e2e -- -uのように別名のスクリプトを通すと、コマンド文字列にplaywright testが出ないので止まりません。プロジェクトに固有の呼び出し名がある場合は、grepの条件にその名前を足します。hookはあくまで手元の安全網で、基準を更新するコミットはレビューを通す運用が前提です。権限ルールでBash(npx playwright test *)ごと確認を挟む選択肢もありますが、通常のテスト実行まで止まってしまいます。
CIの差分画像をClaudeの手元に持ってくる
CIで落ちた差分は、そのままではClaudeに届きません。GitHub Actionsの例では、結果ディレクトリをartifactとして保存します。公式のCIページの例はplaywright-report/を保存する形で、次はそれをtest-results/に置き換えた形です。
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-results
path: test-results/
retention-days: 7if: ${{ !cancelled() }}が肝です。テストが失敗したジョブでも、このステップが実行されます。落ちた実行のIDが分かれば、手元でGitHub CLIから取り出せます。
gh run download <run-id> --name playwright-results --dir ci-results
./triage-visual.sh ci-results/card-card<run-id>はgh run listで確かめます。ここで渡る画像は、CIのLinux上で撮られたものです。基準もLinuxで作らないと、比較は毎回「環境差」になります。
基準の作成環境をCIと揃える
公式の説明は、レンダリングがOS・バージョン・設定・ヘッドレスかどうかなどで変わるため、基準を作った環境でテストを回すよう警告しています。ブラウザ名とOSがファイル名に入るのも、この理由です。
実務上は、CIとローカルで同じDockerイメージを使うのが近道です。Playwright公式のイメージはバージョン付きのタグで配布されており、1.64.1ならmcr.microsoft.com/playwright:v1.64.1-nobleです。公式はバージョンを固定するよう勧めていて、イメージとプロジェクトのPlaywrightのバージョンが合わないとブラウザの実行ファイルが見つからなくなります。
jobs:
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.64.1-nobleローカルでの基準更新も、同じイメージの中で行います。docker run --rm --init --ipc=host -v "$PWD":/work -w /work mcr.microsoft.com/playwright:v1.64.1-noble npx playwright test --update-snapshotsの形です。--ipc=hostと--initは、公式が推奨するDocker構成に含まれています。
誤検知を減らす設定は、数字で理由を残す
動的な内容の揺れが続くときは、比較のほうを調整します。公式が挙げる手段は次の4つです。
maxDiffPixels: 許容する異なるピクセル数(未設定が既定)threshold: 同じ位置の色差をどこまで同じとみなすか。0(厳格)から1(緩い)で、既定は0.2stylePath: 撮影時だけ適用するCSS。iframe { visibility: hidden; }のように揺れる要素を隠せる- ホバー: スクリーンショットはホバー効果も写すので、撮影前に
await page.mouse.move(-1, -1);でマウスを外す
先ほどのテストでmaxDiffPixelsを試すと(Playwright 1.64.1)、差分は2842ピクセルなので、100にすれば失敗し、3000にすれば通りました。この仕組みの怖さがここにあります。許容値を上げれば、同じ画面の崩れも通ってしまうのです。
そこで、閾値を変える変更は次の条件にします。
- 差分画像で揺れている領域を特定し、動的な要素が理由だと分かっている
- まず
stylePathで要素を隠せないか試した - 許容値は、観測した差分ピクセル数を少し上回る程度にとどめ、理由をコメントに書く
Claudeに閾値を調整させるなら、「差分の領域と、設定を緩める理由を書いてから変更する」を条件にします。数字だけを上げて緑にする変更は、基準の更新と同じ危険を持ちます。
画面とテスト、どちらの話かの切り分け
スクリーンショット比較が落ちたとき、原因は画面側とテスト側に分かれます。
| 状況 | 見る場所 |
|---|---|
| 画像が撮れて、差分が出た | 見る場所本記事の流れ(3枚と呼び出しログ) |
| 画像が撮れる前にタイムアウトした | 見る場所待機とセレクタ。traceを読む |
| 同じコミットで、実行のたびに差分が出たり消えたりする | 見る場所動的な内容か、環境の揺れ |
| テストの自動実行そのものを組み込みたい | 見る場所PlaywrightのE2Eテストをhookで自動実行する |
差分が小さくても、アクセシビリティに関わる変更(コントラスト・フォーカスの見た目)が含まれていることがあります。その種の検査は、画像比較ではなくaxeのアクセシビリティ検査のほうが確実に拾います。手順書に貼る画像を台本から撮り直す側の話は、手順書のスクリーンショットを撮り直す手順にあります。
まとめ
差分画像は「どこが変わったか」を、呼び出しログの差分ピクセル数は「どれだけ変わったか」を教えてくれます。Claudeに頼むのは「どれだけ、どこが、なぜ」の整理までで、基準の更新と閾値の変更は人の確認を挟む。この線引きをhookで機械的に引いておくと、緑のテストが崩れた画面を守る事態を避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。