docs: enterprise refactor plan with ralph specs
This commit is contained in:
155
dist/plugins/collectors/VisualRegressionCollector.js
vendored
Normal file
155
dist/plugins/collectors/VisualRegressionCollector.js
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
"use strict";
|
||||
/**
|
||||
* VisualRegressionCollector — captures screenshots and compares against baselines.
|
||||
* Uses pixelmatch for pixel-level comparison and sharp for image normalization.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VisualRegressionCollector = exports.DEFAULT_VISUAL_CONFIG = void 0;
|
||||
exports.compareScreenshots = compareScreenshots;
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const path = __importStar(require("path"));
|
||||
const fs = __importStar(require("fs"));
|
||||
exports.DEFAULT_VISUAL_CONFIG = {
|
||||
enabled: true,
|
||||
threshold: 0.001,
|
||||
screenshotFullPage: false,
|
||||
ignoreSelectors: [],
|
||||
};
|
||||
async function compareScreenshots(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
|
||||
// Dynamic imports to avoid loading heavy deps at startup
|
||||
const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
|
||||
const pixelmatch = (await Promise.resolve().then(() => __importStar(require('pixelmatch')))).default;
|
||||
const [baselineRaw, currentRaw] = await Promise.all([
|
||||
sharp(baselinePath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }),
|
||||
sharp(currentPath).resize(1280, 720).raw().toBuffer({ resolveWithObject: true }),
|
||||
]);
|
||||
const { width, height } = baselineRaw.info;
|
||||
const diffBuffer = Buffer.alloc(width * height * 4);
|
||||
const diffPixels = pixelmatch(baselineRaw.data, currentRaw.data, diffBuffer, width, height, { threshold });
|
||||
const totalPixels = width * height;
|
||||
const diffPercent = totalPixels > 0 ? diffPixels / totalPixels : 0;
|
||||
// Write diff image
|
||||
await sharp(diffBuffer, { raw: { width, height, channels: 4 } })
|
||||
.png()
|
||||
.toFile(diffOutputPath);
|
||||
return { diffPixels, diffPercent, hasDiff: diffPixels > 0 };
|
||||
}
|
||||
class VisualRegressionCollector {
|
||||
constructor(outputDir, repo, config = {}) {
|
||||
this.outputDir = outputDir;
|
||||
this.repo = repo;
|
||||
this.config = { ...exports.DEFAULT_VISUAL_CONFIG, ...config };
|
||||
}
|
||||
/**
|
||||
* Process a screenshot for visual regression.
|
||||
* Returns an anomaly if a regression is detected, otherwise null.
|
||||
*/
|
||||
async processScreenshot(screenshotPath, state, sessionId, actionTrace) {
|
||||
if (!this.config.enabled)
|
||||
return null;
|
||||
const comparisonId = crypto.randomUUID();
|
||||
const baseline = this.repo.findBaselineByStateId(state.id);
|
||||
if (!baseline) {
|
||||
// No baseline: create a new_state comparison record
|
||||
this.repo.createComparison({
|
||||
id: comparisonId,
|
||||
sessionId,
|
||||
stateId: state.id,
|
||||
currentScreenshotPath: screenshotPath,
|
||||
status: 'new_state',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
// Compare against baseline
|
||||
const diffDir = path.join(this.outputDir, 'visual', comparisonId);
|
||||
if (!fs.existsSync(diffDir)) {
|
||||
fs.mkdirSync(diffDir, { recursive: true });
|
||||
}
|
||||
const diffPath = path.join(diffDir, 'diff.png');
|
||||
let diffPixels = 0;
|
||||
let diffPercent = 0;
|
||||
try {
|
||||
const result = await compareScreenshots(baseline.screenshot_path, screenshotPath, diffPath, this.config.threshold);
|
||||
diffPixels = result.diffPixels;
|
||||
diffPercent = result.diffPercent;
|
||||
}
|
||||
catch {
|
||||
// If comparison fails (e.g. image format issues), skip
|
||||
return null;
|
||||
}
|
||||
const thresholdPct = this.config.threshold;
|
||||
const status = diffPercent > thresholdPct ? 'failed' : 'passed';
|
||||
this.repo.createComparison({
|
||||
id: comparisonId,
|
||||
sessionId,
|
||||
stateId: state.id,
|
||||
baselineId: baseline.id,
|
||||
currentScreenshotPath: screenshotPath,
|
||||
diffScreenshotPath: status === 'failed' ? diffPath : undefined,
|
||||
diffPixels,
|
||||
diffPercent,
|
||||
status,
|
||||
});
|
||||
if (status !== 'failed')
|
||||
return null;
|
||||
// Determine severity from diff percent
|
||||
const pct = diffPercent * 100;
|
||||
let severity;
|
||||
if (pct > 15)
|
||||
severity = 'critical';
|
||||
else if (pct > 5)
|
||||
severity = 'high';
|
||||
else if (pct > 1)
|
||||
severity = 'medium';
|
||||
else
|
||||
severity = 'low';
|
||||
const anomaly = {
|
||||
id: crypto.randomUUID(),
|
||||
type: 'visual_regression',
|
||||
severity,
|
||||
observationId: state.id,
|
||||
actionTrace,
|
||||
description: `Visual regression detected: ${(pct).toFixed(2)}% of pixels changed`,
|
||||
evidence: {
|
||||
screenshotPath: diffPath,
|
||||
rawErrors: [`Diff: ${diffPixels} pixels (${(pct).toFixed(2)}%)`],
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return anomaly;
|
||||
}
|
||||
}
|
||||
exports.VisualRegressionCollector = VisualRegressionCollector;
|
||||
Reference in New Issue
Block a user