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:
NW
2026-06-30 21:09:30 +01:00
parent f2d4d6d3b1
commit 8d85776135
10 changed files with 1049 additions and 44 deletions

View File

@@ -0,0 +1,84 @@
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');
const VIEWPORTS = {
mobile: { width: 375, height: 667 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1280, height: 720 },
};
function slugify(pagePath) {
return pagePath.replace(/^\//, '').replace(/[^a-zA-Z0-9_-]/g, '_') || 'home';
}
async function capturePage(page, pagePath, viewportName, outputDir) {
const fullUrl = `${process.env.TARGET_URL}${pagePath}`;
const filename = `${slugify(pagePath)}_${viewportName}.png`;
const filepath = path.join(outputDir, filename);
await page.setViewportSize(VIEWPORTS[viewportName]);
await page.goto(fullUrl, { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(500);
await page.screenshot({ path: filepath, fullPage: false });
return { pagePath, viewport: viewportName, file: filename, saved: true };
}
async function main() {
const mode = process.argv[2];
if (!['baseline', 'current'].includes(mode)) {
console.error('Usage: node capture-screenshots.js <baseline|current>');
process.exit(1);
}
const targetUrl = process.env.TARGET_URL;
const pages = (process.env.PAGES || '/').split(',').map(p => p.trim()).filter(p => /^\/[A-Za-z0-9._\/-]*$/.test(p));
if (pages.length === 0) {
console.error('No valid pages in PAGES environment variable');
process.exit(1);
}
const outputDir = mode === 'baseline'
? (process.env.BASELINE_DIR || './screenshots/baseline')
: (process.env.CURRENT_DIR || './screenshots/current');
if (!targetUrl) {
console.error('TARGET_URL environment variable is required');
process.exit(1);
}
fs.mkdirSync(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
const results = [];
try {
for (const pagePath of pages) {
for (const vpName of Object.keys(VIEWPORTS)) {
try {
const result = await capturePage(page, pagePath, vpName, outputDir);
results.push(result);
console.log(`Captured ${pagePath} @ ${vpName} -> ${result.file}`);
} catch (err) {
results.push({ pagePath, viewport: vpName, error: err.message, saved: false });
console.error(`Failed ${pagePath} @ ${vpName}: ${err.message}`);
}
}
}
} finally {
await browser.close();
}
const report = { mode, targetUrl, pages, outputDir, results };
const reportPath = path.join(outputDir, '..', `${mode}-capture-report.json`);
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`Report saved to ${reportPath}`);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});