@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,20 +1,41 @@
1
1
  /**
2
2
  * Test Runner Service
3
3
  * Orchestrates the test execution flow
4
+ *
5
+ * This class is a thin wrapper around the functional operations in
6
+ * src/test-runner/. It maintains backwards compatibility while
7
+ * delegating to pure functions for testability.
4
8
  */
5
9
 
6
10
  import { spawn } from 'node:child_process';
7
11
  import { EventEmitter } from 'node:events';
12
+ import { createBuild as createApiBuild, createApiClient, finalizeBuild as finalizeApiBuild, getBuild } from '../api/index.js';
8
13
  import { VizzlyError } from '../errors/vizzly-error.js';
14
+ import { cancelTests, createBuild, finalizeBuild, initializeDaemon, runTests } from '../test-runner/index.js';
9
15
  import * as output from '../utils/output.js';
16
+ import { createBuildObject } from './build-manager.js';
10
17
  export class TestRunner extends EventEmitter {
11
- constructor(config, buildManager, serverManager, tddService) {
18
+ constructor(config, serverManager, options = {}) {
12
19
  super();
13
20
  this.config = config;
14
- this.buildManager = buildManager;
15
21
  this.serverManager = serverManager;
16
- this.tddService = tddService;
17
22
  this.testProcess = null;
23
+
24
+ // Simple buildManager using pure function
25
+ this.buildManager = {
26
+ createBuild: buildOptions => createBuildObject(buildOptions)
27
+ };
28
+
29
+ // Dependency injection for testing - defaults to real implementations
30
+ this.deps = options.deps || {
31
+ spawn,
32
+ createApiClient,
33
+ createApiBuild,
34
+ getBuild,
35
+ finalizeApiBuild,
36
+ output,
37
+ createError: (message, code) => new VizzlyError(message, code)
38
+ };
18
39
  }
19
40
 
20
41
  /**
@@ -22,265 +43,86 @@ export class TestRunner extends EventEmitter {
22
43
  * @param {Object} options - Options for server initialization
23
44
  */
24
45
  async initialize(options) {
25
- const {
26
- tdd,
27
- daemon
28
- } = options;
29
- if (!tdd || !daemon) {
30
- throw new VizzlyError('Initialize method is only for TDD daemon mode', 'INVALID_MODE');
31
- }
32
- try {
33
- // Start server manager for daemon mode
34
- await this.serverManager.start(null, tdd, options.setBaseline);
35
- this.emit('server-ready', {
36
- port: options.port,
37
- mode: 'daemon',
38
- tdd: true
39
- });
40
- } catch (error) {
41
- output.error('Failed to initialize TDD daemon server:', error);
42
- throw error;
43
- }
46
+ await initializeDaemon({
47
+ initOptions: options,
48
+ deps: {
49
+ serverManager: this.serverManager,
50
+ createError: this.deps.createError,
51
+ output: this.deps.output,
52
+ onServerReady: data => this.emit('server-ready', data)
53
+ }
54
+ });
44
55
  }
45
56
  async run(options) {
46
- const {
47
- testCommand,
48
- tdd,
49
- allowNoToken
50
- } = options;
51
- const startTime = Date.now();
52
- let buildId = null;
53
- if (!testCommand) {
54
- throw new VizzlyError('No test command provided', 'TEST_COMMAND_MISSING');
55
- }
56
-
57
- // If no token is allowed and not in TDD mode, just run the command without Vizzly integration
58
- if (allowNoToken && !this.config.apiKey && !tdd) {
59
- const env = {
60
- ...process.env,
61
- VIZZLY_ENABLED: 'false'
62
- };
63
- await this.executeTestCommand(testCommand, env);
64
- return {
65
- testsPassed: 1,
66
- testsFailed: 0,
67
- screenshotsCaptured: 0
68
- };
69
- }
70
- let buildUrl = null;
71
- let screenshotCount = 0;
72
- let testSuccess = false;
73
- let testError = null;
74
- try {
75
- // Create build based on mode
76
- buildId = await this.createBuild(options, tdd);
77
- if (!tdd && buildId) {
78
- // Get build URL for API mode
79
- const apiService = await this.createApiService();
80
- if (apiService) {
81
- try {
82
- const build = await apiService.getBuild(buildId);
83
- buildUrl = build.url;
84
- if (buildUrl) {
85
- output.info(`Build URL: ${buildUrl}`);
86
- }
87
- } catch (error) {
88
- output.debug('build', 'could not retrieve url', {
89
- error: error.message
90
- });
91
- }
92
- }
93
- }
94
-
95
- // Start server with appropriate handler
96
- await this.serverManager.start(buildId, tdd, options.setBaseline);
97
- const env = {
98
- ...process.env,
99
- VIZZLY_SERVER_URL: `http://localhost:${this.config.server.port}`,
100
- VIZZLY_BUILD_ID: buildId,
101
- VIZZLY_ENABLED: 'true',
102
- VIZZLY_SET_BASELINE: options.setBaseline || options['set-baseline'] ? 'true' : 'false'
103
- };
104
- try {
105
- await this.executeTestCommand(testCommand, env);
106
- testSuccess = true;
107
- } catch (error) {
108
- testError = error;
109
- testSuccess = false;
110
- }
111
- } catch (error) {
112
- // Error in setup phase
113
- testError = error;
114
- testSuccess = false;
115
- }
116
-
117
- // Get TDD results before stopping the server (comparisons, screenshot count)
118
- let tddResults = null;
119
- if (tdd) {
120
- try {
121
- tddResults = await this.serverManager.getTddResults();
122
- if (tddResults) {
123
- screenshotCount = tddResults.total || 0;
124
- }
125
- } catch (tddError) {
126
- output.debug('tdd', 'failed to get results', {
127
- error: tddError.message
128
- });
129
- }
130
- }
131
-
132
- // Always finalize the build and stop the server (cleanup phase)
133
- try {
134
- const executionTime = Date.now() - startTime;
135
- if (buildId) {
136
- try {
137
- await this.finalizeBuild(buildId, tdd, testSuccess, executionTime);
138
- } catch (finalizeError) {
139
- output.error('Failed to finalize build:', finalizeError);
140
- }
141
- }
142
-
143
- // In API mode, get actual screenshot count from handler after flush
144
- if (!tdd && this.serverManager.server?.getScreenshotCount) {
145
- screenshotCount = this.serverManager.server.getScreenshotCount(buildId) || 0;
146
- }
147
- } finally {
148
- // Always stop the server, even if finalization fails
149
- try {
150
- await this.serverManager.stop();
151
- } catch (stopError) {
152
- output.error('Failed to stop server:', stopError);
57
+ let result = await runTests({
58
+ runOptions: options,
59
+ config: this.config,
60
+ deps: {
61
+ serverManager: this.serverManager,
62
+ buildManager: this.buildManager,
63
+ spawn: (command, spawnOptions) => {
64
+ let proc = this.deps.spawn(command, spawnOptions);
65
+ this.testProcess = proc;
66
+ return proc;
67
+ },
68
+ createApiClient: this.deps.createApiClient,
69
+ createApiBuild: this.deps.createApiBuild,
70
+ getBuild: this.deps.getBuild,
71
+ finalizeApiBuild: this.deps.finalizeApiBuild,
72
+ createError: this.deps.createError,
73
+ output: this.deps.output,
74
+ onBuildCreated: data => this.emit('build-created', data),
75
+ onServerReady: data => this.emit('server-ready', data),
76
+ onFinalizeFailed: data => this.emit('build-finalize-failed', data)
153
77
  }
154
- }
155
-
156
- // If there was a test error, throw it now (after cleanup)
157
- if (testError) {
158
- output.error('Test run failed:', testError);
159
- throw testError;
160
- }
161
- return {
162
- buildId: buildId,
163
- url: buildUrl,
164
- testsPassed: testSuccess ? 1 : 0,
165
- testsFailed: testSuccess ? 0 : 1,
166
- screenshotsCaptured: screenshotCount,
167
- comparisons: tddResults?.comparisons || null,
168
- failed: (tddResults?.failed || 0) > 0
169
- };
78
+ });
79
+ return result;
170
80
  }
171
81
  async createBuild(options, tdd) {
172
- if (tdd) {
173
- // TDD mode: create local build
174
- const build = await this.buildManager.createBuild(options);
175
- output.debug('build', `created ${build.id.substring(0, 8)}`);
176
- return build.id;
177
- } else {
178
- // API mode: create build via API
179
- const apiService = await this.createApiService();
180
- if (apiService) {
181
- const buildPayload = {
182
- name: options.buildName || `Test Run ${new Date().toISOString()}`,
183
- branch: options.branch || 'main',
184
- environment: options.environment || 'test',
185
- commit_sha: options.commit,
186
- commit_message: options.message,
187
- github_pull_request_number: options.pullRequestNumber,
188
- parallel_id: options.parallelId
189
- };
190
-
191
- // Only include metadata if we have meaningful config to send
192
- if (this.config.comparison?.threshold != null || this.config.comparison?.minClusterSize != null) {
193
- buildPayload.metadata = {
194
- comparison: {
195
- threshold: this.config.comparison.threshold,
196
- minClusterSize: this.config.comparison.minClusterSize
197
- }
198
- };
199
- }
200
- const buildResult = await apiService.createBuild(buildPayload);
201
- output.debug('build', `created ${buildResult.id}`);
202
-
203
- // Emit build created event
204
- this.emit('build-created', {
205
- buildId: buildResult.id,
206
- url: buildResult.url,
207
- name: buildResult.name || options.buildName
208
- });
209
- return buildResult.id;
210
- } else {
211
- throw new VizzlyError('No API key available for build creation', 'API_KEY_MISSING');
82
+ return createBuild({
83
+ runOptions: options,
84
+ tdd,
85
+ config: this.config,
86
+ deps: {
87
+ buildManager: this.buildManager,
88
+ createApiClient: this.deps.createApiClient,
89
+ createApiBuild: this.deps.createApiBuild,
90
+ output: this.deps.output
212
91
  }
213
- }
214
- }
215
- async createApiService() {
216
- if (!this.config.apiKey) return null;
217
- const {
218
- ApiService
219
- } = await import('./api-service.js');
220
- return new ApiService({
221
- ...this.config,
222
- command: 'run',
223
- uploadAll: this.config.uploadAll
224
92
  });
225
93
  }
226
94
  async finalizeBuild(buildId, isTddMode, success, executionTime) {
227
- if (!buildId) {
228
- return;
229
- }
230
- try {
231
- if (isTddMode) {
232
- // TDD mode: use server handler to finalize (local-only)
233
- if (this.serverManager.server?.finishBuild) {
234
- await this.serverManager.server.finishBuild(buildId);
235
- output.debug('build', `finalized`, {
236
- success
237
- });
238
- }
239
- } else {
240
- // API mode: flush uploads first, then finalize build
241
- if (this.serverManager.server?.finishBuild) {
242
- await this.serverManager.server.finishBuild(buildId);
243
- }
244
-
245
- // Then update build status via API
246
- const apiService = await this.createApiService();
247
- if (apiService) {
248
- await apiService.finalizeBuild(buildId, success, executionTime);
249
- output.debug('build', 'finalized via api', {
250
- success
251
- });
252
- } else {
253
- output.warn(`No API service available to finalize build ${buildId}`);
254
- }
95
+ await finalizeBuild({
96
+ buildId,
97
+ tdd: isTddMode,
98
+ success,
99
+ executionTime,
100
+ config: this.config,
101
+ deps: {
102
+ serverManager: this.serverManager,
103
+ createApiClient: this.deps.createApiClient,
104
+ finalizeApiBuild: this.deps.finalizeApiBuild,
105
+ output: this.deps.output,
106
+ onFinalizeFailed: data => this.emit('build-finalize-failed', data)
255
107
  }
256
- } catch (error) {
257
- // Don't fail the entire run if build finalization fails
258
- output.warn(`Failed to finalize build ${buildId}:`, error.message);
259
- // Emit event for UI handling
260
- this.emit('build-finalize-failed', {
261
- buildId,
262
- error: error.message,
263
- stack: error.stack
264
- });
265
- }
108
+ });
266
109
  }
267
110
  async executeTestCommand(testCommand, env) {
268
111
  return new Promise((resolve, reject) => {
269
- // Use shell to execute the full command string
270
- this.testProcess = spawn(testCommand, {
112
+ let proc = this.deps.spawn(testCommand, {
271
113
  env,
272
114
  stdio: 'inherit',
273
115
  shell: true
274
116
  });
275
- this.testProcess.on('error', error => {
276
- reject(new VizzlyError(`Failed to run test command: ${error.message}`), 'TEST_COMMAND_FAILED');
117
+ this.testProcess = proc;
118
+ proc.on('error', error => {
119
+ reject(this.deps.createError(`Failed to run test command: ${error.message}`, 'TEST_COMMAND_FAILED'));
277
120
  });
278
- this.testProcess.on('exit', (code, signal) => {
279
- // If process was killed by SIGINT, treat as interruption
121
+ proc.on('exit', (code, signal) => {
280
122
  if (signal === 'SIGINT') {
281
- reject(new VizzlyError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
123
+ reject(this.deps.createError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
282
124
  } else if (code !== 0) {
283
- reject(new VizzlyError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
125
+ reject(this.deps.createError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
284
126
  } else {
285
127
  resolve();
286
128
  }
@@ -288,13 +130,11 @@ export class TestRunner extends EventEmitter {
288
130
  });
289
131
  }
290
132
  async cancel() {
291
- if (this.testProcess && !this.testProcess.killed) {
292
- this.testProcess.kill('SIGKILL');
293
- }
294
-
295
- // Stop server manager if running
296
- if (this.serverManager) {
297
- await this.serverManager.stop();
298
- }
133
+ await cancelTests({
134
+ testProcess: this.testProcess,
135
+ deps: {
136
+ serverManager: this.serverManager
137
+ }
138
+ });
299
139
  }
300
140
  }