@vizzly-testing/cli 0.19.2 → 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 (76) 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/client/index.js +0 -1
  11. package/dist/commands/doctor.js +3 -3
  12. package/dist/commands/finalize.js +41 -15
  13. package/dist/commands/login.js +7 -6
  14. package/dist/commands/logout.js +4 -4
  15. package/dist/commands/project.js +5 -4
  16. package/dist/commands/run.js +158 -90
  17. package/dist/commands/status.js +22 -18
  18. package/dist/commands/tdd.js +105 -78
  19. package/dist/commands/upload.js +61 -26
  20. package/dist/commands/whoami.js +4 -4
  21. package/dist/config/core.js +438 -0
  22. package/dist/config/index.js +13 -0
  23. package/dist/config/operations.js +327 -0
  24. package/dist/index.js +1 -1
  25. package/dist/project/core.js +295 -0
  26. package/dist/project/index.js +13 -0
  27. package/dist/project/operations.js +393 -0
  28. package/dist/report-generator/core.js +315 -0
  29. package/dist/report-generator/index.js +8 -0
  30. package/dist/report-generator/operations.js +196 -0
  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 +80 -48
  38. package/dist/server-manager/core.js +183 -0
  39. package/dist/server-manager/index.js +81 -0
  40. package/dist/server-manager/operations.js +208 -0
  41. package/dist/services/build-manager.js +2 -69
  42. package/dist/services/index.js +21 -48
  43. package/dist/services/screenshot-server.js +40 -74
  44. package/dist/services/server-manager.js +45 -80
  45. package/dist/services/static-report-generator.js +21 -163
  46. package/dist/services/test-runner.js +90 -249
  47. package/dist/services/uploader.js +56 -358
  48. package/dist/tdd/core/hotspot-coverage.js +112 -0
  49. package/dist/tdd/core/signature.js +101 -0
  50. package/dist/tdd/index.js +19 -0
  51. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  52. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  53. package/dist/tdd/services/baseline-downloader.js +151 -0
  54. package/dist/tdd/services/baseline-manager.js +166 -0
  55. package/dist/tdd/services/comparison-service.js +230 -0
  56. package/dist/tdd/services/hotspot-service.js +71 -0
  57. package/dist/tdd/services/result-service.js +123 -0
  58. package/dist/tdd/tdd-service.js +1081 -0
  59. package/dist/test-runner/core.js +255 -0
  60. package/dist/test-runner/index.js +13 -0
  61. package/dist/test-runner/operations.js +483 -0
  62. package/dist/types/client.d.ts +4 -2
  63. package/dist/types/index.d.ts +5 -0
  64. package/dist/uploader/core.js +396 -0
  65. package/dist/uploader/index.js +11 -0
  66. package/dist/uploader/operations.js +412 -0
  67. package/dist/utils/config-schema.js +8 -3
  68. package/package.json +7 -12
  69. package/dist/services/api-service.js +0 -412
  70. package/dist/services/auth-service.js +0 -226
  71. package/dist/services/config-service.js +0 -369
  72. package/dist/services/html-report-generator.js +0 -455
  73. package/dist/services/project-service.js +0 -326
  74. package/dist/services/report-generator/report.css +0 -411
  75. package/dist/services/report-generator/viewer.js +0 -102
  76. package/dist/services/tdd-service.js +0 -1429
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Uploader Operations - I/O operations with dependency injection
3
+ *
4
+ * Each operation takes its dependencies as parameters for testability.
5
+ */
6
+
7
+ import { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, DEFAULT_SHA_CHECK_BATCH_SIZE, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
8
+
9
+ // ============================================================================
10
+ // File Discovery
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Find all PNG screenshots in a directory
15
+ * @param {Object} options - Options
16
+ * @param {string} options.directory - Directory to search
17
+ * @param {Object} options.deps - Dependencies
18
+ * @param {Function} options.deps.glob - Glob function
19
+ * @returns {Promise<Array<string>>} Array of file paths
20
+ */
21
+ export async function findScreenshots({
22
+ directory,
23
+ deps
24
+ }) {
25
+ let {
26
+ glob
27
+ } = deps;
28
+ let pattern = buildScreenshotPattern(directory);
29
+ return glob(pattern, {
30
+ absolute: true
31
+ });
32
+ }
33
+
34
+ // ============================================================================
35
+ // File Processing
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Process files to extract metadata and compute hashes
40
+ * @param {Object} options - Options
41
+ * @param {Array<string>} options.files - File paths
42
+ * @param {AbortSignal} options.signal - Abort signal
43
+ * @param {Function} options.onProgress - Progress callback
44
+ * @param {Object} options.deps - Dependencies
45
+ * @param {Function} options.deps.readFile - File read function
46
+ * @param {Function} options.deps.createError - Error factory
47
+ * @returns {Promise<Array>} File metadata array
48
+ */
49
+ export async function processFiles({
50
+ files,
51
+ signal,
52
+ onProgress,
53
+ deps
54
+ }) {
55
+ let {
56
+ readFile,
57
+ createError
58
+ } = deps;
59
+ let results = [];
60
+ let count = 0;
61
+ for (let filePath of files) {
62
+ if (signal.aborted) {
63
+ throw createError('Operation cancelled', 'UPLOAD_CANCELLED');
64
+ }
65
+ let buffer = await readFile(filePath);
66
+ let metadata = buildFileMetadata(filePath, buffer);
67
+ results.push(metadata);
68
+ count++;
69
+ if (count % 10 === 0 || count === files.length) {
70
+ onProgress(count);
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+
76
+ // ============================================================================
77
+ // SHA Checking / Deduplication
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Check which files already exist on the server
82
+ * @param {Object} options - Options
83
+ * @param {Array} options.fileMetadata - File metadata array
84
+ * @param {Object} options.client - API client
85
+ * @param {AbortSignal} options.signal - Abort signal
86
+ * @param {string} options.buildId - Build ID
87
+ * @param {Object} options.deps - Dependencies
88
+ * @param {Function} options.deps.checkShas - SHA check API function
89
+ * @param {Function} options.deps.createError - Error factory
90
+ * @param {Object} options.deps.output - Output utilities
91
+ * @returns {Promise<{ toUpload: Array, existing: Array, screenshots: Array }>}
92
+ */
93
+ export async function checkExistingFiles({
94
+ fileMetadata,
95
+ client,
96
+ signal,
97
+ buildId,
98
+ deps
99
+ }) {
100
+ let {
101
+ checkShas,
102
+ createError,
103
+ output
104
+ } = deps;
105
+ let existingShas = new Set();
106
+ let allScreenshots = [];
107
+ for (let i = 0; i < fileMetadata.length; i += DEFAULT_SHA_CHECK_BATCH_SIZE) {
108
+ if (signal.aborted) {
109
+ throw createError('Operation cancelled', 'UPLOAD_CANCELLED');
110
+ }
111
+ let batch = fileMetadata.slice(i, i + DEFAULT_SHA_CHECK_BATCH_SIZE);
112
+ let screenshotBatch = batch.map(fileToScreenshotFormat);
113
+ try {
114
+ let res = await checkShas(client, screenshotBatch, buildId);
115
+ let {
116
+ existing = [],
117
+ screenshots = []
118
+ } = res || {};
119
+ for (let sha of existing) {
120
+ existingShas.add(sha);
121
+ }
122
+ allScreenshots.push(...screenshots);
123
+ } catch (error) {
124
+ output.debug('upload', 'SHA check failed, continuing without deduplication', {
125
+ error: error.message
126
+ });
127
+ }
128
+ }
129
+ let partitioned = partitionFilesByExistence(fileMetadata, existingShas);
130
+ return {
131
+ toUpload: partitioned.toUpload,
132
+ existing: partitioned.existing,
133
+ screenshots: allScreenshots
134
+ };
135
+ }
136
+
137
+ // ============================================================================
138
+ // File Upload
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Upload files to Vizzly
143
+ * @param {Object} options - Options
144
+ * @param {Array} options.toUpload - Files to upload
145
+ * @param {string} options.buildId - Build ID
146
+ * @param {Object} options.client - API client
147
+ * @param {AbortSignal} options.signal - Abort signal
148
+ * @param {number} options.batchSize - Batch size
149
+ * @param {Function} options.onProgress - Progress callback
150
+ * @param {Object} options.deps - Dependencies
151
+ * @param {Function} options.deps.createError - Error factory
152
+ * @returns {Promise<{ buildId: string, url: string|null }>}
153
+ */
154
+ export async function uploadFiles({
155
+ toUpload,
156
+ buildId,
157
+ client,
158
+ signal,
159
+ batchSize,
160
+ onProgress,
161
+ deps
162
+ }) {
163
+ let {
164
+ createError
165
+ } = deps;
166
+ let result = null;
167
+ if (toUpload.length === 0) {
168
+ return {
169
+ buildId,
170
+ url: null
171
+ };
172
+ }
173
+ for (let i = 0; i < toUpload.length; i += batchSize) {
174
+ if (signal.aborted) {
175
+ throw createError('Operation cancelled', 'UPLOAD_CANCELLED');
176
+ }
177
+ let batch = toUpload.slice(i, i + batchSize);
178
+ let form = new FormData();
179
+ form.append('build_id', buildId);
180
+ for (let file of batch) {
181
+ let blob = new Blob([file.buffer], {
182
+ type: 'image/png'
183
+ });
184
+ form.append('screenshots', blob, file.filename);
185
+ }
186
+ try {
187
+ result = await client.request('/api/sdk/upload', {
188
+ method: 'POST',
189
+ body: form,
190
+ signal,
191
+ headers: {}
192
+ });
193
+ } catch (err) {
194
+ throw createError(`Upload failed: ${err.message}`, 'UPLOAD_FAILED', {
195
+ batch: Math.floor(i / batchSize) + 1
196
+ });
197
+ }
198
+ onProgress(i + batch.length);
199
+ }
200
+ return {
201
+ buildId,
202
+ url: result?.build?.url || result?.url
203
+ };
204
+ }
205
+
206
+ // ============================================================================
207
+ // Build Waiting
208
+ // ============================================================================
209
+
210
+ /**
211
+ * Wait for a build to complete
212
+ * @param {Object} options - Options
213
+ * @param {string} options.buildId - Build ID
214
+ * @param {number} options.timeout - Timeout in ms
215
+ * @param {AbortSignal} options.signal - Abort signal
216
+ * @param {Object} options.client - API client
217
+ * @param {Object} options.deps - Dependencies
218
+ * @param {Function} options.deps.createError - Error factory
219
+ * @param {Function} options.deps.createTimeoutError - Timeout error factory
220
+ * @returns {Promise<Object>} Build result
221
+ */
222
+ export async function waitForBuild({
223
+ buildId,
224
+ timeout,
225
+ signal,
226
+ client,
227
+ deps
228
+ }) {
229
+ let {
230
+ createError,
231
+ createTimeoutError
232
+ } = deps;
233
+ let startTime = Date.now();
234
+ while (!isTimedOut(startTime, timeout)) {
235
+ if (signal.aborted) {
236
+ throw createError('Operation cancelled', 'UPLOAD_CANCELLED', {
237
+ buildId
238
+ });
239
+ }
240
+ let resp;
241
+ try {
242
+ resp = await client.request(`/api/sdk/builds/${buildId}`, {
243
+ signal
244
+ });
245
+ } catch (err) {
246
+ let code = extractStatusCodeFromError(err?.message);
247
+ throw createError(`Failed to check build status: ${code}`, 'BUILD_STATUS_FAILED');
248
+ }
249
+ let build = resp?.build ?? resp;
250
+ if (build.status === 'completed') {
251
+ return buildWaitResult(build);
252
+ }
253
+ if (build.status === 'failed') {
254
+ throw createError(`Build failed: ${build.error || 'Unknown error'}`, 'BUILD_FAILED');
255
+ }
256
+ }
257
+ throw createTimeoutError(`Build timed out after ${timeout}ms`, {
258
+ buildId,
259
+ timeout,
260
+ elapsed: getElapsedTime(startTime)
261
+ });
262
+ }
263
+
264
+ // ============================================================================
265
+ // Main Upload Operation
266
+ // ============================================================================
267
+
268
+ /**
269
+ * Upload screenshots to Vizzly
270
+ * @param {Object} options - Options
271
+ * @param {Object} options.uploadOptions - Upload options (screenshotsDir, buildName, etc.)
272
+ * @param {Object} options.config - Configuration
273
+ * @param {AbortSignal} options.signal - Abort signal
274
+ * @param {number} options.batchSize - Batch size
275
+ * @param {Object} options.deps - Dependencies
276
+ * @returns {Promise<Object>} Upload result
277
+ */
278
+ export async function upload({
279
+ uploadOptions,
280
+ config,
281
+ signal,
282
+ batchSize,
283
+ deps
284
+ }) {
285
+ let {
286
+ client,
287
+ createBuild,
288
+ getDefaultBranch,
289
+ glob,
290
+ readFile,
291
+ stat,
292
+ checkShas,
293
+ createError,
294
+ createValidationError,
295
+ createUploadError,
296
+ output
297
+ } = deps;
298
+ let {
299
+ screenshotsDir,
300
+ onProgress = () => {}
301
+ } = uploadOptions;
302
+ try {
303
+ // Validate API key
304
+ let apiKeyValidation = validateApiKey(config.apiKey);
305
+ if (!apiKeyValidation.valid) {
306
+ throw createValidationError(apiKeyValidation.error, {
307
+ config: {
308
+ apiKey: config.apiKey,
309
+ apiUrl: config.apiUrl
310
+ }
311
+ });
312
+ }
313
+
314
+ // Validate screenshots directory
315
+ let dirValidation = validateScreenshotsDir(screenshotsDir);
316
+ if (!dirValidation.valid) {
317
+ throw createValidationError(dirValidation.error);
318
+ }
319
+ let stats = await stat(screenshotsDir);
320
+ let statsValidation = validateDirectoryStats(stats, screenshotsDir);
321
+ if (!statsValidation.valid) {
322
+ throw createValidationError(statsValidation.error);
323
+ }
324
+
325
+ // Find screenshots
326
+ let files = await findScreenshots({
327
+ directory: screenshotsDir,
328
+ deps: {
329
+ glob
330
+ }
331
+ });
332
+ let filesValidation = validateFilesFound(files, screenshotsDir);
333
+ if (!filesValidation.valid) {
334
+ throw createUploadError(filesValidation.error, filesValidation.context);
335
+ }
336
+ onProgress(buildScanningProgress(files.length));
337
+
338
+ // Process files
339
+ let fileMetadata = await processFiles({
340
+ files,
341
+ signal,
342
+ onProgress: current => onProgress(buildProcessingProgress(current, files.length)),
343
+ deps: {
344
+ readFile,
345
+ createError
346
+ }
347
+ });
348
+
349
+ // Create build
350
+ let defaultBranch = await getDefaultBranch();
351
+ let buildInfo = buildBuildInfo(uploadOptions, defaultBranch);
352
+ let build = await createBuild(client, buildInfo);
353
+ let buildId = build.id;
354
+
355
+ // Check existing files
356
+ let {
357
+ toUpload,
358
+ existing,
359
+ screenshots
360
+ } = await checkExistingFiles({
361
+ fileMetadata,
362
+ client,
363
+ signal,
364
+ buildId,
365
+ deps: {
366
+ checkShas,
367
+ createError,
368
+ output
369
+ }
370
+ });
371
+ onProgress(buildDeduplicationProgress(toUpload.length, existing.length, files.length));
372
+
373
+ // Upload files
374
+ let result = await uploadFiles({
375
+ toUpload,
376
+ existing,
377
+ screenshots,
378
+ buildId,
379
+ buildInfo,
380
+ client,
381
+ signal,
382
+ batchSize,
383
+ onProgress: current => onProgress(buildUploadingProgress(current, toUpload.length)),
384
+ deps: {
385
+ createError
386
+ }
387
+ });
388
+ onProgress(buildCompletedProgress(result.buildId, result.url));
389
+ return buildUploadResult({
390
+ buildId: result.buildId,
391
+ url: result.url,
392
+ total: files.length,
393
+ uploaded: toUpload.length,
394
+ skipped: existing.length
395
+ });
396
+ } catch (error) {
397
+ output.debug('upload', 'failed', {
398
+ error: error.message
399
+ });
400
+
401
+ // Re-throw if already a VizzlyError
402
+ if (error.name?.includes('Error') && error.code) {
403
+ throw error;
404
+ }
405
+
406
+ // Wrap unknown errors
407
+ throw createUploadError(`Upload failed: ${error.message}`, {
408
+ originalError: error.message,
409
+ stack: error.stack
410
+ });
411
+ }
412
+ }
@@ -36,9 +36,11 @@ const uploadSchema = z.object({
36
36
  /**
37
37
  * Comparison configuration schema
38
38
  * threshold: CIEDE2000 Delta E units (0.0 = exact, 1.0 = JND, 2.0 = recommended, 3.0+ = permissive)
39
+ * minClusterSize: pixels (1 = exact)
39
40
  */
40
41
  const comparisonSchema = z.object({
41
- threshold: z.number().min(0).default(2.0)
42
+ threshold: z.number().min(0).default(2.0),
43
+ minClusterSize: z.int().min(1).default(2)
42
44
  });
43
45
 
44
46
  /**
@@ -70,11 +72,13 @@ export const vizzlyConfigSchema = z.object({
70
72
  timeout: 30000
71
73
  }),
72
74
  comparison: comparisonSchema.default({
73
- threshold: 2.0
75
+ threshold: 2.0,
76
+ minClusterSize: 2
74
77
  }),
75
78
  tdd: tddSchema.default({
76
79
  openReport: false
77
80
  }),
81
+ signatureProperties: z.array(z.string()).default([]),
78
82
  plugins: z.array(z.string()).default([]),
79
83
  // Additional optional fields
80
84
  parallelId: z.string().optional(),
@@ -99,7 +103,8 @@ export const vizzlyConfigSchema = z.object({
99
103
  timeout: 30000
100
104
  },
101
105
  comparison: {
102
- threshold: 2.0
106
+ threshold: 2.0,
107
+ minClusterSize: 2
103
108
  },
104
109
  tdd: {
105
110
  openReport: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.19.2",
3
+ "version": "0.20.1-beta.0",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -58,25 +58,22 @@
58
58
  ],
59
59
  "scripts": {
60
60
  "start": "node src/index.js",
61
- "build": "npm run clean && npm run compile && npm run copy-assets && npm run build:reporter && npm run copy-types",
61
+ "build": "npm run clean && npm run compile && npm run build:reporter && npm run copy-types",
62
62
  "clean": "rimraf dist",
63
63
  "compile": "babel src --out-dir dist --ignore '**/*.test.js'",
64
- "copy-assets": "cp src/services/report-generator/report.css dist/services/report-generator/",
65
64
  "copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
66
65
  "build:reporter": "cd src/reporter && vite build",
67
66
  "dev:reporter": "cd src/reporter && vite --config vite.dev.config.js",
68
67
  "test:types": "tsd",
69
68
  "prepublishOnly": "npm run build",
70
- "test": "vitest run",
71
- "test:watch": "vitest",
69
+ "test": "node --experimental-test-coverage --test --test-name-pattern='.*' $(find tests -name '*.test.js')",
70
+ "test:watch": "node --test --watch $(find tests -name '*.test.js')",
72
71
  "test:reporter": "playwright test --config=tests/reporter/playwright.config.js",
73
72
  "test:reporter:visual": "node bin/vizzly.js run \"npm run test:reporter\"",
74
- "lint": "biome lint src tests",
75
- "lint:fix": "biome lint --write src tests",
73
+ "lint": "biome check src tests",
74
+ "lint:fix": "biome check --write src tests",
76
75
  "format": "biome format --write src tests",
77
- "format:check": "biome format src tests",
78
- "check": "biome check src tests",
79
- "check:fix": "biome check --write src tests"
76
+ "format:check": "biome format src tests"
80
77
  },
81
78
  "engines": {
82
79
  "node": ">=22.0.0"
@@ -118,7 +115,6 @@
118
115
  "@tanstack/react-query": "^5.90.11",
119
116
  "@types/node": "^24.10.1",
120
117
  "@vitejs/plugin-react": "^5.0.3",
121
- "@vitest/coverage-v8": "^4.0.3",
122
118
  "autoprefixer": "^10.4.21",
123
119
  "babel-plugin-transform-remove-console": "^6.9.4",
124
120
  "postcss": "^8.5.6",
@@ -129,7 +125,6 @@
129
125
  "tsd": "^0.33.0",
130
126
  "typescript": "^5.0.4",
131
127
  "vite": "^7.1.7",
132
- "vitest": "^4.0.3",
133
128
  "wouter": "^3.7.1"
134
129
  }
135
130
  }