@vizzly-testing/cli 0.19.2 → 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/client/index.js +0 -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 -249
- 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/types/client.d.ts +4 -2
- package/dist/types/index.d.ts +5 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/config-schema.js +8 -3
- 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 -1429
|
@@ -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,264 +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) {
|
|
193
|
-
buildPayload.metadata = {
|
|
194
|
-
comparison: {
|
|
195
|
-
threshold: this.config.comparison.threshold
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
const buildResult = await apiService.createBuild(buildPayload);
|
|
200
|
-
output.debug('build', `created ${buildResult.id}`);
|
|
201
|
-
|
|
202
|
-
// Emit build created event
|
|
203
|
-
this.emit('build-created', {
|
|
204
|
-
buildId: buildResult.id,
|
|
205
|
-
url: buildResult.url,
|
|
206
|
-
name: buildResult.name || options.buildName
|
|
207
|
-
});
|
|
208
|
-
return buildResult.id;
|
|
209
|
-
} else {
|
|
210
|
-
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
|
|
211
91
|
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
async createApiService() {
|
|
215
|
-
if (!this.config.apiKey) return null;
|
|
216
|
-
const {
|
|
217
|
-
ApiService
|
|
218
|
-
} = await import('./api-service.js');
|
|
219
|
-
return new ApiService({
|
|
220
|
-
...this.config,
|
|
221
|
-
command: 'run',
|
|
222
|
-
uploadAll: this.config.uploadAll
|
|
223
92
|
});
|
|
224
93
|
}
|
|
225
94
|
async finalizeBuild(buildId, isTddMode, success, executionTime) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
} else {
|
|
239
|
-
// API mode: flush uploads first, then finalize build
|
|
240
|
-
if (this.serverManager.server?.finishBuild) {
|
|
241
|
-
await this.serverManager.server.finishBuild(buildId);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Then update build status via API
|
|
245
|
-
const apiService = await this.createApiService();
|
|
246
|
-
if (apiService) {
|
|
247
|
-
await apiService.finalizeBuild(buildId, success, executionTime);
|
|
248
|
-
output.debug('build', 'finalized via api', {
|
|
249
|
-
success
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
output.warn(`No API service available to finalize build ${buildId}`);
|
|
253
|
-
}
|
|
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)
|
|
254
107
|
}
|
|
255
|
-
}
|
|
256
|
-
// Don't fail the entire run if build finalization fails
|
|
257
|
-
output.warn(`Failed to finalize build ${buildId}:`, error.message);
|
|
258
|
-
// Emit event for UI handling
|
|
259
|
-
this.emit('build-finalize-failed', {
|
|
260
|
-
buildId,
|
|
261
|
-
error: error.message,
|
|
262
|
-
stack: error.stack
|
|
263
|
-
});
|
|
264
|
-
}
|
|
108
|
+
});
|
|
265
109
|
}
|
|
266
110
|
async executeTestCommand(testCommand, env) {
|
|
267
111
|
return new Promise((resolve, reject) => {
|
|
268
|
-
|
|
269
|
-
this.testProcess = spawn(testCommand, {
|
|
112
|
+
let proc = this.deps.spawn(testCommand, {
|
|
270
113
|
env,
|
|
271
114
|
stdio: 'inherit',
|
|
272
115
|
shell: true
|
|
273
116
|
});
|
|
274
|
-
this.testProcess
|
|
275
|
-
|
|
117
|
+
this.testProcess = proc;
|
|
118
|
+
proc.on('error', error => {
|
|
119
|
+
reject(this.deps.createError(`Failed to run test command: ${error.message}`, 'TEST_COMMAND_FAILED'));
|
|
276
120
|
});
|
|
277
|
-
|
|
278
|
-
// If process was killed by SIGINT, treat as interruption
|
|
121
|
+
proc.on('exit', (code, signal) => {
|
|
279
122
|
if (signal === 'SIGINT') {
|
|
280
|
-
reject(
|
|
123
|
+
reject(this.deps.createError('Test command was interrupted', 'TEST_COMMAND_INTERRUPTED'));
|
|
281
124
|
} else if (code !== 0) {
|
|
282
|
-
reject(
|
|
125
|
+
reject(this.deps.createError(`Test command exited with code ${code}`, 'TEST_COMMAND_FAILED'));
|
|
283
126
|
} else {
|
|
284
127
|
resolve();
|
|
285
128
|
}
|
|
@@ -287,13 +130,11 @@ export class TestRunner extends EventEmitter {
|
|
|
287
130
|
});
|
|
288
131
|
}
|
|
289
132
|
async cancel() {
|
|
290
|
-
|
|
291
|
-
this.testProcess
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
await this.serverManager.stop();
|
|
297
|
-
}
|
|
133
|
+
await cancelTests({
|
|
134
|
+
testProcess: this.testProcess,
|
|
135
|
+
deps: {
|
|
136
|
+
serverManager: this.serverManager
|
|
137
|
+
}
|
|
138
|
+
});
|
|
298
139
|
}
|
|
299
140
|
}
|