@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
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Test Runner Operations - Test execution operations with dependency injection
3
+ *
4
+ * Each operation takes its dependencies as parameters:
5
+ * - spawn: child_process.spawn for executing commands
6
+ * - serverManager: for starting/stopping screenshot server
7
+ * - buildManager: for local build management (TDD mode)
8
+ * - apiClient: for API builds and finalization
9
+ * - output: for logging
10
+ *
11
+ * This makes them trivially testable without mocking modules.
12
+ */
13
+
14
+ import { buildApiBuildPayload, buildClientOptions, buildDisabledEnv, buildDisabledRunResult, buildRunResult, buildSpawnOptions, buildTestEnv, hasApiKey, normalizeSetBaseline, shouldDisableVizzly, validateDaemonMode, validateTestCommand } from './core.js';
15
+
16
+ // ============================================================================
17
+ // Build Operations
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Create a build (either locally for TDD or via API)
22
+ * @param {Object} options - Options
23
+ * @param {Object} options.runOptions - Run options (buildName, branch, etc.)
24
+ * @param {boolean} options.tdd - Whether in TDD mode
25
+ * @param {Object} options.config - Configuration object
26
+ * @param {Object} options.deps - Dependencies
27
+ * @param {Object} options.deps.buildManager - Build manager for local builds
28
+ * @param {Function} options.deps.createApiClient - API client factory
29
+ * @param {Function} options.deps.createApiBuild - API build creation function
30
+ * @param {Object} options.deps.output - Output utilities
31
+ * @returns {Promise<string>} Build ID
32
+ */
33
+ export async function createBuild({
34
+ runOptions,
35
+ tdd,
36
+ config,
37
+ deps
38
+ }) {
39
+ let {
40
+ buildManager,
41
+ createApiClient,
42
+ createApiBuild,
43
+ output
44
+ } = deps;
45
+ if (tdd) {
46
+ // TDD mode: create local build
47
+ let build = await buildManager.createBuild(runOptions);
48
+ output.debug('build', `created ${build.id.substring(0, 8)}`);
49
+ return build.id;
50
+ }
51
+
52
+ // API mode: create build via API
53
+ let clientOptions = buildClientOptions(config);
54
+ if (!clientOptions) {
55
+ throw new Error('No API key available for build creation');
56
+ }
57
+ let client = createApiClient(clientOptions);
58
+ let payload = buildApiBuildPayload(runOptions, config.comparison);
59
+ let buildResult = await createApiBuild(client, payload);
60
+ output.debug('build', `created ${buildResult.id}`);
61
+ return buildResult.id;
62
+ }
63
+
64
+ /**
65
+ * Get build URL from API
66
+ * @param {Object} options - Options
67
+ * @param {string} options.buildId - Build ID
68
+ * @param {Object} options.config - Configuration object
69
+ * @param {Object} options.deps - Dependencies
70
+ * @param {Function} options.deps.createApiClient - API client factory
71
+ * @param {Function} options.deps.getBuild - Get build function
72
+ * @param {Object} options.deps.output - Output utilities
73
+ * @returns {Promise<string|null>} Build URL or null
74
+ */
75
+ export async function fetchBuildUrl({
76
+ buildId,
77
+ config,
78
+ deps
79
+ }) {
80
+ let {
81
+ createApiClient,
82
+ getBuild,
83
+ output
84
+ } = deps;
85
+ let clientOptions = buildClientOptions(config);
86
+ if (!clientOptions) {
87
+ return null;
88
+ }
89
+ try {
90
+ let client = createApiClient(clientOptions);
91
+ let build = await getBuild(client, buildId);
92
+ return build.url || null;
93
+ } catch (error) {
94
+ output.debug('build', 'could not retrieve url', {
95
+ error: error.message
96
+ });
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Finalize a build
103
+ * @param {Object} options - Options
104
+ * @param {string} options.buildId - Build ID
105
+ * @param {boolean} options.tdd - Whether in TDD mode
106
+ * @param {boolean} options.success - Whether tests passed
107
+ * @param {number} options.executionTime - Execution time in ms
108
+ * @param {Object} options.config - Configuration object
109
+ * @param {Object} options.deps - Dependencies
110
+ * @param {Object} options.deps.serverManager - Server manager
111
+ * @param {Function} options.deps.createApiClient - API client factory
112
+ * @param {Function} options.deps.finalizeApiBuild - API finalize function
113
+ * @param {Object} options.deps.output - Output utilities
114
+ * @param {Function} [options.deps.onFinalizeFailed] - Callback for finalize failure
115
+ */
116
+ export async function finalizeBuild({
117
+ buildId,
118
+ tdd,
119
+ success,
120
+ executionTime,
121
+ config,
122
+ deps
123
+ }) {
124
+ let {
125
+ serverManager,
126
+ createApiClient,
127
+ finalizeApiBuild,
128
+ output,
129
+ onFinalizeFailed
130
+ } = deps;
131
+ if (!buildId) {
132
+ return;
133
+ }
134
+ try {
135
+ if (tdd) {
136
+ // TDD mode: use server handler to finalize (local-only)
137
+ if (serverManager.server?.finishBuild) {
138
+ await serverManager.server.finishBuild(buildId);
139
+ output.debug('build', 'finalized', {
140
+ success
141
+ });
142
+ }
143
+ } else {
144
+ // API mode: flush uploads first, then finalize build
145
+ if (serverManager.server?.finishBuild) {
146
+ await serverManager.server.finishBuild(buildId);
147
+ }
148
+
149
+ // Then update build status via API
150
+ let clientOptions = buildClientOptions(config);
151
+ if (clientOptions) {
152
+ let client = createApiClient(clientOptions);
153
+ await finalizeApiBuild(client, buildId, success, executionTime);
154
+ output.debug('build', 'finalized via api', {
155
+ success
156
+ });
157
+ } else {
158
+ output.warn(`No API service available to finalize build ${buildId}`);
159
+ }
160
+ }
161
+ } catch (error) {
162
+ // Don't fail the entire run if build finalization fails
163
+ output.warn(`Failed to finalize build ${buildId}:`, error.message);
164
+ if (onFinalizeFailed) {
165
+ onFinalizeFailed({
166
+ buildId,
167
+ error: error.message,
168
+ stack: error.stack
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ // ============================================================================
175
+ // Test Execution Operations
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Execute a test command
180
+ * @param {Object} options - Options
181
+ * @param {string} options.command - Test command to execute
182
+ * @param {Object} options.env - Environment variables
183
+ * @param {Object} options.deps - Dependencies
184
+ * @param {Function} options.deps.spawn - Spawn function
185
+ * @param {Function} options.deps.createError - Error factory
186
+ * @returns {Promise<{ process: Object }>} Spawned process reference
187
+ */
188
+ export function executeTestCommand({
189
+ command,
190
+ env,
191
+ deps
192
+ }) {
193
+ let {
194
+ spawn,
195
+ createError
196
+ } = deps;
197
+ return new Promise((resolve, reject) => {
198
+ let spawnOptions = buildSpawnOptions(env);
199
+ let testProcess = spawn(command, spawnOptions);
200
+ testProcess.on('error', error => {
201
+ reject(createError(`Failed to run test command: ${error.message}`, 'TEST_COMMAND_FAILED'));
202
+ });
203
+ testProcess.on('exit', (code, signal) => {
204
+ if (signal === 'SIGINT') {
205
+ reject(createError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
206
+ } else if (code !== 0) {
207
+ reject(createError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
208
+ } else {
209
+ resolve({
210
+ process: testProcess
211
+ });
212
+ }
213
+ });
214
+
215
+ // Return process reference for cancellation
216
+ resolve.__process = testProcess;
217
+ });
218
+ }
219
+
220
+ // ============================================================================
221
+ // High-Level Run Operations
222
+ // ============================================================================
223
+
224
+ /**
225
+ * Run tests with Vizzly integration
226
+ * @param {Object} options - Options
227
+ * @param {Object} options.runOptions - Run options (testCommand, tdd, etc.)
228
+ * @param {Object} options.config - Configuration object
229
+ * @param {Object} options.deps - Dependencies
230
+ * @returns {Promise<Object>} Run result
231
+ */
232
+ export async function runTests({
233
+ runOptions,
234
+ config,
235
+ deps
236
+ }) {
237
+ let {
238
+ serverManager,
239
+ buildManager,
240
+ spawn,
241
+ createApiClient,
242
+ createApiBuild,
243
+ getBuild,
244
+ finalizeApiBuild,
245
+ createError,
246
+ output,
247
+ onBuildCreated,
248
+ onServerReady,
249
+ onFinalizeFailed
250
+ } = deps;
251
+ let {
252
+ testCommand,
253
+ tdd,
254
+ allowNoToken
255
+ } = runOptions;
256
+ let startTime = Date.now();
257
+
258
+ // Validate test command
259
+ let validation = validateTestCommand(testCommand);
260
+ if (!validation.valid) {
261
+ throw createError(validation.error, 'TEST_COMMAND_MISSING');
262
+ }
263
+
264
+ // Check if we should skip Vizzly integration entirely
265
+ if (shouldDisableVizzly({
266
+ allowNoToken,
267
+ hasApiKey: hasApiKey(config),
268
+ tdd
269
+ })) {
270
+ let env = buildDisabledEnv();
271
+ await executeTestCommand({
272
+ command: testCommand,
273
+ env,
274
+ deps: {
275
+ spawn,
276
+ createError
277
+ }
278
+ });
279
+ return buildDisabledRunResult();
280
+ }
281
+ let buildId = null;
282
+ let buildUrl = null;
283
+ let screenshotCount = 0;
284
+ let testSuccess = false;
285
+ let testError = null;
286
+ try {
287
+ // Create build
288
+ buildId = await createBuild({
289
+ runOptions,
290
+ tdd,
291
+ config,
292
+ deps: {
293
+ buildManager,
294
+ createApiClient,
295
+ createApiBuild,
296
+ output
297
+ }
298
+ });
299
+
300
+ // Get build URL for API mode
301
+ if (!tdd && buildId) {
302
+ buildUrl = await fetchBuildUrl({
303
+ buildId,
304
+ config,
305
+ deps: {
306
+ createApiClient,
307
+ getBuild,
308
+ output
309
+ }
310
+ });
311
+ if (buildUrl) {
312
+ output.info(`Build URL: ${buildUrl}`);
313
+ }
314
+ if (onBuildCreated) {
315
+ onBuildCreated({
316
+ buildId,
317
+ url: buildUrl
318
+ });
319
+ }
320
+ }
321
+
322
+ // Start server
323
+ let setBaseline = normalizeSetBaseline(runOptions);
324
+ await serverManager.start(buildId, tdd, setBaseline);
325
+ if (onServerReady) {
326
+ onServerReady({
327
+ port: config.server?.port,
328
+ buildId,
329
+ tdd
330
+ });
331
+ }
332
+
333
+ // Execute test command
334
+ let env = buildTestEnv({
335
+ port: config.server?.port,
336
+ buildId,
337
+ setBaseline
338
+ });
339
+ try {
340
+ await executeTestCommand({
341
+ command: testCommand,
342
+ env,
343
+ deps: {
344
+ spawn,
345
+ createError
346
+ }
347
+ });
348
+ testSuccess = true;
349
+ } catch (error) {
350
+ testError = error;
351
+ testSuccess = false;
352
+ }
353
+ } catch (error) {
354
+ testError = error;
355
+ testSuccess = false;
356
+ }
357
+
358
+ // Get TDD results before stopping the server
359
+ let tddResults = null;
360
+ if (tdd) {
361
+ try {
362
+ tddResults = await serverManager.getTddResults?.();
363
+ if (tddResults) {
364
+ screenshotCount = tddResults.total || 0;
365
+ }
366
+ } catch (tddError) {
367
+ output.debug('tdd', 'failed to get results', {
368
+ error: tddError.message
369
+ });
370
+ }
371
+ }
372
+
373
+ // Always finalize and cleanup
374
+ try {
375
+ let executionTime = Date.now() - startTime;
376
+ if (buildId) {
377
+ try {
378
+ await finalizeBuild({
379
+ buildId,
380
+ tdd,
381
+ success: testSuccess,
382
+ executionTime,
383
+ config,
384
+ deps: {
385
+ serverManager,
386
+ createApiClient,
387
+ finalizeApiBuild,
388
+ output,
389
+ onFinalizeFailed
390
+ }
391
+ });
392
+ } catch (finalizeError) {
393
+ output.error('Failed to finalize build:', finalizeError);
394
+ }
395
+ }
396
+
397
+ // In API mode, get actual screenshot count from handler after flush
398
+ if (!tdd && serverManager.server?.getScreenshotCount) {
399
+ screenshotCount = serverManager.server.getScreenshotCount(buildId) || 0;
400
+ }
401
+ } finally {
402
+ try {
403
+ await serverManager.stop();
404
+ } catch (stopError) {
405
+ output.error('Failed to stop server:', stopError);
406
+ }
407
+ }
408
+
409
+ // Throw test error after cleanup
410
+ if (testError) {
411
+ output.error('Test run failed:', testError);
412
+ throw testError;
413
+ }
414
+ return buildRunResult({
415
+ buildId,
416
+ buildUrl,
417
+ testSuccess,
418
+ screenshotCount,
419
+ tddResults
420
+ });
421
+ }
422
+
423
+ /**
424
+ * Initialize daemon server (TDD mode only)
425
+ * @param {Object} options - Options
426
+ * @param {Object} options.initOptions - Init options
427
+ * @param {Object} options.deps - Dependencies
428
+ * @param {Object} options.deps.serverManager - Server manager
429
+ * @param {Function} options.deps.createError - Error factory
430
+ * @param {Object} options.deps.output - Output utilities
431
+ * @param {Function} [options.deps.onServerReady] - Server ready callback
432
+ */
433
+ export async function initializeDaemon({
434
+ initOptions,
435
+ deps
436
+ }) {
437
+ let {
438
+ serverManager,
439
+ createError,
440
+ output,
441
+ onServerReady
442
+ } = deps;
443
+ let validation = validateDaemonMode(initOptions);
444
+ if (!validation.valid) {
445
+ throw createError(validation.error, 'INVALID_MODE');
446
+ }
447
+ try {
448
+ let setBaseline = normalizeSetBaseline(initOptions);
449
+ await serverManager.start(null, true, setBaseline);
450
+ if (onServerReady) {
451
+ onServerReady({
452
+ port: initOptions.port,
453
+ mode: 'daemon',
454
+ tdd: true
455
+ });
456
+ }
457
+ } catch (error) {
458
+ output.error('Failed to initialize TDD daemon server:', error);
459
+ throw error;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Cancel running tests
465
+ * @param {Object} options - Options
466
+ * @param {Object|null} options.testProcess - Running test process
467
+ * @param {Object} options.deps - Dependencies
468
+ * @param {Object} options.deps.serverManager - Server manager
469
+ */
470
+ export async function cancelTests({
471
+ testProcess,
472
+ deps
473
+ }) {
474
+ let {
475
+ serverManager
476
+ } = deps;
477
+ if (testProcess && !testProcess.killed) {
478
+ testProcess.kill('SIGKILL');
479
+ }
480
+ if (serverManager) {
481
+ await serverManager.stop();
482
+ }
483
+ }