@vizzly-testing/cli 0.20.0 → 0.20.1-beta.1

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 (84) 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 +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -1,455 +0,0 @@
1
- /**
2
- * HTML Report Generator for TDD visual comparison results
3
- * Creates an interactive report with overlay, toggle, and onion skin modes
4
- */
5
-
6
- import { existsSync } from 'node:fs';
7
- import { mkdir, writeFile } from 'node:fs/promises';
8
- import { dirname, join, relative } from 'node:path';
9
- import { fileURLToPath } from 'node:url';
10
- import * as output from '../utils/output.js';
11
- export class HtmlReportGenerator {
12
- constructor(workingDir, config) {
13
- this.workingDir = workingDir;
14
- this.config = config;
15
- this.reportDir = join(workingDir, '.vizzly', 'report');
16
- this.reportPath = join(this.reportDir, 'index.html');
17
-
18
- // Get path to the CSS file that ships with the package
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
- this.cssPath = join(__dirname, 'report-generator', 'report.css');
22
- }
23
-
24
- /**
25
- * Sanitize HTML content to prevent XSS attacks
26
- * @param {string} text - Text to sanitize
27
- * @returns {string} Sanitized text
28
- */
29
- sanitizeHtml(text) {
30
- if (typeof text !== 'string') return '';
31
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
32
- }
33
-
34
- /**
35
- * Sanitize build info object
36
- * @param {Object} buildInfo - Build information to sanitize
37
- * @returns {Object} Sanitized build info
38
- */
39
- sanitizeBuildInfo(buildInfo = {}) {
40
- const sanitized = {};
41
- if (buildInfo.baseline && typeof buildInfo.baseline === 'object') {
42
- sanitized.baseline = {
43
- buildId: this.sanitizeHtml(buildInfo.baseline.buildId || ''),
44
- buildName: this.sanitizeHtml(buildInfo.baseline.buildName || ''),
45
- environment: this.sanitizeHtml(buildInfo.baseline.environment || ''),
46
- branch: this.sanitizeHtml(buildInfo.baseline.branch || '')
47
- };
48
- }
49
- if (typeof buildInfo.threshold === 'number') {
50
- sanitized.threshold = Math.max(0, Math.min(1, buildInfo.threshold));
51
- }
52
- return sanitized;
53
- }
54
-
55
- /**
56
- * Generate HTML report from TDD results
57
- * @param {Object} results - TDD comparison results
58
- * @param {Object} buildInfo - Build information
59
- * @returns {string} Path to generated report
60
- */
61
- async generateReport(results, buildInfo = {}) {
62
- // Validate inputs
63
- if (!results || typeof results !== 'object') {
64
- throw new Error('Invalid results object provided');
65
- }
66
- const {
67
- comparisons = [],
68
- passed = 0,
69
- failed = 0,
70
- total = 0
71
- } = results;
72
-
73
- // Filter only failed comparisons for the report
74
- const failedComparisons = comparisons.filter(comp => comp && comp.status === 'failed');
75
- const reportData = {
76
- buildInfo: {
77
- timestamp: new Date().toISOString(),
78
- ...this.sanitizeBuildInfo(buildInfo)
79
- },
80
- summary: {
81
- total,
82
- passed,
83
- failed,
84
- passRate: total > 0 ? (passed / total * 100).toFixed(1) : '0.0'
85
- },
86
- comparisons: failedComparisons.map(comp => this.processComparison(comp)).filter(Boolean)
87
- };
88
- const htmlContent = this.generateHtmlTemplate(reportData);
89
- try {
90
- // Ensure report directory exists
91
- await mkdir(this.reportDir, {
92
- recursive: true
93
- });
94
- await writeFile(this.reportPath, htmlContent, 'utf8');
95
- output.debug('report', 'generated html report');
96
- return this.reportPath;
97
- } catch (error) {
98
- output.debug('report', 'html generation failed', {
99
- error: error.message
100
- });
101
- throw new Error(`Report generation failed: ${error.message}`);
102
- }
103
- }
104
-
105
- /**
106
- * Process comparison data for HTML report
107
- * @param {Object} comparison - Comparison object
108
- * @returns {Object} Processed comparison data
109
- */
110
- processComparison(comparison) {
111
- if (!comparison || typeof comparison !== 'object') {
112
- output.warn('Invalid comparison object provided');
113
- return null;
114
- }
115
- return {
116
- name: comparison.name || 'unnamed',
117
- status: comparison.status,
118
- baseline: this.getRelativePath(comparison.baseline, this.reportDir),
119
- current: this.getRelativePath(comparison.current, this.reportDir),
120
- diff: this.getRelativePath(comparison.diff, this.reportDir),
121
- threshold: comparison.threshold || 0,
122
- diffPercentage: comparison.diffPercentage || 0
123
- };
124
- }
125
-
126
- /**
127
- * Get relative path from report directory to image file
128
- * @param {string} imagePath - Absolute path to image
129
- * @param {string} reportDir - Report directory path
130
- * @returns {string|null} Relative path or null if invalid
131
- */
132
- getRelativePath(imagePath, reportDir) {
133
- if (!imagePath || !existsSync(imagePath)) {
134
- return null;
135
- }
136
- return relative(reportDir, imagePath);
137
- }
138
-
139
- /**
140
- * Generate the complete HTML template
141
- * @param {Object} data - Report data
142
- * @returns {string} HTML content
143
- */
144
- generateHtmlTemplate(data) {
145
- return `<!DOCTYPE html>
146
- <html lang="en">
147
- <head>
148
- <meta charset="UTF-8">
149
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
150
- <title>Vizzly TDD Report</title>
151
- <link rel="stylesheet" href="file://${this.cssPath}">
152
- </head>
153
- <body>
154
- <div class="container">
155
- <header class="header">
156
- <h1>🐻 Vizzly Visual Testing Report</h1>
157
- <div class="summary">
158
- <div class="stat">
159
- <span class="stat-number">${data.summary.total}</span>
160
- <span class="stat-label">Total</span>
161
- </div>
162
- <div class="stat passed">
163
- <span class="stat-number">${data.summary.passed}</span>
164
- <span class="stat-label">Passed</span>
165
- </div>
166
- <div class="stat failed">
167
- <span class="stat-number">${data.summary.failed}</span>
168
- <span class="stat-label">Failed</span>
169
- </div>
170
- <div class="stat">
171
- <span class="stat-number">${data.summary.passRate}%</span>
172
- <span class="stat-label">Pass Rate</span>
173
- </div>
174
- </div>
175
- <div class="build-info">
176
- <span>Generated: ${new Date(data.buildInfo.timestamp).toLocaleString()}</span>
177
- </div>
178
- </header>
179
-
180
- ${data.comparisons.length === 0 ? '<div class="no-failures">🎉 All tests passed! No visual differences detected.</div>' : `<main class="comparisons">
181
- ${data.comparisons.map(comp => this.generateComparisonHtml(comp)).join('')}
182
- </main>`}
183
- </div>
184
-
185
- <script>
186
- document.addEventListener('DOMContentLoaded', function () {
187
- // Handle view mode switching
188
- document.querySelectorAll('.view-mode-btn').forEach(btn => {
189
- btn.addEventListener('click', function () {
190
- let comparison = this.closest('.comparison');
191
- let mode = this.dataset.mode;
192
-
193
- // Update active button
194
- comparison
195
- .querySelectorAll('.view-mode-btn')
196
- .forEach(b => b.classList.remove('active'));
197
- this.classList.add('active');
198
-
199
- // Update viewer mode
200
- let viewer = comparison.querySelector('.comparison-viewer');
201
- viewer.dataset.mode = mode;
202
-
203
- // Hide all mode containers
204
- viewer.querySelectorAll('.mode-container').forEach(container => {
205
- container.style.display = 'none';
206
- });
207
-
208
- // Show appropriate mode container
209
- let activeContainer = viewer.querySelector('.' + mode + '-mode');
210
- if (activeContainer) {
211
- activeContainer.style.display = 'block';
212
- }
213
- });
214
- });
215
-
216
- // Handle onion skin drag-to-reveal
217
- document.querySelectorAll('.onion-container').forEach(container => {
218
- let isDragging = false;
219
-
220
- function updateOnionSkin(x) {
221
- let rect = container.getBoundingClientRect();
222
- let percentage = Math.max(
223
- 0,
224
- Math.min(100, ((x - rect.left) / rect.width) * 100)
225
- );
226
-
227
- let currentImg = container.querySelector('.onion-current');
228
- let divider = container.querySelector('.onion-divider');
229
-
230
- if (currentImg && divider) {
231
- currentImg.style.clipPath = 'inset(0 ' + (100 - percentage) + '% 0 0)';
232
- divider.style.left = percentage + '%';
233
- }
234
- }
235
-
236
- container.addEventListener('mousedown', function (e) {
237
- isDragging = true;
238
- updateOnionSkin(e.clientX);
239
- e.preventDefault();
240
- });
241
-
242
- container.addEventListener('mousemove', function (e) {
243
- if (isDragging) {
244
- updateOnionSkin(e.clientX);
245
- }
246
- });
247
-
248
- document.addEventListener('mouseup', function () {
249
- isDragging = false;
250
- });
251
-
252
- // Touch events for mobile
253
- container.addEventListener('touchstart', function (e) {
254
- isDragging = true;
255
- updateOnionSkin(e.touches[0].clientX);
256
- e.preventDefault();
257
- });
258
-
259
- container.addEventListener('touchmove', function (e) {
260
- if (isDragging) {
261
- updateOnionSkin(e.touches[0].clientX);
262
- e.preventDefault();
263
- }
264
- });
265
-
266
- document.addEventListener('touchend', function () {
267
- isDragging = false;
268
- });
269
- });
270
-
271
- // Handle overlay mode clicking
272
- document.querySelectorAll('.overlay-container').forEach(container => {
273
- container.addEventListener('click', function () {
274
- let diffImage = this.querySelector('.diff-image');
275
- if (diffImage) {
276
- // Toggle diff visibility
277
- let isVisible = diffImage.style.opacity === '1';
278
- diffImage.style.opacity = isVisible ? '0' : '1';
279
- }
280
- });
281
- });
282
-
283
- // Handle toggle mode clicking
284
- document.querySelectorAll('.toggle-container img').forEach(img => {
285
- let isBaseline = true;
286
- let comparison = img.closest('.comparison');
287
- let baselineSrc = comparison.querySelector('.baseline-image').src;
288
- let currentSrc = comparison.querySelector('.current-image').src;
289
-
290
- img.addEventListener('click', function () {
291
- isBaseline = !isBaseline;
292
- this.src = isBaseline ? baselineSrc : currentSrc;
293
-
294
- // Update cursor style to indicate interactivity
295
- this.style.cursor = 'pointer';
296
- });
297
- });
298
-
299
- console.log('Vizzly TDD Report loaded successfully');
300
- });
301
-
302
- // Accept/Reject baseline functions
303
- async function acceptBaseline(screenshotName) {
304
- const button = document.querySelector(\`button[onclick*="\${screenshotName}"]\`);
305
- if (button) {
306
- button.disabled = true;
307
- button.innerHTML = '⏳ Accepting...';
308
- }
309
-
310
- try {
311
- const response = await fetch('/accept-baseline', {
312
- method: 'POST',
313
- headers: { 'Content-Type': 'application/json' },
314
- body: JSON.stringify({ name: screenshotName })
315
- });
316
-
317
- if (response.ok) {
318
- // Mark as accepted and hide the comparison
319
- const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
320
- if (comparison) {
321
- comparison.style.background = '#e8f5e8';
322
- comparison.style.border = '2px solid #4caf50';
323
-
324
- const status = comparison.querySelector('.diff-status');
325
- if (status) {
326
- status.innerHTML = '✅ Accepted as new baseline';
327
- status.style.color = '#4caf50';
328
- }
329
-
330
- const actions = comparison.querySelector('.comparison-actions');
331
- if (actions) {
332
- actions.innerHTML = '<div style="color: #4caf50; padding: 0.5rem;">✅ Screenshot accepted as new baseline</div>';
333
- }
334
- }
335
-
336
- // Auto-refresh after short delay to show updated report
337
- setTimeout(() => window.location.reload(), 2000);
338
- } else {
339
- throw new Error('Failed to accept baseline');
340
- }
341
- } catch (error) {
342
- console.error('Error accepting baseline:', error);
343
- if (button) {
344
- button.disabled = false;
345
- button.innerHTML = '✅ Accept as Baseline';
346
- }
347
- alert('Failed to accept baseline. Please try again.');
348
- }
349
- }
350
-
351
- function rejectChanges(screenshotName) {
352
- const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
353
- if (comparison) {
354
- comparison.style.background = '#fff3cd';
355
- comparison.style.border = '2px solid #ffc107';
356
-
357
- const status = comparison.querySelector('.diff-status');
358
- if (status) {
359
- status.innerHTML = '⚠️ Changes rejected - baseline unchanged';
360
- status.style.color = '#856404';
361
- }
362
-
363
- const actions = comparison.querySelector('.comparison-actions');
364
- if (actions) {
365
- actions.innerHTML = '<div style="color: #856404; padding: 0.5rem;">⚠️ Changes rejected - baseline kept as-is</div>';
366
- }
367
- }
368
- }
369
- </script>
370
- </body>
371
- </html>`;
372
- }
373
-
374
- /**
375
- * Generate HTML for a single comparison
376
- * @param {Object} comparison - Comparison data
377
- * @returns {string} HTML content
378
- */
379
- generateComparisonHtml(comparison) {
380
- if (!comparison || !comparison.baseline || !comparison.current || !comparison.diff) {
381
- return `<div class="comparison error">
382
- <h3>${this.sanitizeHtml(comparison?.name || 'Unknown')}</h3>
383
- <p>Missing comparison images</p>
384
- </div>`;
385
- }
386
- const safeName = this.sanitizeHtml(comparison.name);
387
- return `
388
- <div class="comparison" data-comparison="${safeName}">
389
- <div class="comparison-header">
390
- <h3>${safeName}</h3>
391
- <div class="comparison-meta">
392
- <span class="diff-status">Visual differences detected</span>
393
- </div>
394
- </div>
395
-
396
- <div class="comparison-controls">
397
- <button class="view-mode-btn active" data-mode="overlay">Overlay</button>
398
- <button class="view-mode-btn" data-mode="toggle">Toggle</button>
399
- <button class="view-mode-btn" data-mode="onion">Onion Skin</button>
400
- <button class="view-mode-btn" data-mode="side-by-side">Side by Side</button>
401
- </div>
402
-
403
- <div class="comparison-actions">
404
- <button class="accept-btn" onclick="acceptBaseline('${safeName}')">
405
- ✅ Accept as Baseline
406
- </button>
407
- <button class="reject-btn" onclick="rejectChanges('${safeName}')">
408
- ❌ Keep Current Baseline
409
- </button>
410
- </div>
411
-
412
- <div class="comparison-viewer">
413
- <!-- Overlay Mode -->
414
- <div class="mode-container overlay-mode" data-mode="overlay">
415
- <div class="overlay-container">
416
- <img class="current-image" src="${comparison.current}" alt="Current" />
417
- <img class="baseline-image" src="${comparison.baseline}" alt="Baseline" />
418
- <img class="diff-image" src="${comparison.diff}" alt="Diff" />
419
- </div>
420
- </div>
421
-
422
- <!-- Toggle Mode -->
423
- <div class="mode-container toggle-mode" data-mode="toggle" style="display: none;">
424
- <div class="toggle-container">
425
- <img class="toggle-image" src="${comparison.baseline}" alt="Baseline" />
426
- </div>
427
- </div>
428
-
429
- <!-- Onion Skin Mode -->
430
- <div class="mode-container onion-mode" data-mode="onion" style="display: none;">
431
- <div class="onion-container">
432
- <img class="onion-baseline" src="${comparison.baseline}" alt="Baseline" />
433
- <img class="onion-current" src="${comparison.current}" alt="Current" />
434
- <div class="onion-divider"></div>
435
- </div>
436
- </div>
437
-
438
- <!-- Side by Side Mode -->
439
- <div class="mode-container side-by-side-mode" data-mode="side-by-side" style="display: none;">
440
- <div class="side-by-side-container">
441
- <div class="side-by-side-image">
442
- <img src="${comparison.baseline}" alt="Baseline" />
443
- <label>Baseline</label>
444
- </div>
445
- <div class="side-by-side-image">
446
- <img src="${comparison.current}" alt="Current" />
447
- <label>Current</label>
448
- </div>
449
- </div>
450
- </div>
451
- </div>
452
-
453
- </div>`;
454
- }
455
- }