こんにちは!IVRyのQAエンジニアの関(@IvryQa)です。
IVRyでは2024年12月から Playwright を用いた Visual Regression Testing(以下、VRT) を導入し、約半年間運用してきました。
ピクセル差分は拾えても これって本当にバグなのか? と判断するのに時間がかかったり、マスク設定やしきい値調整など地味なメンテ工数が積み重なっていました。
そこで今回、画像を理解できる生成AIを使って差分をバグ報告として自然言語化し、Slackに通知する仕組みを試してみました。
同じような悩みを抱えている方の参考になれば嬉しいです!
概要
現状の VRT の仕組みでは、1pxのズレも検知してしまい、本当のバグかどうか人間が判断する必要がありました。また、マスク設定やしきい値調整もやや手間があり、運用コストが高いのが課題としてありました。
そこで、Playwrightで取得した画像ペアを Gemini 2.5 FlashやGPT-4o mini に投入し、差分を自動でERROR/WARN分類してSlack通知する仕組みを構築しました。
実際の検証では、ブランドカラー変更、ローディング残存、レイアウト崩れなどを高精度で検知でき、差分がバグ報告として自然言語で説明されるため、判断が圧倒的に楽になりました。
本記事では、この仕組みの実装内容やプロンプト、複数のモデルを使ってみての検証結果や良かったことなどをご紹介します。
VRT を導入してみての課題感
Playwright でページ全体のスクリーンショットを撮り、ベース画像との差分を比較する仕組みでUI崩れの多くは検知することができます。ただ、運用を続けるうちに差分は拾えても どこがどう変わったのか が直感的に分かりづらかったり、意図的な変更まで毎回通知されたり、日付や記事追加のように動的に変わる要素自体のメンテナンスをするなど辛いポイントがいくつか見えてきました。
▼ 差分を見ても直感的にバグなのかが分かりづらい
例えば、上記の画像で検出された差分は、実際にはヘッダーロゴのリニューアルによるものでした。しかし、現状のVRTでは差分の検知はできても「具体的に何が変わったのか」「それが修正すべき問題なのか」までは分からないため、結局は人間が元画像と比較して「ロゴが変わったのか」と原因を特定する作業が発生します。
「ヘッダーのロゴデザインが更新され、それに伴いヘッダー高さが20px増加しています」といった具体的な説明があれば瞬時に意図的な変更だと判断できるのですが、現状では目視での詳細確認が必要で、この判断工程に時間がかかってしまいます。
▼ ハイライト表示はあるが、多数の差分から問題を特定するのが難しい
このように赤いハイライトが画面全体に散らばっていると、パッと見ただけでは「え、何がダメなの?」という状況になってしまいます。重要な問題なのか、単なる文字の位置ずれなのか、それとも意図的なデザイン変更なのかが直感的にわからず、結局一個ずつ目視で確認する必要があります。
これらの課題を解決するため、生成AIを使って差分画像を分析してバグ報告する仕組みを試してみました。
1. 生成AIを使って差分画像を分析してバグ報告してみる
1.1 生成AIを使った差分画像を分析してバグ報告する仕組み
大枠の流れは下記になります。
具体的な実装例
◼︎ スクリーンショットの撮影から生成AIで差分分析する
async function runVisualTest(useV2 = false): Promisevoid> {
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext();
const page = await context.newPage();
const configs = useV2 ? V2_TEST_CONFIGS : TEST_CONFIGS;
for (const config of configs) {
const baselinePath = `baseline/${config.pageId}.png`;
const baselineExists = existsSync(baselinePath);
if (!baselineExists) {
await captureScreenshot(page, {
pageId: config.pageId,
url: config.baselineUrl,
viewport: config.viewport,
fullPage: config.fullPage ?? false,
}, 'baseline');
}
await captureScreenshot(page, {
pageId: config.pageId,
url: config.currentUrl,
viewport: config.viewport,
fullPage: config.fullPage ?? false,
}, 'screenshots');
const findings = await judgeVisualDiff(config.pageId);
await saveReport(config.pageId, findings);
}
} finally {
await browser.close();
}
}
◼︎ 画像差分分析
OpenAI、Google(Gemini)、Amazon Bedrock などのプロバイダーで試せるように
実装しましたが、ここではOpenAIの実装部分を抜粋します。
export class OpenAIProvider implements VisionProvider {
private client: OpenAI;
constructor(apiKey: string) {
this.client = new OpenAI({ apiKey });
}
async analyzeDiff(baseline: string, current: string): Promise{ findings: DiffFinding[] }> {
const messages = [
{
role: 'system' as const,
content: SYSTEM_PROMPT,
},
{
role: 'user' as const,
content: [
{ type: 'text' as const, text: USER_PROMPT },
{ type: 'image_url' as const, image_url: { url: `data:image/png;base64,${baseline}` } },
{ type: 'image_url' as const, image_url: { url: `data:image/png;base64,${current}` } },
],
},
];
const response = await this.client.chat.completions.create({
model: 'gpt-4.1-mini',
temperature: 0,
max_tokens: 1024,
messages,
response_format: { type: 'json_object' },
});
const content = response.choices[0].message?.content ?? '';
const findings = this.parseResponse(content);
return { findings };
}
private parseResponse(content: string): DiffFinding[] {
try {
const parsed = JSON.parse(content);
if (parsed !== null && typeof parsed === 'object' && 'findings' in parsed) {
const findings = parsed.findings;
if (Array.isArray(findings)) {
return findings;
}
}
if (Array.isArray(parsed)) {
return parsed;
}
if (parsed !== null && typeof parsed === 'object' && 'results' in parsed) {
const results = parsed.results;
if (Array.isArray(results)) {
return results;
}
}
return [];
} catch (error) {
console.error('JSON parse error:', error);
console.error('Raw content:', content);
return [];
}
}
}
1.2 今回使用した画像
まずは、簡単なHTMLのサイトを作り実験しました。
- 左: ベース画像
- 右: 現状の画像(ローディングオーバーレイ+ブランドカラーが赤系に変化、カードが重なっていてレイアウトが崩れている)
1.3 プロンプト
const SYSTEM_PROMPT = `あなたは厳密なビジュアルリグレッションテストボットです。
との画像を比較し、以下の基準で判定してください:
1. ERROR(エラー)として判定すべきもの:
- ブランドカラーの変更(企業のアイデンティティに関わる色の変化)
- レイアウトの崩壊(要素の重なり、グリッドの破壊、配置の大幅なずれ)
- ローディング状態が続いている(回転するスピナー、ローディングオーバーレイ、プログレスバー、スケルトンスクリーンなど、ページ全体または一部にローディング表示が残っている)
- 画像の読み込み失敗(壊れた画像アイコン、404エラー、空白の画像エリア)
- 重要なUIコンポーネントの消失(ボタン、フォーム、ナビゲーション)
- エラーメッセージの表示
2. WARN(警告)として判定すべきもの:
- テキスト文言の変更(誤字脱字、翻訳の変更を含む)
- セクション内の画像コンテンツの変更(異なる写真やイラスト)
- 軽微な位置ずれ(5px以上20px未満)
- フォントサイズやスタイルの変更
- 背景色の軽微な変更(ブランドカラー以外)
- アイコンの変更
3. 無視すべきもの:
- 2px未満のアンチエイリアスノイズ
- 画像圧縮による軽微な色差
- タイムスタンプ、日付、動的な数値
- アニメーションの途中状態
- 広告やサードパーティコンテンツ
結果は以下のJSON配列形式で返してください:
[{
"severity": "ERROR" または "WARN",
"reason": "問題の説明(日本語)",
"bbox": [x, y, width, height] // 問題箇所の座標(オプション)
}]
差分がない場合は空配列 [] を返してください。`;
const USER_PROMPT = `以下の2つの画像を比較してください。1枚目がbaseline(期待される正しい状態)、2枚目がcurrent(現在の状態)です。
上記の2つの画像を詳細に比較し、差分をJSON配列で返してください。特に以下の点に注意してください:
1. ページ全体に半透明のオーバーレイやローディングスピナーが表示されていないか
2. ブランドカラー(ヘッダー、ボタン、リンクなど)が変更されていないか
3. レイアウトが崩れていないか(要素の重なり、位置ずれ)
4. 画像が正しく表示されているか
差分がない場合は空配列[]を返してください。`;
const messages: any[] = [
{ role: 'system', content: SYSTEM_PROMPT },
{
role: 'user',
content: [
{ type: 'text', text: USER_PROMPT },
{ type: 'image_url', image_url: { url: `data:image/png;base64,${baseline}` } },
{ type: 'image_url', image_url: { url: `data:image/png;base64,${current}` } },
],
},
];
1.4 使用したモデルとバージョン
Provider | Model |
---|---|
Gemini 2.5 Flash | |
Gemini 2.0 Flash | |
Gemini 1.5 Pro | |
OpenAI | GPT-4o mini |
OpenAI | GPT-4.1 mini |
Amazon Bedrock | Claude 3 Haiku |
2. 検証結果
検証にあたって、最低限検知してほしい項目を以下のように設定しました。
- [ERROR] メインカラーが大きく変わっている
- [ERROR] ローディングが表示されている
- [ERROR] レイアウトが崩れている
- [WARNING] テキストが変わっている(LPの見出しが変わる、コピーが変わるなど)
2.1 結果
Provider | Model | メインカラーの変更検知 | ローディングが表示されてるか | レイアウト崩れの検知 | テキスト変更がワーニングとして扱われているか |
---|---|---|---|---|---|
Gemini 2.5 Flash | ⚪︎ | ⚪︎ | ⚪︎ | × | |
Gemini 2.0 Flash | ⚪︎ | × | × | × | |
OpenAI | GPT-4.1 mini | ⚪︎ | ⚪︎ | ⚪︎ | × |
OpenAI | GPT-4o mini | ⚪︎ | ⚪︎ | × | ⚪︎ |
Amazon Bedrock | Claude 3 Haiku | × | × | × | × |
結論として、Gemini 2.5 Flash と GPT-4.1 mini は、こちらの期待にだいたい沿った検知ができたと感じました。
一方で GPT-4o mini は、テキスト変更を正しくWARNとして分類できたものの、レイアウト崩れの検知精度が低めでした。
実際にやってみて特に良かった点は、Slack通知の分かりやすさです。
「ローディングスピナーが残っています」「レイアウトが崩れています」 といった具体的な説明により、差分画像を詳しく見なくても問題箇所をすぐに把握することができました!
▼ 通知された内容
◼︎ 分析結果のログ(一部抜粋)
GPT-4.1 mini
[STATS] 分析結果:
[ERROR] [ERROR] ページ全体に赤みがかった半透明のオーバーレイが表示されており、中央にローディングスピナーが存在するため、ローディング状態が続いている。
位置: x=0, y=0, w=360, h=1130
[ERROR] [ERROR] ブランドカラーが青から赤に変更されている。ヘッダーのロゴ、ナビゲーションメニュー、ボタンの文字色、価格表示、フッターのリンク色などが赤色に変わっている。
位置: x=0, y=0, w=360, h=1130
[ERROR] [ERROR] レイアウトが崩れている。製品カードの重なりや位置ずれが発生し、特に2つ目の製品カードが左に大きくずれて重なっている。
位置: x=0, y=300, w=360, h=300
Gemini 2.5 Flash
[STATS] 分析結果:
[ERROR] [ERROR] ページ全体のブランドカラーが広範囲に変更されています。(ヘッダーのナビゲーションリンク色、ヒーローセクションの背景色とテキスト色、ヒーローセクションのボタン色、製品ラインナップ見出し色、各製品カードの価格表示色、フッターのテキスト色などが、青/白基調からピンク基調へ変更されています。)
位置: x=0, y=0, w=1000, h=1050
[ERROR] [ERROR] ヒーローセクションの「無料相談を申し込む」ボタン内にローディングスピナー(赤い回転アイコン)が残った状態になっています。
位置: x=350, y=290, w=300, h=50
[ERROR] [ERROR] 製品カードのレイアウトが崩壊し、「Analytics Pro」カードと「Security Suite」カードが重なっています。
位置: x=50, y=490, w=900, h=200
[WARN] [WARN] 「Analytics Pro」セクションのアイコンが変更されています。([STATS] → [CHART])
位置: x=120, y=510, w=100, h=40
[WARN] [WARN] 「Security Suite」セクションのアイコンが変更されています。([LOCK] → [SECURE])
位置: x=500, y=510, w=100, h=40
その他
=== Gemini 2.0 Flash ===
[STATS] 分析結果:
[ERROR] [ERROR] ブランドカラーの変更(背景色が青からピンクに変更)
位置: x=0, y=0, w=1280, h=600
[WARN] [WARN] 製品ラインナップの画像が変更
位置: x=70, y=450, w=400, h=280
[WARN] [WARN] 製品ラインナップの画像が変更
位置: x=480, y=450, w=330, h=280
=== Gemini 1.5 Pro ===
[STATS] 分析結果:
[ERROR] [ERROR] レイアウトの崩壊(STATSとLOCKのセクションの内容が入れ替わっている)
位置: x=0, y=450, w=400, h=300
[WARN] [WARN] テキスト文言の変更(Security Suiteの説明テキスト)
位置: x=100, y=550, w=300, h=50
[WARN] [WARN] セクション内の画像コンテンツの変更(STATSセクションのアイコン)
位置: x=50, y=500, w=50, h=50
=== GPT-4o mini ===
[STATS] 分析結果:
[ERROR] [ERROR] ブランドカラーが変更されています(青からピンクに変わっています)。
位置: x=undefined, y=undefined, w=undefined, h=undefined
[ERROR] [ERROR] ローディングスピナーが表示されています。
位置: x=undefined, y=undefined, w=undefined, h=undefined
[WARN] [WARN] テキスト文言が変更されています([STATS]が[CHART]に変わっています)。
位置: x=undefined, y=undefined, w=undefined, h=undefined
=== Claude 3 Haiku ===
[STATS] 分析結果:
[OK] 差分なし - テスト合格!
2.2 実際のプロダクトでも試してみた
デスクトップ
◼︎ 使用した画像(画像欠損や、カラーを変えている)
◼︎ 検証結果
[STATS] 分析結果:
[ERROR] [ERROR] ブランドカラーが変更されている。具体的には「IVRとAIで」の文字色と「今すぐ無料で試してみる」ボタンの背景色が黄色からピンクに変わっている。
位置: x=300, y=250, w=200, h=100
[ERROR] [ERROR] レイアウトの崩れ。2枚目の画像で「今すぐ無料で試してみる」ボタンのテキストが中央揃えでなくなり、ボタンの高さも変わっている。また、右下のタブのテキストが「XXXXX XXXXX」となり、表示が崩れている。
位置: x=300, y=350, w=200, h=50
[ERROR] [ERROR] ロゴやアイコンは表示されているが、企業ロゴの下の説明テキストが消失している。
位置: x=0, y=600, w=1200, h=50
▼ 通知された内容
結果として、カラー変更やレイアウト崩れについては期待通りERRORとして検知されました。
ただし、見出し(h1タグ)のテキスト変更が見逃されていました。今回のケースでは意図的な変更でしたが、誤字脱字や不適切な文言変更を見逃す可能性があるため、テキスト変更もWARNINGレベルで検知するようプロンプトを調整する必要がありそうだなと感じました。
モバイル
◼︎ 使用した画像(レイアウト崩れ中心)
◼︎ 検証結果
[STATS] 分析結果:
[ERROR] [ERROR] ブランドカラーの変更。ヘッダーの背景色が緑からピンクに変更されている。
位置: x=0, y=0, w=360, h=80
[ERROR] [ERROR] ボタンの色が黄色からピンクに変更されている。
位置: x=20, y=150, w=320, h=50
[ERROR] [ERROR] レイアウトの崩れ。要素の配置が大幅に異なり、画面全体の構成が変わっている。
位置: x=0, y=80, w=360, h=640
▼ 通知された内容
結果として、モバイル特有のレイアウト崩れ(要素の重なり、画面幅を超えた配置など)も正確に検知できました。レスポンシブデザインの問題は見逃しやすいので、この精度は実用的だなと感じました。
ちなみに、404ページになっている場合なども検知が可能でした。
冒頭の課題画像での検証
記事冒頭で課題として挙げていたヘッダーロゴ変更の差分についても検証してみました。GPT-4.1 miniで分析したところ「ヘッダーのロゴテキストが『IVRy』から『アイブリー』に変更されているため、ブランド表記の文言が変わっている」といったWARNING判定で具体的に説明してくれました。これまで目視で「何が変わったんだっけ?」と確認していた作業が、一瞬で理解できるようになり、大きな改善につながったと感じます。
3. 試してみて良かったことと今後の課題
3.1 試してみて良かったこと
検知差分が文章とセットで返ってくるため、バグかどうかの判断がしやすくなりました。この仕組みを取り込むことで、バグの判断からマージ or バグ修正依頼までのリードタイムが大幅に削減できるかと思います。
また、Playwrightでの閾値調整や動的に変わる部分のマスク指定などを行わずに、プロンプト調整で判定基準を柔軟に対応できそうなところも良いポイントかと思います。
3.2 今後の課題
今後の課題として、レイアウト崩れの細かな定義がまだ固まっておらず、どの程度のズレを崩れとみなすか、要素同士の重なりをどう扱うかといった基準作りに時間が掛かるかなと思っています。実際には、一定期間運用しながらチューニングしていく必要があると感じています。
また、誤検知や見逃しが発生する可能性があるため、定期的なプロンプトの見直しやベース画像の更新など、仕組み自体のメンテナンスを継続的に行う必要があります。
テキスト変更による検知が不十分だった点に関しては、テキスト差分用 の重要文言リスト を用意し、そのリストと照らし合わせて 明らかな誤字・脱字があれば ERROR、そうでなければ OK と判定する運用を試していきたいと考えています。
3.3 今後の展望
今回の検証で一定の成果を得られたと感じるので、まずは今ある VRT の仕組みに、この生成AIを使った分析の仕組みを組み込んでいこうと思います。
また、生成AIの精度の向上によって、観点によっては、人間ではなく生成AIに任せることもできるなと感じたので、UI/UX 観点のテストにも積極的に活用していきたいと考えています。
4. まとめ
今回、Playwright × 生成AI を組み合わせて、VRTの差分判定を自然言語のバグ報告に変換する仕組みを構築しました。
単なるピクセル差分ではなく「ヘッダーの色が〇〇から△△に変わっています」「〇〇のレイアウトが崩れています」といった分かりやすい日本語で教えてくれるため、バグ判断が格段に楽になりましたし、想像以上に実用的だなと感じました。
また、今回の経験を通して、このやり方を応用して、VRTだけでなくUI/UXやアクセシビリティチェックなど、視覚的な品質保証全般に活用できる手応えを感じました。
今後も生成AIを活用しながら、業務の効率化や見落としがちな品質を効率的に底上げする取り組みに挑戦していきます。
ぜひ、今回の記事を読んで興味を持っていただけた方は、お気軽に声をかけてもらえると嬉しいです!
情報交換など X やカジュアルにお話しできたらと思います!
Views: 0