vitest-auto-spy 5.18.0 → 5.19.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.
Files changed (76) hide show
  1. package/AGENTS.md +510 -97
  2. package/README.md +402 -177
  3. package/dist/angular-http.js +27 -5
  4. package/dist/angular-router.d.ts +1 -1
  5. package/dist/angular-router.js +4 -3
  6. package/dist/angular.d.ts +40 -24
  7. package/dist/angular.js +946 -345
  8. package/dist/bun-angular.d.ts +6 -6
  9. package/dist/bun-angular.js +120 -63
  10. package/dist/bun.d.ts +75 -75
  11. package/dist/bun.js +9 -9
  12. package/dist/{chunk-WAMFS3UI.js → chunk-2ERHG4NE.js} +192 -72
  13. package/dist/{chunk-33NJHIS2.js → chunk-3LU77DBV.js} +12 -15
  14. package/dist/chunk-3VXZRDH3.js +12 -0
  15. package/dist/{chunk-DUFMKOKF.js → chunk-4JTRYOL2.js} +3 -2
  16. package/dist/{chunk-M6VMOOZ2.js → chunk-CEDA5PUY.js} +1 -1
  17. package/dist/{chunk-MAKQSO2R.js → chunk-DPTWTLIG.js} +27 -4
  18. package/dist/{chunk-4SJ7EGIY.js → chunk-F3OQDSKX.js} +385 -87
  19. package/dist/{chunk-MUJTPXMJ.js → chunk-HMECO5PK.js} +333 -87
  20. package/dist/{chunk-Q477VHHF.js → chunk-JJVTDL5R.js} +8 -7
  21. package/dist/{chunk-ZU55UY2M.js → chunk-LBQWJZPS.js} +12 -3
  22. package/dist/{chunk-OTOCFH6B.js → chunk-R3DF5ZEO.js} +2 -1
  23. package/dist/chunk-SOROJD3W.js +9 -0
  24. package/dist/{chunk-DTZCPCMA.js → chunk-TCGO3VCY.js} +2 -1
  25. package/dist/chunk-TZ7DRSRD.js +9 -0
  26. package/dist/chunk-X4BVCE4V.js +78 -0
  27. package/dist/cli.js +394 -89
  28. package/dist/console.d.ts +11 -1
  29. package/dist/console.js +28 -11
  30. package/dist/diagnostics.d.ts +15 -1
  31. package/dist/diagnostics.js +86 -20
  32. package/dist/dom-stubs.d.ts +11 -29
  33. package/dist/dom-stubs.js +50 -29
  34. package/dist/eslint-plugin.cjs +239 -146
  35. package/dist/eslint-plugin.d.cts +13 -244
  36. package/dist/eslint-plugin.d.ts +9 -0
  37. package/dist/{expect-emission-CmN9ADyN.d.ts → expect-emission-QuF55ViS.d.ts} +1 -1
  38. package/dist/index.d.ts +5 -5
  39. package/dist/index.js +724 -228
  40. package/dist/jasmine-compat.d.ts +35 -22
  41. package/dist/jasmine-compat.js +3 -1
  42. package/dist/jasmine.d.ts +2 -2
  43. package/dist/jasmine.js +50 -15
  44. package/dist/nestjs.d.ts +2 -2
  45. package/dist/nestjs.js +11 -12
  46. package/dist/node.cjs +730 -226
  47. package/dist/node.d.ts +5 -5
  48. package/dist/node.js +3072 -11
  49. package/dist/observer-spy.d.ts +7 -2
  50. package/dist/observer-spy.js +44 -9
  51. package/dist/{package-identity-lm-gqZAR.d.ts → package-identity-C2L4fM0k.d.ts} +1 -1
  52. package/dist/{prop-mock-DnYNBNJL.d.ts → prop-mock-DMqE-ldi.d.ts} +1 -1
  53. package/dist/react.d.ts +5 -5
  54. package/dist/react.js +3574 -15
  55. package/dist/rstest.d.ts +5 -5
  56. package/dist/rstest.js +9 -9
  57. package/dist/rxjs.d.ts +2 -2
  58. package/dist/rxjs.js +43 -3
  59. package/dist/setup.d.ts +65 -11
  60. package/dist/setup.js +1195 -136
  61. package/dist/shared-state.js +40 -12
  62. package/dist/signal-forms.js +2 -1
  63. package/dist/svelte.d.ts +5 -5
  64. package/dist/svelte.js +3574 -15
  65. package/dist/{track-injections-CIHJWFpp.d.ts → track-injections-BfNI9fHy.d.ts} +1 -1
  66. package/dist/{track-signal-runs-BOfsdYsC.d.ts → track-signal-runs-C5nd4A4_.d.ts} +27 -7
  67. package/dist/{types-D3RkJjc_.d.ts → types-B7Wjo00D.d.ts} +95 -7
  68. package/dist/vue.d.ts +6 -6
  69. package/dist/vue.js +3571 -16
  70. package/dist/zone.js +12 -1
  71. package/package.json +11 -5
  72. package/skills/vitest-auto-spy/SKILL.md +132 -105
  73. package/dist/chunk-M5BULP7X.js +0 -41
  74. package/dist/chunk-N3T7KWWA.js +0 -157
  75. package/dist/chunk-OS4PTU6D.js +0 -99
  76. package/dist/chunk-PA4ZNLNZ.js +0 -8
package/AGENTS.md CHANGED
@@ -88,7 +88,19 @@ and under pnpm's isolated layout it never resolved before.
88
88
 
89
89
  The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
90
90
  CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
91
- Vitest refuses to be required).
91
+ Vitest refuses to be required). The plugin's CommonJS declaration is an `export =`, which is what
92
+ `require('vitest-auto-spy/eslint-plugin')` actually returns — an `eslint.config.cts` / `.cjs` used to
93
+ type-check the call that throws and reject the one that works.
94
+
95
+ `vitest-auto-spy/package.json` resolves as well. Tools that read a dependency's manifest by specifier
96
+ — Storybook, Nx, a renovate helper — used to get `ERR_PACKAGE_PATH_NOT_EXPORTED` and had nowhere to
97
+ go from there.
98
+
99
+ **Every peer is optional now, `vitest` included.** The range is unchanged (`>=2.1.0`); what changed
100
+ is that a suite on `/bun` or `/node`, which never loads the Vitest runner, no longer installs it to
101
+ satisfy a peer. The one thing still to know is that `/bun`, `/bun-angular` and `/node` declarations
102
+ name Vitest's `Mock` type, so a project type-checking those entries without `vitest` installed will
103
+ want it as a devDependency anyway; freeing that is a type change and waits for a major.
92
104
 
93
105
  ---
94
106
 
@@ -201,6 +213,13 @@ asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // →
201
213
  `asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
202
214
  sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
203
215
 
216
+ **A deep node asks what the spy surface is on every read**, rather than remembering the answer from
217
+ the first deep mock of the worker. That matters under `isolate: false`, where the surface grows
218
+ mid-run: `import 'vitest-auto-spy/rxjs'` in a later file adds `nextWith` and friends, and
219
+ `setSpyEngine` swaps the whole prototype. With the answer cached, `deep.feed.items.nextWith(1)` in
220
+ every double built after that resolved to a **child node** — callable, recorded, emitting nothing —
221
+ while the spec waited on a stream that was never fed.
222
+
204
223
  **`selfReturning: true` chains a factory, not a `return this` builder**, and the difference decides
205
224
  where the calls are recorded. A called node answers _itself_, not the object the method was read
206
225
  off, so every hop moves one level deeper: `editor.chain().focus().insertContent('text')` records
@@ -354,15 +373,35 @@ five providers across two thousand tests is about two hundredths of a second. Ca
354
373
  `autoSpyAccessors: true` walks the prototype chain uncached on every call — name the accessors
355
374
  instead.
356
375
 
357
- Memory is the exception, and only on classes wide enough for it to matter. `lazySpies: true` still
358
- defines one accessor per method, and that placeholder is nearly all of what an untouched double
359
- retains: an untouched 100-method double holds 25 601 B against 4 097 B with `lazySpies: 'proxy'`,
360
- which answers every method from a single trap object instead — 256 B per method against 41 B
361
- (`npm run bench:memory`, 2026-09-04). It is opt-in because a `Proxy` cannot remove itself: the
362
- default leaves a plain data property behind once a method materialises, while the proxy goes through
363
- a trap on every read and every call for the life of the double, and on a narrow class it is also
364
- slower to create. Reach for it on generated API clients and ngrx facades under `isolate: false`,
365
- where the doubles of a whole file are alive at once; leave the default everywhere else.
376
+ Memory used to be the exception, and it no longer is for the default. `lazySpies: true` still
377
+ defines one property per method, but the accessor pair behind it is now **shared by method name**
378
+ across every double in the process, so the placeholder is no longer what an untouched double
379
+ retains: an untouched 100-method double holds **215 B** where it held 25 593, and a 300-method one
380
+ 284 B where it held 70 165 (`npm run bench:memory`, 2026-09-17, Node 24). The price is paid at
381
+ construction on very wide classes — building a 300-method double costs about 28 % more — and
382
+ materialising every method of one is about 26 % cheaper in exchange.
383
+
384
+ That inverts the case for `lazySpies: 'proxy'`, which answers every method from a single trap
385
+ object: it now retains **4 090 B** against the default's 215 B at 100 methods — 19× more — and still
386
+ pays a trap on every read (53 ns against 7 ns) for the life of the double, because a `Proxy` cannot
387
+ remove itself while the default leaves a plain data property behind once a method materialises. What
388
+ is left of its advantage is build time on a wide class. The option is unchanged and still supported;
389
+ there is no longer a memory reason to reach for it.
390
+
391
+ A frozen or sealed double is fine now. `Object.freeze(spy)` — a deep-freeze fixture helper, a
392
+ dev-mode state guard — used to make the first read of any method throw `Cannot redefine property`
393
+ from inside the placeholder's getter; the spy is kept beside the double instead, so the read answers
394
+ a stable mock and `mockReturnValue` on it works. On a merely sealed double an assignment still
395
+ reaches the member.
396
+
397
+ **Symbol-keyed methods are discovered and spied.** A method under a symbol the project owns —
398
+ `[SERIALIZE]()`, `Symbol.for('app.render')` — is a method like any other, resets with the rest and
399
+ shows up in `Reflect.ownKeys`. The runtime's own protocol symbols are deliberately left alone
400
+ (everything on `Symbol` itself — `Symbol.iterator`, `Symbol.dispose`, `Symbol.toPrimitive` and the
401
+ rest — plus `Symbol.for('nodejs.util.inspect.custom')`), because a double that answers those stops
402
+ being a double of `T` and starts impersonating an iterable or a disposable. Discovery covers methods
403
+ only: a symbol-keyed **getter or setter** is not found by `autoSpyAccessors`, so patch one with
404
+ `mockAccessorsProp` (§9).
366
405
 
367
406
  ---
368
407
 
@@ -480,8 +519,9 @@ included — TypeScript resolves a type-only import the same way.
480
519
  users.getName.calledWith(1).mockReturnValue('Ada');
481
520
  // argument enforcement — other arguments throw
482
521
  users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
483
- // asymmetric matchers work in both
522
+ // asymmetric matchers work in both, at any depth
484
523
  users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
524
+ users.save.calledWith({ id: expect.any(Number), tags: [expect.any(String)] }).mockReturnValue(true);
485
525
  // re-registering the same arguments replaces the answer — matcher arguments included
486
526
  users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(false);
487
527
 
@@ -523,6 +563,31 @@ makes a second `calledWith(1, expect.anything())` an override rather than a seco
523
563
  behind the first. A hand-rolled `{ asymmetricMatch }` object is compared by identity instead: its
524
564
  verdict is a closure, so only re-registering that same instance overrides.
525
565
 
566
+ **A matcher counts wherever it sits, not only at the top level.** A config argument holding a
567
+ matcher — or a function — anywhere inside it is compared structurally rather than as data:
568
+ `calledWith({ id: expect.any(Number) })`, `calledWith([expect.any(String)])`, a matcher inside a
569
+ `Map` value or a `Set` member. The same comparison decides the rest of an argument's shape, so it is
570
+ worth knowing what it treats as equal: `Map` and `Set` are compared **without regard to insertion
571
+ order**, a `Date` by its time, a `RegExp` by its source and flags, an `Error` by its `name` and
572
+ `message` plus its own enumerable fields, a function by identity, and symbol-keyed properties
573
+ participate like string ones. Cycles are handled, so a component graph with a back-edge is a legal
574
+ argument. `mustBeCalledWith` uses the same map, and its `Wanted:` line renders a matcher as
575
+ `Any<Number>` rather than as the object it serialises to.
576
+
577
+ Each `calledWith(...)` / `mustBeCalledWith(...)` call hands back **its own** handle, so a chain kept
578
+ in a variable stays attached to the arguments it was written with:
579
+
580
+ ```ts
581
+ const found = users.load.calledWith(1);
582
+ users.load.calledWith(2).mockReturnValue(undefined);
583
+
584
+ found.mockReturnValue({ id: 1 }); // configures 1, not 2
585
+ ```
586
+
587
+ `new` on a method spy works: `new sdk.Client()` — the shape `createAutoMock<{ Client: typeof Client }>()`
588
+ and `mockDeep` produce — hands back the instance, or the object a `calledWith(...).mockReturnValue(...)`
589
+ configured for those arguments.
590
+
526
591
  `mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
