From 30063eb3b9ddd01cd6e012328681797c9f18a860 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 8 Feb 2026 23:55:14 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Surface=20honeydiff=20analysis?= =?UTF-8?q?=20data=20in=20comparisons=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose SSIM/GMSD scores, cluster classification, fingerprint hashes, and diff regions from the API in both --json and --verbose output. --- src/commands/comparisons.js | 64 ++++++++++- tests/commands/comparisons.test.js | 163 +++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 2 deletions(-) diff --git a/src/commands/comparisons.js b/src/commands/comparisons.js index e734c4b..f4b4892 100644 --- a/src/commands/comparisons.js +++ b/src/commands/comparisons.js @@ -186,6 +186,17 @@ export async function comparisonsCommand( * Format a comparison for JSON output */ function formatComparisonForJson(comparison) { + // Honeydiff fields come from different shapes depending on the endpoint: + // - Single comparison & build detail: top-level fields + // - Search results: nested under diff_image + let diffImage = comparison.diff_image || {}; + let clusterMetadata = comparison.cluster_metadata || diffImage.cluster_metadata || null; + let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null; + let gmsdScore = comparison.gmsd_score ?? diffImage.gmsd_score ?? null; + let fingerprintHash = comparison.fingerprint_hash || diffImage.fingerprint_hash || null; + + let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash; + return { id: comparison.id, name: comparison.name, @@ -201,6 +212,16 @@ function formatComparisonForJson(comparison) { current: comparison.current_screenshot?.original_url || null, diff: comparison.diff_image?.url || null, }, + honeydiff: hasHoneydiff ? { + ssimScore, + gmsdScore, + clusterClassification: clusterMetadata?.classification || null, + clusterMetadata, + fingerprintHash, + diffRegions: comparison.diff_regions || diffImage.diff_regions || null, + diffLines: comparison.diff_lines || null, + fingerprintData: comparison.fingerprint_data || null, + } : null, buildId: comparison.build_id, buildName: comparison.build_name, buildBranch: comparison.build_branch, @@ -251,6 +272,30 @@ function displayComparison(output, comparison, verbose) { output.labelValue('Commit', comparison.build_commit_sha.substring(0, 8)); } + // Honeydiff analysis in verbose mode + if (verbose) { + let clusterMetadata = comparison.cluster_metadata || comparison.diff_image?.cluster_metadata; + let ssim = comparison.ssim_score ?? comparison.diff_image?.ssim_score; + let gmsd = comparison.gmsd_score ?? comparison.diff_image?.gmsd_score; + let fingerprint = comparison.fingerprint_hash || comparison.diff_image?.fingerprint_hash; + + if (clusterMetadata || ssim != null || gmsd != null) { + output.blank(); + if (clusterMetadata?.classification) { + output.labelValue('Classification', clusterMetadata.classification); + } + if (ssim != null) { + output.labelValue('SSIM', ssim.toFixed(4)); + } + if (gmsd != null) { + output.labelValue('GMSD', gmsd.toFixed(4)); + } + if (fingerprint) { + output.labelValue('Fingerprint', fingerprint); + } + } + } + // URLs in verbose mode if (verbose) { output.blank(); @@ -312,7 +357,10 @@ function displayBuildComparisons(output, build, comparisons, verbose) { comp.diff_percentage != null ? colors.dim(` (${(comp.diff_percentage * 100).toFixed(1)}%)`) : ''; - output.print(` ${icon} ${comp.name}${diffInfo}`); + let classification = verbose + ? getClassificationLabel(colors, comp.cluster_metadata) + : ''; + output.print(` ${icon} ${comp.name}${diffInfo}${classification}`); } if (comparisons.length > (verbose ? 100 : 20)) { @@ -366,7 +414,10 @@ function displaySearchResults( for (let comp of group.comparisons.slice(0, verbose ? 10 : 3)) { let icon = getStatusIcon(colors, comp.status); - output.print(` ${icon} ${comp.name}`); + let classification = verbose + ? getClassificationLabel(colors, comp.cluster_metadata || comp.diff_image?.cluster_metadata) + : ''; + output.print(` ${icon} ${comp.name}${classification}`); } if (group.comparisons.length > (verbose ? 10 : 3)) { @@ -386,6 +437,15 @@ function displaySearchResults( } } +/** + * Get a classification label for verbose display + */ +function getClassificationLabel(colors, clusterMetadata) { + let classification = clusterMetadata?.classification; + if (!classification) return ''; + return colors.dim(` [${classification}]`); +} + /** * Get icon for comparison status */ diff --git a/tests/commands/comparisons.test.js b/tests/commands/comparisons.test.js index a5430bd..bd01da7 100644 --- a/tests/commands/comparisons.test.js +++ b/tests/commands/comparisons.test.js @@ -280,5 +280,168 @@ describe('commands/comparisons', () => { assert.strictEqual(dataCall.args[0].id, 'comp-1'); assert.strictEqual(dataCall.args[0].name, 'button-primary'); }); + + it('includes honeydiff data in JSON output for single comparison', async () => { + let output = createMockOutput(); + let mockComparison = { + id: 'comp-1', + name: 'button-primary', + status: 'changed', + diff_percentage: 0.025, + cluster_metadata: { + classification: 'minor', + density: 0.05, + distribution: 'localized', + }, + ssim_score: 0.9876, + gmsd_score: 0.0123, + fingerprint_hash: 'abc123def456', + diff_regions: [{ x: 10, y: 20, width: 50, height: 30 }], + diff_lines: [20, 30, 40], + fingerprint_data: { hash_components: [1, 2, 3] }, + }; + + await comparisonsCommand( + { id: 'comp-1' }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getComparison: async () => mockComparison, + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + let result = dataCall.args[0]; + assert.ok(result.honeydiff, 'Should include honeydiff data'); + assert.strictEqual(result.honeydiff.ssimScore, 0.9876); + assert.strictEqual(result.honeydiff.gmsdScore, 0.0123); + assert.strictEqual(result.honeydiff.clusterClassification, 'minor'); + assert.strictEqual(result.honeydiff.fingerprintHash, 'abc123def456'); + assert.deepStrictEqual(result.honeydiff.diffRegions, [ + { x: 10, y: 20, width: 50, height: 30 }, + ]); + assert.deepStrictEqual(result.honeydiff.diffLines, [20, 30, 40]); + }); + + it('includes honeydiff data from search results (nested in diff_image)', async () => { + let output = createMockOutput(); + let mockComparisons = [ + { + id: 'comp-1', + name: 'button-primary', + status: 'changed', + build_id: 'b1', + diff_image: { + url: 'https://example.com/diff.png', + cluster_metadata: { + classification: 'dynamic_content', + density: 0.12, + }, + ssim_score: 0.9521, + gmsd_score: 0.0345, + fingerprint_hash: 'search-hash-789', + }, + }, + ]; + + await comparisonsCommand( + { name: 'button-*' }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + searchComparisons: async () => ({ + comparisons: mockComparisons, + pagination: { total: 1, hasMore: false }, + }), + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + let result = dataCall.args[0].comparisons[0]; + assert.ok(result.honeydiff, 'Should include honeydiff data'); + assert.strictEqual(result.honeydiff.ssimScore, 0.9521); + assert.strictEqual(result.honeydiff.gmsdScore, 0.0345); + assert.strictEqual(result.honeydiff.clusterClassification, 'dynamic_content'); + assert.strictEqual(result.honeydiff.fingerprintHash, 'search-hash-789'); + }); + + it('sets honeydiff to null when no analysis data present', async () => { + let output = createMockOutput(); + let mockComparison = { + id: 'comp-1', + name: 'button-primary', + status: 'identical', + }; + + await comparisonsCommand( + { id: 'comp-1' }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getComparison: async () => mockComparison, + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + assert.strictEqual(dataCall.args[0].honeydiff, null); + }); + + it('shows honeydiff analysis in verbose display', async () => { + let output = createMockOutput(); + let mockComparison = { + id: 'comp-1', + name: 'button-primary', + status: 'changed', + diff_percentage: 0.025, + cluster_metadata: { classification: 'minor' }, + ssim_score: 0.9876, + gmsd_score: 0.0123, + fingerprint_hash: 'abc123', + }; + + await comparisonsCommand( + { id: 'comp-1' }, + { verbose: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getComparison: async () => mockComparison, + output, + exit: () => {}, + } + ); + + let labelValues = output.calls + .filter(c => c.method === 'labelValue') + .map(c => c.args); + assert.ok( + labelValues.some(([label]) => label === 'Classification'), + 'Should show classification' + ); + assert.ok( + labelValues.some(([label]) => label === 'SSIM'), + 'Should show SSIM score' + ); + assert.ok( + labelValues.some(([label]) => label === 'GMSD'), + 'Should show GMSD score' + ); + assert.ok( + labelValues.some(([label]) => label === 'Fingerprint'), + 'Should show fingerprint' + ); + }); }); }); From 0d71e85893794483aa74fb44a8360852e41d2eaf Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Mon, 9 Feb 2026 00:23:28 -0600 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20Surface=20URLs=20and=20honeydif?= =?UTF-8?q?f=20data=20across=20comparison/build=20outputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle all API response shapes for image URLs: nested objects (single comparison), flat fields (build detail), and diff_image (search). Add URLs and honeydiff data to builds --comparisons --json output. --- src/commands/builds.js | 43 ++++++++++++++--- src/commands/comparisons.js | 52 +++++++++++++-------- tests/commands/builds.test.js | 74 ++++++++++++++++++++++++++++++ tests/commands/comparisons.test.js | 68 +++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 27 deletions(-) diff --git a/src/commands/builds.js b/src/commands/builds.js index 137541d..f784bd6 100644 --- a/src/commands/builds.js +++ b/src/commands/builds.js @@ -192,13 +192,42 @@ function formatBuildForJson(build, includeComparisons = false) { }; if (includeComparisons && build.comparisons) { - result.comparisonDetails = build.comparisons.map(c => ({ - id: c.id, - name: c.name, - status: c.status, - diffPercentage: c.diff_percentage, - approvalStatus: c.approval_status, - })); + result.comparisonDetails = build.comparisons.map(c => { + let diffUrl = c.diff_image?.url || c.diff_image_url || c.diff_url || null; + let diffImage = c.diff_image || {}; + let clusterMetadata = c.cluster_metadata || diffImage.cluster_metadata || null; + let ssimScore = c.ssim_score ?? diffImage.ssim_score ?? null; + let gmsdScore = c.gmsd_score ?? diffImage.gmsd_score ?? null; + let fingerprintHash = c.fingerprint_hash || diffImage.fingerprint_hash || null; + let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash; + + return { + id: c.id, + name: c.name, + status: c.status, + diffPercentage: c.diff_percentage, + approvalStatus: c.approval_status, + urls: { + baseline: c.baseline_screenshot?.original_url + || c.baseline_original_url + || c.baseline_screenshot_url + || null, + current: c.current_screenshot?.original_url + || c.current_original_url + || c.current_screenshot_url + || null, + diff: diffUrl, + }, + honeydiff: hasHoneydiff ? { + ssimScore, + gmsdScore, + clusterClassification: clusterMetadata?.classification || null, + clusterMetadata, + fingerprintHash, + diffRegions: c.diff_regions || diffImage.diff_regions || null, + } : null, + }; + }); } return result; diff --git a/src/commands/comparisons.js b/src/commands/comparisons.js index f4b4892..bcc49d6 100644 --- a/src/commands/comparisons.js +++ b/src/commands/comparisons.js @@ -186,9 +186,10 @@ export async function comparisonsCommand( * Format a comparison for JSON output */ function formatComparisonForJson(comparison) { - // Honeydiff fields come from different shapes depending on the endpoint: - // - Single comparison & build detail: top-level fields - // - Search results: nested under diff_image + // API endpoints return different shapes: + // - Single comparison: nested baseline_screenshot/current_screenshot + flat diff_url, honeydiff at top level + // - Build comparisons: flat diff_url/diff_image_url, no storage URLs, limited honeydiff + // - Search: nested diff_image with honeydiff, no current/baseline URLs let diffImage = comparison.diff_image || {}; let clusterMetadata = comparison.cluster_metadata || diffImage.cluster_metadata || null; let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null; @@ -208,9 +209,18 @@ function formatComparisonForJson(comparison) { : null, browser: comparison.browser || null, urls: { - baseline: comparison.baseline_screenshot?.original_url || null, - current: comparison.current_screenshot?.original_url || null, - diff: comparison.diff_image?.url || null, + baseline: comparison.baseline_screenshot?.original_url + || comparison.baseline_original_url + || comparison.baseline_screenshot_url + || null, + current: comparison.current_screenshot?.original_url + || comparison.current_original_url + || comparison.current_screenshot_url + || null, + diff: comparison.diff_image?.url + || comparison.diff_image_url + || comparison.diff_url + || null, }, honeydiff: hasHoneydiff ? { ssimScore, @@ -298,20 +308,22 @@ function displayComparison(output, comparison, verbose) { // URLs in verbose mode if (verbose) { - output.blank(); - output.labelValue('URLs', ''); - if (comparison.baseline_screenshot?.original_url) { - output.print( - ` Baseline: ${comparison.baseline_screenshot.original_url}` - ); - } - if (comparison.current_screenshot?.original_url) { - output.print( - ` Current: ${comparison.current_screenshot.original_url}` - ); - } - if (comparison.diff_image?.url) { - output.print(` Diff: ${comparison.diff_image.url}`); + let baselineUrl = comparison.baseline_screenshot?.original_url + || comparison.baseline_original_url + || comparison.baseline_screenshot_url; + let currentUrl = comparison.current_screenshot?.original_url + || comparison.current_original_url + || comparison.current_screenshot_url; + let diffUrl = comparison.diff_image?.url + || comparison.diff_image_url + || comparison.diff_url; + + if (baselineUrl || currentUrl || diffUrl) { + output.blank(); + output.labelValue('URLs', ''); + if (baselineUrl) output.print(` Baseline: ${baselineUrl}`); + if (currentUrl) output.print(` Current: ${currentUrl}`); + if (diffUrl) output.print(` Diff: ${diffUrl}`); } } diff --git a/tests/commands/builds.test.js b/tests/commands/builds.test.js index 678192d..680ea73 100644 --- a/tests/commands/builds.test.js +++ b/tests/commands/builds.test.js @@ -212,6 +212,80 @@ describe('commands/builds', () => { assert.strictEqual(capturedFilters.limit, 10); }); + it('includes URLs and honeydiff in comparisonDetails', async () => { + let output = createMockOutput(); + let mockBuild = { + id: 'build-1', + name: 'Build 1', + status: 'completed', + comparisons: [ + { + id: 'comp-1', + name: 'button-primary', + status: 'changed', + diff_percentage: 0.025, + diff_image_url: 'https://cdn.example.com/diff.png', + cluster_metadata: { classification: 'minor', density: 0.05 }, + ssim_score: 0.9876, + gmsd_score: 0.0123, + fingerprint_hash: 'abc123', + diff_regions: [{ x: 10, y: 20 }], + }, + ], + }; + + await buildsCommand( + { build: 'build-1', comparisons: true }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getBuild: async () => ({ build: mockBuild }), + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + let comp = dataCall.args[0].comparisonDetails[0]; + assert.strictEqual(comp.urls.diff, 'https://cdn.example.com/diff.png'); + assert.ok(comp.honeydiff, 'Should include honeydiff data'); + assert.strictEqual(comp.honeydiff.ssimScore, 0.9876); + assert.strictEqual(comp.honeydiff.clusterClassification, 'minor'); + assert.strictEqual(comp.honeydiff.fingerprintHash, 'abc123'); + }); + + it('sets honeydiff to null when no analysis data in comparisons', async () => { + let output = createMockOutput(); + let mockBuild = { + id: 'build-1', + name: 'Build 1', + status: 'completed', + comparisons: [ + { id: 'comp-1', name: 'button-primary', status: 'identical' }, + ], + }; + + await buildsCommand( + { build: 'build-1', comparisons: true }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getBuild: async () => ({ build: mockBuild }), + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + let comp = dataCall.args[0].comparisonDetails[0]; + assert.strictEqual(comp.honeydiff, null); + assert.strictEqual(comp.urls.diff, null); + }); + it('passes project filter to API', async () => { let output = createMockOutput(); let capturedFilters = null; diff --git a/tests/commands/comparisons.test.js b/tests/commands/comparisons.test.js index bd01da7..a5e9273 100644 --- a/tests/commands/comparisons.test.js +++ b/tests/commands/comparisons.test.js @@ -398,6 +398,74 @@ describe('commands/comparisons', () => { assert.strictEqual(dataCall.args[0].honeydiff, null); }); + it('resolves URLs from flat field names (single comparison endpoint)', async () => { + let output = createMockOutput(); + let mockComparison = { + id: 'comp-1', + name: 'button-primary', + status: 'changed', + baseline_screenshot_url: 'https://cdn.example.com/baseline.png', + current_screenshot_url: 'https://cdn.example.com/current.png', + diff_url: 'https://cdn.example.com/diff.png', + }; + + await comparisonsCommand( + { id: 'comp-1' }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getComparison: async () => mockComparison, + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + let urls = dataCall.args[0].urls; + assert.strictEqual(urls.baseline, 'https://cdn.example.com/baseline.png'); + assert.strictEqual(urls.current, 'https://cdn.example.com/current.png'); + assert.strictEqual(urls.diff, 'https://cdn.example.com/diff.png'); + }); + + it('resolves URLs from build detail field names (diff_image_url)', async () => { + let output = createMockOutput(); + let mockBuild = { + id: 'build-1', + name: 'Build 1', + comparisons: [ + { + id: 'comp-1', + name: 'button-primary', + status: 'changed', + diff_image_url: 'https://cdn.example.com/diff.png', + baseline_original_url: 'https://cdn.example.com/baseline.png', + current_original_url: 'https://cdn.example.com/current.png', + }, + ], + }; + + await comparisonsCommand( + { build: 'build-1' }, + { json: true }, + { + loadConfig: async () => ({ apiKey: 'test-token' }), + createApiClient: () => ({}), + getBuild: async () => ({ build: mockBuild }), + output, + exit: () => {}, + } + ); + + let dataCall = output.calls.find(c => c.method === 'data'); + assert.ok(dataCall); + let urls = dataCall.args[0].comparisons[0].urls; + assert.strictEqual(urls.diff, 'https://cdn.example.com/diff.png'); + assert.strictEqual(urls.baseline, 'https://cdn.example.com/baseline.png'); + assert.strictEqual(urls.current, 'https://cdn.example.com/current.png'); + }); + it('shows honeydiff analysis in verbose display', async () => { let output = createMockOutput(); let mockComparison = { From d5a50693e65919abd29275d6eb45ca401105d388 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Mon, 9 Feb 2026 00:25:31 -0600 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20biome=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/builds.js | 48 +++++++++------ src/commands/comparisons.js | 94 ++++++++++++++++++------------ tests/commands/comparisons.test.js | 5 +- 3 files changed, 90 insertions(+), 57 deletions(-) diff --git a/src/commands/builds.js b/src/commands/builds.js index f784bd6..5f67ae6 100644 --- a/src/commands/builds.js +++ b/src/commands/builds.js @@ -195,11 +195,17 @@ function formatBuildForJson(build, includeComparisons = false) { result.comparisonDetails = build.comparisons.map(c => { let diffUrl = c.diff_image?.url || c.diff_image_url || c.diff_url || null; let diffImage = c.diff_image || {}; - let clusterMetadata = c.cluster_metadata || diffImage.cluster_metadata || null; + let clusterMetadata = + c.cluster_metadata || diffImage.cluster_metadata || null; let ssimScore = c.ssim_score ?? diffImage.ssim_score ?? null; let gmsdScore = c.gmsd_score ?? diffImage.gmsd_score ?? null; - let fingerprintHash = c.fingerprint_hash || diffImage.fingerprint_hash || null; - let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash; + let fingerprintHash = + c.fingerprint_hash || diffImage.fingerprint_hash || null; + let hasHoneydiff = + clusterMetadata || + ssimScore != null || + gmsdScore != null || + fingerprintHash; return { id: c.id, @@ -208,24 +214,28 @@ function formatBuildForJson(build, includeComparisons = false) { diffPercentage: c.diff_percentage, approvalStatus: c.approval_status, urls: { - baseline: c.baseline_screenshot?.original_url - || c.baseline_original_url - || c.baseline_screenshot_url - || null, - current: c.current_screenshot?.original_url - || c.current_original_url - || c.current_screenshot_url - || null, + baseline: + c.baseline_screenshot?.original_url || + c.baseline_original_url || + c.baseline_screenshot_url || + null, + current: + c.current_screenshot?.original_url || + c.current_original_url || + c.current_screenshot_url || + null, diff: diffUrl, }, - honeydiff: hasHoneydiff ? { - ssimScore, - gmsdScore, - clusterClassification: clusterMetadata?.classification || null, - clusterMetadata, - fingerprintHash, - diffRegions: c.diff_regions || diffImage.diff_regions || null, - } : null, + honeydiff: hasHoneydiff + ? { + ssimScore, + gmsdScore, + clusterClassification: clusterMetadata?.classification || null, + clusterMetadata, + fingerprintHash, + diffRegions: c.diff_regions || diffImage.diff_regions || null, + } + : null, }; }); } diff --git a/src/commands/comparisons.js b/src/commands/comparisons.js index bcc49d6..74d3830 100644 --- a/src/commands/comparisons.js +++ b/src/commands/comparisons.js @@ -191,12 +191,18 @@ function formatComparisonForJson(comparison) { // - Build comparisons: flat diff_url/diff_image_url, no storage URLs, limited honeydiff // - Search: nested diff_image with honeydiff, no current/baseline URLs let diffImage = comparison.diff_image || {}; - let clusterMetadata = comparison.cluster_metadata || diffImage.cluster_metadata || null; + let clusterMetadata = + comparison.cluster_metadata || diffImage.cluster_metadata || null; let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null; let gmsdScore = comparison.gmsd_score ?? diffImage.gmsd_score ?? null; - let fingerprintHash = comparison.fingerprint_hash || diffImage.fingerprint_hash || null; + let fingerprintHash = + comparison.fingerprint_hash || diffImage.fingerprint_hash || null; - let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash; + let hasHoneydiff = + clusterMetadata || + ssimScore != null || + gmsdScore != null || + fingerprintHash; return { id: comparison.id, @@ -209,29 +215,35 @@ function formatComparisonForJson(comparison) { : null, browser: comparison.browser || null, urls: { - baseline: comparison.baseline_screenshot?.original_url - || comparison.baseline_original_url - || comparison.baseline_screenshot_url - || null, - current: comparison.current_screenshot?.original_url - || comparison.current_original_url - || comparison.current_screenshot_url - || null, - diff: comparison.diff_image?.url - || comparison.diff_image_url - || comparison.diff_url - || null, + baseline: + comparison.baseline_screenshot?.original_url || + comparison.baseline_original_url || + comparison.baseline_screenshot_url || + null, + current: + comparison.current_screenshot?.original_url || + comparison.current_original_url || + comparison.current_screenshot_url || + null, + diff: + comparison.diff_image?.url || + comparison.diff_image_url || + comparison.diff_url || + null, }, - honeydiff: hasHoneydiff ? { - ssimScore, - gmsdScore, - clusterClassification: clusterMetadata?.classification || null, - clusterMetadata, - fingerprintHash, - diffRegions: comparison.diff_regions || diffImage.diff_regions || null, - diffLines: comparison.diff_lines || null, - fingerprintData: comparison.fingerprint_data || null, - } : null, + honeydiff: hasHoneydiff + ? { + ssimScore, + gmsdScore, + clusterClassification: clusterMetadata?.classification || null, + clusterMetadata, + fingerprintHash, + diffRegions: + comparison.diff_regions || diffImage.diff_regions || null, + diffLines: comparison.diff_lines || null, + fingerprintData: comparison.fingerprint_data || null, + } + : null, buildId: comparison.build_id, buildName: comparison.build_name, buildBranch: comparison.build_branch, @@ -284,10 +296,12 @@ function displayComparison(output, comparison, verbose) { // Honeydiff analysis in verbose mode if (verbose) { - let clusterMetadata = comparison.cluster_metadata || comparison.diff_image?.cluster_metadata; + let clusterMetadata = + comparison.cluster_metadata || comparison.diff_image?.cluster_metadata; let ssim = comparison.ssim_score ?? comparison.diff_image?.ssim_score; let gmsd = comparison.gmsd_score ?? comparison.diff_image?.gmsd_score; - let fingerprint = comparison.fingerprint_hash || comparison.diff_image?.fingerprint_hash; + let fingerprint = + comparison.fingerprint_hash || comparison.diff_image?.fingerprint_hash; if (clusterMetadata || ssim != null || gmsd != null) { output.blank(); @@ -308,15 +322,18 @@ function displayComparison(output, comparison, verbose) { // URLs in verbose mode if (verbose) { - let baselineUrl = comparison.baseline_screenshot?.original_url - || comparison.baseline_original_url - || comparison.baseline_screenshot_url; - let currentUrl = comparison.current_screenshot?.original_url - || comparison.current_original_url - || comparison.current_screenshot_url; - let diffUrl = comparison.diff_image?.url - || comparison.diff_image_url - || comparison.diff_url; + let baselineUrl = + comparison.baseline_screenshot?.original_url || + comparison.baseline_original_url || + comparison.baseline_screenshot_url; + let currentUrl = + comparison.current_screenshot?.original_url || + comparison.current_original_url || + comparison.current_screenshot_url; + let diffUrl = + comparison.diff_image?.url || + comparison.diff_image_url || + comparison.diff_url; if (baselineUrl || currentUrl || diffUrl) { output.blank(); @@ -427,7 +444,10 @@ function displaySearchResults( for (let comp of group.comparisons.slice(0, verbose ? 10 : 3)) { let icon = getStatusIcon(colors, comp.status); let classification = verbose - ? getClassificationLabel(colors, comp.cluster_metadata || comp.diff_image?.cluster_metadata) + ? getClassificationLabel( + colors, + comp.cluster_metadata || comp.diff_image?.cluster_metadata + ) : ''; output.print(` ${icon} ${comp.name}${classification}`); } diff --git a/tests/commands/comparisons.test.js b/tests/commands/comparisons.test.js index a5e9273..d3b8926 100644 --- a/tests/commands/comparisons.test.js +++ b/tests/commands/comparisons.test.js @@ -369,7 +369,10 @@ describe('commands/comparisons', () => { assert.ok(result.honeydiff, 'Should include honeydiff data'); assert.strictEqual(result.honeydiff.ssimScore, 0.9521); assert.strictEqual(result.honeydiff.gmsdScore, 0.0345); - assert.strictEqual(result.honeydiff.clusterClassification, 'dynamic_content'); + assert.strictEqual( + result.honeydiff.clusterClassification, + 'dynamic_content' + ); assert.strictEqual(result.honeydiff.fingerprintHash, 'search-hash-789'); }); From e39e8c38bedcbcf61aa98ba22a4abb03fe267cf1 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Mon, 9 Feb 2026 00:26:48 -0600 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20Fix=20review=20feedback:=20?= =?UTF-8?q?=3F=3F=20for=20array=20fields,=20diffImage=20fallback,=20finger?= =?UTF-8?q?print=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use ?? instead of || for diff_regions, diff_lines, fingerprint_data so empty arrays aren't treated as falsy - Add diffImage fallback for diff_lines and fingerprint_data - Show fingerprint in verbose display even when scores are missing --- src/commands/builds.js | 2 +- src/commands/comparisons.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands/builds.js b/src/commands/builds.js index 5f67ae6..900ac03 100644 --- a/src/commands/builds.js +++ b/src/commands/builds.js @@ -233,7 +233,7 @@ function formatBuildForJson(build, includeComparisons = false) { clusterClassification: clusterMetadata?.classification || null, clusterMetadata, fingerprintHash, - diffRegions: c.diff_regions || diffImage.diff_regions || null, + diffRegions: c.diff_regions ?? diffImage.diff_regions ?? null, } : null, }; diff --git a/src/commands/comparisons.js b/src/commands/comparisons.js index 74d3830..6bef1ea 100644 --- a/src/commands/comparisons.js +++ b/src/commands/comparisons.js @@ -239,9 +239,10 @@ function formatComparisonForJson(comparison) { clusterMetadata, fingerprintHash, diffRegions: - comparison.diff_regions || diffImage.diff_regions || null, - diffLines: comparison.diff_lines || null, - fingerprintData: comparison.fingerprint_data || null, + comparison.diff_regions ?? diffImage.diff_regions ?? null, + diffLines: comparison.diff_lines ?? diffImage.diff_lines ?? null, + fingerprintData: + comparison.fingerprint_data ?? diffImage.fingerprint_data ?? null, } : null, buildId: comparison.build_id, @@ -303,7 +304,7 @@ function displayComparison(output, comparison, verbose) { let fingerprint = comparison.fingerprint_hash || comparison.diff_image?.fingerprint_hash; - if (clusterMetadata || ssim != null || gmsd != null) { + if (clusterMetadata || ssim != null || gmsd != null || fingerprint) { output.blank(); if (clusterMetadata?.classification) { output.labelValue('Classification', clusterMetadata.classification);