@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,396 @@
1
+ /**
2
+ * Uploader Core - Pure functions for upload logic
3
+ *
4
+ * No I/O, no side effects - just data transformations.
5
+ */
6
+
7
+ import crypto from 'node:crypto';
8
+ import { basename } from 'node:path';
9
+
10
+ // ============================================================================
11
+ // Constants
12
+ // ============================================================================
13
+
14
+ export const DEFAULT_BATCH_SIZE = 50;
15
+ export const DEFAULT_SHA_CHECK_BATCH_SIZE = 100;
16
+ export const DEFAULT_TIMEOUT = 30000; // 30 seconds
17
+
18
+ // ============================================================================
19
+ // Validation
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Validate API key is present
24
+ * @param {string|undefined} apiKey - API key to validate
25
+ * @returns {{ valid: boolean, error: string|null }}
26
+ */
27
+ export function validateApiKey(apiKey) {
28
+ if (!apiKey) {
29
+ return {
30
+ valid: false,
31
+ error: 'API key is required'
32
+ };
33
+ }
34
+ return {
35
+ valid: true,
36
+ error: null
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Validate screenshots directory path
42
+ * @param {string|undefined} screenshotsDir - Directory path
43
+ * @returns {{ valid: boolean, error: string|null }}
44
+ */
45
+ export function validateScreenshotsDir(screenshotsDir) {
46
+ if (!screenshotsDir) {
47
+ return {
48
+ valid: false,
49
+ error: 'Screenshots directory is required'
50
+ };
51
+ }
52
+ return {
53
+ valid: true,
54
+ error: null
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Validate directory stats
60
+ * @param {Object} stats - fs.stat result
61
+ * @param {string} path - Directory path for error message
62
+ * @returns {{ valid: boolean, error: string|null }}
63
+ */
64
+ export function validateDirectoryStats(stats, path) {
65
+ if (!stats.isDirectory()) {
66
+ return {
67
+ valid: false,
68
+ error: `${path} is not a directory`
69
+ };
70
+ }
71
+ return {
72
+ valid: true,
73
+ error: null
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Validate files were found
79
+ * @param {Array} files - Array of file paths
80
+ * @param {string} directory - Directory that was searched
81
+ * @returns {{ valid: boolean, error: string|null, context?: Object }}
82
+ */
83
+ export function validateFilesFound(files, directory) {
84
+ if (!files || files.length === 0) {
85
+ return {
86
+ valid: false,
87
+ error: 'No screenshot files found',
88
+ context: {
89
+ directory,
90
+ pattern: '**/*.png'
91
+ }
92
+ };
93
+ }
94
+ return {
95
+ valid: true,
96
+ error: null
97
+ };
98
+ }
99
+
100
+ // ============================================================================
101
+ // Browser Extraction
102
+ // ============================================================================
103
+
104
+ const KNOWN_BROWSERS = ['chrome', 'firefox', 'safari', 'edge', 'webkit'];
105
+
106
+ /**
107
+ * Extract browser name from filename
108
+ * @param {string} filename - The screenshot filename
109
+ * @returns {string|null} Browser name or null if not found
110
+ */
111
+ export function extractBrowserFromFilename(filename) {
112
+ let lowerFilename = filename.toLowerCase();
113
+ for (let browser of KNOWN_BROWSERS) {
114
+ if (lowerFilename.includes(browser)) {
115
+ return browser;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ // ============================================================================
122
+ // Build Info Construction
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Build API build info payload
127
+ * @param {Object} options - Upload options
128
+ * @param {string} [defaultBranch] - Default branch to use
129
+ * @returns {Object} Build info payload
130
+ */
131
+ export function buildBuildInfo(options, defaultBranch = 'main') {
132
+ return {
133
+ name: options.buildName || `Upload ${new Date().toISOString()}`,
134
+ branch: options.branch || defaultBranch || 'main',
135
+ commit_sha: options.commit,
136
+ commit_message: options.message,
137
+ environment: options.environment || 'production',
138
+ threshold: options.threshold,
139
+ github_pull_request_number: options.pullRequestNumber,
140
+ parallel_id: options.parallelId
141
+ };
142
+ }
143
+
144
+ // ============================================================================
145
+ // File Metadata Processing
146
+ // ============================================================================
147
+
148
+ /**
149
+ * Compute SHA256 hash of a buffer
150
+ * @param {Buffer} buffer - File buffer
151
+ * @returns {string} Hex-encoded SHA256 hash
152
+ */
153
+ export function computeSha256(buffer) {
154
+ return crypto.createHash('sha256').update(buffer).digest('hex');
155
+ }
156
+
157
+ /**
158
+ * Build file metadata object
159
+ * @param {string} filePath - Path to file
160
+ * @param {Buffer} buffer - File contents
161
+ * @returns {Object} File metadata
162
+ */
163
+ export function buildFileMetadata(filePath, buffer) {
164
+ return {
165
+ path: filePath,
166
+ filename: basename(filePath),
167
+ buffer,
168
+ sha256: computeSha256(buffer)
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Convert file metadata to screenshot check format
174
+ * @param {Object} file - File metadata
175
+ * @returns {Object} Screenshot format for SHA check
176
+ */
177
+ export function fileToScreenshotFormat(file) {
178
+ return {
179
+ sha256: file.sha256,
180
+ name: file.filename.replace(/\.png$/, ''),
181
+ browser: extractBrowserFromFilename(file.filename) || 'chrome',
182
+ viewport_width: 1920,
183
+ viewport_height: 1080
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Partition files into those that need upload and those that exist
189
+ * @param {Array} fileMetadata - All file metadata
190
+ * @param {Set} existingShas - Set of SHAs that already exist
191
+ * @returns {{ toUpload: Array, existing: Array }}
192
+ */
193
+ export function partitionFilesByExistence(fileMetadata, existingShas) {
194
+ return {
195
+ toUpload: fileMetadata.filter(f => !existingShas.has(f.sha256)),
196
+ existing: fileMetadata.filter(f => existingShas.has(f.sha256))
197
+ };
198
+ }
199
+
200
+ // ============================================================================
201
+ // Progress Reporting
202
+ // ============================================================================
203
+
204
+ /**
205
+ * Build scanning phase progress
206
+ * @param {number} total - Total files found
207
+ * @returns {Object} Progress object
208
+ */
209
+ export function buildScanningProgress(total) {
210
+ return {
211
+ phase: 'scanning',
212
+ message: `Found ${total} screenshots`,
213
+ total
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Build processing phase progress
219
+ * @param {number} current - Current file number
220
+ * @param {number} total - Total files
221
+ * @returns {Object} Progress object
222
+ */
223
+ export function buildProcessingProgress(current, total) {
224
+ return {
225
+ phase: 'processing',
226
+ message: 'Processing files',
227
+ current,
228
+ total
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Build deduplication phase progress
234
+ * @param {number} toUpload - Files to upload
235
+ * @param {number} existing - Existing files
236
+ * @param {number} total - Total files
237
+ * @returns {Object} Progress object
238
+ */
239
+ export function buildDeduplicationProgress(toUpload, existing, total) {
240
+ return {
241
+ phase: 'deduplication',
242
+ message: `Checking for duplicates (${toUpload} to upload, ${existing} existing)`,
243
+ toUpload,
244
+ existing,
245
+ total
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Build uploading phase progress
251
+ * @param {number} current - Current upload number
252
+ * @param {number} total - Total to upload
253
+ * @returns {Object} Progress object
254
+ */
255
+ export function buildUploadingProgress(current, total) {
256
+ return {
257
+ phase: 'uploading',
258
+ message: 'Uploading screenshots',
259
+ current,
260
+ total
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Build completed phase progress
266
+ * @param {string} buildId - Build ID
267
+ * @param {string|null} url - Build URL
268
+ * @returns {Object} Progress object
269
+ */
270
+ export function buildCompletedProgress(buildId, url) {
271
+ return {
272
+ phase: 'completed',
273
+ message: 'Upload completed',
274
+ buildId,
275
+ url
276
+ };
277
+ }
278
+
279
+ // ============================================================================
280
+ // Result Building
281
+ // ============================================================================
282
+
283
+ /**
284
+ * Build successful upload result
285
+ * @param {Object} options - Options
286
+ * @param {string} options.buildId - Build ID
287
+ * @param {string|null} options.url - Build URL
288
+ * @param {number} options.total - Total files
289
+ * @param {number} options.uploaded - Files uploaded
290
+ * @param {number} options.skipped - Files skipped
291
+ * @returns {Object} Upload result
292
+ */
293
+ export function buildUploadResult({
294
+ buildId,
295
+ url,
296
+ total,
297
+ uploaded,
298
+ skipped
299
+ }) {
300
+ return {
301
+ success: true,
302
+ buildId,
303
+ url,
304
+ stats: {
305
+ total,
306
+ uploaded,
307
+ skipped
308
+ }
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Build wait result from build response
314
+ * @param {Object} build - Build object from API
315
+ * @returns {Object} Wait result
316
+ */
317
+ export function buildWaitResult(build) {
318
+ let result = {
319
+ status: 'completed',
320
+ build
321
+ };
322
+ if (typeof build.comparisonsTotal === 'number') {
323
+ result.comparisons = build.comparisonsTotal;
324
+ result.passedComparisons = build.comparisonsPassed || 0;
325
+ result.failedComparisons = build.comparisonsFailed || 0;
326
+ } else {
327
+ result.passedComparisons = 0;
328
+ result.failedComparisons = 0;
329
+ }
330
+ if (build.url) {
331
+ result.url = build.url;
332
+ }
333
+ return result;
334
+ }
335
+
336
+ // ============================================================================
337
+ // Configuration
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Resolve batch size from options and config
342
+ * @param {Object} options - Runtime options
343
+ * @param {Object} uploadConfig - Upload configuration
344
+ * @returns {number} Resolved batch size
345
+ */
346
+ export function resolveBatchSize(options, uploadConfig) {
347
+ return Number(options?.batchSize ?? uploadConfig?.batchSize ?? DEFAULT_BATCH_SIZE);
348
+ }
349
+
350
+ /**
351
+ * Resolve timeout from options and config
352
+ * @param {Object} options - Runtime options
353
+ * @param {Object} uploadConfig - Upload configuration
354
+ * @returns {number} Resolved timeout in ms
355
+ */
356
+ export function resolveTimeout(options, uploadConfig) {
357
+ return Number(options?.timeout ?? uploadConfig?.timeout ?? DEFAULT_TIMEOUT);
358
+ }
359
+
360
+ /**
361
+ * Check if timeout has been exceeded
362
+ * @param {number} startTime - Start timestamp
363
+ * @param {number} timeout - Timeout in ms
364
+ * @returns {boolean} True if timed out
365
+ */
366
+ export function isTimedOut(startTime, timeout) {
367
+ return Date.now() - startTime >= timeout;
368
+ }
369
+
370
+ /**
371
+ * Get elapsed time since start
372
+ * @param {number} startTime - Start timestamp
373
+ * @returns {number} Elapsed time in ms
374
+ */
375
+ export function getElapsedTime(startTime) {
376
+ return Date.now() - startTime;
377
+ }
378
+
379
+ /**
380
+ * Build glob pattern for screenshots
381
+ * @param {string} directory - Base directory
382
+ * @returns {string} Glob pattern
383
+ */
384
+ export function buildScreenshotPattern(directory) {
385
+ return `${directory}/**/*.png`;
386
+ }
387
+
388
+ /**
389
+ * Extract status code from error message
390
+ * @param {string} errorMessage - Error message
391
+ * @returns {string} Status code or 'unknown'
392
+ */
393
+ export function extractStatusCodeFromError(errorMessage) {
394
+ let match = String(errorMessage || '').match(/API request failed: (\d+)/);
395
+ return match ? match[1] : 'unknown';
396
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Uploader Module
3
+ *
4
+ * Exports pure functions (core) and I/O operations for screenshot uploading.
5
+ */
6
+
7
+ // Core - pure functions
8
+ export { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, computeSha256, DEFAULT_BATCH_SIZE, DEFAULT_SHA_CHECK_BATCH_SIZE, DEFAULT_TIMEOUT, extractBrowserFromFilename, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, resolveBatchSize, resolveTimeout, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
9
+
10
+ // Operations - I/O with dependency injection
11
+ export { checkExistingFiles, findScreenshots, processFiles, upload, uploadFiles, waitForBuild } from './operations.js';