CIパイプラインステージとしてのビジュアルQA
先月PRをマージした。コードレビューは問題なかった。テストも通った。それから携帯電話でサイトを開くと、サイドバーが完全に壊れていた。
修正は些細なものだった—メディアクエリの欠落。モバイルビューを実際に見れば、バグは明らかだった。誰も見なかった。
だから見るパイプラインステージを追加した。
GitHubイシューを開いて、こう書く:
クライアントを追加するための手動入力オプションを実装。クリックすると、新しいクライアントを作成するフォームを含む幅広いスライドオーバードロワーが開く。
1つのコミット後、すべてのビューポートで各状態が機能することを証明する30枚以上のスクリーンショットとともにPRが届く。手動テストはゼロ。唯一の労力は機能説明を書くことだった。
コンセプト
PRへのプッシュごとに、GitHub Actionsワークフローが:
- PRブランチからDocker Composeスタック(Django + Postgres)を起動
- マイグレーションを実行し、テストユーザーをシード
- ローカルURLをPlaywright MCP(ヘッドレスChromium)を使用したClaude Codeに渡す
- PR diffに基づいて対話要素を実行
- 3つのビューポートで各状態をスクリーンショット
- 結果をPRコメントとして投稿
Claude Codeがブラウザを制御する。Docker Composeがアプリを提供する。GitHub Actionsがそれらを結びつける。レビュアーはローカルで何も実行せずに機能の証明を見る。
%%{init: {"flowchart": {"subGraphTitleMargin": {"top": 3, "bottom": 10}}} }%%
graph TD
A["PRプッシュ"] --> B
subgraph GHA["GitHub Actions Runner"]
B["Checkout + Docker Compose"] --> C["マイグレーション + シードデータ"]
C --> Agent
subgraph Agent["Claude Code + Playwright"]
direction LR
D["PR Diffを読む"] --> E["ファイル → URLをマップ"]
E --> F["ログイン + ナビゲート"]
F --> G["スクリーンショット ×3ビューポート"]
end
end
Agent --> H["PRコメント"]
style GHA fill:transparent,stroke:#888
style Agent fill:transparent,stroke:#888
ワークフロー自体は単純:
name: UI Screenshots
on:
pull_request:
types: [opened, synchronize]
jobs:
screenshots:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
# ... Docker Composeセットアップ、マイグレーション、テストユーザー作成 ...
- name: Start services
run: |
docker compose -f docker-compose.local.yml up -d --wait
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run Claude for Screenshots
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: >-
--allowed-tools
"mcp__playwright__*,
Bash(gh pr diff:*),
Bash(gh pr comment:*),
Read,Glob,Grep"
prompt: |
You are a visual QA agent. Your job is to visually
verify a UI deployment.
PR: ${{ github.event.pull_request.number }}
The Django app is running at http://localhost:8001
1. Run `gh pr diff` to find changed templates/views
2. Map changed files to URLs — only test pages
affected by this push
3. Login, then screenshot each page at three viewports:
1440x900, 1024x768, 640x1136
4. Save screenshots with descriptive filenames
5. Post a PR comment with:
- Each test performed with pass/fail
- Any visual issues found
- Total screenshots captured
ビューポートマトリックス
| デバイス | サイズ | 理由 |
|---|---|---|
| デスクトップ | 1440×900 | 標準ラップトップ |
| タブレット | 1024×768 | iPadポートレート |
| モバイル | 640×1136 | iPhone SE |
すべての対話状態が3つすべてでキャプチャされる。サイドバーコンポーネントは12枚以上のスクリーンショットを生成する:
- ページ読み込み(閉じた状態) — 3ビューポート
- トリガークリック — 3ビューポート
- 完全に開いた状態 — 3ビューポート
- クローズアニメーション — 3ビューポート
ライト/ダークテーマを掛け合わせると、1つのコンポーネントで24枚のスクリーンショットになる。それがポイント。CIアーティファクトとしての徹底的なビジュアル証明。
実際の出力
手動入力ページを追加したPRからの実際の出力がこれ。エージェントは構造化された要約、各ビューポートでのスクリーンショット、テストチェックリストを含むPRコメントを投稿する:

