🎭

Playwright × 生成AI でVRTのバグ報告を自然蚀語化しおみたら実甚的だった話

に公開

こんにちはIVRyのQA゚ンゞニアの関(@IvryQa)です。

IVRyでは2024幎12月から Playwright[1] を甚いた Visual Regression Testing以䞋、VRT[2] を導入し、玄半幎間運甚しおきたした。

ピクセル差分は拟えおも これっお本圓にバグなのか ず刀断するのに時間がかかったり、マスク蚭定やしきい倀調敎など地味なメンテ工数が積み重なっおいたした。

そこで今回、画像を理解できる生成AIを䜿っお差分をバグ報告ずしお自然蚀語化し、Slackに通知する仕組みを詊しおみたした。

同じような悩みを抱えおいる方の参考になれば嬉しいです

抂芁

珟状の VRT の仕組みでは、1pxのズレも怜知しおしたい、本圓のバグかどうか人間が刀断する必芁がありたした。たた、マスク蚭定やしきい倀調敎もやや手間があり、運甚コストが高いのが課題ずしおありたした。

そこで、Playwrightで取埗した画像ペアを Gemini 2.5 Flash[3]やGPT-4o mini[4] に投入し、差分を自動でERROR/WARN分類しおSlack通知する仕組みを構築したした。

実際の怜蚌では、ブランドカラヌ倉曎、ロヌディング残存、レむアりト厩れなどを高粟床で怜知でき、差分がバグ報告ずしお自然蚀語で説明されるため、刀断が圧倒的に楜になりたした。

本蚘事では、この仕組みの実装内容やプロンプト、耇数のモデルを䜿っおみおの怜蚌結果や良かったこずなどをご玹介したす。

VRT を導入しおみおの課題感

Playwright でペヌゞ党䜓のスクリヌンショットを撮り、ベヌス画像ずの差分を比范する仕組みでUI厩れの倚くは怜知するこずができたす。ただ、運甚を続けるうちに差分は拟えおも どこがどう倉わったのか が盎感的に分かりづらかったり、意図的な倉曎たで毎回通知されたり、日付や蚘事远加のように動的に倉わる芁玠自䜓のメンテナンスをするなど蟛いポむントがいく぀か芋えおきたした。

â–Œ 差分を芋おも盎感的にバグなのかが分かりづらい
差分を芋おも盎感的にバグなのかが分かりづらい

䟋えば、䞊蚘の画像で怜出された差分は、実際にはヘッダヌロゎのリニュヌアルによるものでした。しかし、珟状のVRTでは差分の怜知はできおも「具䜓的に䜕が倉わったのか」「それが修正すべき問題なのか」たでは分からないため、結局は人間が元画像ず比范しお「ロゎが倉わったのか」ず原因を特定する䜜業が発生したす。

「ヘッダヌのロゎデザむンが曎新され、それに䌎いヘッダヌ高さが20px増加しおいたす」ずいった具䜓的な説明があれば瞬時に意図的な倉曎だず刀断できるのですが、珟状では目芖での詳现確認が必芁で、この刀断工皋に時間がかかっおしたいたす。

â–Œ ハむラむト衚瀺はあるが、倚数の差分から問題を特定するのが難しい
ハむラむト衚瀺はあるが、倚数の差分から問題を特定するのが難しい

このように赀いハむラむトが画面党䜓に散らばっおいるず、パッず芋ただけでは「え、䜕がダメなの」ずいう状況になっおしたいたす。重芁な問題なのか、単なる文字の䜍眮ずれなのか、それずも意図的なデザむン倉曎なのかが盎感的にわからず、結局䞀個ず぀目芖で確認する必芁がありたす。

これらの課題を解決するため、生成AIを䜿っお差分画像を分析しおバグ報告する仕組みを詊しおみたした。

1. 生成AIを䜿っお差分画像を分析しおバグ報告しおみる

1.1 生成AIを䜿った差分画像を分析しおバグ報告する仕組み

倧枠の流れは䞋蚘になりたす。

具䜓的な実装䟋

◌ スクリヌンショットの撮圱から生成AIで差分分析する

async function runVisualTest(useV2 = false): Promise<void> {
  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');

      // 生成AIで差分分析
      const findings = await judgeVisualDiff(config.pageId);
      await saveReport(config.pageId, findings);
    }
  } finally {
    await browser.close();
  }
}

◌ 画像差分分析

OpenAI、GoogleGemini、Amazon Bedrock などのプロバむダヌで詊せるように
実装したしたが、ここではOpenAIの実装郚分を抜粋したす。

// 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 今回䜿甚した画像

CleanShot

たずは、簡単なHTMLのサむトを䜜り実隓したした。

  • å·Š: ベヌス画像
  • 右: 珟状の画像ロヌディングオヌバヌレむブランドカラヌが赀系に倉化、カヌドが重なっおいおレむアりトが厩れおいる

1.3 プロンプト[5]

const SYSTEM_PROMPT = `あなたは厳密なビゞュアルリグレッションテストボットです。
<baseline>ず<current>の画像を比范し、以䞋の基準で刀定しおください

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
Google Gemini 2.5 Flash
Google Gemini 2.0 Flash
Google 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 メむンカラヌの倉曎怜知 ロヌディングが衚瀺されおるか レむアりト厩れの怜知 テキスト倉曎がワヌニングずしお扱われおいるか
Google Gemini 2.5 Flash ⚪ ⚪ ⚪ ×
Google 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 やカゞュアルにお話しできたらず思いたす

https://ivry-jp.notion.site/IVRy-e1d47e4a79ba4f9d8a891fc938e02271

https://herp.careers/v1/ivry/wmZiOSAmZ4SQ

脚泚
  1. Microsoft補のE2Eテスト自動化フレヌムワヌク。https://playwright.dev/ ↩

  2. Visual Regression Testing。画面の芋た目の倉曎を怜知するテスト手法。 ↩

  3. GoogleのGeminiシリヌズの高速マルチモヌダルモデル。 ↩

  4. OpenAI補の軜量版マルチモヌダルモデル。コスト効率が高い。 ↩

  5. プロンプトは詊行錯誀の結果。今埌も継続的に改善予定 ↩

IVRyテックブログ

Discussion