Skip to content

Commit e03683c

Browse files
authored
fix(snapshot): improve inline snapshot inside loop rejection (#6339)
1 parent 8ff6356 commit e03683c

File tree

6 files changed

+55
-51
lines changed

6 files changed

+55
-51
lines changed

packages/snapshot/src/port/state.ts

+37-32
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default class SnapshotState {
5353
private _snapshotData: SnapshotData
5454
private _initialData: SnapshotData
5555
private _inlineSnapshots: Array<InlineSnapshot>
56+
private _inlineSnapshotStacks: Array<ParsedStack>
5657
private _rawSnapshots: Array<RawSnapshot>
5758
private _uncheckedKeys: Set<string>
5859
private _snapshotFormat: PrettyFormatOptions
@@ -77,6 +78,7 @@ export default class SnapshotState {
7778
this._snapshotData = data
7879
this._dirty = dirty
7980
this._inlineSnapshots = []
81+
this._inlineSnapshotStacks = []
8082
this._rawSnapshots = []
8183
this._uncheckedKeys = new Set(Object.keys(this._snapshotData))
8284
this._counters = new Map()
@@ -136,38 +138,13 @@ export default class SnapshotState {
136138
private _addSnapshot(
137139
key: string,
138140
receivedSerialized: string,
139-
options: { isInline: boolean; rawSnapshot?: RawSnapshotInfo; error?: Error },
141+
options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack },
140142
): void {
141143
this._dirty = true
142-
if (options.isInline) {
143-
const error = options.error || new Error('snapshot')
144-
const stacks = parseErrorStacktrace(
145-
error,
146-
{ ignoreStackEntries: [] },
147-
)
148-
const _stack = this._inferInlineSnapshotStack(stacks)
149-
if (!_stack) {
150-
throw new Error(
151-
`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify(
152-
stacks,
153-
)}`,
154-
)
155-
}
156-
const stack = this.environment.processStackTrace?.(_stack) || _stack
157-
// removing 1 column, because source map points to the wrong
158-
// location for js files, but `column-1` points to the same in both js/ts
159-
// https://github.com/vitejs/vite/issues/8657
160-
stack.column--
161-
// reject multiple inline snapshots at the same location
162-
const duplicateIndex = this._inlineSnapshots.findIndex(s => s.file === stack.file && s.line === stack.line && s.column === stack.column)
163-
if (duplicateIndex >= 0) {
164-
// remove the first one to avoid updating an inline snapshot
165-
this._inlineSnapshots.splice(duplicateIndex, 1)
166-
throw new Error('toMatchInlineSnapshot cannot be called multiple times at the same location.')
167-
}
144+
if (options.stack) {
168145
this._inlineSnapshots.push({
169146
snapshot: receivedSerialized,
170-
...stack,
147+
...options.stack,
171148
})
172149
}
173150
else if (options.rawSnapshot) {
@@ -316,6 +293,36 @@ export default class SnapshotState {
316293
this._snapshotData[key] = receivedSerialized
317294
}
318295

296+
// find call site of toMatchInlineSnapshot
297+
let stack: ParsedStack | undefined
298+
if (isInline) {
299+
const stacks = parseErrorStacktrace(
300+
error || new Error('snapshot'),
301+
{ ignoreStackEntries: [] },
302+
)
303+
const _stack = this._inferInlineSnapshotStack(stacks)
304+
if (!_stack) {
305+
throw new Error(
306+
`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify(
307+
stacks,
308+
)}`,
309+
)
310+
}
311+
stack = this.environment.processStackTrace?.(_stack) || _stack
312+
// removing 1 column, because source map points to the wrong
313+
// location for js files, but `column-1` points to the same in both js/ts
314+
// https://github.com/vitejs/vite/issues/8657
315+
stack.column--
316+
317+
// reject multiple inline snapshots at the same location
318+
if (this._inlineSnapshotStacks.some(s => s.file === stack!.file && s.line === stack!.line && s.column === stack!.column)) {
319+
// remove already succeeded snapshot
320+
this._inlineSnapshots = this._inlineSnapshots.filter(s => !(s.file === stack!.file && s.line === stack!.line && s.column === stack!.column))
321+
throw new Error('toMatchInlineSnapshot cannot be called multiple times at the same location.')
322+
}
323+
this._inlineSnapshotStacks.push(stack)
324+
}
325+
319326
// These are the conditions on when to write snapshots:
320327
// * There's no snapshot file in a non-CI environment.
321328
// * There is a snapshot file and we decided to update the snapshot.
@@ -338,8 +345,7 @@ export default class SnapshotState {
338345
}
339346

340347
this._addSnapshot(key, receivedSerialized, {
341-
error,
342-
isInline,
348+
stack,
343349
rawSnapshot,
344350
})
345351
}
@@ -349,8 +355,7 @@ export default class SnapshotState {
349355
}
350356
else {
351357
this._addSnapshot(key, receivedSerialized, {
352-
error,
353-
isInline,
358+
stack,
354359
rawSnapshot,
355360
})
356361
this.added++

test/cli/fixtures/fails/inline-snapshop-inside-loop-update-none.test.ts

-9
This file was deleted.

test/cli/fixtures/fails/inline-snapshop-inside-loop-update-all.test.ts test/cli/fixtures/fails/inline-snapshop-inside-loop.test.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
import {test, expect} from "vitest";
22

3-
test("ok", () => {
4-
expect("ok").toMatchInlineSnapshot(`"ok"`);
5-
});
6-
73
test("fail 1", () => {
84
for (const str of ["foo", "bar"]) {
95
expect(str).toMatchInlineSnapshot();
106
}
117
});
128

9+
test("fail 2.1", () => {
10+
for (const str of ["foo", "bar"]) {
11+
expect(str).toMatchInlineSnapshot(`"foo"`);
12+
}
13+
});
14+
15+
test("fail 2.2", () => {
16+
for (const str of ["foo", "bar"]) {
17+
expect(str).toMatchInlineSnapshot(`"bar"`);
18+
}
19+
});
20+
1321
test("fail 3", () => {
1422
for (const str of ["ok", "ok"]) {
1523
expect(str).toMatchInlineSnapshot();
1624
}
1725
});
1826

19-
test("somehow ok", () => {
27+
test("fail 4", () => {
2028
for (const str of ["ok", "ok"]) {
2129
expect(str).toMatchInlineSnapshot(`"ok"`);
2230
}

test/cli/test/__snapshots__/fails.test.ts.snap

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@ Error: InlineSnapshot cannot be used inside of test.each or describe.each
3131
Error: InlineSnapshot cannot be used inside of test.each or describe.each"
3232
`;
3333
34-
exports[`should fail inline-snapshop-inside-loop-update-all.test.ts > inline-snapshop-inside-loop-update-all.test.ts 1`] = `
34+
exports[`should fail inline-snapshop-inside-loop.test.ts > inline-snapshop-inside-loop.test.ts 1`] = `
3535
"Error: toMatchInlineSnapshot cannot be called multiple times at the same location.
36+
Error: toMatchInlineSnapshot cannot be called multiple times at the same location.
37+
Error: toMatchInlineSnapshot cannot be called multiple times at the same location.
38+
Error: toMatchInlineSnapshot cannot be called multiple times at the same location.
3639
Error: toMatchInlineSnapshot cannot be called multiple times at the same location."
3740
`;
3841
39-
exports[`should fail inline-snapshop-inside-loop-update-none.test.ts > inline-snapshop-inside-loop-update-none.test.ts 1`] = `"Error: Snapshot \`fail 2 1\` mismatched"`;
40-
4142
exports[`should fail mock-import-proxy-module.test.ts > mock-import-proxy-module.test.ts 1`] = `"Error: There are some problems in resolving the mocks API."`;
4243
4344
exports[`should fail nested-suite.test.ts > nested-suite.test.ts 1`] = `"AssertionError: expected true to be false // Object.is equality"`;

test/cli/test/fails.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const files = await fg('**/*.test.ts', { cwd: root, dot: true })
1010
it.each(files)('should fail %s', async (file) => {
1111
const { stderr } = await runVitest({
1212
root,
13-
update: file === 'inline-snapshop-inside-loop-update-all.test.ts' ? true : undefined,
13+
update: file === 'inline-snapshop-inside-loop.test.ts' ? true : undefined,
1414
}, [file])
1515

1616
expect(stderr).toBeTruthy()

test/typescript/test-d/test.test-d.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable ts/prefer-ts-expect-error */
21
/* eslint-disable ts/ban-ts-comment */
32

43
import { google, type sheets_v4 } from 'googleapis'

0 commit comments

Comments
 (0)