vitest-auto-spy 4.2.0 → 4.3.0
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/AGENTS.md +89 -2
- package/README.md +135 -6
- package/dist/angular.d.ts +2 -2
- package/dist/angular.js +276 -7
- package/dist/bun-angular.d.ts +3 -3
- package/dist/bun-angular.js +6 -6
- package/dist/bun.d.ts +72 -2
- package/dist/bun.js +4 -4
- package/dist/{chunk-DSIYS6Y5.js → chunk-D5LDT3W4.js} +1 -1
- package/dist/{chunk-2ZG27FVL.js → chunk-EMASXQDZ.js} +1 -1
- package/dist/{chunk-4KK3ES4P.js → chunk-FFKN6QKI.js} +22 -0
- package/dist/{chunk-4KCYFAJX.js → chunk-FYFF67EN.js} +23 -7
- package/dist/{chunk-5URFSYDT.js → chunk-GAY6LNAM.js} +300 -6
- package/dist/{chunk-WQZ2RKS7.js → chunk-KYAJCFUA.js} +1 -1
- package/dist/{chunk-KEKKEIRG.js → chunk-Z4VRMCW2.js} +1 -1
- package/dist/cli.js +606 -16
- package/dist/console.js +2 -2
- package/dist/diagnostics.d.ts +31 -1
- package/dist/diagnostics.js +350 -1
- package/dist/{expect-emission-BDuFuqTb.d.ts → expect-emission-Dy5CvsF2.d.ts} +27 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +444 -111
- package/dist/jasmine.js +4 -4
- package/dist/nestjs.js +5 -5
- package/dist/node.cjs +272 -111
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +2 -2
- package/dist/node.js +17 -5
- package/dist/react.d.ts +2 -2
- package/dist/react.js +5 -5
- package/dist/setup.js +4 -4
- package/dist/shared-state.js +4 -275
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +5 -5
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +6 -6
- package/dist/{zoneless-DGwkbWKm.d.ts → zoneless-LsBDcSNZ.d.ts} +1 -1
- package/package.json +6 -2
- package/skills/vitest-auto-spy/SKILL.md +4 -1
package/dist/console.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { vitestMockAdapter } from './chunk-
|
|
1
|
+
import { vitestMockAdapter } from './chunk-KYAJCFUA.js';
|
|
2
2
|
import './chunk-YHDBRBGU.js';
|
|
3
|
-
import { createFunctionSpy } from './chunk-
|
|
3
|
+
import { createFunctionSpy } from './chunk-FFKN6QKI.js';
|
|
4
4
|
import './chunk-PC3FAZT3.js';
|
|
5
5
|
import './chunk-XRVVZXOM.js';
|
|
6
6
|
import './chunk-B45HLBA4.js';
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -90,4 +90,34 @@ declare function formatTestRunComparison(comparison: TestRunComparison): string;
|
|
|
90
90
|
*/
|
|
91
91
|
declare function diffByField(actual: readonly unknown[], expected: readonly unknown[]): string | undefined;
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Explain what a double is configured to answer and what it was actually asked.
|
|
95
|
+
*
|
|
96
|
+
* With no `method`, every spied member that the double exposes is reported; with one, only that
|
|
97
|
+
* member. The result is a report to print — `console.log(explainSpy(users))` — not something to
|
|
98
|
+
* assert on.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const users = createSpyFromClass(UserService);
|
|
103
|
+
* users.load.calledWith(1).resolveWith('ok');
|
|
104
|
+
*
|
|
105
|
+
* await users.load(2);
|
|
106
|
+
*
|
|
107
|
+
* console.log(explainSpy(users, 'load'));
|
|
108
|
+
* // [vitest-auto-spy] explainSpy
|
|
109
|
+
* //
|
|
110
|
+
* // load — 1 call, 1 configured, none matched
|
|
111
|
+
* // configured:
|
|
112
|
+
* // #1 calledWith(1)
|
|
113
|
+
* // calls:
|
|
114
|
+
* // #1 load(2) -> no configured arguments matched; the default value was used
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @param spy A double, or a single function spy off one.
|
|
118
|
+
* @param method Restrict the report to one member.
|
|
119
|
+
* @returns A human-readable report; never throws, whatever it was handed.
|
|
120
|
+
*/
|
|
121
|
+
declare function explainSpy(spy: object, method?: string): string;
|
|
122
|
+
|
|
123
|
+
export { type TestRunComparison, type TestRunReport, type TestRunSummary, compareTestRuns, diffByField, explainSpy, formatTestRunComparison, summarizeTestRun };
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getMockAdapter } from './shared-state.js';
|
|
2
|
+
|
|
1
3
|
// src/lib/compare-runs.ts
|
|
2
4
|
function shorten(file, root) {
|
|
3
5
|
if (root === void 0) {
|
|
@@ -123,6 +125,9 @@ function serializePrimitive(value) {
|
|
|
123
125
|
}
|
|
124
126
|
return String(value);
|
|
125
127
|
}
|
|
128
|
+
function isDeepValue(value) {
|
|
129
|
+
return typeof value === "object" && value !== null;
|
|
130
|
+
}
|
|
126
131
|
function serializeInContext(value, context) {
|
|
127
132
|
if (typeof value === "object" && value !== null) {
|
|
128
133
|
return serializeObject(value, context);
|
|
@@ -199,4 +204,348 @@ function diffByField(actual, expected) {
|
|
|
199
204
|
return [`${affected.size} of ${actual.length} elements differ.`, ...lines].join("\n");
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
|
|
207
|
+
// src/lib/args-map.ts
|
|
208
|
+
function isSinglePrimitiveArgs(args) {
|
|
209
|
+
if (args.length !== 1) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const [argument] = args;
|
|
213
|
+
const kind = typeof argument;
|
|
214
|
+
if (kind === "object" || kind === "function" || kind === "symbol") {
|
|
215
|
+
return argument === null;
|
|
216
|
+
}
|
|
217
|
+
return !Object.is(argument, -0);
|
|
218
|
+
}
|
|
219
|
+
function isAsymmetricMatcher(value) {
|
|
220
|
+
return typeof value === "object" && value !== null && "asymmetricMatch" in value && typeof value.asymmetricMatch === "function";
|
|
221
|
+
}
|
|
222
|
+
function hasAsymmetricMatcher(args) {
|
|
223
|
+
return args.some(isAsymmetricMatcher);
|
|
224
|
+
}
|
|
225
|
+
var ASYMMETRIC_MATCHER_BRAND = /* @__PURE__ */ Symbol.for("jest.asymmetricMatcher");
|
|
226
|
+
function isSameMatcher(configArg, candidateArg) {
|
|
227
|
+
if (configArg === candidateArg) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
if (!isAsymmetricMatcher(candidateArg) || !isBrandedMatcher(configArg) || !isBrandedMatcher(candidateArg)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
if (Object.getPrototypeOf(configArg) !== Object.getPrototypeOf(candidateArg)) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return serializeValue(configArg) === serializeValue(candidateArg);
|
|
237
|
+
}
|
|
238
|
+
function isBrandedMatcher(matcher) {
|
|
239
|
+
return matcher.$$typeof === ASYMMETRIC_MATCHER_BRAND;
|
|
240
|
+
}
|
|
241
|
+
var ArgsMap = class {
|
|
242
|
+
// Prototype-less so a `'__proto__'` (or `'constructor'`) serialized key is a
|
|
243
|
+
// plain own property, never walking or polluting the object prototype chain.
|
|
244
|
+
#map = /* @__PURE__ */ Object.create(null);
|
|
245
|
+
#matcherConfigs = [];
|
|
246
|
+
// Argument counts present in the exact map. A call with a count nobody configured cannot be in
|
|
247
|
+
// that map — two arg lists of different lengths never serialize to the same string — so the
|
|
248
|
+
// serialization can be skipped outright. That matters because the map is consulted on *every*
|
|
249
|
+
// invocation of a spy that has any `calledWith` config: without this, `service.load(component)`
|
|
250
|
+
// on a spy configured with `calledWith(1)` walks and stringifies the whole component graph to
|
|
251
|
+
// build a key that provably cannot match, and throws it away one line later.
|
|
252
|
+
#arities = /* @__PURE__ */ new Set();
|
|
253
|
+
/**
|
|
254
|
+
* The configs of exactly one primitive argument, keyed by the argument itself.
|
|
255
|
+
*
|
|
256
|
+
* `calledWith(1)` / `calledWith('id')` is what nearly every spec writes, and it is looked up on
|
|
257
|
+
* **every call** of that spy. Through the string map that costs a rendered key per call — an array
|
|
258
|
+
* from `map`, a string per argument, a joined string, then a hash — for a lookup that a `Map` can
|
|
259
|
+
* do on the value with no allocation at all. The configs live in both maps: the string map still
|
|
260
|
+
* owns `configured()` and the arity set, so nothing about the failure messages or the miss check
|
|
261
|
+
* changes.
|
|
262
|
+
*
|
|
263
|
+
* Only shapes where the two agree go in here. A symbol renders by description, so two distinct
|
|
264
|
+
* symbols that share one are the same key in the string map and different keys in a `Map`; `-0`
|
|
265
|
+
* renders apart from `0` and is the same key under `SameValueZero`. Both stay on the string path.
|
|
266
|
+
*/
|
|
267
|
+
#exactSinglePrimitive = /* @__PURE__ */ new Map();
|
|
268
|
+
set(key, value) {
|
|
269
|
+
if (Array.isArray(key) && hasAsymmetricMatcher(key)) {
|
|
270
|
+
this.#setMatcherConfig(this.#buildMatcherConfig(key, value));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (Array.isArray(key)) {
|
|
274
|
+
this.#arities.add(key.length);
|
|
275
|
+
if (isSinglePrimitiveArgs(key)) {
|
|
276
|
+
this.#exactSinglePrimitive.set(key[0], value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.#map[this.#serialize(key)] = value;
|
|
280
|
+
}
|
|
281
|
+
get(key) {
|
|
282
|
+
if (Array.isArray(key) && !this.#arities.has(key.length)) {
|
|
283
|
+
return this.#findByMatcher(key);
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(key) && isSinglePrimitiveArgs(key)) {
|
|
286
|
+
const hit = this.#exactSinglePrimitive.get(key[0]);
|
|
287
|
+
return hit === void 0 ? this.#findByMatcher(key) : hit;
|
|
288
|
+
}
|
|
289
|
+
const serialized = this.#serialize(key);
|
|
290
|
+
if (serialized in this.#map) {
|
|
291
|
+
return this.#map[serialized];
|
|
292
|
+
}
|
|
293
|
+
return this.#findByMatcher(key);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Every configured argument list, rendered the way a lookup key is — the *wanted* half of a
|
|
297
|
+
* `mustBeCalledWith` failure.
|
|
298
|
+
*
|
|
299
|
+
* Nothing is rendered here: the exact configs are keyed by their own serialization, and an
|
|
300
|
+
* asymmetric config carries the description built when it was registered. A failure message is
|
|
301
|
+
* assembling text it already has.
|
|
302
|
+
*/
|
|
303
|
+
configured() {
|
|
304
|
+
const asymmetric = this.#matcherConfigs.map((config) => `[${config.described.join(",")}]`);
|
|
305
|
+
return [...Object.keys(this.#map), ...asymmetric];
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Every configured argument list as a {@link ConfiguredEntry} — the same lists {@link configured}
|
|
309
|
+
* returns, each carrying its own position and its own predicate.
|
|
310
|
+
*
|
|
311
|
+
* The order is the order a lookup consults them (exact configs first, then the asymmetric ones in
|
|
312
|
+
* registration order), so the first entry whose `matches` holds is the config `get` would have
|
|
313
|
+
* answered from. That is what lets a reader be told "call 3 hit config 2" instead of a list of
|
|
314
|
+
* configs and a list of calls with nothing joining them.
|
|
315
|
+
*/
|
|
316
|
+
configuredEntries() {
|
|
317
|
+
const exact = Object.keys(this.#map).map((key, position) => ({
|
|
318
|
+
index: position + 1,
|
|
319
|
+
args: key,
|
|
320
|
+
matches: (actualArgs) => this.#serialize(actualArgs) === key
|
|
321
|
+
}));
|
|
322
|
+
const asymmetric = this.#matcherConfigs.map((config, position) => ({
|
|
323
|
+
index: exact.length + position + 1,
|
|
324
|
+
args: `[${config.described.join(",")}]`,
|
|
325
|
+
matches: (actualArgs) => this.#argsMatch(config, actualArgs)
|
|
326
|
+
}));
|
|
327
|
+
return [...exact, ...asymmetric];
|
|
328
|
+
}
|
|
329
|
+
/** Render one asymmetric config once, at registration time. See {@link MatcherConfig}. */
|
|
330
|
+
#buildMatcherConfig(key, value) {
|
|
331
|
+
const serialized = [];
|
|
332
|
+
const described = [];
|
|
333
|
+
for (const arg of key) {
|
|
334
|
+
if (isAsymmetricMatcher(arg)) {
|
|
335
|
+
serialized.push(void 0);
|
|
336
|
+
described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
|
|
337
|
+
} else {
|
|
338
|
+
const rendered = this.#serialize([arg]);
|
|
339
|
+
serialized.push(rendered);
|
|
340
|
+
described.push(rendered.slice(1, -1));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { args: key, serialized, described, value };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Register an asymmetric config, replacing an equivalent one in place.
|
|
347
|
+
*
|
|
348
|
+
* Re-registering args is how a test overrides an earlier answer, and on the exact map that falls
|
|
349
|
+
* out for free — the second `set` writes the same key. The matcher list has to do it by hand:
|
|
350
|
+
* lookup takes the first predicate that matches, so an appended duplicate would sit behind the
|
|
351
|
+
* config it was meant to replace and never be reached, and a `beforeEach` that reconfigures the
|
|
352
|
+
* same spy would grow the list on every test. Replacing in place also keeps the registration
|
|
353
|
+
* order that decides which of two *overlapping* configs wins.
|
|
354
|
+
*/
|
|
355
|
+
#setMatcherConfig(config) {
|
|
356
|
+
const index = this.#matcherConfigs.findIndex((existing) => this.#sameConfigArgs(existing, config));
|
|
357
|
+
if (index === -1) {
|
|
358
|
+
this.#matcherConfigs.push(config);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.#matcherConfigs[index] = config;
|
|
362
|
+
}
|
|
363
|
+
/** Whether two configs were registered for the same argument list, matcher positions included. */
|
|
364
|
+
#sameConfigArgs(existing, candidate) {
|
|
365
|
+
if (existing.args.length !== candidate.args.length) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
return existing.args.every(
|
|
369
|
+
(arg, index) => isAsymmetricMatcher(arg) ? isSameMatcher(arg, candidate.args[index]) : existing.serialized[index] === candidate.serialized[index]
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
// Keys are always argument arrays; `serializeValue` renders them to a stable,
|
|
373
|
+
// total string (single-quoted strings, bracketed arrays, distinct `undefined`
|
|
374
|
+
// / function / symbol / BigInt / Date renderings, circular-ref safe). Arrays of
|
|
375
|
+
// only primitive args take a fast path that skips the circular-ref bookkeeping.
|
|
376
|
+
#serialize(key) {
|
|
377
|
+
if (Array.isArray(key) && !key.some(isDeepValue)) {
|
|
378
|
+
return `[${key.map((arg) => serializePrimitive(arg)).join(",")}]`;
|
|
379
|
+
}
|
|
380
|
+
return serializeValue(key);
|
|
381
|
+
}
|
|
382
|
+
/** Return the value of the first asymmetric config whose predicate matches the actual args. */
|
|
383
|
+
#findByMatcher(actualArgs) {
|
|
384
|
+
if (!Array.isArray(actualArgs)) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
const match = this.#matcherConfigs.find((config) => this.#argsMatch(config, actualArgs));
|
|
388
|
+
return match?.value;
|
|
389
|
+
}
|
|
390
|
+
/** Whether every configured arg matches the actual arg at the same position (same length). */
|
|
391
|
+
#argsMatch(config, actualArgs) {
|
|
392
|
+
if (config.args.length !== actualArgs.length) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return config.args.every((configArg, index) => this.#valueMatches(configArg, config.serialized[index], actualArgs[index]));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Match a single arg: asymmetric matchers delegate to `asymmetricMatch`, others compare the
|
|
399
|
+
* config arg's serialization — rendered once at `set()` time — against the actual arg's.
|
|
400
|
+
*
|
|
401
|
+
* The branch is taken on `configArg` rather than on `serializedConfigArg === undefined`, even
|
|
402
|
+
* though the two are the same test by construction: the type guard is what narrows `configArg`
|
|
403
|
+
* to something with `asymmetricMatch` on it, and the alternative needs an assertion to say the
|
|
404
|
+
* same thing less safely.
|
|
405
|
+
*/
|
|
406
|
+
#valueMatches(configArg, serializedConfigArg, actualArg) {
|
|
407
|
+
if (isAsymmetricMatcher(configArg)) {
|
|
408
|
+
return configArg.asymmetricMatch(actualArg);
|
|
409
|
+
}
|
|
410
|
+
return serializedConfigArg === this.#serialize([actualArg]);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/lib/spy-mark.ts
|
|
415
|
+
var AUTO_SPY_MARK = /* @__PURE__ */ Symbol.for("vitest-auto-spy.mock");
|
|
416
|
+
function isMarkedMock(value) {
|
|
417
|
+
return typeof value === "function" && AUTO_SPY_MARK in value;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/lib/explain-spy.ts
|
|
421
|
+
var CHAIN_NAMES = ["calledWith", "mustBeCalledWith"];
|
|
422
|
+
var HEADER = "[vitest-auto-spy] explainSpy";
|
|
423
|
+
var NOT_A_DOUBLE = "is not a spy created by vitest-auto-spy, so it has no configured arguments to read. Pass a double built by createSpyFromClass, createAutoMock, createFunctionSpy or mockDeep.";
|
|
424
|
+
var NOTHING_FOUND = "nothing to explain: this value holds no spy created by vitest-auto-spy. Pass a double built by createSpyFromClass, createAutoMock, createFunctionSpy or mockDeep.";
|
|
425
|
+
var NO_MATCH_OUTCOME = "no configured arguments matched; the default value was used";
|
|
426
|
+
function isObject(value) {
|
|
427
|
+
return typeof value === "object" && value !== null;
|
|
428
|
+
}
|
|
429
|
+
function bareArgs(bracketed) {
|
|
430
|
+
return bracketed.substring(1, bracketed.length - 1);
|
|
431
|
+
}
|
|
432
|
+
function asCall(name, args) {
|
|
433
|
+
return `${name}(${bareArgs(serializeValue(args))})`;
|
|
434
|
+
}
|
|
435
|
+
function mockNameOf(mock) {
|
|
436
|
+
const read = Reflect.get(mock, "getMockName");
|
|
437
|
+
const name = typeof read === "function" ? read.call(mock) : void 0;
|
|
438
|
+
return typeof name === "string" && name !== "vi.fn()" ? name : "spy";
|
|
439
|
+
}
|
|
440
|
+
function chainsOf(mock) {
|
|
441
|
+
const mark = Reflect.get(mock, AUTO_SPY_MARK);
|
|
442
|
+
const state = isObject(mark) ? Reflect.get(mark, "state") : void 0;
|
|
443
|
+
if (!isObject(state)) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
const chains = [];
|
|
447
|
+
for (const name of CHAIN_NAMES) {
|
|
448
|
+
const chain = Reflect.get(state, name);
|
|
449
|
+
const map = isObject(chain) ? Reflect.get(chain, "argsToValuesMap") : void 0;
|
|
450
|
+
if (map instanceof ArgsMap) {
|
|
451
|
+
chains.push({ name, map });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return chains;
|
|
455
|
+
}
|
|
456
|
+
function configLines(mock) {
|
|
457
|
+
const lines = [];
|
|
458
|
+
for (const chain of chainsOf(mock)) {
|
|
459
|
+
for (const entry of chain.map.configuredEntries()) {
|
|
460
|
+
lines.push({
|
|
461
|
+
index: lines.length + 1,
|
|
462
|
+
text: `${chain.name}(${bareArgs(entry.args)})`,
|
|
463
|
+
matches: (actualArgs) => entry.matches(actualArgs)
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return lines;
|
|
468
|
+
}
|
|
469
|
+
function headline(name, callCount, configCount, matchedCount) {
|
|
470
|
+
const calls = callCount === 0 ? "never called" : `${callCount} ${callCount === 1 ? "call" : "calls"}`;
|
|
471
|
+
if (configCount === 0) {
|
|
472
|
+
return `${name} \u2014 ${calls}, nothing configured`;
|
|
473
|
+
}
|
|
474
|
+
const verdict = callCount > 0 && matchedCount === 0 ? ", none matched" : "";
|
|
475
|
+
return `${name} \u2014 ${calls}, ${configCount} configured${verdict}`;
|
|
476
|
+
}
|
|
477
|
+
function explainMember(member) {
|
|
478
|
+
const configs = configLines(member.mock);
|
|
479
|
+
const calls = getMockAdapter().getCalls(member.mock);
|
|
480
|
+
const hits = calls.map((args) => configs.find((config) => config.matches(args)));
|
|
481
|
+
const lines = [headline(member.name, calls.length, configs.length, hits.filter((hit) => hit !== void 0).length)];
|
|
482
|
+
if (configs.length > 0) {
|
|
483
|
+
lines.push(" configured:", ...configs.map((config) => ` #${config.index} ${config.text}`));
|
|
484
|
+
}
|
|
485
|
+
if (calls.length > 0) {
|
|
486
|
+
lines.push(" calls:", ...calls.map((args, position) => callLine(member.name, args, position, configs.length, hits[position])));
|
|
487
|
+
}
|
|
488
|
+
return lines.join("\n");
|
|
489
|
+
}
|
|
490
|
+
function callLine(name, args, position, configCount, hit) {
|
|
491
|
+
const call = ` #${position + 1} ${asCall(name, args)}`;
|
|
492
|
+
if (configCount === 0) {
|
|
493
|
+
return call;
|
|
494
|
+
}
|
|
495
|
+
return `${call} -> ${hit ? `matched #${hit.index}` : NO_MATCH_OUTCOME}`;
|
|
496
|
+
}
|
|
497
|
+
function accessorMembers(spy) {
|
|
498
|
+
const bag = Object.getOwnPropertyDescriptor(spy, "accessorSpies")?.value;
|
|
499
|
+
if (!isObject(bag)) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
return [...bagHalf(bag, "getters", "get"), ...bagHalf(bag, "setters", "set")];
|
|
503
|
+
}
|
|
504
|
+
function bagHalf(bag, half, label) {
|
|
505
|
+
const record = Reflect.get(bag, half);
|
|
506
|
+
if (!isObject(record)) {
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
return Object.entries(record).flatMap(
|
|
510
|
+
([property, mock]) => isMarkedMock(mock) ? [{ name: `${label} ${property}`, property, mock }] : []
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
function collectMembers(spy) {
|
|
514
|
+
const found = [];
|
|
515
|
+
for (const key of Object.keys(spy)) {
|
|
516
|
+
const descriptor = Object.getOwnPropertyDescriptor(spy, key);
|
|
517
|
+
if (!descriptor || descriptor.get || descriptor.set) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const { value } = descriptor;
|
|
521
|
+
if (isMarkedMock(value)) {
|
|
522
|
+
found.push({ name: key, property: key, mock: value });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return [...found, ...accessorMembers(spy)];
|
|
526
|
+
}
|
|
527
|
+
function membersNamed(spy, property) {
|
|
528
|
+
const accessors = accessorMembers(spy).filter((member) => member.property === property);
|
|
529
|
+
if (accessors.length > 0) {
|
|
530
|
+
return accessors;
|
|
531
|
+
}
|
|
532
|
+
const value = Reflect.get(spy, property);
|
|
533
|
+
return isMarkedMock(value) ? [{ name: property, property, mock: value }] : [];
|
|
534
|
+
}
|
|
535
|
+
function sections(spy, method) {
|
|
536
|
+
const members = method === void 0 ? collectMembers(spy) : membersNamed(spy, method);
|
|
537
|
+
if (members.length > 0) {
|
|
538
|
+
return members.map(explainMember);
|
|
539
|
+
}
|
|
540
|
+
if (isMarkedMock(spy)) {
|
|
541
|
+
return [explainMember({ name: method ?? mockNameOf(spy), mock: spy })];
|
|
542
|
+
}
|
|
543
|
+
return [method === void 0 ? NOTHING_FOUND : `${method} ${NOT_A_DOUBLE}`];
|
|
544
|
+
}
|
|
545
|
+
function explainSpy(spy, method) {
|
|
546
|
+
return `${HEADER}
|
|
547
|
+
|
|
548
|
+
${sections(spy, method).join("\n\n")}`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export { compareTestRuns, diffByField, explainSpy, formatTestRunComparison, summarizeTestRun };
|
|
@@ -83,6 +83,32 @@ interface AutoMockConfiguration<T> extends StrictSpyConfiguration {
|
|
|
83
83
|
*/
|
|
84
84
|
declare function autoMocked<T>(overrides?: DeepPartial<T>): Spy<T> & T;
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Change the wait every emission helper uses when a call does not name one. Process-wide; belongs
|
|
88
|
+
* in a setup file, next to `setupAutoSpy()`.
|
|
89
|
+
*
|
|
90
|
+
* It exists for one shape, and that shape is common enough to deserve a knob: a suite running under
|
|
91
|
+
* **global fake timers**. The watchdog in `expect-emission.ts` deliberately runs on the real clock,
|
|
92
|
+
* so a *failing* assertion in such a suite spends a real second before it reports — and the reflex
|
|
93
|
+
* that produces, `{ timeout: 0 }` at every call site, is the worst of the options: it disables the
|
|
94
|
+
* watchdog, so the next silent stream hangs until the runner's own timeout with no message worth
|
|
95
|
+
* reading. One line here instead:
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* // vitest.setup.ts
|
|
99
|
+
* setupAutoSpy({ globalFakeTimers: true });
|
|
100
|
+
* setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* 100 ms of *real* time is a large budget under fake timers, where the only real time that can pass
|
|
104
|
+
* is the microtask/macrotask drain between `await`s — nothing that waits on the clock can advance
|
|
105
|
+
* without the spec advancing it. Leave the default alone in a suite with real timers.
|
|
106
|
+
*
|
|
107
|
+
* `expectNoEmission` is unaffected: its wait is a quiet window, not a watchdog, and it defaults to
|
|
108
|
+
* one macrotask.
|
|
109
|
+
*/
|
|
110
|
+
declare function setEmissionTimeout(milliseconds: number): void;
|
|
111
|
+
|
|
86
112
|
/** Minimal observer accepted by {@link SubscribableLike}. */
|
|
87
113
|
interface EmissionObserver<T> {
|
|
88
114
|
next: (value: T) => void;
|
|
@@ -170,31 +196,7 @@ interface EmissionOptions<T = unknown> {
|
|
|
170
196
|
*/
|
|
171
197
|
advance?: () => void;
|
|
172
198
|
}
|
|
173
|
-
|
|
174
|
-
* Change the wait every emission helper uses when a call does not name one. Process-wide; belongs
|
|
175
|
-
* in a setup file, next to `setupAutoSpy()`.
|
|
176
|
-
*
|
|
177
|
-
* It exists for one shape, and that shape is common enough to deserve a knob: a suite running under
|
|
178
|
-
* **global fake timers**. The watchdog below deliberately runs on the real clock, so a *failing*
|
|
179
|
-
* assertion in such a suite spends a real second before it reports — and the reflex that produces,
|
|
180
|
-
* `{ timeout: 0 }` at every call site, is the worst of the options: it disables the watchdog, so
|
|
181
|
-
* the next silent stream hangs until the runner's own timeout with no message worth reading. One
|
|
182
|
-
* line here instead:
|
|
183
|
-
*
|
|
184
|
-
* ```ts
|
|
185
|
-
* // vitest.setup.ts
|
|
186
|
-
* setupAutoSpy({ globalFakeTimers: true });
|
|
187
|
-
* setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
|
|
188
|
-
* ```
|
|
189
|
-
*
|
|
190
|
-
* 100 ms of *real* time is a large budget under fake timers, where the only real time that can pass
|
|
191
|
-
* is the microtask/macrotask drain between `await`s — nothing that waits on the clock can advance
|
|
192
|
-
* without the spec advancing it. Leave the default alone in a suite with real timers.
|
|
193
|
-
*
|
|
194
|
-
* `expectNoEmission` is unaffected: its wait is a quiet window, not a watchdog, and it defaults to
|
|
195
|
-
* one macrotask.
|
|
196
|
-
*/
|
|
197
|
-
declare function setEmissionTimeout(milliseconds: number): void;
|
|
199
|
+
|
|
198
200
|
/**
|
|
199
201
|
* Await **the first value** of `source$` — not a list of them — failing loudly when it never
|
|
200
202
|
* arrives.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { A as AccessorKeysOf, a as AddAccessorsSpies, b as AddCalledWithObservable, c as AddCalledWithPromise, d as AddCalledWithSpyMethods, e as AddObservableSpyMethods, f as AddPromiseSpyMethods, g as AddSpyMethodsByReturnTypes, h as AddThrowHelper, i as AddVoidReturnHelpers, j as AutoSpyRxjsTypes, k as ClassSpyConfiguration, C as ClassType, l as CompleteValueConfig, D as DeepMockProxy, m as DeepPartial, E as ErrorValueConfig, F as Func, M as MethodReturns, n as Mutable, N as NextValueConfig, O as ObservableLike, o as OnlyMethodKeysOf, p as OnlyObservablePropsOf, q as OnlyPropsOf, r as Overload, s as Overloads, P as PropStubValue, S as Spy, t as SpyDisposable, u as SpyOptions, v as StrictSpyConfiguration, w as SubjectLike, x as SubjectOf, U as UnstubbedCall, y as UnstubbedCallHandler, V as ValueConfig, z as ValueConfigPerCall, W as WithMockReturnValue } from './types-CQLPjw8z.js';
|
|
2
|
-
export { ArgCaptor, AsInstances, AssertMockedOptions, ConstructorMock, ConstructorSpy, FixtureFactory, FlushUntilOptions, MockDeepOptions, ModuleNamespace, ModuleNamespaceOptions, asInstance, asInstances, asSpy, assertMocked, captureArg, clearAutoSpy, createFixture, createFixtureFactory, createFunctionSpy, createMock, createSpyClass, createSpyFromClass, errorHandler, flushEventLoop, flushEventLoopUntil, mockConstructor, mockDeep, moduleNamespace, narrow, resetAutoSpy, settleDynamicImport, stubConstructor, withOverrides } from './bun.js';
|
|
3
|
-
export { A as AutoMockConfiguration, C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, c as autoMocked, d as createAutoMock, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-
|
|
2
|
+
export { ArgCaptor, AsInstances, AssertMockedOptions, ConstructorMock, ConstructorSpy, FixtureFactory, FlushUntilOptions, MockDeepOptions, ModuleNamespace, ModuleNamespaceOptions, asInstance, asInstances, asSpy, assertMocked, captureArg, clearAutoSpy, createFixture, createFixtureFactory, createFunctionSpy, createMock, createSpyClass, createSpyFromClass, createSpyFromInstance, errorHandler, flushEventLoop, flushEventLoopUntil, mockConstructor, mockDeep, moduleNamespace, narrow, resetAutoSpy, restoreSpiedInstance, settleDynamicImport, stubConstructor, withOverrides } from './bun.js';
|
|
3
|
+
export { A as AutoMockConfiguration, C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, c as autoMocked, d as createAutoMock, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-Dy5CvsF2.js';
|
|
4
4
|
export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-Dp2ZMFqj.js';
|
|
5
5
|
export { d as describeDuplicateCopies, g as getPackageCopies } from './package-identity-B1pqa-Sh.js';
|
|
6
6
|
import 'vitest';
|