@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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.
Files changed (72) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/commands/doctor.js +3 -3
  11. package/dist/commands/finalize.js +41 -15
  12. package/dist/commands/login.js +7 -6
  13. package/dist/commands/logout.js +4 -4
  14. package/dist/commands/project.js +5 -4
  15. package/dist/commands/run.js +158 -90
  16. package/dist/commands/status.js +22 -18
  17. package/dist/commands/tdd.js +105 -78
  18. package/dist/commands/upload.js +61 -26
  19. package/dist/commands/whoami.js +4 -4
  20. package/dist/config/core.js +438 -0
  21. package/dist/config/index.js +13 -0
  22. package/dist/config/operations.js +327 -0
  23. package/dist/index.js +1 -1
  24. package/dist/project/core.js +295 -0
  25. package/dist/project/index.js +13 -0
  26. package/dist/project/operations.js +393 -0
  27. package/dist/report-generator/core.js +315 -0
  28. package/dist/report-generator/index.js +8 -0
  29. package/dist/report-generator/operations.js +196 -0
  30. package/dist/reporter/reporter-bundle.iife.js +16 -16
  31. package/dist/screenshot-server/core.js +157 -0
  32. package/dist/screenshot-server/index.js +11 -0
  33. package/dist/screenshot-server/operations.js +183 -0
  34. package/dist/sdk/index.js +3 -2
  35. package/dist/server/handlers/api-handler.js +14 -5
  36. package/dist/server/handlers/tdd-handler.js +80 -48
  37. package/dist/server-manager/core.js +183 -0
  38. package/dist/server-manager/index.js +81 -0
  39. package/dist/server-manager/operations.js +208 -0
  40. package/dist/services/build-manager.js +2 -69
  41. package/dist/services/index.js +21 -48
  42. package/dist/services/screenshot-server.js +40 -74
  43. package/dist/services/server-manager.js +45 -80
  44. package/dist/services/static-report-generator.js +21 -163
  45. package/dist/services/test-runner.js +90 -250
  46. package/dist/services/uploader.js +56 -358
  47. package/dist/tdd/core/hotspot-coverage.js +112 -0
  48. package/dist/tdd/core/signature.js +101 -0
  49. package/dist/tdd/index.js +19 -0
  50. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  51. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  52. package/dist/tdd/services/baseline-downloader.js +151 -0
  53. package/dist/tdd/services/baseline-manager.js +166 -0
  54. package/dist/tdd/services/comparison-service.js +230 -0
  55. package/dist/tdd/services/hotspot-service.js +71 -0
  56. package/dist/tdd/services/result-service.js +123 -0
  57. package/dist/tdd/tdd-service.js +1081 -0
  58. package/dist/test-runner/core.js +255 -0
  59. package/dist/test-runner/index.js +13 -0
  60. package/dist/test-runner/operations.js +483 -0
  61. package/dist/uploader/core.js +396 -0
  62. package/dist/uploader/index.js +11 -0
  63. package/dist/uploader/operations.js +412 -0
  64. package/package.json +7 -12
  65. package/dist/services/api-service.js +0 -412
  66. package/dist/services/auth-service.js +0 -226
  67. package/dist/services/config-service.js +0 -369
  68. package/dist/services/html-report-generator.js +0 -455
  69. package/dist/services/project-service.js +0 -326
  70. package/dist/services/report-generator/report.css +0 -411
  71. package/dist/services/report-generator/viewer.js +0 -102
  72. package/dist/services/tdd-service.js +0 -1437
@@ -1,65 +1,118 @@
1
- import { createServices } from '../services/index.js';
2
- import { loadConfig } from '../utils/config-loader.js';
3
- import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
4
- import * as output from '../utils/output.js';
1
+ /**
2
+ * Run command implementation
3
+ * Uses functional operations directly - no class wrappers needed
4
+ */
5
+
6
+ import { spawn as defaultSpawn } from 'node:child_process';
7
+ import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild } from '../api/index.js';
8
+ import { VizzlyError } from '../errors/vizzly-error.js';
9
+ import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
10
+ import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
11
+ import { createUploader as defaultCreateUploader } from '../services/uploader.js';
12
+ import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } from '../test-runner/index.js';
13
+ import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
14
+ import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
15
+ import * as defaultOutput from '../utils/output.js';
5
16
 
