vitest-auto-spy 4.6.0 → 4.6.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.
Files changed (3) hide show
  1. package/AGENTS.md +10 -4
  2. package/README.md +32 -27
  3. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -1135,7 +1135,7 @@ setSpyEngine('runner'); // every double built afterwards is `vi.fn()` per method
1135
1135
  ```
1136
1136
 
1137
1137
  **On Vitest 5 the registry story changes, and this library follows it.** Vitest 5 keeps registered
1138
- mocks behind `WeakRef`s and its `clearAllMocks()` visits only the ones *called* since the last
1138
+ mocks behind `WeakRef`s and its `clearAllMocks()` visits only the ones _called_ since the last
1139
1139
  sweep — so a spy that is not a `vi.fn()` has to make itself reachable to that sweep deliberately. It
1140
1140
  does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMocks: true` default
1141
1141
  (new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
@@ -1145,6 +1145,12 @@ What Vitest 5 does break is the runner's own doing, not this library's: with `cl
1145
1145
  default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
1146
1146
  zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
1147
1147
 
1148
+ **The one exception to "nothing in a spec changes".** `setupAutoSpy({ pruneMockRegistry: true })`
1149
+ discovers long-lived mocks by walking the runner's registry, which Vitest 5 no longer exposes — the
1150
+ automatic half is a no-op there. `keepMockRegistered(mock)` works on every version and is the fix;
1151
+ recommend it whenever a suite on Vitest 5 loses a shared mock's implementation to
1152
+ `vi.resetAllMocks()`.
1153
+
1148
1154
  `getSpyEngine()` reports the current one, `'auto-spy'` is the default, and doubles already built keep
1149
1155
  the engine they were built with. The switch is Vitest-only: on Bun and `node:test` the runner's own
1150
1156
  matchers recognise only the runner's own mocks, so those adapters keep using them.
@@ -2222,7 +2228,7 @@ Spies created **before** the call stay there too. `mock.reset()` in `afterEach`
2222
2228
 
2223
2229
  **A `node:test` spy now prints under its method name.** `mock.fn()` takes no name and has no
2224
2230
  `mockName()`, so a spy inherited the library's internal dispatcher and read back as
2225
- `[Function: dispatch]` everywhere. The adapter names the *implementation* at creation and lets
2231
+ `[Function: dispatch]` everywhere. The adapter names the _implementation_ at creation and lets
2226
2232
  `mock.fn()` carry that name onto the mock, which is where a `node:test` mock takes its `name` from;
2227
2233
  `displayName` is set on the mock alongside it. The name survives `mock.reset()`, `mock.restore()`,
2228
2234
  `resetCalls()` and a `mockImplementation()` swap, and it is what `node:assert` diffs, what
@@ -2481,7 +2487,7 @@ packages, which a subpath export can never be.
2481
2487
  | `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
2482
2488
  | a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
2483
2489
  | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2484
- | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
2490
+ | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
2485
2491
  | `toHaveBeenCalledTimes` reads `0` for a call an earlier test made, after moving to Vitest 5 | `clearMocks` defaults to `true` on Vitest 5, so `vi.clearAllMocks()` runs before every test | count the calls in a plain variable instead of reading the spy, or set `clearMocks: false` (§10) |
2486
2492
  | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2487
2493
  | `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
@@ -2509,7 +2515,7 @@ packages, which a subpath export can never be.
2509
2515
  | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2510
2516
  | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
2511
2517
  | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
2512
- | `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
2518
+ | `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
2513
2519
  | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
2514
2520
  | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2515
2521
  | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
package/README.md CHANGED
@@ -228,7 +228,12 @@ mocks that were called, so a spy engine that is not `vi.fn()` had to make itself
228
228
  sweep; and the `Matchers` interface grew a second type parameter, so the bundled matchers declare
229
229
  themselves on Chai's `Assertion` instead and keep typing on 4 and 5 alike. The one thing Vitest 5
230
230
  can still break is its own doing: `clearMocks` is on by default there, so a spec asserting on a call
231
- recorded by an *earlier* test now reads zero. Record it in a counter, not in the spy.
231
+ recorded by an _earlier_ test now reads zero. Record it in a counter, not in the spy.
232
+
233
+ One feature of this package does need a line changed. `setupAutoSpy({ pruneMockRegistry: true })`
234
+ finds long-lived mocks by walking the runner's own registry, and Vitest 5 no longer exposes it — so
235
+ on Vitest 5 the automatic half does nothing and a mock that must survive `vi.resetAllMocks()` has to
236
+ say so: `keepMockRegistered(vi.fn().mockReturnThis())`. Everything after that mark behaves as before.
232
237
 
233
238
  Measured 2026-09-06 on 40 spec files, 800 tests, 400 spied methods per file, v8 coverage on,
234
239
  Node v24.19.0, median of five runs: the same suite takes **1383 ms on Vitest 4.1.11 and 1276 ms on
@@ -1341,26 +1346,26 @@ time budget — these cases allocate doubles by the tens of thousands, and GC co
1341
1346
  created rather than elapsed time, so a fixed time budget would have handed a faster arm more
1342
1347
  allocations and made it pay for its own speed.
1343
1348
 
1344
- **Every micro-benchmark table is this library's**, and the margin is against the *best* other arm in
1349
+ **Every micro-benchmark table is this library's**, and the margin is against the _best_ other arm in
1345
1350
  that table. The three class sizes are measured project profiles, not round numbers — across four
1346
1351
  private Angular suites (~2 700 spec files, 2 742 doubles built from a class) the median service has
1347
1352
  5-8 methods and the spec touches 1 of them:
1348
1353
 
1349
- | Case (micro) | 4.0 | 4.1 | best other arm | lead |
1350
- | --- | ---: | ---: | ---: | ---: |
1351
- | small project — 6 methods, 1 called | — | **1.42 µs** | 6.83 µs hand-written | 4.82x |
1352
- | medium project — 14 methods, 2 called | — | **2.67 µs** | 15.92 µs hand-written | 5.97x |
1353
- | large project — 45 methods, 2 called | — | **5.79 µs** | 52.79 µs hand-written | 9.11x |
1354
- | worst case — all 14 of 14 methods called | 18.92 µs (0.66x, a loss) | **8.17 µs** | 17.92 µs hand-written | 2.19x |
1355
- | worst case — all 45 of 45 methods called | 75.33 µs (0.71x, a loss) | **26.12 µs** | 62.04 µs hand-written | 2.37x |
1356
- | double from a type — 2 / 10 / 40 members | 3.58 / 18.17 / 72.88 µs (a loss) | **1.00 / 4.67 / 18.92 µs** | 2.79 / 13.83 / 56.79 µs vitest-mock-extended | 2.79-3.00x |
1357
- | deep double, 3 levels, leaf called | 8.83 µs (0.61x, a loss) | **2.29 µs** | 5.46 µs vitest-mock-extended | 2.38x |
1358
- | configure a return + 3 calls — class / type | 3.29 / 2.08 µs (type a loss) | **2.21 / 0.71 µs** | 16.38 µs hand-written / 1.58 µs @golevelup | 7.41x / 2.24x |
1359
- | `calledWith` dispatch, 2 configured + 1 miss | 0.54 µs (parity) | **0.17 µs** | 0.54 µs vitest-mock-extended | 3.25x |
1354
+ | Case (micro) | 4.0 | 4.1 | best other arm | lead |
1355
+ | -------------------------------------------- | -------------------------------: | -------------------------: | -------------------------------------------: | ------------: |
1356
+ | small project — 6 methods, 1 called | — | **1.42 µs** | 6.83 µs hand-written | 4.82x |
1357
+ | medium project — 14 methods, 2 called | — | **2.67 µs** | 15.92 µs hand-written | 5.97x |
1358
+ | large project — 45 methods, 2 called | — | **5.79 µs** | 52.79 µs hand-written | 9.11x |
1359
+ | worst case — all 14 of 14 methods called | 18.92 µs (0.66x, a loss) | **8.17 µs** | 17.92 µs hand-written | 2.19x |
1360
+ | worst case — all 45 of 45 methods called | 75.33 µs (0.71x, a loss) | **26.12 µs** | 62.04 µs hand-written | 2.37x |
1361
+ | double from a type — 2 / 10 / 40 members | 3.58 / 18.17 / 72.88 µs (a loss) | **1.00 / 4.67 / 18.92 µs** | 2.79 / 13.83 / 56.79 µs vitest-mock-extended | 2.79-3.00x |
1362
+ | deep double, 3 levels, leaf called | 8.83 µs (0.61x, a loss) | **2.29 µs** | 5.46 µs vitest-mock-extended | 2.38x |
1363
+ | configure a return + 3 calls — class / type | 3.29 / 2.08 µs (type a loss) | **2.21 / 0.71 µs** | 16.38 µs hand-written / 1.58 µs @golevelup | 7.41x / 2.24x |
1364
+ | `calledWith` dispatch, 2 configured + 1 miss | 0.54 µs (parity) | **0.17 µs** | 0.54 µs vitest-mock-extended | 3.25x |
1360
1365
 
1361
1366
  Six of those rows were losses and one was parity. What changed is that a method spy is no longer a
1362
1367
  `vi.fn()` — see [The spy engine](#the-spy-engine) — plus one thing that is not the engine: a
1363
- `calledWith(x)` of a single primitive argument used to be rendered into a string key on *every call*
1368
+ `calledWith(x)` of a single primitive argument used to be rendered into a string key on _every call_
1364
1369
  of that spy, where a `Map` keyed by the value does the same lookup with no allocation.
1365
1370
 
1366
1371
  The three class sizes changed in the same release, from round numbers to measured profiles, so the
@@ -1373,10 +1378,10 @@ vi.fn() per method` arm is in the table precisely so its size stays visible.
1373
1378
  the 4.1 build, 20-method class, `isolate: true`, coverage on, 3 rounds per cell after a discarded
1374
1379
  warm-up, medians of the three):
