Claude Media
Claude CodeでPlaywrightのE2Eテストをhookで自動実行する

Claude CodeでPlaywrightのE2Eテストをhookで自動実行する

PlaywrightのE2EスイートをPostToolUse hookとStop hookで自動実行し、失敗をClaude Codeに検知させて直させる設定手順とつまずきどころです。

Claude CodeにPlaywrightのE2Eスイートを組み込むと、編集のたびに手でテストを走らせて結果をコピペで報告する作業がなくなります。hookがファイル変更を検知してテストを自動実行し、失敗内容をClaude自身の文脈に返します。あとはClaudeがその内容を読んで修正案を出す、という流れが1セッションの中で完結します。

この記事はPlaywrightをテストフレームワークとして使う前提で、hookによる自動実行と失敗の押し戻しに絞って扱います。ブラウザ操作をテストコードなしで自然文の指示から動かしたい場合はPlaywright MCPサーバーの使い方が別の入口です。hookの仕組み全体はClaude Code Hooks完全ガイドにまとめているので、本記事ではE2E自動実行の実装だけに集中します。

前提条件

以下がそろっている状態を前提にします。

  • Node.jsの最新22.x・24.x・26.xいずれか(Playwright公式のシステム要件)
  • Claude Codeのプロジェクトディレクトリ(.claude/hooks/にスクリプトを置く)
  • jqコマンド(hookスクリプトでJSON入力をパースするため)

hookは.claude/settings.jsonに書いたその場では動きません。インタラクティブセッションでは、そのプロジェクトフォルダに対するワークスペーストラストのダイアログを一度承認するまで、ユーザー設定・プロジェクト設定を問わずすべてのhookが保留されます。claude -pのようなヘッドレス実行ではこのダイアログ自体が出ず、リポジトリに.claude/settings.jsonを含めていればコミットされたhookがそのまま動きます。クラウド上のセッションはローカルの~/.claude/settings.jsonを読まず、リポジトリ側の設定だけを見る点も、CIで同じ構成を再現するときに押さえておく価値があります。

hookスクリプトはClaude Codeを動かしているユーザーの権限でそのままシェルコマンドを実行します。tool_inputから取り出したFILE_PATHのような値は、必ずダブルクォートで囲んで展開し("$FILE_PATH"であって$FILE_PATHではない)、パスに..が含まれていないかを確認してから使います。信頼していないリポジトリをclaude -pで開くときは、先に.claude/配下の設定を目視するか、--settings '{"disableAllHooks": true}'で一時的にhookを無効化してから中身を確認するほうが安全です。

Playwrightをプロジェクトに導入する

まだPlaywrightを入れていない場合は、公式のセットアップコマンドで一括導入します。

npm init playwright@latest

TypeScript / JavaScriptの選択、テストフォルダ名(既定はtests、既にtestsがあればe2e)、GitHub Actionsワークフローの追加、ブラウザバイナリのインストールをプロンプトで聞かれます。完了するとplaywright.config.tsとテストフォルダ配下のexample.spec.tsが生成されます。既存プロジェクトに追加した場合は、依存関係が今のpackage.jsonにそのまま追記されます。

動作確認は次の2つのコマンドで足ります。

npx playwright test
npx playwright show-report

playwright testは既定でChromium・Firefox・WebKitをヘッドレス・並列で実行し、show-reportは失敗・成功・スキップ・flakyを一覧できるHTMLレポートを開きます。ここまでが動けば、hookから同じコマンドを呼ぶ準備ができています。

編集のたびにE2Eを自動実行するhookを作る

まずは「編集を検知したら裏でテストを回し、結果をClaudeに戻す」構成です。PostToolUseイベントにasync: trueを付けると、テストの実行を待たずにClaudeは作業を続けられます。結果はテストが終わり次第、次の会話ターンでadditionalContextとして渡されます。

mkdir -p .claude/hooks
cat > .claude/hooks/run-e2e-async.sh <<'SH'
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
 
