fase(20): visual regression refactor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
debian
2026-03-08 06:02:37 -04:00
parent 49e76c92b1
commit 94defee1f8
40 changed files with 1670 additions and 190 deletions

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApproveAllNewStatesCommand = void 0;
const Result_1 = require("../../../../shared/domain/Result");
const VisualBaseline_1 = require("../../domain/entities/VisualBaseline");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
class ApproveAllNewStatesCommand {
constructor(comparisonRepo, baselineRepo, eventBus) {
this.comparisonRepo = comparisonRepo;
this.baselineRepo = baselineRepo;
this.eventBus = eventBus;
}
async execute(req) {
const pending = await this.comparisonRepo.findByStatus(req.sessionId, 'new_state');
let approved = 0;
for (const comparison of pending) {
const baselineId = UniqueId_1.UniqueId.create();
const baseline = VisualBaseline_1.VisualBaseline.create({
stateId: comparison.stateId,
url: comparison.sessionId,
screenshotPath: comparison.currentScreenshotPath,
width: 1280,
height: 720,
approvedAt: new Date(),
approvedBy: 'user',
}, baselineId);
await this.baselineRepo.save(baseline);
comparison.approve(baselineId.toString());
await this.comparisonRepo.update(comparison);
for (const event of comparison.domainEvents) {
await this.eventBus.publish(event);
}
comparison.clearEvents();
approved++;
}
return (0, Result_1.Ok)({ approved });
}
}
exports.ApproveAllNewStatesCommand = ApproveAllNewStatesCommand;

View File

@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApproveBaselineCommand = void 0;
const Result_1 = require("../../../../shared/domain/Result");
const VisualBaseline_1 = require("../../domain/entities/VisualBaseline");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
class ApproveBaselineCommand {
constructor(comparisonRepo, baselineRepo, eventBus) {
this.comparisonRepo = comparisonRepo;
this.baselineRepo = baselineRepo;
this.eventBus = eventBus;
}
async execute(req) {
const comparison = await this.comparisonRepo.findById(req.comparisonId);
if (!comparison) {
return (0, Result_1.Err)('Comparison not found');
}
const baselineId = UniqueId_1.UniqueId.create();
const baseline = VisualBaseline_1.VisualBaseline.create({
stateId: comparison.stateId,
url: comparison.sessionId,
screenshotPath: comparison.currentScreenshotPath,
width: 1280,
height: 720,
approvedAt: new Date(),
approvedBy: req.approvedBy ?? 'user',
}, baselineId);
await this.baselineRepo.save(baseline);
comparison.approve(baselineId.toString());
await this.comparisonRepo.update(comparison);
for (const event of comparison.domainEvents) {
await this.eventBus.publish(event);
}
comparison.clearEvents();
return (0, Result_1.Ok)({ baselineId: baselineId.toString() });
}
}
exports.ApproveBaselineCommand = ApproveBaselineCommand;

View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RejectComparisonCommand = void 0;
const Result_1 = require("../../../../shared/domain/Result");
class RejectComparisonCommand {
constructor(comparisonRepo) {
this.comparisonRepo = comparisonRepo;
}
async execute(req) {
const comparison = await this.comparisonRepo.findById(req.comparisonId);
if (!comparison) {
return (0, Result_1.Err)('Comparison not found');
}
comparison.reject();
await this.comparisonRepo.update(comparison);
return (0, Result_1.Ok)(undefined);
}
}
exports.RejectComparisonCommand = RejectComparisonCommand;

View File

@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ListComparisonsQuery = void 0;
const Result_1 = require("../../../../shared/domain/Result");
class ListComparisonsQuery {
constructor(repo) {
this.repo = repo;
}
async execute(filters) {
const comparisons = await this.repo.findAll(filters);
return (0, Result_1.Ok)(comparisons);
}
}
exports.ListComparisonsQuery = ListComparisonsQuery;

View File

@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VisualBaseline = void 0;
const Entity_1 = require("../../../../shared/domain/Entity");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
class VisualBaseline extends Entity_1.Entity {
static create(props, id) {
return new VisualBaseline(props, id ?? UniqueId_1.UniqueId.create());
}
static reconstitute(props, id) {
return new VisualBaseline(props, id);
}
get stateId() { return this.props.stateId; }
get url() { return this.props.url; }
get screenshotPath() { return this.props.screenshotPath; }
get width() { return this.props.width; }
get height() { return this.props.height; }
get approvedAt() { return this.props.approvedAt; }
get approvedBy() { return this.props.approvedBy; }
}
exports.VisualBaseline = VisualBaseline;

View File

