@vizzly-testing/cli 0.3.1 → 0.4.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/README.md +26 -28
- package/dist/cli.js +18 -30
- package/dist/client/index.js +1 -1
- package/dist/commands/run.js +34 -9
- package/dist/commands/tdd.js +6 -1
- package/dist/commands/upload.js +52 -3
- package/dist/server/handlers/api-handler.js +83 -0
- package/dist/server/handlers/tdd-handler.js +138 -0
- package/dist/server/http-server.js +132 -0
- package/dist/services/api-service.js +40 -11
- package/dist/services/server-manager.js +45 -29
- package/dist/services/test-runner.js +64 -69
- package/dist/services/uploader.js +47 -82
- package/dist/types/commands/run.d.ts +4 -1
- package/dist/types/commands/tdd.d.ts +4 -1
- package/dist/types/sdk/index.d.ts +6 -6
- package/dist/types/server/handlers/api-handler.d.ts +49 -0
- package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
- package/dist/types/server/http-server.d.ts +5 -0
- package/dist/types/services/api-service.d.ts +4 -2
- package/dist/types/services/server-manager.d.ts +148 -3
- package/dist/types/services/test-runner.d.ts +1 -0
- package/dist/types/utils/config-helpers.d.ts +1 -1
- package/dist/types/utils/console-ui.d.ts +1 -1
- package/dist/utils/console-ui.js +4 -14
- package/docs/api-reference.md +2 -5
- package/docs/getting-started.md +1 -1
- package/docs/tdd-mode.md +9 -9
- package/docs/test-integration.md +3 -17
- package/docs/upload-command.md +7 -0
- package/package.json +1 -1
- package/dist/screenshot-wrapper.js +0 -68
- package/dist/server/index.js +0 -522
- package/dist/services/service-utils.js +0 -171
- package/dist/types/index.js +0 -153
- package/dist/types/screenshot-wrapper.d.ts +0 -27
- package/dist/types/server/index.d.ts +0 -38
- package/dist/types/services/service-utils.d.ts +0 -45
- package/dist/types/types/index.d.ts +0 -373
- package/dist/types/utils/diagnostics.d.ts +0 -69
- package/dist/types/utils/error-messages.d.ts +0 -42
- package/dist/types/utils/framework-detector.d.ts +0 -5
- package/dist/types/utils/help.d.ts +0 -11
- package/dist/types/utils/image-comparison.d.ts +0 -42
- package/dist/types/utils/package.d.ts +0 -1
- package/dist/types/utils/project-detection.d.ts +0 -19
- package/dist/types/utils/ui-helpers.d.ts +0 -23
- package/dist/utils/diagnostics.js +0 -184
- package/dist/utils/error-messages.js +0 -34
- package/dist/utils/framework-detector.js +0 -40
- package/dist/utils/help.js +0 -66
- package/dist/utils/image-comparison.js +0 -172
- package/dist/utils/package.js +0 -9
- package/dist/utils/project-detection.js +0 -145
- package/dist/utils/ui-helpers.js +0 -86
package/dist/server/index.js
DELETED
|
@@ -1,522 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'http';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import { Buffer } from 'buffer';
|
|
4
|
-
// import { createVizzly } from '../sdk/index.js'; // Commented out until needed
|
|
5
|
-
import { createServiceLogger } from '../utils/logger-factory.js';
|
|
6
|
-
import { TddService } from '../services/tdd-service.js';
|
|
7
|
-
import { colors } from '../utils/colors.js';
|
|
8
|
-
const logger = createServiceLogger('SERVER');
|
|
9
|
-
|
|
10
|
-
// Constants for lazy build creation
|
|
11
|
-
const VIZZLY_LAZY_BUILD_ID = 'lazy';
|
|
12
|
-
export class VizzlyServer {
|
|
13
|
-
constructor({
|
|
14
|
-
port,
|
|
15
|
-
config,
|
|
16
|
-
tddMode = false,
|
|
17
|
-
baselineBuild,
|
|
18
|
-
baselineComparison,
|
|
19
|
-
workingDir,
|
|
20
|
-
buildId = null,
|
|
21
|
-
vizzlyApi = null,
|
|
22
|
-
buildInfo = null,
|
|
23
|
-
emitter = null
|
|
24
|
-
}) {
|
|
25
|
-
this.port = port;
|
|
26
|
-
this.config = config;
|
|
27
|
-
this.builds = new Map();
|
|
28
|
-
this.server = null;
|
|
29
|
-
this.tddMode = tddMode;
|
|
30
|
-
this.baselineBuild = baselineBuild;
|
|
31
|
-
this.baselineComparison = baselineComparison;
|
|
32
|
-
this.tddService = tddMode ? new TddService(config, workingDir) : null;
|
|
33
|
-
this.buildId = buildId;
|
|
34
|
-
this.vizzlyApi = vizzlyApi;
|
|
35
|
-
this.buildInfo = buildInfo; // For lazy build creation
|
|
36
|
-
this.emitter = emitter; // Event emitter for UI updates
|
|
37
|
-
this.vizzlyDisabled = false; // Circuit breaker: disable Vizzly after first 500 error
|
|
38
|
-
}
|
|
39
|
-
async start() {
|
|
40
|
-
// Initialize TDD mode if enabled
|
|
41
|
-
if (this.tddMode && this.tddService) {
|
|
42
|
-
logger.info('🔄 TDD mode enabled - setting up local comparison...');
|
|
43
|
-
|
|
44
|
-
// Try to load existing baseline first
|
|
45
|
-
const baseline = await this.tddService.loadBaseline();
|
|
46
|
-
if (!baseline) {
|
|
47
|
-
// Only try to download if we have an API token
|
|
48
|
-
if (this.config.apiKey) {
|
|
49
|
-
logger.info('📥 No local baseline found, downloading from Vizzly...');
|
|
50
|
-
// Download baseline from the latest passed build
|
|
51
|
-
await this.tddService.downloadBaselines(this.baselineBuild, this.baselineComparison);
|
|
52
|
-
} else {
|
|
53
|
-
logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Register active build if provided
|
|
61
|
-
if (this.buildId) {
|
|
62
|
-
this.builds.set(this.buildId, {
|
|
63
|
-
id: this.buildId,
|
|
64
|
-
name: `Active Build ${this.buildId}`,
|
|
65
|
-
branch: 'current',
|
|
66
|
-
environment: 'test',
|
|
67
|
-
screenshots: [],
|
|
68
|
-
createdAt: Date.now()
|
|
69
|
-
});
|
|
70
|
-
logger.debug(`Registered active build: ${this.buildId}`);
|
|
71
|
-
}
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
this.server = createServer(async (req, res) => {
|
|
74
|
-
try {
|
|
75
|
-
await this.handleRequest(req, res);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
logger.error('Server error:', error);
|
|
78
|
-
res.statusCode = 500;
|
|
79
|
-
res.setHeader('Content-Type', 'application/json');
|
|
80
|
-
res.end(JSON.stringify({
|
|
81
|
-
error: 'Internal server error'
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
this.server.listen(this.port, '127.0.0.1', error => {
|
|
86
|
-
if (error) {
|
|
87
|
-
reject(error);
|
|
88
|
-
} else {
|
|
89
|
-
logger.debug(`HTTP server listening on http://127.0.0.1:${this.port}`);
|
|
90
|
-
resolve();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
this.server.on('error', error => {
|
|
94
|
-
if (error.code === 'EADDRINUSE') {
|
|
95
|
-
reject(new Error(`Port ${this.port} is already in use. Try a different port with --port.`));
|
|
96
|
-
} else {
|
|
97
|
-
reject(error);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async handleRequest(req, res) {
|
|
103
|
-
// Set CORS headers
|
|
104
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
105
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
106
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
107
|
-
res.setHeader('Content-Type', 'application/json');
|
|
108
|
-
if (req.method === 'OPTIONS') {
|
|
109
|
-
res.statusCode = 200;
|
|
110
|
-
res.end();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (req.method === 'GET' && req.url === '/health') {
|
|
114
|
-
res.statusCode = 200;
|
|
115
|
-
res.end(JSON.stringify({
|
|
116
|
-
status: 'ok',
|
|
117
|
-
builds: this.builds.size,
|
|
118
|
-
port: this.port,
|
|
119
|
-
uptime: process.uptime()
|
|
120
|
-
}));
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const screenshotPath = this.config?.server?.screenshotPath || '/screenshot';
|
|
124
|
-
if (req.method === 'POST' && req.url === screenshotPath) {
|
|
125
|
-
await this.handleScreenshot(req, res);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
res.statusCode = 404;
|
|
129
|
-
res.end(JSON.stringify({
|
|
130
|
-
error: 'Not found'
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
async handleScreenshot(req, res) {
|
|
134
|
-
try {
|
|
135
|
-
const body = await this.parseRequestBody(req);
|
|
136
|
-
let {
|
|
137
|
-
buildId,
|
|
138
|
-
name,
|
|
139
|
-
properties,
|
|
140
|
-
image
|
|
141
|
-
} = body;
|
|
142
|
-
if (!buildId || !name || !image) {
|
|
143
|
-
res.statusCode = 400;
|
|
144
|
-
res.end(JSON.stringify({
|
|
145
|
-
error: 'buildId, name, and image are required'
|
|
146
|
-
}));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// If Vizzly has been disabled due to server errors, skip upload and continue tests
|
|
151
|
-
if (this.vizzlyDisabled) {
|
|
152
|
-
logger.debug(`Screenshot captured (Vizzly disabled): ${name}`);
|
|
153
|
-
|
|
154
|
-
// Create a mock build entry to track screenshot count for user feedback
|
|
155
|
-
const mockBuildId = 'disabled-build';
|
|
156
|
-
if (!this.builds.has(mockBuildId)) {
|
|
157
|
-
this.builds.set(mockBuildId, {
|
|
158
|
-
id: mockBuildId,
|
|
159
|
-
name: 'Disabled Build',
|
|
160
|
-
screenshots: [],
|
|
161
|
-
createdAt: Date.now()
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
const mockBuild = this.builds.get(mockBuildId);
|
|
165
|
-
mockBuild.screenshots.push({
|
|
166
|
-
name,
|
|
167
|
-
timestamp: Date.now(),
|
|
168
|
-
disabled: true
|
|
169
|
-
});
|
|
170
|
-
res.statusCode = 200;
|
|
171
|
-
res.end(JSON.stringify({
|
|
172
|
-
success: true,
|
|
173
|
-
disabled: true,
|
|
174
|
-
count: mockBuild.screenshots.length,
|
|
175
|
-
message: `Vizzly disabled - ${mockBuild.screenshots.length} screenshots captured but not uploaded`
|
|
176
|
-
}));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Handle lazy build creation or mapping
|
|
181
|
-
if (buildId === VIZZLY_LAZY_BUILD_ID) {
|
|
182
|
-
if (this.buildId) {
|
|
183
|
-
// Build already created, use existing build ID
|
|
184
|
-
buildId = this.buildId;
|
|
185
|
-
} else if (this.buildInfo && this.vizzlyApi) {
|
|
186
|
-
// Create build now
|
|
187
|
-
const creatingMessage = '🏗️ Creating build (first screenshot captured)...';
|
|
188
|
-
logger.debug(creatingMessage); // Change to debug level
|
|
189
|
-
// Don't emit log event - let the build-created event handle UI updates
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const buildResult = await this.vizzlyApi.createBuild({
|
|
193
|
-
build: {
|
|
194
|
-
name: this.buildInfo.buildName,
|
|
195
|
-
branch: this.buildInfo.branch,
|
|
196
|
-
environment: this.buildInfo.environment || 'test',
|
|
197
|
-
commit_sha: this.buildInfo.commitSha,
|
|
198
|
-
commit_message: this.buildInfo.commitMessage
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
this.buildId = buildResult.id;
|
|
202
|
-
buildId = this.buildId; // Update local variable
|
|
203
|
-
const buildUrl = buildResult.url;
|
|
204
|
-
|
|
205
|
-
// Register the build in our local map
|
|
206
|
-
this.builds.set(this.buildId, {
|
|
207
|
-
id: this.buildId,
|
|
208
|
-
name: this.buildInfo.buildName,
|
|
209
|
-
branch: this.buildInfo.branch,
|
|
210
|
-
environment: 'test',
|
|
211
|
-
screenshots: [],
|
|
212
|
-
createdAt: Date.now()
|
|
213
|
-
});
|
|
214
|
-
const createdMessage = `✅ Build created: ${this.buildInfo.buildName}`;
|
|
215
|
-
const urlMessage = `🔗 Build URL: ${buildUrl}`;
|
|
216
|
-
logger.debug(createdMessage); // Change to debug level
|
|
217
|
-
logger.debug(urlMessage); // Change to debug level
|
|
218
|
-
// Don't emit log events - the build-created event will handle UI
|
|
219
|
-
if (this.emitter) {
|
|
220
|
-
this.emitter.emit('build-created', {
|
|
221
|
-
buildId: this.buildId,
|
|
222
|
-
url: buildUrl,
|
|
223
|
-
name: this.buildInfo.buildName
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Clear buildInfo since we no longer need it
|
|
228
|
-
this.buildInfo = null;
|
|
229
|
-
} catch (buildError) {
|
|
230
|
-
logger.error('Failed to create build:', {
|
|
231
|
-
error: buildError.message,
|
|
232
|
-
code: buildError.code,
|
|
233
|
-
stack: buildError.stack,
|
|
234
|
-
buildInfo: this.buildInfo,
|
|
235
|
-
apiUrl: this.vizzlyApi?.apiUrl,
|
|
236
|
-
hasApiKey: !!this.config.apiKey
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Log additional context for debugging
|
|
240
|
-
if (buildError.response) {
|
|
241
|
-
logger.error('API Response details:', {
|
|
242
|
-
status: buildError.response.status,
|
|
243
|
-
statusText: buildError.response.statusText,
|
|
244
|
-
headers: buildError.response.headers
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Disable Vizzly on any build creation error
|
|
249
|
-
this.vizzlyDisabled = true;
|
|
250
|
-
const disabledMessage = `⚠️ Vizzly disabled due to build creation error: ${buildError.message} - continuing tests without visual testing`;
|
|
251
|
-
logger.warn(disabledMessage);
|
|
252
|
-
if (this.emitter) this.emitter.emit('log', disabledMessage);
|
|
253
|
-
|
|
254
|
-
// Return success to allow tests to continue
|
|
255
|
-
res.statusCode = 200;
|
|
256
|
-
res.end(JSON.stringify({
|
|
257
|
-
success: true,
|
|
258
|
-
disabled: true,
|
|
259
|
-
message: 'Vizzly disabled due to build creation error - screenshot captured but not uploaded'
|
|
260
|
-
}));
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
264
|
-
// No buildInfo available and no existing build - this shouldn't happen in lazy mode
|
|
265
|
-
res.statusCode = 400;
|
|
266
|
-
res.end(JSON.stringify({
|
|
267
|
-
error: 'Build creation failed - lazy mode requires valid configuration'
|
|
268
|
-
}));
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const build = this.builds.get(buildId);
|
|
273
|
-
if (!build) {
|
|
274
|
-
res.statusCode = 404;
|
|
275
|
-
res.end(JSON.stringify({
|
|
276
|
-
error: 'Build not found'
|
|
277
|
-
}));
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Store screenshot data (image is base64 encoded)
|
|
282
|
-
const screenshot = {
|
|
283
|
-
name,
|
|
284
|
-
imageData: image,
|
|
285
|
-
properties: properties || {},
|
|
286
|
-
timestamp: Date.now()
|
|
287
|
-
};
|
|
288
|
-
build.screenshots.push(screenshot);
|
|
289
|
-
|
|
290
|
-
// Log screenshot capture (debug only, don't spam UI)
|
|
291
|
-
logger.debug(`Screenshot captured: ${name}`);
|
|
292
|
-
|
|
293
|
-
// Emit count update instead of individual logs
|
|
294
|
-
if (this.emitter) {
|
|
295
|
-
this.emitter.emit('screenshot-captured', {
|
|
296
|
-
name,
|
|
297
|
-
count: build.screenshots.length
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Handle TDD mode comparison - fail fast on visual differences
|
|
302
|
-
if (this.tddMode && this.tddService) {
|
|
303
|
-
const imageBuffer = Buffer.from(image, 'base64');
|
|
304
|
-
const comparison = await this.tddService.compareScreenshot(name, imageBuffer, properties || {});
|
|
305
|
-
if (comparison.status === 'failed') {
|
|
306
|
-
// Visual difference detected - fail immediately (clean logging for TDD)
|
|
307
|
-
res.statusCode = 422; // Unprocessable Entity
|
|
308
|
-
res.end(JSON.stringify({
|
|
309
|
-
error: 'Visual difference detected',
|
|
310
|
-
details: `Screenshot '${name}' differs from baseline`,
|
|
311
|
-
comparison: {
|
|
312
|
-
name: comparison.name,
|
|
313
|
-
status: comparison.status,
|
|
314
|
-
baseline: comparison.baseline,
|
|
315
|
-
current: comparison.current,
|
|
316
|
-
diff: comparison.diff
|
|
317
|
-
},
|
|
318
|
-
tddMode: true
|
|
319
|
-
}));
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (comparison.status === 'baseline-updated') {
|
|
323
|
-
// Baseline was updated successfully
|
|
324
|
-
res.statusCode = 200;
|
|
325
|
-
res.end(JSON.stringify({
|
|
326
|
-
status: 'success',
|
|
327
|
-
message: `Baseline updated for ${name}`,
|
|
328
|
-
comparison: {
|
|
329
|
-
name: comparison.name,
|
|
330
|
-
status: comparison.status,
|
|
331
|
-
baseline: comparison.baseline,
|
|
332
|
-
current: comparison.current
|
|
333
|
-
},
|
|
334
|
-
tddMode: true
|
|
335
|
-
}));
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (comparison.status === 'error') {
|
|
339
|
-
// Comparison error (clean logging for TDD)
|
|
340
|
-
res.statusCode = 500;
|
|
341
|
-
res.end(JSON.stringify({
|
|
342
|
-
error: `Comparison failed: ${comparison.error}`,
|
|
343
|
-
tddMode: true
|
|
344
|
-
}));
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Success (passed or new)
|
|
349
|
-
logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
|
|
350
|
-
res.statusCode = 200;
|
|
351
|
-
res.end(JSON.stringify({
|
|
352
|
-
success: true,
|
|
353
|
-
comparison: {
|
|
354
|
-
name: comparison.name,
|
|
355
|
-
status: comparison.status
|
|
356
|
-
},
|
|
357
|
-
tddMode: true
|
|
358
|
-
}));
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Non-TDD mode: Upload screenshot immediately to API
|
|
363
|
-
if (this.vizzlyApi && buildId !== VIZZLY_LAZY_BUILD_ID && !this.vizzlyDisabled) {
|
|
364
|
-
try {
|
|
365
|
-
const imageBuffer = Buffer.from(image, 'base64');
|
|
366
|
-
const result = await this.vizzlyApi.uploadScreenshot(buildId, name, imageBuffer, properties ?? {});
|
|
367
|
-
|
|
368
|
-
// Log upload or skip
|
|
369
|
-
if (result.skipped) {
|
|
370
|
-
logger.debug(`Screenshot already exists, skipped: ${name}`);
|
|
371
|
-
} else {
|
|
372
|
-
logger.debug(`Screenshot uploaded: ${name}`);
|
|
373
|
-
}
|
|
374
|
-
if (this.emitter) this.emitter.emit('screenshot-uploaded', {
|
|
375
|
-
name,
|
|
376
|
-
skipped: result.skipped
|
|
377
|
-
});
|
|
378
|
-
} catch (uploadError) {
|
|
379
|
-
logger.error(`❌ Failed to upload screenshot ${name}:`, uploadError.message);
|
|
380
|
-
|
|
381
|
-
// Disable Vizzly on any upload error
|
|
382
|
-
this.vizzlyDisabled = true;
|
|
383
|
-
const disabledMessage = '⚠️ Vizzly disabled due to upload error - continuing tests without visual testing';
|
|
384
|
-
logger.warn(disabledMessage);
|
|
385
|
-
if (this.emitter) this.emitter.emit('log', disabledMessage);
|
|
386
|
-
// Continue anyway - don't fail the test for upload errors
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
logger.debug(`Screenshot received: ${name}`);
|
|
390
|
-
res.statusCode = 200;
|
|
391
|
-
res.end(JSON.stringify({
|
|
392
|
-
success: true,
|
|
393
|
-
count: build.screenshots.length,
|
|
394
|
-
name: screenshot.name,
|
|
395
|
-
tddMode: false
|
|
396
|
-
}));
|
|
397
|
-
} catch (error) {
|
|
398
|
-
logger.error('Screenshot upload error:', error);
|
|
399
|
-
res.statusCode = 500;
|
|
400
|
-
res.end(JSON.stringify({
|
|
401
|
-
error: 'Failed to process screenshot'
|
|
402
|
-
}));
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
async parseRequestBody(req) {
|
|
406
|
-
return new Promise((resolve, reject) => {
|
|
407
|
-
let body = '';
|
|
408
|
-
req.on('data', chunk => {
|
|
409
|
-
body += chunk.toString();
|
|
410
|
-
});
|
|
411
|
-
req.on('end', () => {
|
|
412
|
-
try {
|
|
413
|
-
const data = JSON.parse(body);
|
|
414
|
-
resolve(data);
|
|
415
|
-
} catch {
|
|
416
|
-
reject(new Error('Invalid JSON'));
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
req.on('error', reject);
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
async stop() {
|
|
423
|
-
if (this.server) {
|
|
424
|
-
return new Promise(resolve => {
|
|
425
|
-
this.server.close(() => {
|
|
426
|
-
this.server = null;
|
|
427
|
-
logger.debug('HTTP server stopped');
|
|
428
|
-
resolve();
|
|
429
|
-
});
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Clear builds from memory
|
|
434
|
-
this.builds.clear();
|
|
435
|
-
logger.debug('Cleanup completed');
|
|
436
|
-
}
|
|
437
|
-
async createBuild(options) {
|
|
438
|
-
const buildId = randomUUID();
|
|
439
|
-
const build = {
|
|
440
|
-
id: buildId,
|
|
441
|
-
name: options.name,
|
|
442
|
-
branch: options.branch,
|
|
443
|
-
environment: options.environment,
|
|
444
|
-
screenshots: [],
|
|
445
|
-
createdAt: Date.now()
|
|
446
|
-
};
|
|
447
|
-
this.builds.set(buildId, build);
|
|
448
|
-
logger.debug(`Build created: ${buildId} - ${options.name}`);
|
|
449
|
-
return buildId;
|
|
450
|
-
}
|
|
451
|
-
getScreenshotCount(buildId) {
|
|
452
|
-
const build = this.builds.get(buildId);
|
|
453
|
-
return build ? build.screenshots.length : 0;
|
|
454
|
-
}
|
|
455
|
-
getTotalScreenshotCount() {
|
|
456
|
-
let total = 0;
|
|
457
|
-
for (const build of this.builds.values()) {
|
|
458
|
-
total += build.screenshots.length;
|
|
459
|
-
}
|
|
460
|
-
return total;
|
|
461
|
-
}
|
|
462
|
-
async finishBuild(buildId) {
|
|
463
|
-
const build = this.builds.get(buildId);
|
|
464
|
-
if (!build) {
|
|
465
|
-
throw new Error(`Build ${buildId} not found`);
|
|
466
|
-
}
|
|
467
|
-
if (build.screenshots.length === 0) {
|
|
468
|
-
throw new Error('No screenshots to upload. Make sure your tests are calling the Vizzly screenshot function.');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Handle TDD mode completion
|
|
472
|
-
if (this.tddMode && this.tddService) {
|
|
473
|
-
const results = this.tddService.printResults();
|
|
474
|
-
|
|
475
|
-
// Cleanup this build
|
|
476
|
-
await this.cleanupBuild(buildId);
|
|
477
|
-
|
|
478
|
-
// Return TDD results instead of uploading
|
|
479
|
-
return {
|
|
480
|
-
id: buildId,
|
|
481
|
-
name: build.name,
|
|
482
|
-
tddMode: true,
|
|
483
|
-
results,
|
|
484
|
-
url: null,
|
|
485
|
-
// No URL for TDD mode
|
|
486
|
-
passed: results.failed === 0 && results.errors === 0
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Upload to Vizzly API using existing SDK
|
|
491
|
-
const vizzly = createVizzly(this.config);
|
|
492
|
-
await vizzly.startBuild({
|
|
493
|
-
name: build.name,
|
|
494
|
-
branch: build.branch,
|
|
495
|
-
environment: build.environment
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// Upload each screenshot
|
|
499
|
-
for (const screenshot of build.screenshots) {
|
|
500
|
-
const imageBuffer = Buffer.from(screenshot.imageData, 'base64');
|
|
501
|
-
await vizzly.screenshot({
|
|
502
|
-
name: screenshot.name,
|
|
503
|
-
image: imageBuffer,
|
|
504
|
-
properties: screenshot.properties
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
const result = await vizzly.finishBuild();
|
|
508
|
-
|
|
509
|
-
// Cleanup this build
|
|
510
|
-
await this.cleanupBuild(buildId);
|
|
511
|
-
logger.debug(`Build ${buildId} uploaded successfully as ${result.id}`);
|
|
512
|
-
return result;
|
|
513
|
-
}
|
|
514
|
-
async cleanupBuild(buildId) {
|
|
515
|
-
const build = this.builds.get(buildId);
|
|
516
|
-
if (!build) return;
|
|
517
|
-
|
|
518
|
-
// Remove from memory
|
|
519
|
-
this.builds.delete(buildId);
|
|
520
|
-
logger.debug(`Build ${buildId} cleaned up`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Service Utilities
|
|
3
|
-
*
|
|
4
|
-
* Provides utilities for service composition using higher-order functions
|
|
5
|
-
* and event-based architecture patterns.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { EventEmitter } from 'events';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create an event emitter with enhanced functionality
|
|
12
|
-
* @returns {EventEmitter} Enhanced event emitter
|
|
13
|
-
*/
|
|
14
|
-
export function createEventEmitter() {
|
|
15
|
-
const emitter = new EventEmitter();
|
|
16
|
-
|
|
17
|
-
// Add helper methods
|
|
18
|
-
emitter.emitProgress = (stage, message, data = {}) => {
|
|
19
|
-
const progressData = {
|
|
20
|
-
stage,
|
|
21
|
-
message,
|
|
22
|
-
timestamp: new Date().toISOString(),
|
|
23
|
-
...data
|
|
24
|
-
};
|
|
25
|
-
emitter.emit('progress', progressData);
|
|
26
|
-
};
|
|
27
|
-
emitter.emitError = (error, context = {}) => {
|
|
28
|
-
emitter.emit('error', {
|
|
29
|
-
error: error.message,
|
|
30
|
-
stack: error.stack,
|
|
31
|
-
context,
|
|
32
|
-
timestamp: new Date().toISOString()
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
return emitter;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Create a cleanup manager
|
|
40
|
-
* @returns {Object} Cleanup manager with add/execute methods
|
|
41
|
-
*/
|
|
42
|
-
export function createCleanupManager() {
|
|
43
|
-
const cleanupFunctions = [];
|
|
44
|
-
return {
|
|
45
|
-
add: fn => cleanupFunctions.push(fn),
|
|
46
|
-
execute: async () => {
|
|
47
|
-
for (const fn of cleanupFunctions) {
|
|
48
|
-
try {
|
|
49
|
-
await fn();
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Cleanup error:', error);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
cleanupFunctions.length = 0;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Create signal handlers for graceful shutdown
|
|
61
|
-
* @param {Function} onSignal - Function to call on signal
|
|
62
|
-
* @returns {Function} Cleanup function to remove handlers
|
|
63
|
-
*/
|
|
64
|
-
export function createSignalHandlers(onSignal) {
|
|
65
|
-
const handleSignal = async signal => {
|
|
66
|
-
await onSignal(signal);
|
|
67
|
-
process.exit(signal === 'SIGINT' ? 130 : 1);
|
|
68
|
-
};
|
|
69
|
-
process.once('SIGINT', () => handleSignal('SIGINT'));
|
|
70
|
-
process.once('SIGTERM', () => handleSignal('SIGTERM'));
|
|
71
|
-
return () => {
|
|
72
|
-
process.removeAllListeners('SIGINT');
|
|
73
|
-
process.removeAllListeners('SIGTERM');
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Higher-order function to add error handling to any function
|
|
79
|
-
* @param {Function} fn - Function to wrap
|
|
80
|
-
* @param {Object} options - Error handling options
|
|
81
|
-
* @returns {Function} Wrapped function with error handling
|
|
82
|
-
*/
|
|
83
|
-
export function withErrorHandling(fn, options = {}) {
|
|
84
|
-
return async (...args) => {
|
|
85
|
-
try {
|
|
86
|
-
return await fn(...args);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (options.onError) {
|
|
89
|
-
options.onError(error);
|
|
90
|
-
}
|
|
91
|
-
if (options.rethrow !== false) {
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
return options.defaultReturn;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Higher-order function to add logging to any function
|
|
101
|
-
* @param {Function} fn - Function to wrap
|
|
102
|
-
* @param {Object} logger - Logger instance
|
|
103
|
-
* @param {string} operation - Operation name for logging
|
|
104
|
-
* @returns {Function} Wrapped function with logging
|
|
105
|
-
*/
|
|
106
|
-
export function withLogging(fn, logger, operation) {
|
|
107
|
-
return async (...args) => {
|
|
108
|
-
logger.debug(`Starting ${operation}`);
|
|
109
|
-
const start = Date.now();
|
|
110
|
-
try {
|
|
111
|
-
const result = await fn(...args);
|
|
112
|
-
const duration = Date.now() - start;
|
|
113
|
-
logger.debug(`Completed ${operation} in ${duration}ms`);
|
|
114
|
-
return result;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
const duration = Date.now() - start;
|
|
117
|
-
logger.error(`Failed ${operation} after ${duration}ms:`, error.message);
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Compose multiple functions together
|
|
125
|
-
* @param {...Function} fns - Functions to compose
|
|
126
|
-
* @returns {Function} Composed function
|
|
127
|
-
*/
|
|
128
|
-
export function compose(...fns) {
|
|
129
|
-
return value => fns.reduceRight((acc, fn) => fn(acc), value);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Create a service context with shared functionality
|
|
134
|
-
* @param {Object} config - Service configuration
|
|
135
|
-
* @param {Object} options - Service options
|
|
136
|
-
* @returns {Object} Service context
|
|
137
|
-
*/
|
|
138
|
-
export function createServiceContext(config, options = {}) {
|
|
139
|
-
const emitter = createEventEmitter(options);
|
|
140
|
-
const cleanup = createCleanupManager();
|
|
141
|
-
let isRunning = false;
|
|
142
|
-
const context = {
|
|
143
|
-
config,
|
|
144
|
-
emitter,
|
|
145
|
-
cleanup,
|
|
146
|
-
get isRunning() {
|
|
147
|
-
return isRunning;
|
|
148
|
-
},
|
|
149
|
-
set isRunning(value) {
|
|
150
|
-
isRunning = value;
|
|
151
|
-
},
|
|
152
|
-
// Convenience methods
|
|
153
|
-
emitProgress: emitter.emitProgress,
|
|
154
|
-
emitError: emitter.emitError,
|
|
155
|
-
emit: emitter.emit.bind(emitter),
|
|
156
|
-
on: emitter.on.bind(emitter),
|
|
157
|
-
off: emitter.off.bind(emitter),
|
|
158
|
-
once: emitter.once.bind(emitter),
|
|
159
|
-
// Signal handling
|
|
160
|
-
setupSignalHandlers: () => {
|
|
161
|
-
const removeHandlers = createSignalHandlers(async signal => {
|
|
162
|
-
if (isRunning) {
|
|
163
|
-
emitter.emitProgress('cleanup', `Received ${signal}, cleaning up...`);
|
|
164
|
-
await cleanup.execute();
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
cleanup.add(removeHandlers);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
return context;
|
|
171
|
-
}
|