@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.
Files changed (72) 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 +1 -1
  10. package/dist/commands/doctor.js +3 -3
  11. package/dist/commands/finalize.js +41 -15
  12. package/dist/commands/login.js +7 -6
  13. package/dist/commands/logout.js +4 -4
  14. package/dist/commands/project.js +5 -4
  15. package/dist/commands/run.js +158 -90
  16. package/dist/commands/status.js +22 -18
  17. package/dist/commands/tdd.js +105 -78
  18. package/dist/commands/upload.js +61 -26
  19. package/dist/commands/whoami.js +4 -4
  20. package/dist/config/core.js +438 -0
  21. package/dist/config/index.js +13 -0
  22. package/dist/config/operations.js +327 -0
  23. package/dist/index.js +1 -1
  24. package/dist/project/core.js +295 -0
  25. package/dist/project/index.js +13 -0
  26. package/dist/project/operations.js +393 -0
  27. package/dist/report-generator/core.js +315 -0
  28. package/dist/report-generator/index.js +8 -0
  29. package/dist/report-generator/operations.js +196 -0
  30. package/dist/reporter/reporter-bundle.iife.js +16 -16
  31. package/dist/screenshot-server/core.js +157 -0
  32. package/dist/screenshot-server/index.js +11 -0
  33. package/dist/screenshot-server/operations.js +183 -0
  34. package/dist/sdk/index.js +3 -2
  35. package/dist/server/handlers/api-handler.js +14 -5
  36. package/dist/server/handlers/tdd-handler.js +80 -48
  37. package/dist/server-manager/core.js +183 -0
  38. package/dist/server-manager/index.js +81 -0
  39. package/dist/server-manager/operations.js +208 -0
  40. package/dist/services/build-manager.js +2 -69
  41. package/dist/services/index.js +21 -48
  42. package/dist/services/screenshot-server.js +40 -74
  43. package/dist/services/server-manager.js +45 -80
  44. package/dist/services/static-report-generator.js +21 -163
  45. package/dist/services/test-runner.js +90 -250
  46. package/dist/services/uploader.js +56 -358
  47. package/dist/tdd/core/hotspot-coverage.js +112 -0
  48. package/dist/tdd/core/signature.js +101 -0
  49. package/dist/tdd/index.js +19 -0
  50. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  51. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  52. package/dist/tdd/services/baseline-downloader.js +151 -0
  53. package/dist/tdd/services/baseline-manager.js +166 -0
  54. package/dist/tdd/services/comparison-service.js +230 -0
  55. package/dist/tdd/services/hotspot-service.js +71 -0
  56. package/dist/tdd/services/result-service.js +123 -0
  57. package/dist/tdd/tdd-service.js +1081 -0
  58. package/dist/test-runner/core.js +255 -0
  59. package/dist/test-runner/index.js +13 -0
  60. package/dist/test-runner/operations.js +483 -0
  61. package/dist/uploader/core.js +396 -0
  62. package/dist/uploader/index.js +11 -0
  63. package/dist/uploader/operations.js +412 -0
  64. package/package.json +7 -12
  65. package/dist/services/api-service.js +0 -412
  66. package/dist/services/auth-service.js +0 -226
  67. package/dist/services/config-service.js +0 -369
  68. package/dist/services/html-report-generator.js +0 -455
  69. package/dist/services/project-service.js +0 -326
  70. package/dist/services/report-generator/report.css +0 -411
  71. package/dist/services/report-generator/viewer.js +0 -102
  72. package/dist/services/tdd-service.js +0 -1437
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Comparison Service
3
+ *
4
+ * Wraps honeydiff for image comparison and builds comparison result objects.
5
+ */
6
+
7
+ import { compare } from '@vizzly-testing/honeydiff';
8
+ import { calculateHotspotCoverage } from '../core/hotspot-coverage.js';
9
+ import { generateComparisonId } from '../core/signature.js';
10
+
11
+ /**
12
+ * Compare two images using honeydiff
13
+ *
14
+ * @param {string} baselinePath - Path to baseline image
15
+ * @param {string} currentPath - Path to current image
16
+ * @param {string} diffPath - Path to save diff image
17
+ * @param {Object} options - Comparison options
18
+ * @param {number} options.threshold - CIEDE2000 Delta E threshold (default: 2.0)
19
+ * @param {number} options.minClusterSize - Minimum cluster size (default: 2)
20
+ * @returns {Promise<Object>} Honeydiff result
21
+ * @throws {Error} When honeydiff binary fails (e.g., corrupt images, dimension mismatch)
22
+ */
23
+ export async function compareImages(baselinePath, currentPath, diffPath, options = {}) {
24
+ let {
25
+ threshold = 2.0,
26
+ minClusterSize = 2
27
+ } = options;
28
+ return compare(baselinePath, currentPath, {
29
+ threshold,
30
+ antialiasing: true,
31
+ diffPath,
32
+ overwrite: true,
33
+ includeClusters: true,
34
+ minClusterSize
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Build a comparison result object for a passing comparison (no diff)
40
+ *
41
+ * @param {Object} params
42
+ * @param {string} params.name - Screenshot name
43
+ * @param {string} params.signature - Screenshot signature
44
+ * @param {string} params.baselinePath - Path to baseline image
45
+ * @param {string} params.currentPath - Path to current image
46
+ * @param {Object} params.properties - Screenshot properties
47
+ * @param {number} params.threshold - Effective threshold used
48
+ * @param {number} params.minClusterSize - Effective minClusterSize used
49
+ * @param {Object} params.honeydiffResult - Result from honeydiff (optional, for metrics)
50
+ * @returns {Object} Comparison result
51
+ */
52
+ export function buildPassedComparison(params) {
53
+ let {
54
+ name,
55
+ signature,
56
+ baselinePath,
57
+ currentPath,
58
+ properties,
59
+ threshold,
60
+ minClusterSize,
61
+ honeydiffResult
62
+ } = params;
63
+ return {
64
+ id: generateComparisonId(signature),
65
+ name,
66
+ status: 'passed',
67
+ baseline: baselinePath,
68
+ current: currentPath,
69
+ diff: null,
70
+ properties,
71
+ signature,
72
+ threshold,
73
+ minClusterSize,
74
+ totalPixels: honeydiffResult?.totalPixels,
75
+ aaPixelsIgnored: honeydiffResult?.aaPixelsIgnored,
76
+ aaPercentage: honeydiffResult?.aaPercentage
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Build a comparison result object for a new baseline
82
+ *
83
+ * @param {Object} params
84
+ * @param {string} params.name - Screenshot name
85
+ * @param {string} params.signature - Screenshot signature
86
+ * @param {string} params.baselinePath - Path to baseline image
87
+ * @param {string} params.currentPath - Path to current image
88
+ * @param {Object} params.properties - Screenshot properties
89
+ * @returns {Object} Comparison result
90
+ */
91
+ export function buildNewComparison(params) {
92
+ let {
93
+ name,
94
+ signature,
95
+ baselinePath,
96
+ currentPath,
97
+ properties
98
+ } = params;
99
+ return {
100
+ id: generateComparisonId(signature),
101
+ name,
102
+ status: 'new',
103
+ baseline: baselinePath,
104
+ current: currentPath,
105
+ diff: null,
106
+ properties,
107
+ signature
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Build a comparison result object for a failed comparison (with diff)
113
+ *
114
+ * @param {Object} params
115
+ * @param {string} params.name - Screenshot name
116
+ * @param {string} params.signature - Screenshot signature
117
+ * @param {string} params.baselinePath - Path to baseline image
118
+ * @param {string} params.currentPath - Path to current image
119
+ * @param {string} params.diffPath - Path to diff image
120
+ * @param {Object} params.properties - Screenshot properties
121
+ * @param {number} params.threshold - Effective threshold used
122
+ * @param {number} params.minClusterSize - Effective minClusterSize used
123
+ * @param {Object} params.honeydiffResult - Result from honeydiff
124
+ * @param {Object} params.hotspotAnalysis - Hotspot data for this screenshot (optional)
125
+ * @returns {Object} Comparison result
126
+ */
127
+ export function buildFailedComparison(params) {
128
+ let {
129
+ name,
130
+ signature,
131
+ baselinePath,
132
+ currentPath,
133
+ diffPath,
134
+ properties,
135
+ threshold,
136
+ minClusterSize,
137
+ honeydiffResult,
138
+ hotspotAnalysis
139
+ } = params;
140
+
141
+ // Calculate hotspot coverage if we have hotspot data
142
+ let hotspotCoverage = null;
143
+ let isHotspotFiltered = false;
144
+ if (hotspotAnalysis && honeydiffResult.diffClusters?.length > 0) {
145
+ hotspotCoverage = calculateHotspotCoverage(honeydiffResult.diffClusters, hotspotAnalysis);
146
+
147
+ // Check if diff should be filtered as hotspot noise
148
+ // Using shouldFilterAsHotspot helper but also checking confidence_score
149
+ // (cloud uses confidence_score >= 70 which is >0.7 when normalized)
150
+ let isHighConfidence = hotspotAnalysis.confidence === 'high' || hotspotAnalysis.confidence_score !== undefined && hotspotAnalysis.confidence_score >= 70;
151
+ if (isHighConfidence && hotspotCoverage.coverage >= 0.8) {
152
+ isHotspotFiltered = true;
153
+ }
154
+ }
155
+ return {
156
+ id: generateComparisonId(signature),
157
+ name,
158
+ status: isHotspotFiltered ? 'passed' : 'failed',
159
+ baseline: baselinePath,
160
+ current: currentPath,
161
+ diff: diffPath,
162
+ properties,
163
+ signature,
164
+ threshold,
165
+ minClusterSize,
166
+ diffPercentage: honeydiffResult.diffPercentage,
167
+ diffCount: honeydiffResult.diffPixels,
168
+ reason: isHotspotFiltered ? 'hotspot-filtered' : 'pixel-diff',
169
+ totalPixels: honeydiffResult.totalPixels,
170
+ aaPixelsIgnored: honeydiffResult.aaPixelsIgnored,
171
+ aaPercentage: honeydiffResult.aaPercentage,
172
+ boundingBox: honeydiffResult.boundingBox,
173
+ heightDiff: honeydiffResult.heightDiff,
174
+ intensityStats: honeydiffResult.intensityStats,
175
+ diffClusters: honeydiffResult.diffClusters,
176
+ hotspotAnalysis: hotspotCoverage ? {
177
+ coverage: hotspotCoverage.coverage,
178
+ linesInHotspots: hotspotCoverage.linesInHotspots,
179
+ totalLines: hotspotCoverage.totalLines,
180
+ confidence: hotspotAnalysis?.confidence,
181
+ confidenceScore: hotspotAnalysis?.confidence_score,
182
+ regionCount: hotspotAnalysis?.regions?.length || 0,
183
+ isFiltered: isHotspotFiltered
184
+ } : null
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Build a comparison result object for an error
190
+ *
191
+ * @param {Object} params
192
+ * @param {string} params.name - Screenshot name
193
+ * @param {string} params.signature - Screenshot signature
194
+ * @param {string} params.baselinePath - Path to baseline image
195
+ * @param {string} params.currentPath - Path to current image
196
+ * @param {Object} params.properties - Screenshot properties
197
+ * @param {string} params.errorMessage - Error message
198
+ * @returns {Object} Comparison result
199
+ */
200
+ export function buildErrorComparison(params) {
201
+ let {
202
+ name,
203
+ signature,
204
+ baselinePath,
205
+ currentPath,
206
+ properties,
207
+ errorMessage
208
+ } = params;
209
+ return {
210
+ id: generateComparisonId(signature),
211
+ name,
212
+ status: 'error',
213
+ baseline: baselinePath,
214
+ current: currentPath,
215
+ diff: null,
216
+ properties,
217
+ signature,
218
+ error: errorMessage
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Check if an error is a dimension mismatch from honeydiff
224
+ *
225
+ * @param {Error} error
226
+ * @returns {boolean}
227
+ */
228
+ export function isDimensionMismatchError(error) {
229
+ return error.message?.includes("Image dimensions don't match") ?? false;
230
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Hotspot Service
3
+ *
4
+ * Functions for downloading and managing hotspot data from the cloud.
5
+ * Hotspots identify regions that frequently change due to dynamic content.
6
+ */
7
+
8
+ import { saveHotspotMetadata } from '../metadata/hotspot-metadata.js';
9
+
10
+ /**
11
+ * Download hotspots for screenshots from cloud API
12
+ *
13
+ * @param {Object} options
14
+ * @param {Object} options.api - ApiService instance
15
+ * @param {string} options.workingDir - Working directory
16
+ * @param {string[]} options.screenshotNames - Names of screenshots to get hotspots for
17
+ * @returns {Promise<{ success: boolean, count: number, regionCount: number, error?: string }>}
18
+ */
19
+ export async function downloadHotspots(options) {
20
+ let {
21
+ api,
22
+ workingDir,
23
+ screenshotNames
24
+ } = options;
25
+ if (!screenshotNames || screenshotNames.length === 0) {
26
+ return {
27
+ success: true,
28
+ count: 0,
29
+ regionCount: 0
30
+ };
31
+ }
32
+ try {
33
+ let response = await api.getHotspots(screenshotNames);
34
+ if (!response || !response.hotspots) {
35
+ return {
36
+ success: false,
37
+ error: 'API returned no hotspot data'
38
+ };
39
+ }
40
+
41
+ // Save hotspots to disk
42
+ saveHotspotMetadata(workingDir, response.hotspots, response.summary);
43
+
44
+ // Calculate stats
45
+ let count = Object.keys(response.hotspots).length;
46
+ let regionCount = Object.values(response.hotspots).reduce((sum, h) => sum + (h.regions?.length || 0), 0);
47
+ return {
48
+ success: true,
49
+ count,
50
+ regionCount
51
+ };
52
+ } catch (error) {
53
+ return {
54
+ success: false,
55
+ error: error.message
56
+ };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Extract screenshot names from a list of screenshots
62
+ *
63
+ * @param {Array} screenshots - Screenshots with name property
64
+ * @returns {string[]}
65
+ */
66
+ export function extractScreenshotNames(screenshots) {
67
+ if (!screenshots || !Array.isArray(screenshots)) {
68
+ return [];
69
+ }
70
+ return screenshots.map(s => s.name).filter(Boolean);
71
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Result Service
3
+ *
4
+ * Aggregates comparison results and provides summary statistics.
5
+ */
6
+
7
+ /**
8
+ * Calculate summary statistics from comparisons
9
+ *
10
+ * @param {Array} comparisons - Array of comparison results
11
+ * @returns {{ total: number, passed: number, failed: number, new: number, errors: number }}
12
+ */
13
+ export function calculateSummary(comparisons) {
14
+ let passed = 0;
15
+ let failed = 0;
16
+ let newScreenshots = 0;
17
+ let errors = 0;
18
+ for (let c of comparisons) {
19
+ switch (c.status) {
20
+ case 'passed':
21
+ passed++;
22
+ break;
23
+ case 'failed':
24
+ failed++;
25
+ break;
26
+ case 'new':
27
+ newScreenshots++;
28
+ break;
29
+ case 'error':
30
+ errors++;
31
+ break;
32
+ }
33
+ }
34
+ return {
35
+ total: comparisons.length,
36
+ passed,
37
+ failed,
38
+ new: newScreenshots,
39
+ errors
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Build complete results object
45
+ *
46
+ * @param {Array} comparisons - Array of comparison results
47
+ * @param {Object} baselineData - Baseline metadata
48
+ * @returns {Object} Results object with summary and comparisons
49
+ */
50
+ export function buildResults(comparisons, baselineData) {
51
+ let summary = calculateSummary(comparisons);
52
+ return {
53
+ ...summary,
54
+ comparisons,
55
+ baseline: baselineData
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Get failed comparisons from results
61
+ *
62
+ * @param {Array} comparisons - Array of comparison results
63
+ * @returns {Array} Failed comparisons
64
+ */
65
+ export function getFailedComparisons(comparisons) {
66
+ return comparisons.filter(c => c.status === 'failed');
67
+ }
68
+
69
+ /**
70
+ * Get new comparisons from results
71
+ *
72
+ * @param {Array} comparisons - Array of comparison results
73
+ * @returns {Array} New comparisons
74
+ */
75
+ export function getNewComparisons(comparisons) {
76
+ return comparisons.filter(c => c.status === 'new');
77
+ }
78
+
79
+ /**
80
+ * Get error comparisons from results
81
+ *
82
+ * @param {Array} comparisons - Array of comparison results
83
+ * @returns {Array} Error comparisons
84
+ */
85
+ export function getErrorComparisons(comparisons) {
86
+ return comparisons.filter(c => c.status === 'error');
87
+ }
88
+
89
+ /**
90
+ * Check if results indicate overall success (no failures or errors)
91
+ *
92
+ * @param {Array} comparisons - Array of comparison results
93
+ * @returns {boolean}
94
+ */
95
+ export function isSuccessful(comparisons) {
96
+ return !comparisons.some(c => c.status === 'failed' || c.status === 'error');
97
+ }
98
+
99
+ /**
100
+ * Find comparison by ID
101
+ *
102
+ * @param {Array} comparisons - Array of comparison results
103
+ * @param {string} id - Comparison ID
104
+ * @returns {Object|null}
105
+ */
106
+ export function findComparisonById(comparisons, id) {
107
+ return comparisons.find(c => c.id === id) || null;
108
+ }
109
+
110
+ /**
111
+ * Find comparison by name and signature
112
+ *
113
+ * @param {Array} comparisons - Array of comparison results
114
+ * @param {string} name - Screenshot name
115
+ * @param {string} signature - Screenshot signature (optional)
116
+ * @returns {Object|null}
117
+ */
118
+ export function findComparison(comparisons, name, signature = null) {
119
+ if (signature) {
120
+ return comparisons.find(c => c.signature === signature) || null;
121
+ }
122
+ return comparisons.find(c => c.name === name) || null;
123
+ }