feat: add AI-powered visual regression testing infrastructure
- Add docker-compose.web-testing.yml with vlmkit service - Add 5 test scripts: capture, compare, pipeline, console-monitor, link-checker - Add vrt.config.json with 3 viewports + maskSelectors - Add package.json with @mizchi/vlmkit and Node >=24 - Add tests/README.md with VRT documentation - Update .gitignore for test artifacts Based on APAW issue #144 / milestone #106
This commit is contained in:
107
tests/scripts/compare-screenshots.js
Normal file
107
tests/scripts/compare-screenshots.js
Normal file
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user