vitest 4.1.5 → 5.0.0-beta.2
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/LICENSE.md +7 -0
- package/dist/browser.d.ts +9 -9
- package/dist/browser.js +4 -4
- package/dist/chunks/{base.RR7zL1h0.js → base.Opc_YHkk.js} +10 -11
- package/dist/chunks/browser.d.BUhkKcDl.d.ts +899 -0
- package/dist/chunks/{cac.DJJmV0dT.js → cac.8N4bOkkB.js} +23 -11
- package/dist/chunks/{cli-api.Cjt90eJu.js → cli-api.B0RFke2g.js} +5799 -353
- package/dist/chunks/{config.d.A1h_Y6Jt.d.ts → config.d.D91DHYaD.d.ts} +11 -3
- package/dist/chunks/{console.3WNpx0tS.js → console.B3IRP8fX.js} +3 -1
- package/dist/chunks/{constants.CPYnjOGj.js → constants.-juJ8b_4.js} +1 -1
- package/dist/chunks/{coverage.d.BZtK59WP.d.ts → coverage.d.g2xbl2sP.d.ts} +4 -0
- package/dist/chunks/{creator.DgVhQm5q.js → creator.BqL2U_x4.js} +1 -1
- package/dist/chunks/{defaults.9aQKnqFk.js → defaults.szbHWQun.js} +4 -2
- package/dist/chunks/environment.d-DOJxxZV9.d.DOJxxZV9.d.ts +17 -0
- package/dist/chunks/general.d.DFAHgpC2.d.ts +247 -0
- package/dist/chunks/{global.d.DVsSRdQ5.d.ts → global.d.DhbKSQoV.d.ts} +4 -5
- package/dist/chunks/{globals.Dj1TGiMC.js → globals.EHmmu0nC.js} +15 -14
- package/dist/chunks/{index.DXx9Dtk7.js → index.CViWo__T.js} +5 -5
- package/dist/chunks/{startVitestModuleRunner.bRl2_oI_.js → index.CbgUM9E5.js} +731 -5
- package/dist/chunks/{test.DNmyFkvJ.js → index.D_7-4CaB.js} +2659 -14
- package/dist/chunks/{init-forks.UV3ZQGQH.js → init-forks.DMge3WTt.js} +1 -1
- package/dist/chunks/{init-threads.D3eCsY76.js → init-threads.eIoyCTon.js} +1 -1
- package/dist/chunks/{init.D98-gwRW.js → init.BVd7SaCA.js} +3 -5
- package/dist/chunks/{nativeModuleMocker.BRN2oBJd.js → nativeModuleMocker.DKpFw0pk.js} +3 -2
- package/dist/chunks/{index.BCY_7LL2.js → nativeModuleRunner.BOeMnHl4.js} +43 -12
- package/dist/chunks/node.CwFbQqI1.js +47 -0
- package/dist/chunks/{reporters.d.CEnv6XRv.d.ts → plugin.d.cIKZEZ16.d.ts} +306 -19
- package/dist/chunks/plugins.DrsmdUE2.js +37 -0
- package/dist/chunks/{rpc.MzXet3jl.js → rpc.DFRWVnRh.js} +16 -1
- package/dist/chunks/{rpc.d.B_8sPU0w.d.ts → rpc.d.7JZuxZ8u.d.ts} +19 -3
- package/dist/chunks/{setup-common.DYx3LtFI.js → setup-common.Hpq30zVk.js} +7 -3
- package/dist/chunks/{utils.BS4fH3nR.js → utils.DKODp04v.js} +3 -4
- package/dist/chunks/{vm.DVLYObm9.js → vm.2okbRRME.js} +6 -6
- package/dist/chunks/{worker.d.ZpHpO4yb.d.ts → worker.d.Bu1kXGw4.d.ts} +3 -3
- package/dist/cli.js +2 -2
- package/dist/config.cjs +4 -2
- package/dist/config.d.ts +21 -18
- package/dist/config.js +2 -2
- package/dist/index.d.ts +84 -22
- package/dist/index.js +15 -13
- package/dist/module-evaluator.d.ts +5 -3
- package/dist/module-evaluator.js +1 -1
- package/dist/node.d.ts +114 -19
- package/dist/node.js +21 -26
- package/dist/runtime.d.ts +40 -4
- package/dist/runtime.js +5 -6
- package/dist/{chunks/traces.DT5aQ62U.js → traces.js} +1 -1
- package/dist/worker.d.ts +5 -5
- package/dist/worker.js +21 -23
- package/dist/workers/forks.js +21 -23
- package/dist/workers/runVmTests.js +17 -16
- package/dist/workers/threads.js +21 -23
- package/dist/workers/vmForks.js +7 -9
- package/dist/workers/vmThreads.js +7 -9
- package/package.json +21 -38
- package/dist/chunks/benchmark.CX_oY03V.js +0 -40
- package/dist/chunks/benchmark.d.DAaHLpsq.d.ts +0 -24
- package/dist/chunks/browser.d.BcoexmFG.d.ts +0 -62
- package/dist/chunks/coverage.DM_a_rWm.js +0 -1087
- package/dist/chunks/evaluatedModules.Dg1zASAC.js +0 -17
- package/dist/chunks/index.DC7d2Pf8.js +0 -729
- package/dist/chunks/index.DdgEv5B1.js +0 -42
- package/dist/chunks/index.UpGiHP7g.js +0 -4255
- package/dist/chunks/nativeModuleRunner.BIakptoF.js +0 -36
- package/dist/chunks/node.COQbm6gK.js +0 -14
- package/dist/chunks/plugin.d.BM2TCi12.d.ts +0 -38
- package/dist/chunks/suite.d.udJtyAgw.d.ts +0 -10
- package/dist/chunks/traces.d.D2T_R8rx.d.ts +0 -60
- package/dist/coverage.d.ts +0 -123
- package/dist/coverage.js +0 -27
- package/dist/environments.d.ts +0 -22
- package/dist/environments.js +0 -5
- package/dist/reporters.d.ts +0 -27
- package/dist/reporters.js +0 -26
- package/dist/runners.d.ts +0 -70
- package/dist/runners.js +0 -19
- package/dist/snapshot.d.ts +0 -9
- package/dist/snapshot.js +0 -6
- package/dist/suite.d.ts +0 -5
- package/dist/suite.js +0 -8
|
@@ -1,18 +1,1594 @@
|
|
|
1
|
-
import { getCurrentTest, updateTask, createTaskCollector,
|
|
2
|
-
import { assertTypes, createSimpleStackTrace, createDefer } from '@vitest/utils/helpers';
|
|
3
|
-
import { getSafeTimers, delay } from '@vitest/utils/timers';
|
|
4
|
-
import { a as getBenchOptions, g as getBenchFn } from './benchmark.CX_oY03V.js';
|
|
1
|
+
import { TestSyntaxError, getCurrentTest, getCurrentSuite, updateTask, createTaskCollector, getHooks, getFn, afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach, describe, it, onTestFailed, onTestFinished, recordArtifact, suite, test } from '@vitest/runner';
|
|
5
2
|
import { i as isChildProcess, w as waitForImportsToResolve, r as resetModules, g as getWorkerState } from './utils.BX5Fg8C4.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { getSafeTimers, delay } from '@vitest/utils/timers';
|
|
4
|
+
import { isMockFunction, fn, spyOn, restoreAllMocks, resetAllMocks, clearAllMocks } from '@vitest/spy';
|
|
5
|
+
import { getType, isObject, noop, assertTypes, ordinal, createSimpleStackTrace, getCallLastIndex, createDefer } from '@vitest/utils/helpers';
|
|
6
|
+
import { positionToOffset, offsetToLineNumber, lineSplitRE } from '@vitest/utils/offset';
|
|
7
|
+
import { parseSingleStack, parseErrorStacktrace } from '@vitest/utils/source-map';
|
|
8
|
+
import { c as commonjsGlobal, g as getDefaultExportFromCjs } from './_commonjsHelpers.D26ty3Ew.js';
|
|
9
|
+
import { R as RealDate, b as resetDate, m as mockDate, r as rpc, V as VitestEvaluatedModules } from './rpc.DFRWVnRh.js';
|
|
10
|
+
import * as chai from 'chai';
|
|
11
|
+
import { use, util } from 'chai';
|
|
12
|
+
import { getNames, createChainable, getTests, getTestName, matchesTags } from '@vitest/runner/utils';
|
|
8
13
|
import { processError } from '@vitest/utils/error';
|
|
14
|
+
import { g as getSerializers, a as addSerializer } from './plugins.DrsmdUE2.js';
|
|
15
|
+
import { format } from '@vitest/pretty-format';
|
|
16
|
+
import { printDiffOrStringify, diff } from '@vitest/utils/diff';
|
|
17
|
+
import { stringify, inspect } from '@vitest/utils/display';
|
|
18
|
+
import c from 'tinyrainbow';
|
|
9
19
|
import { normalize } from 'pathe';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
import { expectTypeOf } from 'expect-type';
|
|
21
|
+
|
|
22
|
+
const ChaiStyleAssertions = (chai, utils) => {
|
|
23
|
+
function defProperty(name, delegateTo) {
|
|
24
|
+
utils.addProperty(chai.Assertion.prototype, name, function() {
|
|
25
|
+
const jestMethod = chai.Assertion.prototype[delegateTo];
|
|
26
|
+
if (!jestMethod) throw new Error(`Cannot delegate to ${String(delegateTo)}: method not found. Ensure JestChaiExpect plugin is loaded first.`);
|
|
27
|
+
return jestMethod.call(this);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function defPropertyWithArgs(name, delegateTo, ...args) {
|
|
31
|
+
utils.addProperty(chai.Assertion.prototype, name, function() {
|
|
32
|
+
const jestMethod = chai.Assertion.prototype[delegateTo];
|
|
33
|
+
if (!jestMethod) throw new Error(`Cannot delegate to ${String(delegateTo)}: method not found. Ensure JestChaiExpect plugin is loaded first.`);
|
|
34
|
+
return jestMethod.call(this, ...args);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function defMethod(name, delegateTo) {
|
|
38
|
+
utils.addMethod(chai.Assertion.prototype, name, function(...args) {
|
|
39
|
+
const jestMethod = chai.Assertion.prototype[delegateTo];
|
|
40
|
+
if (!jestMethod) throw new Error(`Cannot delegate to ${String(delegateTo)}: method not found. Ensure JestChaiExpect plugin is loaded first.`);
|
|
41
|
+
return jestMethod.call(this, ...args);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// API to (somewhat) mirror sinon-chai
|
|
45
|
+
// https://github.com/chaijs/sinon-chai
|
|
46
|
+
defProperty("called", "toHaveBeenCalled");
|
|
47
|
+
defProperty("calledOnce", "toHaveBeenCalledOnce");
|
|
48
|
+
defPropertyWithArgs("calledTwice", "toHaveBeenCalledTimes", 2);
|
|
49
|
+
defPropertyWithArgs("calledThrice", "toHaveBeenCalledTimes", 3);
|
|
50
|
+
defMethod("callCount", "toHaveBeenCalledTimes");
|
|
51
|
+
defMethod("calledWith", "toHaveBeenCalledWith");
|
|
52
|
+
defMethod("calledOnceWith", "toHaveBeenCalledExactlyOnceWith");
|
|
53
|
+
defMethod("lastCalledWith", "toHaveBeenLastCalledWith");
|
|
54
|
+
defMethod("nthCalledWith", "toHaveBeenNthCalledWith");
|
|
55
|
+
defMethod("returned", "toHaveReturned");
|
|
56
|
+
defMethod("returnedWith", "toHaveReturnedWith");
|
|
57
|
+
defMethod("returnedTimes", "toHaveReturnedTimes");
|
|
58
|
+
defMethod("lastReturnedWith", "toHaveLastReturnedWith");
|
|
59
|
+
defMethod("nthReturnedWith", "toHaveNthReturnedWith");
|
|
60
|
+
defMethod("calledBefore", "toHaveBeenCalledBefore");
|
|
61
|
+
defMethod("calledAfter", "toHaveBeenCalledAfter");
|
|
62
|
+
// TODO: implement
|
|
63
|
+
// defMethod('thrown', 'toHaveThrown')
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const MATCHERS_OBJECT = Symbol.for("matchers-object");
|
|
67
|
+
const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object");
|
|
68
|
+
const GLOBAL_EXPECT = Symbol.for("expect-global");
|
|
69
|
+
const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for("asymmetric-matchers-object");
|
|
70
|
+
|
|
71
|
+
// selectively ported from https://github.com/jest-community/jest-extended
|
|
72
|
+
const customMatchers = {
|
|
73
|
+
toSatisfy(actual, expected, message) {
|
|
74
|
+
const { printReceived, printExpected, matcherHint } = this.utils;
|
|
75
|
+
const pass = expected(actual);
|
|
76
|
+
return {
|
|
77
|
+
pass,
|
|
78
|
+
message: () => pass ? `\
|
|
79
|
+
${matcherHint(".not.toSatisfy", "received", "")}
|
|
80
|
+
|
|
81
|
+
Expected value to not satisfy:
|
|
82
|
+
${message || printExpected(expected)}
|
|
83
|
+
Received:
|
|
84
|
+
${printReceived(actual)}` : `\
|
|
85
|
+
${matcherHint(".toSatisfy", "received", "")}
|
|
86
|
+
|
|
87
|
+
Expected value to satisfy:
|
|
88
|
+
${message || printExpected(expected)}
|
|
89
|
+
|
|
90
|
+
Received:
|
|
91
|
+
${printReceived(actual)}`
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
toBeOneOf(actual, expected) {
|
|
95
|
+
const { equals, customTesters } = this;
|
|
96
|
+
const { printReceived, printExpected, matcherHint } = this.utils;
|
|
97
|
+
let pass;
|
|
98
|
+
if (Array.isArray(expected)) pass = expected.length === 0 || expected.some((item) => equals(item, actual, customTesters));
|
|
99
|
+
else if (expected instanceof Set) pass = expected.size === 0 || expected.has(actual) || [...expected].some((item) => equals(item, actual, customTesters));
|
|
100
|
+
else throw new TypeError(`You must provide an array or set to ${matcherHint(".toBeOneOf")}, not '${typeof expected}'.`);
|
|
101
|
+
return {
|
|
102
|
+
pass,
|
|
103
|
+
message: () => pass ? `\
|
|
104
|
+
${matcherHint(".not.toBeOneOf", "received", "")}
|
|
105
|
+
|
|
106
|
+
Expected value to not be one of:
|
|
107
|
+
${printExpected(expected)}
|
|
108
|
+
Received:
|
|
109
|
+
${printReceived(actual)}` : `\
|
|
110
|
+
${matcherHint(".toBeOneOf", "received", "")}
|
|
111
|
+
|
|
112
|
+
Expected value to be one of:
|
|
113
|
+
${printExpected(expected)}
|
|
114
|
+
|
|
115
|
+
Received:
|
|
116
|
+
${printReceived(actual)}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const EXPECTED_COLOR = c.green;
|
|
122
|
+
const RECEIVED_COLOR = c.red;
|
|
123
|
+
const INVERTED_COLOR = c.inverse;
|
|
124
|
+
const BOLD_WEIGHT = c.bold;
|
|
125
|
+
const DIM_COLOR = c.dim;
|
|
126
|
+
function matcherHint(matcherName, received = "received", expected = "expected", options = {}) {
|
|
127
|
+
const { comment = "", isDirectExpectCall = false, isNot = false, promise = "", secondArgument = "", expectedColor = EXPECTED_COLOR, receivedColor = RECEIVED_COLOR, secondArgumentColor = EXPECTED_COLOR } = options;
|
|
128
|
+
let hint = "";
|
|
129
|
+
let dimString = "expect";
|
|
130
|
+
if (!isDirectExpectCall && received !== "") {
|
|
131
|
+
hint += DIM_COLOR(`${dimString}(`) + receivedColor(received);
|
|
132
|
+
dimString = ")";
|
|
133
|
+
}
|
|
134
|
+
if (promise !== "") {
|
|
135
|
+
hint += DIM_COLOR(`${dimString}.`) + promise;
|
|
136
|
+
dimString = "";
|
|
137
|
+
}
|
|
138
|
+
if (isNot) {
|
|
139
|
+
hint += `${DIM_COLOR(`${dimString}.`)}not`;
|
|
140
|
+
dimString = "";
|
|
141
|
+
}
|
|
142
|
+
if (matcherName.includes("."))
|
|
143
|
+
// Old format: for backward compatibility,
|
|
144
|
+
// especially without promise or isNot options
|
|
145
|
+
dimString += matcherName;
|
|
146
|
+
else {
|
|
147
|
+
// New format: omit period from matcherName arg
|
|
148
|
+
hint += DIM_COLOR(`${dimString}.`) + matcherName;
|
|
149
|
+
dimString = "";
|
|
150
|
+
}
|
|
151
|
+
if (expected === "") dimString += "()";
|
|
152
|
+
else {
|
|
153
|
+
hint += DIM_COLOR(`${dimString}(`) + expectedColor(expected);
|
|
154
|
+
if (secondArgument) hint += DIM_COLOR(", ") + secondArgumentColor(secondArgument);
|
|
155
|
+
dimString = ")";
|
|
156
|
+
}
|
|
157
|
+
if (comment !== "") dimString += ` // ${comment}`;
|
|
158
|
+
if (dimString !== "") hint += DIM_COLOR(dimString);
|
|
159
|
+
return hint;
|
|
160
|
+
}
|
|
161
|
+
const SPACE_SYMBOL = "·";
|
|
162
|
+
// Instead of inverse highlight which now implies a change,
|
|
163
|
+
// replace common spaces with middle dot at the end of any line.
|
|
164
|
+
function replaceTrailingSpaces(text) {
|
|
165
|
+
return text.replace(/\s+$/gm, (spaces) => SPACE_SYMBOL.repeat(spaces.length));
|
|
166
|
+
}
|
|
167
|
+
function printReceived(object) {
|
|
168
|
+
return RECEIVED_COLOR(replaceTrailingSpaces(stringify(object)));
|
|
169
|
+
}
|
|
170
|
+
function printExpected(value) {
|
|
171
|
+
return EXPECTED_COLOR(replaceTrailingSpaces(stringify(value)));
|
|
172
|
+
}
|
|
173
|
+
function getMatcherUtils() {
|
|
174
|
+
return {
|
|
175
|
+
EXPECTED_COLOR,
|
|
176
|
+
RECEIVED_COLOR,
|
|
177
|
+
INVERTED_COLOR,
|
|
178
|
+
BOLD_WEIGHT,
|
|
179
|
+
DIM_COLOR,
|
|
180
|
+
diff,
|
|
181
|
+
matcherHint,
|
|
182
|
+
printReceived,
|
|
183
|
+
printExpected,
|
|
184
|
+
printDiffOrStringify,
|
|
185
|
+
printWithType
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function printWithType(name, value, print) {
|
|
189
|
+
const type = getType(value);
|
|
190
|
+
return (type !== "null" && type !== "undefined" ? `${name} has type: ${type}\n` : "") + `${name} has value: ${print(value)}`;
|
|
191
|
+
}
|
|
192
|
+
function addCustomEqualityTesters(newTesters) {
|
|
193
|
+
if (!Array.isArray(newTesters)) throw new TypeError(`expect.customEqualityTesters: Must be set to an array of Testers. Was given "${getType(newTesters)}"`);
|
|
194
|
+
globalThis[JEST_MATCHERS_OBJECT].customEqualityTesters.push(...newTesters);
|
|
195
|
+
}
|
|
196
|
+
function getCustomEqualityTesters() {
|
|
197
|
+
return globalThis[JEST_MATCHERS_OBJECT].customEqualityTesters;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Extracted out of jasmine 2.5.2
|
|
201
|
+
function equals(a, b, customTesters, strictCheck) {
|
|
202
|
+
customTesters = customTesters || [];
|
|
203
|
+
return eq(a, b, [], [], customTesters, strictCheck ? hasKey : hasDefinedKey);
|
|
204
|
+
}
|
|
205
|
+
function isAsymmetric(obj) {
|
|
206
|
+
return !!obj && typeof obj === "object" && "asymmetricMatch" in obj && isA("Function", obj.asymmetricMatch);
|
|
207
|
+
}
|
|
208
|
+
function asymmetricMatch(a, b, customTesters) {
|
|
209
|
+
const asymmetricA = isAsymmetric(a);
|
|
210
|
+
const asymmetricB = isAsymmetric(b);
|
|
211
|
+
if (asymmetricA && asymmetricB) return;
|
|
212
|
+
if (asymmetricA) return a.asymmetricMatch(b, customTesters);
|
|
213
|
+
if (asymmetricB) return b.asymmetricMatch(a, customTesters);
|
|
214
|
+
}
|
|
215
|
+
// https://github.com/jestjs/jest/blob/905bcbced3d40cdf7aadc4cdf6fb731c4bb3dbe3/packages/expect-utils/src/utils.ts#L509
|
|
216
|
+
function isError(value) {
|
|
217
|
+
if (typeof Error.isError === "function") return Error.isError(value);
|
|
218
|
+
switch (Object.prototype.toString.call(value)) {
|
|
219
|
+
case "[object Error]":
|
|
220
|
+
case "[object Exception]":
|
|
221
|
+
case "[object DOMException]": return true;
|
|
222
|
+
default: return value instanceof Error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Equality function lovingly adapted from isEqual in
|
|
226
|
+
// [Underscore](http://underscorejs.org)
|
|
227
|
+
function eq(a, b, aStack, bStack, customTesters, hasKey) {
|
|
228
|
+
let result = true;
|
|
229
|
+
const asymmetricResult = asymmetricMatch(a, b, customTesters);
|
|
230
|
+
if (asymmetricResult !== void 0) return asymmetricResult;
|
|
231
|
+
const testerContext = { equals };
|
|
232
|
+
for (let i = 0; i < customTesters.length; i++) {
|
|
233
|
+
const customTesterResult = customTesters[i].call(testerContext, a, b, customTesters);
|
|
234
|
+
if (customTesterResult !== void 0) return customTesterResult;
|
|
235
|
+
}
|
|
236
|
+
if (typeof URL === "function" && a instanceof URL && b instanceof URL) return a.href === b.href;
|
|
237
|
+
if (Object.is(a, b)) return true;
|
|
238
|
+
// A strict comparison is necessary because `null == undefined`.
|
|
239
|
+
if (a === null || b === null) return a === b;
|
|
240
|
+
const className = Object.prototype.toString.call(a);
|
|
241
|
+
if (className !== Object.prototype.toString.call(b)) return false;
|
|
242
|
+
switch (className) {
|
|
243
|
+
case "[object Boolean]":
|
|
244
|
+
case "[object String]":
|
|
245
|
+
case "[object Number]": if (typeof a !== typeof b)
|
|
246
|
+
// One is a primitive, one a `new Primitive()`
|
|
247
|
+
return false;
|
|
248
|
+
else if (typeof a !== "object" && typeof b !== "object")
|
|
249
|
+
// both are proper primitives
|
|
250
|
+
return Object.is(a, b);
|
|
251
|
+
else
|
|
252
|
+
// both are `new Primitive()`s
|
|
253
|
+
return Object.is(a.valueOf(), b.valueOf());
|
|
254
|
+
case "[object Date]": {
|
|
255
|
+
const numA = +a;
|
|
256
|
+
const numB = +b;
|
|
257
|
+
// Coerce dates to numeric primitive values. Dates are compared by their
|
|
258
|
+
// millisecond representations. Note that invalid dates with millisecond representations
|
|
259
|
+
// of `NaN` are equivalent.
|
|
260
|
+
return numA === numB || Number.isNaN(numA) && Number.isNaN(numB);
|
|
261
|
+
}
|
|
262
|
+
case "[object RegExp]": return a.source === b.source && a.flags === b.flags;
|
|
263
|
+
case "[object Temporal.Instant]":
|
|
264
|
+
case "[object Temporal.ZonedDateTime]":
|
|
265
|
+
case "[object Temporal.PlainDateTime]":
|
|
266
|
+
case "[object Temporal.PlainDate]":
|
|
267
|
+
case "[object Temporal.PlainTime]":
|
|
268
|
+
case "[object Temporal.PlainYearMonth]":
|
|
269
|
+
case "[object Temporal.PlainMonthDay]": return a.equals(b);
|
|
270
|
+
case "[object Temporal.Duration]": return a.toString() === b.toString();
|
|
271
|
+
}
|
|
272
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
273
|
+
// Use DOM3 method isEqualNode (IE>=9)
|
|
274
|
+
if (isDomNode(a) && isDomNode(b)) return a.isEqualNode(b);
|
|
275
|
+
// Used to detect circular references.
|
|
276
|
+
let length = aStack.length;
|
|
277
|
+
while (length--)
|
|
278
|
+
// Linear search. Performance is inversely proportional to the number of
|
|
279
|
+
// unique nested structures.
|
|
280
|
+
// circular references at same depth are equal
|
|
281
|
+
// circular reference is not equal to non-circular one
|
|
282
|
+
if (aStack[length] === a) return bStack[length] === b;
|
|
283
|
+
else if (bStack[length] === b) return false;
|
|
284
|
+
// Add the first object to the stack of traversed objects.
|
|
285
|
+
aStack.push(a);
|
|
286
|
+
bStack.push(b);
|
|
287
|
+
// Recursively compare objects and arrays.
|
|
288
|
+
// Compare array lengths to determine if a deep comparison is necessary.
|
|
289
|
+
if (className === "[object Array]" && a.length !== b.length) return false;
|
|
290
|
+
if (isError(a) && isError(b)) try {
|
|
291
|
+
return isErrorEqual(a, b, aStack, bStack, customTesters, hasKey);
|
|
292
|
+
} finally {
|
|
293
|
+
aStack.pop();
|
|
294
|
+
bStack.pop();
|
|
295
|
+
}
|
|
296
|
+
// Deep compare objects.
|
|
297
|
+
const aKeys = keys(a, hasKey);
|
|
298
|
+
let key;
|
|
299
|
+
let size = aKeys.length;
|
|
300
|
+
// Ensure that both objects contain the same number of properties before comparing deep equality.
|
|
301
|
+
if (keys(b, hasKey).length !== size) return false;
|
|
302
|
+
while (size--) {
|
|
303
|
+
key = aKeys[size];
|
|
304
|
+
// Deep compare each member
|
|
305
|
+
result = hasKey(b, key) && eq(a[key], b[key], aStack, bStack, customTesters, hasKey);
|
|
306
|
+
if (!result) return false;
|
|
307
|
+
}
|
|
308
|
+
// Remove the first object from the stack of traversed objects.
|
|
309
|
+
aStack.pop();
|
|
310
|
+
bStack.pop();
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
function isErrorEqual(a, b, aStack, bStack, customTesters, hasKey) {
|
|
314
|
+
// https://nodejs.org/docs/latest-v22.x/api/assert.html#comparison-details
|
|
315
|
+
// - [[Prototype]] of objects are compared using the === operator.
|
|
316
|
+
// - Only enumerable "own" properties are considered.
|
|
317
|
+
// - Error names, messages, causes, and errors are always compared, even if these are not enumerable properties. errors is also compared.
|
|
318
|
+
let result = Object.prototype.toString.call(a) === Object.prototype.toString.call(b) && a.name === b.name && a.message === b.message;
|
|
319
|
+
// check Error.cause asymmetrically
|
|
320
|
+
if (typeof b.cause !== "undefined") result &&= eq(a.cause, b.cause, aStack, bStack, customTesters, hasKey);
|
|
321
|
+
// AggregateError.errors
|
|
322
|
+
if (a instanceof AggregateError && b instanceof AggregateError) result &&= eq(a.errors, b.errors, aStack, bStack, customTesters, hasKey);
|
|
323
|
+
// spread to compare enumerable properties
|
|
324
|
+
result &&= eq({ ...a }, { ...b }, aStack, bStack, customTesters, hasKey);
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
function keys(obj, hasKey) {
|
|
328
|
+
const keys = [];
|
|
329
|
+
for (const key in obj) if (hasKey(obj, key)) keys.push(key);
|
|
330
|
+
return keys.concat(Object.getOwnPropertySymbols(obj).filter((symbol) => Object.getOwnPropertyDescriptor(obj, symbol).enumerable));
|
|
331
|
+
}
|
|
332
|
+
function hasDefinedKey(obj, key) {
|
|
333
|
+
return hasKey(obj, key) && obj[key] !== void 0;
|
|
334
|
+
}
|
|
335
|
+
function hasKey(obj, key) {
|
|
336
|
+
return Object.hasOwn(obj, key);
|
|
337
|
+
}
|
|
338
|
+
function isA(typeName, value) {
|
|
339
|
+
return Object.prototype.toString.apply(value) === `[object ${typeName}]`;
|
|
340
|
+
}
|
|
341
|
+
function isDomNode(obj) {
|
|
342
|
+
return obj !== null && typeof obj === "object" && "nodeType" in obj && typeof obj.nodeType === "number" && "nodeName" in obj && typeof obj.nodeName === "string" && "isEqualNode" in obj && typeof obj.isEqualNode === "function";
|
|
343
|
+
}
|
|
344
|
+
// SENTINEL constants are from https://github.com/facebook/immutable-js
|
|
345
|
+
const IS_KEYED_SENTINEL = "@@__IMMUTABLE_KEYED__@@";
|
|
346
|
+
const IS_SET_SENTINEL = "@@__IMMUTABLE_SET__@@";
|
|
347
|
+
const IS_LIST_SENTINEL = "@@__IMMUTABLE_LIST__@@";
|
|
348
|
+
const IS_ORDERED_SENTINEL = "@@__IMMUTABLE_ORDERED__@@";
|
|
349
|
+
const IS_RECORD_SYMBOL = "@@__IMMUTABLE_RECORD__@@";
|
|
350
|
+
function isImmutableUnorderedKeyed(maybeKeyed) {
|
|
351
|
+
return !!(maybeKeyed && maybeKeyed[IS_KEYED_SENTINEL] && !maybeKeyed[IS_ORDERED_SENTINEL]);
|
|
352
|
+
}
|
|
353
|
+
function isImmutableUnorderedSet(maybeSet) {
|
|
354
|
+
return !!(maybeSet && maybeSet[IS_SET_SENTINEL] && !maybeSet[IS_ORDERED_SENTINEL]);
|
|
355
|
+
}
|
|
356
|
+
function isObjectLiteral(source) {
|
|
357
|
+
return source != null && typeof source === "object" && !Array.isArray(source);
|
|
358
|
+
}
|
|
359
|
+
function isImmutableList(source) {
|
|
360
|
+
return Boolean(source && isObjectLiteral(source) && source[IS_LIST_SENTINEL]);
|
|
361
|
+
}
|
|
362
|
+
function isImmutableOrderedKeyed(source) {
|
|
363
|
+
return Boolean(source && isObjectLiteral(source) && source[IS_KEYED_SENTINEL] && source[IS_ORDERED_SENTINEL]);
|
|
364
|
+
}
|
|
365
|
+
function isImmutableOrderedSet(source) {
|
|
366
|
+
return Boolean(source && isObjectLiteral(source) && source[IS_SET_SENTINEL] && source[IS_ORDERED_SENTINEL]);
|
|
367
|
+
}
|
|
368
|
+
function isImmutableRecord(source) {
|
|
369
|
+
return Boolean(source && isObjectLiteral(source) && source[IS_RECORD_SYMBOL]);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
|
373
|
+
*
|
|
374
|
+
* This source code is licensed under the MIT license found in the
|
|
375
|
+
* LICENSE file in the root directory of this source tree.
|
|
376
|
+
*
|
|
377
|
+
*/
|
|
378
|
+
const IteratorSymbol = Symbol.iterator;
|
|
379
|
+
function hasIterator(object) {
|
|
380
|
+
return !!(object != null && object[IteratorSymbol]);
|
|
381
|
+
}
|
|
382
|
+
function iterableEquality(a, b, customTesters = [], aStack = [], bStack = []) {
|
|
383
|
+
if (typeof a !== "object" || typeof b !== "object" || Array.isArray(a) || Array.isArray(b) || !hasIterator(a) || !hasIterator(b)) return;
|
|
384
|
+
if (a.constructor !== b.constructor) return false;
|
|
385
|
+
let length = aStack.length;
|
|
386
|
+
while (length--)
|
|
387
|
+
// Linear search. Performance is inversely proportional to the number of
|
|
388
|
+
// unique nested structures.
|
|
389
|
+
// circular references at same depth are equal
|
|
390
|
+
// circular reference is not equal to non-circular one
|
|
391
|
+
if (aStack[length] === a) return bStack[length] === b;
|
|
392
|
+
aStack.push(a);
|
|
393
|
+
bStack.push(b);
|
|
394
|
+
const filteredCustomTesters = [...customTesters.filter((t) => t !== iterableEquality), iterableEqualityWithStack];
|
|
395
|
+
function iterableEqualityWithStack(a, b) {
|
|
396
|
+
return iterableEquality(a, b, [...customTesters], [...aStack], [...bStack]);
|
|
397
|
+
}
|
|
398
|
+
if (a.size !== void 0) {
|
|
399
|
+
if (a.size !== b.size) return false;
|
|
400
|
+
else if (isA("Set", a) || isImmutableUnorderedSet(a)) {
|
|
401
|
+
let allFound = true;
|
|
402
|
+
for (const aValue of a) if (!b.has(aValue)) {
|
|
403
|
+
let has = false;
|
|
404
|
+
for (const bValue of b) if (equals(aValue, bValue, filteredCustomTesters) === true) has = true;
|
|
405
|
+
if (has === false) {
|
|
406
|
+
allFound = false;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Remove the first value from the stack of traversed values.
|
|
411
|
+
aStack.pop();
|
|
412
|
+
bStack.pop();
|
|
413
|
+
return allFound;
|
|
414
|
+
} else if (isA("Map", a) || isImmutableUnorderedKeyed(a)) {
|
|
415
|
+
let allFound = true;
|
|
416
|
+
for (const aEntry of a) if (!b.has(aEntry[0]) || !equals(aEntry[1], b.get(aEntry[0]), filteredCustomTesters)) {
|
|
417
|
+
let has = false;
|
|
418
|
+
for (const bEntry of b) {
|
|
419
|
+
const matchedKey = equals(aEntry[0], bEntry[0], filteredCustomTesters);
|
|
420
|
+
let matchedValue = false;
|
|
421
|
+
if (matchedKey === true) matchedValue = equals(aEntry[1], bEntry[1], filteredCustomTesters);
|
|
422
|
+
if (matchedValue === true) has = true;
|
|
423
|
+
}
|
|
424
|
+
if (has === false) {
|
|
425
|
+
allFound = false;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Remove the first value from the stack of traversed values.
|
|
430
|
+
aStack.pop();
|
|
431
|
+
bStack.pop();
|
|
432
|
+
return allFound;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const bIterator = b[IteratorSymbol]();
|
|
436
|
+
for (const aValue of a) {
|
|
437
|
+
const nextB = bIterator.next();
|
|
438
|
+
if (nextB.done || !equals(aValue, nextB.value, filteredCustomTesters)) return false;
|
|
439
|
+
}
|
|
440
|
+
if (!bIterator.next().done) return false;
|
|
441
|
+
if (!isImmutableList(a) && !isImmutableOrderedKeyed(a) && !isImmutableOrderedSet(a) && !isImmutableRecord(a)) {
|
|
442
|
+
if (!equals(Object.entries(a), Object.entries(b), filteredCustomTesters)) return false;
|
|
443
|
+
}
|
|
444
|
+
// Remove the first value from the stack of traversed values.
|
|
445
|
+
aStack.pop();
|
|
446
|
+
bStack.pop();
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Checks if `hasOwnProperty(object, key)` up the prototype chain, stopping at `Object.prototype`.
|
|
451
|
+
*/
|
|
452
|
+
function hasPropertyInObject(object, key) {
|
|
453
|
+
if (!object || typeof object !== "object" || object === Object.prototype) return false;
|
|
454
|
+
return Object.hasOwn(object, key) || hasPropertyInObject(Object.getPrototypeOf(object), key);
|
|
455
|
+
}
|
|
456
|
+
function isObjectWithKeys(a) {
|
|
457
|
+
return isObject(a) && !isError(a) && !Array.isArray(a) && !(a instanceof Date) && !(a instanceof Set) && !(a instanceof Map);
|
|
458
|
+
}
|
|
459
|
+
function subsetEquality(object, subset, customTesters = []) {
|
|
460
|
+
const filteredCustomTesters = customTesters.filter((t) => t !== subsetEquality);
|
|
461
|
+
// subsetEquality needs to keep track of the references
|
|
462
|
+
// it has already visited to avoid infinite loops in case
|
|
463
|
+
// there are circular references in the subset passed to it.
|
|
464
|
+
const subsetEqualityWithContext = (seenReferences = /* @__PURE__ */ new WeakMap()) => (object, subset) => {
|
|
465
|
+
if (!isObjectWithKeys(subset)) return;
|
|
466
|
+
return Object.keys(subset).every((key) => {
|
|
467
|
+
if (subset[key] != null && typeof subset[key] === "object") {
|
|
468
|
+
if (seenReferences.has(subset[key])) return equals(object[key], subset[key], filteredCustomTesters);
|
|
469
|
+
seenReferences.set(subset[key], true);
|
|
470
|
+
}
|
|
471
|
+
const result = object != null && hasPropertyInObject(object, key) && equals(object[key], subset[key], [...filteredCustomTesters, subsetEqualityWithContext(seenReferences)]);
|
|
472
|
+
// The main goal of using seenReference is to avoid circular node on tree.
|
|
473
|
+
// It will only happen within a parent and its child, not a node and nodes next to it (same level)
|
|
474
|
+
// We should keep the reference for a parent and its child only
|
|
475
|
+
// Thus we should delete the reference immediately so that it doesn't interfere
|
|
476
|
+
// other nodes within the same level on tree.
|
|
477
|
+
seenReferences.delete(subset[key]);
|
|
478
|
+
return result;
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
return subsetEqualityWithContext()(object, subset);
|
|
482
|
+
}
|
|
483
|
+
function typeEquality(a, b) {
|
|
484
|
+
if (a == null || b == null || a.constructor === b.constructor) return;
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
function arrayBufferEquality(a, b) {
|
|
488
|
+
let dataViewA = a;
|
|
489
|
+
let dataViewB = b;
|
|
490
|
+
if (!(a instanceof DataView && b instanceof DataView)) {
|
|
491
|
+
if (!(a instanceof ArrayBuffer) || !(b instanceof ArrayBuffer)) return;
|
|
492
|
+
try {
|
|
493
|
+
dataViewA = new DataView(a);
|
|
494
|
+
dataViewB = new DataView(b);
|
|
495
|
+
} catch {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Buffers are not equal when they do not have the same byte length
|
|
500
|
+
if (dataViewA.byteLength !== dataViewB.byteLength) return false;
|
|
501
|
+
// Check if every byte value is equal to each other
|
|
502
|
+
for (let i = 0; i < dataViewA.byteLength; i++) if (dataViewA.getUint8(i) !== dataViewB.getUint8(i)) return false;
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
function sparseArrayEquality(a, b, customTesters = []) {
|
|
506
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return;
|
|
507
|
+
// A sparse array [, , 1] will have keys ["2"] whereas [undefined, undefined, 1] will have keys ["0", "1", "2"]
|
|
508
|
+
const aKeys = Object.keys(a);
|
|
509
|
+
const bKeys = Object.keys(b);
|
|
510
|
+
return equals(a, b, customTesters.filter((t) => t !== sparseArrayEquality), true) && equals(aKeys, bKeys);
|
|
511
|
+
}
|
|
512
|
+
function generateToBeMessage(deepEqualityName, expected = "#{this}", actual = "#{exp}") {
|
|
513
|
+
const toBeMessage = `expected ${expected} to be ${actual} // Object.is equality`;
|
|
514
|
+
if (["toStrictEqual", "toEqual"].includes(deepEqualityName)) return `${toBeMessage}\n\nIf it should pass with deep equality, replace "toBe" with "${deepEqualityName}"\n\nExpected: ${expected}\nReceived: serializes to the same string\n`;
|
|
515
|
+
return toBeMessage;
|
|
516
|
+
}
|
|
517
|
+
function pluralize(word, count) {
|
|
518
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
519
|
+
}
|
|
520
|
+
function getObjectKeys(object) {
|
|
521
|
+
return [...Object.keys(object), ...Object.getOwnPropertySymbols(object).filter((s) => Object.getOwnPropertyDescriptor(object, s)?.enumerable)];
|
|
522
|
+
}
|
|
523
|
+
function getObjectSubset(object, subset, customTesters) {
|
|
524
|
+
let stripped = 0;
|
|
525
|
+
const getObjectSubsetWithContext = (seenReferences = /* @__PURE__ */ new WeakMap()) => (object, subset) => {
|
|
526
|
+
if (Array.isArray(object)) {
|
|
527
|
+
if (Array.isArray(subset) && subset.length === object.length)
|
|
528
|
+
// The map method returns correct subclass of subset.
|
|
529
|
+
return subset.map((sub, i) => getObjectSubsetWithContext(seenReferences)(object[i], sub));
|
|
530
|
+
} else if (object instanceof Date) return object;
|
|
531
|
+
else if (isObject(object) && isObject(subset)) {
|
|
532
|
+
if (equals(object, subset, [
|
|
533
|
+
...customTesters,
|
|
534
|
+
iterableEquality,
|
|
535
|
+
subsetEquality
|
|
536
|
+
]))
|
|
537
|
+
// return "expected" subset to avoid showing irrelevant toMatchObject diff
|
|
538
|
+
return subset;
|
|
539
|
+
const trimmed = {};
|
|
540
|
+
seenReferences.set(object, trimmed);
|
|
541
|
+
// preserve constructor for toMatchObject diff
|
|
542
|
+
if (typeof object.constructor === "function" && typeof object.constructor.name === "string") Object.defineProperty(trimmed, "constructor", {
|
|
543
|
+
enumerable: false,
|
|
544
|
+
value: object.constructor
|
|
545
|
+
});
|
|
546
|
+
for (const key of getObjectKeys(object)) if (hasPropertyInObject(subset, key)) trimmed[key] = seenReferences.has(object[key]) ? seenReferences.get(object[key]) : getObjectSubsetWithContext(seenReferences)(object[key], subset[key]);
|
|
547
|
+
else if (!seenReferences.has(object[key])) {
|
|
548
|
+
stripped += 1;
|
|
549
|
+
if (isObject(object[key])) stripped += getObjectKeys(object[key]).length;
|
|
550
|
+
getObjectSubsetWithContext(seenReferences)(object[key], subset[key]);
|
|
551
|
+
}
|
|
552
|
+
if (getObjectKeys(trimmed).length > 0) return trimmed;
|
|
553
|
+
}
|
|
554
|
+
return object;
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
subset: getObjectSubsetWithContext()(object, subset),
|
|
558
|
+
stripped
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Detects if an object is a Standard Schema V1 compatible schema
|
|
563
|
+
*/
|
|
564
|
+
function isStandardSchema(obj) {
|
|
565
|
+
return !!obj && (typeof obj === "object" || typeof obj === "function") && obj["~standard"] && typeof obj["~standard"].validate === "function";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!Object.hasOwn(globalThis, MATCHERS_OBJECT)) {
|
|
569
|
+
const globalState = /* @__PURE__ */ new WeakMap();
|
|
570
|
+
const matchers = Object.create(null);
|
|
571
|
+
const customEqualityTesters = [];
|
|
572
|
+
const asymmetricMatchers = Object.create(null);
|
|
573
|
+
Object.defineProperty(globalThis, MATCHERS_OBJECT, { get: () => globalState });
|
|
574
|
+
Object.defineProperty(globalThis, JEST_MATCHERS_OBJECT, {
|
|
575
|
+
configurable: true,
|
|
576
|
+
get: () => ({
|
|
577
|
+
state: globalState.get(globalThis[GLOBAL_EXPECT]),
|
|
578
|
+
matchers,
|
|
579
|
+
customEqualityTesters
|
|
580
|
+
})
|
|
581
|
+
});
|
|
582
|
+
Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, { get: () => asymmetricMatchers });
|
|
583
|
+
}
|
|
584
|
+
function getState(expect) {
|
|
585
|
+
return globalThis[MATCHERS_OBJECT].get(expect);
|
|
586
|
+
}
|
|
587
|
+
function setState(state, expect) {
|
|
588
|
+
const map = globalThis[MATCHERS_OBJECT];
|
|
589
|
+
const current = map.get(expect) || {};
|
|
590
|
+
// so it keeps getters from `testPath`
|
|
591
|
+
const results = Object.defineProperties(current, {
|
|
592
|
+
...Object.getOwnPropertyDescriptors(current),
|
|
593
|
+
...Object.getOwnPropertyDescriptors(state)
|
|
594
|
+
});
|
|
595
|
+
map.set(expect, results);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
class AsymmetricMatcher {
|
|
599
|
+
// should have "jest" to be compatible with its ecosystem
|
|
600
|
+
$$typeof = Symbol.for("jest.asymmetricMatcher");
|
|
601
|
+
constructor(sample, inverse = false) {
|
|
602
|
+
this.sample = sample;
|
|
603
|
+
this.inverse = inverse;
|
|
604
|
+
}
|
|
605
|
+
getMatcherContext(expect) {
|
|
606
|
+
return {
|
|
607
|
+
...getState(expect || globalThis[GLOBAL_EXPECT]),
|
|
608
|
+
equals,
|
|
609
|
+
isNot: this.inverse,
|
|
610
|
+
customTesters: getCustomEqualityTesters(),
|
|
611
|
+
utils: {
|
|
612
|
+
...getMatcherUtils(),
|
|
613
|
+
diff,
|
|
614
|
+
stringify,
|
|
615
|
+
iterableEquality,
|
|
616
|
+
subsetEquality
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// implement custom chai/loupe inspect for better AssertionError.message formatting
|
|
622
|
+
// https://github.com/chaijs/loupe/blob/9b8a6deabcd50adc056a64fb705896194710c5c6/src/index.ts#L29
|
|
623
|
+
// @ts-expect-error computed properties is not supported when isolatedDeclarations is enabled
|
|
624
|
+
// FIXME: https://github.com/microsoft/TypeScript/issues/61068
|
|
625
|
+
AsymmetricMatcher.prototype[Symbol.for("chai/inspect")] = function(options) {
|
|
626
|
+
// minimal pretty-format with simple manual truncation
|
|
627
|
+
const result = stringify(this, options.depth, { min: true });
|
|
628
|
+
if (result.length <= options.truncate) return result;
|
|
629
|
+
return `${this.toString()}{…}`;
|
|
630
|
+
};
|
|
631
|
+
class StringContaining extends AsymmetricMatcher {
|
|
632
|
+
constructor(sample, inverse = false) {
|
|
633
|
+
if (!isA("String", sample)) throw new Error("Expected is not a string");
|
|
634
|
+
super(sample, inverse);
|
|
635
|
+
}
|
|
636
|
+
asymmetricMatch(other) {
|
|
637
|
+
const result = isA("String", other) && other.includes(this.sample);
|
|
638
|
+
return this.inverse ? !result : result;
|
|
639
|
+
}
|
|
640
|
+
toString() {
|
|
641
|
+
return `String${this.inverse ? "Not" : ""}Containing`;
|
|
642
|
+
}
|
|
643
|
+
getExpectedType() {
|
|
644
|
+
return "string";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
class Anything extends AsymmetricMatcher {
|
|
648
|
+
asymmetricMatch(other) {
|
|
649
|
+
return other != null;
|
|
650
|
+
}
|
|
651
|
+
toString() {
|
|
652
|
+
return "Anything";
|
|
653
|
+
}
|
|
654
|
+
toAsymmetricMatcher() {
|
|
655
|
+
return "Anything";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
class ObjectContaining extends AsymmetricMatcher {
|
|
659
|
+
constructor(sample, inverse = false) {
|
|
660
|
+
super(sample, inverse);
|
|
661
|
+
}
|
|
662
|
+
getPrototype(obj) {
|
|
663
|
+
if (Object.getPrototypeOf) return Object.getPrototypeOf(obj);
|
|
664
|
+
if (obj.constructor.prototype === obj) return null;
|
|
665
|
+
return obj.constructor.prototype;
|
|
666
|
+
}
|
|
667
|
+
hasProperty(obj, property) {
|
|
668
|
+
if (!obj) return false;
|
|
669
|
+
if (Object.hasOwn(obj, property)) return true;
|
|
670
|
+
return this.hasProperty(this.getPrototype(obj), property);
|
|
671
|
+
}
|
|
672
|
+
getProperties(obj) {
|
|
673
|
+
return [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj).filter((s) => Object.getOwnPropertyDescriptor(obj, s)?.enumerable)];
|
|
674
|
+
}
|
|
675
|
+
asymmetricMatch(other, customTesters) {
|
|
676
|
+
if (typeof this.sample !== "object") throw new TypeError(`You must provide an object to ${this.toString()}, not '${typeof this.sample}'.`);
|
|
677
|
+
let result = true;
|
|
678
|
+
const properties = this.getProperties(this.sample);
|
|
679
|
+
for (const property of properties) {
|
|
680
|
+
if (!this.hasProperty(other, property)) {
|
|
681
|
+
result = false;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
const value = this.sample[property];
|
|
685
|
+
const otherValue = other[property];
|
|
686
|
+
if (!equals(value, otherValue, customTesters)) {
|
|
687
|
+
result = false;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return this.inverse ? !result : result;
|
|
692
|
+
}
|
|
693
|
+
toString() {
|
|
694
|
+
return `Object${this.inverse ? "Not" : ""}Containing`;
|
|
695
|
+
}
|
|
696
|
+
getExpectedType() {
|
|
697
|
+
return "object";
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
class ArrayContaining extends AsymmetricMatcher {
|
|
701
|
+
constructor(sample, inverse = false) {
|
|
702
|
+
super(sample, inverse);
|
|
703
|
+
}
|
|
704
|
+
asymmetricMatch(other, customTesters) {
|
|
705
|
+
if (!Array.isArray(this.sample)) throw new TypeError(`You must provide an array to ${this.toString()}, not '${typeof this.sample}'.`);
|
|
706
|
+
const result = this.sample.length === 0 || Array.isArray(other) && this.sample.every((item) => other.some((another) => equals(item, another, customTesters)));
|
|
707
|
+
return this.inverse ? !result : result;
|
|
708
|
+
}
|
|
709
|
+
toString() {
|
|
710
|
+
return `Array${this.inverse ? "Not" : ""}Containing`;
|
|
711
|
+
}
|
|
712
|
+
getExpectedType() {
|
|
713
|
+
return "array";
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
class Any extends AsymmetricMatcher {
|
|
717
|
+
constructor(sample) {
|
|
718
|
+
if (typeof sample === "undefined") throw new TypeError("any() expects to be passed a constructor function. Please pass one or use anything() to match any object.");
|
|
719
|
+
super(sample);
|
|
720
|
+
}
|
|
721
|
+
fnNameFor(func) {
|
|
722
|
+
if (func.name) return func.name;
|
|
723
|
+
const matches = Function.prototype.toString.call(func).match(/^(?:async)?\s*function\s*(?:\*\s*)?([\w$]+)\s*\(/);
|
|
724
|
+
return matches ? matches[1] : "<anonymous>";
|
|
725
|
+
}
|
|
726
|
+
asymmetricMatch(other) {
|
|
727
|
+
if (this.sample === String) return typeof other == "string" || other instanceof String;
|
|
728
|
+
if (this.sample === Number) return typeof other == "number" || other instanceof Number;
|
|
729
|
+
if (this.sample === Function) return typeof other == "function" || typeof other === "function";
|
|
730
|
+
if (this.sample === Boolean) return typeof other == "boolean" || other instanceof Boolean;
|
|
731
|
+
if (this.sample === BigInt) return typeof other == "bigint" || other instanceof BigInt;
|
|
732
|
+
if (this.sample === Symbol) return typeof other == "symbol" || other instanceof Symbol;
|
|
733
|
+
if (this.sample === Object) return typeof other == "object";
|
|
734
|
+
return other instanceof this.sample;
|
|
735
|
+
}
|
|
736
|
+
toString() {
|
|
737
|
+
return "Any";
|
|
738
|
+
}
|
|
739
|
+
getExpectedType() {
|
|
740
|
+
if (this.sample === String) return "string";
|
|
741
|
+
if (this.sample === Number) return "number";
|
|
742
|
+
if (this.sample === Function) return "function";
|
|
743
|
+
if (this.sample === Object) return "object";
|
|
744
|
+
if (this.sample === Boolean) return "boolean";
|
|
745
|
+
return this.fnNameFor(this.sample);
|
|
746
|
+
}
|
|
747
|
+
toAsymmetricMatcher() {
|
|
748
|
+
return `Any<${this.fnNameFor(this.sample)}>`;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
class StringMatching extends AsymmetricMatcher {
|
|
752
|
+
constructor(sample, inverse = false) {
|
|
753
|
+
if (!isA("String", sample) && !isA("RegExp", sample)) throw new Error("Expected is not a String or a RegExp");
|
|
754
|
+
super(new RegExp(sample), inverse);
|
|
755
|
+
}
|
|
756
|
+
asymmetricMatch(other) {
|
|
757
|
+
const result = isA("String", other) && this.sample.test(other);
|
|
758
|
+
return this.inverse ? !result : result;
|
|
759
|
+
}
|
|
760
|
+
toString() {
|
|
761
|
+
return `String${this.inverse ? "Not" : ""}Matching`;
|
|
762
|
+
}
|
|
763
|
+
getExpectedType() {
|
|
764
|
+
return "string";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
class CloseTo extends AsymmetricMatcher {
|
|
768
|
+
precision;
|
|
769
|
+
constructor(sample, precision = 2, inverse = false) {
|
|
770
|
+
if (!isA("Number", sample)) throw new Error("Expected is not a Number");
|
|
771
|
+
if (!isA("Number", precision)) throw new Error("Precision is not a Number");
|
|
772
|
+
super(sample);
|
|
773
|
+
this.inverse = inverse;
|
|
774
|
+
this.precision = precision;
|
|
775
|
+
}
|
|
776
|
+
asymmetricMatch(other) {
|
|
777
|
+
if (!isA("Number", other)) return false;
|
|
778
|
+
let result = false;
|
|
779
|
+
if (other === Number.POSITIVE_INFINITY && this.sample === Number.POSITIVE_INFINITY) result = true;
|
|
780
|
+
else if (other === Number.NEGATIVE_INFINITY && this.sample === Number.NEGATIVE_INFINITY) result = true;
|
|
781
|
+
else result = Math.abs(this.sample - other) < 10 ** -this.precision / 2;
|
|
782
|
+
return this.inverse ? !result : result;
|
|
783
|
+
}
|
|
784
|
+
toString() {
|
|
785
|
+
return `Number${this.inverse ? "Not" : ""}CloseTo`;
|
|
786
|
+
}
|
|
787
|
+
getExpectedType() {
|
|
788
|
+
return "number";
|
|
789
|
+
}
|
|
790
|
+
toAsymmetricMatcher() {
|
|
791
|
+
return [
|
|
792
|
+
this.toString(),
|
|
793
|
+
this.sample,
|
|
794
|
+
`(${pluralize("digit", this.precision)})`
|
|
795
|
+
].join(" ");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
class SchemaMatching extends AsymmetricMatcher {
|
|
799
|
+
result;
|
|
800
|
+
constructor(sample, inverse = false) {
|
|
801
|
+
if (!isStandardSchema(sample)) throw new TypeError("SchemaMatching expected to receive a Standard Schema.");
|
|
802
|
+
super(sample, inverse);
|
|
803
|
+
}
|
|
804
|
+
asymmetricMatch(other) {
|
|
805
|
+
const result = this.sample["~standard"].validate(other);
|
|
806
|
+
// Check if the result is a Promise (async validation)
|
|
807
|
+
if (result instanceof Promise) throw new TypeError("Async schema validation is not supported in asymmetric matchers.");
|
|
808
|
+
this.result = result;
|
|
809
|
+
const pass = !this.result.issues || this.result.issues.length === 0;
|
|
810
|
+
return this.inverse ? !pass : pass;
|
|
811
|
+
}
|
|
812
|
+
toString() {
|
|
813
|
+
return `Schema${this.inverse ? "Not" : ""}Matching`;
|
|
814
|
+
}
|
|
815
|
+
getExpectedType() {
|
|
816
|
+
return "object";
|
|
817
|
+
}
|
|
818
|
+
toAsymmetricMatcher() {
|
|
819
|
+
const { utils } = this.getMatcherContext();
|
|
820
|
+
if ((this.result?.issues || []).length > 0) return `${this.toString()} ${utils.stringify(this.result, void 0, { printBasicPrototype: false })}`;
|
|
821
|
+
return this.toString();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const JestAsymmetricMatchers = (chai, utils) => {
|
|
825
|
+
utils.addMethod(chai.expect, "anything", () => new Anything());
|
|
826
|
+
utils.addMethod(chai.expect, "any", (expected) => new Any(expected));
|
|
827
|
+
utils.addMethod(chai.expect, "stringContaining", (expected) => new StringContaining(expected));
|
|
828
|
+
utils.addMethod(chai.expect, "objectContaining", (expected) => new ObjectContaining(expected));
|
|
829
|
+
utils.addMethod(chai.expect, "arrayContaining", (expected) => new ArrayContaining(expected));
|
|
830
|
+
utils.addMethod(chai.expect, "stringMatching", (expected) => new StringMatching(expected));
|
|
831
|
+
utils.addMethod(chai.expect, "closeTo", (expected, precision) => new CloseTo(expected, precision));
|
|
832
|
+
utils.addMethod(chai.expect, "schemaMatching", (expected) => new SchemaMatching(expected));
|
|
833
|
+
// defineProperty does not work
|
|
834
|
+
chai.expect.not = {
|
|
835
|
+
stringContaining: (expected) => new StringContaining(expected, true),
|
|
836
|
+
objectContaining: (expected) => new ObjectContaining(expected, true),
|
|
837
|
+
arrayContaining: (expected) => new ArrayContaining(expected, true),
|
|
838
|
+
stringMatching: (expected) => new StringMatching(expected, true),
|
|
839
|
+
closeTo: (expected, precision) => new CloseTo(expected, precision, true),
|
|
840
|
+
schemaMatching: (expected) => new SchemaMatching(expected, true)
|
|
841
|
+
};
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
function createAssertionMessage(util, assertion, hasArgs) {
|
|
845
|
+
const soft = util.flag(assertion, "soft") ? ".soft" : "";
|
|
846
|
+
const not = util.flag(assertion, "negate") ? "not." : "";
|
|
847
|
+
const name = `${util.flag(assertion, "_name")}(${hasArgs ? "expected" : ""})`;
|
|
848
|
+
const promiseName = util.flag(assertion, "promise");
|
|
849
|
+
return `expect${soft}(actual)${promiseName ? `.${promiseName}` : ""}.${not}${name}`;
|
|
850
|
+
}
|
|
851
|
+
function recordAsyncExpect(_test, promise, assertion, error, isSoft) {
|
|
852
|
+
const test = _test;
|
|
853
|
+
// record promise for test, that resolves before test ends
|
|
854
|
+
if (test && promise instanceof Promise) {
|
|
855
|
+
// if promise is explicitly awaited, remove it from the list
|
|
856
|
+
promise = promise.finally(() => {
|
|
857
|
+
if (!test.promises) return;
|
|
858
|
+
const index = test.promises.indexOf(promise);
|
|
859
|
+
if (index !== -1) test.promises.splice(index, 1);
|
|
860
|
+
});
|
|
861
|
+
// record promise
|
|
862
|
+
if (!test.promises) test.promises = [];
|
|
863
|
+
// setup `expect.soft` handler here instead of `wrapAssertion`
|
|
864
|
+
// to avoid double error tracking while keeping non-await promise detection.
|
|
865
|
+
if (isSoft) promise = promise.then(noop, (err) => {
|
|
866
|
+
handleTestError(test, err);
|
|
867
|
+
});
|
|
868
|
+
test.promises.push(promise);
|
|
869
|
+
let resolved = false;
|
|
870
|
+
test.onFinished ??= [];
|
|
871
|
+
test.onFinished.push(() => {
|
|
872
|
+
if (!resolved) {
|
|
873
|
+
const stack = (globalThis.__vitest_worker__?.onFilterStackTrace || ((s) => s || ""))(error.stack);
|
|
874
|
+
console.warn([
|
|
875
|
+
`Promise returned by \`${assertion}\` was not awaited. `,
|
|
876
|
+
"Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. ",
|
|
877
|
+
"Please remember to await the assertion.\n",
|
|
878
|
+
stack
|
|
879
|
+
].join(""));
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
return {
|
|
883
|
+
then(onFulfilled, onRejected) {
|
|
884
|
+
resolved = true;
|
|
885
|
+
return promise.then(onFulfilled, onRejected);
|
|
886
|
+
},
|
|
887
|
+
catch(onRejected) {
|
|
888
|
+
resolved = true;
|
|
889
|
+
return promise.catch(onRejected);
|
|
890
|
+
},
|
|
891
|
+
finally(onFinally) {
|
|
892
|
+
resolved = true;
|
|
893
|
+
return promise.finally(onFinally);
|
|
894
|
+
},
|
|
895
|
+
[Symbol.toStringTag]: "Promise"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
return promise;
|
|
899
|
+
}
|
|
900
|
+
function handleTestError(test, err) {
|
|
901
|
+
test.result ||= { state: "fail" };
|
|
902
|
+
test.result.state = "fail";
|
|
903
|
+
test.result.errors ||= [];
|
|
904
|
+
test.result.errors.push(processError(err));
|
|
905
|
+
}
|
|
906
|
+
/** wrap assertion function to support `expect.soft` and provide assertion name as `_name` */
|
|
907
|
+
function wrapAssertion(utils, name, fn) {
|
|
908
|
+
return function(...args) {
|
|
909
|
+
// private
|
|
910
|
+
if (name !== "withTest") utils.flag(this, "_name", name);
|
|
911
|
+
if (!utils.flag(this, "soft"))
|
|
912
|
+
// avoid WebKit's proper tail call to preserve stacktrace offset for inline snapshot
|
|
913
|
+
// https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit
|
|
914
|
+
try {
|
|
915
|
+
return fn.apply(this, args);
|
|
916
|
+
} finally {}
|
|
917
|
+
const test = utils.flag(this, "vitest-test");
|
|
918
|
+
if (!test) throw new Error("expect.soft() can only be used inside a test");
|
|
919
|
+
try {
|
|
920
|
+
const result = fn.apply(this, args);
|
|
921
|
+
if (result && typeof result === "object" && typeof result.then === "function") return result.then(noop, (err) => {
|
|
922
|
+
handleTestError(test, err);
|
|
923
|
+
});
|
|
924
|
+
return result;
|
|
925
|
+
} catch (err) {
|
|
926
|
+
handleTestError(test, err);
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Jest Expect Compact
|
|
932
|
+
const JestChaiExpect = (chai, utils) => {
|
|
933
|
+
const { AssertionError } = chai;
|
|
934
|
+
const customTesters = getCustomEqualityTesters();
|
|
935
|
+
function def(name, fn) {
|
|
936
|
+
const addMethod = (n) => {
|
|
937
|
+
const softWrapper = wrapAssertion(utils, n, fn);
|
|
938
|
+
utils.addMethod(chai.Assertion.prototype, n, softWrapper);
|
|
939
|
+
utils.addMethod(globalThis[JEST_MATCHERS_OBJECT].matchers, n, softWrapper);
|
|
940
|
+
};
|
|
941
|
+
if (Array.isArray(name)) name.forEach((n) => addMethod(n));
|
|
942
|
+
else addMethod(name);
|
|
943
|
+
}
|
|
944
|
+
[
|
|
945
|
+
"throw",
|
|
946
|
+
"throws",
|
|
947
|
+
"Throw"
|
|
948
|
+
].forEach((m) => {
|
|
949
|
+
utils.overwriteMethod(chai.Assertion.prototype, m, (_super) => {
|
|
950
|
+
return function(...args) {
|
|
951
|
+
const promise = utils.flag(this, "promise");
|
|
952
|
+
const object = utils.flag(this, "object");
|
|
953
|
+
const isNot = utils.flag(this, "negate");
|
|
954
|
+
if (promise === "rejects") utils.flag(this, "object", () => {
|
|
955
|
+
throw object;
|
|
956
|
+
});
|
|
957
|
+
else if (promise === "resolves" && typeof object !== "function") if (!isNot) throw new AssertionError(utils.flag(this, "message") || "expected promise to throw an error, but it didn't", { showDiff: false }, utils.flag(this, "ssfi"));
|
|
958
|
+
else return;
|
|
959
|
+
_super.apply(this, args);
|
|
960
|
+
};
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
// @ts-expect-error @internal
|
|
964
|
+
def("withTest", function(test) {
|
|
965
|
+
utils.flag(this, "vitest-test", test);
|
|
966
|
+
return this;
|
|
967
|
+
});
|
|
968
|
+
def("toEqual", function(expected) {
|
|
969
|
+
const actual = utils.flag(this, "object");
|
|
970
|
+
const equal = equals(actual, expected, [...customTesters, iterableEquality]);
|
|
971
|
+
return this.assert(equal, "expected #{this} to deeply equal #{exp}", "expected #{this} to not deeply equal #{exp}", expected, actual);
|
|
972
|
+
});
|
|
973
|
+
def("toStrictEqual", function(expected) {
|
|
974
|
+
const obj = utils.flag(this, "object");
|
|
975
|
+
const equal = equals(obj, expected, [
|
|
976
|
+
...customTesters,
|
|
977
|
+
iterableEquality,
|
|
978
|
+
typeEquality,
|
|
979
|
+
sparseArrayEquality,
|
|
980
|
+
arrayBufferEquality
|
|
981
|
+
], true);
|
|
982
|
+
return this.assert(equal, "expected #{this} to strictly equal #{exp}", "expected #{this} to not strictly equal #{exp}", expected, obj);
|
|
983
|
+
});
|
|
984
|
+
def("toBe", function(expected) {
|
|
985
|
+
const actual = this._obj;
|
|
986
|
+
const pass = Object.is(actual, expected);
|
|
987
|
+
let deepEqualityName = "";
|
|
988
|
+
if (!pass) {
|
|
989
|
+
if (equals(actual, expected, [
|
|
990
|
+
...customTesters,
|
|
991
|
+
iterableEquality,
|
|
992
|
+
typeEquality,
|
|
993
|
+
sparseArrayEquality,
|
|
994
|
+
arrayBufferEquality
|
|
995
|
+
], true)) deepEqualityName = "toStrictEqual";
|
|
996
|
+
else if (equals(actual, expected, [...customTesters, iterableEquality])) deepEqualityName = "toEqual";
|
|
997
|
+
}
|
|
998
|
+
return this.assert(pass, generateToBeMessage(deepEqualityName), "expected #{this} not to be #{exp} // Object.is equality", expected, actual);
|
|
999
|
+
});
|
|
1000
|
+
def("toMatchObject", function(expected) {
|
|
1001
|
+
const actual = this._obj;
|
|
1002
|
+
const pass = equals(actual, expected, [
|
|
1003
|
+
...customTesters,
|
|
1004
|
+
iterableEquality,
|
|
1005
|
+
subsetEquality
|
|
1006
|
+
]);
|
|
1007
|
+
const isNot = utils.flag(this, "negate");
|
|
1008
|
+
const { subset: actualSubset, stripped } = getObjectSubset(actual, expected, customTesters);
|
|
1009
|
+
if (pass && isNot || !pass && !isNot) {
|
|
1010
|
+
const msg = utils.getMessage(this, [
|
|
1011
|
+
pass,
|
|
1012
|
+
"expected #{this} to match object #{exp}",
|
|
1013
|
+
"expected #{this} to not match object #{exp}",
|
|
1014
|
+
expected,
|
|
1015
|
+
actualSubset,
|
|
1016
|
+
false
|
|
1017
|
+
]);
|
|
1018
|
+
throw new AssertionError(stripped === 0 ? msg : `${msg}\n(${stripped} matching ${stripped === 1 ? "property" : "properties"} omitted from actual)`, {
|
|
1019
|
+
showDiff: true,
|
|
1020
|
+
expected,
|
|
1021
|
+
actual: actualSubset
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
def("toMatch", function(expected) {
|
|
1026
|
+
const actual = this._obj;
|
|
1027
|
+
if (typeof actual !== "string") throw new TypeError(`.toMatch() expects to receive a string, but got ${typeof actual}`);
|
|
1028
|
+
return this.assert(typeof expected === "string" ? actual.includes(expected) : actual.match(expected), `expected #{this} to match #{exp}`, `expected #{this} not to match #{exp}`, expected, actual);
|
|
1029
|
+
});
|
|
1030
|
+
def("toContain", function(item) {
|
|
1031
|
+
const actual = this._obj;
|
|
1032
|
+
if (typeof Node !== "undefined" && actual instanceof Node) {
|
|
1033
|
+
if (!(item instanceof Node)) throw new TypeError(`toContain() expected a DOM node as the argument, but got ${typeof item}`);
|
|
1034
|
+
return this.assert(actual.contains(item), "expected #{this} to contain element #{exp}", "expected #{this} not to contain element #{exp}", item, actual);
|
|
1035
|
+
}
|
|
1036
|
+
if (typeof DOMTokenList !== "undefined" && actual instanceof DOMTokenList) {
|
|
1037
|
+
assertTypes(item, "class name", ["string"]);
|
|
1038
|
+
const expectedClassList = utils.flag(this, "negate") ? actual.value.replace(item, "").trim() : `${actual.value} ${item}`;
|
|
1039
|
+
return this.assert(actual.contains(item), `expected "${actual.value}" to contain "${item}"`, `expected "${actual.value}" not to contain "${item}"`, expectedClassList, actual.value);
|
|
1040
|
+
}
|
|
1041
|
+
// handle simple case on our own using `this.assert` to include diff in error message
|
|
1042
|
+
if (typeof actual === "string" && typeof item === "string") return this.assert(actual.includes(item), `expected #{this} to contain #{exp}`, `expected #{this} not to contain #{exp}`, item, actual);
|
|
1043
|
+
// make "actual" indexable to have compatibility with jest
|
|
1044
|
+
if (actual != null && typeof actual !== "string") utils.flag(this, "object", Array.from(actual));
|
|
1045
|
+
return this.contain(item);
|
|
1046
|
+
});
|
|
1047
|
+
def("toContainEqual", function(expected) {
|
|
1048
|
+
const obj = utils.flag(this, "object");
|
|
1049
|
+
const index = Array.from(obj).findIndex((item) => {
|
|
1050
|
+
return equals(item, expected, customTesters);
|
|
1051
|
+
});
|
|
1052
|
+
this.assert(index !== -1, "expected #{this} to deep equally contain #{exp}", "expected #{this} to not deep equally contain #{exp}", expected);
|
|
1053
|
+
});
|
|
1054
|
+
def("toBeTruthy", function() {
|
|
1055
|
+
const obj = utils.flag(this, "object");
|
|
1056
|
+
this.assert(Boolean(obj), "expected #{this} to be truthy", "expected #{this} to not be truthy", true, obj);
|
|
1057
|
+
});
|
|
1058
|
+
def("toBeFalsy", function() {
|
|
1059
|
+
const obj = utils.flag(this, "object");
|
|
1060
|
+
this.assert(!obj, "expected #{this} to be falsy", "expected #{this} to not be falsy", false, obj);
|
|
1061
|
+
});
|
|
1062
|
+
def("toBeGreaterThan", function(expected) {
|
|
1063
|
+
const actual = this._obj;
|
|
1064
|
+
assertTypes(actual, "actual", ["number", "bigint"]);
|
|
1065
|
+
assertTypes(expected, "expected", ["number", "bigint"]);
|
|
1066
|
+
return this.assert(actual > expected, `expected ${actual} to be greater than ${expected}`, `expected ${actual} to be not greater than ${expected}`, expected, actual, false);
|
|
1067
|
+
});
|
|
1068
|
+
def("toBeGreaterThanOrEqual", function(expected) {
|
|
1069
|
+
const actual = this._obj;
|
|
1070
|
+
assertTypes(actual, "actual", ["number", "bigint"]);
|
|
1071
|
+
assertTypes(expected, "expected", ["number", "bigint"]);
|
|
1072
|
+
return this.assert(actual >= expected, `expected ${actual} to be greater than or equal to ${expected}`, `expected ${actual} to be not greater than or equal to ${expected}`, expected, actual, false);
|
|
1073
|
+
});
|
|
1074
|
+
def("toBeLessThan", function(expected) {
|
|
1075
|
+
const actual = this._obj;
|
|
1076
|
+
assertTypes(actual, "actual", ["number", "bigint"]);
|
|
1077
|
+
assertTypes(expected, "expected", ["number", "bigint"]);
|
|
1078
|
+
return this.assert(actual < expected, `expected ${actual} to be less than ${expected}`, `expected ${actual} to be not less than ${expected}`, expected, actual, false);
|
|
1079
|
+
});
|
|
1080
|
+
def("toBeLessThanOrEqual", function(expected) {
|
|
1081
|
+
const actual = this._obj;
|
|
1082
|
+
assertTypes(actual, "actual", ["number", "bigint"]);
|
|
1083
|
+
assertTypes(expected, "expected", ["number", "bigint"]);
|
|
1084
|
+
return this.assert(actual <= expected, `expected ${actual} to be less than or equal to ${expected}`, `expected ${actual} to be not less than or equal to ${expected}`, expected, actual, false);
|
|
1085
|
+
});
|
|
1086
|
+
def("toBeNaN", function() {
|
|
1087
|
+
const obj = utils.flag(this, "object");
|
|
1088
|
+
this.assert(Number.isNaN(obj), "expected #{this} to be NaN", "expected #{this} not to be NaN", NaN, obj);
|
|
1089
|
+
});
|
|
1090
|
+
def("toBeUndefined", function() {
|
|
1091
|
+
const obj = utils.flag(this, "object");
|
|
1092
|
+
this.assert(void 0 === obj, "expected #{this} to be undefined", "expected #{this} not to be undefined", void 0, obj);
|
|
1093
|
+
});
|
|
1094
|
+
def("toBeNull", function() {
|
|
1095
|
+
const obj = utils.flag(this, "object");
|
|
1096
|
+
this.assert(obj === null, "expected #{this} to be null", "expected #{this} not to be null", null, obj);
|
|
1097
|
+
});
|
|
1098
|
+
def("toBeNullable", function() {
|
|
1099
|
+
const obj = utils.flag(this, "object");
|
|
1100
|
+
this.assert(obj == null, "expected #{this} to be nullish", "expected #{this} not to be nullish", null, obj);
|
|
1101
|
+
});
|
|
1102
|
+
def("toBeDefined", function() {
|
|
1103
|
+
const obj = utils.flag(this, "object");
|
|
1104
|
+
this.assert(typeof obj !== "undefined", "expected #{this} to be defined", "expected #{this} to be undefined", obj);
|
|
1105
|
+
});
|
|
1106
|
+
def("toBeTypeOf", function(expected) {
|
|
1107
|
+
const actual = typeof this._obj;
|
|
1108
|
+
const equal = expected === actual;
|
|
1109
|
+
return this.assert(equal, "expected #{this} to be type of #{exp}", "expected #{this} not to be type of #{exp}", expected, actual);
|
|
1110
|
+
});
|
|
1111
|
+
def("toBeInstanceOf", function(obj) {
|
|
1112
|
+
return this.instanceOf(obj);
|
|
1113
|
+
});
|
|
1114
|
+
def("toHaveLength", function(length) {
|
|
1115
|
+
return this.have.length(length);
|
|
1116
|
+
});
|
|
1117
|
+
// destructuring, because it checks `arguments` inside, and value is passing as `undefined`
|
|
1118
|
+
def("toHaveProperty", function(...args) {
|
|
1119
|
+
if (Array.isArray(args[0])) args[0] = args[0].map((key) => String(key).replace(/([.[\]])/g, "\\$1")).join(".");
|
|
1120
|
+
const actual = this._obj;
|
|
1121
|
+
const [propertyName, expected] = args;
|
|
1122
|
+
const getValue = () => {
|
|
1123
|
+
if (Object.hasOwn(actual, propertyName)) return {
|
|
1124
|
+
value: actual[propertyName],
|
|
1125
|
+
exists: true
|
|
1126
|
+
};
|
|
1127
|
+
return utils.getPathInfo(actual, propertyName);
|
|
1128
|
+
};
|
|
1129
|
+
const { value, exists } = getValue();
|
|
1130
|
+
const pass = exists && (args.length === 1 || equals(expected, value, customTesters));
|
|
1131
|
+
const valueString = args.length === 1 ? "" : ` with value ${inspect(expected, { truncate: 40 })}`;
|
|
1132
|
+
return this.assert(pass, `expected #{this} to have property "${propertyName}"${valueString}`, `expected #{this} to not have property "${propertyName}"${valueString}`, expected, exists ? value : void 0);
|
|
1133
|
+
});
|
|
1134
|
+
def("toBeCloseTo", function(received, precision = 2) {
|
|
1135
|
+
const expected = this._obj;
|
|
1136
|
+
let pass = false;
|
|
1137
|
+
let expectedDiff = 0;
|
|
1138
|
+
let receivedDiff = 0;
|
|
1139
|
+
if (received === Number.POSITIVE_INFINITY && expected === Number.POSITIVE_INFINITY) pass = true;
|
|
1140
|
+
else if (received === Number.NEGATIVE_INFINITY && expected === Number.NEGATIVE_INFINITY) pass = true;
|
|
1141
|
+
else {
|
|
1142
|
+
expectedDiff = 10 ** -precision / 2;
|
|
1143
|
+
receivedDiff = Math.abs(expected - received);
|
|
1144
|
+
pass = receivedDiff < expectedDiff;
|
|
1145
|
+
}
|
|
1146
|
+
return this.assert(pass, `expected #{this} to be close to #{exp}, received difference is ${receivedDiff}, but expected ${expectedDiff}`, `expected #{this} to not be close to #{exp}, received difference is ${receivedDiff}, but expected ${expectedDiff}`, received, expected, false);
|
|
1147
|
+
});
|
|
1148
|
+
function assertIsMock(assertion) {
|
|
1149
|
+
if (!isMockFunction(assertion._obj)) throw new TypeError(`${utils.inspect(assertion._obj)} is not a spy or a call to a spy!`);
|
|
1150
|
+
}
|
|
1151
|
+
function getSpy(assertion) {
|
|
1152
|
+
assertIsMock(assertion);
|
|
1153
|
+
return assertion._obj;
|
|
1154
|
+
}
|
|
1155
|
+
def(["toHaveBeenCalledTimes", "toBeCalledTimes"], function(number) {
|
|
1156
|
+
const spy = getSpy(this);
|
|
1157
|
+
const spyName = spy.getMockName();
|
|
1158
|
+
const callCount = spy.mock.calls.length;
|
|
1159
|
+
return this.assert(callCount === number, `expected "${spyName}" to be called #{exp} times, but got ${callCount} times`, `expected "${spyName}" to not be called #{exp} times`, number, callCount, false);
|
|
1160
|
+
});
|
|
1161
|
+
def("toHaveBeenCalledOnce", function() {
|
|
1162
|
+
const spy = getSpy(this);
|
|
1163
|
+
const spyName = spy.getMockName();
|
|
1164
|
+
const callCount = spy.mock.calls.length;
|
|
1165
|
+
return this.assert(callCount === 1, `expected "${spyName}" to be called once, but got ${callCount} times`, `expected "${spyName}" to not be called once`, 1, callCount, false);
|
|
1166
|
+
});
|
|
1167
|
+
def(["toHaveBeenCalled", "toBeCalled"], function() {
|
|
1168
|
+
const spy = getSpy(this);
|
|
1169
|
+
const spyName = spy.getMockName();
|
|
1170
|
+
const callCount = spy.mock.calls.length;
|
|
1171
|
+
const called = callCount > 0;
|
|
1172
|
+
const isNot = utils.flag(this, "negate");
|
|
1173
|
+
let msg = utils.getMessage(this, [
|
|
1174
|
+
called,
|
|
1175
|
+
`expected "${spyName}" to be called at least once`,
|
|
1176
|
+
`expected "${spyName}" to not be called at all, but actually been called ${callCount} times`,
|
|
1177
|
+
true,
|
|
1178
|
+
called
|
|
1179
|
+
]);
|
|
1180
|
+
if (called && isNot) msg = formatCalls(spy, msg);
|
|
1181
|
+
if (called && isNot || !called && !isNot) throw new AssertionError(msg);
|
|
1182
|
+
});
|
|
1183
|
+
// manually compare array elements since `jestEquals` cannot
|
|
1184
|
+
// apply asymmetric matcher to `undefined` array element.
|
|
1185
|
+
function equalsArgumentArray(a, b) {
|
|
1186
|
+
return a.length === b.length && a.every((aItem, i) => equals(aItem, b[i], [...customTesters, iterableEquality]));
|
|
1187
|
+
}
|
|
1188
|
+
def(["toHaveBeenCalledWith", "toBeCalledWith"], function(...args) {
|
|
1189
|
+
const spy = getSpy(this);
|
|
1190
|
+
const spyName = spy.getMockName();
|
|
1191
|
+
const pass = spy.mock.calls.some((callArg) => equalsArgumentArray(callArg, args));
|
|
1192
|
+
const isNot = utils.flag(this, "negate");
|
|
1193
|
+
const msg = utils.getMessage(this, [
|
|
1194
|
+
pass,
|
|
1195
|
+
`expected "${spyName}" to be called with arguments: #{exp}`,
|
|
1196
|
+
`expected "${spyName}" to not be called with arguments: #{exp}`,
|
|
1197
|
+
args
|
|
1198
|
+
]);
|
|
1199
|
+
if (pass && isNot || !pass && !isNot) throw new AssertionError(formatCalls(spy, msg, args));
|
|
1200
|
+
});
|
|
1201
|
+
def("toHaveBeenCalledExactlyOnceWith", function(...args) {
|
|
1202
|
+
const spy = getSpy(this);
|
|
1203
|
+
const spyName = spy.getMockName();
|
|
1204
|
+
const callCount = spy.mock.calls.length;
|
|
1205
|
+
const pass = spy.mock.calls.some((callArg) => equalsArgumentArray(callArg, args)) && callCount === 1;
|
|
1206
|
+
const isNot = utils.flag(this, "negate");
|
|
1207
|
+
const msg = utils.getMessage(this, [
|
|
1208
|
+
pass,
|
|
1209
|
+
`expected "${spyName}" to be called once with arguments: #{exp}`,
|
|
1210
|
+
`expected "${spyName}" to not be called once with arguments: #{exp}`,
|
|
1211
|
+
args
|
|
1212
|
+
]);
|
|
1213
|
+
if (pass && isNot || !pass && !isNot) throw new AssertionError(formatCalls(spy, msg, args));
|
|
1214
|
+
});
|
|
1215
|
+
def("toHaveBeenNthCalledWith", function(times, ...args) {
|
|
1216
|
+
const spy = getSpy(this);
|
|
1217
|
+
const spyName = spy.getMockName();
|
|
1218
|
+
const nthCall = spy.mock.calls[times - 1];
|
|
1219
|
+
const callCount = spy.mock.calls.length;
|
|
1220
|
+
const isCalled = times <= callCount;
|
|
1221
|
+
this.assert(nthCall && equalsArgumentArray(nthCall, args), `expected ${ordinal(times)} "${spyName}" call to have been called with #{exp}${isCalled ? `` : `, but called only ${callCount} times`}`, `expected ${ordinal(times)} "${spyName}" call to not have been called with #{exp}`, args, nthCall, isCalled);
|
|
1222
|
+
});
|
|
1223
|
+
def("toHaveBeenLastCalledWith", function(...args) {
|
|
1224
|
+
const spy = getSpy(this);
|
|
1225
|
+
const spyName = spy.getMockName();
|
|
1226
|
+
const lastCall = spy.mock.calls.at(-1);
|
|
1227
|
+
this.assert(lastCall && equalsArgumentArray(lastCall, args), `expected last "${spyName}" call to have been called with #{exp}`, `expected last "${spyName}" call to not have been called with #{exp}`, args, lastCall);
|
|
1228
|
+
});
|
|
1229
|
+
/**
|
|
1230
|
+
* Used for `toHaveBeenCalledBefore` and `toHaveBeenCalledAfter` to determine if the expected spy was called before the result spy.
|
|
1231
|
+
*/
|
|
1232
|
+
function isSpyCalledBeforeAnotherSpy(beforeSpy, afterSpy, failIfNoFirstInvocation) {
|
|
1233
|
+
const beforeInvocationCallOrder = beforeSpy.mock.invocationCallOrder;
|
|
1234
|
+
const afterInvocationCallOrder = afterSpy.mock.invocationCallOrder;
|
|
1235
|
+
if (beforeInvocationCallOrder.length === 0) return !failIfNoFirstInvocation;
|
|
1236
|
+
if (afterInvocationCallOrder.length === 0) return false;
|
|
1237
|
+
return beforeInvocationCallOrder[0] < afterInvocationCallOrder[0];
|
|
1238
|
+
}
|
|
1239
|
+
def(["toHaveBeenCalledBefore"], function(resultSpy, failIfNoFirstInvocation = true) {
|
|
1240
|
+
const expectSpy = getSpy(this);
|
|
1241
|
+
if (!isMockFunction(resultSpy)) throw new TypeError(`${utils.inspect(resultSpy)} is not a spy or a call to a spy`);
|
|
1242
|
+
this.assert(isSpyCalledBeforeAnotherSpy(expectSpy, resultSpy, failIfNoFirstInvocation), `expected "${expectSpy.getMockName()}" to have been called before "${resultSpy.getMockName()}"`, `expected "${expectSpy.getMockName()}" to not have been called before "${resultSpy.getMockName()}"`, resultSpy, expectSpy);
|
|
1243
|
+
});
|
|
1244
|
+
def(["toHaveBeenCalledAfter"], function(resultSpy, failIfNoFirstInvocation = true) {
|
|
1245
|
+
const expectSpy = getSpy(this);
|
|
1246
|
+
if (!isMockFunction(resultSpy)) throw new TypeError(`${utils.inspect(resultSpy)} is not a spy or a call to a spy`);
|
|
1247
|
+
this.assert(isSpyCalledBeforeAnotherSpy(resultSpy, expectSpy, failIfNoFirstInvocation), `expected "${expectSpy.getMockName()}" to have been called after "${resultSpy.getMockName()}"`, `expected "${expectSpy.getMockName()}" to not have been called after "${resultSpy.getMockName()}"`, resultSpy, expectSpy);
|
|
1248
|
+
});
|
|
1249
|
+
def(["toThrow", "toThrowError"], function(expected) {
|
|
1250
|
+
if (typeof expected === "string" || typeof expected === "undefined" || expected instanceof RegExp) return this.throws(expected);
|
|
1251
|
+
const obj = this._obj;
|
|
1252
|
+
const promise = utils.flag(this, "promise");
|
|
1253
|
+
const isNot = utils.flag(this, "negate");
|
|
1254
|
+
let thrown = null;
|
|
1255
|
+
if (promise === "rejects") thrown = obj;
|
|
1256
|
+
else if (promise === "resolves" && typeof obj !== "function") if (!isNot) throw new AssertionError(utils.flag(this, "message") || "expected promise to throw an error, but it didn't", { showDiff: false }, utils.flag(this, "ssfi"));
|
|
1257
|
+
else return;
|
|
1258
|
+
else {
|
|
1259
|
+
let isThrow = false;
|
|
1260
|
+
try {
|
|
1261
|
+
obj();
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
isThrow = true;
|
|
1264
|
+
thrown = err;
|
|
1265
|
+
}
|
|
1266
|
+
if (!isThrow && !isNot) throw new AssertionError(utils.flag(this, "message") || "expected function to throw an error, but it didn't", { showDiff: false }, utils.flag(this, "ssfi"));
|
|
1267
|
+
}
|
|
1268
|
+
if (typeof expected === "function") {
|
|
1269
|
+
const name = expected.name || expected.prototype.constructor.name;
|
|
1270
|
+
return this.assert(thrown && thrown instanceof expected, `expected error to be instance of ${name}`, `expected error not to be instance of ${name}`, expected, thrown);
|
|
1271
|
+
}
|
|
1272
|
+
if (isError(expected)) {
|
|
1273
|
+
const equal = equals(thrown, expected, [...customTesters, iterableEquality]);
|
|
1274
|
+
return this.assert(equal, "expected a thrown error to be #{exp}", "expected a thrown error not to be #{exp}", expected, thrown);
|
|
1275
|
+
}
|
|
1276
|
+
if (typeof expected === "object" && "asymmetricMatch" in expected && typeof expected.asymmetricMatch === "function") {
|
|
1277
|
+
const matcher = expected;
|
|
1278
|
+
return this.assert(thrown && matcher.asymmetricMatch(thrown), "expected error to match asymmetric matcher", "expected error not to match asymmetric matcher", matcher, thrown);
|
|
1279
|
+
}
|
|
1280
|
+
const equal = equals(thrown, expected, [...customTesters, iterableEquality]);
|
|
1281
|
+
return this.assert(equal, "expected a thrown value to equal #{exp}", "expected a thrown value not to equal #{exp}", expected, thrown);
|
|
1282
|
+
});
|
|
1283
|
+
[{
|
|
1284
|
+
name: "toHaveResolved",
|
|
1285
|
+
condition: (spy) => spy.mock.settledResults.length > 0 && spy.mock.settledResults.some(({ type }) => type === "fulfilled"),
|
|
1286
|
+
action: "resolved"
|
|
1287
|
+
}, {
|
|
1288
|
+
name: ["toHaveReturned", "toReturn"],
|
|
1289
|
+
condition: (spy) => spy.mock.calls.length > 0 && spy.mock.results.some(({ type }) => type !== "throw"),
|
|
1290
|
+
action: "called"
|
|
1291
|
+
}].forEach(({ name, condition, action }) => {
|
|
1292
|
+
def(name, function() {
|
|
1293
|
+
const spy = getSpy(this);
|
|
1294
|
+
const spyName = spy.getMockName();
|
|
1295
|
+
const pass = condition(spy);
|
|
1296
|
+
this.assert(pass, `expected "${spyName}" to be successfully ${action} at least once`, `expected "${spyName}" to not be successfully ${action}`, pass, !pass, false);
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
[{
|
|
1300
|
+
name: "toHaveResolvedTimes",
|
|
1301
|
+
condition: (spy, times) => spy.mock.settledResults.reduce((s, { type }) => type === "fulfilled" ? ++s : s, 0) === times,
|
|
1302
|
+
action: "resolved"
|
|
1303
|
+
}, {
|
|
1304
|
+
name: ["toHaveReturnedTimes", "toReturnTimes"],
|
|
1305
|
+
condition: (spy, times) => spy.mock.results.reduce((s, { type }) => type === "throw" ? s : ++s, 0) === times,
|
|
1306
|
+
action: "called"
|
|
1307
|
+
}].forEach(({ name, condition, action }) => {
|
|
1308
|
+
def(name, function(times) {
|
|
1309
|
+
const spy = getSpy(this);
|
|
1310
|
+
const spyName = spy.getMockName();
|
|
1311
|
+
const pass = condition(spy, times);
|
|
1312
|
+
this.assert(pass, `expected "${spyName}" to be successfully ${action} ${times} times`, `expected "${spyName}" to not be successfully ${action} ${times} times`, `expected resolved times: ${times}`, `received resolved times: ${pass}`, false);
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
[{
|
|
1316
|
+
name: "toHaveResolvedWith",
|
|
1317
|
+
condition: (spy, value) => spy.mock.settledResults.some(({ type, value: result }) => type === "fulfilled" && equals(value, result)),
|
|
1318
|
+
action: "resolve"
|
|
1319
|
+
}, {
|
|
1320
|
+
name: ["toHaveReturnedWith", "toReturnWith"],
|
|
1321
|
+
condition: (spy, value) => spy.mock.results.some(({ type, value: result }) => type === "return" && equals(value, result)),
|
|
1322
|
+
action: "return"
|
|
1323
|
+
}].forEach(({ name, condition, action }) => {
|
|
1324
|
+
def(name, function(value) {
|
|
1325
|
+
const spy = getSpy(this);
|
|
1326
|
+
const pass = condition(spy, value);
|
|
1327
|
+
const isNot = utils.flag(this, "negate");
|
|
1328
|
+
if (pass && isNot || !pass && !isNot) {
|
|
1329
|
+
const spyName = spy.getMockName();
|
|
1330
|
+
const msg = utils.getMessage(this, [
|
|
1331
|
+
pass,
|
|
1332
|
+
`expected "${spyName}" to ${action} with: #{exp} at least once`,
|
|
1333
|
+
`expected "${spyName}" to not ${action} with: #{exp}`,
|
|
1334
|
+
value
|
|
1335
|
+
]);
|
|
1336
|
+
throw new AssertionError(formatReturns(spy, action === "return" ? spy.mock.results : spy.mock.settledResults, msg, value));
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
[{
|
|
1341
|
+
name: "toHaveLastResolvedWith",
|
|
1342
|
+
condition: (spy, value) => {
|
|
1343
|
+
const result = spy.mock.settledResults.at(-1);
|
|
1344
|
+
return Boolean(result && result.type === "fulfilled" && equals(result.value, value));
|
|
1345
|
+
},
|
|
1346
|
+
action: "resolve"
|
|
1347
|
+
}, {
|
|
1348
|
+
name: "toHaveLastReturnedWith",
|
|
1349
|
+
condition: (spy, value) => {
|
|
1350
|
+
const result = spy.mock.results.at(-1);
|
|
1351
|
+
return Boolean(result && result.type === "return" && equals(result.value, value));
|
|
1352
|
+
},
|
|
1353
|
+
action: "return"
|
|
1354
|
+
}].forEach(({ name, condition, action }) => {
|
|
1355
|
+
def(name, function(value) {
|
|
1356
|
+
const spy = getSpy(this);
|
|
1357
|
+
const result = (action === "return" ? spy.mock.results : spy.mock.settledResults).at(-1);
|
|
1358
|
+
const spyName = spy.getMockName();
|
|
1359
|
+
this.assert(condition(spy, value), `expected last "${spyName}" call to ${action} #{exp}`, `expected last "${spyName}" call to not ${action} #{exp}`, value, result?.value);
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
[{
|
|
1363
|
+
name: "toHaveNthResolvedWith",
|
|
1364
|
+
condition: (spy, index, value) => {
|
|
1365
|
+
const result = spy.mock.settledResults[index - 1];
|
|
1366
|
+
return result && result.type === "fulfilled" && equals(result.value, value);
|
|
1367
|
+
},
|
|
1368
|
+
action: "resolve"
|
|
1369
|
+
}, {
|
|
1370
|
+
name: "toHaveNthReturnedWith",
|
|
1371
|
+
condition: (spy, index, value) => {
|
|
1372
|
+
const result = spy.mock.results[index - 1];
|
|
1373
|
+
return result && result.type === "return" && equals(result.value, value);
|
|
1374
|
+
},
|
|
1375
|
+
action: "return"
|
|
1376
|
+
}].forEach(({ name, condition, action }) => {
|
|
1377
|
+
def(name, function(nthCall, value) {
|
|
1378
|
+
const spy = getSpy(this);
|
|
1379
|
+
const spyName = spy.getMockName();
|
|
1380
|
+
const result = (action === "return" ? spy.mock.results : spy.mock.settledResults)[nthCall - 1];
|
|
1381
|
+
const ordinalCall = `${ordinal(nthCall)} call`;
|
|
1382
|
+
this.assert(condition(spy, nthCall, value), `expected ${ordinalCall} "${spyName}" call to ${action} #{exp}`, `expected ${ordinalCall} "${spyName}" call to not ${action} #{exp}`, value, result?.value);
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
// @ts-expect-error @internal
|
|
1386
|
+
def("withContext", function(context) {
|
|
1387
|
+
for (const key in context) utils.flag(this, key, context[key]);
|
|
1388
|
+
return this;
|
|
1389
|
+
});
|
|
1390
|
+
utils.addProperty(chai.Assertion.prototype, "resolves", function __VITEST_RESOLVES__() {
|
|
1391
|
+
const error = /* @__PURE__ */ new Error("resolves");
|
|
1392
|
+
utils.flag(this, "promise", "resolves");
|
|
1393
|
+
utils.flag(this, "error", error);
|
|
1394
|
+
const test = utils.flag(this, "vitest-test");
|
|
1395
|
+
const obj = utils.flag(this, "object");
|
|
1396
|
+
if (utils.flag(this, "poll")) throw new SyntaxError(`expect.poll() is not supported in combination with .resolves`);
|
|
1397
|
+
if (typeof obj?.then !== "function") throw new TypeError(`You must provide a Promise to expect() when using .resolves, not '${typeof obj}'.`);
|
|
1398
|
+
const proxy = new Proxy(this, { get: (target, key, receiver) => {
|
|
1399
|
+
const result = Reflect.get(target, key, receiver);
|
|
1400
|
+
if (typeof result !== "function") return result instanceof chai.Assertion ? proxy : result;
|
|
1401
|
+
return (...args) => {
|
|
1402
|
+
utils.flag(this, "_name", key);
|
|
1403
|
+
return recordAsyncExpect(test, Promise.resolve(obj).then((value) => {
|
|
1404
|
+
utils.flag(this, "object", value);
|
|
1405
|
+
return result.call(this, ...args);
|
|
1406
|
+
}, (err) => {
|
|
1407
|
+
const _error = new AssertionError(`promise rejected "${utils.inspect(err)}" instead of resolving`, { showDiff: false });
|
|
1408
|
+
_error.cause = err;
|
|
1409
|
+
throw _error;
|
|
1410
|
+
}).catch((err) => {
|
|
1411
|
+
if (isError(err) && error.stack) err.stack = error.stack.replace(error.message, err.message);
|
|
1412
|
+
throw err;
|
|
1413
|
+
}), createAssertionMessage(utils, this, !!args.length), error, utils.flag(this, "soft"));
|
|
1414
|
+
};
|
|
1415
|
+
} });
|
|
1416
|
+
return proxy;
|
|
1417
|
+
});
|
|
1418
|
+
utils.addProperty(chai.Assertion.prototype, "rejects", function __VITEST_REJECTS__() {
|
|
1419
|
+
const error = /* @__PURE__ */ new Error("rejects");
|
|
1420
|
+
utils.flag(this, "promise", "rejects");
|
|
1421
|
+
utils.flag(this, "error", error);
|
|
1422
|
+
const test = utils.flag(this, "vitest-test");
|
|
1423
|
+
const obj = utils.flag(this, "object");
|
|
1424
|
+
const wrapper = typeof obj === "function" ? obj() : obj;
|
|
1425
|
+
if (utils.flag(this, "poll")) throw new SyntaxError(`expect.poll() is not supported in combination with .rejects`);
|
|
1426
|
+
if (typeof wrapper?.then !== "function") throw new TypeError(`You must provide a Promise to expect() when using .rejects, not '${typeof wrapper}'.`);
|
|
1427
|
+
const proxy = new Proxy(this, { get: (target, key, receiver) => {
|
|
1428
|
+
const result = Reflect.get(target, key, receiver);
|
|
1429
|
+
if (typeof result !== "function") return result instanceof chai.Assertion ? proxy : result;
|
|
1430
|
+
return (...args) => {
|
|
1431
|
+
utils.flag(this, "_name", key);
|
|
1432
|
+
return recordAsyncExpect(test, Promise.resolve(wrapper).then((value) => {
|
|
1433
|
+
throw new AssertionError(`promise resolved "${utils.inspect(value)}" instead of rejecting`, {
|
|
1434
|
+
showDiff: true,
|
|
1435
|
+
expected: /* @__PURE__ */ new Error("rejected promise"),
|
|
1436
|
+
actual: value
|
|
1437
|
+
});
|
|
1438
|
+
}, (err) => {
|
|
1439
|
+
utils.flag(this, "object", err);
|
|
1440
|
+
return result.call(this, ...args);
|
|
1441
|
+
}).catch((err) => {
|
|
1442
|
+
if (isError(err) && error.stack) err.stack = error.stack.replace(error.message, err.message);
|
|
1443
|
+
throw err;
|
|
1444
|
+
}), createAssertionMessage(utils, this, !!args.length), error, utils.flag(this, "soft"));
|
|
1445
|
+
};
|
|
1446
|
+
} });
|
|
1447
|
+
return proxy;
|
|
1448
|
+
});
|
|
1449
|
+
};
|
|
1450
|
+
function formatCalls(spy, msg, showActualCall) {
|
|
1451
|
+
if (spy.mock.calls.length) msg += c.gray(`\n\nReceived:\n\n${spy.mock.calls.map((callArg, i) => {
|
|
1452
|
+
let methodCall = c.bold(` ${ordinal(i + 1)} ${spy.getMockName()} call:\n\n`);
|
|
1453
|
+
if (showActualCall) methodCall += diff(showActualCall, callArg, { omitAnnotationLines: true });
|
|
1454
|
+
else methodCall += stringify(callArg).split("\n").map((line) => ` ${line}`).join("\n");
|
|
1455
|
+
methodCall += "\n";
|
|
1456
|
+
return methodCall;
|
|
1457
|
+
}).join("\n")}`);
|
|
1458
|
+
msg += c.gray(`\n\nNumber of calls: ${c.bold(spy.mock.calls.length)}\n`);
|
|
1459
|
+
return msg;
|
|
1460
|
+
}
|
|
1461
|
+
function formatReturns(spy, results, msg, showActualReturn) {
|
|
1462
|
+
if (results.length) msg += c.gray(`\n\nReceived:\n\n${results.map((callReturn, i) => {
|
|
1463
|
+
let methodCall = c.bold(` ${ordinal(i + 1)} ${spy.getMockName()} call return:\n\n`);
|
|
1464
|
+
if (showActualReturn) methodCall += diff(showActualReturn, callReturn.value, { omitAnnotationLines: true });
|
|
1465
|
+
else methodCall += stringify(callReturn).split("\n").map((line) => ` ${line}`).join("\n");
|
|
1466
|
+
methodCall += "\n";
|
|
1467
|
+
return methodCall;
|
|
1468
|
+
}).join("\n")}`);
|
|
1469
|
+
msg += c.gray(`\n\nNumber of calls: ${c.bold(spy.mock.calls.length)}\n`);
|
|
1470
|
+
return msg;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function getMatcherState(assertion, expect) {
|
|
1474
|
+
const obj = assertion._obj;
|
|
1475
|
+
const isNot = util.flag(assertion, "negate");
|
|
1476
|
+
const promise = util.flag(assertion, "promise") || "";
|
|
1477
|
+
const customMessage = util.flag(assertion, "message");
|
|
1478
|
+
const jestUtils = {
|
|
1479
|
+
...getMatcherUtils(),
|
|
1480
|
+
diff,
|
|
1481
|
+
stringify,
|
|
1482
|
+
iterableEquality,
|
|
1483
|
+
subsetEquality
|
|
1484
|
+
};
|
|
1485
|
+
let task = util.flag(assertion, "vitest-test");
|
|
1486
|
+
const currentTestName = task?.fullTestName ?? "";
|
|
1487
|
+
if (task?.type !== "test") task = void 0;
|
|
1488
|
+
const matcherState = {
|
|
1489
|
+
...getState(expect),
|
|
1490
|
+
currentTestName,
|
|
1491
|
+
customTesters: getCustomEqualityTesters(),
|
|
1492
|
+
isNot,
|
|
1493
|
+
utils: jestUtils,
|
|
1494
|
+
promise,
|
|
1495
|
+
equals,
|
|
1496
|
+
suppressedErrors: [],
|
|
1497
|
+
soft: util.flag(assertion, "soft"),
|
|
1498
|
+
poll: util.flag(assertion, "poll"),
|
|
1499
|
+
assertion
|
|
1500
|
+
};
|
|
1501
|
+
Object.assign(matcherState, { task });
|
|
1502
|
+
return {
|
|
1503
|
+
state: matcherState,
|
|
1504
|
+
isNot,
|
|
1505
|
+
obj,
|
|
1506
|
+
customMessage
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
class JestExtendError extends Error {
|
|
1510
|
+
constructor(message, actual, expected, __vitest_error_context__) {
|
|
1511
|
+
super(message);
|
|
1512
|
+
this.actual = actual;
|
|
1513
|
+
this.expected = expected;
|
|
1514
|
+
this.__vitest_error_context__ = __vitest_error_context__;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function JestExtendPlugin(c, expect, matchers) {
|
|
1518
|
+
return (_, utils) => {
|
|
1519
|
+
Object.entries(matchers).forEach(([expectAssertionName, expectAssertion]) => {
|
|
1520
|
+
function __VITEST_EXTEND_ASSERTION__(...args) {
|
|
1521
|
+
const { state, isNot, obj, customMessage } = getMatcherState(this, expect);
|
|
1522
|
+
const result = expectAssertion.call(state, obj, ...args);
|
|
1523
|
+
if (result && typeof result === "object" && typeof result.then === "function") return result.then(({ pass, message, actual, expected, meta }) => {
|
|
1524
|
+
if (pass && isNot || !pass && !isNot) throw new JestExtendError((customMessage ? `${customMessage}: ` : "") + message(), actual, expected, {
|
|
1525
|
+
assertionName: expectAssertionName,
|
|
1526
|
+
meta
|
|
1527
|
+
});
|
|
1528
|
+
});
|
|
1529
|
+
const { pass, message, actual, expected, meta } = result;
|
|
1530
|
+
if (pass && isNot || !pass && !isNot) throw new JestExtendError((customMessage ? `${customMessage}: ` : "") + message(), actual, expected, {
|
|
1531
|
+
assertionName: expectAssertionName,
|
|
1532
|
+
meta
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
const softWrapper = wrapAssertion(utils, expectAssertionName, __VITEST_EXTEND_ASSERTION__);
|
|
1536
|
+
utils.addMethod(globalThis[JEST_MATCHERS_OBJECT].matchers, expectAssertionName, softWrapper);
|
|
1537
|
+
utils.addMethod(c.Assertion.prototype, expectAssertionName, softWrapper);
|
|
1538
|
+
// `expect.poll()` inspects the installed Chai assertion method,
|
|
1539
|
+
// so copy the internal marker from the original matcher function.
|
|
1540
|
+
// this is only for domain snapshot matchers for now.
|
|
1541
|
+
if (expectAssertion.__vitest_poll_takeover__) {
|
|
1542
|
+
const addedMethod = c.Assertion.prototype[expectAssertionName];
|
|
1543
|
+
Object.defineProperty(addedMethod, "__vitest_poll_takeover__", { value: true });
|
|
1544
|
+
}
|
|
1545
|
+
class CustomMatcher extends AsymmetricMatcher {
|
|
1546
|
+
constructor(inverse = false, ...sample) {
|
|
1547
|
+
super(sample, inverse);
|
|
1548
|
+
}
|
|
1549
|
+
asymmetricMatch(other) {
|
|
1550
|
+
const { pass } = expectAssertion.call(this.getMatcherContext(expect), other, ...this.sample);
|
|
1551
|
+
return this.inverse ? !pass : pass;
|
|
1552
|
+
}
|
|
1553
|
+
toString() {
|
|
1554
|
+
return `${this.inverse ? "not." : ""}${expectAssertionName}`;
|
|
1555
|
+
}
|
|
1556
|
+
getExpectedType() {
|
|
1557
|
+
return "any";
|
|
1558
|
+
}
|
|
1559
|
+
toAsymmetricMatcher() {
|
|
1560
|
+
return `${this.toString()}<${this.sample.map((item) => stringify(item)).join(", ")}>`;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const customMatcher = (...sample) => new CustomMatcher(false, ...sample);
|
|
1564
|
+
Object.defineProperty(expect, expectAssertionName, {
|
|
1565
|
+
configurable: true,
|
|
1566
|
+
enumerable: true,
|
|
1567
|
+
value: customMatcher,
|
|
1568
|
+
writable: true
|
|
1569
|
+
});
|
|
1570
|
+
Object.defineProperty(expect.not, expectAssertionName, {
|
|
1571
|
+
configurable: true,
|
|
1572
|
+
enumerable: true,
|
|
1573
|
+
value: (...sample) => new CustomMatcher(true, ...sample),
|
|
1574
|
+
writable: true
|
|
1575
|
+
});
|
|
1576
|
+
// keep track of asymmetric matchers on global so that it can be copied over to local context's `expect`.
|
|
1577
|
+
// note that the negated variant is automatically shared since it's assigned on the single `expect.not` object.
|
|
1578
|
+
Object.defineProperty(globalThis[ASYMMETRIC_MATCHERS_OBJECT], expectAssertionName, {
|
|
1579
|
+
configurable: true,
|
|
1580
|
+
enumerable: true,
|
|
1581
|
+
value: customMatcher,
|
|
1582
|
+
writable: true
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
const JestExtend = (chai, utils) => {
|
|
1588
|
+
utils.addMethod(chai.expect, "extend", (expect, expects) => {
|
|
1589
|
+
use(JestExtendPlugin(chai, expect, expects));
|
|
1590
|
+
});
|
|
1591
|
+
};
|
|
16
1592
|
|
|
17
1593
|
var fakeTimersSrc = {};
|
|
18
1594
|
|
|
@@ -3838,6 +5414,1006 @@ function copyStackTrace(target, source) {
|
|
|
3838
5414
|
return target;
|
|
3839
5415
|
}
|
|
3840
5416
|
|
|
5417
|
+
var naturalCompare$1 = {exports: {}};
|
|
5418
|
+
|
|
5419
|
+
var hasRequiredNaturalCompare;
|
|
5420
|
+
|
|
5421
|
+
function requireNaturalCompare () {
|
|
5422
|
+
if (hasRequiredNaturalCompare) return naturalCompare$1.exports;
|
|
5423
|
+
hasRequiredNaturalCompare = 1;
|
|
5424
|
+
/*
|
|
5425
|
+
* @version 1.4.0
|
|
5426
|
+
* @date 2015-10-26
|
|
5427
|
+
* @stability 3 - Stable
|
|
5428
|
+
* @author Lauri Rooden (https://github.com/litejs/natural-compare-lite)
|
|
5429
|
+
* @license MIT License
|
|
5430
|
+
*/
|
|
5431
|
+
|
|
5432
|
+
|
|
5433
|
+
var naturalCompare = function(a, b) {
|
|
5434
|
+
var i, codeA
|
|
5435
|
+
, codeB = 1
|
|
5436
|
+
, posA = 0
|
|
5437
|
+
, posB = 0
|
|
5438
|
+
, alphabet = String.alphabet;
|
|
5439
|
+
|
|
5440
|
+
function getCode(str, pos, code) {
|
|
5441
|
+
if (code) {
|
|
5442
|
+
for (i = pos; code = getCode(str, i), code < 76 && code > 65;) ++i;
|
|
5443
|
+
return +str.slice(pos - 1, i)
|
|
5444
|
+
}
|
|
5445
|
+
code = alphabet && alphabet.indexOf(str.charAt(pos));
|
|
5446
|
+
return code > -1 ? code + 76 : ((code = str.charCodeAt(pos) || 0), code < 45 || code > 127) ? code
|
|
5447
|
+
: code < 46 ? 65 // -
|
|
5448
|
+
: code < 48 ? code - 1
|
|
5449
|
+
: code < 58 ? code + 18 // 0-9
|
|
5450
|
+
: code < 65 ? code - 11
|
|
5451
|
+
: code < 91 ? code + 11 // A-Z
|
|
5452
|
+
: code < 97 ? code - 37
|
|
5453
|
+
: code < 123 ? code + 5 // a-z
|
|
5454
|
+
: code - 63
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5457
|
+
|
|
5458
|
+
if ((a+="") != (b+="")) for (;codeB;) {
|
|
5459
|
+
codeA = getCode(a, posA++);
|
|
5460
|
+
codeB = getCode(b, posB++);
|
|
5461
|
+
|
|
5462
|
+
if (codeA < 76 && codeB < 76 && codeA > 66 && codeB > 66) {
|
|
5463
|
+
codeA = getCode(a, posA, posA);
|
|
5464
|
+
codeB = getCode(b, posB, posA = i);
|
|
5465
|
+
posB = i;
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5468
|
+
if (codeA != codeB) return (codeA < codeB) ? -1 : 1
|
|
5469
|
+
}
|
|
5470
|
+
return 0
|
|
5471
|
+
};
|
|
5472
|
+
|
|
5473
|
+
try {
|
|
5474
|
+
naturalCompare$1.exports = naturalCompare;
|
|
5475
|
+
} catch (e) {
|
|
5476
|
+
String.naturalCompare = naturalCompare;
|
|
5477
|
+
}
|
|
5478
|
+
return naturalCompare$1.exports;
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
var naturalCompareExports = requireNaturalCompare();
|
|
5482
|
+
var naturalCompare = /*@__PURE__*/getDefaultExportFromCjs(naturalCompareExports);
|
|
5483
|
+
|
|
5484
|
+
// TODO: rewrite and clean up
|
|
5485
|
+
function testNameToKey(testName, count) {
|
|
5486
|
+
return `${testName} ${count}`;
|
|
5487
|
+
}
|
|
5488
|
+
function keyToTestName(key) {
|
|
5489
|
+
if (!/ \d+$/.test(key)) throw new Error("Snapshot keys must end with a number.");
|
|
5490
|
+
return key.replace(/ \d+$/, "");
|
|
5491
|
+
}
|
|
5492
|
+
function getSnapshotData(content, options) {
|
|
5493
|
+
const update = options.updateSnapshot;
|
|
5494
|
+
const data = Object.create(null);
|
|
5495
|
+
let snapshotContents = "";
|
|
5496
|
+
let dirty = false;
|
|
5497
|
+
if (content != null) try {
|
|
5498
|
+
snapshotContents = content;
|
|
5499
|
+
new Function("exports", snapshotContents)(data);
|
|
5500
|
+
} catch {}
|
|
5501
|
+
// if (update === 'none' && isInvalid)
|
|
5502
|
+
// throw validationResult
|
|
5503
|
+
if ((update === "all" || update === "new") && snapshotContents) dirty = true;
|
|
5504
|
+
return {
|
|
5505
|
+
data,
|
|
5506
|
+
dirty
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
// Add extra line breaks at beginning and end of multiline snapshot
|
|
5510
|
+
// to make the content easier to read.
|
|
5511
|
+
function addExtraLineBreaks(string) {
|
|
5512
|
+
return string.includes("\n") ? `\n${string}\n` : string;
|
|
5513
|
+
}
|
|
5514
|
+
// Remove extra line breaks at beginning and end of multiline snapshot.
|
|
5515
|
+
// Instead of trim, which can remove additional newlines or spaces
|
|
5516
|
+
// at beginning or end of the content from a custom serializer.
|
|
5517
|
+
function removeExtraLineBreaks(string) {
|
|
5518
|
+
return string.length > 2 && string[0] === "\n" && string.endsWith("\n") ? string.slice(1, -1) : string;
|
|
5519
|
+
}
|
|
5520
|
+
// export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => {
|
|
5521
|
+
// const lines = stack.split('\n')
|
|
5522
|
+
// for (let i = 0; i < lines.length; i += 1) {
|
|
5523
|
+
// // It's a function name specified in `packages/expect/src/index.ts`
|
|
5524
|
+
// // for external custom matchers.
|
|
5525
|
+
// if (lines[i].includes('__EXTERNAL_MATCHER_TRAP__'))
|
|
5526
|
+
// return lines.slice(i + 1).join('\n')
|
|
5527
|
+
// }
|
|
5528
|
+
// return stack
|
|
5529
|
+
// }
|
|
5530
|
+
const escapeRegex = true;
|
|
5531
|
+
const printFunctionName = false;
|
|
5532
|
+
function serialize(val, indent = 2, formatOverrides = {}) {
|
|
5533
|
+
return normalizeNewlines(format(val, {
|
|
5534
|
+
escapeRegex,
|
|
5535
|
+
indent,
|
|
5536
|
+
plugins: getSerializers(),
|
|
5537
|
+
printFunctionName,
|
|
5538
|
+
...formatOverrides
|
|
5539
|
+
}));
|
|
5540
|
+
}
|
|
5541
|
+
function escapeBacktickString(str) {
|
|
5542
|
+
return str.replace(/`|\\|\$\{/g, "\\$&");
|
|
5543
|
+
}
|
|
5544
|
+
function printBacktickString(str) {
|
|
5545
|
+
return `\`${escapeBacktickString(str)}\``;
|
|
5546
|
+
}
|
|
5547
|
+
function normalizeNewlines(string) {
|
|
5548
|
+
return string.replace(/\r\n|\r/g, "\n");
|
|
5549
|
+
}
|
|
5550
|
+
async function saveSnapshotFile(environment, snapshotData, snapshotPath) {
|
|
5551
|
+
const snapshots = Object.keys(snapshotData).sort(naturalCompare).map((key) => `exports[${printBacktickString(key)}] = ${printBacktickString(normalizeNewlines(snapshotData[key]))};`);
|
|
5552
|
+
const content = `${environment.getHeader()}\n\n${snapshots.join("\n\n")}\n`;
|
|
5553
|
+
const oldContent = await environment.readSnapshotFile(snapshotPath);
|
|
5554
|
+
if (oldContent != null && oldContent === content) return;
|
|
5555
|
+
await environment.saveSnapshotFile(snapshotPath, content);
|
|
5556
|
+
}
|
|
5557
|
+
function deepMergeArray(target = [], source = []) {
|
|
5558
|
+
const mergedOutput = Array.from(target);
|
|
5559
|
+
source.forEach((sourceElement, index) => {
|
|
5560
|
+
const targetElement = mergedOutput[index];
|
|
5561
|
+
if (Array.isArray(target[index])) mergedOutput[index] = deepMergeArray(target[index], sourceElement);
|
|
5562
|
+
else if (isObject(targetElement)) mergedOutput[index] = deepMergeSnapshot(target[index], sourceElement);
|
|
5563
|
+
else
|
|
5564
|
+
// Source does not exist in target or target is primitive and cannot be deep merged
|
|
5565
|
+
mergedOutput[index] = sourceElement;
|
|
5566
|
+
});
|
|
5567
|
+
return mergedOutput;
|
|
5568
|
+
}
|
|
5569
|
+
/**
|
|
5570
|
+
* Deep merge, but considers asymmetric matchers. Unlike base util's deep merge,
|
|
5571
|
+
* will merge any object-like instance.
|
|
5572
|
+
* Compatible with Jest's snapshot matcher. Should not be used outside of snapshot.
|
|
5573
|
+
*
|
|
5574
|
+
* @example
|
|
5575
|
+
* ```ts
|
|
5576
|
+
* toMatchSnapshot({
|
|
5577
|
+
* name: expect.stringContaining('text')
|
|
5578
|
+
* })
|
|
5579
|
+
* ```
|
|
5580
|
+
*/
|
|
5581
|
+
function deepMergeSnapshot(target, source) {
|
|
5582
|
+
if (isObject(target) && isObject(source)) {
|
|
5583
|
+
const mergedOutput = { ...target };
|
|
5584
|
+
Object.keys(source).forEach((key) => {
|
|
5585
|
+
if (isObject(source[key]) && !source[key].$$typeof) if (!(key in target)) Object.assign(mergedOutput, { [key]: source[key] });
|
|
5586
|
+
else mergedOutput[key] = deepMergeSnapshot(target[key], source[key]);
|
|
5587
|
+
else if (Array.isArray(source[key])) mergedOutput[key] = deepMergeArray(target[key], source[key]);
|
|
5588
|
+
else Object.assign(mergedOutput, { [key]: source[key] });
|
|
5589
|
+
});
|
|
5590
|
+
return mergedOutput;
|
|
5591
|
+
} else if (Array.isArray(target) && Array.isArray(source)) return deepMergeArray(target, source);
|
|
5592
|
+
return target;
|
|
5593
|
+
}
|
|
5594
|
+
class DefaultMap extends Map {
|
|
5595
|
+
constructor(defaultFn, entries) {
|
|
5596
|
+
super(entries);
|
|
5597
|
+
this.defaultFn = defaultFn;
|
|
5598
|
+
}
|
|
5599
|
+
get(key) {
|
|
5600
|
+
if (!this.has(key)) this.set(key, this.defaultFn(key));
|
|
5601
|
+
return super.get(key);
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
class CounterMap extends DefaultMap {
|
|
5605
|
+
constructor() {
|
|
5606
|
+
super(() => 0);
|
|
5607
|
+
}
|
|
5608
|
+
// compat for jest-image-snapshot https://github.com/vitest-dev/vitest/issues/7322
|
|
5609
|
+
// `valueOf` and `Snapshot.added` setter allows
|
|
5610
|
+
// snapshotState.added = snapshotState.added + 1
|
|
5611
|
+
// to function as
|
|
5612
|
+
// snapshotState.added.total_ = snapshotState.added.total() + 1
|
|
5613
|
+
_total;
|
|
5614
|
+
valueOf() {
|
|
5615
|
+
return this._total = this.total();
|
|
5616
|
+
}
|
|
5617
|
+
increment(key) {
|
|
5618
|
+
if (typeof this._total !== "undefined") this._total++;
|
|
5619
|
+
this.set(key, this.get(key) + 1);
|
|
5620
|
+
}
|
|
5621
|
+
total() {
|
|
5622
|
+
if (typeof this._total !== "undefined") return this._total;
|
|
5623
|
+
let total = 0;
|
|
5624
|
+
for (const x of this.values()) total += x;
|
|
5625
|
+
return total;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
5629
|
+
function memo(fn) {
|
|
5630
|
+
const cache = /* @__PURE__ */ new Map();
|
|
5631
|
+
return (arg) => {
|
|
5632
|
+
if (!cache.has(arg)) cache.set(arg, fn(arg));
|
|
5633
|
+
return cache.get(arg);
|
|
5634
|
+
};
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
async function saveInlineSnapshots(environment, snapshots) {
|
|
5638
|
+
const MagicString = (await import('magic-string')).default;
|
|
5639
|
+
const files = new Set(snapshots.map((i) => i.file));
|
|
5640
|
+
await Promise.all(Array.from(files).map(async (file) => {
|
|
5641
|
+
const snaps = snapshots.filter((i) => i.file === file);
|
|
5642
|
+
const code = await environment.readSnapshotFile(file);
|
|
5643
|
+
if (code == null) throw new Error(`cannot read ${file} when saving inline snapshot`);
|
|
5644
|
+
const s = new MagicString(code);
|
|
5645
|
+
for (const snap of snaps) replaceInlineSnap(code, s, positionToOffset(code, snap.line, snap.column), snap.snapshot, snap.assertionName);
|
|
5646
|
+
const transformed = s.toString();
|
|
5647
|
+
if (transformed !== code) await environment.saveSnapshotFile(file, transformed);
|
|
5648
|
+
}));
|
|
5649
|
+
}
|
|
5650
|
+
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]))*\{/;
|
|
5651
|
+
function escapeRegExp(s) {
|
|
5652
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5653
|
+
}
|
|
5654
|
+
const buildStartObjectRegex = memo((assertionName) => {
|
|
5655
|
+
const replaced = defaultStartObjectRegex.source.replace("toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot", escapeRegExp(assertionName));
|
|
5656
|
+
return new RegExp(replaced);
|
|
5657
|
+
});
|
|
5658
|
+
function replaceObjectSnap(code, s, index, newSnap, assertionName) {
|
|
5659
|
+
let _code = code.slice(index);
|
|
5660
|
+
const startMatch = (assertionName ? buildStartObjectRegex(assertionName) : defaultStartObjectRegex).exec(_code);
|
|
5661
|
+
if (!startMatch) return false;
|
|
5662
|
+
_code = _code.slice(startMatch.index);
|
|
5663
|
+
let callEnd = getCallLastIndex(_code);
|
|
5664
|
+
if (callEnd === null) return false;
|
|
5665
|
+
callEnd += index + startMatch.index;
|
|
5666
|
+
const shapeEnd = getObjectShapeEndIndex(code, index + startMatch.index + startMatch[0].length);
|
|
5667
|
+
const snap = `, ${prepareSnapString(newSnap, code, index)}`;
|
|
5668
|
+
if (shapeEnd === callEnd)
|
|
5669
|
+
// toMatchInlineSnapshot({ foo: expect.any(String) })
|
|
5670
|
+
s.appendLeft(callEnd, snap);
|
|
5671
|
+
else
|
|
5672
|
+
// toMatchInlineSnapshot({ foo: expect.any(String) }, ``)
|
|
5673
|
+
s.overwrite(shapeEnd, callEnd, snap);
|
|
5674
|
+
return true;
|
|
5675
|
+
}
|
|
5676
|
+
function getObjectShapeEndIndex(code, index) {
|
|
5677
|
+
let startBraces = 1;
|
|
5678
|
+
let endBraces = 0;
|
|
5679
|
+
while (startBraces !== endBraces && index < code.length) {
|
|
5680
|
+
const s = code[index++];
|
|
5681
|
+
if (s === "{") startBraces++;
|
|
5682
|
+
else if (s === "}") endBraces++;
|
|
5683
|
+
}
|
|
5684
|
+
return index;
|
|
5685
|
+
}
|
|
5686
|
+
function prepareSnapString(snap, source, index) {
|
|
5687
|
+
const lineNumber = offsetToLineNumber(source, index);
|
|
5688
|
+
const indent = source.split(lineSplitRE)[lineNumber - 1].match(/^\s*/)[0] || "";
|
|
5689
|
+
const indentNext = indent.includes(" ") ? `${indent}\t` : `${indent} `;
|
|
5690
|
+
const lines = snap.trim().replace(/\\/g, "\\\\").split(/\n/g);
|
|
5691
|
+
const isOneline = lines.length <= 1;
|
|
5692
|
+
const quote = "`";
|
|
5693
|
+
if (isOneline) return `${quote}${lines.join("\n").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}${quote}`;
|
|
5694
|
+
return `${quote}\n${lines.map((i) => i ? indentNext + i : "").join("\n").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")}\n${indent}${quote}`;
|
|
5695
|
+
}
|
|
5696
|
+
const defaultMethodNames = ["toMatchInlineSnapshot", "toThrowErrorMatchingInlineSnapshot"];
|
|
5697
|
+
// on webkit, the line number is at the end of the method, not at the start
|
|
5698
|
+
function getCodeStartingAtIndex(code, index, methodNames) {
|
|
5699
|
+
for (const name of methodNames) {
|
|
5700
|
+
const adjusted = index - name.length;
|
|
5701
|
+
if (adjusted >= 0 && code.slice(adjusted, index) === name) return {
|
|
5702
|
+
code: code.slice(adjusted),
|
|
5703
|
+
index: adjusted
|
|
5704
|
+
};
|
|
5705
|
+
}
|
|
5706
|
+
return {
|
|
5707
|
+
code: code.slice(index),
|
|
5708
|
+
index
|
|
5709
|
+
};
|
|
5710
|
+
}
|
|
5711
|
+
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$]*(['"`)])/;
|
|
5712
|
+
const buildStartRegex = memo((assertionName) => {
|
|
5713
|
+
const replaced = defaultStartRegex.source.replace("toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot", escapeRegExp(assertionName));
|
|
5714
|
+
return new RegExp(replaced);
|
|
5715
|
+
});
|
|
5716
|
+
function replaceInlineSnap(code, s, currentIndex, newSnap, assertionName) {
|
|
5717
|
+
const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex, assertionName ? [assertionName] : defaultMethodNames);
|
|
5718
|
+
const startMatch = (assertionName ? buildStartRegex(assertionName) : defaultStartRegex).exec(codeStartingAtIndex);
|
|
5719
|
+
const firstKeywordMatch = (assertionName ? new RegExp(escapeRegExp(assertionName)) : /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/).exec(codeStartingAtIndex);
|
|
5720
|
+
if (!startMatch || startMatch.index !== firstKeywordMatch?.index) return replaceObjectSnap(code, s, index, newSnap, assertionName);
|
|
5721
|
+
const quote = startMatch[1];
|
|
5722
|
+
const startIndex = index + startMatch.index + startMatch[0].length;
|
|
5723
|
+
const snapString = prepareSnapString(newSnap, code, index);
|
|
5724
|
+
if (quote === ")") {
|
|
5725
|
+
s.appendRight(startIndex - 1, snapString);
|
|
5726
|
+
return true;
|
|
5727
|
+
}
|
|
5728
|
+
const endMatch = new RegExp(`(?:^|[^\\\\])${quote}`).exec(code.slice(startIndex));
|
|
5729
|
+
if (!endMatch) return false;
|
|
5730
|
+
const endIndex = startIndex + endMatch.index + endMatch[0].length;
|
|
5731
|
+
s.overwrite(startIndex - 1, endIndex, snapString);
|
|
5732
|
+
return true;
|
|
5733
|
+
}
|
|
5734
|
+
const INDENTATION_REGEX = /^([^\S\n]*)\S/m;
|
|
5735
|
+
function stripSnapshotIndentation(inlineSnapshot) {
|
|
5736
|
+
// Find indentation if exists.
|
|
5737
|
+
const match = inlineSnapshot.match(INDENTATION_REGEX);
|
|
5738
|
+
if (!match || !match[1])
|
|
5739
|
+
// No indentation.
|
|
5740
|
+
return inlineSnapshot;
|
|
5741
|
+
const indentation = match[1];
|
|
5742
|
+
const lines = inlineSnapshot.split(/\n/g);
|
|
5743
|
+
if (lines.length <= 2)
|
|
5744
|
+
// Must be at least 3 lines.
|
|
5745
|
+
return inlineSnapshot;
|
|
5746
|
+
if (lines[0].trim() !== "" || lines.at(-1)?.trim() !== "")
|
|
5747
|
+
// If not blank first and last lines, abort.
|
|
5748
|
+
return inlineSnapshot;
|
|
5749
|
+
for (let i = 1; i < lines.length - 1; i++) if (lines[i] !== "") {
|
|
5750
|
+
if (lines[i].indexOf(indentation) !== 0)
|
|
5751
|
+
// All lines except first and last should either be blank or have the same
|
|
5752
|
+
// indent as the first line (or more). If this isn't the case we don't
|
|
5753
|
+
// want to touch the snapshot at all.
|
|
5754
|
+
return inlineSnapshot;
|
|
5755
|
+
lines[i] = lines[i].substring(indentation.length);
|
|
5756
|
+
}
|
|
5757
|
+
// Last line is a special case because it won't have the same indent as others
|
|
5758
|
+
// but may still have been given some indent to line up.
|
|
5759
|
+
lines[lines.length - 1] = "";
|
|
5760
|
+
// Return inline snapshot, now at indent 0.
|
|
5761
|
+
inlineSnapshot = lines.join("\n");
|
|
5762
|
+
return inlineSnapshot;
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
async function saveRawSnapshots(environment, snapshots) {
|
|
5766
|
+
await Promise.all(snapshots.map(async (snap) => {
|
|
5767
|
+
if (!snap.readonly) await environment.saveSnapshotFile(snap.file, snap.snapshot);
|
|
5768
|
+
}));
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
function isSameStackPosition(x, y) {
|
|
5772
|
+
return x.file === y.file && x.column === y.column && x.line === y.line;
|
|
5773
|
+
}
|
|
5774
|
+
class SnapshotState {
|
|
5775
|
+
_counters = new CounterMap();
|
|
5776
|
+
_dirty;
|
|
5777
|
+
_updateSnapshot;
|
|
5778
|
+
_snapshotData;
|
|
5779
|
+
_initialData;
|
|
5780
|
+
_inlineSnapshots;
|
|
5781
|
+
_inlineSnapshotStacks;
|
|
5782
|
+
_testIdToKeys = new DefaultMap(() => []);
|
|
5783
|
+
_rawSnapshots;
|
|
5784
|
+
_uncheckedKeys;
|
|
5785
|
+
_snapshotFormat;
|
|
5786
|
+
_environment;
|
|
5787
|
+
_fileExists;
|
|
5788
|
+
expand;
|
|
5789
|
+
// getter/setter for jest-image-snapshot compat
|
|
5790
|
+
// https://github.com/vitest-dev/vitest/issues/7322
|
|
5791
|
+
_added = new CounterMap();
|
|
5792
|
+
_matched = new CounterMap();
|
|
5793
|
+
_unmatched = new CounterMap();
|
|
5794
|
+
_updated = new CounterMap();
|
|
5795
|
+
get added() {
|
|
5796
|
+
return this._added;
|
|
5797
|
+
}
|
|
5798
|
+
set added(value) {
|
|
5799
|
+
this._added._total = value;
|
|
5800
|
+
}
|
|
5801
|
+
get matched() {
|
|
5802
|
+
return this._matched;
|
|
5803
|
+
}
|
|
5804
|
+
set matched(value) {
|
|
5805
|
+
this._matched._total = value;
|
|
5806
|
+
}
|
|
5807
|
+
get unmatched() {
|
|
5808
|
+
return this._unmatched;
|
|
5809
|
+
}
|
|
5810
|
+
set unmatched(value) {
|
|
5811
|
+
this._unmatched._total = value;
|
|
5812
|
+
}
|
|
5813
|
+
get updated() {
|
|
5814
|
+
return this._updated;
|
|
5815
|
+
}
|
|
5816
|
+
set updated(value) {
|
|
5817
|
+
this._updated._total = value;
|
|
5818
|
+
}
|
|
5819
|
+
constructor(testFilePath, snapshotPath, snapshotContent, options) {
|
|
5820
|
+
this.testFilePath = testFilePath;
|
|
5821
|
+
this.snapshotPath = snapshotPath;
|
|
5822
|
+
const { data, dirty } = getSnapshotData(snapshotContent, options);
|
|
5823
|
+
this._fileExists = snapshotContent != null;
|
|
5824
|
+
this._initialData = { ...data };
|
|
5825
|
+
this._snapshotData = { ...data };
|
|
5826
|
+
this._dirty = dirty;
|
|
5827
|
+
this._inlineSnapshots = [];
|
|
5828
|
+
this._inlineSnapshotStacks = [];
|
|
5829
|
+
this._rawSnapshots = [];
|
|
5830
|
+
this._uncheckedKeys = new Set(Object.keys(this._snapshotData));
|
|
5831
|
+
this.expand = options.expand || false;
|
|
5832
|
+
this._updateSnapshot = options.updateSnapshot;
|
|
5833
|
+
this._snapshotFormat = {
|
|
5834
|
+
printBasicPrototype: false,
|
|
5835
|
+
escapeString: false,
|
|
5836
|
+
maxOutputLength: 2 ** 27,
|
|
5837
|
+
...options.snapshotFormat
|
|
5838
|
+
};
|
|
5839
|
+
this._environment = options.snapshotEnvironment;
|
|
5840
|
+
}
|
|
5841
|
+
static async create(testFilePath, options) {
|
|
5842
|
+
const snapshotPath = await options.snapshotEnvironment.resolvePath(testFilePath);
|
|
5843
|
+
return new SnapshotState(testFilePath, snapshotPath, await options.snapshotEnvironment.readSnapshotFile(snapshotPath), options);
|
|
5844
|
+
}
|
|
5845
|
+
get snapshotUpdateState() {
|
|
5846
|
+
return this._updateSnapshot;
|
|
5847
|
+
}
|
|
5848
|
+
get environment() {
|
|
5849
|
+
return this._environment;
|
|
5850
|
+
}
|
|
5851
|
+
markSnapshotsAsCheckedForTest(testName) {
|
|
5852
|
+
this._uncheckedKeys.forEach((uncheckedKey) => {
|
|
5853
|
+
// skip snapshots with following keys
|
|
5854
|
+
// testName n
|
|
5855
|
+
// testName > xxx n (this is for toMatchSnapshot("xxx") API)
|
|
5856
|
+
if (/ \d+$| > /.test(uncheckedKey.slice(testName.length))) this._uncheckedKeys.delete(uncheckedKey);
|
|
5857
|
+
});
|
|
5858
|
+
}
|
|
5859
|
+
clearTest(testId) {
|
|
5860
|
+
// clear inline
|
|
5861
|
+
this._inlineSnapshots = this._inlineSnapshots.filter((s) => s.testId !== testId);
|
|
5862
|
+
this._inlineSnapshotStacks = this._inlineSnapshotStacks.filter((s) => s.testId !== testId);
|
|
5863
|
+
// clear file
|
|
5864
|
+
for (const key of this._testIdToKeys.get(testId)) {
|
|
5865
|
+
const name = keyToTestName(key);
|
|
5866
|
+
const count = this._counters.get(name);
|
|
5867
|
+
if (count > 0) {
|
|
5868
|
+
if (key in this._snapshotData || key in this._initialData) this._snapshotData[key] = this._initialData[key];
|
|
5869
|
+
this._counters.set(name, count - 1);
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
this._testIdToKeys.delete(testId);
|
|
5873
|
+
// clear stats
|
|
5874
|
+
this.added.delete(testId);
|
|
5875
|
+
this.updated.delete(testId);
|
|
5876
|
+
this.matched.delete(testId);
|
|
5877
|
+
this.unmatched.delete(testId);
|
|
5878
|
+
}
|
|
5879
|
+
_inferInlineSnapshotStack(stacks) {
|
|
5880
|
+
// if called inside resolves/rejects, stacktrace is different
|
|
5881
|
+
const promiseIndex = stacks.findIndex((i) => i.method.match(/__VITEST_(RESOLVES|REJECTS)__/));
|
|
5882
|
+
if (promiseIndex !== -1) return stacks[promiseIndex + 3];
|
|
5883
|
+
// support poll + inline snapshot
|
|
5884
|
+
const pollChainIndex = stacks.findIndex((i) => i.method.match(/__VITEST_POLL_CHAIN__/));
|
|
5885
|
+
if (pollChainIndex !== -1) return stacks[pollChainIndex + 1];
|
|
5886
|
+
// inline snapshot function can be named __INLINE_SNAPSHOT_OFFSET_<n>__
|
|
5887
|
+
// to specify a custom stack offset
|
|
5888
|
+
for (let i = 0; i < stacks.length; i++) {
|
|
5889
|
+
const match = stacks[i].method.match(/__INLINE_SNAPSHOT_OFFSET_(\d+)__/);
|
|
5890
|
+
if (match) return stacks[i + Number(match[1])] ?? null;
|
|
5891
|
+
}
|
|
5892
|
+
// custom matcher registered via expect.extend() — the wrapper function
|
|
5893
|
+
// in jest-extend.ts is named __VITEST_EXTEND_ASSERTION__
|
|
5894
|
+
const customMatcherIndex = stacks.findIndex((i) => i.method.includes("__VITEST_EXTEND_ASSERTION__"));
|
|
5895
|
+
if (customMatcherIndex !== -1) return stacks[customMatcherIndex + 3] ?? null;
|
|
5896
|
+
// inline snapshot function is called __INLINE_SNAPSHOT__
|
|
5897
|
+
// in integrations/snapshot/chai.ts
|
|
5898
|
+
const stackIndex = stacks.findIndex((i) => i.method.includes("__INLINE_SNAPSHOT__"));
|
|
5899
|
+
return stackIndex !== -1 ? stacks[stackIndex + 2] : null;
|
|
5900
|
+
}
|
|
5901
|
+
_addSnapshot(key, receivedSerialized, options) {
|
|
5902
|
+
this._dirty = true;
|
|
5903
|
+
if (options.stack) this._inlineSnapshots.push({
|
|
5904
|
+
...options.stack,
|
|
5905
|
+
snapshot: receivedSerialized,
|
|
5906
|
+
testId: options.testId,
|
|
5907
|
+
assertionName: options.assertionName
|
|
5908
|
+
});
|
|
5909
|
+
else if (options.rawSnapshot) this._rawSnapshots.push({
|
|
5910
|
+
...options.rawSnapshot,
|
|
5911
|
+
snapshot: receivedSerialized
|
|
5912
|
+
});
|
|
5913
|
+
else this._snapshotData[key] = receivedSerialized;
|
|
5914
|
+
}
|
|
5915
|
+
_resolveKey(testId, testName, key) {
|
|
5916
|
+
this._counters.increment(testName);
|
|
5917
|
+
const count = this._counters.get(testName);
|
|
5918
|
+
if (!key) key = testNameToKey(testName, count);
|
|
5919
|
+
this._testIdToKeys.get(testId).push(key);
|
|
5920
|
+
return {
|
|
5921
|
+
key,
|
|
5922
|
+
count
|
|
5923
|
+
};
|
|
5924
|
+
}
|
|
5925
|
+
_resolveInlineStack(options) {
|
|
5926
|
+
const { testId, snapshot, assertionName, error } = options;
|
|
5927
|
+
const stacks = parseErrorStacktrace(error, { ignoreStackEntries: [] });
|
|
5928
|
+
const _stack = this._inferInlineSnapshotStack(stacks);
|
|
5929
|
+
if (!_stack) {
|
|
5930
|
+
const message = stacks.map((s) => ` ${s.file}:${s.line}:${s.column}${s.method ? ` (${s.method})` : ""}`).join("\n");
|
|
5931
|
+
throw new Error(`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${message}`);
|
|
5932
|
+
}
|
|
5933
|
+
const stack = this.environment.processStackTrace?.(_stack) || _stack;
|
|
5934
|
+
// removing 1 column, because source map points to the wrong
|
|
5935
|
+
// location for js files, but `column-1` points to the same in both js/ts
|
|
5936
|
+
// https://github.com/vitejs/vite/issues/8657
|
|
5937
|
+
stack.column--;
|
|
5938
|
+
// reject multiple inline snapshots at the same location if snapshot is different
|
|
5939
|
+
const snapshotsWithSameStack = this._inlineSnapshotStacks.filter((s) => isSameStackPosition(s, stack));
|
|
5940
|
+
if (snapshotsWithSameStack.length > 0) {
|
|
5941
|
+
// ensure only one snapshot will be written at the same location
|
|
5942
|
+
this._inlineSnapshots = this._inlineSnapshots.filter((s) => !isSameStackPosition(s, stack));
|
|
5943
|
+
const differentSnapshot = snapshotsWithSameStack.find((s) => s.snapshot !== snapshot);
|
|
5944
|
+
if (differentSnapshot) throw Object.assign(/* @__PURE__ */ new Error(`${assertionName} with different snapshots cannot be called at the same location`), {
|
|
5945
|
+
actual: snapshot,
|
|
5946
|
+
expected: differentSnapshot.snapshot
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
this._inlineSnapshotStacks.push({
|
|
5950
|
+
...stack,
|
|
5951
|
+
testId,
|
|
5952
|
+
snapshot
|
|
5953
|
+
});
|
|
5954
|
+
return stack;
|
|
5955
|
+
}
|
|
5956
|
+
_reconcile(opts) {
|
|
5957
|
+
// These are the conditions on when to write snapshots:
|
|
5958
|
+
// * There's no snapshot file in a non-CI environment.
|
|
5959
|
+
// * There is a snapshot file and we decided to update the snapshot.
|
|
5960
|
+
// * There is a snapshot file, but it doesn't have this snapshot.
|
|
5961
|
+
// These are the conditions on when not to write snapshots:
|
|
5962
|
+
// * The update flag is set to 'none'.
|
|
5963
|
+
// * There's no snapshot file or a file without this snapshot on a CI environment.
|
|
5964
|
+
if (opts.hasSnapshot && this._updateSnapshot === "all" || (!opts.hasSnapshot || !opts.snapshotIsPersisted) && (this._updateSnapshot === "new" || this._updateSnapshot === "all")) {
|
|
5965
|
+
if (this._updateSnapshot === "all") if (!opts.pass) {
|
|
5966
|
+
if (opts.hasSnapshot) this.updated.increment(opts.testId);
|
|
5967
|
+
else this.added.increment(opts.testId);
|
|
5968
|
+
this._addSnapshot(opts.key, opts.addValue, {
|
|
5969
|
+
stack: opts.stack,
|
|
5970
|
+
testId: opts.testId,
|
|
5971
|
+
rawSnapshot: opts.rawSnapshot,
|
|
5972
|
+
assertionName: opts.assertionName
|
|
5973
|
+
});
|
|
5974
|
+
} else this.matched.increment(opts.testId);
|
|
5975
|
+
else {
|
|
5976
|
+
this._addSnapshot(opts.key, opts.addValue, {
|
|
5977
|
+
stack: opts.stack,
|
|
5978
|
+
testId: opts.testId,
|
|
5979
|
+
rawSnapshot: opts.rawSnapshot,
|
|
5980
|
+
assertionName: opts.assertionName
|
|
5981
|
+
});
|
|
5982
|
+
this.added.increment(opts.testId);
|
|
5983
|
+
}
|
|
5984
|
+
return {
|
|
5985
|
+
actual: "",
|
|
5986
|
+
count: opts.count,
|
|
5987
|
+
expected: "",
|
|
5988
|
+
key: opts.key,
|
|
5989
|
+
pass: true
|
|
5990
|
+
};
|
|
5991
|
+
} else if (!opts.pass) {
|
|
5992
|
+
this.unmatched.increment(opts.testId);
|
|
5993
|
+
return {
|
|
5994
|
+
actual: opts.actualDisplay,
|
|
5995
|
+
count: opts.count,
|
|
5996
|
+
expected: opts.expectedDisplay,
|
|
5997
|
+
key: opts.key,
|
|
5998
|
+
pass: false
|
|
5999
|
+
};
|
|
6000
|
+
} else {
|
|
6001
|
+
this.matched.increment(opts.testId);
|
|
6002
|
+
return {
|
|
6003
|
+
actual: "",
|
|
6004
|
+
count: opts.count,
|
|
6005
|
+
expected: "",
|
|
6006
|
+
key: opts.key,
|
|
6007
|
+
pass: true
|
|
6008
|
+
};
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
async save() {
|
|
6012
|
+
const hasExternalSnapshots = Object.keys(this._snapshotData).length;
|
|
6013
|
+
const hasInlineSnapshots = this._inlineSnapshots.length;
|
|
6014
|
+
const hasRawSnapshots = this._rawSnapshots.length;
|
|
6015
|
+
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots && !hasRawSnapshots;
|
|
6016
|
+
const status = {
|
|
6017
|
+
deleted: false,
|
|
6018
|
+
saved: false
|
|
6019
|
+
};
|
|
6020
|
+
if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) {
|
|
6021
|
+
if (hasExternalSnapshots) {
|
|
6022
|
+
await saveSnapshotFile(this._environment, this._snapshotData, this.snapshotPath);
|
|
6023
|
+
this._fileExists = true;
|
|
6024
|
+
}
|
|
6025
|
+
if (hasInlineSnapshots) await saveInlineSnapshots(this._environment, this._inlineSnapshots);
|
|
6026
|
+
if (hasRawSnapshots) await saveRawSnapshots(this._environment, this._rawSnapshots);
|
|
6027
|
+
status.saved = true;
|
|
6028
|
+
} else if (!hasExternalSnapshots && this._fileExists) {
|
|
6029
|
+
if (this._updateSnapshot === "all") {
|
|
6030
|
+
await this._environment.removeSnapshotFile(this.snapshotPath);
|
|
6031
|
+
this._fileExists = false;
|
|
6032
|
+
}
|
|
6033
|
+
status.deleted = true;
|
|
6034
|
+
}
|
|
6035
|
+
return status;
|
|
6036
|
+
}
|
|
6037
|
+
getUncheckedCount() {
|
|
6038
|
+
return this._uncheckedKeys.size || 0;
|
|
6039
|
+
}
|
|
6040
|
+
getUncheckedKeys() {
|
|
6041
|
+
return Array.from(this._uncheckedKeys);
|
|
6042
|
+
}
|
|
6043
|
+
removeUncheckedKeys() {
|
|
6044
|
+
if (this._updateSnapshot === "all" && this._uncheckedKeys.size) {
|
|
6045
|
+
this._dirty = true;
|
|
6046
|
+
this._uncheckedKeys.forEach((key) => delete this._snapshotData[key]);
|
|
6047
|
+
this._uncheckedKeys.clear();
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6050
|
+
probeExpectedSnapshot(options) {
|
|
6051
|
+
const count = this._counters.get(options.testName) + 1;
|
|
6052
|
+
const key = testNameToKey(options.testName, count);
|
|
6053
|
+
return {
|
|
6054
|
+
key,
|
|
6055
|
+
count,
|
|
6056
|
+
data: options?.isInline ? options.inlineSnapshot : this._snapshotData[key],
|
|
6057
|
+
markAsChecked: () => {
|
|
6058
|
+
this._counters.increment(options.testName);
|
|
6059
|
+
this._testIdToKeys.get(options.testId).push(key);
|
|
6060
|
+
this._uncheckedKeys.delete(key);
|
|
6061
|
+
}
|
|
6062
|
+
};
|
|
6063
|
+
}
|
|
6064
|
+
match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, assertionName }) {
|
|
6065
|
+
const resolved = this._resolveKey(testId, testName, key);
|
|
6066
|
+
key = resolved.key;
|
|
6067
|
+
const count = resolved.count;
|
|
6068
|
+
// Do not mark the snapshot as "checked" if the snapshot is inline and
|
|
6069
|
+
// there's an external snapshot. This way the external snapshot can be
|
|
6070
|
+
// removed with `--updateSnapshot`.
|
|
6071
|
+
if (!(isInline && this._snapshotData[key] !== void 0)) this._uncheckedKeys.delete(key);
|
|
6072
|
+
let receivedSerialized = rawSnapshot && typeof received === "string" ? received : serialize(received, void 0, this._snapshotFormat);
|
|
6073
|
+
if (!rawSnapshot) receivedSerialized = addExtraLineBreaks(receivedSerialized);
|
|
6074
|
+
if (rawSnapshot) {
|
|
6075
|
+
// normalize EOL when snapshot contains CRLF but received is LF
|
|
6076
|
+
if (rawSnapshot.content && rawSnapshot.content.match(/\r\n/) && !receivedSerialized.match(/\r\n/)) rawSnapshot.content = normalizeNewlines(rawSnapshot.content);
|
|
6077
|
+
}
|
|
6078
|
+
const expected = isInline ? inlineSnapshot : rawSnapshot ? rawSnapshot.content : this._snapshotData[key];
|
|
6079
|
+
const expectedTrimmed = rawSnapshot ? expected : expected?.trim();
|
|
6080
|
+
const pass = expectedTrimmed === (rawSnapshot ? receivedSerialized : receivedSerialized.trim());
|
|
6081
|
+
const hasSnapshot = expected !== void 0;
|
|
6082
|
+
const snapshotIsPersisted = isInline || this._fileExists || rawSnapshot && rawSnapshot.content != null;
|
|
6083
|
+
if (pass && !isInline && !rawSnapshot)
|
|
6084
|
+
// When the file is re-saved (because other snapshots changed), the JS
|
|
6085
|
+
// round-trip can lose proper escaping. Refresh in-memory data with the
|
|
6086
|
+
// freshly serialized string so the file is written correctly.
|
|
6087
|
+
// _reconcile does not write _snapshotData on pass, so this is the only
|
|
6088
|
+
// place it gets refreshed. Domain snapshots skip this because the stored
|
|
6089
|
+
// value may contain match patterns that differ from the received output.
|
|
6090
|
+
this._snapshotData[key] = receivedSerialized;
|
|
6091
|
+
const stack = isInline ? this._resolveInlineStack({
|
|
6092
|
+
testId,
|
|
6093
|
+
snapshot: receivedSerialized,
|
|
6094
|
+
assertionName: assertionName || "toMatchInlineSnapshot",
|
|
6095
|
+
error: error || /* @__PURE__ */ new Error("snapshot")
|
|
6096
|
+
}) : void 0;
|
|
6097
|
+
return this._reconcile({
|
|
6098
|
+
testId,
|
|
6099
|
+
key,
|
|
6100
|
+
count,
|
|
6101
|
+
pass,
|
|
6102
|
+
hasSnapshot,
|
|
6103
|
+
snapshotIsPersisted: !!snapshotIsPersisted,
|
|
6104
|
+
addValue: receivedSerialized,
|
|
6105
|
+
actualDisplay: rawSnapshot ? receivedSerialized : removeExtraLineBreaks(receivedSerialized),
|
|
6106
|
+
expectedDisplay: expectedTrimmed !== void 0 ? rawSnapshot ? expectedTrimmed : removeExtraLineBreaks(expectedTrimmed) : void 0,
|
|
6107
|
+
stack,
|
|
6108
|
+
rawSnapshot,
|
|
6109
|
+
assertionName
|
|
6110
|
+
});
|
|
6111
|
+
}
|
|
6112
|
+
processDomainSnapshot({ testId, received, expectedSnapshot, matchResult, isInline, error, assertionName }) {
|
|
6113
|
+
const stack = isInline ? this._resolveInlineStack({
|
|
6114
|
+
testId,
|
|
6115
|
+
snapshot: received,
|
|
6116
|
+
assertionName,
|
|
6117
|
+
error: error || /* @__PURE__ */ new Error("STACK_TRACE_ERROR")
|
|
6118
|
+
}) : void 0;
|
|
6119
|
+
const actualResolved = matchResult?.resolved ?? received;
|
|
6120
|
+
const expectedResolved = matchResult?.expected ?? expectedSnapshot.data;
|
|
6121
|
+
return this._reconcile({
|
|
6122
|
+
testId,
|
|
6123
|
+
key: expectedSnapshot.key,
|
|
6124
|
+
count: expectedSnapshot.count,
|
|
6125
|
+
pass: matchResult?.pass ?? false,
|
|
6126
|
+
hasSnapshot: expectedSnapshot.data !== void 0,
|
|
6127
|
+
snapshotIsPersisted: isInline ? true : this._fileExists,
|
|
6128
|
+
addValue: actualResolved,
|
|
6129
|
+
actualDisplay: removeExtraLineBreaks(actualResolved),
|
|
6130
|
+
expectedDisplay: expectedResolved !== void 0 ? removeExtraLineBreaks(expectedResolved) : void 0,
|
|
6131
|
+
stack,
|
|
6132
|
+
assertionName
|
|
6133
|
+
});
|
|
6134
|
+
}
|
|
6135
|
+
async pack() {
|
|
6136
|
+
const snapshot = {
|
|
6137
|
+
filepath: this.testFilePath,
|
|
6138
|
+
added: 0,
|
|
6139
|
+
fileDeleted: false,
|
|
6140
|
+
matched: 0,
|
|
6141
|
+
unchecked: 0,
|
|
6142
|
+
uncheckedKeys: [],
|
|
6143
|
+
unmatched: 0,
|
|
6144
|
+
updated: 0
|
|
6145
|
+
};
|
|
6146
|
+
const uncheckedCount = this.getUncheckedCount();
|
|
6147
|
+
const uncheckedKeys = this.getUncheckedKeys();
|
|
6148
|
+
if (uncheckedCount) this.removeUncheckedKeys();
|
|
6149
|
+
const status = await this.save();
|
|
6150
|
+
snapshot.fileDeleted = status.deleted;
|
|
6151
|
+
snapshot.added = this.added.total();
|
|
6152
|
+
snapshot.matched = this.matched.total();
|
|
6153
|
+
snapshot.unmatched = this.unmatched.total();
|
|
6154
|
+
snapshot.updated = this.updated.total();
|
|
6155
|
+
snapshot.unchecked = !status.deleted ? uncheckedCount : 0;
|
|
6156
|
+
snapshot.uncheckedKeys = Array.from(uncheckedKeys);
|
|
6157
|
+
return snapshot;
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6161
|
+
function createMismatchError(message, expand, actual, expected) {
|
|
6162
|
+
const error = new Error(message);
|
|
6163
|
+
Object.defineProperty(error, "actual", {
|
|
6164
|
+
value: actual,
|
|
6165
|
+
enumerable: true,
|
|
6166
|
+
configurable: true,
|
|
6167
|
+
writable: true
|
|
6168
|
+
});
|
|
6169
|
+
Object.defineProperty(error, "expected", {
|
|
6170
|
+
value: expected,
|
|
6171
|
+
enumerable: true,
|
|
6172
|
+
configurable: true,
|
|
6173
|
+
writable: true
|
|
6174
|
+
});
|
|
6175
|
+
Object.defineProperty(error, "diffOptions", { value: { expand } });
|
|
6176
|
+
return error;
|
|
6177
|
+
}
|
|
6178
|
+
class SnapshotClient {
|
|
6179
|
+
snapshotStateMap = /* @__PURE__ */ new Map();
|
|
6180
|
+
constructor(options = {}) {
|
|
6181
|
+
this.options = options;
|
|
6182
|
+
}
|
|
6183
|
+
async setup(filepath, options) {
|
|
6184
|
+
if (this.snapshotStateMap.has(filepath)) return;
|
|
6185
|
+
this.snapshotStateMap.set(filepath, await SnapshotState.create(filepath, options));
|
|
6186
|
+
}
|
|
6187
|
+
async finish(filepath) {
|
|
6188
|
+
const result = await this.getSnapshotState(filepath).pack();
|
|
6189
|
+
this.snapshotStateMap.delete(filepath);
|
|
6190
|
+
return result;
|
|
6191
|
+
}
|
|
6192
|
+
skipTest(filepath, testName) {
|
|
6193
|
+
this.getSnapshotState(filepath).markSnapshotsAsCheckedForTest(testName);
|
|
6194
|
+
}
|
|
6195
|
+
clearTest(filepath, testId) {
|
|
6196
|
+
this.getSnapshotState(filepath).clearTest(testId);
|
|
6197
|
+
}
|
|
6198
|
+
getSnapshotState(filepath) {
|
|
6199
|
+
const state = this.snapshotStateMap.get(filepath);
|
|
6200
|
+
if (!state) throw new Error(`The snapshot state for '${filepath}' is not found. Did you call 'SnapshotClient.setup()'?`);
|
|
6201
|
+
return state;
|
|
6202
|
+
}
|
|
6203
|
+
match(options) {
|
|
6204
|
+
const { filepath, name, testId = name, message, isInline = false, properties, inlineSnapshot, error, errorMessage, rawSnapshot, assertionName } = options;
|
|
6205
|
+
let { received } = options;
|
|
6206
|
+
if (!filepath) throw new Error("Snapshot cannot be used outside of test");
|
|
6207
|
+
const snapshotState = this.getSnapshotState(filepath);
|
|
6208
|
+
const testName = [name, ...message ? [message] : []].join(" > ");
|
|
6209
|
+
// Probe first so we can mark as checked even on early return
|
|
6210
|
+
const expectedSnapshot = snapshotState.probeExpectedSnapshot({
|
|
6211
|
+
testName,
|
|
6212
|
+
testId,
|
|
6213
|
+
isInline,
|
|
6214
|
+
inlineSnapshot
|
|
6215
|
+
});
|
|
6216
|
+
if (typeof properties === "object") {
|
|
6217
|
+
if (typeof received !== "object" || !received) {
|
|
6218
|
+
expectedSnapshot.markAsChecked();
|
|
6219
|
+
throw new Error("Received value must be an object when the matcher has properties");
|
|
6220
|
+
}
|
|
6221
|
+
let propertiesPass;
|
|
6222
|
+
try {
|
|
6223
|
+
propertiesPass = this.options.isEqual?.(received, properties) ?? false;
|
|
6224
|
+
} catch (err) {
|
|
6225
|
+
expectedSnapshot.markAsChecked();
|
|
6226
|
+
throw err;
|
|
6227
|
+
}
|
|
6228
|
+
if (!propertiesPass) {
|
|
6229
|
+
expectedSnapshot.markAsChecked();
|
|
6230
|
+
return {
|
|
6231
|
+
pass: false,
|
|
6232
|
+
message: () => errorMessage || "Snapshot properties mismatched",
|
|
6233
|
+
actual: received,
|
|
6234
|
+
expected: properties
|
|
6235
|
+
};
|
|
6236
|
+
}
|
|
6237
|
+
received = deepMergeSnapshot(received, properties);
|
|
6238
|
+
}
|
|
6239
|
+
const { actual, expected, key, pass } = snapshotState.match({
|
|
6240
|
+
testId,
|
|
6241
|
+
testName,
|
|
6242
|
+
received,
|
|
6243
|
+
isInline,
|
|
6244
|
+
error,
|
|
6245
|
+
inlineSnapshot,
|
|
6246
|
+
rawSnapshot,
|
|
6247
|
+
assertionName
|
|
6248
|
+
});
|
|
6249
|
+
return {
|
|
6250
|
+
pass,
|
|
6251
|
+
message: () => `Snapshot \`${key || "unknown"}\` mismatched`,
|
|
6252
|
+
actual: rawSnapshot ? actual : actual?.trim(),
|
|
6253
|
+
expected: rawSnapshot ? expected : expected?.trim()
|
|
6254
|
+
};
|
|
6255
|
+
}
|
|
6256
|
+
assert(options) {
|
|
6257
|
+
const result = this.match(options);
|
|
6258
|
+
if (!result.pass) {
|
|
6259
|
+
const snapshotState = this.getSnapshotState(options.filepath);
|
|
6260
|
+
throw createMismatchError(result.message(), snapshotState.expand, result.actual, result.expected);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
matchDomain(options) {
|
|
6264
|
+
const { received, filepath, name, testId = name, message, adapter, isInline = false, inlineSnapshot, error } = options;
|
|
6265
|
+
if (!filepath) throw new Error("Snapshot cannot be used outside of test");
|
|
6266
|
+
const captured = adapter.capture(received);
|
|
6267
|
+
const rendered = adapter.render(captured);
|
|
6268
|
+
const snapshotState = this.getSnapshotState(filepath);
|
|
6269
|
+
const testName = [name, ...message ? [message] : []].join(" > ");
|
|
6270
|
+
const expectedSnapshot = snapshotState.probeExpectedSnapshot({
|
|
6271
|
+
testName,
|
|
6272
|
+
testId,
|
|
6273
|
+
isInline,
|
|
6274
|
+
inlineSnapshot
|
|
6275
|
+
});
|
|
6276
|
+
expectedSnapshot.markAsChecked();
|
|
6277
|
+
const matchResult = expectedSnapshot.data !== void 0 ? adapter.match(captured, adapter.parseExpected(expectedSnapshot.data)) : void 0;
|
|
6278
|
+
const { actual, expected, key, pass } = snapshotState.processDomainSnapshot({
|
|
6279
|
+
testId,
|
|
6280
|
+
received: rendered,
|
|
6281
|
+
expectedSnapshot,
|
|
6282
|
+
matchResult,
|
|
6283
|
+
isInline,
|
|
6284
|
+
error,
|
|
6285
|
+
assertionName: options.assertionName
|
|
6286
|
+
});
|
|
6287
|
+
return {
|
|
6288
|
+
pass,
|
|
6289
|
+
message: () => `Snapshot \`${key}\` mismatched`,
|
|
6290
|
+
actual: actual?.trim(),
|
|
6291
|
+
expected: expected?.trim()
|
|
6292
|
+
};
|
|
6293
|
+
}
|
|
6294
|
+
async pollMatchDomain(options) {
|
|
6295
|
+
const { poll, filepath, name, testId = name, message, adapter, isInline = false, inlineSnapshot, error, timeout = 1e3, interval = 50 } = options;
|
|
6296
|
+
if (!filepath) throw new Error("Snapshot cannot be used outside of test");
|
|
6297
|
+
const snapshotState = this.getSnapshotState(filepath);
|
|
6298
|
+
const testName = [name, ...message ? [message] : []].join(" > ");
|
|
6299
|
+
const expectedSnapshot = snapshotState.probeExpectedSnapshot({
|
|
6300
|
+
testName,
|
|
6301
|
+
testId,
|
|
6302
|
+
isInline,
|
|
6303
|
+
inlineSnapshot
|
|
6304
|
+
});
|
|
6305
|
+
const reference = expectedSnapshot.data !== void 0 && snapshotState.snapshotUpdateState !== "all" ? adapter.parseExpected(expectedSnapshot.data) : void 0;
|
|
6306
|
+
const stableResult = await getStableSnapshot({
|
|
6307
|
+
adapter,
|
|
6308
|
+
poll,
|
|
6309
|
+
interval,
|
|
6310
|
+
timedOut: timeout > 0 ? new Promise((r) => setTimeout(r, timeout)) : void 0,
|
|
6311
|
+
match: reference ? (captured) => adapter.match(captured, reference).pass : void 0
|
|
6312
|
+
});
|
|
6313
|
+
expectedSnapshot.markAsChecked();
|
|
6314
|
+
if (stableResult?.rendered === void 0) {
|
|
6315
|
+
// the original caller `expect.poll` later manipulates error via `throwWithCause`,
|
|
6316
|
+
// so here we can directly throw `lastPollError` if exists.
|
|
6317
|
+
if (stableResult?.lastPollError) throw stableResult.lastPollError;
|
|
6318
|
+
return {
|
|
6319
|
+
pass: false,
|
|
6320
|
+
message: () => `poll() did not produce a stable snapshot within the timeout`
|
|
6321
|
+
};
|
|
6322
|
+
}
|
|
6323
|
+
// TODO: should `all` mode ignore parse error?
|
|
6324
|
+
// Sielently hiding the error and creating snaphsot full scratch isn't good either.
|
|
6325
|
+
// Users can fix or purge the broken snapshot manually and that decision affects how domain snapshot gets updated.
|
|
6326
|
+
const matchResult = expectedSnapshot.data !== void 0 ? adapter.match(stableResult.captured, adapter.parseExpected(expectedSnapshot.data)) : void 0;
|
|
6327
|
+
const { actual, expected, key, pass } = snapshotState.processDomainSnapshot({
|
|
6328
|
+
testId,
|
|
6329
|
+
received: stableResult.rendered,
|
|
6330
|
+
expectedSnapshot,
|
|
6331
|
+
matchResult,
|
|
6332
|
+
isInline,
|
|
6333
|
+
error,
|
|
6334
|
+
assertionName: options.assertionName
|
|
6335
|
+
});
|
|
6336
|
+
return {
|
|
6337
|
+
pass,
|
|
6338
|
+
message: () => `Snapshot \`${key}\` mismatched`,
|
|
6339
|
+
actual: actual?.trim(),
|
|
6340
|
+
expected: expected?.trim()
|
|
6341
|
+
};
|
|
6342
|
+
}
|
|
6343
|
+
async assertRaw(options) {
|
|
6344
|
+
if (!options.rawSnapshot) throw new Error("Raw snapshot is required");
|
|
6345
|
+
const { filepath, rawSnapshot } = options;
|
|
6346
|
+
if (rawSnapshot.content == null) {
|
|
6347
|
+
if (!filepath) throw new Error("Snapshot cannot be used outside of test");
|
|
6348
|
+
const snapshotState = this.getSnapshotState(filepath);
|
|
6349
|
+
// save the filepath, so it don't lose even if the await make it out-of-context
|
|
6350
|
+
options.filepath ||= filepath;
|
|
6351
|
+
// resolve and read the raw snapshot file
|
|
6352
|
+
rawSnapshot.file = await snapshotState.environment.resolveRawPath(filepath, rawSnapshot.file);
|
|
6353
|
+
rawSnapshot.content = await snapshotState.environment.readSnapshotFile(rawSnapshot.file) ?? void 0;
|
|
6354
|
+
}
|
|
6355
|
+
return this.assert(options);
|
|
6356
|
+
}
|
|
6357
|
+
clear() {
|
|
6358
|
+
this.snapshotStateMap.clear();
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
/**
|
|
6362
|
+
* Polls repeatedly until the value reaches a stable state.
|
|
6363
|
+
*
|
|
6364
|
+
* Compares consecutive rendered outputs from the current session —
|
|
6365
|
+
* when two consecutive polls produce the same rendered string,
|
|
6366
|
+
* the value is considered stable.
|
|
6367
|
+
*
|
|
6368
|
+
* Every `await` (poll call, interval delay) races against `timedOut`
|
|
6369
|
+
* so that hanging polls and delays are interrupted.
|
|
6370
|
+
*/
|
|
6371
|
+
async function getStableSnapshot({ adapter, poll, interval, timedOut, match }) {
|
|
6372
|
+
let lastRendered;
|
|
6373
|
+
let lastPollError;
|
|
6374
|
+
let lastStable;
|
|
6375
|
+
while (true) {
|
|
6376
|
+
try {
|
|
6377
|
+
const pollResult = await raceWith(Promise.resolve(poll()), timedOut);
|
|
6378
|
+
if (!pollResult.ok) break;
|
|
6379
|
+
const captured = adapter.capture(pollResult.value);
|
|
6380
|
+
const rendered = adapter.render(captured);
|
|
6381
|
+
if (lastRendered !== void 0 && rendered === lastRendered) {
|
|
6382
|
+
lastStable = {
|
|
6383
|
+
captured,
|
|
6384
|
+
rendered
|
|
6385
|
+
};
|
|
6386
|
+
if (!match || match(captured)) break;
|
|
6387
|
+
} else {
|
|
6388
|
+
lastRendered = rendered;
|
|
6389
|
+
lastStable = void 0;
|
|
6390
|
+
}
|
|
6391
|
+
} catch (pollError) {
|
|
6392
|
+
// poll() threw — reset stability baseline and retry
|
|
6393
|
+
lastRendered = void 0;
|
|
6394
|
+
lastStable = void 0;
|
|
6395
|
+
lastPollError = pollError;
|
|
6396
|
+
}
|
|
6397
|
+
if (!(await raceWith(new Promise((r) => setTimeout(r, interval)), timedOut)).ok) break;
|
|
6398
|
+
}
|
|
6399
|
+
return {
|
|
6400
|
+
...lastStable,
|
|
6401
|
+
lastPollError
|
|
6402
|
+
};
|
|
6403
|
+
}
|
|
6404
|
+
/** Type-safe `Promise.race` — tells you which promise won. */
|
|
6405
|
+
function raceWith(promise, other) {
|
|
6406
|
+
const left = promise.then((value) => ({
|
|
6407
|
+
ok: true,
|
|
6408
|
+
value
|
|
6409
|
+
}));
|
|
6410
|
+
if (!other) return left;
|
|
6411
|
+
return Promise.race([left, other.then((value) => ({
|
|
6412
|
+
ok: false,
|
|
6413
|
+
value
|
|
6414
|
+
}))]);
|
|
6415
|
+
}
|
|
6416
|
+
|
|
3841
6417
|
let _client;
|
|
3842
6418
|
function getSnapshotClient() {
|
|
3843
6419
|
if (!_client) _client = new SnapshotClient({ isEqual: (received, expected) => {
|
|
@@ -3873,6 +6449,7 @@ function getAssertionName(assertion) {
|
|
|
3873
6449
|
function getTest(obj) {
|
|
3874
6450
|
const test = chai.util.flag(obj, "vitest-test");
|
|
3875
6451
|
if (!test) throw new Error(`'${getAssertionName(obj)}' cannot be used without test context`);
|
|
6452
|
+
if (test.fails) throw new TestSyntaxError(`'${getAssertionName(obj)}' cannot be used with 'test.fails'`);
|
|
3876
6453
|
return test;
|
|
3877
6454
|
}
|
|
3878
6455
|
function validateAssertion(assertion) {
|
|
@@ -3936,7 +6513,7 @@ function toMatchDomainSnapshotImpl(opts) {
|
|
|
3936
6513
|
const assertionName = getAssertionName(assertion);
|
|
3937
6514
|
const test = getTest(assertion);
|
|
3938
6515
|
let { inlineSnapshot } = opts;
|
|
3939
|
-
if (inlineSnapshot) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot);
|
|
6516
|
+
if (inlineSnapshot !== void 0) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot);
|
|
3940
6517
|
const pollFn = chai.util.flag(assertion, "_poll.fn");
|
|
3941
6518
|
if (pollFn) return getSnapshotClient().pollMatchDomain({
|
|
3942
6519
|
poll: pollFn,
|
|
@@ -4165,6 +6742,40 @@ function inject(key) {
|
|
|
4165
6742
|
return getWorkerState().providedContext[key];
|
|
4166
6743
|
}
|
|
4167
6744
|
|
|
6745
|
+
const benchFns = /* @__PURE__ */ new WeakMap();
|
|
6746
|
+
const benchOptsMap = /* @__PURE__ */ new WeakMap();
|
|
6747
|
+
function getBenchOptions(key) {
|
|
6748
|
+
return benchOptsMap.get(key);
|
|
6749
|
+
}
|
|
6750
|
+
function getBenchFn(key) {
|
|
6751
|
+
return benchFns.get(key);
|
|
6752
|
+
}
|
|
6753
|
+
const bench = createBenchmark(function(name, fn = noop, options = {}) {
|
|
6754
|
+
if (getWorkerState().config.mode !== "benchmark") throw new Error("`bench()` is only available in benchmark mode.");
|
|
6755
|
+
const task = getCurrentSuite().task(formatName(name), {
|
|
6756
|
+
...this,
|
|
6757
|
+
meta: { benchmark: true }
|
|
6758
|
+
});
|
|
6759
|
+
benchFns.set(task, fn);
|
|
6760
|
+
benchOptsMap.set(task, options);
|
|
6761
|
+
// vitest runner sets mode to `todo` if handler is not passed down
|
|
6762
|
+
// but we store handler separately
|
|
6763
|
+
if (!this.todo && task.mode === "todo") task.mode = "run";
|
|
6764
|
+
});
|
|
6765
|
+
function createBenchmark(fn) {
|
|
6766
|
+
const benchmark = createChainable([
|
|
6767
|
+
"skip",
|
|
6768
|
+
"only",
|
|
6769
|
+
"todo"
|
|
6770
|
+
], fn);
|
|
6771
|
+
benchmark.skipIf = (condition) => condition ? benchmark.skip : benchmark;
|
|
6772
|
+
benchmark.runIf = (condition) => condition ? benchmark : benchmark.skip;
|
|
6773
|
+
return benchmark;
|
|
6774
|
+
}
|
|
6775
|
+
function formatName(name) {
|
|
6776
|
+
return typeof name === "string" ? name : typeof name === "function" ? name.name || "<anonymous>" : String(name);
|
|
6777
|
+
}
|
|
6778
|
+
|
|
4168
6779
|
function createBenchmarkResult(name) {
|
|
4169
6780
|
return {
|
|
4170
6781
|
name,
|
|
@@ -4427,4 +7038,38 @@ function clearModuleMocks(config) {
|
|
|
4427
7038
|
if (unstubGlobals) vi.unstubAllGlobals();
|
|
4428
7039
|
}
|
|
4429
7040
|
|
|
4430
|
-
|
|
7041
|
+
const assertType = function assertType() {};
|
|
7042
|
+
|
|
7043
|
+
var index = /*#__PURE__*/Object.freeze({
|
|
7044
|
+
__proto__: null,
|
|
7045
|
+
BenchmarkRunner: NodeBenchmarkRunner,
|
|
7046
|
+
EvaluatedModules: VitestEvaluatedModules,
|
|
7047
|
+
Snapshots: Snapshots,
|
|
7048
|
+
TestRunner: TestRunner,
|
|
7049
|
+
afterAll: afterAll,
|
|
7050
|
+
afterEach: afterEach,
|
|
7051
|
+
aroundAll: aroundAll,
|
|
7052
|
+
aroundEach: aroundEach,
|
|
7053
|
+
assert: assert,
|
|
7054
|
+
assertType: assertType,
|
|
7055
|
+
beforeAll: beforeAll,
|
|
7056
|
+
beforeEach: beforeEach,
|
|
7057
|
+
bench: bench,
|
|
7058
|
+
chai: chai,
|
|
7059
|
+
createExpect: createExpect,
|
|
7060
|
+
describe: describe,
|
|
7061
|
+
expect: globalExpect,
|
|
7062
|
+
expectTypeOf: expectTypeOf,
|
|
7063
|
+
inject: inject,
|
|
7064
|
+
it: it,
|
|
7065
|
+
onTestFailed: onTestFailed,
|
|
7066
|
+
onTestFinished: onTestFinished,
|
|
7067
|
+
recordArtifact: recordArtifact,
|
|
7068
|
+
should: should,
|
|
7069
|
+
suite: suite,
|
|
7070
|
+
test: test,
|
|
7071
|
+
vi: vi,
|
|
7072
|
+
vitest: vitest
|
|
7073
|
+
});
|
|
7074
|
+
|
|
7075
|
+
export { NodeBenchmarkRunner as N, Snapshots as S, TestRunner as T, assert as a, assertType as b, bench as c, createExpect as d, inject as e, vitest as f, globalExpect as g, index as i, should as s, vi as v };
|