その要約の下に、エージェントは実行した各状態のスクリーンショットを追加する。これがデスクトップビュー—データを含むクライアントリストと、開いているクライアント追加ドロワー:

エージェントがテストした内容:
- 空のフォーム状態
- 検証エラーのあるフォーム(必須の名前フィールドが欠落)
- フォーム送信の成功
- データを含むクライアントリストのレンダリング
- 画像がない場合のアバターフォールバック
- モーダルの開閉トランジション
- 3つすべてのビューポート
このPRでは個別のフィールドフォーカス状態をスキップした—シンプルなフォームでは収穫逓減。エージェントはできるが、テキスト入力がフォーカスを受け取る50枚のスクリーンショットは有用なシグナルではない。
PRコメントはビジュアルチェンジログになる。各テスト実行はチェックリストで終わる:

6か月後、任意のPRを見て、出荷時にUIがどのように見えたかを正確に確認できる。
テストマニフェスト
上記のワークフローのプロンプトは汎用的 — どのPRにも機能する。特定性はdiff自体から来る。エージェントはgh pr diffを読み、どのテンプレートとビューが変更されたかを把握し、それらをURLにマップし、何をテストするかを決定する。
フォームコンポーネントの場合、これは次のことを実行することを意味する:
component: ClientForm
states:
- empty form
- filled form (valid data)
- validation error (submit without name)
- submitting (loading state)
- success (confirmation)
interactions:
- submit empty form (trigger validation)
- fill all fields, submit
- verify client appears in list after submit
手動テストプランは不要。エージェントはコード変更からテストサーフェスを推測する。
PRコメントにスクリーンショットを入れる理由
PRコメント内のインラインスクリーンショットにはいくつかの利点がある:
- 可視性 — レビュアーはアーティファクトをクリックせずに見られる
- コンテキスト性 — レビューしているdiffのすぐ隣にある
- 比較可能性 — 各プッシュがコメントを更新するので、前後が見られる
- 監査可能性 — コメント履歴が各コミットで何がテストされたかを示す
これが捕捉するもの
最初の週に捕捉された実際のバグ:
overflow: hiddenの親によってクリップされたドロップダウン(タブレットでのみ表示)- ダークモードで見えないボタンテキスト(間違ったCSS変数)
- iPhoneでずれたフォームラベル(flexbox gapの問題)
- タッチ後に固定されたホバー状態(
@media (hover: hover)が必要) - ランドスケープタブレットでビューポート全体をカバーしないモーダルバックドロップ
これらすべてがコードレビューを通過した。すべてがスクリーンショットで明らかだった。
コスト
CIでPlaywright MCPを使用してClaudeを実行するには、テストする必要がある状態の数に応じて2〜4分かかる。1つのコンポーネントに触れる典型的なPRの場合、約90秒。
比較対象: 本番環境にデプロイし、モバイルレイアウトが壊れていることをユーザーから知らされる。プライスレス。
シフト
ビジュアルQAは常にボトルネックだった。ユニットテスト、統合テスト、さらにはエンドツーエンドフローさえ自動化できる — しかし誰かがまだUIを見なければならない。それは何十年も真実だった。
もはや真実ではない。ブラウザを持つエージェントはスクリプトを実行するだけではない。解釈し、ナビゲートし、対話し、判断する。テストサーフェスはハードコードされていない — 変更から推測される。各PRは、手動プロセスでは決して一致できない徹底的なビジュアルカバレッジを得る。
これはQEをゲートから保証に変える。「誰かがチェックしたか」ではなく「パイプラインがチェックした、これが証明だ」。すべてのPR、すべてのプッシュ、すべてのビューポート。QEの役割は消えない。上流に移動する。テストプランを実行する代わりに、エージェントが何を気にすべきかを定義している。バグをキャッチする代わりに、それらをキャッチするシステムを設計している。