vitest-auto-spy 3.13.0 → 3.13.1

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 CHANGED
@@ -358,6 +358,8 @@ users.getName.calledWith(1).mockReturnValue('Ada');
358
358
  users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
359
359
  // asymmetric matchers work in both
360
360
  users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
361
+ // re-registering the same arguments replaces the answer — matcher arguments included
362
+ users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(false);
361
363
 
362
364
  // promises
363
365
  users.load.resolveWith({ id: 1 });
@@ -374,6 +376,13 @@ feed.items$.throwWith('FAKE ERROR');
374
376
  const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the helpers miss
375
377
  ```
376
378
 
379
+ An exact argument list is matched before the asymmetric configs, and those are tried in
380
+ registration order — a narrow config written first keeps its calls. Two matchers count as the same
381
+ argument when they accept the same values (same matcher class, sample and inversion), which is what
382
+ makes a second `calledWith(1, expect.anything())` an override rather than a second config sitting
383
+ behind the first. A hand-rolled `{ asymmetricMatch }` object is compared by identity instead: its
384
+ verdict is a closure, so only re-registering that same instance overrides.
385
+
377
386
  `mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
378
387
  all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
379
388
 
package/README.md CHANGED
@@ -16,7 +16,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
16
16
  [![npm version](https://img.shields.io/npm/v/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
17
17
  [![npm downloads](https://img.shields.io/npm/dm/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
18
18
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
19
- [![minzipped size](https://img.shields.io/badge/minzip-14.6%20kB-brightgreen)](#install)
19
+ [![minzipped size](https://img.shields.io/badge/minzip-14.8%20kB-brightgreen)](#install)
20
20
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
21
21
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
22
22
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -1231,6 +1231,24 @@ Wanted: getName(1)
1231
1231
  Actual: getName(2)
1232
1232
  ```
1233
1233
 
1234
+ ### Matching order, and re-configuring the same arguments
1235
+
1236
+ An exact argument list is matched first; the asymmetric configs (`expect.any(Number)`,
1237
+ `expect.objectContaining({ … })`, …) are then tried in the order they were registered, so a narrow
1238
+ config placed before a wide one keeps its calls. Registering the **same** argument list again
1239
+ replaces the answer it gave before — matcher arguments included:
1240
+
1241
+ ```ts
1242
+ myService.getName.calledWith(expect.anything()).mockReturnValue('first');
1243
+ myService.getName.calledWith(expect.anything()).mockReturnValue('second');
1244
+ expect(myService.getName(1)).toBe('second');
1245
+ ```
1246
+
1247
+ Each `expect.anything()` is a new object, so the two are compared by what they accept rather than by
1248
+ identity — same matcher class, same sample, same inversion. A hand-rolled `{ asymmetricMatch }`
1249
+ object is the exception: its verdict lives in a closure nothing can read, so two of them are always
1250
+ two configs and only the very same instance, re-registered, overrides.
1251
+
1234
1252
  ## Promise-returning methods
1235
1253
 
1236
1254
  ```ts
package/dist/angular.js CHANGED
@@ -102,6 +102,9 @@ function serializeShape(value, context) {
102
102
  if (value instanceof Date) {
103
103
  return `new Date(${value.getTime()})`;
104
104
  }
105
+ if (value instanceof RegExp) {
106
+ return String(value);
107
+ }
105
108
  if (value instanceof Map) {
106
109
  return serializeMap(value, context);
107
110
  }
@@ -172,6 +175,22 @@ function isAsymmetricMatcher(value) {
172
175
  function hasAsymmetricMatcher(args) {
173
176
  return args.some(isAsymmetricMatcher);
174
177
  }
178
+ var ASYMMETRIC_MATCHER_BRAND = /* @__PURE__ */ Symbol.for("jest.asymmetricMatcher");
179
+ function isSameMatcher(configArg, candidateArg) {
180
+ if (configArg === candidateArg) {
181
+ return true;
182
+ }
183
+ if (!isAsymmetricMatcher(candidateArg) || !isBrandedMatcher(configArg) || !isBrandedMatcher(candidateArg)) {
184
+ return false;
185
+ }
186
+ if (Object.getPrototypeOf(configArg) !== Object.getPrototypeOf(candidateArg)) {
187
+ return false;
188
+ }
189
+ return serializeValue(configArg) === serializeValue(candidateArg);
190
+ }
191
+ function isBrandedMatcher(matcher) {
192
+ return matcher.$$typeof === ASYMMETRIC_MATCHER_BRAND;
193
+ }
175
194
  var ArgsMap = class {
176
195
  // Prototype-less so a `'__proto__'` (or `'constructor'`) serialized key is a
177
196
  // plain own property, never walking or polluting the object prototype chain.
@@ -186,19 +205,7 @@ var ArgsMap = class {
186
205
  #arities = /* @__PURE__ */ new Set();
187
206
  set(key, value) {
188
207
  if (Array.isArray(key) && hasAsymmetricMatcher(key)) {
189
- const serialized = [];
190
- const described = [];
191
- for (const arg of key) {
192
- if (isAsymmetricMatcher(arg)) {
193
- serialized.push(void 0);
194
- described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
195
- } else {
196
- const rendered = this.#serialize([arg]);
197
- serialized.push(rendered);
198
- described.push(rendered.slice(1, -1));
199
- }
200
- }
201
- this.#matcherConfigs.push({ args: key, serialized, described, value });
208
+ this.#setMatcherConfig(this.#buildMatcherConfig(key, value));
202
209
  return;
203
210
  }
204
211
  if (Array.isArray(key)) {
@@ -228,6 +235,49 @@ var ArgsMap = class {
228
235
  const asymmetric = this.#matcherConfigs.map((config) => `[${config.described.join(",")}]`);
229
236
  return [...Object.keys(this.#map), ...asymmetric];
230
237
  }
238
+ /** Render one asymmetric config once, at registration time. See {@link MatcherConfig}. */
239
+ #buildMatcherConfig(key, value) {
240
+ const serialized = [];
241
+ const described = [];
242
+ for (const arg of key) {
243
+ if (isAsymmetricMatcher(arg)) {
244
+ serialized.push(void 0);
245
+ described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
246
+ } else {
247
+ const rendered = this.#serialize([arg]);
248
+ serialized.push(rendered);
249
+ described.push(rendered.slice(1, -1));
250
+ }
251
+ }
252
+ return { args: key, serialized, described, value };
253
+ }
254
+ /**
255
+ * Register an asymmetric config, replacing an equivalent one in place.
256
+ *
257
+ * Re-registering args is how a test overrides an earlier answer, and on the exact map that falls
258
+ * out for free — the second `set` writes the same key. The matcher list has to do it by hand:
259
+ * lookup takes the first predicate that matches, so an appended duplicate would sit behind the
260
+ * config it was meant to replace and never be reached, and a `beforeEach` that reconfigures the
261
+ * same spy would grow the list on every test. Replacing in place also keeps the registration
262
+ * order that decides which of two *overlapping* configs wins.
263
+ */
264
+ #setMatcherConfig(config) {
265
+ const index = this.#matcherConfigs.findIndex((existing) => this.#sameConfigArgs(existing, config));
266
+ if (index === -1) {
267
+ this.#matcherConfigs.push(config);
268
+ return;
269
+ }
270
+ this.#matcherConfigs[index] = config;
271
+ }
272
+ /** Whether two configs were registered for the same argument list, matcher positions included. */
273
+ #sameConfigArgs(existing, candidate) {
274
+ if (existing.args.length !== candidate.args.length) {
275
+ return false;
276
+ }
277
+ return existing.args.every(
278
+ (arg, index) => isAsymmetricMatcher(arg) ? isSameMatcher(arg, candidate.args[index]) : existing.serialized[index] === candidate.serialized[index]
279
+ );
280
+ }
231
281
  // Keys are always argument arrays; `serializeValue` renders them to a stable,
232
282
  // total string (single-quoted strings, bracketed arrays, distinct `undefined`
233
283
  // / function / symbol / BigInt / Date renderings, circular-ref safe). Arrays of
@@ -1,12 +1,12 @@
1
1
  import './chunk-Y4YG7UOK.js';
2
- import { createSpyForToken } from './chunk-JV5XRAS4.js';
2
+ import { createSpyForToken } from './chunk-CEVEKNJZ.js';
3
3
  import './chunk-NQXXUJPS.js';
4
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-IKH4OTMV.js';
5
- export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
6
- import { createSpyFromClass } from './chunk-7YTPHYP7.js';
7
- export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-7YTPHYP7.js';
8
- import { withDocs, DOCS_LINKS, isAutoSpyLike } from './chunk-3AWKPZCE.js';
9
- export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
4
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-C3IZLTSJ.js';
5
+ export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-WVW4XRL5.js';
6
+ import { createSpyFromClass } from './chunk-PCP6A6PD.js';
7
+ export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-PCP6A6PD.js';
8
+ import { withDocs, DOCS_LINKS, isAutoSpyLike } from './chunk-ZY7KPBJJ.js';
9
+ export { createFunctionSpy, errorHandler } from './chunk-ZY7KPBJJ.js';
10
10
  import './chunk-J5O72UHX.js';
11
11
  import '@angular/compiler';
12
12
  import { NgModule, provideZonelessChangeDetection, Injector, runInInjectionContext, NO_ERRORS_SCHEMA, ApplicationRef, ɵSIGNAL as _SIGNAL } from '@angular/core';
package/dist/bun.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import './chunk-Y4YG7UOK.js';
2
2
  import './chunk-NQXXUJPS.js';
3
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-IKH4OTMV.js';
4
- export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
5
- export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-7YTPHYP7.js';
6
- export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
3
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-C3IZLTSJ.js';
4
+ export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-WVW4XRL5.js';
5
+ export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-PCP6A6PD.js';
6
+ export { createFunctionSpy, errorHandler } from './chunk-ZY7KPBJJ.js';
7
7
  import './chunk-J5O72UHX.js';
@@ -1,6 +1,6 @@
1
- import { mockValueProp, mockReadonlyPropGetter } from './chunk-R4EWAG7Y.js';
2
- import { createSpyFromClass, createProxyPropStore, describeStoredProp, dropStoredProp, storeDefinedProp, writeStoredAccessor, writeStoredValue, readStoredAccessor, NOT_STORED, DISPOSE, disposeAutoSpy, isProtocolKey, isDeletedProp } from './chunk-7YTPHYP7.js';
3
- import { defineHelper, withDocs, DOCS_LINKS, serializeValue, createFunctionSpy, DEEP_CHILDREN } from './chunk-3AWKPZCE.js';
1
+ import { mockValueProp, mockReadonlyPropGetter } from './chunk-WVW4XRL5.js';
2
+ import { createSpyFromClass, createProxyPropStore, describeStoredProp, dropStoredProp, storeDefinedProp, writeStoredAccessor, writeStoredValue, readStoredAccessor, NOT_STORED, DISPOSE, disposeAutoSpy, isProtocolKey, isDeletedProp } from './chunk-PCP6A6PD.js';
3
+ import { defineHelper, withDocs, DOCS_LINKS, serializeValue, createFunctionSpy, DEEP_CHILDREN } from './chunk-ZY7KPBJJ.js';
4
4
  import { getMockAdapter } from './shared-state.js';
5
5
  export { describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, getPackageCopies, setEmissionTimeout } from './shared-state.js';
6
6
 
@@ -1,5 +1,5 @@
1
- import { createSpyFromClass, createAutoMock } from './chunk-7YTPHYP7.js';
2
- import { withDocs, DOCS_LINKS } from './chunk-3AWKPZCE.js';
1
+ import { createSpyFromClass, createAutoMock } from './chunk-PCP6A6PD.js';
2
+ import { withDocs, DOCS_LINKS } from './chunk-ZY7KPBJJ.js';
3
3
 
4
4
  // src/lib/track-injections.ts
5
5
  function createSpyForToken(token) {
@@ -1,4 +1,4 @@
1
- import { runClearHook, runConfigReset, resolveUnstubbedGuard, withDocs, DOCS_LINKS, AUTO_SPY_MARK, createFunctionSpy, isMarkedMock, readDeepChildren, markAsMock } from './chunk-3AWKPZCE.js';
1
+ import { runClearHook, runConfigReset, resolveUnstubbedGuard, withDocs, DOCS_LINKS, AUTO_SPY_MARK, createFunctionSpy, isMarkedMock, readDeepChildren, markAsMock } from './chunk-ZY7KPBJJ.js';
2
2
  import { getMockAdapter, requireObservableSupport } from './shared-state.js';
3
3
 
4
4
  // src/lib/dispose-symbol.ts
@@ -1,4 +1,4 @@
1
- import { withDocs, DOCS_LINKS } from './chunk-3AWKPZCE.js';
1
+ import { withDocs, DOCS_LINKS } from './chunk-ZY7KPBJJ.js';
2
2
  import { getMockAdapter } from './shared-state.js';
3
3
 
4
4
  // src/lib/redefine-failure.ts
@@ -81,6 +81,9 @@ function serializeShape(value, context) {
81
81
  if (value instanceof Date) {
82
82
  return `new Date(${value.getTime()})`;
83
83
  }
84
+ if (value instanceof RegExp) {
85
+ return String(value);
86
+ }
84
87
  if (value instanceof Map) {
85
88
  return serializeMap(value, context);
86
89
  }
@@ -187,6 +190,22 @@ function isAsymmetricMatcher(value) {
187
190
  function hasAsymmetricMatcher(args) {
188
191
  return args.some(isAsymmetricMatcher);
189
192
  }
193
+ var ASYMMETRIC_MATCHER_BRAND = /* @__PURE__ */ Symbol.for("jest.asymmetricMatcher");
194
+ function isSameMatcher(configArg, candidateArg) {
195
+ if (configArg === candidateArg) {
196
+ return true;
197
+ }
198
+ if (!isAsymmetricMatcher(candidateArg) || !isBrandedMatcher(configArg) || !isBrandedMatcher(candidateArg)) {
199
+ return false;
200
+ }
201
+ if (Object.getPrototypeOf(configArg) !== Object.getPrototypeOf(candidateArg)) {
202
+ return false;
203
+ }
204
+ return serializeValue(configArg) === serializeValue(candidateArg);
205
+ }
206
+ function isBrandedMatcher(matcher) {
207
+ return matcher.$$typeof === ASYMMETRIC_MATCHER_BRAND;
208
+ }
190
209
  var ArgsMap = class {
191
210
  // Prototype-less so a `'__proto__'` (or `'constructor'`) serialized key is a
192
211
  // plain own property, never walking or polluting the object prototype chain.
@@ -201,19 +220,7 @@ var ArgsMap = class {
201
220
  #arities = /* @__PURE__ */ new Set();
202
221
  set(key, value) {
203
222
  if (Array.isArray(key) && hasAsymmetricMatcher(key)) {
204
- const serialized = [];
205
- const described = [];
206
- for (const arg of key) {
207
- if (isAsymmetricMatcher(arg)) {
208
- serialized.push(void 0);
209
- described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
210
- } else {
211
- const rendered = this.#serialize([arg]);
212
- serialized.push(rendered);
213
- described.push(rendered.slice(1, -1));
214
- }
215
- }
216
- this.#matcherConfigs.push({ args: key, serialized, described, value });
223
+ this.#setMatcherConfig(this.#buildMatcherConfig(key, value));
217
224
  return;
218
225
  }
219
226
  if (Array.isArray(key)) {
@@ -243,6 +250,49 @@ var ArgsMap = class {
243
250
  const asymmetric = this.#matcherConfigs.map((config) => `[${config.described.join(",")}]`);
244
251
  return [...Object.keys(this.#map), ...asymmetric];
245
252
  }
253
+ /** Render one asymmetric config once, at registration time. See {@link MatcherConfig}. */
254
+ #buildMatcherConfig(key, value) {
255
+ const serialized = [];
256
+ const described = [];
257
+ for (const arg of key) {
258
+ if (isAsymmetricMatcher(arg)) {
259
+ serialized.push(void 0);
260
+ described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
261
+ } else {
262
+ const rendered = this.#serialize([arg]);
263
+ serialized.push(rendered);
264
+ described.push(rendered.slice(1, -1));
265
+ }
266
+ }
267
+ return { args: key, serialized, described, value };
268
+ }
269
+ /**
270
+ * Register an asymmetric config, replacing an equivalent one in place.
271
+ *
272
+ * Re-registering args is how a test overrides an earlier answer, and on the exact map that falls
273
+ * out for free — the second `set` writes the same key. The matcher list has to do it by hand:
274
+ * lookup takes the first predicate that matches, so an appended duplicate would sit behind the
275
+ * config it was meant to replace and never be reached, and a `beforeEach` that reconfigures the
276
+ * same spy would grow the list on every test. Replacing in place also keeps the registration
277
+ * order that decides which of two *overlapping* configs wins.
278
+ */
279
+ #setMatcherConfig(config) {
280
+ const index = this.#matcherConfigs.findIndex((existing) => this.#sameConfigArgs(existing, config));
281
+ if (index === -1) {
282
+ this.#matcherConfigs.push(config);
283
+ return;
284
+ }
285
+ this.#matcherConfigs[index] = config;
286
+ }
287
+ /** Whether two configs were registered for the same argument list, matcher positions included. */
288
+ #sameConfigArgs(existing, candidate) {
289
+ if (existing.args.length !== candidate.args.length) {
290
+ return false;
291
+ }
292
+ return existing.args.every(
293
+ (arg, index) => isAsymmetricMatcher(arg) ? isSameMatcher(arg, candidate.args[index]) : existing.serialized[index] === candidate.serialized[index]
294
+ );
295
+ }
246
296
  // Keys are always argument arrays; `serializeValue` renders them to a stable,
247
297
  // total string (single-quoted strings, bracketed arrays, distinct `undefined`
248
298
  // / function / symbol / BigInt / Date renderings, circular-ref safe). Arrays of
package/dist/console.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { vitestMockAdapter } from './chunk-MG5EAMZR.js';
2
- import { createFunctionSpy } from './chunk-3AWKPZCE.js';
2
+ import { createFunctionSpy } from './chunk-ZY7KPBJJ.js';
3
3
  import './chunk-J5O72UHX.js';
4
4
  import { hasMockAdapter, registerMockAdapter, getMockAdapter } from './shared-state.js';
5
5
 
package/dist/index.js CHANGED
@@ -164,6 +164,9 @@ function serializeShape(value, context) {
164
164
  if (value instanceof Date) {
165
165
  return `new Date(${value.getTime()})`;
166
166
  }
167
+ if (value instanceof RegExp) {
168
+ return String(value);
169
+ }
167
170
  if (value instanceof Map) {
168
171
  return serializeMap(value, context);
169
172
  }
@@ -234,6 +237,22 @@ function isAsymmetricMatcher(value) {
234
237
  function hasAsymmetricMatcher(args) {
235
238
  return args.some(isAsymmetricMatcher);
236
239
  }
240
+ var ASYMMETRIC_MATCHER_BRAND = /* @__PURE__ */ Symbol.for("jest.asymmetricMatcher");
241
+ function isSameMatcher(configArg, candidateArg) {
242
+ if (configArg === candidateArg) {
243
+ return true;
244
+ }
245
+ if (!isAsymmetricMatcher(candidateArg) || !isBrandedMatcher(configArg) || !isBrandedMatcher(candidateArg)) {
246
+ return false;
247
+ }
248
+ if (Object.getPrototypeOf(configArg) !== Object.getPrototypeOf(candidateArg)) {
249
+ return false;
250
+ }
251
+ return serializeValue(configArg) === serializeValue(candidateArg);
252
+ }
253
+ function isBrandedMatcher(matcher) {
254
+ return matcher.$$typeof === ASYMMETRIC_MATCHER_BRAND;
255
+ }
237
256
  var ArgsMap = class {
238
257
  // Prototype-less so a `'__proto__'` (or `'constructor'`) serialized key is a
239
258
  // plain own property, never walking or polluting the object prototype chain.
@@ -248,19 +267,7 @@ var ArgsMap = class {
248
267
  #arities = /* @__PURE__ */ new Set();
249
268
  set(key, value) {
250
269
  if (Array.isArray(key) && hasAsymmetricMatcher(key)) {
251
- const serialized = [];
252
- const described = [];
253
- for (const arg of key) {
254
- if (isAsymmetricMatcher(arg)) {
255
- serialized.push(void 0);
256
- described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
257
- } else {
258
- const rendered = this.#serialize([arg]);
259
- serialized.push(rendered);
260
- described.push(rendered.slice(1, -1));
261
- }
262
- }
263
- this.#matcherConfigs.push({ args: key, serialized, described, value });
270
+ this.#setMatcherConfig(this.#buildMatcherConfig(key, value));
264
271
  return;
265
272
  }
266
273
  if (Array.isArray(key)) {
@@ -290,6 +297,49 @@ var ArgsMap = class {
290
297
  const asymmetric = this.#matcherConfigs.map((config) => `[${config.described.join(",")}]`);
291
298
  return [...Object.keys(this.#map), ...asymmetric];
292
299
  }
300
+ /** Render one asymmetric config once, at registration time. See {@link MatcherConfig}. */
301
+ #buildMatcherConfig(key, value) {
302
+ const serialized = [];
303
+ const described = [];
304
+ for (const arg of key) {
305
+ if (isAsymmetricMatcher(arg)) {
306
+ serialized.push(void 0);
307
+ described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
308
+ } else {
309
+ const rendered = this.#serialize([arg]);
310
+ serialized.push(rendered);
311
+ described.push(rendered.slice(1, -1));
312
+ }
313
+ }
314
+ return { args: key, serialized, described, value };
315
+ }
316
+ /**
317
+ * Register an asymmetric config, replacing an equivalent one in place.
318
+ *
319
+ * Re-registering args is how a test overrides an earlier answer, and on the exact map that falls
320
+ * out for free — the second `set` writes the same key. The matcher list has to do it by hand:
321
+ * lookup takes the first predicate that matches, so an appended duplicate would sit behind the
322
+ * config it was meant to replace and never be reached, and a `beforeEach` that reconfigures the
323
+ * same spy would grow the list on every test. Replacing in place also keeps the registration
324
+ * order that decides which of two *overlapping* configs wins.
325
+ */
326
+ #setMatcherConfig(config) {
327
+ const index = this.#matcherConfigs.findIndex((existing) => this.#sameConfigArgs(existing, config));
328
+ if (index === -1) {
329
+ this.#matcherConfigs.push(config);
330
+ return;
331
+ }
332
+ this.#matcherConfigs[index] = config;
333
+ }
334
+ /** Whether two configs were registered for the same argument list, matcher positions included. */
335
+ #sameConfigArgs(existing, candidate) {
336
+ if (existing.args.length !== candidate.args.length) {
337
+ return false;
338
+ }
339
+ return existing.args.every(
340
+ (arg, index) => isAsymmetricMatcher(arg) ? isSameMatcher(arg, candidate.args[index]) : existing.serialized[index] === candidate.serialized[index]
341
+ );
342
+ }
293
343
  // Keys are always argument arrays; `serializeValue` renders them to a stable,
294
344
  // total string (single-quoted strings, bracketed arrays, distinct `undefined`
295
345
  // / function / symbol / BigInt / Date renderings, circular-ref safe). Arrays of
package/dist/nestjs.js CHANGED
@@ -1,7 +1,7 @@
1
- export { trackInjections } from './chunk-JV5XRAS4.js';
1
+ export { trackInjections } from './chunk-CEVEKNJZ.js';
2
2
  import { vitestMockAdapter } from './chunk-MG5EAMZR.js';
3
- import { createSpyFromClass } from './chunk-7YTPHYP7.js';
4
- import './chunk-3AWKPZCE.js';
3
+ import { createSpyFromClass } from './chunk-PCP6A6PD.js';
4
+ import './chunk-ZY7KPBJJ.js';
5
5
  import './chunk-J5O72UHX.js';
6
6
  import { registerMockAdapter } from './shared-state.js';
7
7
 
package/dist/node.cjs CHANGED
@@ -322,6 +322,9 @@ function serializeShape(value, context) {
322
322
  if (value instanceof Date) {
323
323
  return `new Date(${value.getTime()})`;
324
324
  }
325
+ if (value instanceof RegExp) {
326
+ return String(value);
327
+ }
325
328
  if (value instanceof Map) {
326
329
  return serializeMap(value, context);
327
330
  }
@@ -392,6 +395,22 @@ function isAsymmetricMatcher(value) {
392
395
  function hasAsymmetricMatcher(args) {
393
396
  return args.some(isAsymmetricMatcher);
394
397
  }
398
+ var ASYMMETRIC_MATCHER_BRAND = /* @__PURE__ */ Symbol.for("jest.asymmetricMatcher");
399
+ function isSameMatcher(configArg, candidateArg) {
400
+ if (configArg === candidateArg) {
401
+ return true;
402
+ }
403
+ if (!isAsymmetricMatcher(candidateArg) || !isBrandedMatcher(configArg) || !isBrandedMatcher(candidateArg)) {
404
+ return false;
405
+ }
406
+ if (Object.getPrototypeOf(configArg) !== Object.getPrototypeOf(candidateArg)) {
407
+ return false;
408
+ }
409
+ return serializeValue(configArg) === serializeValue(candidateArg);
410
+ }
411
+ function isBrandedMatcher(matcher) {
412
+ return matcher.$$typeof === ASYMMETRIC_MATCHER_BRAND;
413
+ }
395
414
  var ArgsMap = class {
396
415
  // Prototype-less so a `'__proto__'` (or `'constructor'`) serialized key is a
397
416
  // plain own property, never walking or polluting the object prototype chain.
@@ -406,19 +425,7 @@ var ArgsMap = class {
406
425
  #arities = /* @__PURE__ */ new Set();
407
426
  set(key, value) {
408
427
  if (Array.isArray(key) && hasAsymmetricMatcher(key)) {
409
- const serialized = [];
410
- const described = [];
411
- for (const arg of key) {
412
- if (isAsymmetricMatcher(arg)) {
413
- serialized.push(void 0);
414
- described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
415
- } else {
416
- const rendered = this.#serialize([arg]);
417
- serialized.push(rendered);
418
- described.push(rendered.slice(1, -1));
419
- }
420
- }
421
- this.#matcherConfigs.push({ args: key, serialized, described, value });
428
+ this.#setMatcherConfig(this.#buildMatcherConfig(key, value));
422
429
  return;
423
430
  }
424
431
  if (Array.isArray(key)) {
@@ -448,6 +455,49 @@ var ArgsMap = class {
448
455
  const asymmetric = this.#matcherConfigs.map((config) => `[${config.described.join(",")}]`);
449
456
  return [...Object.keys(this.#map), ...asymmetric];
450
457
  }
458
+ /** Render one asymmetric config once, at registration time. See {@link MatcherConfig}. */
459
+ #buildMatcherConfig(key, value) {
460
+ const serialized = [];
461
+ const described = [];
462
+ for (const arg of key) {
463
+ if (isAsymmetricMatcher(arg)) {
464
+ serialized.push(void 0);
465
+ described.push(arg.toAsymmetricMatcher?.() ?? String(arg));
466
+ } else {
467
+ const rendered = this.#serialize([arg]);
468
+ serialized.push(rendered);
469
+ described.push(rendered.slice(1, -1));
470
+ }
471
+ }
472
+ return { args: key, serialized, described, value };
473
+ }
474
+ /**
475
+ * Register an asymmetric config, replacing an equivalent one in place.
476
+ *
477
+ * Re-registering args is how a test overrides an earlier answer, and on the exact map that falls
478
+ * out for free — the second `set` writes the same key. The matcher list has to do it by hand:
479
+ * lookup takes the first predicate that matches, so an appended duplicate would sit behind the
480
+ * config it was meant to replace and never be reached, and a `beforeEach` that reconfigures the
481
+ * same spy would grow the list on every test. Replacing in place also keeps the registration
482
+ * order that decides which of two *overlapping* configs wins.
483
+ */
484
+ #setMatcherConfig(config) {
485
+ const index = this.#matcherConfigs.findIndex((existing) => this.#sameConfigArgs(existing, config));
486
+ if (index === -1) {
487
+ this.#matcherConfigs.push(config);
488
+ return;
489
+ }
490
+ this.#matcherConfigs[index] = config;
491
+ }
492
+ /** Whether two configs were registered for the same argument list, matcher positions included. */
493
+ #sameConfigArgs(existing, candidate) {
494
+ if (existing.args.length !== candidate.args.length) {
495
+ return false;
496
+ }
497
+ return existing.args.every(
498
+ (arg, index) => isAsymmetricMatcher(arg) ? isSameMatcher(arg, candidate.args[index]) : existing.serialized[index] === candidate.serialized[index]
499
+ );
500
+ }
451
501
  // Keys are always argument arrays; `serializeValue` renders them to a stable,
452
502
  // total string (single-quoted strings, bracketed arrays, distinct `undefined`
453
503
  // / function / symbol / BigInt / Date renderings, circular-ref safe). Arrays of
package/dist/node.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createRedefineMockAdapter } from './chunk-NQXXUJPS.js';
2
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-IKH4OTMV.js';
3
- export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
4
- export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-7YTPHYP7.js';
5
- export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
2
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-C3IZLTSJ.js';
3
+ export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-WVW4XRL5.js';
4
+ export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-PCP6A6PD.js';
5
+ export { createFunctionSpy, errorHandler } from './chunk-ZY7KPBJJ.js';
6
6
  import './chunk-J5O72UHX.js';
7
7
  import { mock } from 'node:test';
8
8
  import { registerMockAdapter, guardAccessorSpies } from './shared-state.js';
package/dist/react.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { vitestMockAdapter } from './chunk-MG5EAMZR.js';
2
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-IKH4OTMV.js';
3
- export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
4
- export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-7YTPHYP7.js';
5
- export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
2
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-C3IZLTSJ.js';
3
+ export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-WVW4XRL5.js';
4
+ export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-PCP6A6PD.js';
5
+ export { createFunctionSpy, errorHandler } from './chunk-ZY7KPBJJ.js';
6
6
  import './chunk-J5O72UHX.js';
7
7
  import { registerMockAdapter } from './shared-state.js';
8
8
 
package/dist/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { vitestMockAdapter } from './chunk-MG5EAMZR.js';
2
- import { mockValueProp, countMockedProps, restoreMockedProps } from './chunk-R4EWAG7Y.js';
3
- import { withDocs, DOCS_LINKS, setDefaultStrictMode } from './chunk-3AWKPZCE.js';
2
+ import { mockValueProp, countMockedProps, restoreMockedProps } from './chunk-WVW4XRL5.js';
3
+ import { withDocs, DOCS_LINKS, setDefaultStrictMode } from './chunk-ZY7KPBJJ.js';
4
4
  import './chunk-J5O72UHX.js';
5
5
  import { registerMockAdapter, describeDuplicateCopies } from './shared-state.js';
6
6
  export { describeDuplicateCopies, getPackageCopies } from './shared-state.js';
@@ -18,6 +18,9 @@ function serializeShape(value, context) {
18
18
  if (value instanceof Date) {
19
19
  return `new Date(${value.getTime()})`;
20
20
  }
21
+ if (value instanceof RegExp) {
22
+ return String(value);
23
+ }
21
24
  if (value instanceof Map) {
22
25
  return serializeMap(value, context);
23
26
  }
package/dist/svelte.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { vitestMockAdapter } from './chunk-MG5EAMZR.js';
2
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-IKH4OTMV.js';
3
- export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
4
- export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-7YTPHYP7.js';
5
- export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
2
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-C3IZLTSJ.js';
3
+ export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-WVW4XRL5.js';
4
+ export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-PCP6A6PD.js';
5
+ export { createFunctionSpy, errorHandler } from './chunk-ZY7KPBJJ.js';
6
6
  import './chunk-J5O72UHX.js';
7
7
  import { registerMockAdapter } from './shared-state.js';
8
8
 
package/dist/vue.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { vitestMockAdapter } from './chunk-MG5EAMZR.js';
2
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-IKH4OTMV.js';
3
- export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
4
- import { createSpyFromClass } from './chunk-7YTPHYP7.js';
5
- export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-7YTPHYP7.js';
6
- export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
2
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-C3IZLTSJ.js';
3
+ export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-WVW4XRL5.js';
4
+ import { createSpyFromClass } from './chunk-PCP6A6PD.js';
5
+ export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-PCP6A6PD.js';
6
+ export { createFunctionSpy, errorHandler } from './chunk-ZY7KPBJJ.js';
7
7
  import './chunk-J5O72UHX.js';
8
8
  import { registerMockAdapter } from './shared-state.js';
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "3.13.0",
3
+ "version": "3.13.1",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -209,6 +209,14 @@ it('loads', async () => {
209
209
  - **Never assert a resource with `expect(r.value()).toEqual(...)` alone** — an unresolved resource
210
210
  still holds its _default_, so that passes while proving nothing. `toHaveResourceValue(v)` after
211
211
  `registerResourceMatchers()` fails unless the resource actually resolved.
212
+ - **`calledWith` matches exact arguments first, then the matcher configs in registration order** —
213
+ so put the narrow `expect.any(Number)` before the wide `expect.anything()` when both can match.
214
+ Re-registering the same arguments replaces the previous answer, matcher arguments included: two
215
+ `calledWith(1, expect.anything())` lines are an override, not two configs.
216
+ - **On Bun (`bun:test`), an asymmetric matcher inside `calledWith` matches nothing** — Bun's
217
+ `expect.any()` / `expect.objectContaining()` are native objects carrying no `asymmetricMatch`, so
218
+ the config is kept as an ordinary argument and the call falls through to `undefined` instead of
219
+ failing. Dispatch on exact arguments there, or use `mockImplementation`.
212
220
  - **Never put `captureArg()` in `calledWith`** — it matches every value, so it configures a return
213
221
  for every call. It belongs in `toHaveBeenCalledWith`; the types enforce this.
214
222
  - **Never `vi.mock('@angular/core')`** (or any relative path) under the Angular unit-test builder —