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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 17 additions & 34 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,58 +87,30 @@ program
process.exit(1);
}

await tddCommand(command, options, globalOptions);
const result = await tddCommand(command, options, globalOptions);
if (result && !result.success && result.exitCode > 0) {
process.exit(result.exitCode);
}
});

program
.command('run')
.description('Run tests with Vizzly integration')
.argument('<command>', 'Test command to run')
.option('--tdd', 'Enable TDD mode with auto-reload')
.option('--port <port>', 'Port for screenshot server', '47392')
.option('-b, --build-name <name>', 'Custom build name')
.option('--branch <branch>', 'Git branch override')
.option('--commit <sha>', 'Git commit SHA')
.option('--message <msg>', 'Commit message')
.option('--environment <env>', 'Environment name', 'test')
.option('--threshold <number>', 'Comparison threshold', parseFloat)
.option('--token <token>', 'API token override')
.option('--wait', 'Wait for build completion')
.option('--timeout <ms>', 'Server timeout in milliseconds', '30000')
.option('--eager', 'Create build immediately (default: lazy)')
.option('--allow-no-token', 'Allow running without API token')
.option('--baseline-build <id>', 'Use specific build as baseline')
.option('--baseline-comparison <id>', 'Use specific comparison as baseline')
.option('--upload-all', 'Upload all screenshots without SHA deduplication')
.action(async (command, options) => {
const globalOptions = program.opts();

// Forward --tdd flag to TDD command (shortcut)
if (options.tdd) {
// Forward to tdd command with appropriate options
const tddOptions = {
port: options.port,
branch: options.branch,
environment: options.environment,
threshold: options.threshold,
token: options.token,
timeout: options.timeout,
baselineBuild: options.baselineBuild,
baselineComparison: options.baselineComparison,
allowNoToken: options.allowNoToken,
};

// Validate options using TDD validator
const validationErrors = validateTddOptions(command, tddOptions);
if (validationErrors.length > 0) {
console.error('Validation errors:');
validationErrors.forEach(error => console.error(` - ${error}`));
process.exit(1);
}

await tddCommand(command, tddOptions, globalOptions);
return;
}

// Validate options
const validationErrors = validateRunOptions(command, options);
if (validationErrors.length > 0) {
Expand All @@ -147,7 +119,18 @@ program
process.exit(1);
}

await runCommand(command, options, globalOptions);
try {
const result = await runCommand(command, options, globalOptions);
if (result && !result.success && result.exitCode > 0) {
process.exit(result.exitCode);
}
} catch (error) {
console.error('Command failed:', error.message);
if (globalOptions.verbose) {
console.error('Stack trace:', error.stack);
}
process.exit(1);
}
});

