Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/server/handlers/api-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Buffer } from 'buffer';
import { createServiceLogger } from '../../utils/logger-factory.js';

const logger = createServiceLogger('API-HANDLER');

export const createApiHandler = apiService => {
let vizzlyDisabled = false;
let screenshotCount = 0;

const handleScreenshot = async (buildId, name, image, properties = {}) => {
if (vizzlyDisabled) {
logger.debug(`Screenshot captured (Vizzly disabled): ${name}`);
return {
statusCode: 200,
body: {
success: true,
disabled: true,
count: ++screenshotCount,
message: `Vizzly disabled - ${screenshotCount} screenshots captured but not uploaded`,
},
};
}

if (!buildId) {
return {
statusCode: 400,
body: {
error: 'Build ID is required for screenshot upload',
},
};
}

if (!apiService) {
return {
statusCode: 500,
body: {
error: 'API service not available',
},
};
}

try {
const imageBuffer = Buffer.from(image, 'base64');
const result = await apiService.uploadScreenshot(
buildId,
name,
imageBuffer,
properties ?? {}
);

if (result.skipped) {
logger.debug(`Screenshot already exists, skipped: ${name}`);
} else {
logger.debug(`Screenshot uploaded: ${name}`);
}

if (!result.skipped) {
screenshotCount++;
}

return {
statusCode: 200,
body: {
success: true,
name,
skipped: result.skipped,
count: screenshotCount,
},
};
} catch (uploadError) {
logger.error(
`❌ Failed to upload screenshot ${name}:`,
uploadError.message
);

vizzlyDisabled = true;
const disabledMessage =
'⚠️ Vizzly disabled due to upload error - continuing tests without visual testing';
logger.warn(disabledMessage);

return {
statusCode: 200,
body: {
success: true,
name,
disabled: true,
message: disabledMessage,
},
};
}
};

const getScreenshotCount = () => screenshotCount;

const cleanup = () => {
vizzlyDisabled = false;
screenshotCount = 0;
logger.debug('API handler cleanup completed');
};

return {
handleScreenshot,
getScreenshotCount,
cleanup,
};
};
174 changes: 174 additions & 0 deletions src/server/handlers/tdd-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Buffer } from 'buffer';
import { createServiceLogger } from '../../utils/logger-factory.js';
import { TddService } from '../../services/tdd-service.js';
import { colors } from '../../utils/colors.js';

const logger = createServiceLogger('TDD-HANDLER');

export const createTddHandler = (
config,
workingDir,
baselineBuild,
baselineComparison
) => {
const tddService = new TddService(config, workingDir);
const builds = new Map();

const initialize = async () => {
logger.info('🔄 TDD mode enabled - setting up local comparison...');

const baseline = await tddService.loadBaseline();

if (!baseline) {
if (config.apiKey) {
logger.info('📥 No local baseline found, downloading from Vizzly...');
await tddService.downloadBaselines(baselineBuild, baselineComparison);
} else {
logger.info(
'📝 No local baseline found and no API token - all screenshots will be marked as new'
);
}
} else {
logger.info(
`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`
);
}
};

const registerBuild = buildId => {
builds.set(buildId, {
id: buildId,
name: `TDD Build ${buildId}`,
branch: 'current',
environment: 'test',
screenshots: [],
createdAt: Date.now(),
});
logger.debug(`Registered TDD build: ${buildId}`);
};

const handleScreenshot = async (buildId, name, image, properties = {}) => {
const build = builds.get(buildId);
if (!build) {
throw new Error(`Build ${buildId} not found`);
}

const screenshot = {
name,
imageData: image,
properties,
timestamp: Date.now(),
};

build.screenshots.push(screenshot);

const imageBuffer = Buffer.from(image, 'base64');
const comparison = await tddService.compareScreenshot(
name,
imageBuffer,
properties
);

if (comparison.status === 'failed') {
return {
statusCode: 422,
body: {
error: 'Visual difference detected',
details: `Screenshot '${name}' differs from baseline`,
comparison: {
name: comparison.name,
status: comparison.status,
baseline: comparison.baseline,
current: comparison.current,
diff: comparison.diff,
},
tddMode: true,
},
};
}

if (comparison.status === 'baseline-updated') {
return {
statusCode: 200,
body: {
status: 'success',
message: `Baseline updated for ${name}`,
comparison: {
name: comparison.name,
status: comparison.status,
baseline: comparison.baseline,
current: comparison.current,
},
tddMode: true,
},
};
}

if (comparison.status === 'error') {
return {
statusCode: 500,
body: {
error: `Comparison failed: ${comparison.error}`,
tddMode: true,
},
};
}

logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
return {
statusCode: 200,
body: {
success: true,
comparison: {
name: comparison.name,
status: comparison.status,
},
tddMode: true,
},
};
};

const getScreenshotCount = buildId => {
const build = builds.get(buildId);
return build ? build.screenshots.length : 0;
};

const finishBuild = async buildId => {
const build = builds.get(buildId);
if (!build) {
throw new Error(`Build ${buildId} not found`);
}

if (build.screenshots.length === 0) {
throw new Error(
'No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.'
);
}

const results = tddService.printResults();
builds.delete(buildId);

return {
id: buildId,
name: build.name,
tddMode: true,
results,
url: null,
passed: results.failed === 0 && results.errors === 0,
};
};

const cleanup = () => {
builds.clear();
logger.debug('TDD handler cleanup completed');
};

return {
initialize,
registerBuild,
handleScreenshot,
getScreenshotCount,
finishBuild,
cleanup,
};
};
Loading
Loading