527
592
  all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
528
593
 
@@ -541,11 +606,28 @@ onDone.value(); // and now exercise what was passed
541
606
  expect(component.finished()).toBe(true);
542
607
  ```
543
608
 
544
- `.values` holds every match, oldest first; `.captured` asks without triggering the "nothing was
545
- captured" throw; `.reset()` lets one captor serve two phases. **Assertions only** — a captor matches
546
- every value, so putting one in `calledWith` would configure a return for every call, which is
547
- `mockReturnValue` spelled less clearly, and `calledWith` is typed to the method's own parameters so
548
- it will not compile anyway.
609
+ `.values` holds every value the captor was **offered**, oldest first — candidates, not matches. A
610
+ captor in position 0 of `toHaveBeenCalledWith(captor, 3)` is asked about the first argument of every
611
+ call the runner tries, including the ones the `3` then rejects, so `.values` can be longer than the
612
+ set of calls the assertion accepted. `.captured` asks whether anything was recorded without
613
+ triggering the "nothing was captured" throw that reading `.value` raises; `.reset()` lets one captor
614
+ serve two phases.
615
+
616
+ `captureArg({ where })` narrows both halves at once — the filter decides what is recorded **and**
617
+ whether that position matches at all, so a rejected candidate leaves no entry in `.values` and the
618
+ whole expectation fails on it:
619
+
620
+ ```ts
621
+ const config = captureArg<RequestInit>({ where: (value) => (value as RequestInit)?.method === 'POST' });
622
+
623
+ expect(fetchSpy).toHaveBeenCalledWith(url, config); // only the POST call satisfies this
624
+ expect(config.value.headers).toEqual({ 'x-trace': '1' });
625
+ ```
626
+
627
+ The filter receives the raw argument as `unknown`, so narrow it yourself. **Assertions only** — a
628
+ captor without `where` matches every value, so putting one in `calledWith` would configure a return
629
+ for every call, which is `mockReturnValue` spelled less clearly, and `calledWith` is typed to the
630
+ method's own parameters so it will not compile anyway.
549
631
 
550
632
  **The observable helpers are backed by a `ReplaySubject(1)` that belongs to the spy, and it is
551
633
  configuration — so it must be reset with the rest of it.** Two failures used to come out of that
@@ -563,7 +645,14 @@ The code under test therefore ran the **success** branch on stale data, and the
563
645
  one emission late. The second: `error()` and `complete()` close a Subject permanently, so a later
564
646
  `nextWith` on that spy pushed into a dead subject and emitted nothing at all. Both are fixed —
565
647
  `resetAutoSpy(spy)` now drops the subject, and a terminated one is replaced on the next
566
- configuration.
648
+ configuration. That holds for a subject a spec closed **itself**, too:
649
+ `spy.items$.returnSubject().complete()` marks the stream closed, so the next `nextWith` opens a new
650
+ one rather than disappearing.
651
+
652
+ `nextWithValues` on an observable **property** builds a new stream, which a subscriber that already
653
+ holds the old one never sees — the spec's values go nowhere and the assertion below reads the
654
+ initial state. That case is reported now rather than passing silently; `nextWith` pushes into the
655
+ subject the current subscriber is on and is the one to reach for mid-test.
567
656
 
568
657
  What that does **not** change: `vi.clearAllMocks()` and `clearMocks: true` still cannot reach it,
569
658
  for the same reason they cannot reach a `calledWith` chain — that state lives in this library's
@@ -726,7 +815,8 @@ winning, scalars decided by the call site when it names the key. The bare-array
726
815
  walking the prototype chain would let one registration change doubles in files nobody was looking at.
727
816
  A second registration for the same class replaces the first, because two of them in one suite is the
728
817
  drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
729
- `clearAutoSpyDefaults()` the lot.
818
+ `clearAutoSpyDefaults()` the lot. `createSpyFromInstance(obj)` reads the registration of the class
819
+ `obj.constructor` names, merged the same way; an object literal resolves none.
730
820
 
731
821
  A setup file that registers more than a handful of classes can say them as one table instead of one
732
822
  call each. Rows apply in order, and each is checked against **its own** class — a key `Router` does
@@ -858,6 +948,12 @@ Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs:
858
948
  `accessorSpies.setters.theme` was `undefined`, and the failure read
859
949
  `Cannot read properties of undefined` three steps from the configuration behind it.
860
950
 
951
+ The bag is typed over every key of `T` unless the lists are repeated in the options type argument —
952
+ `createSpyFromClass<Settings, { gettersToSpyOn: ['theme'] }>(Settings, { gettersToSpyOn: ['theme'] })`
953
+ keys both halves by exactly the configured names, so `accessorSpies.setters.other` is a compile error
954
+ instead of an `undefined` at run time. Use it when a spec reaches into the bag by name; a
955
+ non-literal `string[]` falls back to the every-key bag.
956
+
861
957
  Only spy a getter when the spec asserts that it was **read**. To make one _answer_ something, on a
862
958
  spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
863
959
  factory, which is the part that is otherwise found by trial:
@@ -957,6 +1053,14 @@ resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mo
957
1053
  Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
958
1054
  proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
959
1055
 
1056
+ **`resetAutoSpy` is `vi.resetAllMocks()` for one double**, and two things it used to leave behind now
1057
+ go with the rest: a pending `mockReturnValueOnce` / `mockResolvedValueOnce` queue (it used to answer
1058
+ the first call _after_ the reset, in a later test) and an accessor spy's configuration (a
1059
+ `accessorSpies.getters.x.mockReturnValue(…)` used to survive and hand the next test the previous
1060
+ test's value). A spec that relied on a queued `Once` value outliving the reset reads `undefined`
1061
+ now. The double stays usable: the library's own dispatch is put back afterwards, so a fresh
1062
+ `calledWith` configures it as normal.
1063
+
960
1064
  Every double also carries `[Symbol.dispose]()` — it runs `resetAutoSpy(this)` — so an `afterEach`
961
1065
  that exists only to reset one spy can be deleted:
962
1066
 
@@ -1001,7 +1105,8 @@ await expectCompletion(service.purgeCache()); // "it finished" — the value is
1001
1105
  ```
1002
1106
 
1003
1107
  Options: `{ timeout, label }`. `timeout` defaults to `1000` ms (`0` for `expectNoEmission`, whose
1004
- wait is a quiet window rather than a watchdog). The source is duck-typed, so rxjs `Observable`s,
1108
+ wait is a quiet window rather than a watchdog). `0` and `Infinity` both mean **no watchdog** — the
1109
+ wait then runs to the runner's own test timeout, which is the trade. The source is duck-typed, so rxjs `Observable`s,
1005
1110
  `Subject`s, Angular `toObservable()` results, Angular `output()` (`OutputEmitterRef`, whose
1006
1111
  `subscribe` takes a bare callback) and hand-rolled subscribables all work — and every helper infers
1007
1112
  the emitted type, so `expectEmission(of(1))` is a `Promise<number>`.
@@ -1069,15 +1174,34 @@ await expect(expectEmission(purchased$, { advance: () => vi.runAllTimers() })).r
1069
1174
  That replaces the fragile shape people arrive at — hold the promise, advance, then await — which
1070
1175
  breaks silently the moment somebody adds an `await` one line above it. It is a callback rather than
1071
1176
  an `advanceTimers: true` flag because these helpers are in the core entry, which contains no test
