From 5364614b07e8301250922c190b621300e86ba4fd Mon Sep 17 00:00:00 2001 From: unional Date: Thu, 20 Aug 2026 23:00:31 -0700 Subject: [PATCH] fix(vitest-plugin-vis): show snapshot paths relative to the project root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Expected:` / `Actual:` / `Difference:` lines in image snapshot failures resolved `info.*Path` against `projectRoot`, printing an absolute path. Editors and terminals resolve the project-relative path just as well, and the absolute one leaks the local home directory into any failure output pasted into a bug report. The three message strings now go through `formatSnapshotPath`, which prints `./`-prefixed project-relative paths and falls back to absolute for anything outside the project root — possible in a monorepo via a custom `snapshotRootDir` — where a `../../..` chain would be neither shorter nor clearer. Filesystem operations are untouched and still resolve to absolute paths. Closes #93 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01J1RSK6XtmajN5fASgJHqVb --- .changeset/blue-pugs-smile.md | 7 ++++ .../expect/to_match_image_snapshot.spec.tsx | 5 +++ .../snapshot/compare_image_snapshot.logic.ts | 17 ++++++++++ .../client/snapshot/compare_image_snapshot.ts | 14 ++++---- .../snapshot/compare_image_snapshot.unit.ts | 32 +++++++++++++++++-- 5 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 .changeset/blue-pugs-smile.md diff --git a/.changeset/blue-pugs-smile.md b/.changeset/blue-pugs-smile.md new file mode 100644 index 000000000..59dfc2842 --- /dev/null +++ b/.changeset/blue-pugs-smile.md @@ -0,0 +1,7 @@ +--- +'vitest-plugin-vis': patch +--- + +Show snapshot paths in failure messages relative to the project root (`./__vis__/...`) instead of absolute. + +Editors and terminals resolve the relative path just as well, and the output no longer leaks the local absolute path when a failure is pasted into a bug report. Paths outside the project root stay absolute. Filesystem operations are unaffected and still resolve to absolute paths. diff --git a/packages/vitest-plugin-vis/src/client/expect/to_match_image_snapshot.spec.tsx b/packages/vitest-plugin-vis/src/client/expect/to_match_image_snapshot.spec.tsx index a5da72f8b..776dc3dc9 100644 --- a/packages/vitest-plugin-vis/src/client/expect/to_match_image_snapshot.spec.tsx +++ b/packages/vitest-plugin-vis/src/client/expect/to_match_image_snapshot.spec.tsx @@ -88,6 +88,9 @@ it('fails when the image is different', async ({ expect }) => { }, (error) => { expect(error.message).toMatch(/Expected image to match but was differ by \d+ pixels./) + expect(error.message).toMatch(/Expected:\s+\.\/__vis__\/.*\.png/) + expect(error.message).toMatch(/Actual:\s+\.\/__vis__\/.*\.png/) + expect(error.message).toMatch(/Difference:\s+\.\/__vis__\/.*\.png/) }, ) }) @@ -298,6 +301,8 @@ it('should fail with additional info when it does not fail with expectToFail', a expect(error.message).toMatch(/Snapshot .* matched but expected to fail/) expect(error.message).toMatch(/Options:\s+failureThreshold: \d+ pixels/) expect(error.message).toMatch(/Diff:\s+\d+ pixels/) + expect(error.message).toMatch(/Expected:\s+\.\/__vis__\/.*\.png/) + expect(error.message).toMatch(/Actual:\s+\.\/__vis__\/.*\.png/) }, ) }) diff --git a/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.logic.ts b/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.logic.ts index 0eadfed5f..0154e8043 100644 --- a/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.logic.ts +++ b/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.logic.ts @@ -1,3 +1,4 @@ +import { isAbsolute, relative, resolve } from 'pathe' import type { ToMatchImageSnapshotOptions } from '../../shared/types.ts' export function prettifyOptions(options: ToMatchImageSnapshotOptions | undefined) { @@ -13,3 +14,19 @@ export function prettifyOptions(options: ToMatchImageSnapshotOptions | unde .filter(Boolean) .join('\n ') } + +/** + * Formats a snapshot path for display in a failure message. + * + * Paths within the project root are shown relative to it so that the failure output can be shared + * verbatim without leaking the local absolute path, while remaining clickable in editors and terminals. + * Paths outside the project root stay absolute, as a `../../..` chain is neither shorter nor clearer. + */ +export function formatSnapshotPath(projectRoot: string, path: string) { + const absolutePath = resolve(projectRoot, path) + const relativePath = relative(projectRoot, absolutePath) + if (!relativePath || relativePath === '..' || relativePath.startsWith('../') || isAbsolute(relativePath)) { + return absolutePath + } + return `./${relativePath}` +} diff --git a/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.ts b/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.ts index ad5e8f68e..11c5472a8 100644 --- a/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.ts +++ b/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.ts @@ -10,7 +10,7 @@ import type { ImageSnapshotComparisonInfo, ToMatchImageSnapshotOptions } from '. import { toDataURL, toImageData } from '../external/browser/image_data.ts' import { server } from '../external/vitest/vitest_browser_context_proxy.ts' import { alignImagesToSameSize } from '../image/align_images.ts' -import { prettifyOptions } from './compare_image_snapshot.logic.ts' +import { formatSnapshotPath, prettifyOptions } from './compare_image_snapshot.logic.ts' export async function compareImageSnapshot( commands: BrowserCommands & PrepareImageSnapshotComparisonCommand & ImageSnapshotNextIndexCommand, @@ -31,7 +31,7 @@ export async function compareImageSnapshot( dedent`Snapshot \`${taskId}\` has no baseline image. Please review the new snapshot and update the baseline image. Options: ${prettifyOptions(options)} - Actual: ${resolve(info.projectRoot, info.resultPath)}`, + Actual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}`, ) } @@ -55,8 +55,8 @@ export async function compareImageSnapshot( Options: ${prettifyOptions(options)} Diff: ${options.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`} - Expected: ${resolve(info.projectRoot, info.baselinePath)} - Actual: ${resolve(info.projectRoot, info.resultPath)}`, + Expected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)} + Actual: ${formatSnapshotPath(info.projectRoot, info.resultPath)}`, ) } return @@ -85,9 +85,9 @@ export async function compareImageSnapshot( Options: ${prettifyOptions(options)} - Expected: ${resolve(info.projectRoot, info.baselinePath)} - Actual: ${resolve(info.projectRoot, info.resultPath)} - Difference: ${resolve(info.projectRoot, info.diffPath)}`, + Expected: ${formatSnapshotPath(info.projectRoot, info.baselinePath)} + Actual: ${formatSnapshotPath(info.projectRoot, info.resultPath)} + Difference: ${formatSnapshotPath(info.projectRoot, info.diffPath)}`, ) } diff --git a/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.unit.ts b/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.unit.ts index 5ad4ea615..07bf7cfd0 100644 --- a/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.unit.ts +++ b/packages/vitest-plugin-vis/src/client/snapshot/compare_image_snapshot.unit.ts @@ -1,5 +1,5 @@ -import { it } from 'vitest' -import { prettifyOptions } from './compare_image_snapshot.logic.ts' +import { describe, it } from 'vitest' +import { formatSnapshotPath, prettifyOptions } from './compare_image_snapshot.logic.ts' it('returns none when no options', ({ expect }) => { expect(prettifyOptions(undefined)).toBe('none') @@ -23,3 +23,31 @@ it('stringify diffOptions', ({ expect }) => { /failureThreshold: 0 pixels\s{17}comparisonMethod: pixel\s{17}diffOptions: {"threshold":0.1}/, ) }) + +describe('formatSnapshotPath', () => { + it('formats a path within the project root as a relative path', ({ expect }) => { + expect(formatSnapshotPath('/home/me/project', '__vis__/local/__baselines__/some.spec.tsx/case-1.png')).toBe( + './__vis__/local/__baselines__/some.spec.tsx/case-1.png', + ) + }) + + it('does not leak the project root of an absolute path within the project', ({ expect }) => { + expect(formatSnapshotPath('/home/me/project', '/home/me/project/__vis__/local/case-1.png')).toBe( + './__vis__/local/case-1.png', + ) + }) + + it('keeps a path outside the project root absolute', ({ expect }) => { + expect(formatSnapshotPath('/home/me/project/packages/app', '../../__vis__/local/case-1.png')).toBe( + '/home/me/project/__vis__/local/case-1.png', + ) + }) + + it('keeps the project root itself absolute', ({ expect }) => { + expect(formatSnapshotPath('/home/me/project', '.')).toBe('/home/me/project') + }) + + it('does not mistake a leading dot-dot in a filename for an escape', ({ expect }) => { + expect(formatSnapshotPath('/home/me/project', '..vis/case-1.png')).toBe('./..vis/case-1.png') + }) +})