Tantangan Determinisme pada Pengujian Visual Canvas

Komponen antarmuka berbasis waktu seperti jam analog (misalnya pada showcase clocks.dev) menggunakan loop requestAnimationFrame dan objek Date global untuk me-render posisi jarum secara kontinu. Menjalankan visual regression testing pada elemen ini sering menghasilkan kegagalan palsu (flaky tests) akibat ketidaksesuaian mikrodetik saat snapshot diambil.

Selain faktor waktu, pipeline CI (seperti GitHub Actions berbasis Linux) menggunakan engine rasterisasi font dan sub-pixel rendering GPU yang berbeda dibanding mesin pengembang lokal (macOS atau Windows). Dua masalah utama ini harus dieliminasi: non-determinisme waktu eksekusi dan variasi render grafis headless.

Mocking System Clock via Playwright

Playwright menyediakan API page.clock untuk mengontrol waktu browser secara deterministik. API ini menggantikan implementasi Date, setTimeout, setInterval, dan requestAnimationFrame bawaan.

import { test, expect } from '@playwright/test';

test.describe('Komponen Jam Canvas', () => {
  test('harus merender posisi jarum jam 10:10:30 dengan presisi', async ({ page }) => {
    // Inisialisasi waktu tetap sebelum memuat halaman
    const fixedTime = new Date('2025-01-15T10:10:30.000Z');
    await page.clock.install({ time: fixedTime });

    await page.goto('/components/analog-clock');

    const clockCanvas = page.locator('#clock-canvas');
    await expect(clockCanvas).toBeVisible();

    // Pastikan satu frame render canvas selesai dieksekusi
    await page.clock.runFor(16);

    // Ambil visual snapshot dari elemen canvas saja
    await expect(clockCanvas).toHaveScreenshot('clock-10-10-30.png', {
      maxDiffPixelRatio: 0.01,
      animations: 'disabled',
    });
  });
});

Penggunaan page.clock.install membekukan waktu JavaScript. Pemanggilan page.clock.runFor(16) memajukan siklus sebanyak satu frame (60 FPS) agar logika render internal canvas selesai diproses sebelum Playwright mengeksekusi buffer snapshot.

Konfigurasi Headless Worker untuk Hasil Konsisten

Perbedaan layer grafis antar platform dapat diselesaikan dengan memaksa browser headless menggunakan parameter rendering software konsisten via konfigurasi Playwright.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  workers: process.env.CI ? 2 : undefined,
  use: {
    ...devices['Desktop Chrome'],
    deviceScaleFactor: 1,
    launchOptions: {
      args: [
        '--font-render-hinting=none',
        '--disable-skia-runtime-opts',
        '--disable-font-subpixel-positioning',
        '--disable-lcd-text',
      ],
    },
  },
  expect: {
    toHaveScreenshot: {
      threshold: 0.2, // Sensitivitas perbedaan warna per piksel (0 - 1)
      maxDiffPixelRatio: 0.005, // Toleransi total luas piksel berbeda (0.5%)
    },
  },
});

Flag Chromium di atas mematikan optimasi hardware-specific antialiasing dan subpixel positioning font. Nilai deviceScaleFactor: 1 mengunci resolusi viewport agar rasio DPI tidak berubah di lingkungan server.

Integrasi CI Pipeline (GitHub Actions)

Snapshot visual harus dihasilkan dan divalidasi pada lingkungan sistem operasi yang identik. Jalankan test suite di dalam container Docker resmi Playwright pada CI runner untuk mengeliminasi variasi library font dan OS dependencies.

name: Visual Regression

on: [push, pull_request]

jobs:
  visual-test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.49.0-jammy
    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npx playwright test

      - name: Upload Artifacts (Gagal Snapshot)
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: visual-diff-results
          path: test-results/
          retention-days: 7

Jika terjadi perbedaan rendering yang melampaui ambang batas maxDiffPixelRatio, artifact berupa gambar perbandingan (expected, actual, dan diff mask) diunggah otomatis untuk dianalisis oleh developer.

Trade-off dan Debugging

Bila canvas mengandalkan WebGL alih-alih 2D context standar, tambahkan flag --use-gl=swiftshader pada launchOptions untuk memastikan rasterisasi dieksekusi via software CPU di CI tanpa driver GPU fisik.

Hindari menaikkan maxDiffPixelRatio di atas 0.02. Toleransi yang terlalu longgar akan meloloskan regresi visual nyata seperti jarum detik yang melompat beberapa derajat.