program
Expand Down
2 changes: 1 addition & 1 deletion src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function createSimpleClient(serverUrl) {
buildId: getBuildId(),
name,
image: imageBuffer.toString('base64'),
properties: options.properties || {},
properties: options,
threshold: options.threshold || 0,
variant: options.variant,
fullPage: options.fullPage || false,
Expand Down
46 changes: 38 additions & 8 deletions src/commands/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export async function runCommand(
testCommand,
port: config.server.port,
timeout: config.server.timeout,
tddMode: options.tdd || false,
branch,
commit: commit?.substring(0, 7),
message,
Expand All @@ -93,8 +92,12 @@ export async function runCommand(

// Create service container and get test runner service
ui.startSpinner('Initializing test runner...');
const configWithVerbose = { ...config, verbose: globalOptions.verbose };
const command = options.tdd ? 'tdd' : 'run';
const configWithVerbose = {
...config,
verbose: globalOptions.verbose,
uploadAll: options.uploadAll || false,
};
const command = 'run';
const container = await createServiceContainer(configWithVerbose, command);
testRunner = await container.get('testRunner'); // Assign to outer scope variable
ui.stopSpinner();
Expand Down Expand Up @@ -130,22 +133,36 @@ export async function runCommand(

testRunner.on('build-created', buildInfo => {
buildUrl = buildInfo.url;
// Debug: Log build creation details
if (globalOptions.verbose) {
ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
}
// Use UI for consistent formatting
if (buildUrl) {
ui.info(`Vizzly: ${buildUrl}`);
}
});

testRunner.on('build-failed', buildError => {
ui.error('Failed to create build', buildError);
});

testRunner.on('error', error => {
ui.stopSpinner(); // Stop spinner to ensure error is visible
ui.error('Test runner error occurred', error, 0); // Don't exit immediately, let runner handle it
});

testRunner.on('build-finalize-failed', errorInfo => {
ui.warning(
`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`
);
});

// Prepare run options
const runOptions = {
testCommand,
port: config.server.port,
timeout: config.server.timeout,
tdd: options.tdd || false,
buildName,
branch,
commit,
Expand All @@ -154,9 +171,8 @@ export async function runCommand(
threshold: config.comparison.threshold,
eager: config.eager || false,
allowNoToken: config.allowNoToken || false,
baselineBuildId: config.baselineBuildId,
baselineComparisonId: config.baselineComparisonId,
wait: config.wait || options.wait || false,
uploadAll: options.uploadAll || false,
};

// Start test run
Expand Down Expand Up @@ -194,15 +210,29 @@ export async function runCommand(
ui.error(
`${buildResult.failedComparisons} visual comparisons failed`,
{},
1
0
);
// Return error status without calling process.exit in tests
return { success: false, exitCode: 1 };
}
}
}

ui.cleanup();
} catch (error) {
ui.error('Test run failed', error);
ui.stopSpinner(); // Ensure spinner is stopped before showing error

// Provide more context about where the error occurred
let errorContext = 'Test run failed';
if (error.message && error.message.includes('build')) {
errorContext = 'Build creation failed';
} else if (error.message && error.message.includes('screenshot')) {
errorContext = 'Screenshot processing failed';
} else if (error.message && error.message.includes('server')) {
errorContext = 'Server startup failed';
}

ui.error(errorContext, error);
} finally {
// Remove event listeners to prevent memory leaks
process.removeListener('SIGINT', sigintHandler);
Expand Down
4 changes: 3 additions & 1 deletion src/commands/tdd.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ export async function tddCommand(
(result.comparisons &&
result.comparisons.some(c => c.status === 'failed'))
) {
ui.error('Visual differences detected in TDD mode', {}, 1);
ui.error('Visual differences detected in TDD mode', {}, 0);
// Return error status without calling process.exit in tests
return { success: false, exitCode: 1 };
}

ui.cleanup();
Expand Down
60 changes: 53 additions & 7 deletions src/commands/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,16 @@ export async function uploadCommand(

// Note: ConsoleUI handles cleanup via global process listeners

let buildId = null;
let config = null;
const uploadStartTime = Date.now();

try {
ui.info('Starting upload process...');

// Load configuration with CLI overrides
const allOptions = { ...globalOptions, ...options };
const config = await loadConfig(globalOptions.config, allOptions);
config = await loadConfig(globalOptions.config, allOptions);

// Validate API token
if (!config.apiKey) {
Expand Down Expand Up @@ -115,19 +119,47 @@ export async function uploadCommand(
current,
total,
phase,
buildId: progressBuildId,
} = progressData;
ui.progress(
progressMessage ||
`${phase || 'Processing'}: ${current || 0}/${total || 0}`,
current,
total
);

// Track buildId when it becomes available
if (progressBuildId) {
buildId = progressBuildId;
}

let displayMessage = progressMessage;
if (!displayMessage && phase) {
if (current !== undefined && total !== undefined) {
displayMessage = `${phase}: ${current}/${total}`;
} else {
displayMessage = phase;
}
}

ui.progress(displayMessage || 'Processing...', current, total);
},
};

// Start upload
ui.progress('Starting upload...');
const result = await uploader.upload(uploadOptions);
buildId = result.buildId; // Ensure we have the buildId

// Mark build as completed
if (result.buildId) {
ui.progress('Finalizing build...');
try {
const apiService = new ApiService({
baseUrl: config.apiUrl,
token: config.apiKey,
command: 'upload',
});
const executionTime = Date.now() - uploadStartTime;
await apiService.finalizeBuild(result.buildId, true, executionTime);
} catch (error) {
ui.warning(`Failed to finalize build: ${error.message}`);
}
}

ui.success('Upload completed successfully');

Expand Down Expand Up @@ -171,6 +203,20 @@ export async function uploadCommand(

ui.cleanup();
} catch (error) {
// Mark build as failed if we have a buildId and config
if (buildId && config) {
try {
const apiService = new ApiService({
baseUrl: config.apiUrl,
token: config.apiKey,
command: 'upload',
});
const executionTime = Date.now() - uploadStartTime;
await apiService.finalizeBuild(buildId, false, executionTime);
} catch {
// Silent fail on cleanup
}
}
ui.error('Upload failed', error);
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/services/api-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class ApiService {
constructor(options = {}) {
this.baseUrl = options.baseUrl || getApiUrl();
this.token = options.token || getApiToken();
this.uploadAll = options.uploadAll || false;

// Build User-Agent string
const command = options.command || 'run'; // Default to 'run' for API service
Expand Down Expand Up @@ -119,7 +120,7 @@ export class ApiService {
return this.request('/api/sdk/builds', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metadata),
body: JSON.stringify({ build: metadata }),
});
}

Expand Down Expand Up @@ -156,7 +157,22 @@ export class ApiService {
* @returns {Promise<Object>} Upload result
*/
async uploadScreenshot(buildId, name, buffer, metadata = {}) {
// Calculate SHA256 of the image
// Skip SHA deduplication entirely if uploadAll flag is set
if (this.uploadAll) {
// Upload directly without SHA calculation or checking
return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
image_data: buffer.toString('base64'),
properties: metadata ?? {},
// No SHA included when bypassing deduplication
}),
});
}

// Normal flow with SHA deduplication
const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');

// Check if this SHA already exists
Expand Down
Loading
Loading