Uji coba end-to-end (E2E) berbasis Playwright atau Cypress kerap meloloskan regresi visual seperti overlap elemen CSS, modal transparan, animasi macet, atau teks terpotong selama elemen DOM target tetap berstatus visible dan dapat menerima event click. Pendekatan perbandingan screenshot pixel-by-pixel konvensional seringkali rapuh terhadap rendering font lintas sistem operasi dan anti-aliasing minor.

Solusinya adalah mengintegrasikan evaluasi visual semantik langsung ke pipeline CI/CD. Alur kerjanya meliputi: merekam sesi uji coba menjadi video (WebM/MP4), mengekstrak keyframe relevan menggunakan FFmpeg, memangkas frame statis berulang untuk efisiensi token, dan menganalisis sekuens frame tersebut dengan Vision LLM API untuk menghasilkan status exit code.

Arsitektur Pipeline

Pipeline eksekusi terdiri dari empat tahap terpisah:

  1. Test Execution: Runner Playwright mengeksekusi skenario uji dan merekam artifact video pada runner lokal.
  2. Frame Extraction & Deduplication: FFmpeg mengekstrak frame dengan threshold perubahan scene guna membuang rendering statis.
  3. Visual Inference: Skrip memanggil Vision API dengan prompt terstruktur berbasis format JSON.
  4. CI Gate: Skrip mengembalikan exit code 1 jika ditemukan anomali visual kategori CRITICAL atau HIGH untuk memblokir merge Pull Request.

Ekstraksi Frame Dinamis Menggunakan FFmpeg

Mengirim seluruh frame dari rekaman 60 FPS akan memboroskan token dan kuota rate limit API. Gunakan filter select pada FFmpeg dengan metrik scene untuk menangkap frame hanya saat terjadi perubahan tampilan UI yang signifikan, serta kompres resolusinya ke batas baca LLM.

ffmpeg -i test-recording.webm \
  -vf "fps=2,select='gt(scene,0.02)',scale=1280:-1" \
  -vsync vfr \
  -q:v 3 \
  frames/frame_%03d.jpg

Parameter gt(scene,0.02) mendeteksi perubahan frame sebesar 2% atau lebih. Parameter fps=2 memastikan sampling visual dibatasi maksimum dua frame per detik agar interaksi lambat tidak menghasilkan frame identik secara berlebihan.

Implementasi Skrip Analisis Node.js

Skrip berikut membaca frame hasil ekstraksi FFmpeg, menyusun payload multimodal, dan menginstruksikan model untuk mengevaluasi layout breaking atau visual glitch dalam format JSON valid.

import fs from 'node:fs';
import path from 'node:path';

const FRAMES_DIR = './frames';
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;

async function analyzeFrames() {
  const files = fs.readdirSync(FRAMES_DIR)
    .filter(file => file.endsWith('.jpg'))
    .sort();

  if (files.length === 0) {
    console.log('Tidak ada frame yang diekstrak.');
    process.exit(0);
  }

  const imageContents = files.map(file => {
    const base64Data = fs.readFileSync(path.join(FRAMES_DIR, file)).toString('base64');
    return {
      type: 'image_url',
      image_url: {
        url: `data:image/jpeg;base64,${base64Data}`,
        detail: 'low' // Batasi biaya token per frame
      }
    };
  });

  const prompt = `Anda adalah QA Automation Visual Reviewer. Periksa urutan frame rekaman interaksi UI ini.
Identifikasi apakah ada:
1. Elemen UI yang saling bertumpuk (layout overlapping/broken).
2. Teks yang terpotong atau overflow dari kontainer.
3. Dialog/Modal tanpa backdrop atau rendering z-index rusak.
4. Loading spinner yang membeku di atas konten final.

Wajib kembalikan format JSON murni:
{
  "passed": boolean,
  "severity": "NONE" | "LOW" | "HIGH" | "CRITICAL",
  "reasons": string[]
}`;

  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${OPENAI_API_KEY}`
    },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      response_format: { type: 'json_object' },
      messages: [
        {
          role: 'user',
          content: [
            { type: 'text', text: prompt },
            ...imageContents
          ]
        }
      ]
    })
  });

  if (!response.ok) {
    const errText = await response.text();
    console.error(`API Error: ${response.status} - ${errText}`);
    process.exit(1);
  }

  const result = await response.json();
  const verdict = JSON.parse(result.choices[0].message.content);

  console.log('--- Hasil Analisis Visual ---');
  console.log(JSON.stringify(verdict, null, 2));

  if (!verdict.passed && (verdict.severity === 'HIGH' || verdict.severity === 'CRITICAL')) {
    console.error('CI Gagal: Ditemukan regresi visual kritis.');
    process.exit(1);
  }

  console.log('CI Lolos: Tampilan UI valid.');
  process.exit(0);
}

analyzeFrames().catch(err => {
  console.error(err);
  process.exit(1);
});

Optimasi Token dan Penanganan Rate Limit CI

Penggunaan Vision LLM di pipeline multi-branch memerlukan efisiensi biaya dan ketahanan jaringan:

  • Image Detail Tier: Gunakan parameter detail: 'low' jika layout makro cukup untuk dideteksi. Mode low mengonsumsi fixed cost sekitar 85 token per gambar terlepas dari resolusi aslinya.
  • Hashing Dedup (P-Hash / SHA256): Terapkan filter hashing biner sederhana sebelum memanggil API jika FFmpeg masih menyisakan frame dengan selisih metadata rendering yang tidak kasat mata.
  • Retry Exponential Backoff: Pasang logic penanganan HTTP 429 pada skrip HTTP client untuk mengatasi burst limit saat beberapa build CI berjalan bersamaan.

Integrasi GitHub Actions Pipeline

Konfigurasi job berikut menjalankan runner Playwright, menjalankan ekstraksi via package FFmpeg bawaan runner Ubuntu, dan mengunci Pull Request bila pengujian gagal.

name: E2E Visual Verification

on: [pull_request]

jobs:
  ui-verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install Dependencies
        run: npm ci

      - name: Run Playwright Tests
        run: npx playwright test
        continue-on-error: true

      - name: Extract Video Frames
        run: |
          mkdir -p frames
          for video in test-results/**/*.webm; do
            [ -f "$video" ] || continue
            ffmpeg -i "$video" -vf "fps=2,select='gt(scene,0.02)',scale=1280:-1" -vsync vfr -q:v 3 "frames/$(basename "$video" .webm)_%03d.jpg"
          done

      - name: Run Vision LLM Audit
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: node scripts/verify-ui-llm.js

Dengan mengevaluasi representasi video secara asinkron berbasis frame diskrit, sistem verifikasi UI menangkap kecacatan visual dinamis tanpa menambah dependensi baseline snapshot yang rentan flakiness.