@vitest/snapshot 4.1.2 → 4.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SnapshotStateOptions, a as SnapshotMatchOptions, b as SnapshotResult, R as RawSnapshotInfo } from './rawSnapshot.d-U2kJUxDr.js';
2
- export { c as SnapshotData, d as SnapshotSerializer, e as SnapshotSummary, f as SnapshotUpdateState, U as UncheckedSnapshot } from './rawSnapshot.d-U2kJUxDr.js';
1
+ import { S as SnapshotStateOptions, a as SnapshotMatchOptions, b as SnapshotResult, R as RawSnapshotInfo } from './rawSnapshot.d-CmdgHNLi.js';
2
+ export { c as SnapshotData, d as SnapshotSerializer, e as SnapshotSummary, f as SnapshotUpdateState, U as UncheckedSnapshot } from './rawSnapshot.d-CmdgHNLi.js';
3
3
  import { ParsedStack } from '@vitest/utils';
4
4
  import { S as SnapshotEnvironment } from './environment.d-DOJxxZV9.js';
5
5
  import { Plugin, Plugins } from '@vitest/pretty-format';
@@ -78,7 +78,11 @@ declare class SnapshotState {
78
78
  getUncheckedCount(): number;
79
79
  getUncheckedKeys(): Array<string>;
80
80
  removeUncheckedKeys(): void;
81
- match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot }: SnapshotMatchOptions): SnapshotReturnOptions;
81
+ probeExpectedSnapshot(options: Pick<SnapshotMatchOptions, "testName" | "testId" | "isInline" | "inlineSnapshot">): {
82
+ data?: string;
83
+ markAsChecked: () => void;
84
+ };
85
+ match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, assertionName }: SnapshotMatchOptions): SnapshotReturnOptions;
82
86
  pack(): Promise<SnapshotResult>;
83
87
  }
84
88
 
@@ -98,6 +102,14 @@ interface AssertOptions {
98
102
  error?: Error;
99
103
  errorMessage?: string;
100
104
  rawSnapshot?: RawSnapshotInfo;
105
+ assertionName?: string;
106
+ }
107
+ /** Same shape as expect.extend custom matcher result (SyncExpectationResult from @vitest/expect) */
108
+ interface MatchResult {
109
+ pass: boolean;
110
+ message: () => string;
111
+ actual?: unknown;
112
+ expected?: unknown;
101
113
  }