1375
1380
 
1376
- | Comparison | 1 000 tests | 3 000 tests | 10 000 tests |
1377
- | --- | ---: | ---: | ---: |
1378
- | vitest-auto-spy | 1.33 s | 2.86 s | 10.02 s |
1379
- | hand-written `vi.fn()` | 1.31 s (0.99×) | 2.85 s (1.00×) | 9.25 s (0.92×) |
1381
+ | Comparison | 1 000 tests | 3 000 tests | 10 000 tests |
1382
+ | --------------------------- | -------------: | -------------: | --------------: |
1383
+ | vitest-auto-spy | 1.33 s | 2.86 s | 10.02 s |
1384
+ | hand-written `vi.fn()` | 1.31 s (0.99×) | 2.85 s (1.00×) | 9.25 s (0.92×) |
1380
1385
  | @bugsplat/vitest-auto-spies | 2.00 s (1.50×) | 4.61 s (1.61×) | 15.45 s (1.54×) |
1381
1386
 
1382
1387
  Hand-written doubles are still cheaper — **about 3% at the median**, down from 10-15% before 4.1,
@@ -1387,11 +1392,11 @@ suite swamps it.
1387
1392
 
1388
1393
  **Memory, `isolate: false`, 100-method class, 10 000 tests, peak RSS:**