1072
- runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`.
1073
-
1074
- **The watchdog runs on real time, on purpose — even under fake timers.** A virtual one would race
1075
- the timers the spec advances: `expectEmission(source$, { timeout: 200 })` followed by
1076
- `vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the spec was
1077
- about to advance into. The cost is that in a suite with global fake timers a _failing_ assertion
1078
- spends a real second. Do **not** answer that with `{ timeout: 0 }` at every call site — that
1079
- disables the watchdog, and the next silent stream hangs to the runner's own timeout with nothing
1080
- useful in the message. Lower the default once instead:
1177
+ runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`. A throw out of
1178
+ `advance` — or out of an `until` predicate — is reported as itself and tears the subscription down,
1179
+ rather than being lost while the wait runs on to the timeout.
1180
+
1181
+ **These helpers subscribe as a subscriber, so a synchronous source stops at the value that settles
1182
+ the wait.** `expectEmission(from([1, 2, 3]).pipe(tap(spy)))` calls `tap` once, not three times, so
1183
+ `expect(spy).toHaveBeenCalledTimes(1)` is honest and a `finalize` runs at the stop; before, the
1184
+ producer ran to completion before anything could unsubscribe, and an endless synchronous source
1185
+ (`of(1).pipe(repeat())`) hung the worker instead of resolving. `expectEmissions(source$, 3)` stops
1186
+ at the third. A source that cannot be subscribed to at all is reported by name now, with a separate
1187
+ hint when what was passed is a promise.
1188
+
1189
+ `expectEmissions(source$, 0)` throws at the call — a count below 1 is not something a stream can
1190
+ satisfy, and the message names `expectNoEmission` instead. A suite that wrote
1191
+ `expectEmissions(s, expected.length)` with an empty expectation is the one this changes.
1192
+
1193
+ **The watchdog runs on real time, on purpose — even under fake timers, and even under zone.js.** A
1194
+ virtual one would race the timers the spec advances: `expectEmission(source$, { timeout: 200 })`
1195
+ followed by `vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the
1196
+ spec was about to advance into. Inside `fakeAsync` that used to happen anyway, because zone.js
1197
+ patches the global `setTimeout` and the watchdog was scheduled onto the virtual queue: a
1198
+ `tick(1_500)` towards a `debounceTime(2_000)` rejected the wait it was advancing. The timer is taken
1199
+ from `__zone_symbol__setTimeout` now, so it is outside the zone and `tick()` cannot reach it.
1200
+
1201
+ The cost is that in a suite with global fake timers a _failing_ assertion spends a real second. Do
1202
+ **not** answer that with `{ timeout: 0 }` at every call site — that disables the watchdog, and the
1203
+ next silent stream hangs to the runner's own timeout with nothing useful in the message. Lower the
1204
+ default once instead:
1081
1205
 
1082
1206
  ```ts
1083
1207
  // vitest.setup.ts
@@ -1088,6 +1212,11 @@ setupAutoSpy({ globalFakeTimers: true });
1088
1212
  setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
1089
1213
  ```
1090
1214
 
1215
+ `setEmissionTimeout` takes `0` (no watchdog) and `Infinity` (the same), and **throws** on `NaN` and
1216
+ on a negative number rather than installing a default that fails every wait. The commonest way to
1217
+ reach it is arithmetic: `setEmissionTimeout(Number(process.env.EMISSION_TIMEOUT))` with the variable
1218
+ unset.
1219
+
1091
1220
  **The code frame these failures open is the spec line, not this package.** The helpers build their
1092
1221
  error inside a `subscribe` or timer callback, so the stack is captured at helper entry and pinned on
1093
1222
  when the failure is finally built; only errors these helpers make themselves are re-anchored, and
@@ -1261,6 +1390,15 @@ setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).
1261
1390
  A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
1262
1391
  the previous file is the one to fix.
1263
1392
 
1393
+ **`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
1394
+ away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
1395
+ the fake clock hands out bypasses the count entirely — which makes `expect(countStrayTimers()).toBe(0)`
1396
+ vacuous in any file running on a frozen clock, `setupAutoSpy({ strayTimers: true, globalFakeTimers: true })`
1397
+ included. Read `vi.getTimerCount()` for the fake clock's own backlog; the two answer different
1398
+ questions and neither covers the other. A handle cleared by the number it coerces to
1399
+ (`clearTimeout(+handle)`) or through its own `close()` is forgotten properly now, so those stopped
1400
+ being counted as strays.
1401
+
1264
1402
  **With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
1265
1403
  arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
1266
1404
  Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
@@ -1288,8 +1426,10 @@ file does (`import 'zone.js';`) or the Angular builder does. Without it the call
1288
1426
  than quietly watching nothing. Native, non-zone rejections already fail a Vitest run, and nothing
1289
1427
  here touches them. The pieces are exported too — `trackStrayRejections()` (idempotent, returns the
1290
1428
  undo), `flushStrayRejections()` (takes what was captured and starts again from empty) and
1291
- `countStrayRejections()`. The `no-floating-assertion` lint rule catches the commonest shape before
1292
- it ever runs (§16).
1429
+ `countStrayRejections()`. The count is complete; the **reasons** retained are capped at 100, so a
1430
+ file rejecting in a loop cannot hold its whole run's worth of stacks. A promise rejected with
1431
+ `undefined` is reported as that rather than as `[object Object]`. The `no-floating-assertion` lint
1432
+ rule catches the commonest shape before it ever runs (§16).
1293
1433
 
1294
1434
  **`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
1295
1435
  about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
@@ -1435,6 +1575,12 @@ another:
1435
1575
  setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setting
1436
1576
  ```
1437
1577
 
1578
+ **Called twice, the last caller's mode wins.** The setup file installs from its own `beforeEach`,
1579
+ which runs before the spec's, so a spec calling `blockNetwork({ xhr: 'reject' })` because the failure
1580
+ branch is what it is testing used to be served the setup file's empty 200 and pass on the wrong
1581
+ branch. The stubs are also installed once rather than re-journalled per test, so `restoreProps: false`
1582
+ no longer grows the restore journal for the length of the run.
1583
+
1438
1584
  A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
1439
1585
  serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
1440
1586
  and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
@@ -1571,6 +1717,17 @@ reads as a regression and is not one.
1571
1717
  `Date is not a constructor` with a stack in production code and no mention of timers.
1572
1718
  `mockSystemTime` does the right thing either way.
1573
1719
 
1720
+ Two rules about which of these composes with which, both of them earned:
1721
+
1722
+ - **`withSystemTime` puts the previous time back**, including when fake timers were already running.
1723
+ Its undo used to be a no-op there, so the clock stayed where the callback left it for the rest of
1724
+ the test.
1725
+ - **`mockSystemTime()` installs `Date` and nothing else**, so `advanceTimers()` under it has nothing
1726
+ to advance. It used to pass having done nothing; it now refuses and names `setupFakeTimers()`.
1727
+ `setupFakeTimers(config)` installs the config it was given rather than deferring to whatever fakes
1728
+ were already up — a nested call, a global pair plus a local one, and the `Date`-only case all now
1729
+ end with the clock the call asked for, where the outer configuration used to win silently.
1730
+
1574
1731
  `useCountingClock` exists because under fake timers every call inside one test reports the _same_
1575
1732
  "now", so a spec that asserts on **order** or **duration** — analytics batches, tracing, a rate
1576
1733
  limiter, a TTL cache — cannot express its expectation at all. Patching `Date.now` by hand does not
@@ -1640,8 +1797,18 @@ setupAutoSpy({ guardGlobals: 'throw' }); // or 'warn' while a suite is being cle
1640
1797
  `Object.defineProperty(document, 'cookie', { value })` defaults `configurable` to `false`, so the
1641
1798
  property can no longer be redefined _or_ deleted. Under `isolate: false` every later file in the
1642
1799
  worker inherits it, and what fails is some library, every other run, with nothing naming the file
1643
- that did it. The guard compares `globalThis` / `document` / `navigator` around every test and reports
1644
- only what appeared and cannot be removed.
1800
+ that did it. The guard compares `globalThis`, `document`, `navigator`, `location`, `screen` and the
1801
+ DOM prototypes a Jest-era stub reaches for — `Element`, `HTMLElement`, `HTMLCanvasElement`,
1802
+ `HTMLMediaElement`, `Node`, `EventTarget` — around every test, symbol keys included, and reports only
1803
+ what appeared and cannot be removed.
1804
+
1805
+ **Its blind spot is an existing name redefined in place.** The guard reads a descriptor only for a
1806
+ key that was not there before, so `Object.defineProperty(document, 'cookie', { value, configurable: false })`
1807
+ over the `cookie` the environment already has is invisible to it: the name was in the snapshot, and
1808
+ only its configurability changed. What it catches is the addition — the shape that actually breaks a
1809
+ later file, because the next `defineProperty` of a name nothing expected is the one that throws.
1810
+ The snapshot is taken once per file, in `beforeAll`, rather than per test, and the comparison is also
1811
+ run after every `afterAll`, so a write made in a `beforeAll` or an `afterAll` is reported too.
1645
1812
 
1646
1813
  ### Naming the file that polluted `Object.prototype`
1647
1814
 
@@ -1662,6 +1829,16 @@ a `useValue` provider or a test double — patch the prototype of the class the
1662
1829
  `guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
1663
1830
  that does not call `setupAutoSpy()`.
1664
1831
 
1832
+ **A key an earlier file left behind is taken back before the next one is collected.** The per-test
1833
+ comparison cannot see a write made while a spec file was being imported or collected, or in its
1834
+ `afterAll` — all three happen outside any test, and the file that then fails to collect is the
1835
+ innocent one. `setupAutoSpy()` therefore also checks at the moment it is called, which is the one
1836
+ seam Vitest leaves between files: the key is deleted, a line goes to **stderr** naming it, and
1837
+ **nothing fails** — the file that would fail is not the file that wrote it. A key that refuses to be
1838
+ deleted is adopted into the baseline rather than reported once per file for the rest of the run. The
1839
+ in-file check still throws at its default grade; the file's own baseline is taken in `beforeAll` and
1840
+ re-checked after every `afterAll`, so a write made in either is covered as well.
1841
+
1665
1842
  ### Naming the test that left an attribute on `<body>`
1666
1843
 
