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:
- Test Execution: Runner Playwright mengeksekusi skenario uji dan merekam artifact video pada runner lokal.
- Frame Extraction & Deduplication: FFmpeg mengekstrak frame dengan threshold perubahan scene guna membuang rendering statis.
- Visual Inference: Skrip memanggil Vision API dengan prompt terstruktur berbasis format JSON.
- CI Gate: Skrip mengembalikan exit code
1jika ditemukan anomali visual kategoriCRITICALatauHIGHuntuk 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.jpgParameter 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.jsDengan mengevaluasi representasi video secara asinkron berbasis frame diskrit, sistem verifikasi UI menangkap kecacatan visual dinamis tanpa menambah dependensi baseline snapshot yang rentan flakiness.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!