#!/usr/bin/env node /** * Compare visual-tests/baselines/ vs visual-tests/current/ using pixelmatch. * Writes diffs to visual-tests/diffs/ and report to visual-tests/reports/. * * Usage: node scripts/compare-visual.mjs [threshold] * threshold Pixelmatch threshold 0–1 (default: VISUAL_DIFF_THRESHOLD or 0.1) * * Exit code: 0 if no diffs exceed maxDiffPixels (env VISUAL_MAX_DIFF_PIXELS, default 0). */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const BASELINES = path.join(ROOT, 'visual-tests', 'baselines'); const CURRENT = path.join(ROOT, 'visual-tests', 'current'); const DIFFS = path.join(ROOT, 'visual-tests', 'diffs'); const REPORTS = path.join(ROOT, 'visual-tests', 'reports'); const THRESHOLD = parseFloat(process.env.VISUAL_DIFF_THRESHOLD || '0.1', 10); const MAX_DIFF_PIXELS = process.env.VISUAL_MAX_DIFF_PIXELS ? parseInt(process.env.VISUAL_MAX_DIFF_PIXELS, 10) : 0; function loadPng(filePath) { const data = fs.readFileSync(filePath); return PNG.sync.read(data); } function compare(baselinePath, currentPath, diffPath, threshold) { const imgExpected = loadPng(baselinePath); const imgActual = loadPng(currentPath); if (imgExpected.width !== imgActual.width || imgExpected.height !== imgActual.height) { return { error: `Dimensions differ: ${imgExpected.width}x${imgExpected.height} vs ${imgActual.width}x${imgActual.height}`, diffPixels: null, totalPixels: imgExpected.width * imgExpected.height, }; } const { width, height } = imgExpected; const totalPixels = width * height; const diff = new PNG({ width, height }); const numDiffPixels = pixelmatch( imgExpected.data, imgActual.data, diff.data, width, height, { threshold } ); fs.mkdirSync(path.dirname(diffPath), { recursive: true }); fs.writeFileSync(diffPath, PNG.sync.write(diff)); return { diffPixels: numDiffPixels, totalPixels, error: null }; } function listPngs(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir).filter((f) => f.endsWith('.png')); } function generateHtmlReport(results, threshold) { const rows = results .map( (r) => `
${r.name}| Screen | Diff pixels | Total pixels | Result | Error | Links |
|---|