1667
1844
  ```ts
@@ -1707,13 +1884,19 @@ frame outside `node_modules`:
1707
1884
  - **Absorbed** means the call never reached the console: a `/console` spy installed for the test or
1708
1885
  the file, `vi.spyOn(console, m).mockImplementation(…)`, any replacement that does not call
1709
1886
  through. A bare `vi.spyOn(console, m)` calls through and **counts**.
1887
+ - **This library's own `'warn'`-grade reports are not stray console output.** `guardGlobals`,
1888
+ `prototypePollution`, `unconfiguredReads`, `swallowedStrictCalls`, `misconfiguration`, the
1889
+ duplicate-copy report, the skipped-teardown net and the `test.concurrent` notice write past the
1890
+ guard's wrapper, so `'warn'` stays a warning under `strayConsole: 'throw'` instead of failing the
1891
+ test it is advising about. A `vi.spyOn(console, 'warn')` a test installs still absorbs them — the
1892
+ channel checks that the guard's own wrapper is the installed method and steps aside when anything
1893
+ sits on top of it.
1710
1894
  - Output **outside any test** — collection/import, `beforeAll` / `afterAll`, a callback after its
1711
1895
  test ended — fails the **file** in `afterAll` (`… outside any test …`).
1712
1896
  - Every console method a test replaced is put back after it; one a file replaced, after the file.
1713
1897
  - Under the guard, **importing `vitest-auto-spy/console` installs nothing** — call
1714
1898
  `installConsoleSpies()` in a `beforeEach` (or at the top of the file). Spies an import installed
1715
1899
  before the guard armed are taken off.
1716
- - The library's own warnings are console output: under the guard they fail the test that caused them.
1717
1900
  - `strayConsole: { allow: ['…', /…/] }` is the last resort, for environment noise no spec can reach.
1718
1901
  Output written to `process.stdout` / `process.stderr` directly, and jsdom's own virtual console, is
1719
1902
  not seen.
@@ -1747,6 +1930,23 @@ every occurrence. The grade is process-wide and released after the file. The pri
1747
1930
  cancels — jsdom answers every Web Storage write with a real `setTimeout(…, 0)`, and the library's own
1748
1931
  storage probe runs under it.
1749
1932
 
1933
+ **`test.concurrent` earns one warning per worker, and it is worth reading rather than silencing.**
1934
+ The per-test guards assume one test at a time: the document snapshot, the console window and the
1935
+ unconfigured-read counter are opened and judged per test, so with two in flight a finding can be
1936
+ charged to the other one — or cleared before anything sees it. The restores still run for every
1937
+ test, and the skipped-teardown net now remembers per test rather than per file, which is the half
1938
+ that was simply wrong. Run the files that need a guard sequentially, or keep `test.concurrent` for
1939
+ files whose setup passes `strayConsole: 'off'`, `documentPollution: 'off'` and
1940
+ `unconfiguredReads: 'off'`.
1941
+
1942
+ **What `/setup` costs per test**, measured on happy-dom over 10 000 empty tests (2026-09-17, Node
1943
+ 24): the default `setupAutoSpy()` **19 µs**, `guardGlobals` **59 µs** (it snapshots once per file
1944
+ now, not once per test), `documentPollution` **23 µs** — it shares the `onTestFinished` the teardown
1945
+ net registers anyway — and `preset: 'strict'` **67 µs**. `documentPollution: { nodes: true }` over a
1946
+ `<head>` of 1000 children is **0.19 ms** per test rather than 7.1 ms, because the children are walked
1947
+ as a sibling chain instead of through a live `HTMLCollection`; an `ignoreNodes` selector's answer for
1948
+ an element is decided at snapshot time and reused for that test.
1949
+
1750
1950
  ### Hook order differs from Jest
1751
1951
 
1752
1952
  Vitest runs `afterEach` hooks as a stack (innermost / last-registered first); Jest ran them in
@@ -1853,6 +2053,26 @@ expect(Image.instances[0].src).toBe('https://tns.example/hit');
1853
2053
  throws a named error if it is ever called **without** `new`. `stubConstructor` installs through
1854
2054
  `mockValueProp`, so `restoreMockedProps()` puts the platform's constructor back.
1855
2055
 
2056
+ **Static members are opt-in — `createSpyClass(Class, config, { statics: true })`.** Production code
2057
+ that reaches the class rather than an instance (`BackgroundWorker.isSupported()`,
2058
+ `Client.fromToken(t)`, a `VERSION` constant) finds nothing on the double otherwise, and the failure
2059
+ is a `TypeError` on the class, not on the spy:
2060
+
2061
+ ```ts
2062
+ const WorkerSpy = createSpyClass(BackgroundWorker, undefined, { statics: true });
2063
+
2064
+ WorkerSpy.isSupported.mockReturnValue(true); // needs a cast — see below
2065
+ ```
2066
+
2067
+ What the option copies, walking the class's own prototype chain so inherited statics come too: a
2068
+ static **function** becomes a full spy of its own, a static **data member** is copied by value, and a
2069
+ static **accessor** is skipped and never evaluated — a getter that reads configuration or touches the
2070
+ network must not run because a double was built. `prototype`, `length`, `name`, `caller`,
2071
+ `arguments` and the double's own `calls` / `instances` are never overwritten. It is off by default
2072
+ for that last reason: a class with a static named `calls` would otherwise shadow the construction
2073
+ log. **The statics are not typed** — the return type is still `ConstructorSpy<T>`, so reading one
2074
+ needs a cast at the spec; type them and this note goes.
2075
+
1856
2076
  For the three observers, prefer the purpose-built stubs (§13). For `AbortController` — which breaks
1857
2077
  in a jsdom run for a reason involving none of the three parties in the stack trace — use
1858
2078
  `stubAbortController()`.
@@ -1879,11 +2099,15 @@ const media = stubMediaElement({ duration: 120 });
1879
2099
 
1880
2100
  media.set(video, { readyState: 1 }); // → loadedmetadata
1881
2101
  media.set(video, { currentTime: 119 }); // → timeupdate
1882
- media.set(video, { ended: true }); // → ended
2102
+ media.set(video, { ended: true }); // → pause, then ended — and the element is paused
1883
2103
  expect(media.play).toHaveBeenCalledTimes(1);
1884
2104
  ```
1885
2105
 
1886
- State is per element, so an ad and the content report different durations.
2106
+ State is per element, so an ad and the content report different durations — and per **install**, so
2107
+ an element held in module scope or left in the document under `isolate: false` takes the new
2108
+ install's `duration` rather than the previous one's. `set({ ended: true })` pauses the element and
2109
+ fires `pause` before `ended`, as the platform does: an element that is both ended and playing is a
2110
+ state no browser produces, and a player listening for `pause` was never told.
1887
2111
 
1888
2112
  `currentTime` is a get/set pair, so a player restarting itself with `video.currentTime = 0` reaches
1889
2113
  the record and fires `timeupdate` too — `media.set()` is not the only way in, and the component's own
@@ -1939,6 +2163,11 @@ mock` from **inside that dependency**. `moduleNamespace` is the shape it expects
1939
2163
  vi.mock('shaka-player', () => moduleNamespace({ Player: mockConstructor(() => playerStub) }));
1940
2164
  ```
1941
2165
 
2166
+ A factory that spells out its own `default` **keeps it** — `moduleNamespace({ default: dayjsStub, utc })`
2167
+ gives the probing dependency `dayjsStub`, where it used to be replaced by the namespace itself and
2168
+ the default export silently became the wrong object. Only a factory without one gets
2169
+ `default: <the namespace>`, which is the interop shape it was there for; the return type follows.
2170
+
1942
2171
  There is no `mockModule(…)` helper here, and there cannot be: Vitest hoists the literal `vi.mock`
1943
2172
  call, so a wrapper around it would be hoisted as a call to a function that does not exist yet. Share
1944
2173
  a fixture between the factory and the tests with `vi.hoisted()`.
@@ -2103,12 +2332,19 @@ old signal, silently and for the rest of the test. Writing through keeps all of
2103
2332
  `model()`'s output half alive, and leaves nothing for `restoreMockedProps()` to undo — the value
2104
2333
  stays where the spec left it.
2105
2334
 
2106
- Two shapes are still swapped: a `computed()` the class declares, and a member the spy does not have
2335
+ **A `signal().asReadonly()` member counts as writable.** The view a class exposes is a different
2336
+ function from the signal behind it, but it is the same reactive node, so the value is set through the
2337
+ node and the member the class publishes keeps working — whenever it was first read. That is the
2338
+ common shape for a store's public state (`readonly items = this.#items.asReadonly()`), and it used to
2339
+ be swapped, which meant a `computed()` that had already read it never saw the new value.
2340
+
2341
+ One shape is still swapped: a `computed()` the class declares, and a member the double does not have
2107
2342
  yet. **Patch those before anything reads them** — before the first `detectChanges()` /
2108
- `stable(fixture)`. A read-only signal a live consumer has already read is refused by name rather
2109
- than replaced where nothing would notice it, and an `input()` is refused outright: Angular sets an
2110
- input through the input node rather than the property, so a replaced one breaks the host's next
2111
- write. Drive an input with `fixture.componentRef.setInput(name, value)`, or with
2343
+ `stable(fixture)`. A `computed()` a live consumer has already read is refused by name rather than
2344
+ replaced where nothing would notice it — the refusal now names `computed()` specifically, and says
2345
+ that driving the signal the computation reads is the way through. An `input()` is refused outright:
2346
+ Angular sets an input through the input node rather than the property, so a replaced one breaks the
2347
+ host's next write. Drive an input with `await setInputs(fixture, { … })`, or with
2112
2348
  `renderShallow(Component, { inputs: { … } })` for the value it starts at.
2113
2349
 
2114
2350
  ### Observers the component constructs itself
@@ -2143,10 +2379,18 @@ Three more knobs, each for a shape that otherwise gets hand-rolled:
2143
2379
  ```ts
2144
2380
  stubIntersectionObserver({ autoEmit: true }); // every observed target reports as visible, at once
2145
2381
  observers.last.options; // the init object: { rootMargin, threshold, … }
2382
+ observers.last.host; // the observer the code under test constructed — and its callback's 2nd argument
2146
2383
  observers.last.emit([mutationRecord(host, { addedNodes: [span] })]);
2147
2384
  observers.last.emit([resizeEntry(host, { width: 320 })]);
2148
2385
  ```
2149
2386
 
2387
+ **The callback's second argument is the observer the code under test holds**, not an internal
2388
+ record — so `(entries, observer) => observer.unobserve(entries[0].target)`, `observer.disconnect()`
2389
+ and `observer.takeRecords()` all reach the thing the spec drives, which is what production code
2390
+ written against the platform expects. `root`, `rootMargin` and `thresholds` read back off the init
2391
+ the way the platform normalises them (`null`, `'0px 0px 0px 0px'`, `[0]` by default) rather than as
2392
+ empty values.
2393
+
2150
2394
  `autoEmit` is the mode a suite ported from Jest needs: there the global mock fired its callback with
2151
2395
  `isIntersecting: true` immediately, so lazily-loading sections fetched their data during
2152
2396
  `detectChanges()`. Against the default inert observer those specs assert on an empty component and
@@ -2311,12 +2555,32 @@ helper of its own or wants the HTTP one mid-test — reading takes the requests,
2311
2555
  re-report what you inspected.