@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VisualComparison = void 0;
const AggregateRoot_1 = require("../../../../shared/domain/AggregateRoot");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const ComparisonStatus_1 = require("../value-objects/ComparisonStatus");
const BaselineApproved_1 = require("../events/BaselineApproved");
const RegressionDetected_1 = require("../events/RegressionDetected");
class VisualComparison extends AggregateRoot_1.AggregateRoot {
static create(props, id) {
const comparison = new VisualComparison({ ...props, createdAt: new Date() }, id ?? UniqueId_1.UniqueId.create());
if (props.status.isFailed()) {
comparison.addDomainEvent(new RegressionDetected_1.RegressionDetected(comparison.id.toString(), {
sessionId: props.sessionId,
stateId: props.stateId,
diffPercent: props.diffPercent ?? 0,
}));
}
return comparison;
}
static reconstitute(props, id) {
return new VisualComparison(props, id);
}
get sessionId() { return this.props.sessionId; }
get stateId() { return this.props.stateId; }
get baselineId() { return this.props.baselineId; }
get currentScreenshotPath() { return this.props.currentScreenshotPath; }
get diffScreenshotPath() { return this.props.diffScreenshotPath; }
get diffPixels() { return this.props.diffPixels; }
get diffPercent() { return this.props.diffPercent; }
get status() { return this.props.status; }
get createdAt() { return this.props.createdAt; }
approve(newBaselineId) {
this.props.status = ComparisonStatus_1.ComparisonStatus.passed();
this.props.baselineId = newBaselineId;
this.addDomainEvent(new BaselineApproved_1.BaselineApproved(this.id.toString(), {
sessionId: this.props.sessionId,
stateId: this.props.stateId,
baselineId: newBaselineId,
}));
}
reject() {
this.props.status = ComparisonStatus_1.ComparisonStatus.failed();
}
}
exports.VisualComparison = VisualComparison;

View File

@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaselineApproved = void 0;
class BaselineApproved {
constructor(aggregateId, payload) {
this.aggregateId = aggregateId;
this.payload = payload;
this.eventName = 'visual.baseline_approved';
this.eventId = crypto.randomUUID();
this.occurredOn = new Date();
}
}
exports.BaselineApproved = BaselineApproved;

View File

@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RegressionDetected = void 0;
class RegressionDetected {
constructor(aggregateId, payload) {
this.aggregateId = aggregateId;
this.payload = payload;
this.eventName = 'visual.regression_detected';
this.eventId = crypto.randomUUID();
this.occurredOn = new Date();
}
}
exports.RegressionDetected = RegressionDetected;

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComparisonStatus = void 0;
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
class ComparisonStatus extends ValueObject_1.ValueObject {
get value() {
return this.props.value;
}
static passed() { return new ComparisonStatus({ value: 'passed' }); }
static failed() { return new ComparisonStatus({ value: 'failed' }); }
static newState() { return new ComparisonStatus({ value: 'new_state' }); }
static pending() { return new ComparisonStatus({ value: 'pending' }); }
static from(value) {
if (!['passed', 'failed', 'new_state', 'pending'].includes(value)) {
throw new Error(`Invalid comparison status: ${value}`);
}
return new ComparisonStatus({ value: value });
}
isPassed() { return this.props.value === 'passed'; }
isFailed() { return this.props.value === 'failed'; }
isNewState() { return this.props.value === 'new_state'; }
}
exports.ComparisonStatus = ComparisonStatus;

32
dist/modules/visual-regression/index.js vendored Normal file
View File

