@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-
|
|
2
|
-
export { c as SnapshotData, d as SnapshotSerializer, e as SnapshotSummary, f as SnapshotUpdateState, U as UncheckedSnapshot } from './rawSnapshot.d-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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-
|
|
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';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest/snapshot",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.1.
|
|
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/
|
|
49
|
-
"@vitest/
|
|
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",
|