config: full APAW agent infrastructure + Phantom project files
- Added all agent definitions (.kile/agents/*.md) - Added commands, rules, skills, shared modules - Added src/, scripts/, tests/, docker/, agent-evolution/ - Extracted 3 archives: website/, workspace/, release/ - Created .env with Gitea creds for UniqueSoft/Phantom - Created docs/ with project-specific guides - Added .gitignore for node_modules
This commit is contained in:
230
tests/scripts/compare-screenshots.js
Normal file
230
tests/scripts/compare-screenshots.js
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user