"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;