@vizzly-testing/cli 0.3.2 → 0.5.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 (60) hide show
  1. package/README.md +50 -28
  2. package/dist/cli.js +34 -30
  3. package/dist/client/index.js +1 -1
  4. package/dist/commands/run.js +38 -11
  5. package/dist/commands/tdd.js +30 -18
  6. package/dist/commands/upload.js +56 -5
  7. package/dist/server/handlers/api-handler.js +83 -0
  8. package/dist/server/handlers/tdd-handler.js +138 -0
  9. package/dist/server/http-server.js +132 -0
  10. package/dist/services/api-service.js +22 -2
  11. package/dist/services/server-manager.js +45 -29
  12. package/dist/services/test-runner.js +66 -69
  13. package/dist/services/uploader.js +11 -4
  14. package/dist/types/commands/run.d.ts +4 -1
  15. package/dist/types/commands/tdd.d.ts +5 -1
  16. package/dist/types/sdk/index.d.ts +6 -6
  17. package/dist/types/server/handlers/api-handler.d.ts +49 -0
  18. package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
  19. package/dist/types/server/http-server.d.ts +5 -0
  20. package/dist/types/services/api-service.d.ts +1 -0
  21. package/dist/types/services/server-manager.d.ts +148 -3
  22. package/dist/types/services/test-runner.d.ts +1 -0
  23. package/dist/types/services/uploader.d.ts +2 -1
  24. package/dist/types/utils/ci-env.d.ts +55 -0
  25. package/dist/types/utils/config-helpers.d.ts +1 -1
  26. package/dist/types/utils/console-ui.d.ts +1 -1
  27. package/dist/types/utils/git.d.ts +12 -0
  28. package/dist/utils/ci-env.js +293 -0
  29. package/dist/utils/console-ui.js +4 -14
  30. package/dist/utils/git.js +38 -0
  31. package/docs/api-reference.md +17 -5
  32. package/docs/getting-started.md +1 -1
  33. package/docs/tdd-mode.md +9 -9
  34. package/docs/test-integration.md +9 -17
  35. package/docs/upload-command.md +7 -0
  36. package/package.json +4 -5
  37. package/dist/screenshot-wrapper.js +0 -68
  38. package/dist/server/index.js +0 -522
  39. package/dist/services/service-utils.js +0 -171
  40. package/dist/types/index.js +0 -153
  41. package/dist/types/screenshot-wrapper.d.ts +0 -27
  42. package/dist/types/server/index.d.ts +0 -38
  43. package/dist/types/services/service-utils.d.ts +0 -45
  44. package/dist/types/types/index.d.ts +0 -373
  45. package/dist/types/utils/diagnostics.d.ts +0 -69
  46. package/dist/types/utils/error-messages.d.ts +0 -42
  47. package/dist/types/utils/framework-detector.d.ts +0 -5
  48. package/dist/types/utils/help.d.ts +0 -11
  49. package/dist/types/utils/image-comparison.d.ts +0 -42
  50. package/dist/types/utils/package.d.ts +0 -1
  51. package/dist/types/utils/project-detection.d.ts +0 -19
  52. package/dist/types/utils/ui-helpers.d.ts +0 -23
  53. package/dist/utils/diagnostics.js +0 -184
  54. package/dist/utils/error-messages.js +0 -34
  55. package/dist/utils/framework-detector.js +0 -40
  56. package/dist/utils/help.js +0 -66
  57. package/dist/utils/image-comparison.js +0 -172
  58. package/dist/utils/package.js +0 -9
  59. package/dist/utils/project-detection.js +0 -145
  60. package/dist/utils/ui-helpers.js +0 -86
@@ -1,68 +0,0 @@
1
- /**
2
- * Simple factory for creating Vizzly instances with shared configuration
3
- * Users handle their own screenshots, just pass the buffer to Vizzly
4
- */
5
-
6
- import { createVizzly as createVizzlySDK } from './vizzly.js';
7
-
8
- /**
9
- * Create a factory that pre-configures Vizzly instances
10
- *
11
- * @param {Object} config - Shared configuration
12
- * @param {Object} [config.defaultProperties] - Default metadata for all screenshots
13
- * @param {number} [config.defaultThreshold] - Default comparison threshold
14
- *
15
- * @example
16
- * // test-setup.js - Configure once
17
- * export const createVizzly = vizzlyFactory({
18
- * defaultProperties: {
19
- * framework: 'playwright',
20
- * project: 'web-app'
21
- * }
22
- * });
23
- *
24
- * // my-test.spec.js - Use everywhere
25
- * const vizzly = createVizzly();
26
- *
27
- * const screenshot = await page.screenshot({ fullPage: true }); // Your method
28
- * await vizzly.screenshot({
29
- * name: 'homepage',
30
- * image: screenshot, // Your buffer
31
- * properties: { browser: 'chrome' } // Merges with defaults
32
- * });
33
- */
34
- export function vizzlyFactory(globalConfig) {
35
- const {
36
- defaultProperties = {},
37
- defaultThreshold,
38
- ...vizzlyConfig
39
- } = globalConfig;
40
- return function createVizzly(overrideConfig = {}) {
41
- const vizzly = createVizzlySDK({
42
- ...vizzlyConfig,
43
- ...overrideConfig
44
- });
45
- return {
46
- ...vizzly,
47
- /**
48
- * Take a screenshot with default properties merged in
49
- *
50
- * @param {Object} screenshot - Screenshot object
51
- * @param {string} screenshot.name - Screenshot name
52
- * @param {Buffer} screenshot.image - Image buffer from YOUR screenshot method
53
- * @param {Object} [screenshot.properties] - Additional metadata (merged with defaults)
54
- * @param {number} [screenshot.threshold] - Comparison threshold (defaults to global)
55
- */
56
- async screenshot(screenshot) {
57
- return await vizzly.screenshot({
58
- ...screenshot,
59
- properties: {
60
- ...defaultProperties,
61
- ...screenshot.properties
62
- },
63
- threshold: screenshot.threshold || defaultThreshold
64
- });
65
- }
66
- };
67
- };
68
- }
@@ -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
- }