CIパイプラインステージとしてのビジュアルQA

先月PRをマージした。コードレビューは問題なかった。テストも通った。それから携帯電話でサイトを開くと、サイドバーが完全に壊れていた。

修正は些細なものだった—メディアクエリの欠落。モバイルビューを実際に見れば、バグは明らかだった。誰も見なかった。

だから見るパイプラインステージを追加した。

GitHubイシューを開いて、こう書く:

クライアントを追加するための手動入力オプションを実装。クリックすると、新しいクライアントを作成するフォームを含む幅広いスライドオーバードロワーが開く。

1つのコミット後、すべてのビューポートで各状態が機能することを証明する30枚以上のスクリーンショットとともにPRが届く。手動テストはゼロ。唯一の労力は機能説明を書くことだった。

コンセプト

PRへのプッシュごとに、GitHub Actionsワークフローが:

  1. PRブランチからDocker Composeスタック(Django + Postgres)を起動
  2. マイグレーションを実行し、テストユーザーをシード
  3. ローカルURLをPlaywright MCP(ヘッドレスChromium)を使用したClaude Codeに渡す
  4. PR diffに基づいて対話要素を実行
  5. 3つのビューポートで各状態をスクリーンショット
  6. 結果を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×768iPadポートレート
モバイル640×1136iPhone SE

すべての対話状態が3つすべてでキャプチャされる。サイドバーコンポーネントは12枚以上のスクリーンショットを生成する:

ライト/ダークテーマを掛け合わせると、1つのコンポーネントで24枚のスクリーンショットになる。それがポイント。CIアーティファクトとしての徹底的なビジュアル証明。

実際の出力

手動入力ページを追加したPRからの実際の出力がこれ。エージェントは構造化された要約、各ビューポートでのスクリーンショット、テストチェックリストを含むPRコメントを投稿する:

ビジュアルQAエージェントによって作成されたGitHubイシュー

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

デスクトップビュー: 1440x900でのクライアントリストとクライアント追加ドロワー

エージェントがテストした内容:

このPRでは個別のフィールドフォーカス状態をスキップした—シンプルなフォームでは収穫逓減。エージェントはできるが、テキスト入力がフォーカスを受け取る50枚のスクリーンショットは有用なシグナルではない。

PRコメントはビジュアルチェンジログになる。各テスト実行はチェックリストで終わる:

すべての項目が合格したTesting Performedチェックリスト

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コメント内のインラインスクリーンショットにはいくつかの利点がある:

  1. 可視性 — レビュアーはアーティファクトをクリックせずに見られる
  2. コンテキスト性 — レビューしているdiffのすぐ隣にある
  3. 比較可能性 — 各プッシュがコメントを更新するので、前後が見られる
  4. 監査可能性 — コメント履歴が各コミットで何がテストされたかを示す

これが捕捉するもの

最初の週に捕捉された実際のバグ:

これらすべてがコードレビューを通過した。すべてがスクリーンショットで明らかだった。

コスト

CIでPlaywright MCPを使用してClaudeを実行するには、テストする必要がある状態の数に応じて2〜4分かかる。1つのコンポーネントに触れる典型的なPRの場合、約90秒。

比較対象: 本番環境にデプロイし、モバイルレイアウトが壊れていることをユーザーから知らされる。プライスレス。

シフト

ビジュアルQAは常にボトルネックだった。ユニットテスト、統合テスト、さらにはエンドツーエンドフローさえ自動化できる — しかし誰かがまだUIを見なければならない。それは何十年も真実だった。

もはや真実ではない。ブラウザを持つエージェントはスクリプトを実行するだけではない。解釈し、ナビゲートし、対話し、判断する。テストサーフェスはハードコードされていない — 変更から推測される。各PRは、手動プロセスでは決して一致できない徹底的なビジュアルカバレッジを得る。

これはQEをゲートから保証に変える。「誰かがチェックしたか」ではなく「パイプラインがチェックした、これが証明だ」。すべてのPR、すべてのプッシュ、すべてのビューポート。QEの役割は消えない。上流に移動する。テストプランを実行する代わりに、エージェントが何を気にすべきかを定義している。バグをキャッチする代わりに、それらをキャッチするシステムを設計している。