@@ -0,0 +1,32 @@
"use strict";
// Visual Regression Module — Public API
Object.defineProperty(exports, "__esModule", { value: true });
exports.createVisualRegressionRouter = exports.VisualRegressionAdapter = exports.KyselyVisualComparisonRepository = exports.KyselyVisualBaselineRepository = exports.ListComparisonsQuery = exports.ApproveAllNewStatesCommand = exports.RejectComparisonCommand = exports.ApproveBaselineCommand = exports.RegressionDetected = exports.BaselineApproved = exports.ComparisonStatus = exports.VisualComparison = exports.VisualBaseline = void 0;
// Domain
var VisualBaseline_1 = require("./domain/entities/VisualBaseline");
Object.defineProperty(exports, "VisualBaseline", { enumerable: true, get: function () { return VisualBaseline_1.VisualBaseline; } });
var VisualComparison_1 = require("./domain/entities/VisualComparison");
Object.defineProperty(exports, "VisualComparison", { enumerable: true, get: function () { return VisualComparison_1.VisualComparison; } });
var ComparisonStatus_1 = require("./domain/value-objects/ComparisonStatus");
Object.defineProperty(exports, "ComparisonStatus", { enumerable: true, get: function () { return ComparisonStatus_1.ComparisonStatus; } });
var BaselineApproved_1 = require("./domain/events/BaselineApproved");
Object.defineProperty(exports, "BaselineApproved", { enumerable: true, get: function () { return BaselineApproved_1.BaselineApproved; } });
var RegressionDetected_1 = require("./domain/events/RegressionDetected");
Object.defineProperty(exports, "RegressionDetected", { enumerable: true, get: function () { return RegressionDetected_1.RegressionDetected; } });
// Application
var ApproveBaselineCommand_1 = require("./application/commands/ApproveBaselineCommand");
Object.defineProperty(exports, "ApproveBaselineCommand", { enumerable: true, get: function () { return ApproveBaselineCommand_1.ApproveBaselineCommand; } });
var RejectComparisonCommand_1 = require("./application/commands/RejectComparisonCommand");
Object.defineProperty(exports, "RejectComparisonCommand", { enumerable: true, get: function () { return RejectComparisonCommand_1.RejectComparisonCommand; } });
var ApproveAllNewStatesCommand_1 = require("./application/commands/ApproveAllNewStatesCommand");
Object.defineProperty(exports, "ApproveAllNewStatesCommand", { enumerable: true, get: function () { return ApproveAllNewStatesCommand_1.ApproveAllNewStatesCommand; } });
var ListComparisonsQuery_1 = require("./application/queries/ListComparisonsQuery");
Object.defineProperty(exports, "ListComparisonsQuery", { enumerable: true, get: function () { return ListComparisonsQuery_1.ListComparisonsQuery; } });
// Infrastructure
var KyselyVisualRepository_1 = require("./infrastructure/repositories/KyselyVisualRepository");
Object.defineProperty(exports, "KyselyVisualBaselineRepository", { enumerable: true, get: function () { return KyselyVisualRepository_1.KyselyVisualBaselineRepository; } });
Object.defineProperty(exports, "KyselyVisualComparisonRepository", { enumerable: true, get: function () { return KyselyVisualRepository_1.KyselyVisualComparisonRepository; } });
var VisualRegressionAdapter_1 = require("./infrastructure/adapters/VisualRegressionAdapter");
Object.defineProperty(exports, "VisualRegressionAdapter", { enumerable: true, get: function () { return VisualRegressionAdapter_1.VisualRegressionAdapter; } });
var VisualRegressionController_1 = require("./infrastructure/http/VisualRegressionController");
Object.defineProperty(exports, "createVisualRegressionRouter", { enumerable: true, get: function () { return VisualRegressionController_1.createVisualRegressionRouter; } });

View File

@@ -0,0 +1,157 @@
"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;

View File

@@ -0,0 +1,82 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createVisualRegressionRouter = createVisualRegressionRouter;
const express_1 = require("express");
const Result_1 = require("../../../../shared/domain/Result");
function createVisualRegressionRouter(deps) {
const router = (0, express_1.Router)();
// GET /api/visual/comparisons
router.get('/comparisons', async (req, res, next) => {
try {
const sessionId = req.query['sessionId'];
const status = req.query['status'];
const result = await deps.listComparisons.execute({ sessionId, status });
if ((0, Result_1.isErr)(result)) {
res.status(500).json({ error: result.error });
return;
}
const comparisons = result.value.map((c) => ({
id: c.id.toString(),
session_id: c.sessionId,
state_id: c.stateId,
baseline_id: c.baselineId,
current_screenshot_path: c.currentScreenshotPath,
diff_screenshot_path: c.diffScreenshotPath,
diff_pixels: c.diffPixels,
diff_percent: c.diffPercent,
status: c.status.value,
created_at: c.createdAt.getTime(),
}));
res.json(comparisons);
}
catch (err) {
next(err);
}
});
// POST /api/visual/baselines/:comparisonId/approve
router.post('/baselines/:comparisonId/approve', async (req, res, next) => {
try {
const comparisonId = String(req.params['comparisonId']);
const result = await deps.approveBaseline.execute({ comparisonId });
if ((0, Result_1.isErr)(result)) {
res.status(404).json({ error: result.error });
return;
}
res.json({ baselineId: result.value.baselineId, status: 'approved' });
}
catch (err) {
next(err);
}
});
// POST /api/visual/baselines/:comparisonId/reject
router.post('/baselines/:comparisonId/reject', async (req, res, next) => {
try {
const comparisonId = String(req.params['comparisonId']);
const result = await deps.rejectComparison.execute({ comparisonId });
if ((0, Result_1.isErr)(result)) {
res.status(404).json({ error: result.error });
return;
}
res.json({ status: 'rejected' });
}
catch (err) {
next(err);
}
});
// POST /api/visual/baselines/approve-all
router.post('/baselines/approve-all', async (req, res, next) => {
try {
const { sessionId } = req.body;
const result = await deps.approveAllNewStates.execute({ sessionId });
if ((0, Result_1.isErr)(result)) {
res.status(500).json({ error: result.error });
return;
}
res.json({ approved: result.value.approved });
}
catch (err) {
next(err);
}
});
return router;
}