# テスト対象外の拡張子は素通りさせる
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.tsx ]]; then
  exit 0
fi
 
RESULT=$(npx playwright test 2>&1)
EXIT_CODE=$?
 
if [ $EXIT_CODE -eq 0 ]; then
  MSG="E2Eテストは全て成功しました($FILE_PATH の変更後)"
else
  MSG="E2Eテストが失敗しました($FILE_PATH の変更後): $(echo "$RESULT" | tail -n 30)"
fi
 
jq -nc --arg msg "$MSG" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $msg}}'
SH
chmod +x .claude/hooks/run-e2e-async.sh

.claude/settings.json側は次のとおりです。matcherEdit|Writeに絞ることで、Read・Grepのような読み取り系ツールではテストが走らないようにします。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-e2e-async.sh",
            "args": [],
            "async": true
          }
        ]
      }
    ]
  }
}

このままだと、テストの結果はClaudeが次のターンを迎えるまで届きません。数十秒で終わる小さいスイートなら気になりませんが、フルのE2Eスイートが数分かかる場合、失敗に気づくのが遅れます。ここでasync: trueの代わりにasyncRewake: trueを使うと、バックグラウンド実行のまま、終了コード2でClaudeを即座に呼び戻せます。標準エラー出力(空なら標準出力)がシステムリマインダーとしてClaudeに渡されるので、テストが失敗した瞬間だけ割り込みが入り、成功時は静かに終わります。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-e2e-async.sh",
            "args": [],
            "asyncRewake": true
          }
        ]
      }
    ]
  }
}

スクリプト側は失敗時にexit 2へ変えるだけで流用できます。

テストが通るまでClaude Codeを終了させないhook

「編集後にE2Eを自動実行し、失敗をClaude Codeに修正させる」運用を強制したい場合、PostToolUseの非同期通知だけでは足りません。PostToolUseはツールがすでに実行された後に発火するイベントで、テストが失敗していてもツール呼び出しそのものを止める力を持たないからです。ここで効くのがStop hookです。Claudeがターンを終えようとするたびに同期で発火し、テストが失敗していればexit 2でターンの終了そのものをブロックできます。

cat > .claude/hooks/e2e-gate.sh <<'SH'
#!/bin/bash
RESULT=$(npx playwright test 2>&1)
EXIT_CODE=$?
 
if [ $EXIT_CODE -ne 0 ]; then
  echo "E2Eテストが失敗しています。修正してから終了してください。" >&2
  echo "$RESULT" | tail -n 40 >&2
  exit 2
fi
 
exit 0
SH
chmod +x .claude/hooks/e2e-gate.sh
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/e2e-gate.sh",
            "args": [],
            "timeout": 900
          }
        ]
      }
    ]
  }
}

exit 2で渡した標準エラー出力は、Claudeが継続すべき理由としてそのまま会話に読み込まれます。JSON出力で明示するなら{"decision": "block", "reason": "..."}と等価です。Stop hookのイベント自体にはPostToolUseのようなmatcherは不要で、テスト対象ツールを問わずターン終了のたびに発火します。テストの実行時間が長いプロジェクトでは、コマンドhookの既定タイムアウト(600秒)を超えないようtimeoutを明示しておくと安全です。

フルのE2Eスイートを毎回ChromiumとFirefoxとWebKitの3ブラウザで回すと、Stop hookのたびに待ち時間が積み上がります。npx playwright test --project=chromiumのようにブラウザを1つに絞る、あるいは変更されたファイルに対応するspecだけをnpx playwright test tests/checkout.spec.tsのように指定すると、gateとして使うhookの体感速度が変わります。全ブラウザでの網羅的な実行はCIの別ジョブに任せ、hook側は編集直後のすばやいフィードバックに徹する、という役割分担が実務的です。

hookの使い分け早見表