1389
1394
 
1390
- | Arm | Peak RSS |
1391
- | --- | ---: |
1392
- | hand-written `vi.fn()` | 6366 MB |
1393
- | vitest-auto-spy, default | 2103 MB |
1394
- | vitest-auto-spy, `lazySpies: 'proxy'` | 1851 MB |
1395
+ | Arm | Peak RSS |
1396
+ | ------------------------------------- | -------: |
1397
+ | hand-written `vi.fn()` | 6366 MB |
1398
+ | vitest-auto-spy, default | 2103 MB |
1399
+ | vitest-auto-spy, `lazySpies: 'proxy'` | 1851 MB |
1395
1400
 
1396
1401
  Hand-written doubles use 3.0x the default's peak RSS here — the difference between finishing and an
1397
1402
  OOM'd worker on a large suite.
@@ -2508,7 +2513,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
2508
2513
  | `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
2509
2514
  | `setupAngularTestEnv(opts)` | `/angular` | Zone and zoneless spec files in one worker, switching platforms per file |
2510
2515
  | `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
2511
- | `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
2516
+ | `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
2512
2517
  | `trackMockRegistry()` / `keepMockRegistered(mock)` / `restoreLongLivedImplementations()` | `/setup` | Keep @vitest/spy's mock registry to the mocks that outlive a file; mark one the split would miss; put back an implementation a cross-file `vi.resetAllMocks()` dropped ([details](#test-run-hygiene)) |
2513
2518
  | `trackNodeMocks()` / `pruneNodeMocks()` / `countNodeMocks()` | `/node` | Give this library its own `node:test` `MockTracker` so a dropped spy is freed — 21× less retained heap; sweep by hand, and read the count back |
2514
2519
  | `setSpyEngine(engine)` / `getSpyEngine()` | `/setup` | Build method spies from this library's own mock (`'auto-spy'`, the default) or from `vi.fn()` (`'runner'`) ([details](#the-spy-engine)) |
@@ -2752,10 +2757,10 @@ expensive to diagnose when it is missing. The first three are on by default:
2752
2757
  so the 404 is never written and a routing mistake is reported as a slow test.
2753
2758
  11. **Web Storage the runner never handed over.** On by default, because it only ever repairs.
2754
2759
  Vitest copies a DOM environment's globals onto `globalThis` behind `if (k in global) return
2755
- KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
2760
+ KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
2756
2761
  arrive only because Node put neither on `globalThis`, so the first half was false. Node's own
2757
2762
  Web Storage made the key exist, and now the environment's storage never arrives: `setItem is
2758
- not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
2763
+ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
2759
2764
  filter runs before either. A suite stays green with this broken — only the specs that touch
2760
2765
  storage fail — so it arrives as "CI moved to a new Node and eleven unrelated specs died". The
2761
2766
  repair decides by using the storage rather than looking at it, replaces only one that cannot
@@ -2773,7 +2778,7 @@ expensive to diagnose when it is missing. The first three are on by default:
2773
2778
  | `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
2774
2779
  | `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
2775
2780
  | `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
2776
- | `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
2781
+ | `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
2777
2782
  | `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
2778
2783
  | `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
2779
2784
  | `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "4.6.0",
3
+ "version": "4.6.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 and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",