@vizzly-testing/cli 0.28.1 → 0.29.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.
@@ -110,11 +110,27 @@ export async function buildsCommand(options = {}, globalOptions = {}, deps = {})
110
110
  for (let build of builds) {
111
111
  let statusColor = getStatusColor(colors, build.status);
112
112
  let statusBadge = statusColor(build.status.toUpperCase());
113
- output.print(` ${colors.bold(build.name || build.id)} ${statusBadge}`);
113
+
114
+ // Approval badge
115
+ let approvalBadge = '';
116
+ if (build.approval_status && build.status === 'completed') {
117
+ approvalBadge = ` ${getApprovalBadge(colors, build.approval_status)}`;
118
+ }
119
+ output.print(` ${colors.bold(build.name || build.id)} ${statusBadge}${approvalBadge}`);
114
120
  let details = [];
115
121
  if (build.branch) details.push(build.branch);
116
122
  if (build.commit_sha) details.push(build.commit_sha.substring(0, 7));
117
123
  if (build.screenshot_count) details.push(`${build.screenshot_count} screenshots`);
124
+
125
+ // Comparison counts summary
126
+ let compParts = [];
127
+ let changed = build.changed_comparisons || 0;
128
+ let identical = build.identical_comparisons || 0;
129
+ let newCount = build.new_comparisons || 0;
130
+ if (changed > 0) compParts.push(`${changed} changed`);
131
+ if (newCount > 0) compParts.push(`${newCount} new`);
132
+ if (identical > 0) compParts.push(`${identical} identical`);
133
+ if (compParts.length > 0) details.push(compParts.join(' · '));
118
134
  if (details.length > 0) {
119
135
  output.print(` ${colors.dim(details.join(' · '))}`);
120
136
  }
@@ -161,13 +177,35 @@ function formatBuildForJson(build, includeComparisons = false) {
161
177
  completedAt: build.completed_at
162
178
  };
163
179
  if (includeComparisons && build.comparisons) {
164
- result.comparisonDetails = build.comparisons.map(c => ({
165
- id: c.id,
166
- name: c.name,
167
- status: c.status,
168
- diffPercentage: c.diff_percentage,
169
- approvalStatus: c.approval_status
170
- }));
180
+ result.comparisonDetails = build.comparisons.map(c => {
181
+ let diffUrl = c.diff_image?.url || c.diff_image_url || c.diff_url || null;
182
+ let diffImage = c.diff_image || {};
183
+ let clusterMetadata = c.cluster_metadata || diffImage.cluster_metadata || null;
184
+ let ssimScore = c.ssim_score ?? diffImage.ssim_score ?? null;
185
+ let gmsdScore = c.gmsd_score ?? diffImage.gmsd_score ?? null;
186
+ let fingerprintHash = c.fingerprint_hash || diffImage.fingerprint_hash || null;
187
+ let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash;
188
+ return {
189
+ id: c.id,
190
+ name: c.name || c.current_name,
191
+ status: c.status,
192
+ diffPercentage: c.diff_percentage,
193
+ approvalStatus: c.approval_status,
194
+ urls: {
195
+ baseline: c.baseline_screenshot?.original_url || c.baseline_original_url || c.baseline_screenshot_url || null,
196
+ current: c.current_screenshot?.original_url || c.current_original_url || c.current_screenshot_url || null,
197
+ diff: diffUrl
198
+ },
199
+ honeydiff: hasHoneydiff ? {
200
+ ssimScore,
201
+ gmsdScore,
202
+ clusterClassification: clusterMetadata?.classification || null,
203
+ clusterMetadata,
204
+ fingerprintHash,
205
+ diffRegions: c.diff_regions ?? diffImage.diff_regions ?? null
206
+ } : null
207
+ };
208
+ });
171
209
  }
172
210
  return result;
173
211
  }