2312
2556
 
2313
2557
  Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
2314
- `provideHttpClientTesting()` / `HttpClientTestingModule`, and a project using neither is inert.
2315
- `ngModuleScopes` only fires on a module that contributes **nothing at all** — a providers-only module
2316
- is legitimately scope-empty, so a stripped scope that still has providers passes silently; hand-call
2558
+ `provideHttpClientTesting()` / `HttpClientTestingModule` — including one reached through a nested
2559
+ module, which is the usual `imports: [SharedTestingModule]` shape and used to make `pendingRequests`
2560
+ inert without saying so — and a project using neither is genuinely inert. `ngModuleScopes` only fires
2561
+ on a module that contributes **nothing at all** — a providers-only module is legitimately
2562
+ scope-empty, so a stripped scope that still has providers passes silently; hand-call
2317
2563
  `assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
2318
2564
  not fire when `declarations` is non-empty.
2319
2565
 
2566
+ **`getTestBed()` is covered as well as `TestBed`.** The instrumentation sits on the TestBed
2567
+ _instance_ every static delegates to, so the configuration inspectors, `shadowedProviders` and the
2568
+ `overrideComponentProvider` verification all see `getTestBed().configureTestingModule(…)` and
2569
+ `getTestBed().createComponent(…)` — the shape a setup helper of your own usually writes — and
2570
+ `overrideTemplate` is counted with the rest of the overrides. A suite on `getTestBed()` used to get
2571
+ silence from every one of these.
2572
+
2573
+ Two more things now judge what Angular ends up with rather than one call: `deadSchemas` tallies
2574
+ `schemas` and `declarations` across **every** `configureTestingModule` of a test, because Angular
2575
+ accumulates them — a schema added in a second call next to a component declared in the first is no
2576
+ longer reported, and the reverse order is no longer silently passed — and a test that resets the
2577
+ testing module twice reports the requests of every module it built, not only the last.
2578
+
2579
+ `enableAngularDiagnostics()` and `mockSignalProp()` both check once per worker that the Angular
2580
+ internals they read are still there, and **throw** naming the installed `@angular/core` version when
2581
+ one has moved. Nothing about that is fixable from a spec: the throw tells you which shape went and
2582
+ what stopped working, and the repair is a release of this package.
2583
+
2320
2584
  ### `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
2321
2585
 
2322
2586
  ```ts
@@ -2363,6 +2627,11 @@ Three things to know before reaching for it:
2363
2627
  suite using `provideHttpTesting()` everywhere the diagnostic is redundant; keep it on while any
2364
2628
  file still configures HTTP testing by hand.
2365
2629
 
2630
+ `provideHttpTesting({ verifyOnTeardown })` reads which test is running off the runner's own state,
2631
+ and where that state is absent — `bun:test`, `node:test`, anything that is not Vitest — the
2632
+ end-of-test check cannot arm. It now says so once per worker instead of checking nothing quietly;
2633
+ call `verifyNoPendingRequests()` yourself there.
2634
+
2366
2635
  ### `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
2367
2636
 
2368
2637
  ```ts
@@ -2576,6 +2845,10 @@ Five things to know:
2576
2845
  records the reload, leaves `location.href` real, and `win.location.href = '/next'` moves the
2577
2846
  double rather than the address bar. `mockValueProp(TestBed.inject(WINDOW), 'innerWidth', 800)` and
2578
2847
  its restore work on the double too, and neither touches the global.
2848
+ - **Constructors come out unbound.** `win.Date`, `win.Promise`, `win.Object`, `win.Event` are handed
2849
+ back as themselves rather than through a binding that loses the statics, so `win.Date.now()`,
2850
+ `win.Promise.resolve()`, `win.Object.keys(x)` and `new win.Event('x')` work, and
2851
+ `win.Event === window.Event` holds — which is what `instanceof` in production code needs.
2579
2852
 
2580
2853
  ### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
2581
2854
 
@@ -2772,6 +3045,7 @@ const { fixture, component } = renderShallow(TaskListComponent, {
2772
3045
  await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
2773
3046
  await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
2774
3047
  // a fixture whose componentType carries no ɵcmp is refused by name too, not as a bare TypeError
3048
+ // aliases resolve by CLASS FIELD name, and an input a hostDirectives entry exposes is accepted
2775
3049
 
2776
3050
  // build a class through DI, every unprovided token auto-spied
2777
3051
  const { instance, spies } = createWithAutoSpies(CartService, {
@@ -2815,6 +3089,15 @@ expect(component.products).toHaveResourceValue([product]);
2815
3089
  expect(component.products).toHaveResourceError(/503/);
2816
3090
  ```
2817
3091
 
3092
+ **`renderShallow({ inputs })` and `setInputs` resolve a name the same way**, which they did not
3093
+ before. Both read the compiled definition, so an input renamed with an alias
3094
+ (`@Input('chart-series') series` or `series = input(…, { alias: 'chartSeries' })`) is set by its
3095
+ **class-field** name as well as its public one; both accept an input a `hostDirectives` entry
3096
+ exposes, which `setInputs` used to refuse with "It declares no inputs at all"; and both refuse a name
3097
+ the component does not declare, at the call, listing the ones it does. Angular's own answer to an
3098
+ undeclared name is an `NG0303` on the console and no change at all, so the spec used to fail several
3099
+ assertions later, on state nothing moved.
3100
+
2818
3101
  Two zoneless traps:
2819
3102
 
2820
3103
  - `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
@@ -2845,6 +3128,22 @@ writable and a write through it lands in `'local'`, `set` / `update` / `asReadon
2845
3128
  `true` while the resource is loading over a value that is defined, which is what Angular has answered
2846
3129
  since v20. Nothing is in flight, so there is nothing to await.
2847
3130
 
3131
+ **The double is no more forgiving than the real thing, in two places that used to hide a defect.**
3132
+ `value()` read after `fail(reason)` **throws** — a `ResourceValueError` carrying the reason on
3133
+ `cause` and naming the property — exactly as a real `ResourceRef` does, so a spec that read the value
3134
+ after arranging the error branch fails now instead of reading the last good value. Branch on
3135
+ `hasValue()` / `status()` first, or assert with `toHaveResourceError()`. And `reload()` answers
3136
+ `false` while the resource is `'idle'` or `'loading'`, as Angular's does, rather than a constant
3137
+ `true` — `expect(products.reload).toHaveBeenCalled()` is still the assertion to write; the return
3138
+ value is only for code that branches on it.
3139
+
3140
+ **The resource, focus and directive matchers throw on an argument of the wrong type.** They used to
3141
+ report `{ pass: false }`, which `.not` turns into a pass — so
3142
+ `expect(products.value()).not.toBeLoading()` (the value, not the resource),
3143
+ `expect(missingEl).not.toHaveFocus()` (a query that found nothing) and
3144
+ `expect(undefined).not.toHaveDirectiveApplied(X)` were three green assertions about nothing. Each
3145
+ now throws, naming what it received.
3146
+
2848
3147
  Per-file timing, to find which specs actually pay for `TestBed`:
2849
3148
 
2850
3149
  ```ts
@@ -2871,10 +3170,14 @@ wants **Angular >= 20** and a declared `@angular/platform-browser`; on 18 or 19
2871
3170
  loading, because the zoneless provider was still called
2872
3171
  `provideExperimentalZonelessChangeDetection` there.
2873
3172
 
2874
- It re-exports everything in this section except `registerSignalMatchers`,
2875
- `registerResourceMatchers`, `mockSignalProp` / `mockResourceProp` and the TestBed diagnostics — the
2876
- matchers and diagnostics need the runner's `expect.extend` and suite-level hooks, and the `mock*Prop`
2877
- family is not re-exported there either.
3173
+ It exports the core, `provideAutoSpy` / `injectSpy`, `renderShallow`, `createWithAutoSpies`,
3174
+ `setInputs`, `runEffect`, `settleResource`, `trackEffectRuns` / `trackRecomputations` and
3175
+ `stable` / `flushEffects` — nothing else from this section. The matcher registrars
3176
+ (`registerSignalMatchers`, `registerDirectiveMatchers`, `registerResourceMatchers`) need the runner's
3177
+ `expect.extend` and the TestBed diagnostics its suite-level hooks; the overrides, `extendWithAutoSpies`,
3178
+ `provideAutoSpyForToken`, `trackInjections`, `setupAngularTestEnv`, the stub factories and the
3179
+ `mock*Prop`, platform and dialog doubles are simply not routed to Bun. Import them from `/angular`
3180
+ under Vitest.
2878
3181
 
2879
3182
  ---
2880
3183
 
@@ -2895,7 +3198,9 @@ setupAngularTestEnv({
2895
3198
  ```
2896
3199
 
2897
3200
  It resets the environment only when the mode actually changes, and the initialisers stay yours —
2898
- which platform and which providers is not this library's decision.
3201
+ which platform and which providers is not this library's decision. The mode is remembered **per
3202
+ worker**, so under `isolate: false` a run of files all in one mode initialises once: before, every
3203
+ spec file reset the environment and re-ran your initialiser.
2899
3204
 
2900
3205
  ---
2901
3206
 
@@ -2973,10 +3278,12 @@ chart.pointSelected.emit(2); // outputs are EventEmitters; a model() stays a mod
2973
3278
  Copied from `ɵcmp` / `ɵdir` / `ɵpipe`: the selector, every input under its public name (transforms
2974
3279
  included), every output, `exportAs`, a pipe's name and purity. Not copied: the template (the stub
2975
3280
  renders one `<ng-content>` per slot the real one projects — `{ template }` in the third argument
2976
- replaces it), host bindings, providers, lifecycle hooks, queries. The second argument seeds members
2977
- per instance — a method the parent calls through `viewChild`, a pipe's `transform` (identity by
2978
- default). With `renderShallow`: `renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })`
2979
- — the real child is dropped as a component import and the stub is the one child left.
3281
+ replaces it), host bindings, providers, lifecycle hooks, queries, and **`hostDirectives`** — so an
3282
+ input the real component exposes through one is not on the stub, even though `setInputs` accepts that
3283
+ name on the real thing. The second argument seeds members per instance — a method the parent calls
3284
+ through `viewChild`, a pipe's `transform` (identity by default). With `renderShallow`:
3285
+ `renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })` — the real child is
3286
+ dropped as a component import and the stub is the one child left.
2980
3287
 
2981
3288
  ### Patching a property of a spy
2982
3289
 
@@ -3107,6 +3414,11 @@ its constructor, and `tick()` in the `fakeAsync` test has to see those timers. U
3107
3414
  `installProxyZonePatch({ scope: 'callback' })` for `test.concurrent`, where two callbacks are in
