- 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
230 lines
6.2 KiB
JavaScript
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);
|
|
}); |