6
17
  /**
7
18
  * Run command implementation
8
19
  * @param {string} testCommand - Test command to execute
9
20
  * @param {Object} options - Command options
10
21
  * @param {Object} globalOptions - Global CLI options
22
+ * @param {Object} deps - Dependencies for testing
11
23
  */
12
- export async function runCommand(testCommand, options = {}, globalOptions = {}) {
24
+ export async function runCommand(testCommand, options = {}, globalOptions = {}, deps = {}) {
25
+ let {
26
+ loadConfig = defaultLoadConfig,
27
+ createApiClient = defaultCreateApiClient,
28
+ createApiBuild = defaultCreateApiBuild,
29
+ finalizeApiBuild = defaultFinalizeApiBuild,
30
+ getBuild = defaultGetBuild,
31
+ createServerManager = defaultCreateServerManager,
32
+ createBuildObject = defaultCreateBuildObject,
33
+ createUploader = defaultCreateUploader,
34
+ finalizeBuild = defaultFinalizeBuild,
35
+ runTests = defaultRunTests,
36
+ detectBranch = defaultDetectBranch,
37
+ detectCommit = defaultDetectCommit,
38
+ detectCommitMessage = defaultDetectCommitMessage,
39
+ detectPullRequestNumber = defaultDetectPullRequestNumber,
40
+ generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
41
+ spawn = defaultSpawn,
42
+ output = defaultOutput,
43
+ exit = code => process.exit(code),
44
+ processOn = (event, handler) => process.on(event, handler),
45
+ processRemoveListener = (event, handler) => process.removeListener(event, handler)
46
+ } = deps;
13
47
  output.configure({
14
48
  json: globalOptions.json,
15
49
  verbose: globalOptions.verbose,
16
50
  color: !globalOptions.noColor
17
51
  });
18
- let testRunner = null;
52
+ let serverManager = null;
53
+ let testProcess = null;
19
54
  let buildId = null;
20
55
  let startTime = null;
21
56
  let isTddMode = false;
57
+ let config = null;
22
58
 
23
59
  // Ensure cleanup on exit
24
- const cleanup = async () => {
60
+ let cleanup = async () => {
25
61
  output.cleanup();
26
62
 
27
- // Cancel test runner (kills process and stops server)
28
- if (testRunner) {
63
+ // Kill test process if running
64
+ if (testProcess && !testProcess.killed) {
65
+ testProcess.kill('SIGKILL');
66
+ }
67
+
68
+ // Stop server
69
+ if (serverManager) {
29
70
  try {
30
- await testRunner.cancel();
71
+ await serverManager.stop();
31
72
  } catch {
32
73
  // Silent fail
33
74
  }
34
75
  }
35
76
 
36
77
  // Finalize build if we have one
37
- if (testRunner && buildId) {
78
+ if (buildId && config) {
38
79
  try {
39
- const executionTime = Date.now() - (startTime || Date.now());
40
- await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
80
+ let executionTime = Date.now() - (startTime || Date.now());
81
+ await finalizeBuild({
82
+ buildId,
83
+ tdd: isTddMode,
84
+ success: false,
85
+ executionTime,
86
+ config,
87
+ deps: {
88
+ serverManager,
89
+ createApiClient,
90
+ finalizeApiBuild,
91
+ output
92
+ }
93
+ });
41
94
  } catch {
42
95
  // Silent fail on cleanup
43
96
  }
44
97
  }
45
98
  };
46
- const sigintHandler = async () => {
99
+ let sigintHandler = async () => {
47
100
  await cleanup();
48
- process.exit(1);
101
+ exit(1);
49
102
  };
50
- const exitHandler = () => output.cleanup();
51
- process.on('SIGINT', sigintHandler);
52
- process.on('exit', exitHandler);
103
+ let exitHandler = () => output.cleanup();
104
+ processOn('SIGINT', sigintHandler);
105
+ processOn('exit', exitHandler);
53
106
  try {
54
107
  // Load configuration with CLI overrides
55
- const allOptions = {
108
+ let allOptions = {
56
109
  ...globalOptions,
57
110
  ...options
58
111
  };
59
112
  output.debug('[RUN] Loading config', {
60
113
  hasToken: !!allOptions.token
61
114
  });
62
- const config = await loadConfig(globalOptions.config, allOptions);
115
+ config = await loadConfig(globalOptions.config, allOptions);
63
116
  output.debug('[RUN] Config loaded', {
64
117
  hasApiKey: !!config.apiKey,
65
118
  apiKeyPrefix: config.apiKey ? `${config.apiKey.substring(0, 8)}***` : 'NONE'
@@ -78,15 +131,19 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
78
131
  // Validate API token (unless --allow-no-token is set)
79
132
  if (!config.apiKey && !config.allowNoToken) {
80
133
  output.error('API token required. Use --token, set VIZZLY_TOKEN environment variable, or use --allow-no-token to run without uploading');
81
- process.exit(1);
134
+ exit(1);
135
+ return {
136
+ success: false,
137
+ reason: 'no-api-key'
138
+ };
82
139
  }
83
140
 
84
141
  // Collect git metadata and build info
85
- const branch = await detectBranch(options.branch);
86
- const commit = await detectCommit(options.commit);
87
- const message = options.message || (await detectCommitMessage());
88
- const buildName = await generateBuildNameWithGit(options.buildName);
89
- const pullRequestNumber = detectPullRequestNumber();
142
+ let branch = await detectBranch(options.branch);
143
+ let commit = await detectCommit(options.commit);
144
+ let message = options.message || (await detectCommitMessage());
145
+ let buildName = await generateBuildNameWithGit(options.buildName);
146
+ let pullRequestNumber = detectPullRequestNumber();
90
147
  if (globalOptions.verbose) {
91
148
  output.info('Configuration loaded');
92
149
  output.debug('Config details', {
@@ -102,9 +159,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
102
159
  });
103
160
  }
104
161
 
105
- // Create service container and get test runner service
162
+ // Create functional dependencies
106
163
  output.startSpinner('Initializing test runner...');
107
- const configWithVerbose = {
164
+ let configWithVerbose = {
108
165
  ...config,
109
166
  verbose: globalOptions.verbose,
110
167
  uploadAll: options.uploadAll || false
@@ -112,59 +169,29 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
112
169
  output.debug('[RUN] Creating services', {
113
170
  hasApiKey: !!configWithVerbose.apiKey
114
171
  });
115
- const services = createServices(configWithVerbose, 'run');
116
- testRunner = services.testRunner;
117
- output.stopSpinner();
118
172
 
119
- // Track build URL for display
120
- let buildUrl = null;
173
+ // Create server manager (functional object)
174
+ serverManager = createServerManager(configWithVerbose, {});
121
175
 
122
- // Set up event handlers
123
- testRunner.on('progress', progressData => {
124
- const {
125
- message: progressMessage
126
- } = progressData;
127
- output.progress(progressMessage || 'Running tests...');
128
- });
129
- testRunner.on('test-output', data => {
130
- // In non-JSON mode, show test output directly
131
- if (!globalOptions.json) {
132
- output.stopSpinner();
133
- output.print(data.data);
134
- }
135
- });
136
- testRunner.on('server-ready', serverInfo => {
137
- if (globalOptions.verbose) {
138
- output.info(`Screenshot server running on port ${serverInfo.port}`);
139
- output.debug('Server details', serverInfo);
140
- }
141
- });
142
- testRunner.on('screenshot-captured', screenshotInfo => {
143
- output.info(`Vizzly: Screenshot captured - ${screenshotInfo.name}`);
144
- });
145
- testRunner.on('build-created', buildInfo => {
146
- buildUrl = buildInfo.url;
147
- buildId = buildInfo.buildId;
148
- if (globalOptions.verbose) {
149
- output.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
176
+ // Create build manager (functional object)
177
+ let buildManager = {
178
+ async createBuild(buildOptions) {
179
+ return createBuildObject(buildOptions);
150
180
  }
151
- if (buildUrl) {
152
- output.info(`Vizzly: ${buildUrl}`);
153
- }
154
- });
155
- testRunner.on('build-failed', buildError => {
156
- output.error('Failed to create build', buildError);
157
- });
158
- testRunner.on('error', error => {
159
- output.stopSpinner();
160
- output.error('Test runner error occurred', error);
161
- });
162
- testRunner.on('build-finalize-failed', errorInfo => {
163
- output.warn(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
181
+ };
182
+
183
+ // Create uploader for --wait functionality
184
+ let uploader = createUploader({
185
+ ...configWithVerbose,
186
+ command: 'run'
164
187
  });
188
+ output.stopSpinner();
189
+
190
+ // Track build URL for display
191
+ let buildUrl = null;
165
192
 
166
193
  // Prepare run options
167
- const runOptions = {
194
+ let runOptions = {
168
195
  testCommand,
169
196
  port: config.server.port,
170
197
  timeout: config.server.timeout,
@@ -188,7 +215,43 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
188
215
  isTddMode = runOptions.tdd || false;
189
216
  let result;
190
217
  try {
191
- result = await testRunner.run(runOptions);
218
+ result = await runTests({
219
+ runOptions,
220
+ config: configWithVerbose,
221
+ deps: {
222
+ serverManager,
223
+ buildManager,
224
+ spawn: (command, spawnOptions) => {
225
+ let proc = spawn(command, spawnOptions);
226
+ testProcess = proc;
227
+ return proc;
228
+ },
229
+ createApiClient,
230
+ createApiBuild,
231
+ getBuild,
232
+ finalizeApiBuild,
233
+ createError: (msg, code) => new VizzlyError(msg, code),
234
+ output,
235
+ onBuildCreated: data => {
236
+ buildUrl = data.url;
237
+ buildId = data.buildId;
238
+ if (globalOptions.verbose) {
239
+ output.info(`Build created: ${data.buildId}`);
240
+ }
241
+ if (buildUrl) {
242
+ output.info(`Vizzly: ${buildUrl}`);
243
+ }
244
+ },
245
+ onServerReady: data => {
246
+ if (globalOptions.verbose) {
247
+ output.info(`Screenshot server running on port ${data.port}`);
248
+ }
249
+ },
250
+ onFinalizeFailed: data => {
251
+ output.warn(`Failed to finalize build ${data.buildId}: ${data.error}`);
252
+ }
253
+ }
254
+ });
192
255
 
193
256
  // Store buildId for cleanup purposes
194
257
  if (result.buildId) {
@@ -210,8 +273,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
210
273
  // Check if it's a test command failure (as opposed to setup failure)
211
274
  if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
212
275
  // Extract exit code from error message if available
213
- const exitCodeMatch = error.message.match(/exited with code (\d+)/);
214
- const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
276
+ let exitCodeMatch = error.message.match(/exited with code (\d+)/);
277
+ let exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
215
278
  output.error('Test run failed');
216
279
  return {
217
280
  success: false,
@@ -233,10 +296,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
233
296
  if (runOptions.wait) {
234
297
  output.info('Waiting for build completion...');
235
298
  output.startSpinner('Processing comparisons...');
236
- const {
237
- uploader
238
- } = services;
239
- const buildResult = await uploader.waitForBuild(result.buildId);
299
+ let buildResult = await uploader.waitForBuild(result.buildId);
240
300
  output.success('Build processing completed');
241
301
 
242
302
  // Exit with appropriate code based on comparison results
@@ -250,6 +310,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
250
310
  }
251
311
  }
252
312
  output.cleanup();
313
+ return {
314
+ success: true,
315
+ result
316
+ };
253
317
  } catch (error) {
254
318
  output.stopSpinner();
255
319
 
@@ -263,11 +327,15 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
263
327
  errorContext = 'Server startup failed';
264
328
  }
265
329
  output.error(errorContext, error);
266
- process.exit(1);
330
+ exit(1);
331
+ return {
332
+ success: false,
333
+ error
334
+ };
267
335
  } finally {
268
336
  // Remove event listeners to prevent memory leaks
269
- process.removeListener('SIGINT', sigintHandler);
270
- process.removeListener('exit', exitHandler);
337
+ processRemoveListener('SIGINT', sigintHandler);
338
+ processRemoveListener('exit', exitHandler);
271
339
  }
272
340
  }
273
341
 
@@ -277,30 +345,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
277
345
  * @param {Object} options - Command options
278
346
  */
279
347
  export function validateRunOptions(testCommand, options) {
280
- const errors = [];
348
+ let errors = [];
281
349
  if (!testCommand || testCommand.trim() === '') {
282
350
  errors.push('Test command is required');
283
351
  }
284
352
  if (options.port) {
285
- const port = parseInt(options.port, 10);
353
+ let port = parseInt(options.port, 10);
286
354
  if (Number.isNaN(port) || port < 1 || port > 65535) {
287
355
  errors.push('Port must be a valid number between 1 and 65535');
288
356
  }
289
357
  }
290
358
  if (options.timeout) {
291
- const timeout = parseInt(options.timeout, 10);
359
+ let timeout = parseInt(options.timeout, 10);
292
360
  if (Number.isNaN(timeout) || timeout < 1000) {
293
361
  errors.push('Timeout must be at least 1000 milliseconds');
294
362
  }
295
363
  }
296
364
  if (options.batchSize !== undefined) {
297
- const n = parseInt(options.batchSize, 10);
365
+ let n = parseInt(options.batchSize, 10);
298
366
  if (!Number.isFinite(n) || n <= 0) {
299
367
  errors.push('Batch size must be a positive integer');
300
368
  }
301
369
  }
302
370
  if (options.uploadTimeout !== undefined) {
303
- const n = parseInt(options.uploadTimeout, 10);
371
+ let n = parseInt(options.uploadTimeout, 10);
304
372
  if (!Number.isFinite(n) || n <= 0) {
305
373
  errors.push('Upload timeout must be a positive integer (milliseconds)');
306
374
  }
@@ -1,4 +1,9 @@
1
- import { createServices } from '../services/index.js';
1
+ /**
2
+ * Status command implementation
3
+ * Uses functional API operations directly
4
+ */
5
+
6
+ import { createApiClient, getBuild } from '../api/index.js';
2
7
  import { loadConfig } from '../utils/config-loader.js';
3
8
  import { getApiUrl } from '../utils/environment-config.js';
4
9
  import * as output from '../utils/output.js';
@@ -19,11 +24,11 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
19
24
  output.info(`Checking status for build: ${buildId}`);
20
25
 
21
26
  // Load configuration with CLI overrides
22
- const allOptions = {
27
+ let allOptions = {
23
28
  ...globalOptions,
24
29
  ...options
25
30
  };
26
- const config = await loadConfig(globalOptions.config, allOptions);
31
+ let config = await loadConfig(globalOptions.config, allOptions);
27
32
 
28
33
  // Validate API token
29
34
  if (!config.apiKey) {
@@ -31,19 +36,18 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
31
36
  process.exit(1);
32
37
  }
33
38
 
34
- // Get API service
39
+ // Get build details via functional API
35
40
  output.startSpinner('Fetching build status...');
36
- const services = createServices(config, 'status');
37
- const {
38
- apiService
39
- } = services;
40
-
41
- // Get build details via unified ApiService
42
- const buildStatus = await apiService.getBuild(buildId);
41
+ let client = createApiClient({
42
+ baseUrl: config.apiUrl,
43
+ token: config.apiKey,
44
+ command: 'status'
45
+ });
46
+ let buildStatus = await getBuild(client, buildId);
43
47
  output.stopSpinner();
44
48
 
45
49
  // Extract build data from API response
46
- const build = buildStatus.build || buildStatus;
50
+ let build = buildStatus.build || buildStatus;
47
51
 
48
52
  // Display build summary
49
53
  output.success(`Build: ${build.name || build.id}`);
@@ -77,15 +81,15 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
77
81
  }
78
82
 
79
83
  // Show build URL if we can construct it
80
- const baseUrl = config.baseUrl || getApiUrl();
84
+ let baseUrl = config.baseUrl || getApiUrl();
81
85
  if (baseUrl && build.project_id) {
82
- const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
86
+ let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
83
87
  output.info(`View Build: ${buildUrl}`);
84
88
  }
85
89
 
86
90
  // Output JSON data for --json mode
87
91
  if (globalOptions.json) {
88
- const statusData = {
92
+ let statusData = {
89
93
  buildId: build.id,
90
94
  status: build.status,
91
95
  name: build.name,
@@ -131,9 +135,9 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
131
135
 
132
136
  // Show progress if build is still processing
133
137
  if (build.status === 'processing' || build.status === 'pending') {
134
- const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
138
+ let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
135
139
  if (totalJobs > 0) {
136
- const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
140
+ let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
137
141
  output.info(`Progress: ${Math.round(progress * 100)}% complete`);
138
142
  }
139
143
  }
@@ -155,7 +159,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
155
159
  * @param {Object} options - Command options
156
160
  */
157
161
  export function validateStatusOptions(buildId) {
158
- const errors = [];
162
+ let errors = [];
159
163
  if (!buildId || buildId.trim() === '') {
160
164
  errors.push('Build ID is required');
161
165
  }