3108
3415
  flight at once and would otherwise swap the same `ProxyZoneSpec` delegate under one another.
3109
3416
 
3417
+ Calling `installProxyZonePatch()` yourself is idempotent now: a second call is a no-op instead of
3418
+ wrapping the previous wrapper, and so is the undo it returns. Under `isolate: false` an explicit
3419
+ call in a setup file used to add a `Proxy` layer per spec file — two hundred layers at two hundred
3420
+ files, every `fakeAsync` callback paying for all of them.
3421
+
3110
3422
  **Invariant of this package, not a detail of one release:** `zone.js` is a **devDependency and only a
3111
3423
  devDependency** — never a dependency, never a peer, not even an optional one. Everything about zones
3112
3424
  lives behind this one subpath; no other entry reaches it, even transitively, and the module imports
@@ -3162,6 +3474,11 @@ their calls. **Do not rely on the import to install them**: it does so once per
3162
3474
  (`no-import-time-console-spies` reports it). Under `setupAutoSpy({ strayConsole })` the import installs
3163
3475
  nothing at all.
3164
3476
 
3477
+ The spies survive `vi.resetModules()`. A fresh copy of the module used to record the previous copy's
3478
+ spy as "the real `console.warn`", after which `restoreConsole()` installed that dead spy for the rest
3479
+ of the worker and every log from then on went nowhere. The real methods are kept on one shared table
3480
+ and a spy of another copy is refused as an original.
3481
+
3165
3482
  Import your runtime entry (`…/bun`, `…/node`) **before** `…/console`, or it registers the Vitest
3166
3483
  adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives a detached one.
3167
3484
 
@@ -3195,46 +3512,46 @@ export default [
3195
3512
  errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
3196
3513
  blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
3197
3514
 
3198
- | Rule | Level | Fix | Flags |
3199
- | --------------------------------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3200
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
3201
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3202
- | `prefer-provide-auto-spy` | `error` | fix | a hand-rolled `useValue`, `useFactory`, `useClass` or `useExisting` (also `useValue: new StubMock()`), in a provider **or** a `TestBed.overrideProvider(X, { … })` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`. A name in the slot is followed one step, through an initialiser or a single `beforeEach` assignment. `{ provide: X, useValue: createSpyFromClass(X, config) }` — the factory's own body, written out — is reported and **rewritten** to `provideAutoSpy(X, config)`; a double read from a _different_ class (`{ provide: LocalStorage, useValue: createSpyFromClass(BaseLocalStorage) }`) is not, since the abstract token has no prototype for `provideAutoSpy` to read |
3203
- | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3204
- | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy` and delete the stub; a decorated, `implements`-ing, `extends`-ing or unnamed class is exempt — option: `{ minRunnerFns }` |
3205
- | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`; a bare `let fn: Mock` is a callback and never reported — option: `{ minRunnerFns }` |
3206
- | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }` |
3207
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
3208
- | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
3209
- | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3210
- | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
3211
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3212
- | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
3213
- | `no-overridden-provider` | `error` | suggest | two providers for one token in one array, or one a `TestBed.overrideProvider` in the same hook replaces → the earlier one never runs; the exact duplicate can be deleted |
3214
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3215
- | `no-private-member-access` | `error` | — | `instance['privateMember']`, `(instance as any).privateMember` (and `as unknown as`, and a decoy type), `vi.spyOn(Object.getPrototypeOf(x), 'm')` → drive the member through the public API. **Type-aware**: silent without `parserOptions.project`, and silent on an index signature |
3216
- | `no-dead-schemas` | `error` | — | `schemas` on a testing module with no `declarations` — the schema applies to nothing; the file decides, so a `declarations` in another `configureTestingModule` call silences it |
3217
- | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
3218
- | `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
3219
- | `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
3220
- | `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
3221
- | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3222
- | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory` / `useExisting`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3223
- | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3224
- | `no-console-in-spec` | `error` | — | a spec calling `console.x(…)` itself, or `console.x = …`, which nothing restores → absorb the code's output through `vitest-auto-spy/console` |
3225
- | `no-import-time-console-spies` | `error` | — | an import of `vitest-auto-spy/console` in a file that never calls `installConsoleSpies()` — the import installs once per worker and silences every later file → `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` |
3226
- | `no-mistyped-use-value` | `error` | — | `{ provide: TOKEN, useValue }` whose value is not assignable to the primitive `T` of `InjectionToken<T>` (string, number, boolean, bigint, enum, their literals, `null`, `undefined`) — `useValue` is `any`, so `{}` for a `boolean` token compiles and is truthy. **Type-aware**: silent without `parserOptions.project`; object-typed tokens are `no-unknown-use-value-key`'s |
3227
- | `no-unknown-use-value-key` | `error` | — | a key of an object `useValue` literal the provided type does not have — `T` of `InjectionToken<T>`, or the instance type of a `provide:` class; `{ provide: ActivatedRoute, useValue: { queryParams$: … } }` compiles, since `useValue` is `any`. Keys only, never assignability (a partial fixture is fine). **Type-aware**; silent on `any` / `unknown` / `object`, an index signature, a spread's keys, `multi: true` |
3228
- | `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(instance, 'ngOnInit')` (and `ngOnDestroy`, `ngDoCheck`, `ngAfterContent*`, `ngAfterView*`) — a view calls the hook read off the prototype, so the instance spy is never called and its stub never runs → `vi.spyOn(Cls.prototype, …)` before `createComponent`, or assert the effect. `ngOnChanges` is exempt: Angular calls it through the instance |
3229
- | `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `returnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature → an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`; otherwise the fixture is the wrong shape, checked against `ReturnType<X['m']>`. A reason after the directive does not silence it; a deliberate out-of-type value keeps it under `eslint-disable-next-line … -- <why>` |
3230
- | `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer it already fixes (`toBe` / `toEqual` / `toStrictEqual` against a literal; `toBeTruthy`, `toBeDefined`, `toBeNull`… for any literal) → assert on what the code produced, or `expect.fail(…)` for an unreachable branch |
3231
- | `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — every statement of the body an `expect(x)` under `toBeTruthy` / `toBeDefined` / `toBeInstanceOf` (or their negated twins), weighed against the tests that run the same setup: the rest of the block, and everything the blocks nested in it declare → delete it; the suggestion removes the test and the blank line above it. Silent where that test is the block's only running one, and a skipped sibling does not count as proof |
3232
- | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
3233
- | `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`, through `TestBed`, `getTestBed()`, a chain of those, or a name holding one → drop the `await`, and the `async` of a hook that then awaits nothing else; the suggestion does both. Reads **no types**, so it reports without `parserOptions.project`, where `@typescript-eslint/await-thenable` cannot. `inject` and `runInInjectionContext` are never reported: each answers whatever the token or the callback holds, which can be a promise |
3234
- | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
3235
- | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
3236
- | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3237
- | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
3515
+ | Rule | Level | Fix | Flags |
3516
+ | --------------------------------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3517
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
3518
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3519
+ | `prefer-provide-auto-spy` | `error` | fix | a hand-rolled `useValue`, `useFactory`, `useClass` or `useExisting` (also `useValue: new StubMock()`), in a provider **or** a `TestBed.overrideProvider(X, { … })` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`. A name in the slot is followed one step, through an initialiser or a single `beforeEach` assignment. `{ provide: X, useValue: createSpyFromClass(X, config) }` — the factory's own body, written out — is reported and **rewritten** to `provideAutoSpy(X, config)`; a double read from a _different_ class (`{ provide: LocalStorage, useValue: createSpyFromClass(BaseLocalStorage) }`) is not, since the abstract token has no prototype for `provideAutoSpy` to read. On the `ActivatedRoute` token — in a provider or in an `overrideProvider` — the advice is `provideActivatedRoute()` from `/angular-router` instead, with a message of its own, so the two route rules agree |
3520
+ | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3521
+ | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy` and delete the stub; a decorated, `implements`-ing, `extends`-ing or unnamed class is exempt — option: `{ minRunnerFns }` |
3522
+ | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`; a bare `let fn: Mock` is a callback and never reported — option: `{ minRunnerFns }` |
3523
+ | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }` |
3524
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
3525
+ | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
3526
+ | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3527
+ | `no-done-callback` | `error` | — | a first parameter of a test or hook that is **called**, **passed on** as an argument, or never used at all → `async` + an awaited assertion, and `done.fail(…)` at the call site. A parameter read only as `ctx.skip()` / `ctx.task` / `ctx.expect` is Vitest's `TestContext` and is left alone, destructured or not |
3528
+ | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3529
+ | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
3530
+ | `no-overridden-provider` | `error` | suggest | two providers for one token in one array, or one a `TestBed.overrideProvider` in the same hook replaces → the earlier one never runs; the exact duplicate can be deleted |
3531
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3532
+ | `no-private-member-access` | `error` | — | `instance['privateMember']`, `(instance as any).privateMember` (and `as unknown as`, and a decoy type), `vi.spyOn(Object.getPrototypeOf(x), 'm')` → drive the member through the public API. **Type-aware**: silent without `parserOptions.project`, and silent on an index signature |
3533
+ | `no-dead-schemas` | `error` | — | `schemas` on a testing module with no `declarations` — the schema applies to nothing; the file decides, so a `declarations` in another `configureTestingModule` call silences it |
3534
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
3535
+ | `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
3536
+ | `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
3537
+ | `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
3538
+ | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3539
+ | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory` / `useExisting`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3540
+ | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3541
+ | `no-console-in-spec` | `error` | — | a spec calling `console.x(…)` itself, or `console.x = …`, which nothing restores → absorb the code's output through `vitest-auto-spy/console` |
3542
+ | `no-import-time-console-spies` | `error` | — | an import of `vitest-auto-spy/console` in a file that never calls `installConsoleSpies()` — the import installs once per worker and silences every later file → `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` |
3543
+ | `no-mistyped-use-value` | `error` | — | `{ provide: TOKEN, useValue }` whose value is not assignable to the primitive `T` of `InjectionToken<T>` (string, number, boolean, bigint, enum, their literals, `null`, `undefined`) — `useValue` is `any`, so `{}` for a `boolean` token compiles and is truthy. **Type-aware**: silent without `parserOptions.project`; object-typed tokens are `no-unknown-use-value-key`'s |
3544
+ | `no-unknown-use-value-key` | `error` | — | a key of an object `useValue` literal the provided type does not have — `T` of `InjectionToken<T>`, or the instance type of a `provide:` class; `{ provide: ActivatedRoute, useValue: { queryParams$: … } }` compiles, since `useValue` is `any`. Keys only, never assignability (a partial fixture is fine). **Type-aware**; silent on `any` / `unknown` / `object`, an index signature, a spread's keys, `multi: true` |
3545
+ | `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(instance, 'ngOnInit')` (and `ngOnDestroy`, `ngDoCheck`, `ngAfterContent*`, `ngAfterView*`) — a view calls the hook read off the prototype, so the instance spy is never called and its stub never runs → `vi.spyOn(Cls.prototype, …)` before `createComponent`, or assert the effect. `ngOnChanges` is exempt: Angular calls it through the instance |
3546
+ | `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `returnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature → an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`; otherwise the fixture is the wrong shape, checked against `ReturnType<X['m']>`. A reason after the directive does not silence it; a deliberate out-of-type value keeps it under `eslint-disable-next-line … -- <why>` |
3547
+ | `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer it already fixes (`toBe` / `toEqual` / `toStrictEqual` against a literal; `toBeTruthy`, `toBeDefined`, `toBeNull`… for any literal) → assert on what the code produced, or `expect.fail(…)` for an unreachable branch |
3548
+ | `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — every statement of the body an `expect(x)` under `toBeTruthy` / `toBeDefined` / `toBeInstanceOf` (or their negated twins), weighed against the tests that run the same setup: the rest of the block, and everything the blocks nested in it declare → delete it; the suggestion removes the test and the blank line above it. Silent where that test is the block's only running one, and a skipped sibling does not count as proof |
3549
+ | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
3550
+ | `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`, through `TestBed`, `getTestBed()`, a chain of those, or a name holding one → drop the `await`, and the `async` of a hook that then awaits nothing else; the suggestion does both. Reads **no types**, so it reports without `parserOptions.project`, where `@typescript-eslint/await-thenable` cannot. `inject` and `runInInjectionContext` are never reported: each answers whatever the token or the callback holds, which can be a promise |
3551
+ | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
3552
+ | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
3553
+ | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3554
+ | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
3238
3555
 
