From aeffebd5bd5187ff0c315daecbdf17d9a4371961 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 11:46:37 +0900 Subject: [PATCH 1/9] fix: false-flagging obsolete snapshots for snapshot properties mismatch --- packages/snapshot/src/client.ts | 67 ++++++++++++------- packages/snapshot/src/index.ts | 1 + packages/snapshot/src/port/state.ts | 24 +++++++ .../obsolete-properties/src/test1.test.ts | 6 ++ .../obsolete-properties/vitest.config.ts | 3 + 5 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts create mode 100644 test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index cd7f7d229ac7..dc59466f15c8 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -50,6 +50,14 @@ interface AssertOptions { rawSnapshot?: RawSnapshotInfo } +/** Same shape as SyncExpectationResult from @vitest/expect */ +export interface MatchResult { + pass: boolean + message: () => string + actual?: unknown + expected?: unknown +} + export interface SnapshotClientOptions { isEqual?: (received: unknown, expected: unknown) => boolean } @@ -99,7 +107,7 @@ export class SnapshotClient { return state } - assert(options: AssertOptions): void { + match(options: AssertOptions): MatchResult { const { filepath, name, @@ -119,37 +127,37 @@ export class SnapshotClient { } const snapshotState = this.getSnapshotState(filepath) + const testName = [name, ...(message ? [message] : [])].join(' > ') + + // Probe first so we can mark as checked even on early return + const expectedSnapshot = snapshotState.probeExpectedSnapshot({ + testName, + testId, + isInline, + inlineSnapshot, + }) if (typeof properties === 'object') { if (typeof received !== 'object' || !received) { + expectedSnapshot.markAsChecked() throw new Error( 'Received value must be an object when the matcher has properties', ) } - try { - const pass = this.options.isEqual?.(received, properties) ?? false - // const pass = equals(received, properties, [iterableEquality, subsetEquality]) - if (!pass) { - throw createMismatchError( - 'Snapshot properties mismatched', - snapshotState.expand, - received, - properties, - ) - } - else { - received = deepMergeSnapshot(received, properties) + const propertiesPass = this.options.isEqual?.(received, properties) ?? false + if (!propertiesPass) { + expectedSnapshot.markAsChecked() + return { + pass: false, + message: () => errorMessage || 'Snapshot properties mismatched', + actual: received, + expected: properties, } } - catch (err: any) { - err.message = errorMessage || 'Snapshot mismatched' - throw err - } + received = deepMergeSnapshot(received, properties) } - const testName = [name, ...(message ? [message] : [])].join(' > ') - const { actual, expected, key, pass } = snapshotState.match({ testId, testName, @@ -160,12 +168,23 @@ export class SnapshotClient { rawSnapshot, }) - if (!pass) { + return { + pass, + message: () => `Snapshot \`${key || 'unknown'}\` mismatched`, + actual: rawSnapshot ? actual : actual?.trim(), + expected: rawSnapshot ? expected : expected?.trim(), + } + } + + assert(options: AssertOptions): void { + const result = this.match(options) + if (!result.pass) { + const snapshotState = this.getSnapshotState(options.filepath) throw createMismatchError( - `Snapshot \`${key || 'unknown'}\` mismatched`, + result.message(), snapshotState.expand, - rawSnapshot ? actual : actual?.trim(), - rawSnapshot ? expected : expected?.trim(), + result.actual, + result.expected, ) } } diff --git a/packages/snapshot/src/index.ts b/packages/snapshot/src/index.ts index 4fa23f225b9f..b8d06d110363 100644 --- a/packages/snapshot/src/index.ts +++ b/packages/snapshot/src/index.ts @@ -1,4 +1,5 @@ export { SnapshotClient } from './client' +export type { MatchResult } from './client' export { stripSnapshotIndentation } from './port/inlineSnapshot' export { addSerializer, getSerializers } from './port/plugins' diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index e3249f8af992..d0572620cd94 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -267,6 +267,30 @@ export default class SnapshotState { } } + probeExpectedSnapshot(options: { testName: string; testId: string; isInline?: boolean; inlineSnapshot?: string }): { + data?: string + markAsChecked: () => void + } { + const count = this._counters.get(options.testName) + 1 + const key = testNameToKey(options.testName, count) + let data: string | undefined + if (options?.isInline) { + data = options.inlineSnapshot + } + else { + data = this._snapshotData[key] + } + + return { + data, + markAsChecked: () => { + this._counters.increment(options.testName) + this._testIdToKeys.get(options.testId).push(key) + this._uncheckedKeys.delete(key) + }, + } + } + match({ testId, testName, diff --git a/test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts b/test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts new file mode 100644 index 000000000000..cb21fa59277b --- /dev/null +++ b/test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts @@ -0,0 +1,6 @@ +import { expect, it } from 'vitest' + +it('with properties', () => { + const age = process.env.TEST_BREAK_PROPERTIES ? 'thirty' : 30 + expect({ name: 'alice', age }).toMatchSnapshot({ age: expect.any(Number) }) +}) diff --git a/test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts b/test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts new file mode 100644 index 000000000000..abed6b2116e1 --- /dev/null +++ b/test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({}) From 876aba116ac7d753ebbb21472e3af8c1d0d64834 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:00:47 +0900 Subject: [PATCH 2/9] chore: comment --- packages/snapshot/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index dc59466f15c8..e3b3cc8b9344 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -50,7 +50,7 @@ interface AssertOptions { rawSnapshot?: RawSnapshotInfo } -/** Same shape as SyncExpectationResult from @vitest/expect */ +/** Same shape as expect.extend custom matcher result (SyncExpectationResult from @vitest/expect) */ export interface MatchResult { pass: boolean message: () => string From afcd7368bbeebe2732e6a84dad10efd971202575 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:05:06 +0900 Subject: [PATCH 3/9] test: e2e --- .../obsolete-properties/src/test1.test.ts | 6 - .../obsolete-properties/vitest.config.ts | 3 - .../test/fixtures/properties/.gitignore | 1 + .../test/fixtures/properties/basic.test.ts | 24 ++ test/snapshots/test/properties.test.ts | 277 ++++++++++++++++++ 5 files changed, 302 insertions(+), 9 deletions(-) delete mode 100644 test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts delete mode 100644 test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts create mode 100644 test/snapshots/test/fixtures/properties/.gitignore create mode 100644 test/snapshots/test/fixtures/properties/basic.test.ts create mode 100644 test/snapshots/test/properties.test.ts diff --git a/test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts b/test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts deleted file mode 100644 index cb21fa59277b..000000000000 --- a/test/snapshots/test/fixtures/obsolete-properties/src/test1.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, it } from 'vitest' - -it('with properties', () => { - const age = process.env.TEST_BREAK_PROPERTIES ? 'thirty' : 30 - expect({ name: 'alice', age }).toMatchSnapshot({ age: expect.any(Number) }) -}) diff --git a/test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts b/test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts deleted file mode 100644 index abed6b2116e1..000000000000 --- a/test/snapshots/test/fixtures/obsolete-properties/vitest.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineConfig } from 'vitest/config' - -export default defineConfig({}) diff --git a/test/snapshots/test/fixtures/properties/.gitignore b/test/snapshots/test/fixtures/properties/.gitignore new file mode 100644 index 000000000000..b05c2dfa7007 --- /dev/null +++ b/test/snapshots/test/fixtures/properties/.gitignore @@ -0,0 +1 @@ +__snapshots__ diff --git a/test/snapshots/test/fixtures/properties/basic.test.ts b/test/snapshots/test/fixtures/properties/basic.test.ts new file mode 100644 index 000000000000..accafd08bef2 --- /dev/null +++ b/test/snapshots/test/fixtures/properties/basic.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "vitest"; + +test("file", () => { + expect({ name: "alice", age: 30 }).toMatchSnapshot({ age: expect.any(Number) }); +}); + +test("file asymmetric", () => { + expect({ name: "bob", score: 95 }).toMatchSnapshot({ + score: expect.toSatisfy(function lessThan100(n) { + return n < 100; + }), + }); +}); + +// -- TEST INLINE START -- +test("inline", () => { + expect({ name: "carol", age: 25 }).toMatchInlineSnapshot({ age: expect.any(Number) }, ` + Object { + "age": Any, + "name": "carol", + } + `); +}); +// -- TEST INLINE END -- diff --git a/test/snapshots/test/properties.test.ts b/test/snapshots/test/properties.test.ts new file mode 100644 index 000000000000..05e5bd3d61a5 --- /dev/null +++ b/test/snapshots/test/properties.test.ts @@ -0,0 +1,277 @@ +import fs, { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { expect, test } from 'vitest' +import { editFile, runVitest } from '../../test-utils' + +const INLINE_BLOCK_RE = /\/\/ -- TEST INLINE START --\n([\s\S]*?)\/\/ -- TEST INLINE END --/g + +function extractInlineBlocks(content: string): string { + return [...content.matchAll(INLINE_BLOCK_RE)].map(m => m[1].trim()).join('\n\n') +} + +test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { + const root = join(import.meta.dirname, 'fixtures/properties') + const testFile = join(root, 'basic.test.ts') + const snapshotFile = join(root, '__snapshots__/basic.test.ts.snap') + + // remove snapshots + fs.rmSync(join(root, '__snapshots__'), { recursive: true, force: true }) + editFile(testFile, s => + s.replace(/toMatchInlineSnapshot\((\{[^}]*\}),\s*`[^`]*`\)/g, 'toMatchInlineSnapshot($1)')) + + // create snapshots from scratch + let result = await runVitest({ root, update: 'new' }) + expect(result.stderr).toMatchInlineSnapshot(`""`) + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + + exports[\`file 1\`] = \` + Object { + "age": Any, + "name": "alice", + } + \`; + + exports[\`file asymmetric 1\`] = \` + Object { + "name": "bob", + "score": toSatisfy<[Function lessThan100]>, + } + \`; + " + `) + expect(extractInlineBlocks(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(` + "test("inline", () => { + expect({ name: "carol", age: 25 }).toMatchInlineSnapshot({ age: expect.any(Number) }, \` + Object { + "age": Any, + "name": "carol", + } + \`); + });" + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "file": "passed", + "file asymmetric": "passed", + "inline": "passed", + }, + } + `) + + // edit tests to break properties check + editFile(testFile, s => + s + .replace('age: 30', 'age: \'thirty\'') + .replace('score: 95', 'score: 999') + .replace('age: 25', 'age: \'twenty-five\'')) + + // properties mismatch should NOT cause false-positive obsolete snapshot + result = await runVitest({ root, update: 'none' }) + expect(result.stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL basic.test.ts > file + Error: Snapshot properties mismatched + + - Expected + + Received + + { + - "age": Any, + + "age": "thirty", + + "name": "alice", + } + + ❯ basic.test.ts:4:44 + 2| + 3| test("file", () => { + 4| expect({ name: "alice", age: 'thirty' }).toMatchSnapshot({ age: expe… + | ^ + 5| }); + 6| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯ + + FAIL basic.test.ts > file asymmetric + Error: Snapshot properties mismatched + + - Expected + + Received + + { + - "score": toSatisfy<[Function lessThan100]>, + + "name": "bob", + + "score": 999, + } + + ❯ basic.test.ts:8:39 + 6| + 7| test("file asymmetric", () => { + 8| expect({ name: "bob", score: 999 }).toMatchSnapshot({ + | ^ + 9| score: expect.toSatisfy(function lessThan100(n) { + 10| return n < 100; + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ + + FAIL basic.test.ts > inline + Error: Snapshot properties mismatched + + - Expected + + Received + + { + - "age": Any, + + "age": "twenty-five", + + "name": "carol", + } + + ❯ basic.test.ts:17:49 + 15| // -- TEST INLINE START -- + 16| test("inline", () => { + 17| expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot(… + | ^ + 18| Object { + 19| "age": Any, + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯ + + " + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "file": Array [ + "Snapshot properties mismatched", + ], + "file asymmetric": Array [ + "Snapshot properties mismatched", + ], + "inline": Array [ + "Snapshot properties mismatched", + ], + }, + } + `) + + // run with update — file/inline snapshots update, properties errors persist + result = await runVitest({ root, update: 'all' }) + expect(result.stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL basic.test.ts > file + Error: Snapshot properties mismatched + + - Expected + + Received + + { + - "age": Any, + + "age": "thirty", + + "name": "alice", + } + + ❯ basic.test.ts:4:44 + 2| + 3| test("file", () => { + 4| expect({ name: "alice", age: 'thirty' }).toMatchSnapshot({ age: expe… + | ^ + 5| }); + 6| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯ + + FAIL basic.test.ts > file asymmetric + Error: Snapshot properties mismatched + + - Expected + + Received + + { + - "score": toSatisfy<[Function lessThan100]>, + + "name": "bob", + + "score": 999, + } + + ❯ basic.test.ts:8:39 + 6| + 7| test("file asymmetric", () => { + 8| expect({ name: "bob", score: 999 }).toMatchSnapshot({ + | ^ + 9| score: expect.toSatisfy(function lessThan100(n) { + 10| return n < 100; + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ + + FAIL basic.test.ts > inline + Error: Snapshot properties mismatched + + - Expected + + Received + + { + - "age": Any, + + "age": "twenty-five", + + "name": "carol", + } + + ❯ basic.test.ts:17:49 + 15| // -- TEST INLINE START -- + 16| test("inline", () => { + 17| expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot(… + | ^ + 18| Object { + 19| "age": Any, + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯ + + " + `) + expect(readFileSync(snapshotFile, 'utf-8')).toMatchInlineSnapshot(` + "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + + exports[\`file 1\`] = \` + Object { + "age": Any, + "name": "alice", + } + \`; + + exports[\`file asymmetric 1\`] = \` + Object { + "name": "bob", + "score": toSatisfy<[Function lessThan100]>, + } + \`; + " + `) + expect(extractInlineBlocks(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(` + "test("inline", () => { + expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot({ age: expect.any(Number) }, \` + Object { + "age": Any, + "name": "carol", + } + \`); + });" + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + Object { + "basic.test.ts": Object { + "file": Array [ + "Snapshot properties mismatched", + ], + "file asymmetric": Array [ + "Snapshot properties mismatched", + ], + "inline": Array [ + "Snapshot properties mismatched", + ], + }, + } + `) +}) From 5aab088356cad8f241552119f56abf941b68007b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:11:51 +0900 Subject: [PATCH 4/9] test: more --- .../test/fixtures/properties/basic.test.ts | 4 + test/snapshots/test/properties.test.ts | 74 +++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/test/snapshots/test/fixtures/properties/basic.test.ts b/test/snapshots/test/fixtures/properties/basic.test.ts index accafd08bef2..2568ce140096 100644 --- a/test/snapshots/test/fixtures/properties/basic.test.ts +++ b/test/snapshots/test/fixtures/properties/basic.test.ts @@ -12,6 +12,10 @@ test("file asymmetric", () => { }); }); +test("file snapshot-only", () => { + expect({ name: "dave", age: 42 }).toMatchSnapshot({ age: expect.any(Number) }); +}); + // -- TEST INLINE START -- test("inline", () => { expect({ name: "carol", age: 25 }).toMatchInlineSnapshot({ age: expect.any(Number) }, ` diff --git a/test/snapshots/test/properties.test.ts b/test/snapshots/test/properties.test.ts index 05e5bd3d61a5..39e56854a236 100644 --- a/test/snapshots/test/properties.test.ts +++ b/test/snapshots/test/properties.test.ts @@ -38,6 +38,13 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { "score": toSatisfy<[Function lessThan100]>, } \`; + + exports[\`file snapshot-only 1\`] = \` + Object { + "age": Any, + "name": "dave", + } + \`; " `) expect(extractInlineBlocks(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(` @@ -55,6 +62,7 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { "basic.test.ts": Object { "file": "passed", "file asymmetric": "passed", + "file snapshot-only": "passed", "inline": "passed", }, } @@ -65,13 +73,14 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { s .replace('age: 30', 'age: \'thirty\'') .replace('score: 95', 'score: 999') + .replace('name: "dave"', 'name: "dave-edit"') .replace('age: 25', 'age: \'twenty-five\'')) // properties mismatch should NOT cause false-positive obsolete snapshot result = await runVitest({ root, update: 'none' }) expect(result.stderr).toMatchInlineSnapshot(` " - ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯ FAIL basic.test.ts > file Error: Snapshot properties mismatched @@ -93,7 +102,7 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { 5| }); 6| - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯ + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯ FAIL basic.test.ts > file asymmetric Error: Snapshot properties mismatched @@ -115,7 +124,29 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { 9| score: expect.toSatisfy(function lessThan100(n) { 10| return n < 100; - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/4]⎯ + + FAIL basic.test.ts > file snapshot-only + Error: Snapshot \`file snapshot-only 1\` mismatched + + - Expected + + Received + + Object { + "age": Any, + - "name": "dave", + + "name": "dave-edit", + } + + ❯ basic.test.ts:16:42 + 14| + 15| test("file snapshot-only", () => { + 16| expect({ name: "dave-edit", age: 42 }).toMatchSnapshot({ age: expect… + | ^ + 17| }); + 18| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/4]⎯ FAIL basic.test.ts > inline Error: Snapshot properties mismatched @@ -129,15 +160,15 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { + "name": "carol", } - ❯ basic.test.ts:17:49 - 15| // -- TEST INLINE START -- - 16| test("inline", () => { - 17| expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot(… + ❯ basic.test.ts:21:49 + 19| // -- TEST INLINE START -- + 20| test("inline", () => { + 21| expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot(… | ^ - 18| Object { - 19| "age": Any, + 22| Object { + 23| "age": Any, - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯ + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/4]⎯ " `) @@ -150,6 +181,9 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { "file asymmetric": Array [ "Snapshot properties mismatched", ], + "file snapshot-only": Array [ + "Snapshot \`file snapshot-only 1\` mismatched", + ], "inline": Array [ "Snapshot properties mismatched", ], @@ -219,13 +253,13 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { + "name": "carol", } - ❯ basic.test.ts:17:49 - 15| // -- TEST INLINE START -- - 16| test("inline", () => { - 17| expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot(… + ❯ basic.test.ts:21:49 + 19| // -- TEST INLINE START -- + 20| test("inline", () => { + 21| expect({ name: "carol", age: 'twenty-five' }).toMatchInlineSnapshot(… | ^ - 18| Object { - 19| "age": Any, + 22| Object { + 23| "age": Any, ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯ @@ -247,6 +281,13 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { "score": toSatisfy<[Function lessThan100]>, } \`; + + exports[\`file snapshot-only 1\`] = \` + Object { + "age": Any, + "name": "dave-edit", + } + \`; " `) expect(extractInlineBlocks(readFileSync(testFile, 'utf-8'))).toMatchInlineSnapshot(` @@ -268,6 +309,7 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { "file asymmetric": Array [ "Snapshot properties mismatched", ], + "file snapshot-only": "passed", "inline": Array [ "Snapshot properties mismatched", ], From 769d5625ac84e495eb8ea960a71b2c6591f2ae58 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:17:20 +0900 Subject: [PATCH 5/9] test: verify idempotency on new -> none re-run --- test/snapshots/test/properties.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/snapshots/test/properties.test.ts b/test/snapshots/test/properties.test.ts index 39e56854a236..75c091fdabb1 100644 --- a/test/snapshots/test/properties.test.ts +++ b/test/snapshots/test/properties.test.ts @@ -68,6 +68,11 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { } `) + // verify idempotency — re-run without update passes cleanly + const result2 = await runVitest({ root, update: 'none' }) + expect(result2.stderr).toMatchInlineSnapshot(`""`) + expect(result2.errorTree()).toEqual(result.errorTree()) + // edit tests to break properties check editFile(testFile, s => s From 7d1c1d007019b0bc4ce61d37d5ee0abbce6efaa6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:19:28 +0900 Subject: [PATCH 6/9] refactor: nit --- packages/snapshot/src/port/state.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index d0572620cd94..cc5dc928a815 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -273,16 +273,8 @@ export default class SnapshotState { } { const count = this._counters.get(options.testName) + 1 const key = testNameToKey(options.testName, count) - let data: string | undefined - if (options?.isInline) { - data = options.inlineSnapshot - } - else { - data = this._snapshotData[key] - } - return { - data, + data: options?.isInline ? options.inlineSnapshot : this._snapshotData[key], markAsChecked: () => { this._counters.increment(options.testName) this._testIdToKeys.get(options.testId).push(key) From 53f8c4f7b9ff57f183665c5b5085e0926bc7d268 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:20:35 +0900 Subject: [PATCH 7/9] refactor: more nit --- packages/snapshot/src/port/state.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index cc5dc928a815..39e905808ac8 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -267,7 +267,9 @@ export default class SnapshotState { } } - probeExpectedSnapshot(options: { testName: string; testId: string; isInline?: boolean; inlineSnapshot?: string }): { + probeExpectedSnapshot( + options: Pick, + ): { data?: string markAsChecked: () => void } { From cb8c8124d8d0cc541a7c1ef466a460e5eeb156e6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:23:18 +0900 Subject: [PATCH 8/9] chore: no slop comment --- test/snapshots/test/properties.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/snapshots/test/properties.test.ts b/test/snapshots/test/properties.test.ts index 75c091fdabb1..53ab48326ced 100644 --- a/test/snapshots/test/properties.test.ts +++ b/test/snapshots/test/properties.test.ts @@ -196,7 +196,7 @@ test('toMatchSnapshot and toMatchInlineSnapshot with properties', async () => { } `) - // run with update — file/inline snapshots update, properties errors persist + // run with update — properties errors persist result = await runVitest({ root, update: 'all' }) expect(result.stderr).toMatchInlineSnapshot(` " From a774822e0c08754bf7f640f6a60d6eacd43b559f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 27 Mar 2026 12:26:44 +0900 Subject: [PATCH 9/9] fix: guard isEqual with try/catch to ensure markAsChecked on throw Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/snapshot/src/client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index e3b3cc8b9344..72da2a9e39c5 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -145,7 +145,14 @@ export class SnapshotClient { ) } - const propertiesPass = this.options.isEqual?.(received, properties) ?? false + let propertiesPass: boolean + try { + propertiesPass = this.options.isEqual?.(received, properties) ?? false + } + catch (err) { + expectedSnapshot.markAsChecked() + throw err + } if (!propertiesPass) { expectedSnapshot.markAsChecked() return {