158 lines
6.4 KiB
JavaScript
158 lines
6.4 KiB
JavaScript
"use strict";
|
|
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.VisualRegressionAdapter = exports.DEFAULT_VISUAL_CONFIG = void 0;
|
|
/**
|
|
* VisualRegressionAdapter — wraps screenshot comparison logic.
|
|
* Uses IStorageProvider for persisting diff images instead of direct fs calls.
|
|
*/
|
|
const crypto = __importStar(require("crypto"));
|
|
const path = __importStar(require("path"));
|
|
const VisualComparison_1 = require("../../domain/entities/VisualComparison");
|
|
const ComparisonStatus_1 = require("../../domain/value-objects/ComparisonStatus");
|
|
exports.DEFAULT_VISUAL_CONFIG = {
|
|
enabled: true,
|
|
threshold: 0.001,
|
|
screenshotFullPage: false,
|
|
ignoreSelectors: [],
|
|
};
|
|
async function compareScreenshots(baselinePath, currentPath, threshold) {
|
|
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;
|
|
// Encode diff as PNG
|
|
const pngBuffer = await sharp(diffBuffer, { raw: { width, height, channels: 4 } })
|
|
.png()
|
|
.toBuffer();
|
|
return { diffPixels, diffPercent, diffBuffer: pngBuffer, width, height };
|
|
}
|
|
class VisualRegressionAdapter {
|
|
constructor(storage, baselineRepo, comparisonRepo, eventBus, config = {}) {
|
|
this.storage = storage;
|
|
this.baselineRepo = baselineRepo;
|
|
this.comparisonRepo = comparisonRepo;
|
|
this.eventBus = eventBus;
|
|
this.config = { ...exports.DEFAULT_VISUAL_CONFIG, ...config };
|
|
}
|
|
async processScreenshot(screenshotPath, state, sessionId, actionTrace) {
|
|
if (!this.config.enabled)
|
|
return null;
|
|
const comparisonId = crypto.randomUUID();
|
|
const baseline = await this.baselineRepo.findByStateId(state.id);
|
|
if (!baseline) {
|
|
const comparison = VisualComparison_1.VisualComparison.create({
|
|
sessionId,
|
|
stateId: state.id,
|
|
baselineId: null,
|
|
currentScreenshotPath: screenshotPath,
|
|
diffScreenshotPath: null,
|
|
diffPixels: null,
|
|
diffPercent: null,
|
|
status: ComparisonStatus_1.ComparisonStatus.newState(),
|
|
});
|
|
await this.comparisonRepo.save(comparison);
|
|
return null;
|
|
}
|
|
let diffPixels = 0;
|
|
let diffPercent = 0;
|
|
let diffScreenshotPath = null;
|
|
try {
|
|
const result = await compareScreenshots(baseline.screenshotPath, screenshotPath, this.config.threshold);
|
|
diffPixels = result.diffPixels;
|
|
diffPercent = result.diffPercent;
|
|
if (diffPixels > 0) {
|
|
const diffRelativePath = path.join('visual', comparisonId, 'diff.png');
|
|
diffScreenshotPath = await this.storage.save(diffRelativePath, result.diffBuffer);
|
|
}
|
|
}
|
|
catch {
|
|
return null;
|
|
}
|
|
const hasFailed = diffPercent > this.config.threshold;
|
|
const status = hasFailed ? ComparisonStatus_1.ComparisonStatus.failed() : ComparisonStatus_1.ComparisonStatus.passed();
|
|
const comparison = VisualComparison_1.VisualComparison.create({
|
|
sessionId,
|
|
stateId: state.id,
|
|
baselineId: baseline.id.toString(),
|
|
currentScreenshotPath: screenshotPath,
|
|
diffScreenshotPath,
|
|
diffPixels,
|
|
diffPercent,
|
|
status,
|
|
});
|
|
await this.comparisonRepo.save(comparison);
|
|
// Publish domain events
|
|
for (const event of comparison.domainEvents) {
|
|
await this.eventBus.publish(event);
|
|
}
|
|
comparison.clearEvents();
|
|
if (!hasFailed)
|
|
return null;
|
|
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: diffScreenshotPath ?? screenshotPath,
|
|
rawErrors: [`Diff: ${diffPixels} pixels (${pct.toFixed(2)}%)`],
|
|
},
|
|
timestamp: Date.now(),
|
|
};
|
|
return anomaly;
|
|
}
|
|
}
|
|
exports.VisualRegressionAdapter = VisualRegressionAdapter;
|