前回のふりかえり
以前「AIエージェントを並列で走らせてみたら、ボトルネックは自分だった話」という記事を書きました。あの記事の結論は、エージェントを何個並列で走らせても、最終的に確認するのは自分1人で、並列数の上限はAIの性能ではなく自分のレビュー速度で決まる、というものでした。
今回はその「確認」という工程を、もう少し楽にできないか考えてみた話です。
きっかけ
普段の業務で、既存コードの共通化や整理といったリファクタ案件を持つことが増えてきました。この手の案件で一番気を遣うのは、「機能面は問題なく動くけど、見た目や表示内容が本番と変わってしまっていないか」という確認です。
対象ページを本番環境とテスト環境で開いて、目でパッと見比べる。数ページならまだいいですが、対象ページ数が多い案件や、共通ファイルを触っていて影響範囲が広い案件だと、この目視確認がそれなりに時間を取ります。しかも人間の目は集中力が切れると意外と見逃します。
というわけで、確認作業を2本のCLIツールに分担させることにしました。
1本目:表示内容(テキスト・数値)の差分を検出する
1本目は、本番環境とテスト環境で同じページを開いて、表示されているテキストや数値の差分を検出するツールです。もともとは料金表示の整理案件で「リファクタ前後で表示金額が変わっていないか」を確認するために作りました。
やることは単純です。
- 本番URLとテスト/ローカルURLを渡す
- Playwrightでヘッドレスブラウザから両方のページを実際に開く(JSで描画される価格にも対応)
- 表示テキストをLCS差分で突き合わせて、変更・追加・削除を検出する
素朴にHTMLの文字列を取得して比較するだけだと、JSで後から差し込まれる価格表示を取りこぼします。なので、実際にヘッドレスブラウザで開いて描画が終わった状態のDOMからテキストを抜き出すようにしています。日付や広告のような、そもそも表示内容が変わって当然の要素は除外セレクタで弾き、全角/半角や円記号の表記揺れも正規化してから比較することで、表示上のブレによる誤検知を減らしています。
差分の検出には、行ごとの単純比較ではなく最長共通部分列(LCS)を使っています。単純な行単位の比較では、途中に要素が追加されたり削除されたりしただけでも、その後の内容がすべて差分として扱われてしまいます。
そこで、共通している部分を見つけながら差分を取れるLCSを使っています。さらに保険として、テキスト内の数値だけを集合として抜き出し、順序や重複に依存しない突き合わせも別で行っています。見出しの並び順が変わっても、金額が消えた・増えたという変化そのものは取りこぼさないようにする仕組みです。
本番URLを1つ渡すだけで実行できるようにしていて、テスト環境URLの命名規則(ホスト名末尾に`.test`を付ける形)に合わせて、テストURLも自動で導出してくれます。差分があればHTMLレポートで「本番: 3,980円 → テスト: 4,980円」のように具体的に教えてくれます。数値だけに絞るモードもあるので、料金整理の案件ではノイズを最小限にして確認できます。

実際のレポート例。本番とテストで値が違う箇所を具体的に教えてくれる
2本目:見た目(レイアウト・色味)の差分を検出する
もう1本は、見た目の差分を検出するツールです。1本目はテキスト・数値の「内容」を見ていますが、リファクタ案件で本当に怖いのは、文言や数値は変わっていないのにレイアウトが崩れているパターンです。余白がズレた、色が変わった、要素が重なった、といった「見た目」の崩れはテキスト比較では拾えません。
そこで、本番とテスト/ローカル環境のスクリーンショットを撮って、画素単位で比較するツールを作りました。構成はこんな感じです。
- `playwright-core`でChrome(導入済みのものを使うのでブラウザの追加ダウンロード不要)を操作してフルページスクショを取得
- `pixelmatch`で2枚の画像を画素単位で比較し、差分のあった箇所を赤くハイライトしたPNGを生成
- 差分画素の比率が閾値を超えたらNG判定
スクショを撮る前に、CSSを注入して2つの前処理をしています。1つはアニメーション・トランジションの無効化です。スクショのタイミングでアニメーションの途中の状態を切り取ってしまうと、それだけで差分扱いになってしまうためです。もう1つは、日付や広告のような表示内容が変わって当然の要素を`visibility:hidden`で隠す処理です。`display:none`で消してしまうと、その分レイアウトが詰まってそれ自体が差分として検出されてしまうので、スペースは残したまま見えなくする方式にしています。
画像比較は`pngjs`でPNGをデコードしてから`pixelmatch`に渡す形です。本番とテストでスクショの縦幅が違う場合(コンテンツの有無などで発生します)は、大きい方に合わせて白背景でパディングしてから比較し、サイズが違っていたこと自体もレポート上に明記するようにしています。
対象サイトはアコーディオンで開閉するUIが多いので、「初期状態(閉じた状態)しか見ていないと、開いた中の崩れに気付けない」という問題がありました。これに対応するため、設定ファイルに「クリックする要素のセレクタ」を書いておくと、スクショを撮る前に自動で全部クリックしてから撮影する仕組みを入れています。サイトによってアコーディオンの有無やクラス名がバラバラなので、該当セレクタがそのページに存在しない場合はエラーにせずスキップするようにしています。