方式イベントClaudeを待たせるか向く用途
同期PostToolUseイベントPostToolUseClaudeを待たせるか待たせる(テスト完了まで)向く用途小さいスイートで即結果が欲しいとき
非同期PostToolUse(async)イベントPostToolUseClaudeを待たせるか待たせない、結果は次ターン向く用途作業を止めずにバックグラウンドで検証したいとき
asyncRewakeイベントPostToolUseClaudeを待たせるか待たせない、失敗時だけ割り込み向く用途長時間スイートを裏で回しつつ失敗だけ即通知したいとき
Stop(同期)イベントStopClaudeを待たせるか終了そのものをブロック向く用途「テストが通るまで終わらせない」運用の本命

PostToolUseはどの構成でもツール呼び出しそのものは止められません。テストがすでに実行された後に発火するイベントだからです。編集を止めずに検証だけ挟みたいなら非同期系、修正を最後まで詰めさせたいならStop hookという住み分けになります。

Claudeが何度修正してもテストが落ちる場合は、いったんhookの外に出て人間側で状況を見るのが早い場合もあります。npx playwright test --uiを手元で実行すると、ウォッチモード・ステップごとの実行過程・タイムトラベルでの巻き戻しが使えるUIモードが開き、hookのログだけでは追いにくい失敗原因を目視で追えます。

よくあるつまずき

matcherを絞らずCPUを無駄に使う

matcherを空や"*"のままにすると、Read・Glob・Bashなどファイル編集と無関係なツール呼び出しのたびにE2Eスイートが起動します。Edit|Writeに絞り、さらにスクリプト側で拡張子や対象ディレクトリを見て早期exit 0するのが安全です。

stop_hook_activeを見落として無限にブロックし続ける

Stop hookの入力にはstop_hook_activeという真偽値が含まれ、直前のStop hookの結果としてすでに会話が継続中かどうかを示します。修正が入らない条件を無条件にブロックし続けると、Claude Codeは8回連続のブロックでターンを強制終了しますが、そこまでの間はテストが失敗し続けたままリトライを繰り返すことになります。テストの前提条件自体が壊れている可能性がある場合は、hookスクリプト側にも打ち切り条件を持たせておくと安全です。

CI・コンテナでブラウザバイナリが無く落ちる

ヘッドレス実行用のブラウザバイナリが入っていないと、playwright testはhookの中でエラー終了します。事前にnpx playwright install --with-depsを実行して、Chromium・Firefox・WebKitと依存パッケージを揃えておきます。

非対話セッションでasyncフックが打ち切られる

claude -pのような非対話モードでは、セッション終了時に実行中の非同期hookが強制終了され、結果はcancelled扱いになります。CIジョブの中でセッションが短命な場合、フルのE2Eスイートを裏で完走させたいなら、hookスクリプトの中から完全に独立したプロセスとして起動する必要があります。

Playwright本体とブラウザバイナリのバージョンがずれる

@playwright/testパッケージだけを更新してブラウザバイナリを更新し忘れると、hookの中のplaywright testが起動時エラーで落ちて、テストの中身とは無関係な失敗をClaudeに報告し続けます。パッケージを更新したらnpm install -D @playwright/test@latestのあとにnpx playwright install --with-depsも忘れずに実行し、npx playwright --versionで両者のバージョンを揃えておきます。

まとめ

Playwrightのテストスイートをhookに組み込むと、編集→テスト→修正のループを手作業なしで回せます。まずはPostToolUseasync: trueを付けて動かし、結果を待つタイムラグが気になるならasyncRewakeで失敗時だけ即時通知に切り替えます。「テストが通るまで終わらせない」運用まで踏み込みたい場合は、Stop hookでexit 2によるブロックを使うのが最短です。ファイル編集直後の後処理全般はPostToolUse hookでツール実行後の後処理を自動化するにまとめています。モノレポでテスト戦略自体をSKILL.mdでClaudeに教える構成はClaude Codeモノレポのテスト戦略をSKILL.mdで教える手順が参考になります。

この記事を共有:XはてブLinkedIn