Menguji antarmuka visual (UI) bergaya retro seperti tampilan monokrom 1-bit ala Macintosh Classic (resolusi 512×342) atau game 8-bit memiliki tantangan unik. Berbeda dengan web modern yang memanfaatkan subpixel rendering dan vector smoothing, UI retro bergantung pada penempatan piksel biner yang presisi. Pergeseran subpixel sebesar 0.5px atau variasi anti-aliasing GPU akan menyebabkan snapshot diff gagal total.
Artikel ini membahas arsitektur visual regression testing deterministik untuk UI retro menggunakan harness mock framebuffer berbasis node-canvas, eliminasi flaky test, masking area dinamis, serta integrasi pada CI pipeline.
Penyebab Flaky Test pada Render UI Retro
Ketidakcocokan snapshot pada pipeline CI umumnya dipicu oleh tiga faktor non-deterministik:
- Font Smoothing dan Anti-Aliasing: FreeType di Linux (CI) dan CoreText di macOS merender glyph secara berbeda pada tingkat subpixel jika font smoothing aktif.
- Asynchronous Font Loading: Render buffer dieksekusi sebelum custom bitmap font (misal format
.woffatau.bdf) selesai di-parsing oleh runtime, menyebabkan fallback ke system font. - Interpolasi Skala: Browser atau renderer secara default menggunakan interpolasi bilinear saat mengubah ukuran framebuffer retro ke layar resolusi tinggi, mengaburkan batas tegas antar piksel.
Strategi Determinisme Render
Sebelum membandingkan byte buffer, lingkungan render harus dipaksa berada dalam kondisi deterministik.
1. Menonaktifkan Font Smoothing dan Image Smoothing
Pada CSS atau Canvas context, matikan seluruh algoritma interpolasi piksel:
/* Global CSS untuk web-based retro render */
canvas, .retro-ui {
image-rendering: pixelated;
font-smooth: never;
-webkit-font-smoothing: none;
text-rendering: geometricPrecision;
}2. Memastikan Ketersediaan Web Font
Gunakan API document.fonts.ready sebelum snapshot diambil untuk memastikan font bitmap lokal telah terpasang ke dalam memory map engine.
Implementasi Test Harness: Mock Framebuffer & Diffing
Daripada menjalankan browser penuh via Playwright yang memakan resource dan rentan terhadap perbedaan driver GPU di CI, pendekatan paling deterministik untuk UI retro resolusi rendah adalah merender langsung ke in-memory pixel buffer menggunakan canvas di Node.js.
Berikut adalah implementasi harness pengujian yang memvalidasi output render terhadap snapshot baseline:
import { createCanvas, registerFont } from 'canvas';
import fs from 'node:fs';
import path from 'node:path';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
// Daftarkan bitmap font tetap
registerFont('./fonts/Chicago-12.ttf', { family: 'RetroMac' });
const WIDTH = 512;
const HEIGHT = 342;
export function renderRetroWindow(state) {
const canvas = createCanvas(WIDTH, HEIGHT);
const ctx = canvas.getContext('2d');
// Matikan smoothing pada software context
ctx.imageSmoothingEnabled = false;
// Clear background (White 1-bit)
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Title Bar dengan garis horizontal khas Mac OS
ctx.fillStyle = '#000000';
for (let y = 4; y < 18; y += 2) {
ctx.fillRect(4, y, WIDTH - 8, 1);
}
// Window Title box
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(40, 2, 120, 16);
ctx.fillStyle = '#000000';
ctx.font = '12px RetroMac';
ctx.fillText(state.title, 45, 14);
// Border utama
ctx.strokeRect(1, 1, WIDTH - 2, HEIGHT - 2);
// Area Dinamis: Timestamp / Status bar
ctx.fillText(`Clock: ${state.clock}`, 10, HEIGHT - 10);
return ctx.getImageData(0, 0, WIDTH, HEIGHT);
}Masking Elemen Dinamis pada Framebuffer
Komponen dinamis seperti jam sistem atau ID transaksi harus di-masking sebelum fungsi perbandingan dijalankan agar tidak merusak validasi snapshot.
export function maskDynamicArea(imageData, x, y, width, height) {
const data = imageData.data;
for (let row = y; row < y + height; row++) {
for (let col = x; col < x + width; col++) {
const idx = (row * imageData.width + col) * 4;
// Ubah area mask menjadi hitam absolut (0,0,0,255) pada kedua buffer
data[idx] = 0;
data[idx + 1] = 0;
data[idx + 2] = 0;
data[idx + 3] = 255;
}
}
}Assertion Test dengan Pixelmatch
Tes membandingkan buffer hasil render dengan file baseline PNG yang telah disimpan di repositori.
import assert from 'node:assert';
import { renderRetroWindow, maskDynamicArea } from './renderer.js';
function runSnapshotTest() {
const current = renderRetroWindow({
title: 'System Preferences',
clock: '12:45:00'
});
// Mask area jam sistem di (10, 320) dengan ukuran 120x20
maskDynamicArea(current, 10, 320, 120, 20);
const baselinePath = './snapshots/window_baseline.png';
const baselineData = PNG.sync.read(fs.readFileSync(baselinePath));
const diff = new PNG({ width: WIDTH, height: HEIGHT });
const mismatchedPixels = pixelmatch(
current.data,
baselineData.data,
diff.data,
WIDTH,
HEIGHT,
{ threshold: 0 } // Strict: 0 toleransi untuk 1-bit palette
);
if (mismatchedPixels > 0) {
fs.writeFileSync('./snapshots/diff.png', PNG.sync.write(diff));
throw new Error(`Visual regression failed: ${mismatchedPixels} pixels mismatched.`);
}
}
runSnapshotTest();
console.log('Visual regression test passed.');Integrasi pada CI Pipeline
Untuk memastikan rendering font pada software canvas sepenuhnya konsisten antara mesin lokal dan runner Linux, jalankan tes di dalam Docker container atau konfigurasikan GitHub Actions dengan font dependencies eksplisit:
name: Visual Regression
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install System Font Engine Dependencies
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev libpango1.0-dev libgif-dev librsvg2-dev
- run: npm ci
- name: Run Snapshot Tests
run: node test/visual-regression.test.js
- name: Upload Diff Artifact
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diff
path: snapshots/diff.pngCatatan: Parameterthreshold: 0pada pixelmatch hanya direkomendasikan ketika antarmuka membatasi palet warna secara ketat (misalnya monokrom murni). Jika antarmuka retro menggunakan dithering kompleks (seperti Floyd-Steinberg), naikkan threshold ke0.05guna mengakomodasi variasi rounding pada floating point math lintas arsitektur CPU (x86_64 vs ARM64).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!