From d5cb821276271241754a3bb0af587a1e5c4c3069 Mon Sep 17 00:00:00 2001 From: "jyc.dev" Date: Thu, 27 Feb 2025 03:49:45 +0100 Subject: [PATCH] fix(snapshot): allow inline snapshot calls on same location with same snapshot (#7464) Co-authored-by: Hiroshi Ogawa --- packages/snapshot/src/port/state.ts | 33 +- .../fails/inline-snapshop-inside-loop.test.ts | 31 -- .../cli/test/__snapshots__/fails.test.ts.snap | 8 - test/cli/test/fails.test.ts | 5 +- .../inline-multiple-calls/different.test.ts | 7 + .../inline-multiple-calls/different2.test.ts | 13 + .../inline-multiple-calls/same.test.ts | 7 + .../inline-multiple-calls/same2.test.ts | 13 + .../test/inline-multiple-calls.test.ts | 386 ++++++++++++++++++ 9 files changed, 453 insertions(+), 50 deletions(-) delete mode 100644 test/cli/fixtures/fails/inline-snapshop-inside-loop.test.ts create mode 100644 test/snapshots/test/fixtures/inline-multiple-calls/different.test.ts create mode 100644 test/snapshots/test/fixtures/inline-multiple-calls/different2.test.ts create mode 100644 test/snapshots/test/fixtures/inline-multiple-calls/same.test.ts create mode 100644 test/snapshots/test/fixtures/inline-multiple-calls/same2.test.ts create mode 100644 test/snapshots/test/inline-multiple-calls.test.ts diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index be480185ad11..80ef8d4bd815 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -48,6 +48,12 @@ interface SaveStatus { saved: boolean } +type ParsedStackPosition = Pick + +function isSameStackPosition(x: ParsedStackPosition, y: ParsedStackPosition) { + return x.file === y.file && x.column === y.column && x.line === y.line +} + export default class SnapshotState { private _counters = new CounterMap() private _dirty: boolean @@ -55,7 +61,7 @@ export default class SnapshotState { private _snapshotData: SnapshotData private _initialData: SnapshotData private _inlineSnapshots: Array - private _inlineSnapshotStacks: Array + private _inlineSnapshotStacks: Array private _testIdToKeys = new DefaultMap(() => []) private _rawSnapshots: Array private _uncheckedKeys: Set @@ -343,13 +349,26 @@ export default class SnapshotState { // https://github.com/vitejs/vite/issues/8657 stack.column-- - // reject multiple inline snapshots at the same location - if (this._inlineSnapshotStacks.some(s => s.file === stack!.file && s.line === stack!.line && s.column === stack!.column)) { - // remove already succeeded snapshot - this._inlineSnapshots = this._inlineSnapshots.filter(s => !(s.file === stack!.file && s.line === stack!.line && s.column === stack!.column)) - throw new Error('toMatchInlineSnapshot cannot be called multiple times at the same location.') + // reject multiple inline snapshots at the same location if snapshot is different + const snapshotsWithSameStack = this._inlineSnapshotStacks.filter(s => isSameStackPosition(s, stack!)) + if (snapshotsWithSameStack.length > 0) { + // ensure only one snapshot will be written at the same location + this._inlineSnapshots = this._inlineSnapshots.filter(s => !isSameStackPosition(s, stack!)) + + const differentSnapshot = snapshotsWithSameStack.find(s => s.snapshot !== receivedSerialized) + if (differentSnapshot) { + throw Object.assign( + new Error( + 'toMatchInlineSnapshot with different snapshots cannot be called at the same location', + ), + { + actual: receivedSerialized, + expected: differentSnapshot.snapshot, + }, + ) + } } - this._inlineSnapshotStacks.push({ ...stack, testId }) + this._inlineSnapshotStacks.push({ ...stack, testId, snapshot: receivedSerialized }) } // These are the conditions on when to write snapshots: diff --git a/test/cli/fixtures/fails/inline-snapshop-inside-loop.test.ts b/test/cli/fixtures/fails/inline-snapshop-inside-loop.test.ts deleted file mode 100644 index a03c6aa5aaf9..000000000000 --- a/test/cli/fixtures/fails/inline-snapshop-inside-loop.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {test, expect} from "vitest"; - -test("fail 1", () => { - for (const str of ["foo", "bar"]) { - expect(str).toMatchInlineSnapshot(); - } -}); - -test("fail 2.1", () => { - for (const str of ["foo", "bar"]) { - expect(str).toMatchInlineSnapshot(`"foo"`); - } -}); - -test("fail 2.2", () => { - for (const str of ["foo", "bar"]) { - expect(str).toMatchInlineSnapshot(`"bar"`); - } -}); - -test("fail 3", () => { - for (const str of ["ok", "ok"]) { - expect(str).toMatchInlineSnapshot(); - } -}); - -test("fail 4", () => { - for (const str of ["ok", "ok"]) { - expect(str).toMatchInlineSnapshot(`"ok"`); - } -}); diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap index 38c0ef3e2389..e75221b82057 100644 --- a/test/cli/test/__snapshots__/fails.test.ts.snap +++ b/test/cli/test/__snapshots__/fails.test.ts.snap @@ -50,14 +50,6 @@ Error: InlineSnapshot cannot be used inside of test.each or describe.each Error: InlineSnapshot cannot be used inside of test.each or describe.each" `; -exports[`should fail inline-snapshop-inside-loop.test.ts 1`] = ` -"Error: toMatchInlineSnapshot cannot be called multiple times at the same location. -Error: toMatchInlineSnapshot cannot be called multiple times at the same location. -Error: toMatchInlineSnapshot cannot be called multiple times at the same location. -Error: toMatchInlineSnapshot cannot be called multiple times at the same location. -Error: toMatchInlineSnapshot cannot be called multiple times at the same location." -`; - exports[`should fail mock-import-proxy-module.test.ts 1`] = `"Error: There are some problems in resolving the mocks API."`; exports[`should fail nested-suite.test.ts 1`] = `"AssertionError: expected true to be false // Object.is equality"`; diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts index 3cd0ec1bee49..c7c79d9dd5e2 100644 --- a/test/cli/test/fails.test.ts +++ b/test/cli/test/fails.test.ts @@ -9,10 +9,7 @@ const root = resolve(__dirname, '../fixtures/fails') const files = await glob(['**/*.test.ts'], { cwd: root, dot: true, expandDirectories: false }) it.each(files)('should fail %s', async (file) => { - const { stderr } = await runVitest({ - root, - update: file === 'inline-snapshop-inside-loop.test.ts' ? true : undefined, - }, [file]) + const { stderr } = await runVitest({ root }, [file]) expect(stderr).toBeTruthy() const msg = String(stderr) diff --git a/test/snapshots/test/fixtures/inline-multiple-calls/different.test.ts b/test/snapshots/test/fixtures/inline-multiple-calls/different.test.ts new file mode 100644 index 000000000000..9286f1aefc78 --- /dev/null +++ b/test/snapshots/test/fixtures/inline-multiple-calls/different.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' + +test('single', () => { + for (const value of ["test1", "test2"]) { + expect(value).toMatchInlineSnapshot() + } +}) diff --git a/test/snapshots/test/fixtures/inline-multiple-calls/different2.test.ts b/test/snapshots/test/fixtures/inline-multiple-calls/different2.test.ts new file mode 100644 index 000000000000..76f4b1772f86 --- /dev/null +++ b/test/snapshots/test/fixtures/inline-multiple-calls/different2.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from 'vitest' + +test('a', () => { + snap('test1') +}) + +test('b', () => { + snap('test2') +}) + +function snap(value: unknown) { + expect(value).toMatchInlineSnapshot() +} diff --git a/test/snapshots/test/fixtures/inline-multiple-calls/same.test.ts b/test/snapshots/test/fixtures/inline-multiple-calls/same.test.ts new file mode 100644 index 000000000000..7d432a1cd6bd --- /dev/null +++ b/test/snapshots/test/fixtures/inline-multiple-calls/same.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' + +test('single', () => { + for (const value of ["test1", "test1"]) { + expect(value).toMatchInlineSnapshot(`"test1"`) + } +}) diff --git a/test/snapshots/test/fixtures/inline-multiple-calls/same2.test.ts b/test/snapshots/test/fixtures/inline-multiple-calls/same2.test.ts new file mode 100644 index 000000000000..bbcd666616b6 --- /dev/null +++ b/test/snapshots/test/fixtures/inline-multiple-calls/same2.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from 'vitest' + +test('a', () => { + snap('test1') +}) + +test('b', () => { + snap('test1') +}) + +function snap(value: unknown) { + expect(value).toMatchInlineSnapshot(`"test1"`) +} diff --git a/test/snapshots/test/inline-multiple-calls.test.ts b/test/snapshots/test/inline-multiple-calls.test.ts new file mode 100644 index 000000000000..fd8be4f748bb --- /dev/null +++ b/test/snapshots/test/inline-multiple-calls.test.ts @@ -0,0 +1,386 @@ +import fs from 'node:fs' +import { join } from 'pathe' +import { expect, test } from 'vitest' +import { editFile, runVitest } from '../../test-utils' + +// pnpm -C test/snapshots test:snaps inline-multiple-calls + +test('same snapshots in single test', async () => { + // pnpm -C test/snapshots test:fixtures --root test/fixtures/inline-multiple-calls same.test + + // reset snapshot + const root = join(import.meta.dirname, 'fixtures/inline-multiple-calls') + const testFile = join(root, 'same.test.ts') + editFile(testFile, s => s.replace(/toMatchInlineSnapshot\(`.*`\)/gs, 'toMatchInlineSnapshot()')) + + // iniital run (create snapshot) + let vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toBe('') + expect(vitest.ctx?.snapshot.summary).toMatchInlineSnapshot(` + Object { + "added": 2, + "didUpdate": true, + "failure": false, + "filesAdded": 1, + "filesRemoved": 0, + "filesRemovedList": Array [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 0, + "total": 2, + "unchecked": 0, + "uncheckedKeysByFile": Array [], + "unmatched": 0, + "updated": 0, + } + `) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + // no-update run + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + expect(vitest.stderr).toBe('') + expect(vitest.ctx?.snapshot.summary).toMatchInlineSnapshot(` + Object { + "added": 0, + "didUpdate": false, + "failure": false, + "filesAdded": 0, + "filesRemoved": 0, + "filesRemovedList": Array [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 2, + "total": 2, + "unchecked": 0, + "uncheckedKeysByFile": Array [], + "unmatched": 0, + "updated": 0, + } + `) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + // update run + vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.ctx?.snapshot.summary).toMatchInlineSnapshot(` + Object { + "added": 0, + "didUpdate": true, + "failure": false, + "filesAdded": 0, + "filesRemoved": 0, + "filesRemovedList": Array [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 2, + "total": 2, + "unchecked": 0, + "uncheckedKeysByFile": Array [], + "unmatched": 0, + "updated": 0, + } + `) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') +}) + +test('same snapshots in multiple tests', async () => { + // pnpm -C test/snapshots test:fixtures --root test/fixtures/inline-multiple-calls same2.test + + // reset snapshot + const root = join(import.meta.dirname, 'fixtures/inline-multiple-calls') + const testFile = join(root, 'same2.test.ts') + editFile(testFile, s => s.replace(/toMatchInlineSnapshot\(`.*`\)/gs, 'toMatchInlineSnapshot()')) + + // iniital run (create snapshot) + let vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toBe('') + expect(vitest.ctx?.snapshot.summary).toMatchInlineSnapshot(` + Object { + "added": 2, + "didUpdate": true, + "failure": false, + "filesAdded": 1, + "filesRemoved": 0, + "filesRemovedList": Array [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 0, + "total": 2, + "unchecked": 0, + "uncheckedKeysByFile": Array [], + "unmatched": 0, + "updated": 0, + } + `) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + // no-update run + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + expect(vitest.stderr).toBe('') + expect(vitest.ctx?.snapshot.summary).toMatchInlineSnapshot(` + Object { + "added": 0, + "didUpdate": false, + "failure": false, + "filesAdded": 0, + "filesRemoved": 0, + "filesRemovedList": Array [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 2, + "total": 2, + "unchecked": 0, + "uncheckedKeysByFile": Array [], + "unmatched": 0, + "updated": 0, + } + `) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + // update run + vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.ctx?.snapshot.summary).toMatchInlineSnapshot(` + Object { + "added": 0, + "didUpdate": true, + "failure": false, + "filesAdded": 0, + "filesRemoved": 0, + "filesRemovedList": Array [], + "filesUnmatched": 0, + "filesUpdated": 0, + "matched": 2, + "total": 2, + "unchecked": 0, + "uncheckedKeysByFile": Array [], + "unmatched": 0, + "updated": 0, + } + `) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') +}) + +test('different snapshots in single test', async () => { + // pnpm -C test/snapshots test:fixtures --root test/fixtures/inline-multiple-calls different.test + + // reset snapshot + const root = join(import.meta.dirname, 'fixtures/inline-multiple-calls') + const testFile = join(root, 'different.test.ts') + editFile(testFile, s => s.replace(/toMatchInlineSnapshot\(`.*`\)/gs, 'toMatchInlineSnapshot()')) + + // update run should fail + let vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot()') + expect(vitest.exitCode).not.toBe(0) + + // no-update run should fail + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + if (process.env.CI) { + expect(vitest.stderr).toContain(` +Error: Snapshot \`single 1\` mismatched +`) + } + else { + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + } + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot()') + + // current snapshot is "test1" + editFile(testFile, s => s.replace('expect(value).toMatchInlineSnapshot()', 'expect(value).toMatchInlineSnapshot(`"test1"`)')) + vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + // current snapshot is "test2" + editFile(testFile, s => s.replace('expect(value).toMatchInlineSnapshot(`"test1"`)', 'expect(value).toMatchInlineSnapshot(`"test2"`)')) + vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test2"`)') + + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + expect(vitest.stderr).toContain(` +Error: Snapshot \`single 1\` mismatched + +Expected: ""test2"" +Received: ""test1"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test2"`)') +}) + +test('different snapshots in multiple tests', async () => { + // pnpm -C test/snapshots test:fixtures --root test/fixtures/inline-multiple-calls different2.test + + // reset snapshot + const root = join(import.meta.dirname, 'fixtures/inline-multiple-calls') + const testFile = join(root, 'different2.test.ts') + editFile(testFile, s => s.replace(/toMatchInlineSnapshot\(`.*`\)/gs, 'toMatchInlineSnapshot()')) + + // update run should fail + let vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot()') + expect(vitest.exitCode).not.toBe(0) + + // no-update run should fail + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + if (process.env.CI) { + expect(vitest.stderr).toContain(` +Error: Snapshot \`a 1\` mismatched +`) + } + else { + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + } + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot()') + + // current snapshot is "test1" + editFile(testFile, s => s.replace('expect(value).toMatchInlineSnapshot()', 'expect(value).toMatchInlineSnapshot(`"test1"`)')) + vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test1"`)') + + // current snapshot is "test2" + editFile(testFile, s => s.replace('expect(value).toMatchInlineSnapshot(`"test1"`)', 'expect(value).toMatchInlineSnapshot(`"test2"`)')) + vitest = await runVitest({ + root, + include: [testFile], + update: true, + }) + expect(vitest.stderr).toContain(` +Error: toMatchInlineSnapshot with different snapshots cannot be called at the same location + +Expected: ""test1"" +Received: ""test2"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test2"`)') + + vitest = await runVitest({ + root, + include: [testFile], + update: false, + }) + expect(vitest.stderr).toContain(` +Error: Snapshot \`a 1\` mismatched + +Expected: ""test2"" +Received: ""test1"" +`) + expect(fs.readFileSync(testFile, 'utf-8')).toContain('expect(value).toMatchInlineSnapshot(`"test2"`)') +})