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.
- package/AGENTS.md +10 -4
- package/README.md +32 -27
- 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
|
|
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
|
|
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>`
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
1350
|
-
|
|
|
1351
|
-
| small project — 6 methods, 1 called
|
|
1352
|
-
| medium project — 14 methods, 2 called
|
|
1353
|
-
| large project — 45 methods, 2 called
|
|
1354
|
-
| worst case — all 14 of 14 methods called
|
|
1355
|
-
| worst case — all 45 of 45 methods called
|
|
1356
|
-
| double from a type — 2 / 10 / 40 members
|
|
1357
|
-
| deep double, 3 levels, leaf called
|
|
1358
|
-
| configure a return + 3 calls — class / type
|
|
1359
|
-
| `calledWith` dispatch, 2 configured + 1 miss |
|
|
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
|
|
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
|
|
1377
|
-
|
|
|
1378
|
-
| vitest-auto-spy
|
|
1379
|
-
| hand-written `vi.fn()`
|
|
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
|
|
1391
|
-
|
|
|
1392
|
-
| hand-written `vi.fn()`
|
|
1393
|
-
| vitest-auto-spy, default
|
|
1394
|
-
| vitest-auto-spy, `lazySpies: 'proxy'` |
|
|
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?)`
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|