@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.
- package/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/finalize.js +41 -15
- package/dist/commands/login.js +7 -6
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +5 -4
- package/dist/commands/run.js +158 -90
- package/dist/commands/status.js +22 -18
- package/dist/commands/tdd.js +105 -78
- package/dist/commands/upload.js +61 -26
- package/dist/commands/whoami.js +4 -4
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/report-generator/core.js +315 -0
- package/dist/report-generator/index.js +8 -0
- package/dist/report-generator/operations.js +196 -0
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +80 -48
- package/dist/server-manager/core.js +183 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +208 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/static-report-generator.js +21 -163
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1081 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/package.json +7 -12
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- 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,
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
// If process was killed by SIGINT, treat as interruption
|
|
121
|
+
proc.on('exit', (code, signal) => {
|
|
280
122
|
if (signal === 'SIGINT') {
|
|
281
|
-
reject(
|
|
123
|
+
reject(this.deps.createError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
|
|
282
124
|
} else if (code !== 0) {
|
|
283
|
-
reject(
|
|
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
|
-
|
|
292
|
-
this.testProcess
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
}
|