3239
3556
  Thirty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3240
3557
  `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-five are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
@@ -3399,6 +3716,14 @@ directive. The report sits on the directive's line so that comment reaches it. `
3399
3716
  `failWith` and `throwWith` are not read: their parameter is `unknown`, so a directive there
3400
3717
  suppresses something other than the stub.
3401
3718
 
3719
+ **What the plugin costs is bounded by the file, not by what is in it.** Three rules used to re-read
3720
+ the file per finding-site rather than once: over this repository's 173 spec files the whole plugin
3721
+ takes **56 ms** where it took 93, and on a single 1.7 MB spec **68 ms** where it took 3 263. The
3722
+ rules answer the same way they did — the ordering rules (`no-inject-before-override`,
3723
+ `no-overridden-provider`) collect the `override*` and `resetTestingModule` positions in one pass and
3724
+ decide by range, `prefer-render-shallow` asks the template-read question once per file, and
3725
+ `no-redundant-smoke-test` indexes identifiers only where a smoke test exists to judge.
3726
+
3402
3727
  The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
3403
3728
  packages, which a subpath export can never be.
3404
3729
 
@@ -3410,9 +3735,14 @@ packages, which a subpath export can never be.
3410
3735
  | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3411
3736
  | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
3412
3737
  | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
3738
+ | `… is not subscribable (…)` | the argument is not an observable — most often the value it emits, or a promise | pass the observable itself; `await` a promise directly (§8) |
3739
+ | ``… the `advance` callback threw: …`` | the `{ advance }` callback failed; the original is on `cause` and the subscription is torn down | fix the callback — a `vi.runAllTimers()` with no fake timers installed is the usual one (§8) |
3740
+ | `expectEmissions(source$, 0) can never succeed` | a count below 1 — usually `expectEmissions(s, expected.length)` with an empty expectation | `expectNoEmission(source$)`, which is the assertion that was meant (§8) |
3741
+ | `setEmissionTimeout(NaN) needs a non-negative number of milliseconds` | arithmetic on something unset reached the setter | pass a number, `0` or `Infinity` for no watchdog (§8) |
3413
3742
  | `Type 'SubjectLike<T>' is not assignable to type 'Subject<T>'` | 4.0.0: `returnSubject()` is rxjs-typed only where `vitest-auto-spy/rxjs` is in the TypeScript program, and it is not in this one | put `import 'vitest-auto-spy/rxjs';` in a file the spec `tsconfig` includes — a `setupFiles` entry outside `include` is the usual cause |
3414
3743
  | `Cannot read properties of undefined (reading 'returnValue')` on `spy.m.and.…` | the jasmine namespaces are not installed — the spy was built before them, or nothing installed them at all | import from `vitest-auto-spy/jasmine`; on Bun / `node:test`, `enableJasmineCompat()` in the setup file (§20) |
3415
3744
  | `spy.withArgs is not a function` | the same, seen from the argument-matching side | the same — or write `spy.m.calledWith(a).mockReturnValue(v)`, which needs no layer (§20) |
3745
+ | `withArgs(…).and.callFake() is not supported` | `callFake` / `callThrough` / `returnValues` install an implementation, which answers every call rather than one argument list | `.withArgs(…).and.returnValue(v)` / `.throwError(e)` / `.resolveTo(v)`, or take the whole spy with `spy.and.callFake(…)` (§20) |
3416
3746
  | `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
3417
3747
  | `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)` |
3418
3748
  | 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) |
@@ -3422,6 +3752,7 @@ packages, which a subpath export can never be.
3422
3752
  | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
3423
3753
  | `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 |
3424
3754
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
3755
+ | `advanceTimers() found only the clock faked, not the timers` | `mockSystemTime()` installed `Date` alone, so there is nothing to advance and the call used to pass having done nothing | `setupFakeTimers()` (or `vi.useFakeTimers()`) in the test that drives timers (§11) |
3425
3756
  | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
3426
3757
  | `X.member was read N times and nothing configured it, and strict mode is on` (or `was subscribed to N times and nothing fed it`), after the test | `setupAutoSpy({ unconfiguredReads })` — the code under test read a strict double's spied getter and got `undefined`, or subscribed to its observable property and nothing ever emitted | configure it: `accessorSpies.getters.member.mockReturnValue(…)`, `overrides: { member }`, `mockReadonlyProp`; `nextWith` / `returnSubject` / `complete` for a stream; `mockReturnValue(undefined)` when `undefined` is the answer meant; `{ strict: false }` on that double (§5) |
3427
3758
  | `[vitest-auto-spy] "…" wrote to the console N time(s) and nothing absorbed it` | `setupAutoSpy({ strayConsole })` — the test printed, and nothing that does not call through stood on `console`: no `/console` spy installed for it, no `mockImplementation`. A bare `vi.spyOn(console, m)` calls through, and a `console.warn` from this library counts too | absorb and assert: `installConsoleSpies()` in a `beforeEach`, then `expect(consoleErrorSpy)…`; or fix the code that printed. Importing a spy installs nothing under the guard. `strayConsole: { allow: [...] }` only for environment noise no spec can reach |
@@ -3434,6 +3765,9 @@ packages, which a subpath export can never be.
3434
3765
  | `Cannot mock the property 'X': it is not configurable` | the member was defined with `configurable: false`, so it cannot be redefined | members replaced before it are still restorable — `restoreSpiedInstance(instance)` or `restoreMockedProps()` |
3435
3766
  | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
3436
3767
  | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
3768
+ | `.value() was read while the resource is in the error state: …` | `mockResourceProp`'s double after `fail(reason)`, as a real `ResourceRef` behaves; the reason is on `cause` | branch on `hasValue()` / `status()` first, or assert with `toHaveResourceError()` (§13) |
3769
+ | `@angular/core <version> no longer carries …, which this package reads` | an Angular internal this package reads has moved; checked once per worker | nothing is fixable from a spec — report the version in the message and pin the previous Angular until a release reads the new shape (§13) |
3770
+ | `provideHttpTesting(): globalThis.__vitest_worker__ is not there` | the runner does not say which test is running, so the end-of-test check cannot arm — `bun:test`, `node:test` | call `verifyNoPendingRequests()` yourself (§13) |
3437
3771
  | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
3438
3772
  | `Cannot read properties of undefined`, stack inside the component's own constructor or a field initializer | a member the double must **hold** rather than spy on is read while the component is being constructed, and the `mockReadonlyProp` that seeds it is written _below_ `TestBed.createComponent` — so it has not run yet, and never will | seed it **before** construction: `provideAutoSpy(X, { overrides: { paymentParams: … } })`, or `provideAutoSpyForToken(TOKEN, { … })`. No later call can repair this — the throw happens inside `createComponent`, so the helper is not reached and cannot say so; the stack names the component (§9, §13) |
3439
3773
  | `arr.map(asInstance)` infers `unknown[]` | `asInstance` is overloaded, and passing it **by reference** resolves against the `DeepMockProxy<T>` overload, from which `T` cannot be inferred | `arr.map((spy) => asInstance(spy))` — the arrow gives each call its own inference. For a fixed set, `asInstances(a, b, c)` keeps the tuple types (§6) |
@@ -3465,7 +3799,7 @@ packages, which a subpath export can never be.
3465
3799
  | `runEffect(): this effect has been destroyed` | the fixture was destroyed, or effectRef.destroy() ran | move the call above `fixture.destroy()`, or assert what the teardown left behind |
3466
3800
  | `trackRecomputations(): this signal keeps no computation` | a plain `signal()`, which holds a value rather than computing one | track the `computed()` that reads it, or `trackEffectRuns()` the effect that does |
3467
3801
  | `mockSignalProp: '…' is an input() signal` | an `input()` replaced instead of set | `fixture.componentRef.setInput(name, value)`, or `renderShallow(C, { inputs: { … } })` |
3468
- | `mockSignalProp: '…' is a read-only signal something has already read` | a `computed()` member swapped after the first render | patch before the first `detectChanges()` / `stable(fixture)`, or expose the member as a writable `signal()` |
3802
+ | `mockSignalProp: '…' is a computed() something has already read` | a `computed()` member swapped after the first render — a `signal().asReadonly()` member is written through instead and is never refused | patch before the first `detectChanges()` / `stable(fixture)`, or expose the member as a writable `signal()` |
3469
3803
  | `expected a signal (a zero-argument getter), received a spy` | `toHaveSignalValue` was handed a spied method instead of a signal — usually a typo, or a signal member spied as a method | assert the spy itself with `toHaveBeenCalled`, or put a real signal on the property with `mockSignalProp`; the matcher refuses it rather than calling it |
3470
3804
  | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
3471
3805
  | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