実際のレポート例。色や余白が変わった箇所が赤くハイライトされる
なぜ2本に分けたのか
「内容の差分」と「見た目の差分」は、検出の原理がそもそも違います。1本目はDOMからテキストを抜き出して文字列として比較するので、「どこがどう変わったか」を具体的に教えてくれますが、レイアウトのズレそのものは分かりません。2本目は画素として比較するので、レイアウト崩れは拾えますが、「何が変わったか」までは教えてくれず、差分画像を見て人間が判断する必要があります。
もう一段掘って考えると、これは人間が目視で確認するときにやっている2種類の確認を、そのままツールに分けただけです。
内容の確認(Content Diff)
- 「3,980円 → 4,980円になってない?」
- 「文言が消えてない?」
- 「数字が変わってない?」
見た目の確認(Visual Diff)
- 「余白が変わってない?」
- 「色が変わってない?」
- 「レイアウトが崩れてない?」
目視で本番とテストを見比べているとき、自分の中でも無意識にこの2種類の問いを切り替えながら確認していたんだと思います。それを、それぞれ専用のツールに分担させただけ、というのが実際のところです。
1本にまとめて全部やろうとすると中途半端になりそうだったので、役割を分けて、案件の性質に応じて使い分ける形にしました。
実際の運用フロー
リファクタ案件では、実装が終わった段階でこの2本を両方回すようにしています。
- テキスト・数値の変更を伴わないはずのリファクタ → 見た目チェックのツールをメインで実行
- 料金表示など数値を扱うコード整理 → 内容チェックのツールも必ず追加
複数ページが対象の案件では、対象URLを1行1URLのテキストファイルにまとめて一括で実行できるようにしているので、対象ページが多い案件でも実行自体はコマンド1つで済みます。
ちなみに、実行のたびにコマンドを打つのも地味に面倒だったので、複数URLをまとめてブラウザから実行できる小さなローカルサーバーも作りました。macOSのLaunchAgentでログイン時に自動起動する設定にしておいたので、あとはブラウザのブックマークをワンタップするだけで使えます。
やってみて感じたこと
導入してみて、目視確認にかけていた時間はかなり減りました。ただ、万能というわけではなく、いくつか割り切っている部分があります。
- ホバーでしか出ない要素や、クリック以外のインタラクションで変化する表示は対象外
- 意図的に表示内容が違う案件(テスト環境だけキャンペーン内容が違う、など)は、差分が出て当然なので、結局レポートを見て人間が判断する
- コンテンツの有無で高さが変わると、それより下の要素が全部ズレて差分扱いになる。「差分率が大きい=崩れている」と早合点せず、差分画像の中身は見る必要がある
なので、「確認を完全に自動化した」というよりは、「確認の一次スクリーニングをツールに任せて、人間は差分レポートを見るだけで済むようにした」というのが実感に近いです。それでも、目視で全ページ見比べる作業からは解放されました。
まとめ
- 内容チェック用のツール: 本番/テスト環境のテキスト・数値の内容差分を検出するCLI
- 見た目チェック用のツール: 本番/テスト環境のスクリーンショットを画素単位で比較し、見た目の崩れを検出するCLI
- 検出原理が違うので役割を分け、案件の性質に応じて使い分けている
AIエージェントを使えば実装速度は上がりますが、増やせば増やすほど、「作る」ことより「確認する」ことの方がボトルネックになっていきます。前回の記事で書いた「確認速度が並列開発のボトルネックになる」という話は、結局そこに行き着きます。今回やったのは、その確認工程の一部を、自分の目からさらに機械に移したという話でした。それでは、また次の記事で。