@vizzly-testing/cli 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -28
- package/dist/cli.js +18 -30
- package/dist/client/index.js +1 -1
- package/dist/commands/run.js +34 -9
- package/dist/commands/tdd.js +6 -1
- package/dist/commands/upload.js +52 -3
- package/dist/server/handlers/api-handler.js +83 -0
- package/dist/server/handlers/tdd-handler.js +138 -0
- package/dist/server/http-server.js +132 -0
- package/dist/services/api-service.js +40 -11
- package/dist/services/server-manager.js +45 -29
- package/dist/services/test-runner.js +64 -69
- package/dist/services/uploader.js +47 -82
- package/dist/types/commands/run.d.ts +4 -1
- package/dist/types/commands/tdd.d.ts +4 -1
- package/dist/types/sdk/index.d.ts +6 -6
- package/dist/types/server/handlers/api-handler.d.ts +49 -0
- package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
- package/dist/types/server/http-server.d.ts +5 -0
- package/dist/types/services/api-service.d.ts +4 -2
- package/dist/types/services/server-manager.d.ts +148 -3
- package/dist/types/services/test-runner.d.ts +1 -0
- package/dist/types/utils/config-helpers.d.ts +1 -1
- package/dist/types/utils/console-ui.d.ts +1 -1
- package/dist/utils/console-ui.js +4 -14
- package/docs/api-reference.md +2 -5
- package/docs/getting-started.md +1 -1
- package/docs/tdd-mode.md +9 -9
- package/docs/test-integration.md +3 -17
- package/docs/upload-command.md +7 -0
- package/package.json +1 -1
- package/dist/screenshot-wrapper.js +0 -68
- package/dist/server/index.js +0 -522
- package/dist/services/service-utils.js +0 -171
- package/dist/types/index.js +0 -153
- package/dist/types/screenshot-wrapper.d.ts +0 -27
- package/dist/types/server/index.d.ts +0 -38
- package/dist/types/services/service-utils.d.ts +0 -45
- package/dist/types/types/index.d.ts +0 -373
- package/dist/types/utils/diagnostics.d.ts +0 -69
- package/dist/types/utils/error-messages.d.ts +0 -42
- package/dist/types/utils/framework-detector.d.ts +0 -5
- package/dist/types/utils/help.d.ts +0 -11
- package/dist/types/utils/image-comparison.d.ts +0 -42
- package/dist/types/utils/package.d.ts +0 -1
- package/dist/types/utils/project-detection.d.ts +0 -19
- package/dist/types/utils/ui-helpers.d.ts +0 -23
- package/dist/utils/diagnostics.js +0 -184
- package/dist/utils/error-messages.js +0 -34
- package/dist/utils/framework-detector.js +0 -40
- package/dist/utils/help.js +0 -66
- package/dist/utils/image-comparison.js +0 -172
- package/dist/utils/package.js +0 -9
- package/dist/utils/project-detection.js +0 -145
- package/dist/utils/ui-helpers.js +0 -86
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ vizzly upload ./screenshots --build-name "Release v1.2.3"
|
|
|
55
55
|
vizzly run "npm test"
|
|
56
56
|
|
|
57
57
|
# Use TDD mode for local development
|
|
58
|
-
vizzly
|
|
58
|
+
vizzly tdd "npm test"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
### In your test code
|
|
@@ -81,15 +81,14 @@ await vizzlyScreenshot('homepage', screenshot, {
|
|
|
81
81
|
```bash
|
|
82
82
|
vizzly upload <directory> # Upload screenshots from directory
|
|
83
83
|
vizzly upload ./screenshots --wait # Wait for processing
|
|
84
|
+
vizzly upload ./screenshots --upload-all # Upload all without deduplication
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
### Run Tests with Integration
|
|
87
88
|
```bash
|
|
88
89
|
vizzly run "npm test" # Run with Vizzly integration
|
|
89
|
-
vizzly run "npm test" --tdd # Local TDD mode
|
|
90
90
|
vizzly run "pytest" --port 3002 # Custom port
|
|
91
91
|
vizzly run "npm test" --wait # Wait for build completion
|
|
92
|
-
vizzly run "npm test" --eager # Create build immediately
|
|
93
92
|
vizzly run "npm test" --allow-no-token # Run without API token
|
|
94
93
|
```
|
|
95
94
|
|
|
@@ -108,19 +107,36 @@ vizzly run "npm test" --allow-no-token # Run without API token
|
|
|
108
107
|
|
|
109
108
|
**Processing Options:**
|
|
110
109
|
- `--wait` - Wait for build completion and exit with appropriate code
|
|
111
|
-
- `--eager` - Create build immediately (default: lazy creation)
|
|
112
110
|
- `--threshold <number>` - Comparison threshold (0-1, default: 0.01)
|
|
113
|
-
- `--batch-size <n>` - Upload batch size used with `--wait`
|
|
114
111
|
- `--upload-timeout <ms>` - Upload wait timeout in ms
|
|
112
|
+
- `--upload-all` - Upload all screenshots without SHA deduplication
|
|
115
113
|
|
|
116
114
|
**Development & Testing:**
|
|
117
|
-
- `--tdd` - Enable TDD mode with local comparisons
|
|
118
115
|
- `--allow-no-token` - Allow running without API token (useful for local development)
|
|
119
116
|
- `--token <token>` - API token override
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
## TDD Command
|
|
119
|
+
|
|
120
|
+
For local visual testing with immediate feedback, use the dedicated `tdd` command:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# First run - creates local baselines
|
|
124
|
+
vizzly tdd "npm test"
|
|
125
|
+
|
|
126
|
+
# Make changes and test - fails if visual differences detected
|
|
127
|
+
vizzly tdd "npm test"
|
|
128
|
+
|
|
129
|
+
# Accept changes as new baseline
|
|
130
|
+
vizzly tdd "npm test" --set-baseline
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**TDD Command Options:**
|
|
134
|
+
- `--set-baseline` - Accept current screenshots as new baseline
|
|
135
|
+
- `--baseline-build <id>` - Use specific build as baseline (requires API token)
|
|
136
|
+
- `--baseline-comparison <id>` - Use specific comparison as baseline (requires API token)
|
|
137
|
+
- `--threshold <number>` - Comparison threshold (0-1, default: 0.1)
|
|
138
|
+
- `--port <port>` - Server port (default: 47392)
|
|
139
|
+
- `--timeout <ms>` - Server timeout (default: 30000)
|
|
124
140
|
|
|
125
141
|
### Setup and Status Commands
|
|
126
142
|
```bash
|
|
@@ -176,25 +192,7 @@ VIZZLY_TOKEN=your-token vizzly doctor --api
|
|
|
176
192
|
vizzly doctor --json
|
|
177
193
|
```
|
|
178
194
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
TDD mode enables fast local development by comparing screenshots locally without uploading to Vizzly:
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
# First run - creates local baselines (no token needed)
|
|
185
|
-
npx vizzly tdd "npm test"
|
|
186
|
-
|
|
187
|
-
# Make changes and test - fails if visual differences detected
|
|
188
|
-
npx vizzly tdd "npm test"
|
|
189
|
-
|
|
190
|
-
# Accept changes as new baseline
|
|
191
|
-
npx vizzly tdd "npm test" --set-baseline
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
- **🐻 Auto-baseline creation**: Creates baselines locally when none exist
|
|
195
|
-
- **🐻 No token required**: Works entirely offline for local development
|
|
196
|
-
- **🐻 Tests fail on differences**: Immediate feedback when visuals change
|
|
197
|
-
- **🐻 Accept changes**: Use `--set-baseline` to update baselines
|
|
195
|
+
The dedicated `tdd` command provides fast local development with immediate visual feedback. See the [TDD Mode Guide](./docs/tdd-mode.md) for complete details on local visual testing.
|
|
198
196
|
|
|
199
197
|
## Configuration
|
|
200
198
|
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
16
16
|
...options
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
-
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').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').action(async (path, options) => {
|
|
19
|
+
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').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('--upload-all', 'Upload all screenshots without SHA deduplication').action(async (path, options) => {
|
|
20
20
|
const globalOptions = program.opts();
|
|
21
21
|
|
|
22
22
|
// Validate options
|
|
@@ -38,37 +38,14 @@ program.command('tdd').description('Run tests in TDD mode with local visual comp
|
|
|
38
38
|
validationErrors.forEach(error => console.error(` - ${error}`));
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
|
-
await tddCommand(command, options, globalOptions);
|
|
41
|
+
const result = await tddCommand(command, options, globalOptions);
|
|
42
|
+
if (result && !result.success && result.exitCode > 0) {
|
|
43
|
+
process.exit(result.exitCode);
|
|
44
|
+
}
|
|
42
45
|
});
|
|
43
|
-
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--
|
|
46
|
+
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').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('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').action(async (command, options) => {
|
|
44
47
|
const globalOptions = program.opts();
|
|
45
48
|
|
|
46
|
-
// Forward --tdd flag to TDD command (shortcut)
|
|
47
|
-
if (options.tdd) {
|
|
48
|
-
// Forward to tdd command with appropriate options
|
|
49
|
-
const tddOptions = {
|
|
50
|
-
port: options.port,
|
|
51
|
-
branch: options.branch,
|
|
52
|
-
environment: options.environment,
|
|
53
|
-
threshold: options.threshold,
|
|
54
|
-
token: options.token,
|
|
55
|
-
timeout: options.timeout,
|
|
56
|
-
baselineBuild: options.baselineBuild,
|
|
57
|
-
baselineComparison: options.baselineComparison,
|
|
58
|
-
allowNoToken: options.allowNoToken
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Validate options using TDD validator
|
|
62
|
-
const validationErrors = validateTddOptions(command, tddOptions);
|
|
63
|
-
if (validationErrors.length > 0) {
|
|
64
|
-
console.error('Validation errors:');
|
|
65
|
-
validationErrors.forEach(error => console.error(` - ${error}`));
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
await tddCommand(command, tddOptions, globalOptions);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
49
|
// Validate options
|
|
73
50
|
const validationErrors = validateRunOptions(command, options);
|
|
74
51
|
if (validationErrors.length > 0) {
|
|
@@ -76,7 +53,18 @@ program.command('run').description('Run tests with Vizzly integration').argument
|
|
|
76
53
|
validationErrors.forEach(error => console.error(` - ${error}`));
|
|
77
54
|
process.exit(1);
|
|
78
55
|
}
|
|
79
|
-
|
|
56
|
+
try {
|
|
57
|
+
const result = await runCommand(command, options, globalOptions);
|
|
58
|
+
if (result && !result.success && result.exitCode > 0) {
|
|
59
|
+
process.exit(result.exitCode);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Command failed:', error.message);
|
|
63
|
+
if (globalOptions.verbose) {
|
|
64
|
+
console.error('Stack trace:', error.stack);
|
|
65
|
+
}
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
80
68
|
});
|
|
81
69
|
program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
|
|
82
70
|
const globalOptions = program.opts();
|
package/dist/client/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function createSimpleClient(serverUrl) {
|
|
|
66
66
|
buildId: getBuildId(),
|
|
67
67
|
name,
|
|
68
68
|
image: imageBuffer.toString('base64'),
|
|
69
|
-
properties: options
|
|
69
|
+
properties: options,
|
|
70
70
|
threshold: options.threshold || 0,
|
|
71
71
|
variant: options.variant,
|
|
72
72
|
fullPage: options.fullPage || false
|
package/dist/commands/run.js
CHANGED
|
@@ -62,7 +62,6 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
62
62
|
testCommand,
|
|
63
63
|
port: config.server.port,
|
|
64
64
|
timeout: config.server.timeout,
|
|
65
|
-
tddMode: options.tdd || false,
|
|
66
65
|
branch,
|
|
67
66
|
commit: commit?.substring(0, 7),
|
|
68
67
|
message,
|
|
@@ -76,9 +75,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
76
75
|
ui.startSpinner('Initializing test runner...');
|
|
77
76
|
const configWithVerbose = {
|
|
78
77
|
...config,
|
|
79
|
-
verbose: globalOptions.verbose
|
|
78
|
+
verbose: globalOptions.verbose,
|
|
79
|
+
uploadAll: options.uploadAll || false
|
|
80
80
|
};
|
|
81
|
-
const command =
|
|
81
|
+
const command = 'run';
|
|
82
82
|
const container = await createServiceContainer(configWithVerbose, command);
|
|
83
83
|
testRunner = await container.get('testRunner'); // Assign to outer scope variable
|
|
84
84
|
ui.stopSpinner();
|
|
@@ -112,21 +112,31 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
112
|
});
|
|
113
113
|
testRunner.on('build-created', buildInfo => {
|
|
114
114
|
buildUrl = buildInfo.url;
|
|
115
|
+
// Debug: Log build creation details
|
|
116
|
+
if (globalOptions.verbose) {
|
|
117
|
+
ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
|
|
118
|
+
}
|
|
115
119
|
// Use UI for consistent formatting
|
|
116
120
|
if (buildUrl) {
|
|
117
121
|
ui.info(`Vizzly: ${buildUrl}`);
|
|
118
122
|
}
|
|
119
123
|
});
|
|
124
|
+
testRunner.on('build-failed', buildError => {
|
|
125
|
+
ui.error('Failed to create build', buildError);
|
|
126
|
+
});
|
|
120
127
|
testRunner.on('error', error => {
|
|
128
|
+
ui.stopSpinner(); // Stop spinner to ensure error is visible
|
|
121
129
|
ui.error('Test runner error occurred', error, 0); // Don't exit immediately, let runner handle it
|
|
122
130
|
});
|
|
131
|
+
testRunner.on('build-finalize-failed', errorInfo => {
|
|
132
|
+
ui.warning(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
|
|
133
|
+
});
|
|
123
134
|
|
|
124
135
|
// Prepare run options
|
|
125
136
|
const runOptions = {
|
|
126
137
|
testCommand,
|
|
127
138
|
port: config.server.port,
|
|
128
139
|
timeout: config.server.timeout,
|
|
129
|
-
tdd: options.tdd || false,
|
|
130
140
|
buildName,
|
|
131
141
|
branch,
|
|
132
142
|
commit,
|
|
@@ -135,9 +145,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
135
145
|
threshold: config.comparison.threshold,
|
|
136
146
|
eager: config.eager || false,
|
|
137
147
|
allowNoToken: config.allowNoToken || false,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
wait: config.wait || options.wait || false
|
|
148
|
+
wait: config.wait || options.wait || false,
|
|
149
|
+
uploadAll: options.uploadAll || false
|
|
141
150
|
};
|
|
142
151
|
|
|
143
152
|
// Start test run
|
|
@@ -172,13 +181,29 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
172
181
|
|
|
173
182
|
// Exit with appropriate code based on comparison results
|
|
174
183
|
if (buildResult.failedComparisons > 0) {
|
|
175
|
-
ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {},
|
|
184
|
+
ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 0);
|
|
185
|
+
// Return error status without calling process.exit in tests
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
exitCode: 1
|
|
189
|
+
};
|
|
176
190
|
}
|
|
177
191
|
}
|
|
178
192
|
}
|
|
179
193
|
ui.cleanup();
|
|
180
194
|
} catch (error) {
|
|
181
|
-
ui.
|
|
195
|
+
ui.stopSpinner(); // Ensure spinner is stopped before showing error
|
|
196
|
+
|
|
197
|
+
// Provide more context about where the error occurred
|
|
198
|
+
let errorContext = 'Test run failed';
|
|
199
|
+
if (error.message && error.message.includes('build')) {
|
|
200
|
+
errorContext = 'Build creation failed';
|
|
201
|
+
} else if (error.message && error.message.includes('screenshot')) {
|
|
202
|
+
errorContext = 'Screenshot processing failed';
|
|
203
|
+
} else if (error.message && error.message.includes('server')) {
|
|
204
|
+
errorContext = 'Server startup failed';
|
|
205
|
+
}
|
|
206
|
+
ui.error(errorContext, error);
|
|
182
207
|
} finally {
|
|
183
208
|
// Remove event listeners to prevent memory leaks
|
|
184
209
|
process.removeListener('SIGINT', sigintHandler);
|
package/dist/commands/tdd.js
CHANGED
|
@@ -168,7 +168,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
168
168
|
|
|
169
169
|
// Exit with appropriate code based on comparison results
|
|
170
170
|
if (result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed')) {
|
|
171
|
-
ui.error('Visual differences detected in TDD mode', {},
|
|
171
|
+
ui.error('Visual differences detected in TDD mode', {}, 0);
|
|
172
|
+
// Return error status without calling process.exit in tests
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
exitCode: 1
|
|
176
|
+
};
|
|
172
177
|
}
|
|
173
178
|
ui.cleanup();
|
|
174
179
|
} catch (error) {
|
package/dist/commands/upload.js
CHANGED
|
@@ -49,6 +49,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
49
49
|
|
|
50
50
|
// Note: ConsoleUI handles cleanup via global process listeners
|
|
51
51
|
|
|
52
|
+
let buildId = null;
|
|
53
|
+
let config = null;
|
|
54
|
+
const uploadStartTime = Date.now();
|
|
52
55
|
try {
|
|
53
56
|
ui.info('Starting upload process...');
|
|
54
57
|
|
|
@@ -57,7 +60,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
57
60
|
...globalOptions,
|
|
58
61
|
...options
|
|
59
62
|
};
|
|
60
|
-
|
|
63
|
+
config = await loadConfig(globalOptions.config, allOptions);
|
|
61
64
|
|
|
62
65
|
// Validate API token
|
|
63
66
|
if (!config.apiKey) {
|
|
@@ -94,21 +97,53 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
94
97
|
message,
|
|
95
98
|
environment: config.build.environment,
|
|
96
99
|
threshold: config.comparison.threshold,
|
|
100
|
+
uploadAll: options.uploadAll || false,
|
|
97
101
|
metadata: options.metadata ? JSON.parse(options.metadata) : {},
|
|
98
102
|
onProgress: progressData => {
|
|
99
103
|
const {
|
|
100
104
|
message: progressMessage,
|
|
101
105
|
current,
|
|
102
106
|
total,
|
|
103
|
-
phase
|
|
107
|
+
phase,
|
|
108
|
+
buildId: progressBuildId
|
|
104
109
|
} = progressData;
|
|
105
|
-
|
|
110
|
+
|
|
111
|
+
// Track buildId when it becomes available
|
|
112
|
+
if (progressBuildId) {
|
|
113
|
+
buildId = progressBuildId;
|
|
114
|
+
}
|
|
115
|
+
let displayMessage = progressMessage;
|
|
116
|
+
if (!displayMessage && phase) {
|
|
117
|
+
if (current !== undefined && total !== undefined) {
|
|
118
|
+
displayMessage = `${phase}: ${current}/${total}`;
|
|
119
|
+
} else {
|
|
120
|
+
displayMessage = phase;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
ui.progress(displayMessage || 'Processing...', current, total);
|
|
106
124
|
}
|
|
107
125
|
};
|
|
108
126
|
|
|
109
127
|
// Start upload
|
|
110
128
|
ui.progress('Starting upload...');
|
|
111
129
|
const result = await uploader.upload(uploadOptions);
|
|
130
|
+
buildId = result.buildId; // Ensure we have the buildId
|
|
131
|
+
|
|
132
|
+
// Mark build as completed
|
|
133
|
+
if (result.buildId) {
|
|
134
|
+
ui.progress('Finalizing build...');
|
|
135
|
+
try {
|
|
136
|
+
const apiService = new ApiService({
|
|
137
|
+
baseUrl: config.apiUrl,
|
|
138
|
+
token: config.apiKey,
|
|
139
|
+
command: 'upload'
|
|
140
|
+
});
|
|
141
|
+
const executionTime = Date.now() - uploadStartTime;
|
|
142
|
+
await apiService.finalizeBuild(result.buildId, true, executionTime);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
ui.warning(`Failed to finalize build: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
112
147
|
ui.success('Upload completed successfully');
|
|
113
148
|
|
|
114
149
|
// Show Vizzly summary
|
|
@@ -138,6 +173,20 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
138
173
|
}
|
|
139
174
|
ui.cleanup();
|
|
140
175
|
} catch (error) {
|
|
176
|
+
// Mark build as failed if we have a buildId and config
|
|
177
|
+
if (buildId && config) {
|
|
178
|
+
try {
|
|
179
|
+
const apiService = new ApiService({
|
|
180
|
+
baseUrl: config.apiUrl,
|
|
181
|
+
token: config.apiKey,
|
|
182
|
+
command: 'upload'
|
|
183
|
+
});
|
|
184
|
+
const executionTime = Date.now() - uploadStartTime;
|
|
185
|
+
await apiService.finalizeBuild(buildId, false, executionTime);
|
|
186
|
+
} catch {
|
|
187
|
+
// Silent fail on cleanup
|
|
188
|
+
}
|
|
189
|
+
}
|
|
141
190
|
ui.error('Upload failed', error);
|
|
142
191
|
}
|
|
143
192
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
3
|
+
const logger = createServiceLogger('API-HANDLER');
|
|
4
|
+
export const createApiHandler = apiService => {
|
|
5
|
+
let vizzlyDisabled = false;
|
|
6
|
+
let screenshotCount = 0;
|
|
7
|
+
const handleScreenshot = async (buildId, name, image, properties = {}) => {
|
|
8
|
+
if (vizzlyDisabled) {
|
|
9
|
+
logger.debug(`Screenshot captured (Vizzly disabled): ${name}`);
|
|
10
|
+
return {
|
|
11
|
+
statusCode: 200,
|
|
12
|
+
body: {
|
|
13
|
+
success: true,
|
|
14
|
+
disabled: true,
|
|
15
|
+
count: ++screenshotCount,
|
|
16
|
+
message: `Vizzly disabled - ${screenshotCount} screenshots captured but not uploaded`
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (!buildId) {
|
|
21
|
+
return {
|
|
22
|
+
statusCode: 400,
|
|
23
|
+
body: {
|
|
24
|
+
error: 'Build ID is required for screenshot upload'
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (!apiService) {
|
|
29
|
+
return {
|
|
30
|
+
statusCode: 500,
|
|
31
|
+
body: {
|
|
32
|
+
error: 'API service not available'
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const imageBuffer = Buffer.from(image, 'base64');
|
|
38
|
+
const result = await apiService.uploadScreenshot(buildId, name, imageBuffer, properties ?? {});
|
|
39
|
+
if (result.skipped) {
|
|
40
|
+
logger.debug(`Screenshot already exists, skipped: ${name}`);
|
|
41
|
+
} else {
|
|
42
|
+
logger.debug(`Screenshot uploaded: ${name}`);
|
|
43
|
+
}
|
|
44
|
+
if (!result.skipped) {
|
|
45
|
+
screenshotCount++;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
statusCode: 200,
|
|
49
|
+
body: {
|
|
50
|
+
success: true,
|
|
51
|
+
name,
|
|
52
|
+
skipped: result.skipped,
|
|
53
|
+
count: screenshotCount
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
} catch (uploadError) {
|
|
57
|
+
logger.error(`❌ Failed to upload screenshot ${name}:`, uploadError.message);
|
|
58
|
+
vizzlyDisabled = true;
|
|
59
|
+
const disabledMessage = '⚠️ Vizzly disabled due to upload error - continuing tests without visual testing';
|
|
60
|
+
logger.warn(disabledMessage);
|
|
61
|
+
return {
|
|
62
|
+
statusCode: 200,
|
|
63
|
+
body: {
|
|
64
|
+
success: true,
|
|
65
|
+
name,
|
|
66
|
+
disabled: true,
|
|
67
|
+
message: disabledMessage
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const getScreenshotCount = () => screenshotCount;
|
|
73
|
+
const cleanup = () => {
|
|
74
|
+
vizzlyDisabled = false;
|
|
75
|
+
screenshotCount = 0;
|
|
76
|
+
logger.debug('API handler cleanup completed');
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
handleScreenshot,
|
|
80
|
+
getScreenshotCount,
|
|
81
|
+
cleanup
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
3
|
+
import { TddService } from '../../services/tdd-service.js';
|
|
4
|
+
import { colors } from '../../utils/colors.js';
|
|
5
|
+
const logger = createServiceLogger('TDD-HANDLER');
|
|
6
|
+
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison) => {
|
|
7
|
+
const tddService = new TddService(config, workingDir);
|
|
8
|
+
const builds = new Map();
|
|
9
|
+
const initialize = async () => {
|
|
10
|
+
logger.info('🔄 TDD mode enabled - setting up local comparison...');
|
|
11
|
+
const baseline = await tddService.loadBaseline();
|
|
12
|
+
if (!baseline) {
|
|
13
|
+
if (config.apiKey) {
|
|
14
|
+
logger.info('📥 No local baseline found, downloading from Vizzly...');
|
|
15
|
+
await tddService.downloadBaselines(baselineBuild, baselineComparison);
|
|
16
|
+
} else {
|
|
17
|
+
logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const registerBuild = buildId => {
|
|
24
|
+
builds.set(buildId, {
|
|
25
|
+
id: buildId,
|
|
26
|
+
name: `TDD Build ${buildId}`,
|
|
27
|
+
branch: 'current',
|
|
28
|
+
environment: 'test',
|
|
29
|
+
screenshots: [],
|
|
30
|
+
createdAt: Date.now()
|
|
31
|
+
});
|
|
32
|
+
logger.debug(`Registered TDD build: ${buildId}`);
|
|
33
|
+
};
|
|
34
|
+
const handleScreenshot = async (buildId, name, image, properties = {}) => {
|
|
35
|
+
const build = builds.get(buildId);
|
|
36
|
+
if (!build) {
|
|
37
|
+
throw new Error(`Build ${buildId} not found`);
|
|
38
|
+
}
|
|
39
|
+
const screenshot = {
|
|
40
|
+
name,
|
|
41
|
+
imageData: image,
|
|
42
|
+
properties,
|
|
43
|
+
timestamp: Date.now()
|
|
44
|
+
};
|
|
45
|
+
build.screenshots.push(screenshot);
|
|
46
|
+
const imageBuffer = Buffer.from(image, 'base64');
|
|
47
|
+
const comparison = await tddService.compareScreenshot(name, imageBuffer, properties);
|
|
48
|
+
if (comparison.status === 'failed') {
|
|
49
|
+
return {
|
|
50
|
+
statusCode: 422,
|
|
51
|
+
body: {
|
|
52
|
+
error: 'Visual difference detected',
|
|
53
|
+
details: `Screenshot '${name}' differs from baseline`,
|
|
54
|
+
comparison: {
|
|
55
|
+
name: comparison.name,
|
|
56
|
+
status: comparison.status,
|
|
57
|
+
baseline: comparison.baseline,
|
|
58
|
+
current: comparison.current,
|
|
59
|
+
diff: comparison.diff
|
|
60
|
+
},
|
|
61
|
+
tddMode: true
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (comparison.status === 'baseline-updated') {
|
|
66
|
+
return {
|
|
67
|
+
statusCode: 200,
|
|
68
|
+
body: {
|
|
69
|
+
status: 'success',
|
|
70
|
+
message: `Baseline updated for ${name}`,
|
|
71
|
+
comparison: {
|
|
72
|
+
name: comparison.name,
|
|
73
|
+
status: comparison.status,
|
|
74
|
+
baseline: comparison.baseline,
|
|
75
|
+
current: comparison.current
|
|
76
|
+
},
|
|
77
|
+
tddMode: true
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (comparison.status === 'error') {
|
|
82
|
+
return {
|
|
83
|
+
statusCode: 500,
|
|
84
|
+
body: {
|
|
85
|
+
error: `Comparison failed: ${comparison.error}`,
|
|
86
|
+
tddMode: true
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
|
|
91
|
+
return {
|
|
92
|
+
statusCode: 200,
|
|
93
|
+
body: {
|
|
94
|
+
success: true,
|
|
95
|
+
comparison: {
|
|
96
|
+
name: comparison.name,
|
|
97
|
+
status: comparison.status
|
|
98
|
+
},
|
|
99
|
+
tddMode: true
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const getScreenshotCount = buildId => {
|
|
104
|
+
const build = builds.get(buildId);
|
|
105
|
+
return build ? build.screenshots.length : 0;
|
|
106
|
+
};
|
|
107
|
+
const finishBuild = async buildId => {
|
|
108
|
+
const build = builds.get(buildId);
|
|
109
|
+
if (!build) {
|
|
110
|
+
throw new Error(`Build ${buildId} not found`);
|
|
111
|
+
}
|
|
112
|
+
if (build.screenshots.length === 0) {
|
|
113
|
+
throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
|
|
114
|
+
}
|
|
115
|
+
const results = tddService.printResults();
|
|
116
|
+
builds.delete(buildId);
|
|
117
|
+
return {
|
|
118
|
+
id: buildId,
|
|
119
|
+
name: build.name,
|
|
120
|
+
tddMode: true,
|
|
121
|
+
results,
|
|
122
|
+
url: null,
|
|
123
|
+
passed: results.failed === 0 && results.errors === 0
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const cleanup = () => {
|
|
127
|
+
builds.clear();
|
|
128
|
+
logger.debug('TDD handler cleanup completed');
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
initialize,
|
|
132
|
+
registerBuild,
|
|
133
|
+
handleScreenshot,
|
|
134
|
+
getScreenshotCount,
|
|
135
|
+
finishBuild,
|
|
136
|
+
cleanup
|
|
137
|
+
};
|
|
138
|
+
};
|