@@ -3488,7 +3822,7 @@ packages, which a subpath export can never be.
3488
3822
  | `A metric with the name … has already been registered` as a failed suite with 0 failed tests, only in a full run | under `@angular/build:unit-test` with `isolate: false` a workspace module can run its module scope once **per spec file**, while an external package such as `prom-client` keeps one default registry per worker | not something a hook can catch — it throws at import. Look the metric up before creating it: `register.getSingleMetric(name) ?? new Histogram({ name, … })`, or give the module its own `new Registry()` |
3489
3823
  | `NotSupportedError: This name has already been registered in the registry` at import | the same per-spec-file evaluation reaching a module-scope `customElements.define` — the registry belongs to the worker's document | `if (!customElements.get(name)) customElements.define(name, Element)`; a definition cannot be undone, so nothing can sweep it |
3490
3824
  | a block of files reported as failed suites with no stack, and zero failing tests | a test left an own enumerable key on `Object.prototype`; `mergeHooks` spreads it and collection dies | `setupAutoSpy()` guards it by default; `prototypePollution` tunes the reaction |
3491
- | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
3825
+ | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` — a `Date`, `Map`, `Set` or class instance is compared whole and reported as `the element`, where it used to have no string keys and so read as "no difference" |
3492
3826
  | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
3493
3827
  | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
3494
3828
  | `settleResource: … never started — its status is 'idle'` | the `params()` / `request()` computation returned `undefined`, so the loader never ran and `value()` is still the default | set the signal that computation reads, `flushEffects()`, then await again — or `{ allowIdle: true }` when idle is the state under assertion |
@@ -3509,7 +3843,7 @@ packages, which a subpath export can never be.
3509
3843
  | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
3510
3844
  | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
3511
3845
  | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
3512
- | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
3846
+ | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers; `TestRunSummary.counts` carries the multiplicity, so a duplicate name that dropped from two to one reads `name (×2 → ×1)` instead of vanishing from both sides |
3513
3847
  | a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
3514
3848
  | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
3515
3849
  | a green test whose only line about a call is `spy.m.calledWith(1);` | that is a **stub**, not an assertion — chai's `expect(fn).to.have.been.calledWith(x)` is the one that checks | continue the chain, or `expect(spy.m).toHaveBeenCalledWith(1)` — `no-bare-called-with` (§16) |
@@ -3613,6 +3947,28 @@ diagnostic and is free to change: print it, never assert on it.
3613
3947
  | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
3614
3948
  | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
3615
3949
  | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
3950
+ | `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
3951
+
3952
+ **A hand-rolled double trips `rxjs-x/finnish`, and a double from this package does not.** The rule
3953
+ reports any name whose type is an `Observable` **or whose call signature returns one**
3954
+ (`couldReturnObservable`). `vi.fn()` and `vi.spyOn()` hand back a callable mock carrying the
3955
+ signature of the method it stands for, so a double for a method returning an `Observable` reads to
3956
+ the rule as a stream and it asks for a `$` — on a spy, where the `$` would lie about what the
3957
+ variable holds. `functions: false` gives no relief: that option is about function _declarations_,
3958
+ not a variable holding one. A `Spy<T>` from `createSpyFromClass`, `provideAutoSpy` or
3959
+ `createAutoMock` is an ordinary object instead — the Observable sits on its members, not on the
3960
+ variable — so nothing is reported, and neither the name nor the assertion has to be bent. Measured
3961
+ on an Angular workspace of 1771 spec files: the rule named seven sites, every one a hand-rolled
3962
+ `vi.fn`/`vi.spyOn`, and none on a double built here.
3963
+
3964
+ The overrides bag is the one shape worth knowing in detail, because the repair is not a rename.
3965
+ Written inline, `provideAutoSpy(X, { overrides: { m: vi.fn(() => of(x)) } })` is invisible to the
3966
+ rule — an object literal inside a call is skipped. Hoisted into a `const` it is reported **per key**,
3967
+ and the key cannot be renamed: it is the method's name on `X`. What clears it is the annotation the
3968
+ bag should carry anyway, `const overrides: DeepPartial<X> = { … }` — the rule skips a literal whose
3969
+ variable is annotated, and the bag starts being checked against `X` where it is written rather than
3970
+ only at the call. For a spy whose name is genuinely free, `*Spy` plus a `names` exemption in the
3971
+ rule's own options says the same thing once for the whole repository.
3616
3972
 
3617
3973
  **The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
3618
3974
  when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
@@ -3670,6 +4026,26 @@ with `as` still counts), the call must both begin and end a statement, and the p
3670
4026
  the installed major differs from the table's. Neither has a fixer; `doctor` still never writes.
3671
4027
  Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
3672
4028
 
4029
+ **Three things about the CLI that decide whether its answer means anything:**
4030
+
4031
+ - **An unknown flag is refused, exit 2, nothing runs.** `init --dryrun` used to write the files and
4032
+ `perf --gat` used to pass with no gate at all — a typo in CI that read as a clean result. The two
4033
+ stderr lines name the flag and list what the command accepts; `--cwd`, `--help` and `--version`
4034
+ work everywhere.
4035
+ - **The scan does not descend into a nested repository or a git worktree.** A tree carrying
4036
+ worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
4037
+ `codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
4038
+ it is a directory (a nested clone) or a file (a worktree). Past 50 000 files the scan still
4039
+ truncates and says so; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4040
+ - **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
4041
+ read is not a clean result. Absolute paths and `./`-style ones resolve against `--cwd`.
4042
+
4043
+ Exit codes, as the commands implement them: **0** is "ran, nothing to report"; **1** is "ran, and
4044
+ here is the finding" — `doctor` with an error, `init --check` with a stale block, `codemod` with any
4045
+ note, `perf --gate` over budget; **2** is "there was nothing to judge" — no command, an unknown
4046
+ command, an unknown flag, an unreadable `--only` / `--from` value, a path matching no file, and a
4047
+ `perf` run that measured nothing (including a red suite, which the gate will not judge at all).
4048
+
3673
4049
  ### If you were asked why a suite is slow
3674
4050
 
3675
4051
  ```bash
@@ -3769,6 +4145,20 @@ migrated by hand. Run it after `--write`, and again after any manual clean-up. `
3769
4145
  select transforms by id, `--list` prints them. Full reference:
3770
4146
  <https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
3771
4147
 
4148
+ **Every rewrite is parsed before it is written.** The result goes through the project's own
4149
+ `typescript` and its diagnostics are compared with the original's; a file the run would have broken
4150
+ is reported as `codemod-broke-syntax` and **left exactly as it was**, so the run exits 1 with one
4151
+ file named rather than a tree that no longer compiles. Where `typescript` is not installed the check
4152
+ is skipped silently — it is a safety net, not an install instruction.
4153
+
4154
+ It visits JavaScript specs too — `*.spec.js`, `*.test.jsx`, the `.cjs` / `.mjs` forms — because a
4155
+ Jest suite that was never TypeScript is the suite with the most `jest.` in it. Two things it now
4156
+ reports instead of rewriting into something wrong: `jest.fn<R, [A]>()` / `jest.spyOn<…>()` in a file
4157
+ that imports from `@jest/globals` (`jest-mock-type-arguments` — `jest-mock` 29 already takes the
4158
+ whole function type, so transposing a second time produces a return type of a return type), and
4159
+ `.withArgs(…)` on a `vi.spyOn` chain (`jasmine-with-args-on-spy-on` — `vi.spyOn` has no
4160
+ `calledWith`, and renaming it onto one produces a method that does not exist).
4161
+
3772
4162
  Past 50 000 files the repository scan truncates and the run says so — _Nothing left to migrate_ off a
3773
4163
  truncated list is a claim about a tree the tool never looked at. `VITEST_AUTO_SPY_SCAN_CAP` raises
3774
4164
  the cap.
@@ -3826,6 +4216,27 @@ npx vitest-auto-spy codemod --from jasmine --verify # match the result, not th
3826
4216
  matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
3827
4217
  on `globalThis` — it is one explicit line per file, which the codemod later deletes.
3828
4218
 
4219
+ Four places where that surface now matches jasmine's rather than approximating it:
4220
+
4221
+ - **`spy.withArgs(…).and` carries the strategies that describe one argument list** — `stub()`,
4222
+ `throwError(…)`, `resolveTo(…)` and `returnValue(…)`, plus this library's own `resolveWith` /
4223
+ `nextWith` / `returnSubject` family. The three that install an _implementation_ —
4224
+ `callFake`, `callThrough`, `returnValues` — are present and **throw** a message naming the
4225
+ alternative, rather than arriving as `… is not a function`: an implementation answers every call,
4226
+ which is the opposite of configuring one argument list. Take the whole spy with `spy.and.callFake(…)`
4227
+ if that is what the line meant.
4228
+ - **`jasmine.mapContaining` compares keys with the runner's equality**, as jasmine does, so an
4229
+ asymmetric matcher as a key and an object key compared deeply both work. `Map.has` alone answered
4230
+ on reference identity and missed both.
4231
+ - **`jasmine.clock().install()` leaves `Date` real**, exactly as jasmine's does — the timers are
4232
+ faked, the clock is not. `jasmine.clock().mockDate()` is what takes `Date` over, and because that
4233
+ re-installs the fake clock it reports when callbacks were already scheduled and have just been
4234
+ dropped. Call `mockDate()` right after `install()`, before anything schedules a timer.
4235
+ - **`jasmine.createSpyObj`'s third argument builds spied accessors**, reachable through
4236
+ `Object.getOwnPropertyDescriptor(obj, name).get`. Reading still answers the seed, so nothing about
4237
+ a migrated spec changes; what is gained is moving the value mid-test and asserting that the code
4238
+ under test wrote it.
4239
+
3829
4240
  ### The renames, once the suite is green
3830
4241
 
3831
4242
  | jasmine | here |
@@ -3875,8 +4286,10 @@ on `globalThis` — it is one explicit line per file, which the codemod later de
3875
4286
  once naming both rather than silently swallowing the write.
3876
4287
 
3877
4288
  `it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
3878
- shape, not a name. Use `async` + `await`; `no-done-callback` reports both the parameter and any
3879
- `done.fail(…)`.
4289
+ shape, not a name. Use `async` + `await`; `no-done-callback` reports the parameter and any
4290
+ `done.fail(…)`. It reports the parameter that is _called_, handed on as an argument, or never used —
4291
+ not every named first parameter: `it('x', (ctx) => ctx.skip())` is Vitest's own `TestContext` read
4292
+ without destructuring, which is legal, and used to be reported as a `done` callback.
3880
4293
 
3881
4294
  On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
3882
4295
  importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a