Skip to content

Commit e8ce94c

Browse files
authoredDec 5, 2024··
fix(snapshot)!: reset snapshot state for retry and repeats (#6817)
1 parent db7a888 commit e8ce94c

31 files changed

+554
-145
lines changed
 

‎packages/snapshot/src/client.ts

+44-44
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ export interface Context {
3434

3535
interface AssertOptions {
3636
received: unknown
37-
filepath?: string
38-
name?: string
37+
filepath: string
38+
name: string
39+
/**
40+
* Not required but needed for `SnapshotClient.clearTest` to implement test-retry behavior.
41+
* @default name
42+
*/
43+
testId?: string
3944
message?: string
4045
isInline?: boolean
4146
properties?: object
@@ -50,51 +55,55 @@ export interface SnapshotClientOptions {
5055
}
5156

5257
export class SnapshotClient {
53-
filepath?: string
54-
name?: string
55-
snapshotState: SnapshotState | undefined
5658
snapshotStateMap: Map<string, SnapshotState> = new Map()
5759

5860
constructor(private options: SnapshotClientOptions = {}) {}
5961

60-
async startCurrentRun(
62+
async setup(
6163
filepath: string,
62-
name: string,
6364
options: SnapshotStateOptions,
6465
): Promise<void> {
65-
this.filepath = filepath
66-
this.name = name
67-
68-
if (this.snapshotState?.testFilePath !== filepath) {
69-
await this.finishCurrentRun()
70-
71-
if (!this.getSnapshotState(filepath)) {
72-
this.snapshotStateMap.set(
73-
filepath,
74-
await SnapshotState.create(filepath, options),
75-
)
76-
}
77-
this.snapshotState = this.getSnapshotState(filepath)
66+
if (this.snapshotStateMap.has(filepath)) {
67+
return
7868
}
69+
this.snapshotStateMap.set(
70+
filepath,
71+
await SnapshotState.create(filepath, options),
72+
)
7973
}
8074

81-
getSnapshotState(filepath: string): SnapshotState {
82-
return this.snapshotStateMap.get(filepath)!
75+
async finish(filepath: string): Promise<SnapshotResult> {
76+
const state = this.getSnapshotState(filepath)
77+
const result = await state.pack()
78+
this.snapshotStateMap.delete(filepath)
79+
return result
80+
}
81+
82+
skipTest(filepath: string, testName: string): void {
83+
const state = this.getSnapshotState(filepath)
84+
state.markSnapshotsAsCheckedForTest(testName)
8385
}
8486

85-
clearTest(): void {
86-
this.filepath = undefined
87-
this.name = undefined
87+
clearTest(filepath: string, testId: string): void {
88+
const state = this.getSnapshotState(filepath)
89+
state.clearTest(testId)
8890
}
8991

90-
skipTestSnapshots(name: string): void {
91-
this.snapshotState?.markSnapshotsAsCheckedForTest(name)
92+
getSnapshotState(filepath: string): SnapshotState {
93+
const state = this.snapshotStateMap.get(filepath)
94+
if (!state) {
95+
throw new Error(
96+
`The snapshot state for '${filepath}' is not found. Did you call 'SnapshotClient.setup()'?`,
97+
)
98+
}
99+
return state
92100
}
93101

94102
assert(options: AssertOptions): void {
95103
const {
96-
filepath = this.filepath,
97-
name = this.name,
104+
filepath,
105+
name,
106+
testId = name,
98107
message,
99108
isInline = false,
100109
properties,
@@ -109,6 +118,8 @@ export class SnapshotClient {
109118
throw new Error('Snapshot cannot be used outside of test')
110119
}
111120

121+
const snapshotState = this.getSnapshotState(filepath)
122+
112123
if (typeof properties === 'object') {
113124
if (typeof received !== 'object' || !received) {
114125
throw new Error(
@@ -122,7 +133,7 @@ export class SnapshotClient {
122133
if (!pass) {
123134
throw createMismatchError(
124135
'Snapshot properties mismatched',
125-
this.snapshotState?.expand,
136+
snapshotState.expand,
126137
received,
127138
properties,
128139
)
@@ -139,9 +150,8 @@ export class SnapshotClient {
139150

140151
const testName = [name, ...(message ? [message] : [])].join(' > ')
141152

142-
const snapshotState = this.getSnapshotState(filepath)
143-
144153
const { actual, expected, key, pass } = snapshotState.match({
154+
testId,
145155
testName,
146156
received,
147157
isInline,
@@ -153,7 +163,7 @@ export class SnapshotClient {
153163
if (!pass) {
154164
throw createMismatchError(
155165
`Snapshot \`${key || 'unknown'}\` mismatched`,
156-
this.snapshotState?.expand,
166+
snapshotState.expand,
157167
actual?.trim(),
158168
expected?.trim(),
159169
)
@@ -165,7 +175,7 @@ export class SnapshotClient {
165175
throw new Error('Raw snapshot is required')
166176
}
167177

168-
const { filepath = this.filepath, rawSnapshot } = options
178+
const { filepath, rawSnapshot } = options
169179

170180
if (rawSnapshot.content == null) {
171181
if (!filepath) {
@@ -189,16 +199,6 @@ export class SnapshotClient {
189199
return this.assert(options)
190200
}
191201

192-
async finishCurrentRun(): Promise<SnapshotResult | null> {
193-
if (!this.snapshotState) {
194-
return null
195-
}
196-
const result = await this.snapshotState.pack()
197-
198-
this.snapshotState = undefined
199-
return result
200-
}
201-
202202
clear(): void {
203203
this.snapshotStateMap.clear()
204204
}

‎packages/snapshot/src/port/inlineSnapshot.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
export interface InlineSnapshot {
1111
snapshot: string
12+
testId: string
1213
file: string
1314
line: number
1415
column: number

‎packages/snapshot/src/port/state.ts

+56-39
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { saveRawSnapshots } from './rawSnapshot'
2323

2424
import {
2525
addExtraLineBreaks,
26+
CounterMap,
27+
DefaultMap,
2628
getSnapshotData,
2729
keyToTestName,
2830
normalizeNewlines,
@@ -47,24 +49,24 @@ interface SaveStatus {
4749
}
4850

4951
export default class SnapshotState {
50-
private _counters: Map<string, number>
52+
private _counters = new CounterMap<string>()
5153
private _dirty: boolean
5254
private _updateSnapshot: SnapshotUpdateState
5355
private _snapshotData: SnapshotData
5456
private _initialData: SnapshotData
5557
private _inlineSnapshots: Array<InlineSnapshot>
56-
private _inlineSnapshotStacks: Array<ParsedStack>
58+
private _inlineSnapshotStacks: Array<ParsedStack & { testId: string }>
59+
private _testIdToKeys = new DefaultMap<string, string[]>(() => [])
5760
private _rawSnapshots: Array<RawSnapshot>
5861
private _uncheckedKeys: Set<string>
5962
private _snapshotFormat: PrettyFormatOptions
6063
private _environment: SnapshotEnvironment
6164
private _fileExists: boolean
62-
63-
added: number
65+
private added = new CounterMap<string>()
66+
private matched = new CounterMap<string>()
67+
private unmatched = new CounterMap<string>()
68+
private updated = new CounterMap<string>()
6469
expand: boolean
65-
matched: number
66-
unmatched: number
67-
updated: number
6870

6971
private constructor(
7072
public testFilePath: string,
@@ -74,20 +76,15 @@ export default class SnapshotState {
7476
) {
7577
const { data, dirty } = getSnapshotData(snapshotContent, options)
7678
this._fileExists = snapshotContent != null // TODO: update on watch?
77-
this._initialData = data
78-
this._snapshotData = data
79+
this._initialData = { ...data }
80+
this._snapshotData = { ...data }
7981
this._dirty = dirty
8082
this._inlineSnapshots = []
8183
this._inlineSnapshotStacks = []
8284
this._rawSnapshots = []
8385
this._uncheckedKeys = new Set(Object.keys(this._snapshotData))
84-
this._counters = new Map()
8586
this.expand = options.expand || false
86-
this.added = 0
87-
this.matched = 0
88-
this.unmatched = 0
8987
this._updateSnapshot = options.updateSnapshot
90-
this.updated = 0
9188
this._snapshotFormat = {
9289
printBasicPrototype: false,
9390
escapeString: false,
@@ -118,6 +115,31 @@ export default class SnapshotState {
118115
})
119116
}
120117

118+
clearTest(testId: string): void {
119+
// clear inline
120+
this._inlineSnapshots = this._inlineSnapshots.filter(s => s.testId !== testId)
121+
this._inlineSnapshotStacks = this._inlineSnapshotStacks.filter(s => s.testId !== testId)
122+
123+
// clear file
124+
for (const key of this._testIdToKeys.get(testId)) {
125+
const name = keyToTestName(key)
126+
const count = this._counters.get(name)
127+
if (count > 0) {
128+
if (key in this._snapshotData || key in this._initialData) {
129+
this._snapshotData[key] = this._initialData[key]
130+
}
131+
this._counters.set(name, count - 1)
132+
}
133+
}
134+
this._testIdToKeys.delete(testId)
135+
136+
// clear stats
137+
this.added.delete(testId)
138+
this.updated.delete(testId)
139+
this.matched.delete(testId)
140+
this.unmatched.delete(testId)
141+
}
142+
121143
protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null {
122144
// if called inside resolves/rejects, stacktrace is different
123145
const promiseIndex = stacks.findIndex(i =>
@@ -138,12 +160,13 @@ export default class SnapshotState {
138160
private _addSnapshot(
139161
key: string,
140162
receivedSerialized: string,
141-
options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack },
163+
options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack; testId: string },
142164
): void {
143165
this._dirty = true
144166
if (options.stack) {
145167
this._inlineSnapshots.push({
146168
snapshot: receivedSerialized,
169+
testId: options.testId,
147170
...options.stack,
148171
})
149172
}
@@ -158,17 +181,6 @@ export default class SnapshotState {
158181
}
159182
}
160183

161-
clear(): void {
162-
this._snapshotData = this._initialData
163-
// this._inlineSnapshots = []
164-
this._counters = new Map()
165-
this.added = 0
166-
this.matched = 0
167-
this.unmatched = 0
168-
this.updated = 0
169-
this._dirty = false
170-
}
171-
172184
async save(): Promise<SaveStatus> {
173185
const hasExternalSnapshots = Object.keys(this._snapshotData).length
174186
const hasInlineSnapshots = this._inlineSnapshots.length
@@ -228,6 +240,7 @@ export default class SnapshotState {
228240
}
229241

230242
match({
243+
testId,
231244
testName,
232245
received,
233246
key,
@@ -236,12 +249,14 @@ export default class SnapshotState {
236249
error,
237250
rawSnapshot,
238251
}: SnapshotMatchOptions): SnapshotReturnOptions {
239-
this._counters.set(testName, (this._counters.get(testName) || 0) + 1)
240-
const count = Number(this._counters.get(testName))
252+
// this also increments counter for inline snapshots. maybe we shouldn't?
253+
this._counters.increment(testName)
254+
const count = this._counters.get(testName)
241255

242256
if (!key) {
243257
key = testNameToKey(testName, count)
244258
}
259+
this._testIdToKeys.get(testId).push(key)
245260

246261
// Do not mark the snapshot as "checked" if the snapshot is inline and
247262
// there's an external snapshot. This way the external snapshot can be
@@ -320,7 +335,7 @@ export default class SnapshotState {
320335
this._inlineSnapshots = this._inlineSnapshots.filter(s => !(s.file === stack!.file && s.line === stack!.line && s.column === stack!.column))
321336
throw new Error('toMatchInlineSnapshot cannot be called multiple times at the same location.')
322337
}
323-
this._inlineSnapshotStacks.push(stack)
338+
this._inlineSnapshotStacks.push({ ...stack, testId })
324339
}
325340

326341
// These are the conditions on when to write snapshots:
@@ -338,27 +353,29 @@ export default class SnapshotState {
338353
if (this._updateSnapshot === 'all') {
339354
if (!pass) {
340355
if (hasSnapshot) {
341-
this.updated++
356+
this.updated.increment(testId)
342357
}
343358
else {
344-
this.added++
359+
this.added.increment(testId)
345360
}
346361

347362
this._addSnapshot(key, receivedSerialized, {
348363
stack,
364+
testId,
349365
rawSnapshot,
350366
})
351367
}
352368
else {
353-
this.matched++
369+
this.matched.increment(testId)
354370
}
355371
}
356372
else {
357373
this._addSnapshot(key, receivedSerialized, {
358374
stack,
375+
testId,
359376
rawSnapshot,
360377
})
361-
this.added++
378+
this.added.increment(testId)
362379
}
363380

364381
return {
@@ -371,7 +388,7 @@ export default class SnapshotState {
371388
}
372389
else {
373390
if (!pass) {
374-
this.unmatched++
391+
this.unmatched.increment(testId)
375392
return {
376393
actual: removeExtraLineBreaks(receivedSerialized),
377394
count,
@@ -384,7 +401,7 @@ export default class SnapshotState {
384401
}
385402
}
386403
else {
387-
this.matched++
404+
this.matched.increment(testId)
388405
return {
389406
actual: '',
390407
count,
@@ -415,10 +432,10 @@ export default class SnapshotState {
415432

416433
const status = await this.save()
417434
snapshot.fileDeleted = status.deleted
418-
snapshot.added = this.added
419-
snapshot.matched = this.matched
420-
snapshot.unmatched = this.unmatched
421-
snapshot.updated = this.updated
435+
snapshot.added = this.added.total()
436+
snapshot.matched = this.matched.total()
437+
snapshot.unmatched = this.unmatched.total()
438+
snapshot.updated = this.updated.total()
422439
snapshot.unchecked = !status.deleted ? uncheckedCount : 0
423440
snapshot.uncheckedKeys = Array.from(uncheckedKeys)
424441

‎packages/snapshot/src/port/utils.ts

+34
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,37 @@ export function deepMergeSnapshot(target: any, source: any): any {
265265
}
266266
return target
267267
}
268+
269+
export class DefaultMap<K, V> extends Map<K, V> {
270+
constructor(
271+
private defaultFn: (key: K) => V,
272+
entries?: Iterable<readonly [K, V]>,
273+
) {
274+
super(entries)
275+
}
276+
277+
override get(key: K): V {
278+
if (!this.has(key)) {
279+
this.set(key, this.defaultFn(key))
280+
}
281+
return super.get(key)!
282+
}
283+
}
284+
285+
export class CounterMap<K> extends DefaultMap<K, number> {
286+
constructor() {
287+
super(() => 0)
288+
}
289+
290+
increment(key: K): void {
291+
this.set(key, this.get(key) + 1)
292+
}
293+
294+
total(): number {
295+
let total = 0
296+
for (const x of this.values()) {
297+
total += x
298+
}
299+
return total
300+
}
301+
}

‎packages/snapshot/src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface SnapshotStateOptions {
2424
}
2525

2626
export interface SnapshotMatchOptions {
27+
testId: string
2728
testName: string
2829
received: unknown
2930
key?: string

‎packages/vitest/src/integrations/snapshot/chai.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,23 @@ function getError(expected: () => void | Error, promise: string | undefined) {
4444
throw new Error('snapshot function didn\'t throw')
4545
}
4646

47-
function getTestNames(test?: Test) {
48-
if (!test) {
49-
return {}
50-
}
47+
function getTestNames(test: Test) {
5148
return {
5249
filepath: test.file.filepath,
5350
name: getNames(test).slice(1).join(' > '),
51+
testId: test.id,
5452
}
5553
}
5654

5755
export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
56+
function getTest(assertionName: string, obj: object) {
57+
const test = utils.flag(obj, 'vitest-test')
58+
if (!test) {
59+
throw new Error(`'${assertionName}' cannot be used without test context`)
60+
}
61+
return test as Test
62+
}
63+
5864
for (const key of ['matchSnapshot', 'toMatchSnapshot']) {
5965
utils.addMethod(
6066
chai.Assertion.prototype,
@@ -70,7 +76,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
7076
throw new Error(`${key} cannot be used with "not"`)
7177
}
7278
const expected = utils.flag(this, 'object')
73-
const test = utils.flag(this, 'vitest-test')
79+
const test = getTest(key, this)
7480
if (typeof properties === 'string' && typeof message === 'undefined') {
7581
message = properties
7682
properties = undefined
@@ -99,7 +105,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
99105
}
100106
const error = new Error('resolves')
101107
const expected = utils.flag(this, 'object')
102-
const test = utils.flag(this, 'vitest-test') as Test
108+
const test = getTest('toMatchFileSnapshot', this)
103109
const errorMessage = utils.flag(this, 'message')
104110

105111
const promise = getSnapshotClient().assertRaw({
@@ -136,8 +142,8 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
136142
if (isNot) {
137143
throw new Error('toMatchInlineSnapshot cannot be used with "not"')
138144
}
139-
const test = utils.flag(this, 'vitest-test')
140-
const isInsideEach = test && (test.each || test.suite?.each)
145+
const test = getTest('toMatchInlineSnapshot', this)
146+
const isInsideEach = test.each || test.suite?.each
141147
if (isInsideEach) {
142148
throw new Error(
143149
'InlineSnapshot cannot be used inside of test.each or describe.each',
@@ -179,7 +185,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
179185
)
180186
}
181187
const expected = utils.flag(this, 'object')
182-
const test = utils.flag(this, 'vitest-test')
188+
const test = getTest('toThrowErrorMatchingSnapshot', this)
183189
const promise = utils.flag(this, 'promise') as string | undefined
184190
const errorMessage = utils.flag(this, 'message')
185191
getSnapshotClient().assert({
@@ -204,8 +210,8 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
204210
'toThrowErrorMatchingInlineSnapshot cannot be used with "not"',
205211
)
206212
}
207-
const test = utils.flag(this, 'vitest-test')
208-
const isInsideEach = test && (test.each || test.suite?.each)
213+
const test = getTest('toThrowErrorMatchingInlineSnapshot', this)
214+
const isInsideEach = test.each || test.suite?.each
209215
if (isInsideEach) {
210216
throw new Error(
211217
'InlineSnapshot cannot be used inside of test.each or describe.each',

‎packages/vitest/src/runtime/runners/test.ts

+8-17
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,8 @@ export class VitestTestRunner implements VitestRunner {
4444
this.workerState.current = file
4545
}
4646

47-
onBeforeRunFiles() {
48-
this.snapshotClient.clear()
49-
}
50-
5147
onAfterRunFiles() {
48+
this.snapshotClient.clear()
5249
this.workerState.current = undefined
5350
}
5451

@@ -62,22 +59,18 @@ export class VitestTestRunner implements VitestRunner {
6259
for (const test of getTests(suite)) {
6360
if (test.mode === 'skip') {
6461
const name = getNames(test).slice(1).join(' > ')
65-
this.snapshotClient.skipTestSnapshots(name)
62+
this.snapshotClient.skipTest(suite.file.filepath, name)
6663
}
6764
}
6865

69-
const result = await this.snapshotClient.finishCurrentRun()
70-
if (result) {
71-
await rpc().snapshotSaved(result)
72-
}
66+
const result = await this.snapshotClient.finish(suite.file.filepath)
67+
await rpc().snapshotSaved(result)
7368
}
7469

7570
this.workerState.current = suite.suite || suite.file
7671
}
7772

7873
onAfterRunTask(test: Task) {
79-
this.snapshotClient.clearTest()
80-
8174
if (this.config.logHeapUsage && typeof process !== 'undefined') {
8275
test.result!.heap = process.memoryUsage().heapUsed
8376
}
@@ -116,11 +109,8 @@ export class VitestTestRunner implements VitestRunner {
116109

117110
// initialize snapshot state before running file suite
118111
if (suite.mode !== 'skip' && 'filepath' in suite) {
119-
// default "name" is irrelevant for Vitest since each snapshot assertion
120-
// (e.g. `toMatchSnapshot`) specifies "filepath" / "name" pair explicitly
121-
await this.snapshotClient.startCurrentRun(
122-
(suite as File).filepath,
123-
'__default_name_',
112+
await this.snapshotClient.setup(
113+
suite.file.filepath,
124114
this.workerState.config.snapshotOptions,
125115
)
126116
}
@@ -129,6 +119,7 @@ export class VitestTestRunner implements VitestRunner {
129119
}
130120

131121
onBeforeTryTask(test: Task) {
122+
this.snapshotClient.clearTest(test.file.filepath, test.id)
132123
setState(
133124
{
134125
assertionCalls: 0,
@@ -138,7 +129,7 @@ export class VitestTestRunner implements VitestRunner {
138129
expectedAssertionsNumberErrorGen: null,
139130
testPath: test.file.filepath,
140131
currentTestName: getTestName(test),
141-
snapshotState: this.snapshotClient.snapshotState,
132+
snapshotState: this.snapshotClient.getSnapshotState(test.file.filepath),
142133
},
143134
(globalThis as any)[GLOBAL_EXPECT],
144135
)

‎test/snapshots/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
"type": "module",
44
"private": true,
55
"scripts": {
6-
"test": "pnpm run test:generate && pnpm run test:update && pnpm run test:snaps",
7-
"test:generate": "node tools/generate-inline-test.mjs",
6+
"test": "pnpm run test:generate && pnpm run test:update && pnpm test:update-new && pnpm test:update-none && pnpm run test:snaps",
7+
"test:generate": "rm -rf ./test-update && cp -r ./test/fixtures/test-update ./test-update",
88
"test:snaps": "vitest run --dir test",
99
"test:update": "vitest run -u --dir test-update",
10+
"test:update-none": "CI=true vitest run --dir test-update",
11+
"test:update-new": "CI=false vitest run --dir test-update",
12+
"test:fixtures": "vitest",
1013
"coverage": "vitest run --coverage"
1114
},
1215
"dependencies": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`file repeats 1`] = `"foo"`;
4+
5+
exports[`file repeats many 1`] = `"foo"`;
6+
7+
exports[`file repeats many 2`] = `"bar"`;
8+
9+
exports[`file retry 1`] = `"foo"`;
10+
11+
exports[`file retry many 1`] = `"foo"`;
12+
13+
exports[`file retry many 2`] = `"bar"`;
14+
15+
exports[`file retry partial 1`] = `"foo"`;
16+
17+
exports[`file retry partial 2`] = `"bar"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`same title exist 1`] = `"a"`;
4+
5+
exports[`same title exist 2`] = `"b"`;
6+
7+
exports[`same title new 1`] = `"a"`;
8+
9+
exports[`same title new 2`] = `"b"`;
10+
11+
exports[`same title new 3`] = `"c"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('file repeats', { repeats: 1 }, () => {
4+
expect('foo').toMatchSnapshot()
5+
})
6+
7+
test('file retry', { retry: 1 }, (ctx) => {
8+
expect('foo').toMatchSnapshot()
9+
if (ctx.task.result?.retryCount === 0) {
10+
throw new Error('boom')
11+
}
12+
})
13+
14+
test('file repeats many', { repeats: 1 }, () => {
15+
expect('foo').toMatchSnapshot()
16+
expect('bar').toMatchSnapshot()
17+
})
18+
19+
test('file retry many', { retry: 1 }, (ctx) => {
20+
expect('foo').toMatchSnapshot()
21+
expect('bar').toMatchSnapshot()
22+
if (ctx.task.result?.retryCount === 0) {
23+
throw new Error('boom')
24+
}
25+
})
26+
27+
test('file retry partial', { retry: 1 }, (ctx) => {
28+
expect('foo').toMatchSnapshot()
29+
if (ctx.task.result?.retryCount === 0) {
30+
throw new Error('boom')
31+
}
32+
expect('bar').toMatchSnapshot()
33+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('inline repeats', { repeats: 1 }, () => {
4+
expect('foo').toMatchInlineSnapshot(`"foo"`)
5+
})
6+
7+
test('inline retry', { retry: 1 }, (ctx) => {
8+
expect('foo').toMatchInlineSnapshot(`"foo"`)
9+
if (ctx.task.result?.retryCount === 0) {
10+
throw new Error('boom')
11+
}
12+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('same title exist', () => {
4+
// correct entry exists in .snap
5+
expect('a').toMatchSnapshot()
6+
})
7+
8+
test('same title exist', () => {
9+
// wrong entry exists in .snap
10+
expect('b').toMatchSnapshot()
11+
})
12+
13+
test('same title new', () => {
14+
expect('a').toMatchSnapshot()
15+
})
16+
17+
test('same title new', () => {
18+
expect('b').toMatchSnapshot()
19+
expect('c').toMatchSnapshot()
20+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('same title', () => {
4+
expect('new').toMatchInlineSnapshot(`"new"`)
5+
expect('new').toMatchInlineSnapshot(`"new"`)
6+
})
7+
8+
test('same title', () => {
9+
expect('a').toMatchInlineSnapshot(`"a"`)
10+
expect('a').toMatchInlineSnapshot(`"a"`)
11+
})
12+
13+
test('same title', () => {
14+
expect('b').toMatchInlineSnapshot(`"b"`)
15+
expect('b').toMatchInlineSnapshot(`"b"`)
16+
})

‎test/snapshots/test/__snapshots__/snapshots.test.ts.snap ‎test/snapshots/test/__snapshots__/test-update.test.ts.snap

+54-18
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`concurrent snapshot update 1`] = `
4-
"import { it } from 'vitest'
5-
6-
it.concurrent('1st', ({ expect }) => {
7-
expect('hi1').toMatchInlineSnapshot(\`"hi1"\`)
8-
})
9-
10-
it.concurrent('2nd', ({ expect }) => {
11-
expect('hi2').toMatchInlineSnapshot(\`"hi2"\`)
12-
})
13-
14-
it.concurrent('3rd', ({ expect }) => {
15-
expect('hi3').toMatchInlineSnapshot(\`"hi3"\`)
16-
})
17-
"
18-
`;
19-
20-
exports[`js snapshots generated correctly 1`] = `
3+
exports[`inline.test.js 1`] = `
214
"import { describe, expect, test } from 'vitest'
225
236
// when snapshots are generated Vitest reruns \`toMatchInlineSnapshot\` checks
@@ -89,3 +72,56 @@ describe('snapshots with properties', () => {
8972
})
9073
"
9174
`;
75+
76+
exports[`inline-concurrent.test.js 1`] = `
77+
"import { it } from 'vitest'
78+
79+
it.concurrent('1st', ({ expect }) => {
80+
expect('hi1').toMatchInlineSnapshot(\`"hi1"\`)
81+
})
82+
83+
it.concurrent('2nd', ({ expect }) => {
84+
expect('hi2').toMatchInlineSnapshot(\`"hi2"\`)
85+
})
86+
87+
it.concurrent('3rd', ({ expect }) => {
88+
expect('hi3').toMatchInlineSnapshot(\`"hi3"\`)
89+
})
90+
"
91+
`;
92+
93+
exports[`retry-inline.test.ts 1`] = `
94+
"import { expect, test } from 'vitest'
95+
96+
test('inline repeats', { repeats: 1 }, () => {
97+
expect('foo').toMatchInlineSnapshot(\`"foo"\`)
98+
})
99+
100+
test('inline retry', { retry: 1 }, (ctx) => {
101+
expect('foo').toMatchInlineSnapshot(\`"foo"\`)
102+
if (ctx.task.result?.retryCount === 0) {
103+
throw new Error('boom')
104+
}
105+
})
106+
"
107+
`;
108+
109+
exports[`same-title-inline.test.js 1`] = `
110+
"import { expect, test } from 'vitest'
111+
112+
test('same title', () => {
113+
expect('new').toMatchInlineSnapshot(\`"new"\`)
114+
expect('new').toMatchInlineSnapshot(\`"new"\`)
115+
})
116+
117+
test('same title', () => {
118+
expect('a').toMatchInlineSnapshot(\`"a"\`)
119+
expect('a').toMatchInlineSnapshot(\`"a"\`)
120+
})
121+
122+
test('same title', () => {
123+
expect('b').toMatchInlineSnapshot(\`"b"\`)
124+
expect('b').toMatchInlineSnapshot(\`"b"\`)
125+
})
126+
"
127+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`file normal 1`] = `"@SNAP4"`;
4+
5+
exports[`file repeats 1`] = `"@SNAP5"`;
6+
7+
exports[`file repeats many 1`] = `"@SNAP7"`;
8+
9+
exports[`file repeats many 2`] = `"@SNAP8"`;
10+
11+
exports[`file retry 1`] = `"@SNAP6"`;
12+
13+
exports[`file retry many 1`] = `"@SNAP9"`;
14+
15+
exports[`file retry many 2`] = `"@SNAP10"`;
16+
17+
exports[`file retry partial 1`] = `"@SNAP11"`;
18+
19+
exports[`file retry partial 2`] = `"@SNAP12"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('inline normal', () => {
4+
expect('@SNAP1').toMatchInlineSnapshot(`"@SNAP1"`)
5+
})
6+
7+
test('inline repeats', { repeats: 1 }, () => {
8+
expect('@SNAP2').toMatchInlineSnapshot(`"@SNAP2"`)
9+
})
10+
11+
test('inline retry', { retry: 1 }, (ctx) => {
12+
expect('@SNAP3').toMatchInlineSnapshot(`"@SNAP3"`)
13+
if (ctx.task.result?.retryCount === 0) {
14+
throw new Error('boom')
15+
}
16+
})
17+
18+
test('file normal', () => {
19+
expect('@SNAP4').toMatchSnapshot()
20+
})
21+
22+
test('file repeats', { repeats: 1 }, () => {
23+
expect('@SNAP5').toMatchSnapshot()
24+
})
25+
26+
test('file retry', { retry: 1 }, (ctx) => {
27+
expect('@SNAP6').toMatchSnapshot()
28+
if (ctx.task.result?.retryCount === 0) {
29+
throw new Error('@retry')
30+
}
31+
})
32+
33+
test('file repeats many', { repeats: 1 }, () => {
34+
expect('@SNAP7').toMatchSnapshot()
35+
expect('@SNAP8').toMatchSnapshot()
36+
})
37+
38+
test('file retry many', { retry: 1 }, (ctx) => {
39+
expect('@SNAP9').toMatchSnapshot()
40+
expect('@SNAP10').toMatchSnapshot()
41+
if (ctx.task.result?.retryCount === 0) {
42+
throw new Error('@retry')
43+
}
44+
})
45+
46+
test('file retry partial', { retry: 1 }, (ctx) => {
47+
expect('@SNAP11').toMatchSnapshot()
48+
if (ctx.task.result?.retryCount === 0) {
49+
throw new Error('@retry')
50+
}
51+
expect('@SNAP12').toMatchSnapshot()
52+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`file repeats 1`] = `"foo"`;
4+
5+
exports[`file repeats many 1`] = `"foo"`;
6+
7+
exports[`file repeats many 2`] = `"bar"`;
8+
9+
exports[`file retry 1`] = `"foo"`;
10+
11+
exports[`file retry many 1`] = `"foo"`;
12+
13+
exports[`file retry many 2`] = `"bar"`;
14+
15+
exports[`file retry partial 1`] = `"foo"`;
16+
17+
exports[`file retry partial 2`] = `"bar"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`same title exist 1`] = `"a"`;
4+
5+
exports[`same title exist 2`] = `"wrong"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('file repeats', { repeats: 1 }, () => {
4+
expect('foo').toMatchSnapshot()
5+
})
6+
7+
test('file retry', { retry: 1 }, (ctx) => {
8+
expect('foo').toMatchSnapshot()
9+
if (ctx.task.result?.retryCount === 0) {
10+
throw new Error('boom')
11+
}
12+
})
13+
14+
test('file repeats many', { repeats: 1 }, () => {
15+
expect('foo').toMatchSnapshot()
16+
expect('bar').toMatchSnapshot()
17+
})
18+
19+
test('file retry many', { retry: 1 }, (ctx) => {
20+
expect('foo').toMatchSnapshot()
21+
expect('bar').toMatchSnapshot()
22+
if (ctx.task.result?.retryCount === 0) {
23+
throw new Error('boom')
24+
}
25+
})
26+
27+
test('file retry partial', { retry: 1 }, (ctx) => {
28+
expect('foo').toMatchSnapshot()
29+
if (ctx.task.result?.retryCount === 0) {
30+
throw new Error('boom')
31+
}
32+
expect('bar').toMatchSnapshot()
33+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('inline repeats', { repeats: 1 }, () => {
4+
expect('foo').toMatchInlineSnapshot()
5+
})
6+
7+
test('inline retry', { retry: 1 }, (ctx) => {
8+
expect('foo').toMatchInlineSnapshot()
9+
if (ctx.task.result?.retryCount === 0) {
10+
throw new Error('boom')
11+
}
12+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('same title exist', () => {
4+
// correct entry exists in .snap
5+
expect('a').toMatchSnapshot()
6+
})
7+
8+
test('same title exist', () => {
9+
// wrong entry exists in .snap
10+
expect('b').toMatchSnapshot()
11+
})
12+
13+
test('same title new', () => {
14+
expect('a').toMatchSnapshot()
15+
})
16+
17+
test('same title new', () => {
18+
expect('b').toMatchSnapshot()
19+
expect('c').toMatchSnapshot()
20+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('same title', () => {
4+
expect('new').toMatchInlineSnapshot()
5+
expect('new').toMatchInlineSnapshot()
6+
})
7+
8+
test('same title', () => {
9+
expect('a').toMatchInlineSnapshot(`"a"`)
10+
expect('a').toMatchInlineSnapshot(`"a"`)
11+
})
12+
13+
test('same title', () => {
14+
expect('b').toMatchInlineSnapshot(`"wrong"`)
15+
expect('b').toMatchInlineSnapshot(`"wrong"`)
16+
})

‎test/snapshots/test/snapshots.test.ts

-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import fs from 'node:fs/promises'
2-
import pathe from 'pathe'
31
import { expect, test } from 'vitest'
42

53
import { editFile, runVitest } from '../../test-utils'
@@ -14,18 +12,6 @@ test('non default snapshot format', () => {
1412
`)
1513
})
1614

17-
test('js snapshots generated correctly', async () => {
18-
const path = pathe.resolve(__dirname, '../test-update/snapshots-inline-js.test.js')
19-
const content = await fs.readFile(path, 'utf8')
20-
expect(content).toMatchSnapshot()
21-
})
22-
23-
test('concurrent snapshot update', async () => {
24-
const path = pathe.resolve(__dirname, '../test-update/inline-test-template-concurrent.test.js')
25-
const content = await fs.readFile(path, 'utf8')
26-
expect(content).toMatchSnapshot()
27-
})
28-
2915
test('--update works for workspace project', async () => {
3016
// setup wrong snapshot value
3117
editFile(

‎test/snapshots/test/summary.test.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import fs from 'node:fs'
2+
import { join } from 'node:path'
3+
import { expect, test } from 'vitest'
4+
import { runVitest } from '../../test-utils'
5+
6+
function fsUpdate(file: string, updateFn: (data: string) => string) {
7+
fs.writeFileSync(file, updateFn(fs.readFileSync(file, 'utf-8')))
8+
}
9+
10+
test('summary', async () => {
11+
// cleanup snapshot
12+
const dir = join(import.meta.dirname, 'fixtures/summary')
13+
const testFile = join(dir, 'basic.test.ts')
14+
const snapshotFile = join(dir, '__snapshots__/basic.test.ts.snap')
15+
fsUpdate(testFile, s => s.replace(/`"@SNAP\d"`/g, ''))
16+
fs.rmSync(snapshotFile, { recursive: true, force: true })
17+
18+
// write everything
19+
let vitest = await runVitest({
20+
root: 'test/fixtures/summary',
21+
update: true,
22+
})
23+
expect(vitest.stdout).toContain('Snapshots 12 written')
24+
25+
// write partially
26+
fsUpdate(testFile, s => s.replace('`"@SNAP2"`', ''))
27+
fsUpdate(snapshotFile, s => s.replace('exports[`file repeats 1`] = `"@SNAP5"`;', ''))
28+
vitest = await runVitest({
29+
root: 'test/fixtures/summary',
30+
update: true,
31+
})
32+
expect(vitest.stdout).toContain('Snapshots 2 written')
33+
34+
// update partially
35+
fsUpdate(testFile, s => s.replace('`"@SNAP2"`', '`"@WRONG"`'))
36+
fsUpdate(snapshotFile, s => s.replace('`"@SNAP5"`', '`"@WRONG"`'))
37+
vitest = await runVitest({
38+
root: 'test/fixtures/summary',
39+
update: true,
40+
})
41+
expect(vitest.stdout).toContain('Snapshots 2 updated')
42+
})
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { expect, test } from 'vitest'
2+
3+
const entries = import.meta.glob('../test-update/*inline*', { eager: true, query: 'raw' })
4+
for (const [file, mod] of Object.entries(entries)) {
5+
test(file.split('/').at(-1)!, () => {
6+
expect((mod as any).default).toMatchSnapshot()
7+
})
8+
}

0 commit comments

Comments
 (0)
Please sign in to comment.