Files
APAW/tests/scripts/compare-screenshots.js
¨NW¨ e074612046 feat: add web testing infrastructure
- Docker configurations for Playwright MCP (no host pollution)
- Visual regression testing with pixelmatch
- Link checking for 404/500 errors
- Console error detection with Gitea issue creation
- Form testing capabilities
- /web-test and /web-test-fix commands
- web-testing skill documentation
- Reorganize project structure (docker/, scripts/, tests/)
- Update orchestrator model to ollama-cloud/glm-5

Structure:
- docker/ - Docker configurations (moved from archive)
- scripts/ - Utility scripts
- tests/ - Test suite with visual, console, links testing
- .kilo/commands/ - /web-test and /web-test-fix commands
- .kilo/skills/ - web-testing skill

Issues: #58 #60 #62
2026-04-07 08:55:24 +01:00

230 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Visual Regression Testing Script
*
* Compares current screenshots with baseline using pixelmatch
* Reports visual differences: overlaps, font shifts, color mismatches
*
* Usage: node compare-screenshots.js [options]
* Options:
* --threshold 0.05 - Pixel difference threshold (default: 5%)
* --baseline ./baseline - Baseline directory
* --current ./current - Current screenshots directory
* --diff ./diff - Diff output directory
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const config = {
baselineDir: process.env.BASELINE_DIR || './tests/visual/baseline',
currentDir: process.env.CURRENT_DIR || './tests/visual/current',
diffDir: process.env.DIFF_DIR || './tests/visual/diff',
reportsDir: process.env.REPORTS_DIR || './tests/reports',
threshold: parseFloat(process.env.PIXELMATCH_THRESHOLD || '0.05'),
};
// Ensure directories exist
[config.diffDir, config.reportsDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
/**
* Compare two PNG images using pixelmatch
*/
async function compareImages(baselinePath, currentPath, diffPath) {
const pixelmatch = require('pixelmatch');
const PNG = require('pngjs').PNG;
const baselineImg = PNG.sync.read(fs.readFileSync(baselinePath));
const currentImg = PNG.sync.read(fs.readFileSync(currentPath));
const { width, height } = baselineImg;
// Check if sizes match
if (width !== currentImg.width || height !== currentImg.height) {
return {
success: false,
error: `Size mismatch: baseline ${width}x${height} vs current ${currentImg.width}x${currentImg.height}`,
diffPixels: -1,
totalPixels: width * height,
};
}
// Create diff image
const diffImg = new PNG({ width, height });
// Compare
const diffPixels = pixelmatch(
baselineImg.data,
currentImg.data,
diffImg.data,
width,
height,
{
threshold: 0.1, // Pixel similarity threshold
diffColor: [255, 0, 0], // Red for differences
diffColorAlt: [255, 255, 0], // Yellow for anti-aliased
}
);
// Save diff image
fs.writeFileSync(diffPath, PNG.sync.write(diffImg));
const diffPercent = (diffPixels / (width * height)) * 100;
return {
success: diffPercent <= (config.threshold * 100),
diffPixels,
totalPixels: width * height,
diffPercent: diffPercent.toFixed(2),
width,
height,
};
}
/**
* Detect specific visual issues
*/
function detectVisualIssues(baselinePath, currentPath) {
// This would ideally use Playwright for element-level analysis
// For now, return generic analysis
return {
potentialIssues: [
'element_overlap',
'font_shift',
'color_mismatch',
'layout_break',
]
};
}
/**
* Get all PNG files from a directory
*/
function getPNGFiles(dir) {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.endsWith('.png'))
.map(f => path.basename(f, '.png'));
}
/**
* Main comparison function
*/
async function main() {
console.log('=== Visual Regression Testing ===\n');
console.log(`Baseline: ${config.baselineDir}`);
console.log(`Current: ${config.currentDir}`);
console.log(`Diff: ${config.diffDir}`);
console.log(`Threshold: ${config.threshold * 100}%\n`);
const baselineFiles = getPNGFiles(config.baselineDir);
const currentFiles = getPNGFiles(config.currentDir);
const results = [];
let passed = 0;
let failed = 0;
let missing = 0;
// Check for missing baselines
for (const file of currentFiles) {
if (!baselineFiles.includes(file)) {
console.log(`⚠️ New screenshot: ${file}`);
missing++;
results.push({
name: file,
status: 'NEW',
message: 'No baseline exists - will be created as baseline',
});
}
}
// Compare existing baselines
for (const file of baselineFiles) {
const baselinePath = path.join(config.baselineDir, `${file}.png`);
const currentPath = path.join(config.currentDir, `${file}.png`);
const diffPath = path.join(config.diffDir, `${file}_diff.png`);
if (!fs.existsSync(currentPath)) {
console.log(`❌ Missing: ${file}`);
failed++;
results.push({
name: file,
status: 'MISSING',
message: 'Current screenshot not found',
});
continue;
}
try {
console.log(`🔍 Comparing: ${file}...`);
const result = await compareImages(baselinePath, currentPath, diffPath);
if (result.success) {
console.log(`✅ PASS: ${file} (${result.diffPercent}% diff)`);
passed++;
} else {
console.log(`❌ FAIL: ${file} (${result.diffPercent}% diff)`);
console.log(` ${result.diffPixels} pixels changed of ${result.totalPixels}`);
failed++;
}
results.push({
name: file,
status: result.success ? 'PASS' : 'FAIL',
diffPercent: result.diffPercent,
diffPixels: result.diffPixels,
totalPixels: result.totalPixels,
width: result.width,
height: result.height,
diffPath: diffPath,
});
} catch (error) {
console.log(`❌ ERROR: ${file} - ${error.message}`);
failed++;
results.push({
name: file,
status: 'ERROR',
message: error.message,
});
}
}
// Generate report
const report = {
timestamp: new Date().toISOString(),
threshold: config.threshold,
summary: {
total: baselineFiles.length,
passed,
failed,
missing,
newScreenshots: missing,
},
results,
};
const reportPath = path.join(config.reportsDir, 'visual-regression-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📊 Summary:`);
console.log(` Total: ${baselineFiles.length}`);
console.log(` ✅ Pass: ${passed}`);
console.log(` ❌ Fail: ${failed}`);
console.log(` ⚠️ New: ${missing}`);
console.log(`\n📄 Report saved to: ${reportPath}`);
// Exit with error code if failures
process.exit(failed > 0 ? 1 : 0);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});