const pixelmatch = require('pixelmatch'); const { PNG } = require('pngjs'); const fs = require('fs'); const path = require('path'); const THRESHOLD = parseFloat(process.env.PIXELMATCH_THRESHOLD) || 0.05; const BASELINE_DIR = process.env.BASELINE_DIR || './screenshots/baseline'; const CURRENT_DIR = process.env.CURRENT_DIR || './screenshots/current'; const DIFF_DIR = process.env.DIFF_DIR || './screenshots/diffs'; const REPORTS_DIR = process.env.REPORTS_DIR || './reports'; function readPNG(filepath) { const data = fs.readFileSync(filepath); return PNG.sync.read(data); } function compareImages(baselinePath, currentPath, diffPath) { const img1 = readPNG(baselinePath); const img2 = readPNG(currentPath); const { width, height } = img1; const diff = new PNG({ width, height }); const mismatchedPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: THRESHOLD, }); const totalPixels = width * height; const mismatchPercent = (mismatchedPixels / totalPixels) * 100; if (mismatchedPixels > 0) { fs.mkdirSync(path.dirname(diffPath), { recursive: true }); fs.writeFileSync(diffPath, PNG.sync.write(diff)); } return { mismatchedPixels, totalPixels, mismatchPercent, diffCreated: mismatchedPixels > 0 }; } function findPNGFiles(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir).filter(f => f.endsWith('.png')); } async function main() { const baselineFiles = findPNGFiles(BASELINE_DIR); const currentFiles = findPNGFiles(CURRENT_DIR); if (baselineFiles.length === 0) { console.error(`No baseline screenshots found in ${BASELINE_DIR}`); process.exit(1); } if (currentFiles.length === 0) { console.error(`No current screenshots found in ${CURRENT_DIR}`); process.exit(1); } const allFiles = [...new Set([...baselineFiles, ...currentFiles])]; const comparisons = []; for (const file of allFiles) { const baselinePath = path.join(BASELINE_DIR, file); const currentPath = path.join(CURRENT_DIR, file); if (!fs.existsSync(baselinePath)) { comparisons.push({ file, status: 'missing_baseline' }); continue; } if (!fs.existsSync(currentPath)) { comparisons.push({ file, status: 'missing_current' }); continue; } try { const diffPath = path.join(DIFF_DIR, file); const result = compareImages(baselinePath, currentPath, diffPath); comparisons.push({ file, status: result.mismatchedPixels === 0 ? 'pass' : 'fail', ...result }); } catch (err) { comparisons.push({ file, status: 'error', error: err.message }); } } const passed = comparisons.filter(c => c.status === 'pass').length; const failed = comparisons.filter(c => c.status === 'fail').length; const errors = comparisons.filter(c => c.status === 'error').length; const missing = comparisons.filter(c => c.status === 'missing_baseline' || c.status === 'missing_current').length; const report = { timestamp: new Date().toISOString(), threshold: THRESHOLD, summary: { total: comparisons.length, passed, failed, errors, missing }, comparisons, }; fs.mkdirSync(REPORTS_DIR, { recursive: true }); const reportPath = path.join(REPORTS_DIR, 'visual-test-report.json'); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log(`Report saved to ${reportPath}`); console.log(`Passed: ${passed}, Failed: ${failed}, Errors: ${errors}, Missing: ${missing}`); if (failed > 0 || errors > 0) process.exit(1); } main().catch(err => { console.error('Fatal error:', err); process.exit(1); });