View File

@@ -0,0 +1,130 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KyselyVisualComparisonRepository = exports.KyselyVisualBaselineRepository = void 0;
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const VisualBaseline_1 = require("../../domain/entities/VisualBaseline");
const VisualComparison_1 = require("../../domain/entities/VisualComparison");
const ComparisonStatus_1 = require("../../domain/value-objects/ComparisonStatus");
class KyselyVisualBaselineRepository {
constructor(db) {
this.db = db;
}
async save(baseline) {
await this.db.insertInto('visual_baselines').values({
id: baseline.id.toString(),
state_id: baseline.stateId,
url: baseline.url,
screenshot_path: baseline.screenshotPath,
approved_at: baseline.approvedAt.getTime(),
approved_by: baseline.approvedBy,
width: baseline.width,
height: baseline.height,
}).onConflict(oc => oc.column('id').doUpdateSet({
screenshot_path: baseline.screenshotPath,
approved_at: baseline.approvedAt.getTime(),
approved_by: baseline.approvedBy,
})).execute();
}
async findByStateId(stateId) {
const row = await this.db
.selectFrom('visual_baselines')
.selectAll()
.where('state_id', '=', stateId)
.orderBy('approved_at', 'desc')
.limit(1)
.executeTakeFirst();
return row ? this.toDomain(row) : null;
}
async findById(id) {
const row = await this.db
.selectFrom('visual_baselines')
.selectAll()
.where('id', '=', id)
.executeTakeFirst();
return row ? this.toDomain(row) : null;
}
toDomain(row) {
return VisualBaseline_1.VisualBaseline.reconstitute({
stateId: row.state_id,
url: row.url,
screenshotPath: row.screenshot_path,
width: row.width,
height: row.height,
approvedAt: new Date(row.approved_at),
approvedBy: row.approved_by ?? 'user',
}, UniqueId_1.UniqueId.from(row.id));
}
}
exports.KyselyVisualBaselineRepository = KyselyVisualBaselineRepository;
class KyselyVisualComparisonRepository {
constructor(db) {
this.db = db;
}
async save(comparison) {
await this.db.insertInto('visual_comparisons').values({
id: comparison.id.toString(),
session_id: comparison.sessionId,
state_id: comparison.stateId,
baseline_id: comparison.baselineId,
current_screenshot_path: comparison.currentScreenshotPath,
diff_screenshot_path: comparison.diffScreenshotPath,
diff_pixels: comparison.diffPixels,
diff_percent: comparison.diffPercent,
status: comparison.status.value,
created_at: comparison.createdAt.getTime(),
}).execute();
}
async update(comparison) {
await this.db.updateTable('visual_comparisons')
.set({
status: comparison.status.value,
baseline_id: comparison.baselineId,
})
.where('id', '=', comparison.id.toString())
.execute();
}
async findById(id) {
const row = await this.db
.selectFrom('visual_comparisons')
.selectAll()
.where('id', '=', id)
.executeTakeFirst();
return row ? this.toDomain(row) : null;
}
async findAll(filters) {
let query = this.db.selectFrom('visual_comparisons').selectAll();
if (filters?.sessionId) {
query = query.where('session_id', '=', filters.sessionId);
}
if (filters?.status) {
query = query.where('status', '=', filters.status);
}
const rows = await query.orderBy('created_at', 'desc').execute();
return rows.map((r) => this.toDomain(r));
}
async findByStatus(sessionId, status) {
let query = this.db
.selectFrom('visual_comparisons')
.selectAll()
.where('status', '=', status);
if (sessionId) {
query = query.where('session_id', '=', sessionId);
}
const rows = await query.orderBy('created_at', 'desc').execute();
return rows.map((r) => this.toDomain(r));
}
toDomain(row) {
return VisualComparison_1.VisualComparison.reconstitute({
sessionId: row.session_id,
stateId: row.state_id,
baselineId: row.baseline_id,
currentScreenshotPath: row.current_screenshot_path,
diffScreenshotPath: row.diff_screenshot_path,
diffPixels: row.diff_pixels,
diffPercent: row.diff_percent,
status: ComparisonStatus_1.ComparisonStatus.from(row.status),
createdAt: new Date(row.created_at),
}, UniqueId_1.UniqueId.from(row.id));
}
}
exports.KyselyVisualComparisonRepository = KyselyVisualComparisonRepository;