102
114
  interface SnapshotClientOptions {
103
115
  isEqual?: (received: unknown, expected: unknown) => boolean;
@@ -111,6 +123,7 @@ declare class SnapshotClient {
111
123
  skipTest(filepath: string, testName: string): void;
112
124
  clearTest(filepath: string, testId: string): void;
113
125
  getSnapshotState(filepath: string): SnapshotState;
126
+ match(options: AssertOptions): MatchResult;
114
127
  assert(options: AssertOptions): void;
115
128
  assertRaw(options: AssertOptions): Promise<void>;
116
129
  clear(): void;
@@ -129,3 +142,4 @@ declare function addSerializer(plugin: Plugin): void;
129
142
  declare function getSerializers(): Plugins;
130
143
 
131
144
  export { SnapshotClient, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotState, SnapshotStateOptions, addSerializer, getSerializers, stripSnapshotIndentation };
145
+ export type { MatchResult };
package/dist/index.js CHANGED
@@ -1,171 +1,8 @@
1
1
  import { parseErrorStacktrace } from '@vitest/utils/source-map';
2
- import { getCallLastIndex, isObject } from '@vitest/utils/helpers';
2
+ import { isObject, getCallLastIndex } from '@vitest/utils/helpers';
3
3
  import { positionToOffset, offsetToLineNumber, lineSplitRE } from '@vitest/utils/offset';
4
4
  import { plugins, format } from '@vitest/pretty-format';
5
5
 
6
- async function saveInlineSnapshots(environment, snapshots) {
7
- const MagicString = (await import('magic-string')).default;
8
- const files = new Set(snapshots.map((i) => i.file));
9
- await Promise.all(Array.from(files).map(async (file) => {
10
- const snaps = snapshots.filter((i) => i.file === file);
11
- const code = await environment.readSnapshotFile(file);
12
- if (code == null) {
13
- throw new Error(`cannot read ${file} when saving inline snapshot`);
14
- }
15
- const s = new MagicString(code);
16
- for (const snap of snaps) {
17
- const index = positionToOffset(code, snap.line, snap.column);
18
- replaceInlineSnap(code, s, index, snap.snapshot);
19
- }
20
- const transformed = s.toString();
21
- if (transformed !== code) {
22
- await environment.saveSnapshotFile(file, transformed);
23
- }
24
- }));
25
- }
26
- const startObjectRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*\{/;
27
- function replaceObjectSnap(code, s, index, newSnap) {
28
- let _code = code.slice(index);
29
- const startMatch = startObjectRegex.exec(_code);
30
- if (!startMatch) {
31
- return false;
32
- }
33
- _code = _code.slice(startMatch.index);
34
- let callEnd = getCallLastIndex(_code);
35
- if (callEnd === null) {
36
- return false;
37
- }
38
- callEnd += index + startMatch.index;
39
- const shapeStart = index + startMatch.index + startMatch[0].length;
40
- const shapeEnd = getObjectShapeEndIndex(code, shapeStart);
41
- const snap = `, ${prepareSnapString(newSnap, code, index)}`;
42
- if (shapeEnd === callEnd) {
43
- // toMatchInlineSnapshot({ foo: expect.any(String) })
44
- s.appendLeft(callEnd, snap);
45
- } else {
46
- // toMatchInlineSnapshot({ foo: expect.any(String) }, ``)
47
- s.overwrite(shapeEnd, callEnd, snap);
48
- }
49
- return true;
50
- }
51
- function getObjectShapeEndIndex(code, index) {
52
- let startBraces = 1;
53
- let endBraces = 0;
54
- while (startBraces !== endBraces && index < code.length) {
55
- const s = code[index++];
56
- if (s === "{") {
57
- startBraces++;
58
- } else if (s === "}") {
59
- endBraces++;
60
- }
61
- }
62
- return index;
63
- }
64
- function prepareSnapString(snap, source, index) {
65
- const lineNumber = offsetToLineNumber(source, index);
66
- const line = source.split(lineSplitRE)[lineNumber - 1];
67
- const indent = line.match(/^\s*/)[0] || "";
68
- const indentNext = indent.includes(" ") ? `${indent}\t` : `${indent} `;
69
- const lines = snap.trim().replace(/\\/g, "\\\\").split(/\n/g);
70
- const isOneline = lines.length <= 1;
71
- const quote = "`";
72
- if (isOneline) {
73
- return `${quote}${lines.join("\n").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}${quote}`;
74
- }
75
- return `${quote}\n${lines.map((i) => i ? indentNext + i : "").join("\n").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}\n${indent}${quote}`;
76
- }
77
- const toMatchInlineName = "toMatchInlineSnapshot";
78
- const toThrowErrorMatchingInlineName = "toThrowErrorMatchingInlineSnapshot";
79
- // on webkit, the line number is at the end of the method, not at the start
80
- function getCodeStartingAtIndex(code, index) {
81
- const indexInline = index - toMatchInlineName.length;
82
- if (code.slice(indexInline, index) === toMatchInlineName) {
83
- return {
84
- code: code.slice(indexInline),
85
- index: indexInline
86
- };
87
- }
88
- const indexThrowInline = index - toThrowErrorMatchingInlineName.length;
89
- if (code.slice(index - indexThrowInline, index) === toThrowErrorMatchingInlineName) {
90
- return {
91
- code: code.slice(index - indexThrowInline),
92
- index: index - indexThrowInline
93
- };
94
- }
95
- return {
96
- code: code.slice(index),
97
- index
98
- };
99
- }
100
- const startRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/;
101
- function replaceInlineSnap(code, s, currentIndex, newSnap) {
102
- const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex);
103
- const startMatch = startRegex.exec(codeStartingAtIndex);
104
- const firstKeywordMatch = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec(codeStartingAtIndex);
105
- if (!startMatch || startMatch.index !== firstKeywordMatch?.index) {
106
- return replaceObjectSnap(code, s, index, newSnap);
107
- }
108
- const quote = startMatch[1];
109
- const startIndex = index + startMatch.index + startMatch[0].length;
110
- const snapString = prepareSnapString(newSnap, code, index);
111
- if (quote === ")") {
112
- s.appendRight(startIndex - 1, snapString);
113
- return true;
114
- }
115
- const quoteEndRE = new RegExp(`(?:^|[^\\\\])${quote}`);
116
- const endMatch = quoteEndRE.exec(code.slice(startIndex));
117
- if (!endMatch) {
118
- return false;
119
- }
120
- const endIndex = startIndex + endMatch.index + endMatch[0].length;
121
- s.overwrite(startIndex - 1, endIndex, snapString);
122
- return true;
123
- }
124
- const INDENTATION_REGEX = /^([^\S\n]*)\S/m;
125
- function stripSnapshotIndentation(inlineSnapshot) {
126
- // Find indentation if exists.
127
- const match = inlineSnapshot.match(INDENTATION_REGEX);
128
- if (!match || !match[1]) {
129
- // No indentation.
130
- return inlineSnapshot;
131
- }
132
- const indentation = match[1];
133
- const lines = inlineSnapshot.split(/\n/g);
134
- if (lines.length <= 2) {
135
- // Must be at least 3 lines.
136
- return inlineSnapshot;
137
- }
138
- if (lines[0].trim() !== "" || lines.at(-1)?.trim() !== "") {
139
- // If not blank first and last lines, abort.
140
- return inlineSnapshot;
141
- }
142
- for (let i = 1; i < lines.length - 1; i++) {
143
- if (lines[i] !== "") {
144
- if (lines[i].indexOf(indentation) !== 0) {
145
- // All lines except first and last should either be blank or have the same
146
- // indent as the first line (or more). If this isn't the case we don't
147
- // want to touch the snapshot at all.
148
- return inlineSnapshot;
149
- }
150
- lines[i] = lines[i].substring(indentation.length);
151
- }
152
- }
153
- // Last line is a special case because it won't have the same indent as others
154
- // but may still have been given some indent to line up.
155
- lines[lines.length - 1] = "";
156
- // Return inline snapshot, now at indent 0.
157
- inlineSnapshot = lines.join("\n");
158
- return inlineSnapshot;
159
- }
160
-
161
- async function saveRawSnapshots(environment, snapshots) {
162
- await Promise.all(snapshots.map(async (snap) => {
163
- if (!snap.readonly) {
164
- await environment.saveSnapshotFile(snap.file, snap.snapshot);
165
- }
166
- }));
167
- }
168
-
169
6
  function getDefaultExportFromCjs(x) {
170
7
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
171
8
  }
@@ -448,6 +285,188 @@ class CounterMap extends DefaultMap {
448
285
  return total;
449
286
  }
450
287
  }
288
+ /* @__NO_SIDE_EFFECTS__ */
289
+ function memo(fn) {
290
+ const cache = new Map();
291
+ return (arg) => {
292
+ if (!cache.has(arg)) {
293
+ cache.set(arg, fn(arg));
294
+ }
295
+ return cache.get(arg);
296
+ };
297
+ }
298
+
299
+ async function saveInlineSnapshots(environment, snapshots) {
300
+ const MagicString = (await import('magic-string')).default;
301
+ const files = new Set(snapshots.map((i) => i.file));
302
+ await Promise.all(Array.from(files).map(async (file) => {
303
+ const snaps = snapshots.filter((i) => i.file === file);
304
+ const code = await environment.readSnapshotFile(file);
305
+ if (code == null) {
306
+ throw new Error(`cannot read ${file} when saving inline snapshot`);
307
+ }
308
+ const s = new MagicString(code);
309
+ for (const snap of snaps) {
310
+ const index = positionToOffset(code, snap.line, snap.column);
311
+ replaceInlineSnap(code, s, index, snap.snapshot, snap.assertionName);
312
+ }
313
+ const transformed = s.toString();
314
+ if (transformed !== code) {
315
+ await environment.saveSnapshotFile(file, transformed);
316
+ }
317
+ }));
318
+ }
319
+ const defaultStartObjectRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*\{/;
320
+ function escapeRegExp(s) {
321
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
322
+ }
323
+ const buildStartObjectRegex = memo((assertionName) => {
324
+ const replaced = defaultStartObjectRegex.source.replace("toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot", escapeRegExp(assertionName));
325
+ return new RegExp(replaced);
326
+ });
327
+ function replaceObjectSnap(code, s, index, newSnap, assertionName) {
328
+ let _code = code.slice(index);
329
+ const regex = assertionName ? buildStartObjectRegex(assertionName) : defaultStartObjectRegex;
330
+ const startMatch = regex.exec(_code);
331
+ if (!startMatch) {
332
+ return false;
333
+ }
334
+ _code = _code.slice(startMatch.index);
335
+ let callEnd = getCallLastIndex(_code);
336
+ if (callEnd === null) {
337
+ return false;
338
+ }
339
+ callEnd += index + startMatch.index;
340
+ const shapeStart = index + startMatch.index + startMatch[0].length;
341
+ const shapeEnd = getObjectShapeEndIndex(code, shapeStart);
342
+ const snap = `, ${prepareSnapString(newSnap, code, index)}`;
343
+ if (shapeEnd === callEnd) {
344
+ // toMatchInlineSnapshot({ foo: expect.any(String) })
345
+ s.appendLeft(callEnd, snap);
346
+ } else {
347
+ // toMatchInlineSnapshot({ foo: expect.any(String) }, ``)
348
+ s.overwrite(shapeEnd, callEnd, snap);
349
+ }
350
+ return true;
351
+ }
352
+ function getObjectShapeEndIndex(code, index) {
353
+ let startBraces = 1;
354
+ let endBraces = 0;
355
+ while (startBraces !== endBraces && index < code.length) {
356
+ const s = code[index++];
357
+ if (s === "{") {
358
+ startBraces++;
359
+ } else if (s === "}") {
360
+ endBraces++;
361
+ }
362
+ }
363
+ return index;
364
+ }
365
+ function prepareSnapString(snap, source, index) {
366
+ const lineNumber = offsetToLineNumber(source, index);
367
+ const line = source.split(lineSplitRE)[lineNumber - 1];
368
+ const indent = line.match(/^\s*/)[0] || "";
369
+ const indentNext = indent.includes(" ") ? `${indent}\t` : `${indent} `;
370
+ const lines = snap.trim().replace(/\\/g, "\\\\").split(/\n/g);
371
+ const isOneline = lines.length <= 1;
372
+ const quote = "`";
373
+ if (isOneline) {
374
+ return `${quote}${lines.join("\n").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}${quote}`;
375
+ }
376
+ return `${quote}\n${lines.map((i) => i ? indentNext + i : "").join("\n").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}\n${indent}${quote}`;
377
+ }
378
+ const defaultMethodNames = ["toMatchInlineSnapshot", "toThrowErrorMatchingInlineSnapshot"];
379
+ // on webkit, the line number is at the end of the method, not at the start
380
+ function getCodeStartingAtIndex(code, index, methodNames) {
381
+ for (const name of methodNames) {
382
+ const adjusted = index - name.length;
383
+ if (adjusted >= 0 && code.slice(adjusted, index) === name) {
384
+ return {
385
+ code: code.slice(adjusted),
386
+ index: adjusted
387
+ };
388
+ }
389
+ }
390
+ return {
391
+ code: code.slice(index),
392
+ index
393
+ };
394
+ }
395
+ const defaultStartRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/;
396
+ const buildStartRegex = memo((assertionName) => {
397
+ const replaced = defaultStartRegex.source.replace("toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot", escapeRegExp(assertionName));
398
+ return new RegExp(replaced);
399
+ });
400
+ function replaceInlineSnap(code, s, currentIndex, newSnap, assertionName) {
401
+ const methodNames = assertionName ? [assertionName] : defaultMethodNames;
402
+ const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex, methodNames);
403
+ const startRegex = assertionName ? buildStartRegex(assertionName) : defaultStartRegex;
404
+ const startMatch = startRegex.exec(codeStartingAtIndex);
405
+ const keywordRegex = assertionName ? new RegExp(escapeRegExp(assertionName)) : /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/;
406
+ const firstKeywordMatch = keywordRegex.exec(codeStartingAtIndex);
407
+ if (!startMatch || startMatch.index !== firstKeywordMatch?.index) {
408
+ return replaceObjectSnap(code, s, index, newSnap, assertionName);
409
+ }
410
+ const quote = startMatch[1];
411
+ const startIndex = index + startMatch.index + startMatch[0].length;
412
+ const snapString = prepareSnapString(newSnap, code, index);
413
+ if (quote === ")") {
414
+ s.appendRight(startIndex - 1, snapString);
415
+ return true;
416
+ }
417
+ const quoteEndRE = new RegExp(`(?:^|[^\\\\])${quote}`);
418
+ const endMatch = quoteEndRE.exec(code.slice(startIndex));
419
+ if (!endMatch) {
420
+ return false;
421
+ }
422
+ const endIndex = startIndex + endMatch.index + endMatch[0].length;
423
+ s.overwrite(startIndex - 1, endIndex, snapString);
424
+ return true;
425
+ }
426
+ const INDENTATION_REGEX = /^([^\S\n]*)\S/m;
427
+ function stripSnapshotIndentation(inlineSnapshot) {
428
+ // Find indentation if exists.
429
+ const match = inlineSnapshot.match(INDENTATION_REGEX);
430
+ if (!match || !match[1]) {
431
+ // No indentation.
432
+ return inlineSnapshot;
433
+ }
434
+ const indentation = match[1];
435
+ const lines = inlineSnapshot.split(/\n/g);
436
+ if (lines.length <= 2) {
437
+ // Must be at least 3 lines.
438
+ return inlineSnapshot;
439
+ }
440
+ if (lines[0].trim() !== "" || lines.at(-1)?.trim() !== "") {
441
+ // If not blank first and last lines, abort.
442
+ return inlineSnapshot;
443
+ }
444
+ for (let i = 1; i < lines.length - 1; i++) {
445
+ if (lines[i] !== "") {
446
+ if (lines[i].indexOf(indentation) !== 0) {
447
+ // All lines except first and last should either be blank or have the same
448
+ // indent as the first line (or more). If this isn't the case we don't
449
+ // want to touch the snapshot at all.
450
+ return inlineSnapshot;
451
+ }
452
+ lines[i] = lines[i].substring(indentation.length);
453
+ }
454
+ }
455
+ // Last line is a special case because it won't have the same indent as others
456
+ // but may still have been given some indent to line up.
457
+ lines[lines.length - 1] = "";
458
+ // Return inline snapshot, now at indent 0.
459
+ inlineSnapshot = lines.join("\n");
460
+ return inlineSnapshot;
461
+ }
462
+
463
+ async function saveRawSnapshots(environment, snapshots) {
464
+ await Promise.all(snapshots.map(async (snap) => {
465
+ if (!snap.readonly) {
466
+ await environment.saveSnapshotFile(snap.file, snap.snapshot);
467
+ }
468
+ }));
469
+ }
451
470
 
452
471
  function isSameStackPosition(x, y) {
453
472
  return x.file === y.file && x.column === y.column && x.line === y.line;
@@ -572,6 +591,12 @@ class SnapshotState {
572
591
  return stacks[i + Number(match[1])] ?? null;
573
592
  }
574
593
  }
594
+ // custom matcher registered via expect.extend() — the wrapper function
595
+ // in jest-extend.ts is named __VITEST_EXTEND_ASSERTION__
596
+ const customMatcherIndex = stacks.findIndex((i) => i.method.includes("__VITEST_EXTEND_ASSERTION__"));
597
+ if (customMatcherIndex !== -1) {
598
+ return stacks[customMatcherIndex + 3] ?? null;
599
+ }
575
600
  // inline snapshot function is called __INLINE_SNAPSHOT__
576
601
  // in integrations/snapshot/chai.ts
577
602
  const stackIndex = stacks.findIndex((i) => i.method.includes("__INLINE_SNAPSHOT__"));
@@ -581,9 +606,10 @@ class SnapshotState {
581
606
  this._dirty = true;
582
607
  if (options.stack) {
583
608
  this._inlineSnapshots.push({
609
+ ...options.stack,
584
610
  snapshot: receivedSerialized,
585
611
  testId: options.testId,
586
- ...options.stack
612
+ assertionName: options.assertionName
587
613
  });
588
614
  } else if (options.rawSnapshot) {
589
615
  this._rawSnapshots.push({
@@ -637,7 +663,19 @@ class SnapshotState {
637
663
  this._uncheckedKeys.clear();
638
664
  }
639
665
  }
640
- match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot }) {
666
+ probeExpectedSnapshot(options) {
667
+ const count = this._counters.get(options.testName) + 1;
668
+ const key = testNameToKey(options.testName, count);
669
+ return {
670
+ data: options?.isInline ? options.inlineSnapshot : this._snapshotData[key],
671
+ markAsChecked: () => {
672
+ this._counters.increment(options.testName);
673
+ this._testIdToKeys.get(options.testId).push(key);
674
+ this._uncheckedKeys.delete(key);
675
+ }
676
+ };
677
+ }
678
+ match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, assertionName }) {
641
679
  // this also increments counter for inline snapshots. maybe we shouldn't?
642
680
  this._counters.increment(testName);
643
681
  const count = this._counters.get(testName);
@@ -725,7 +763,8 @@ class SnapshotState {
725
763
  this._addSnapshot(key, receivedSerialized, {
726
764
  stack,
727
765
  testId,
728
- rawSnapshot
766
+ rawSnapshot,
767
+ assertionName
729
768
  });
730
769
  } else {
731
770
  this.matched.increment(testId);
@@ -734,7 +773,8 @@ class SnapshotState {
734
773
  this._addSnapshot(key, receivedSerialized, {
735
774
  stack,
736
775
  testId,
737
- rawSnapshot
776
+ rawSnapshot,
777
+ assertionName
738
778
  });
739
779
  this.added.increment(testId);
740
780
  }
@@ -844,31 +884,44 @@ class SnapshotClient {
844
884
  }
845
885
  return state;
846
886
  }
847
- assert(options) {
848
- const { filepath, name, testId = name, message, isInline = false, properties, inlineSnapshot, error, errorMessage, rawSnapshot } = options;
887
+ match(options) {
888
+ const { filepath, name, testId = name, message, isInline = false, properties, inlineSnapshot, error, errorMessage, rawSnapshot, assertionName } = options;
849
889
  let { received } = options;
850
890
  if (!filepath) {
851
891
  throw new Error("Snapshot cannot be used outside of test");
852
892
  }
853
893
  const snapshotState = this.getSnapshotState(filepath);
894
+ const testName = [name, ...message ? [message] : []].join(" > ");
895
+ // Probe first so we can mark as checked even on early return
896
+ const expectedSnapshot = snapshotState.probeExpectedSnapshot({
897
+ testName,
898
+ testId,
899
+ isInline,
900
+ inlineSnapshot
901
+ });
854
902
  if (typeof properties === "object") {
855
903
  if (typeof received !== "object" || !received) {
904
+ expectedSnapshot.markAsChecked();
856
905
  throw new Error("Received value must be an object when the matcher has properties");
857
906
  }
907
+ let propertiesPass;
858
908
  try {
859
- const pass = this.options.isEqual?.(received, properties) ?? false;
860
- // const pass = equals(received, properties, [iterableEquality, subsetEquality])
861
- if (!pass) {
862
- throw createMismatchError("Snapshot properties mismatched", snapshotState.expand, received, properties);
863
- } else {
864
- received = deepMergeSnapshot(received, properties);
865
- }
909
+ propertiesPass = this.options.isEqual?.(received, properties) ?? false;
866
910
  } catch (err) {
867
- err.message = errorMessage || "Snapshot mismatched";
911
+ expectedSnapshot.markAsChecked();
868
912
  throw err;
869
913
  }
914
+ if (!propertiesPass) {
915
+ expectedSnapshot.markAsChecked();
916
+ return {
917
+ pass: false,
918
+ message: () => errorMessage || "Snapshot properties mismatched",
919
+ actual: received,
920
+ expected: properties
921
+ };
922
+ }
923
+ received = deepMergeSnapshot(received, properties);
870
924
  }
871
- const testName = [name, ...message ? [message] : []].join(" > ");
872
925
  const { actual, expected, key, pass } = snapshotState.match({
873
926
  testId,
874
927
  testName,
@@ -876,10 +929,21 @@ class SnapshotClient {
876
929
  isInline,
877
930
  error,
878
931
  inlineSnapshot,
879
- rawSnapshot
932
+ rawSnapshot,
933
+ assertionName
880
934
  });
881
- if (!pass) {
882
- throw createMismatchError(`Snapshot \`${key || "unknown"}\` mismatched`, snapshotState.expand, rawSnapshot ? actual : actual?.trim(), rawSnapshot ? expected : expected?.trim());
935
+ return {
936
+ pass,
937
+ message: () => `Snapshot \`${key || "unknown"}\` mismatched`,
938
+ actual: rawSnapshot ? actual : actual?.trim(),
939
+ expected: rawSnapshot ? expected : expected?.trim()
940
+ };
941
+ }
942
+ assert(options) {
943
+ const result = this.match(options);
944
+ if (!result.pass) {
945
+ const snapshotState = this.getSnapshotState(options.filepath);
946
+ throw createMismatchError(result.message(), snapshotState.expand, result.actual, result.expected);
883
947
  }
884
948
  }
885
949
  async assertRaw(options) {
package/dist/manager.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as SnapshotStateOptions, e as SnapshotSummary, b as SnapshotResult } from './rawSnapshot.d-U2kJUxDr.js';
1
+ import { S as SnapshotStateOptions, e as SnapshotSummary, b as SnapshotResult } from './rawSnapshot.d-CmdgHNLi.js';
2
2
  import '@vitest/pretty-format';
3
3
  import './environment.d-DOJxxZV9.js';
4
4
  import '@vitest/utils';
@@ -20,6 +20,7 @@ interface SnapshotMatchOptions {
20
20
  isInline: boolean;
21
21
  error?: Error;
22
22
  rawSnapshot?: RawSnapshotInfo;
23
+ assertionName?: string;
23
24
  }
24
25
  interface SnapshotResult {
25
26
  filepath: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/snapshot",
3
3
  "type": "module",
4
- "version": "4.1.2",
4
+ "version": "4.1.3",
5
5
  "description": "Vitest snapshot manager",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -45,8 +45,8 @@
45
45
  "dependencies": {
46
46
  "magic-string": "^0.30.21",
47
47
  "pathe": "^2.0.3",
48
- "@vitest/pretty-format": "4.1.2",
49
- "@vitest/utils": "4.1.2"
48
+ "@vitest/utils": "4.1.3",
49
+ "@vitest/pretty-format": "4.1.3"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/natural-compare": "^1.4.3",