@@ -214,8 +252,17 @@ function displayBuild(output, build, verbose) {
214
252
  output.blank();
215
253
  output.labelValue('Comparisons', '');
216
254
  for (let comp of build.comparisons.slice(0, verbose ? 50 : 10)) {
217
- let statusIcon = getComparisonStatusIcon(colors, comp.status);
218
- output.print(` ${statusIcon} ${comp.name}`);
255
+ let resultIcon = getComparisonStatusIcon(colors, comp.result || comp.status);
256
+ let compName = comp.name || comp.current_name || comp.id;
257
+ let diffInfo = '';
258
+ if (comp.diff_percentage > 0) {
259
+ diffInfo = colors.dim(` (${comp.diff_percentage.toFixed(2)}%)`);
260
+ }
261
+ let classification = '';
262
+ if (verbose && comp.cluster_metadata?.classification) {
263
+ classification = colors.dim(` [${comp.cluster_metadata.classification}]`);
264
+ }
265
+ output.print(` ${resultIcon} ${compName}${diffInfo}${classification}`);
219
266
  }
220
267
  if (build.comparisons.length > (verbose ? 50 : 10)) {
221
268
  output.hint(` ... and ${build.comparisons.length - (verbose ? 50 : 10)} more`);
@@ -240,6 +287,23 @@ function getStatusColor(colors, status) {
240
287
  }
241
288
  }
242
289
 
290
+ /**
291
+ * Get colored approval badge
292
+ */
293
+ function getApprovalBadge(colors, approvalStatus) {
294
+ switch (approvalStatus) {
295
+ case 'approved':
296
+ case 'auto_approved':
297
+ return colors.brand.success('APPROVED');
298
+ case 'rejected':
299
+ return colors.brand.error('REJECTED');
300
+ case 'pending':
301
+ return colors.brand.warning('PENDING');
302
+ default:
303
+ return colors.dim(approvalStatus?.toUpperCase() || '');
304
+ }
305
+ }
306
+
243
307
  /**
244
308
  * Get icon for comparison status
245
309
  */
@@ -154,6 +154,16 @@ export async function comparisonsCommand(options = {}, globalOptions = {}, deps
154
154
  * Format a comparison for JSON output
155
155
  */
156
156
  function formatComparisonForJson(comparison) {
157
+ // API endpoints return different shapes:
158
+ // - Single comparison: nested baseline_screenshot/current_screenshot + flat diff_url, honeydiff at top level
159
+ // - Build comparisons: flat diff_url/diff_image_url, no storage URLs, limited honeydiff
160
+ // - Search: nested diff_image with honeydiff, no current/baseline URLs
161
+ let diffImage = comparison.diff_image || {};
162
+ let clusterMetadata = comparison.cluster_metadata || diffImage.cluster_metadata || null;
163
+ let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null;
164
+ let gmsdScore = comparison.gmsd_score ?? diffImage.gmsd_score ?? null;
165
+ let fingerprintHash = comparison.fingerprint_hash || diffImage.fingerprint_hash || null;
166
+ let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash;
157
167
  return {
158
168
  id: comparison.id,
159
169
  name: comparison.name,
@@ -166,10 +176,20 @@ function formatComparisonForJson(comparison) {
166
176
  } : null,
167
177
  browser: comparison.browser || null,
168
178
  urls: {
169
- baseline: comparison.baseline_screenshot?.original_url || null,
170
- current: comparison.current_screenshot?.original_url || null,
171
- diff: comparison.diff_image?.url || null
179
+ baseline: comparison.baseline_screenshot?.original_url || comparison.baseline_original_url || comparison.baseline_screenshot_url || null,
180
+ current: comparison.current_screenshot?.original_url || comparison.current_original_url || comparison.current_screenshot_url || null,
181
+ diff: comparison.diff_image?.url || comparison.diff_image_url || comparison.diff_url || null
172
182
  },
183
+ honeydiff: hasHoneydiff ? {
184
+ ssimScore,
185
+ gmsdScore,
186
+ clusterClassification: clusterMetadata?.classification || null,
187
+ clusterMetadata,
188
+ fingerprintHash,
189
+ diffRegions: comparison.diff_regions ?? diffImage.diff_regions ?? null,
190
+ diffLines: comparison.diff_lines ?? diffImage.diff_lines ?? null,
191
+ fingerprintData: comparison.fingerprint_data ?? diffImage.fingerprint_data ?? null
192
+ } : null,
173
193
  buildId: comparison.build_id,
174
194
  buildName: comparison.build_name,
175
195
  buildBranch: comparison.build_branch,
@@ -209,18 +229,40 @@ function displayComparison(output, comparison, verbose) {
209
229
  output.labelValue('Commit', comparison.build_commit_sha.substring(0, 8));
210
230
  }
211
231
 
212
- // URLs in verbose mode
232
+ // Honeydiff analysis in verbose mode
213
233
  if (verbose) {
214
- output.blank();
215
- output.labelValue('URLs', '');
216
- if (comparison.baseline_screenshot?.original_url) {
217
- output.print(` Baseline: ${comparison.baseline_screenshot.original_url}`);
218
- }
219
- if (comparison.current_screenshot?.original_url) {
220
- output.print(` Current: ${comparison.current_screenshot.original_url}`);
234
+ let clusterMetadata = comparison.cluster_metadata || comparison.diff_image?.cluster_metadata;
235
+ let ssim = comparison.ssim_score ?? comparison.diff_image?.ssim_score;
236
+ let gmsd = comparison.gmsd_score ?? comparison.diff_image?.gmsd_score;
237
+ let fingerprint = comparison.fingerprint_hash || comparison.diff_image?.fingerprint_hash;
238
+ if (clusterMetadata || ssim != null || gmsd != null || fingerprint) {
239
+ output.blank();
240
+ if (clusterMetadata?.classification) {
241
+ output.labelValue('Classification', clusterMetadata.classification);
242
+ }
243
+ if (ssim != null) {
244
+ output.labelValue('SSIM', ssim.toFixed(4));
245
+ }
246
+ if (gmsd != null) {
247
+ output.labelValue('GMSD', gmsd.toFixed(4));
248
+ }
249
+ if (fingerprint) {
250
+ output.labelValue('Fingerprint', fingerprint);
251
+ }
221
252
  }
222
- if (comparison.diff_image?.url) {
223
- output.print(` Diff: ${comparison.diff_image.url}`);
253
+ }
254
+
255
+ // URLs in verbose mode
256
+ if (verbose) {
257
+ let baselineUrl = comparison.baseline_screenshot?.original_url || comparison.baseline_original_url || comparison.baseline_screenshot_url;
258
+ let currentUrl = comparison.current_screenshot?.original_url || comparison.current_original_url || comparison.current_screenshot_url;
259
+ let diffUrl = comparison.diff_image?.url || comparison.diff_image_url || comparison.diff_url;
260
+ if (baselineUrl || currentUrl || diffUrl) {
261
+ output.blank();
262
+ output.labelValue('URLs', '');
263
+ if (baselineUrl) output.print(` Baseline: ${baselineUrl}`);
264
+ if (currentUrl) output.print(` Current: ${currentUrl}`);
265
+ if (diffUrl) output.print(` Diff: ${diffUrl}`);
224
266
  }
225
267
  }
226
268
  if (comparison.created_at) {
@@ -258,7 +300,8 @@ function displayBuildComparisons(output, build, comparisons, verbose) {
258
300
  for (let comp of comparisons.slice(0, verbose ? 100 : 20)) {
259
301
  let icon = getStatusIcon(colors, comp.status);
260
302
  let diffInfo = comp.diff_percentage != null ? colors.dim(` (${(comp.diff_percentage * 100).toFixed(1)}%)`) : '';
261
- output.print(` ${icon} ${comp.name}${diffInfo}`);
303
+ let classification = verbose ? getClassificationLabel(colors, comp.cluster_metadata) : '';
304
+ output.print(` ${icon} ${comp.name}${diffInfo}${classification}`);
262
305
  }
263
306
  if (comparisons.length > (verbose ? 100 : 20)) {
264
307
  output.blank();
@@ -299,7 +342,8 @@ function displaySearchResults(output, comparisons, searchPattern, pagination, ve
299
342
  }
300
343
  for (let comp of group.comparisons.slice(0, verbose ? 10 : 3)) {
301
344
  let icon = getStatusIcon(colors, comp.status);
302
- output.print(` ${icon} ${comp.name}`);
345
+ let classification = verbose ? getClassificationLabel(colors, comp.cluster_metadata || comp.diff_image?.cluster_metadata) : '';
346
+ output.print(` ${icon} ${comp.name}${classification}`);
303
347
  }
304
348
  if (group.comparisons.length > (verbose ? 10 : 3)) {
305
349
  output.print(` ${colors.dim(`... and ${group.comparisons.length - (verbose ? 10 : 3)} more`)}`);
@@ -311,6 +355,15 @@ function displaySearchResults(output, comparisons, searchPattern, pagination, ve
311
355
  }
312
356
  }
313
357
 
358
+ /**
359
+ * Get a classification label for verbose display
360
+ */
361
+ function getClassificationLabel(colors, clusterMetadata) {
362
+ let classification = clusterMetadata?.classification;
363
+ if (!classification) return '';
364
+ return colors.dim(` [${classification}]`);
365
+ }
366
+
314
367
  /**
315
368
  * Get icon for comparison status
316
369
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.28.1",
3
+ "version": "0.29.1",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",