vitest-auto-spy 5.18.0 → 5.20.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 (78) hide show
  1. package/AGENTS.md +644 -110
  2. package/README.md +466 -192
  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 +985 -351
  8. package/dist/bun-angular.d.ts +6 -6
  9. package/dist/bun-angular.js +120 -63
  10. package/dist/bun.d.ts +157 -81
  11. package/dist/bun.js +9 -9
  12. package/dist/{chunk-33NJHIS2.js → chunk-2KBFEQOV.js} +17 -18
  13. package/dist/chunk-3VXZRDH3.js +12 -0
  14. package/dist/{chunk-MUJTPXMJ.js → chunk-4SJY7ZHM.js} +354 -88
  15. package/dist/{chunk-M6VMOOZ2.js → chunk-CEDA5PUY.js} +1 -1
  16. package/dist/{chunk-MAKQSO2R.js → chunk-DPTWTLIG.js} +27 -4
  17. package/dist/{chunk-DUFMKOKF.js → chunk-IIZTARIO.js} +3 -2
  18. package/dist/{chunk-Q477VHHF.js → chunk-JJVTDL5R.js} +8 -7
  19. package/dist/{chunk-ZU55UY2M.js → chunk-LE6IB3DK.js} +12 -3
  20. package/dist/chunk-QB5VKPTI.js +1285 -0
  21. package/dist/{chunk-OTOCFH6B.js → chunk-R3DF5ZEO.js} +2 -1
  22. package/dist/chunk-SOROJD3W.js +9 -0
  23. package/dist/{chunk-WAMFS3UI.js → chunk-SXR7EAUS.js} +229 -75
  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 +1233 -661
  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 +55 -32
  34. package/dist/eslint-plugin.cjs +415 -253
  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-CjwVMb2W.d.ts} +1 -1
  38. package/dist/index.d.ts +5 -5
  39. package/dist/index.js +1045 -250
  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 +1046 -244
  47. package/dist/node.d.cts +1 -0
  48. package/dist/node.d.ts +5 -5
  49. package/dist/node.js +3369 -11
  50. package/dist/observer-spy.d.ts +7 -2
  51. package/dist/observer-spy.js +44 -9
  52. package/dist/{package-identity-lm-gqZAR.d.ts → package-identity-Cn7yncmJ.d.ts} +1 -1
  53. package/dist/{prop-mock-DnYNBNJL.d.ts → prop-mock-DMEpvaAU.d.ts} +1 -1
  54. package/dist/react.d.ts +5 -5
  55. package/dist/react.js +3873 -15
  56. package/dist/rstest.d.ts +5 -5
  57. package/dist/rstest.js +9 -9
  58. package/dist/rxjs.d.ts +2 -2
  59. package/dist/rxjs.js +43 -3
  60. package/dist/setup.d.ts +92 -12
  61. package/dist/setup.js +1237 -136
  62. package/dist/shared-state.js +40 -12
  63. package/dist/signal-forms.js +2 -1
  64. package/dist/svelte.d.ts +5 -5
  65. package/dist/svelte.js +3873 -15
  66. package/dist/{track-injections-CIHJWFpp.d.ts → track-injections-Dig5t28T.d.ts} +1 -1
  67. package/dist/{track-signal-runs-BOfsdYsC.d.ts → track-signal-runs-CErdcgTA.d.ts} +37 -8
  68. package/dist/{types-D3RkJjc_.d.ts → types-BM3BcWj1.d.ts} +119 -7
  69. package/dist/vue.d.ts +6 -6
  70. package/dist/vue.js +3870 -16
  71. package/dist/zone.js +12 -1
  72. package/package.json +11 -5
  73. package/skills/vitest-auto-spy/SKILL.md +164 -106
  74. package/dist/chunk-4SJ7EGIY.js +0 -744
  75. package/dist/chunk-M5BULP7X.js +0 -41
  76. package/dist/chunk-N3T7KWWA.js +0 -157
  77. package/dist/chunk-OS4PTU6D.js +0 -99
  78. package/dist/chunk-PA4ZNLNZ.js +0 -8
package/AGENTS.md CHANGED
@@ -18,14 +18,15 @@ most of the field), `CLAUDE.md` (Claude Code) and `GEMINI.md` (Gemini CLI), plus
18
18
  rule file of any tool whose own directory already exists. `--check` is the CI form. Full table:
19
19
  <https://asdalexey.github.io/vitest-auto-spy/agents>.
20
20
 
21
- | Resource | Where |
22
- | ----------------------- | -------------------------------------------------------------------- |
23
- | Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
24
- | Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
25
- | Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
26
- | Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
27
- | Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
28
- | Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
21
+ | Resource | Where |
22
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
24
+ | Task recipes | <https://asdalexey.github.io/vitest-auto-spy/guides/mocking-classes> · `/guides/mocking-local-storage` · `/guides/mocking-prisma` · `/guides/storybook-angular` |
25
+ | Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
26
+ | Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
27
+ | Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
28
+ | Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
29
+ | Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
29
30
 
30
31
  **Read `dist/*.d.ts` before inventing a call.** Every export is typed and documented there, and the
31
32
  type is the authority when this file and the code disagree.
@@ -62,13 +63,18 @@ Add-ons, orthogonal to the runner:
62
63
  | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
63
64
  | Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
64
65
  | Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
65
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
66
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
66
67
  | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
67
68
  | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
68
69
 
69
70
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
70
71
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
71
72
 
73
+ `vitest-auto-spy/setup` is Vitest-only as well — `setupAutoSpy()` registers Vitest hooks. On
74
+ `bun test` nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy between tests;
75
+ put `afterEach(() => { restoreMockedProps(); mock.restore(); })` in a `--preload` file, and any
76
+ `mock.module()` there too (inside a test it swaps bindings after the original module already ran).
77
+
72
78
  The rxjs peer is **>= 7.2**, not `>=7`. The observable layer used to pull `concatMap`, `delay`,
73
79
  `switchMap`, `take`, `takeUntil` and `takeWhile` from `rxjs/operators`; rxjs 8 removes that deep path,
74
80
  so the specifier moved to the root `rxjs` entry — which is where rxjs re-exported them in 7.2 — and
@@ -88,7 +94,19 @@ and under pnpm's isolated layout it never resolved before.
88
94
 
89
95
  The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
90
96
  CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
91
- Vitest refuses to be required).
97
+ Vitest refuses to be required). The plugin's CommonJS declaration is an `export =`, which is what
98
+ `require('vitest-auto-spy/eslint-plugin')` actually returns — an `eslint.config.cts` / `.cjs` used to
99
+ type-check the call that throws and reject the one that works.
100
+
101
+ `vitest-auto-spy/package.json` resolves as well. Tools that read a dependency's manifest by specifier
102
+ — Storybook, Nx, a renovate helper — used to get `ERR_PACKAGE_PATH_NOT_EXPORTED` and had nowhere to
103
+ go from there.
104
+
105
+ **Every peer is optional now, `vitest` included.** The range is unchanged (`>=2.1.0`); what changed
106
+ is that a suite on `/bun` or `/node`, which never loads the Vitest runner, no longer installs it to
107
+ satisfy a peer. The one thing still to know is that `/bun`, `/bun-angular` and `/node` declarations
108
+ name Vitest's `Mock` type, so a project type-checking those entries without `vitest` installed will
109
+ want it as a devDependency anyway; freeing that is a type change and waits for a major.
92
110
 
93
111
  ---
94
112
 
@@ -127,7 +145,11 @@ noise scales with the number of them. Both take the same second argument (`retur
127
145
  `observablePropsToSpyOn`).
128
146
 
129
147
  `createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
130
- `no-type-assertion` lint rule without an `eslint-disable` on every fixture.
148
+ `no-type-assertion` lint rule without an `eslint-disable` on every fixture. `createMock<T>(undefined)`
149
+ is the same call as `createMock<T>()` and answers `{}`, **not** `undefined` — a forwarding helper
150
+ with an optional `overrides` parameter relies on that. A fixture that means "no value" passes
151
+ `undefined` itself: `getters.profile.mockReturnValue(undefined)`, never
152
+ `mockReturnValue(createMock<Profile>(undefined))`.
131
153
 
132
154
  **`createSpyFromInstance(instance, config?)` is the one case where nothing is constructed.** Every
133
155
  factory above builds the double, which is no help once the object exists and other code already
@@ -160,6 +182,17 @@ member declared `configurable: false`, come back as this package's diagnostic ra
160
182
  `sinon.createStubInstance` builds a new object from a constructor instead of patching the one you
161
183
  hold, and `bun:test` and `node:test` have nothing.
162
184
 
185
+ **`passthrough: true` keeps the patched object working.** Unconfigured methods run the real
186
+ implementation (with the instance as `this`) and are still recorded; any configuration takes the
187
+ whole method over and `resetAutoSpy` hands it back. The Angular shape is
188
+ `createSpyFromInstance(TestBed.inject(CartService), { passthrough: true })` — dependencies, `signal()`
189
+ fields, `ɵprov` and the real `ngOnDestroy` keep working. Lifecycle hooks, discovered callables with an
190
+ API of their own (signals) and discovered classes are left real rather than spied. Do **not** combine
191
+ it with `strict: true` or `onUnstubbedCall` on the same call — that throws; a suite-wide strict yields
192
+ to it. A `calledWith(1)` miss answers `undefined`, not the real method: configuring a method hands the
193
+ whole method over. Use it to assert an interaction on a real collaborator; use a plain double when
194
+ the test must not touch the real one.
195
+
163
196
  **`createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` are for the model
164
197
  that more than one spec builds.** The difference from `createMock` is the `defaults` argument: it is
165
198
  a **complete** `T`, checked in full, in one place — so a field the model dropped fails there instead
@@ -198,9 +231,26 @@ boot(asInstance(logger)); // → AppLogger, for the API under test
198
231
  asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // → the helpers
199
232
  ```
200
233
 
234
+ **Arrays and unmocked calls on `mockDeep`.** A member read by a numeric index (`api.items[0]`) is a
235
+ real `Array` of deep mocks from then on, and `map`, iteration and `toEqual` work on it. Read the
236
+ member again after the first index: a handle taken before the first index is still a node. To make a
237
+ call nobody configured fail, use `mockDeep<T>({}, { fallbackMockImplementation: () => { throw … } })`.
238
+ Options go in the **second** argument, not the first as in vitest-mock-extended. The precedence is
239
+ configuration > fallback > `selfReturning`. A `calledWith` miss answers `undefined`, not the
240
+ fallback; use `mustBeCalledWith` for "other arguments fail". For a callback API (`$transaction`), use
241
+ `method.mockImplementation((run) => run(asInstance(mock)))`; there is no option for it.
242
+ `vi.spyOn(mock.repo, 'find')` works on a member nobody has read and returns the node's own spy.
243
+
201
244
  `asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
202
245
  sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
203
246
 
247
+ **A deep node asks what the spy surface is on every read**, rather than remembering the answer from
248
+ the first deep mock of the worker. That matters under `isolate: false`, where the surface grows
249
+ mid-run: `import 'vitest-auto-spy/rxjs'` in a later file adds `nextWith` and friends, and
250
+ `setSpyEngine` swaps the whole prototype. With the answer cached, `deep.feed.items.nextWith(1)` in
251
+ every double built after that resolved to a **child node** — callable, recorded, emitting nothing —
252
+ while the spec waited on a stream that was never fed.
253
+
204
254
  **`selfReturning: true` chains a factory, not a `return this` builder**, and the difference decides
205
255
  where the calls are recorded. A called node answers _itself_, not the object the method was read
206
256
  off, so every hop moves one level deeper: `editor.chain().focus().insertContent('text')` records
@@ -354,15 +404,44 @@ five providers across two thousand tests is about two hundredths of a second. Ca
354
404
  `autoSpyAccessors: true` walks the prototype chain uncached on every call — name the accessors
355
405
  instead.
356
406
 
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.
407
+ Memory used to be the exception, and it no longer is for the default. `lazySpies: true` still
408
+ defines one property per method, but the accessor pair behind it is now **shared by method name**
409
+ across every double in the process, so the placeholder is no longer what an untouched double
410
+ retains: an untouched 100-method double holds **215 B** where it held 25 593, and a 300-method one
411
+ 284 B where it held 70 165 (`npm run bench:memory`, 2026-09-17, Node 24). The price is paid at
412
+ construction on very wide classes — building a 300-method double costs about 28 % more — and
413
+ materialising every method of one is about 26 % cheaper in exchange.
414
+
415
+ That inverts the case for `lazySpies: 'proxy'`, which answers every method from a single trap
416
+ object: it now retains **4 090 B** against the default's 215 B at 100 methods — 19× more — and still
417
+ pays a trap on every read (53 ns against 7 ns) for the life of the double, because a `Proxy` cannot
418
+ remove itself while the default leaves a plain data property behind once a method materialises. What
419
+ is left of its advantage is build time on a wide class. The option is unchanged and still supported;
420
+ there is no longer a memory reason to reach for it.
421
+
422
+ A frozen or sealed double is fine now. `Object.freeze(spy)` — a deep-freeze fixture helper, a
423
+ dev-mode state guard — used to make the first read of any method throw `Cannot redefine property`
424
+ from inside the placeholder's getter; the spy is kept beside the double instead, so the read answers
425
+ a stable mock and `mockReturnValue` on it works. On a merely sealed double an assignment still
426
+ reaches the member.
427
+
428
+ `vi.spyOn(double, 'load')` on a method nobody has read yet works again. Vitest reads an accessor by
429
+ calling its getter with no receiver, and 5.19.0's shared getter answered that with
430
+ `TypeError: Invalid value used as weak map key`. The call now wraps a forwarder: a configured
431
+ `mockReturnValue` answers, an unconfigured call reaches the double's own spy (strict guard included),
432
+ and `mockRestore()` / `vi.restoreAllMocks()` hand back that same spy with the calls it recorded. It
433
+ is still redundant — the member already is a spy, so `double.load.mockReturnValue(…)` is the line to
434
+ write — and a wrapped method called off its double (`const { load } = double; load()`) throws a
435
+ message saying so.
436
+
437
+ **Symbol-keyed methods are discovered and spied.** A method under a symbol the project owns —
438
+ `[SERIALIZE]()`, `Symbol.for('app.render')` — is a method like any other, resets with the rest and
439
+ shows up in `Reflect.ownKeys`. The runtime's own protocol symbols are deliberately left alone
440
+ (everything on `Symbol` itself — `Symbol.iterator`, `Symbol.dispose`, `Symbol.toPrimitive` and the
441
+ rest — plus `Symbol.for('nodejs.util.inspect.custom')`), because a double that answers those stops
442
+ being a double of `T` and starts impersonating an iterable or a disposable. Discovery covers methods
443
+ only: a symbol-keyed **getter or setter** is not found by `autoSpyAccessors`, so patch one with
444
+ `mockAccessorsProp` (§9).
366
445
 
367
446
  ---
368
447
 
@@ -480,8 +559,9 @@ included — TypeScript resolves a type-only import the same way.
480
559
  users.getName.calledWith(1).mockReturnValue('Ada');
481
560
  // argument enforcement — other arguments throw
482
561
  users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
483
- // asymmetric matchers work in both
562
+ // asymmetric matchers work in both, at any depth
484
563
  users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
564
+ users.save.calledWith({ id: expect.any(Number), tags: [expect.any(String)] }).mockReturnValue(true);
485
565
  // re-registering the same arguments replaces the answer — matcher arguments included
486
566
  users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(false);
487
567
 
@@ -523,6 +603,31 @@ makes a second `calledWith(1, expect.anything())` an override rather than a seco
523
603
  behind the first. A hand-rolled `{ asymmetricMatch }` object is compared by identity instead: its
524
604
  verdict is a closure, so only re-registering that same instance overrides.
525
605
 
606
+ **A matcher counts wherever it sits, not only at the top level.** A config argument holding a
607
+ matcher — or a function — anywhere inside it is compared structurally rather than as data:
608
+ `calledWith({ id: expect.any(Number) })`, `calledWith([expect.any(String)])`, a matcher inside a
609
+ `Map` value or a `Set` member. The same comparison decides the rest of an argument's shape, so it is
610
+ worth knowing what it treats as equal: `Map` and `Set` are compared **without regard to insertion
611
+ order**, a `Date` by its time, a `RegExp` by its source and flags, an `Error` by its `name` and
612
+ `message` plus its own enumerable fields, a function by identity, and symbol-keyed properties
613
+ participate like string ones. Cycles are handled, so a component graph with a back-edge is a legal
614
+ argument. `mustBeCalledWith` uses the same map, and its `Wanted:` line renders a matcher as
615
+ `Any<Number>` rather than as the object it serialises to.
616
+
617
+ Each `calledWith(...)` / `mustBeCalledWith(...)` call hands back **its own** handle, so a chain kept
618
+ in a variable stays attached to the arguments it was written with:
619
+
620
+ ```ts
621
+ const found = users.load.calledWith(1);
622
+ users.load.calledWith(2).mockReturnValue(undefined);
623
+
624
+ found.mockReturnValue({ id: 1 }); // configures 1, not 2
625
+ ```
626
+
627
+ `new` on a method spy works: `new sdk.Client()` — the shape `createAutoMock<{ Client: typeof Client }>()`
628
+ and `mockDeep` produce — hands back the instance, or the object a `calledWith(...).mockReturnValue(...)`
629
+ configured for those arguments.
630
+
526
631
  `mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
527
632
  all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
528
633
 
@@ -541,11 +646,28 @@ onDone.value(); // and now exercise what was passed
541
646
  expect(component.finished()).toBe(true);
542
647
  ```
543
648
 
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.
649
+ `.values` holds every value the captor was **offered**, oldest first — candidates, not matches. A
650
+ captor in position 0 of `toHaveBeenCalledWith(captor, 3)` is asked about the first argument of every
651
+ call the runner tries, including the ones the `3` then rejects, so `.values` can be longer than the
652
+ set of calls the assertion accepted. `.captured` asks whether anything was recorded without
653
+ triggering the "nothing was captured" throw that reading `.value` raises; `.reset()` lets one captor
654
+ serve two phases.
655
+
656
+ `captureArg({ where })` narrows both halves at once — the filter decides what is recorded **and**
657
+ whether that position matches at all, so a rejected candidate leaves no entry in `.values` and the
658
+ whole expectation fails on it:
659
+
660
+ ```ts
661
+ const config = captureArg<RequestInit>({ where: (value) => (value as RequestInit)?.method === 'POST' });
662
+
663
+ expect(fetchSpy).toHaveBeenCalledWith(url, config); // only the POST call satisfies this
664
+ expect(config.value.headers).toEqual({ 'x-trace': '1' });
665
+ ```
666
+
667
+ The filter receives the raw argument as `unknown`, so narrow it yourself. **Assertions only** — a
668
+ captor without `where` matches every value, so putting one in `calledWith` would configure a return
669
+ for every call, which is `mockReturnValue` spelled less clearly, and `calledWith` is typed to the
670
+ method's own parameters so it will not compile anyway.
549
671
 
550
672
  **The observable helpers are backed by a `ReplaySubject(1)` that belongs to the spy, and it is
551
673
  configuration — so it must be reset with the rest of it.** Two failures used to come out of that
@@ -563,7 +685,14 @@ The code under test therefore ran the **success** branch on stale data, and the
563
685
  one emission late. The second: `error()` and `complete()` close a Subject permanently, so a later
564
686
  `nextWith` on that spy pushed into a dead subject and emitted nothing at all. Both are fixed —
565
687
  `resetAutoSpy(spy)` now drops the subject, and a terminated one is replaced on the next
566
- configuration.
688
+ configuration. That holds for a subject a spec closed **itself**, too:
689
+ `spy.items$.returnSubject().complete()` marks the stream closed, so the next `nextWith` opens a new
690
+ one rather than disappearing.
691
+
692
+ `nextWithValues` on an observable **property** builds a new stream, which a subscriber that already
693
+ holds the old one never sees — the spec's values go nowhere and the assertion below reads the
694
+ initial state. That case is reported now rather than passing silently; `nextWith` pushes into the
695
+ subject the current subscriber is on and is the one to reach for mid-test.
567
696
 
568
697
  What that does **not** change: `vi.clearAllMocks()` and `clearMocks: true` still cannot reach it,
569
698
  for the same reason they cannot reach a `calledWith` chain — that state lives in this library's
@@ -726,7 +855,15 @@ winning, scalars decided by the call site when it names the key. The bare-array
726
855
  walking the prototype chain would let one registration change doubles in files nobody was looking at.
727
856
  A second registration for the same class replaces the first, because two of them in one suite is the
728
857
  drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
729
- `clearAutoSpyDefaults()` the lot.
858
+ `clearAutoSpyDefaults()` the lot. `createSpyFromInstance(obj)` reads the registration of the class
859
+ `obj.constructor` names, merged the same way; an object literal resolves none. One exception, because
860
+ an instance is real: when the call site lists `onlyMethodsToSpyOn`, the rest of the object stays real,
861
+ so the registration contributes only `strict`, `onUnstubbedCall`, `onUnstubbedRead` and the
862
+ `returns` / `selfReturning` entries of the listed methods — not its accessor lists, its other method
863
+ lists or its `overrides`. `registerAutoSpyDefaults(Router, { gettersToSpyOn: ['url'] })` therefore
864
+ leaves `router.url` live under `createSpyFromInstance(router, { onlyMethodsToSpyOn: ['navigateByUrl'] })`.
865
+ A `returns` or `selfReturning` name the call site itself wrote for a method it left real is reported
866
+ as a misconfiguration and skipped.
730
867
 
731
868
  A setup file that registers more than a handful of classes can say them as one table instead of one
732
869
  call each. Rows apply in order, and each is checked against **its own** class — a key `Router` does
@@ -858,6 +995,12 @@ Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs:
858
995
  `accessorSpies.setters.theme` was `undefined`, and the failure read
859
996
  `Cannot read properties of undefined` three steps from the configuration behind it.
860
997
 
998
+ The bag is typed over every key of `T` unless the lists are repeated in the options type argument —
999
+ `createSpyFromClass<Settings, { gettersToSpyOn: ['theme'] }>(Settings, { gettersToSpyOn: ['theme'] })`
1000
+ keys both halves by exactly the configured names, so `accessorSpies.setters.other` is a compile error
1001
+ instead of an `undefined` at run time. Use it when a spec reaches into the bag by name; a
1002
+ non-literal `string[]` falls back to the every-key bag.
1003
+
861
1004
  Only spy a getter when the spec asserts that it was **read**. To make one _answer_ something, on a
862
1005
  spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
863
1006
  factory, which is the part that is otherwise found by trial:
@@ -916,6 +1059,15 @@ const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); //
916
1059
  const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
917
1060
  ```
918
1061
 
1062
+ `injectSpy(X)` without the argument keeps a declared default when the constructor does not take the
1063
+ type parameter. When it does — `constructor(public data: T, …)`, the shape of most modal refs — 5.19.0
1064
+ inferred `X<never>`, and with the typed `accessorSpies` bag `Spy<X<never>>` no longer assigns to
1065
+ `Spy<X<unknown>>`. Such a class is now read at its **constraint**: `ModalRef<T = unknown>` gives
1066
+ `Spy<ModalRef<unknown>>`, `ConfigService<T extends Config = Defaults>` gives
1067
+ `Spy<ConfigService<Config>>`. A constructor cannot hand TypeScript the default here, so spell the
1068
+ argument out whenever the default or a particular instantiation is what the spec means:
1069
+ `injectSpy<ConfigService>(ConfigService)`, `injectSpy<ModalRef<PurchaseOptions>>(ModalRef)`.
1070
+
919
1071
  It applies to `createSpyFromClass` with a configuration too, in one combination: an accessor list
920
1072
  (or `overrides`) **and** `returns` on a generic class. TypeScript checks a generic class argument
921
1073
  after the configuration, reads `T` back from `gettersToSpyOn: ['remoteConfig']` as
@@ -957,6 +1109,14 @@ resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mo
957
1109
  Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
958
1110
  proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
959
1111
 
1112
+ **`resetAutoSpy` is `vi.resetAllMocks()` for one double**, and two things it used to leave behind now
1113
+ go with the rest: a pending `mockReturnValueOnce` / `mockResolvedValueOnce` queue (it used to answer
1114
+ the first call _after_ the reset, in a later test) and an accessor spy's configuration (a
1115
+ `accessorSpies.getters.x.mockReturnValue(…)` used to survive and hand the next test the previous
1116
+ test's value). A spec that relied on a queued `Once` value outliving the reset reads `undefined`
1117
+ now. The double stays usable: the library's own dispatch is put back afterwards, so a fresh
1118
+ `calledWith` configures it as normal.
1119
+
960
1120
  Every double also carries `[Symbol.dispose]()` — it runs `resetAutoSpy(this)` — so an `afterEach`
961
1121
  that exists only to reset one spy can be deleted:
962
1122
 
@@ -1001,7 +1161,8 @@ await expectCompletion(service.purgeCache()); // "it finished" — the value is
1001
1161
  ```
1002
1162
 
1003
1163
  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,
1164
+ wait is a quiet window rather than a watchdog). `0` and `Infinity` both mean **no watchdog** — the
1165
+ wait then runs to the runner's own test timeout, which is the trade. The source is duck-typed, so rxjs `Observable`s,
1005
1166
  `Subject`s, Angular `toObservable()` results, Angular `output()` (`OutputEmitterRef`, whose
1006
1167
  `subscribe` takes a bare callback) and hand-rolled subscribables all work — and every helper infers
1007
1168
  the emitted type, so `expectEmission(of(1))` is a `Promise<number>`.
@@ -1069,15 +1230,34 @@ await expect(expectEmission(purchased$, { advance: () => vi.runAllTimers() })).r
1069
1230
  That replaces the fragile shape people arrive at — hold the promise, advance, then await — which
1070
1231
  breaks silently the moment somebody adds an `await` one line above it. It is a callback rather than
1071
1232
  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:
1233
+ runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`. A throw out of
1234
+ `advance` — or out of an `until` predicate — is reported as itself and tears the subscription down,
1235
+ rather than being lost while the wait runs on to the timeout.
1236
+
1237
+ **These helpers subscribe as a subscriber, so a synchronous source stops at the value that settles
1238
+ the wait.** `expectEmission(from([1, 2, 3]).pipe(tap(spy)))` calls `tap` once, not three times, so
1239
+ `expect(spy).toHaveBeenCalledTimes(1)` is honest and a `finalize` runs at the stop; before, the
1240
+ producer ran to completion before anything could unsubscribe, and an endless synchronous source
1241
+ (`of(1).pipe(repeat())`) hung the worker instead of resolving. `expectEmissions(source$, 3)` stops
1242
+ at the third. A source that cannot be subscribed to at all is reported by name now, with a separate
1243
+ hint when what was passed is a promise.
1244
+
1245
+ `expectEmissions(source$, 0)` throws at the call — a count below 1 is not something a stream can
1246
+ satisfy, and the message names `expectNoEmission` instead. A suite that wrote
1247
+ `expectEmissions(s, expected.length)` with an empty expectation is the one this changes.
1248
+
1249
+ **The watchdog runs on real time, on purpose — even under fake timers, and even under zone.js.** A
1250
+ virtual one would race the timers the spec advances: `expectEmission(source$, { timeout: 200 })`
1251
+ followed by `vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the
1252
+ spec was about to advance into. Inside `fakeAsync` that used to happen anyway, because zone.js
1253
+ patches the global `setTimeout` and the watchdog was scheduled onto the virtual queue: a
1254
+ `tick(1_500)` towards a `debounceTime(2_000)` rejected the wait it was advancing. The timer is taken
1255
+ from `__zone_symbol__setTimeout` now, so it is outside the zone and `tick()` cannot reach it.
1256
+
1257
+ The cost is that in a suite with global fake timers a _failing_ assertion spends a real second. Do
1258
+ **not** answer that with `{ timeout: 0 }` at every call site — that disables the watchdog, and the
1259
+ next silent stream hangs to the runner's own timeout with nothing useful in the message. Lower the
1260
+ default once instead:
1081
1261
 
1082
1262
  ```ts
1083
1263
  // vitest.setup.ts
@@ -1088,6 +1268,11 @@ setupAutoSpy({ globalFakeTimers: true });
1088
1268
  setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
1089
1269
  ```
1090
1270
 
1271
+ `setEmissionTimeout` takes `0` (no watchdog) and `Infinity` (the same), and **throws** on `NaN` and
1272
+ on a negative number rather than installing a default that fails every wait. The commonest way to
1273
+ reach it is arithmetic: `setEmissionTimeout(Number(process.env.EMISSION_TIMEOUT))` with the variable
1274
+ unset.
1275
+
1091
1276
  **The code frame these failures open is the spec line, not this package.** The helpers build their
1092
1277
  error inside a `subscribe` or timer callback, so the stack is captured at helper entry and pinned on
1093
1278
  when the failure is finally built; only errors these helpers make themselves are re-anchored, and
@@ -1261,6 +1446,15 @@ setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).
1261
1446
  A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
1262
1447
  the previous file is the one to fix.
1263
1448
 
1449
+ **`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
1450
+ away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
1451
+ the fake clock hands out bypasses the count entirely — which makes `expect(countStrayTimers()).toBe(0)`
1452
+ vacuous in any file running on a frozen clock, `setupAutoSpy({ strayTimers: true, globalFakeTimers: true })`
1453
+ included. Read `vi.getTimerCount()` for the fake clock's own backlog; the two answer different
1454
+ questions and neither covers the other. A handle cleared by the number it coerces to
1455
+ (`clearTimeout(+handle)`) or through its own `close()` is forgotten properly now, so those stopped
1456
+ being counted as strays.
1457
+
1264
1458
  **With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
1265
1459
  arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
1266
1460
  Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
@@ -1288,8 +1482,10 @@ file does (`import 'zone.js';`) or the Angular builder does. Without it the call
1288
1482
  than quietly watching nothing. Native, non-zone rejections already fail a Vitest run, and nothing
1289
1483
  here touches them. The pieces are exported too — `trackStrayRejections()` (idempotent, returns the
1290
1484
  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).
1485
+ `countStrayRejections()`. The count is complete; the **reasons** retained are capped at 100, so a
1486
+ file rejecting in a loop cannot hold its whole run's worth of stacks. A promise rejected with
1487
+ `undefined` is reported as that rather than as `[object Object]`. The `no-floating-assertion` lint
1488
+ rule catches the commonest shape before it ever runs (§16).
1293
1489
 
1294
1490
  **`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
1295
1491
  about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
@@ -1390,6 +1586,13 @@ does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMock
1390
1586
  (new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
1391
1587
  changes, and the peer range still starts at 2.1 — one install spans Vitest 2.1 through 5.x.
1392
1588
 
1589
+ What each config flag does to an auto-spy, measured on both spy engines: `clearMocks` empties the
1590
+ calls and keeps the configuration; `mockReset` empties the calls and drops `mockReturnValue` /
1591
+ `mockImplementation` but **keeps `calledWith` rules**; `restoreMocks` never touches an auto-spy (it
1592
+ only undoes `vi.spyOn`). Do not claim the flags skip auto-spies — a sweep sentinel in the Vitest
1593
+ adapter routes the first two. `resetAutoSpy(spy)` is the call that drops everything, `calledWith`
1594
+ included. `mockReset()` on a `vi.spyOn(…)` runs the **real** method again (Vitest ≥ 3).
1595
+
1393
1596
  What Vitest 5 does break is the runner's own doing, not this library's: with `clearMocks` on by
1394
1597
  default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
1395
1598
  zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
@@ -1435,6 +1638,12 @@ another:
1435
1638
  setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setting
1436
1639
  ```
1437
1640
 
1641
+ **Called twice, the last caller's mode wins.** The setup file installs from its own `beforeEach`,
1642
+ which runs before the spec's, so a spec calling `blockNetwork({ xhr: 'reject' })` because the failure
1643
+ branch is what it is testing used to be served the setup file's empty 200 and pass on the wrong
1644
+ branch. The stubs are also installed once rather than re-journalled per test, so `restoreProps: false`
1645
+ no longer grows the restore journal for the length of the run.
1646
+
1438
1647
  A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
1439
1648
  serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
1440
1649
  and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
@@ -1443,6 +1652,16 @@ nothing listening on that port.`WebSocket`and`EventSource`are left alone: their
1443
1652
  event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
1444
1653
  a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
1445
1654
 
1655
+ - `stubResponse(init?)` (`/setup`) builds a real `Response` — never write `{ ok, json } as Response`.
1656
+ Plain data in `body` goes out as JSON with `application/json`; an `ok` that disagrees with
1657
+ `status` throws. A body can be read once: for a stub answering several calls use
1658
+ `mockImplementation(async () => stubResponse(…))`, not `mockResolvedValue`.
1659
+ - `blockNetwork` leaves `fetch` alone while `Symbol.for('fetch-interceptor')` is on `globalThis`
1660
+ (MSW `setupServer`, nock 14), so MSW handlers keep answering; XHR stays blocked for what MSW does
1661
+ not handle. MSW's `onUnhandledRequest: 'error'` exempts asset-looking URLs (`.svg`, `.json`,
1662
+ fonts…); a last `http.all('*', () => HttpResponse.error())` is the hard floor. MSW's browser
1663
+ `setupWorker` has not been checked.
1664
+
1446
1665
  `restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
1447
1666
  fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
1448
1667
  the realm, not owned by `globalThis`), and with `isolate: false` the next file dies inside Vitest's
@@ -1571,6 +1790,17 @@ reads as a regression and is not one.
1571
1790
  `Date is not a constructor` with a stack in production code and no mention of timers.
1572
1791
  `mockSystemTime` does the right thing either way.
1573
1792
 
1793
+ Two rules about which of these composes with which, both of them earned:
1794
+
1795
+ - **`withSystemTime` puts the previous time back**, including when fake timers were already running.
1796
+ Its undo used to be a no-op there, so the clock stayed where the callback left it for the rest of
1797
+ the test.
1798
+ - **`mockSystemTime()` installs `Date` and nothing else**, so `advanceTimers()` under it has nothing
1799
+ to advance. It used to pass having done nothing; it now refuses and names `setupFakeTimers()`.
1800
+ `setupFakeTimers(config)` installs the config it was given rather than deferring to whatever fakes
1801
+ were already up — a nested call, a global pair plus a local one, and the `Date`-only case all now
1802
+ end with the clock the call asked for, where the outer configuration used to win silently.
1803
+
1574
1804
  `useCountingClock` exists because under fake timers every call inside one test reports the _same_
1575
1805
  "now", so a spec that asserts on **order** or **duration** — analytics batches, tracing, a rate
1576
1806
  limiter, a TTL cache — cannot express its expectation at all. Patching `Date.now` by hand does not
@@ -1640,8 +1870,18 @@ setupAutoSpy({ guardGlobals: 'throw' }); // or 'warn' while a suite is being cle
1640
1870
  `Object.defineProperty(document, 'cookie', { value })` defaults `configurable` to `false`, so the
1641
1871
  property can no longer be redefined _or_ deleted. Under `isolate: false` every later file in the
1642
1872
  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.
1873
+ that did it. The guard compares `globalThis`, `document`, `navigator`, `location`, `screen` and the
1874
+ DOM prototypes a Jest-era stub reaches for — `Element`, `HTMLElement`, `HTMLCanvasElement`,
1875
+ `HTMLMediaElement`, `Node`, `EventTarget` — around every test, symbol keys included, and reports only
1876
+ what appeared and cannot be removed.
1877
+
1878
+ **Its blind spot is an existing name redefined in place.** The guard reads a descriptor only for a
1879
+ key that was not there before, so `Object.defineProperty(document, 'cookie', { value, configurable: false })`
1880
+ over the `cookie` the environment already has is invisible to it: the name was in the snapshot, and
1881
+ only its configurability changed. What it catches is the addition — the shape that actually breaks a
1882
+ later file, because the next `defineProperty` of a name nothing expected is the one that throws.
1883
+ The snapshot is taken once per file, in `beforeAll`, rather than per test, and the comparison is also
1884
+ run after every `afterAll`, so a write made in a `beforeAll` or an `afterAll` is reported too.
1645
1885
 
1646
1886
  ### Naming the file that polluted `Object.prototype`
1647
1887
 
@@ -1662,6 +1902,16 @@ a `useValue` provider or a test double — patch the prototype of the class the
1662
1902
  `guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
1663
1903
  that does not call `setupAutoSpy()`.
1664
1904
 
1905
+ **A key an earlier file left behind is taken back before the next one is collected.** The per-test
1906
+ comparison cannot see a write made while a spec file was being imported or collected, or in its
1907
+ `afterAll` — all three happen outside any test, and the file that then fails to collect is the
1908
+ innocent one. `setupAutoSpy()` therefore also checks at the moment it is called, which is the one
1909
+ seam Vitest leaves between files: the key is deleted, a line goes to **stderr** naming it, and
1910
+ **nothing fails** — the file that would fail is not the file that wrote it. A key that refuses to be
1911
+ deleted is adopted into the baseline rather than reported once per file for the rest of the run. The
1912
+ in-file check still throws at its default grade; the file's own baseline is taken in `beforeAll` and
1913
+ re-checked after every `afterAll`, so a write made in either is covered as well.
1914
+
1665
1915
  ### Naming the test that left an attribute on `<body>`
1666
1916
 
1667
1917
  ```ts
@@ -1707,13 +1957,19 @@ frame outside `node_modules`:
1707
1957
  - **Absorbed** means the call never reached the console: a `/console` spy installed for the test or
1708
1958
  the file, `vi.spyOn(console, m).mockImplementation(…)`, any replacement that does not call
1709
1959
  through. A bare `vi.spyOn(console, m)` calls through and **counts**.
1960
+ - **This library's own `'warn'`-grade reports are not stray console output.** `guardGlobals`,
1961
+ `prototypePollution`, `unconfiguredReads`, `swallowedStrictCalls`, `misconfiguration`, the
1962
+ duplicate-copy report, the skipped-teardown net and the `test.concurrent` notice write past the
1963
+ guard's wrapper, so `'warn'` stays a warning under `strayConsole: 'throw'` instead of failing the
1964
+ test it is advising about. A `vi.spyOn(console, 'warn')` a test installs still absorbs them — the
1965
+ channel checks that the guard's own wrapper is the installed method and steps aside when anything
1966
+ sits on top of it.
1710
1967
  - Output **outside any test** — collection/import, `beforeAll` / `afterAll`, a callback after its
1711
1968
  test ended — fails the **file** in `afterAll` (`… outside any test …`).
1712
1969
  - Every console method a test replaced is put back after it; one a file replaced, after the file.
1713
1970
  - Under the guard, **importing `vitest-auto-spy/console` installs nothing** — call
1714
1971
  `installConsoleSpies()` in a `beforeEach` (or at the top of the file). Spies an import installed
1715
1972
  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
1973
  - `strayConsole: { allow: ['…', /…/] }` is the last resort, for environment noise no spec can reach.
1718
1974
  Output written to `process.stdout` / `process.stderr` directly, and jsdom's own virtual console, is
1719
1975
  not seen.
@@ -1747,6 +2003,23 @@ every occurrence. The grade is process-wide and released after the file. The pri
1747
2003
  cancels — jsdom answers every Web Storage write with a real `setTimeout(…, 0)`, and the library's own
1748
2004
  storage probe runs under it.
1749
2005
 
2006
+ **`test.concurrent` earns one warning per worker, and it is worth reading rather than silencing.**
2007
+ The per-test guards assume one test at a time: the document snapshot, the console window and the
2008
+ unconfigured-read counter are opened and judged per test, so with two in flight a finding can be
2009
+ charged to the other one — or cleared before anything sees it. The restores still run for every
2010
+ test, and the skipped-teardown net now remembers per test rather than per file, which is the half
2011
+ that was simply wrong. Run the files that need a guard sequentially, or keep `test.concurrent` for
2012
+ files whose setup passes `strayConsole: 'off'`, `documentPollution: 'off'` and
2013
+ `unconfiguredReads: 'off'`.
2014
+
2015
+ **What `/setup` costs per test**, measured on happy-dom over 10 000 empty tests (2026-09-17, Node
2016
+ 24): the default `setupAutoSpy()` **19 µs**, `guardGlobals` **59 µs** (it snapshots once per file
2017
+ now, not once per test), `documentPollution` **23 µs** — it shares the `onTestFinished` the teardown
2018
+ net registers anyway — and `preset: 'strict'` **67 µs**. `documentPollution: { nodes: true }` over a
2019
+ `<head>` of 1000 children is **0.19 ms** per test rather than 7.1 ms, because the children are walked
2020
+ as a sibling chain instead of through a live `HTMLCollection`; an `ignoreNodes` selector's answer for
2021
+ an element is decided at snapshot time and reused for that test.
2022
+
1750
2023
  ### Hook order differs from Jest
1751
2024
 
1752
2025
  Vitest runs `afterEach` hooks as a stack (innermost / last-registered first); Jest ran them in
@@ -1853,6 +2126,26 @@ expect(Image.instances[0].src).toBe('https://tns.example/hit');
1853
2126
  throws a named error if it is ever called **without** `new`. `stubConstructor` installs through
1854
2127
  `mockValueProp`, so `restoreMockedProps()` puts the platform's constructor back.
1855
2128
 
2129
+ **Static members are opt-in — `createSpyClass(Class, config, { statics: true })`.** Production code
2130
+ that reaches the class rather than an instance (`BackgroundWorker.isSupported()`,
2131
+ `Client.fromToken(t)`, a `VERSION` constant) finds nothing on the double otherwise, and the failure
2132
+ is a `TypeError` on the class, not on the spy:
2133
+
2134
+ ```ts
2135
+ const WorkerSpy = createSpyClass(BackgroundWorker, undefined, { statics: true });
2136
+
2137
+ WorkerSpy.isSupported.mockReturnValue(true); // needs a cast — see below
2138
+ ```
2139
+
2140
+ What the option copies, walking the class's own prototype chain so inherited statics come too: a
2141
+ static **function** becomes a full spy of its own, a static **data member** is copied by value, and a
2142
+ static **accessor** is skipped and never evaluated — a getter that reads configuration or touches the
2143
+ network must not run because a double was built. `prototype`, `length`, `name`, `caller`,
2144
+ `arguments` and the double's own `calls` / `instances` are never overwritten. It is off by default
2145
+ for that last reason: a class with a static named `calls` would otherwise shadow the construction
2146
+ log. **The statics are not typed** — the return type is still `ConstructorSpy<T>`, so reading one
2147
+ needs a cast at the spec; type them and this note goes.
2148
+
1856
2149
  For the three observers, prefer the purpose-built stubs (§13). For `AbortController` — which breaks
1857
2150
  in a jsdom run for a reason involving none of the three parties in the stack trace — use
1858
2151
  `stubAbortController()`.
@@ -1879,11 +2172,15 @@ const media = stubMediaElement({ duration: 120 });
1879
2172
 
1880
2173
  media.set(video, { readyState: 1 }); // → loadedmetadata
1881
2174
  media.set(video, { currentTime: 119 }); // → timeupdate
1882
- media.set(video, { ended: true }); // → ended
2175
+ media.set(video, { ended: true }); // → pause, then ended — and the element is paused
1883
2176
  expect(media.play).toHaveBeenCalledTimes(1);
1884
2177
  ```
1885
2178
 
1886
- State is per element, so an ad and the content report different durations.
2179
+ State is per element, so an ad and the content report different durations — and per **install**, so
2180
+ an element held in module scope or left in the document under `isolate: false` takes the new
2181
+ install's `duration` rather than the previous one's. `set({ ended: true })` pauses the element and
2182
+ fires `pause` before `ended`, as the platform does: an element that is both ended and playing is a
2183
+ state no browser produces, and a player listening for `pause` was never told.
1887
2184
 
1888
2185
  `currentTime` is a get/set pair, so a player restarting itself with `video.currentTime = 0` reaches
1889
2186
  the record and fires `timeupdate` too — `media.set()` is not the only way in, and the component's own
@@ -1939,6 +2236,29 @@ mock` from **inside that dependency**. `moduleNamespace` is the shape it expects
1939
2236
  vi.mock('shaka-player', () => moduleNamespace({ Player: mockConstructor(() => playerStub) }));
1940
2237
  ```
1941
2238
 
2239
+ A factory that spells out its own `default` **keeps it** — `moduleNamespace({ default: dayjsStub, utc })`
2240
+ gives the probing dependency `dayjsStub`, where it used to be replaced by the namespace itself and
2241
+ the default export silently became the wrong object. Only a factory without one gets
2242
+ `default: <the namespace>`, which is the interop shape it was there for; the return type follows.
2243
+
2244
+ A factory's `vi.fn()` comes back typed as the real function, without `calledWith` or `resolveWith`.
2245
+ `adoptMock` takes it over in place — same object, history kept, typed from the export's signature —
2246
+ and an unconfigured call keeps answering what the mock answered before:
2247
+
2248
+ ```ts
2249
+ import { loadUser } from './api';
2250
+
2251
+ vi.mock('./api', () => ({ loadUser: vi.fn() }));
2252
+ adoptMock(loadUser).calledWith(7).resolveWith({ id: 7, name: 'Ada' });
2253
+ ```
2254
+
2255
+ To keep the real module and configure one case, spy it through:
2256
+ `vi.mock('./api', async (importOriginal) => moduleNamespace(await importOriginal(), { passthrough: true }))`.
2257
+ Every function export runs for real and is recorded until configured; classes and values stay real;
2258
+ calls one export makes to another inside the module are not recorded. A `vi.spyOn` / `{ spy: true }`
2259
+ mock calls an original it does not report, so adopting it makes an unconfigured call answer
2260
+ `undefined`; `node:test`'s `mock.fn()` is refused.
2261
+
1942
2262
  There is no `mockModule(…)` helper here, and there cannot be: Vitest hoists the literal `vi.mock`
1943
2263
  call, so a wrapper around it would be hoisted as a call to a function that does not exist yet. Share
1944
2264
  a fixture between the factory and the tests with `vi.hoisted()`.
@@ -2103,12 +2423,19 @@ old signal, silently and for the rest of the test. Writing through keeps all of
2103
2423
  `model()`'s output half alive, and leaves nothing for `restoreMockedProps()` to undo — the value
2104
2424
  stays where the spec left it.
2105
2425
 
2106
- Two shapes are still swapped: a `computed()` the class declares, and a member the spy does not have
2426
+ **A `signal().asReadonly()` member counts as writable.** The view a class exposes is a different
2427
+ function from the signal behind it, but it is the same reactive node, so the value is set through the
2428
+ node and the member the class publishes keeps working — whenever it was first read. That is the
2429
+ common shape for a store's public state (`readonly items = this.#items.asReadonly()`), and it used to
2430
+ be swapped, which meant a `computed()` that had already read it never saw the new value.
2431
+
2432
+ One shape is still swapped: a `computed()` the class declares, and a member the double does not have
2107
2433
  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
2434
+ `stable(fixture)`. A `computed()` a live consumer has already read is refused by name rather than
2435
+ replaced where nothing would notice it — the refusal now names `computed()` specifically, and says
2436
+ that driving the signal the computation reads is the way through. An `input()` is refused outright:
2437
+ Angular sets an input through the input node rather than the property, so a replaced one breaks the
2438
+ host's next write. Drive an input with `await setInputs(fixture, { … })`, or with
2112
2439
  `renderShallow(Component, { inputs: { … } })` for the value it starts at.
2113
2440
 
2114
2441
  ### Observers the component constructs itself
@@ -2143,10 +2470,18 @@ Three more knobs, each for a shape that otherwise gets hand-rolled:
2143
2470
  ```ts
2144
2471
  stubIntersectionObserver({ autoEmit: true }); // every observed target reports as visible, at once
2145
2472
  observers.last.options; // the init object: { rootMargin, threshold, … }
2473
+ observers.last.host; // the observer the code under test constructed — and its callback's 2nd argument
2146
2474
  observers.last.emit([mutationRecord(host, { addedNodes: [span] })]);
2147
2475
  observers.last.emit([resizeEntry(host, { width: 320 })]);
2148
2476
  ```
2149
2477
 
2478
+ **The callback's second argument is the observer the code under test holds**, not an internal
2479
+ record — so `(entries, observer) => observer.unobserve(entries[0].target)`, `observer.disconnect()`
2480
+ and `observer.takeRecords()` all reach the thing the spec drives, which is what production code
2481
+ written against the platform expects. `root`, `rootMargin` and `thresholds` read back off the init
2482
+ the way the platform normalises them (`null`, `'0px 0px 0px 0px'`, `[0]` by default) rather than as
2483
+ empty values.
2484
+
2150
2485
  `autoEmit` is the mode a suite ported from Jest needs: there the global mock fired its callback with
2151
2486
  `isIntersecting: true` immediately, so lazily-loading sections fetched their data during
2152
2487
  `detectChanges()`. Against the default inert observer those specs assert on an empty component and
@@ -2311,12 +2646,32 @@ helper of its own or wants the HTTP one mid-test — reading takes the requests,
2311
2646
  re-report what you inspected.
2312
2647
 
2313
2648
  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
2649
+ `provideHttpClientTesting()` / `HttpClientTestingModule` — including one reached through a nested
2650
+ module, which is the usual `imports: [SharedTestingModule]` shape and used to make `pendingRequests`
2651
+ inert without saying so — and a project using neither is genuinely inert. `ngModuleScopes` only fires
2652
+ on a module that contributes **nothing at all** — a providers-only module is legitimately
2653
+ scope-empty, so a stripped scope that still has providers passes silently; hand-call
2317
2654
  `assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
2318
2655
  not fire when `declarations` is non-empty.
2319
2656
 
2657
+ **`getTestBed()` is covered as well as `TestBed`.** The instrumentation sits on the TestBed
2658
+ _instance_ every static delegates to, so the configuration inspectors, `shadowedProviders` and the
2659
+ `overrideComponentProvider` verification all see `getTestBed().configureTestingModule(…)` and
2660
+ `getTestBed().createComponent(…)` — the shape a setup helper of your own usually writes — and
2661
+ `overrideTemplate` is counted with the rest of the overrides. A suite on `getTestBed()` used to get
2662
+ silence from every one of these.
2663
+
2664
+ Two more things now judge what Angular ends up with rather than one call: `deadSchemas` tallies
2665
+ `schemas` and `declarations` across **every** `configureTestingModule` of a test, because Angular
2666
+ accumulates them — a schema added in a second call next to a component declared in the first is no
2667
+ longer reported, and the reverse order is no longer silently passed — and a test that resets the
2668
+ testing module twice reports the requests of every module it built, not only the last.
2669
+
2670
+ `enableAngularDiagnostics()` and `mockSignalProp()` both check once per worker that the Angular
2671
+ internals they read are still there, and **throw** naming the installed `@angular/core` version when
2672
+ one has moved. Nothing about that is fixable from a spec: the throw tells you which shape went and
2673
+ what stopped working, and the repair is a release of this package.
2674
+
2320
2675
  ### `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
2321
2676
 
2322
2677
  ```ts
@@ -2363,6 +2718,11 @@ Three things to know before reaching for it:
2363
2718
  suite using `provideHttpTesting()` everywhere the diagnostic is redundant; keep it on while any
2364
2719
  file still configures HTTP testing by hand.
2365
2720
 
2721
+ `provideHttpTesting({ verifyOnTeardown })` reads which test is running off the runner's own state,
2722
+ and where that state is absent — `bun:test`, `node:test`, anything that is not Vitest — the
2723
+ end-of-test check cannot arm. It now says so once per worker instead of checking nothing quietly;
2724
+ call `verifyNoPendingRequests()` yourself there.
2725
+
2366
2726
  ### `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
2367
2727
 
2368
2728
  ```ts
@@ -2576,6 +2936,10 @@ Five things to know:
2576
2936
  records the reload, leaves `location.href` real, and `win.location.href = '/next'` moves the
2577
2937
  double rather than the address bar. `mockValueProp(TestBed.inject(WINDOW), 'innerWidth', 800)` and
2578
2938
  its restore work on the double too, and neither touches the global.
2939
+ - **Constructors come out unbound.** `win.Date`, `win.Promise`, `win.Object`, `win.Event` are handed
2940
+ back as themselves rather than through a binding that loses the statics, so `win.Date.now()`,
2941
+ `win.Promise.resolve()`, `win.Object.keys(x)` and `new win.Event('x')` work, and
2942
+ `win.Event === window.Event` holds — which is what `instanceof` in production code needs.
2579
2943
 
2580
2944
  ### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
2581
2945
 
@@ -2772,6 +3136,7 @@ const { fixture, component } = renderShallow(TaskListComponent, {
2772
3136
  await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
2773
3137
  await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
2774
3138
  // a fixture whose componentType carries no ɵcmp is refused by name too, not as a bare TypeError
3139
+ // aliases resolve by CLASS FIELD name, and an input a hostDirectives entry exposes is accepted
2775
3140
 
2776
3141
  // build a class through DI, every unprovided token auto-spied
2777
3142
  const { instance, spies } = createWithAutoSpies(CartService, {
@@ -2815,6 +3180,15 @@ expect(component.products).toHaveResourceValue([product]);
2815
3180
  expect(component.products).toHaveResourceError(/503/);
2816
3181
  ```
2817
3182
 
3183
+ **`renderShallow({ inputs })` and `setInputs` resolve a name the same way**, which they did not
3184
+ before. Both read the compiled definition, so an input renamed with an alias
3185
+ (`@Input('chart-series') series` or `series = input(…, { alias: 'chartSeries' })`) is set by its
3186
+ **class-field** name as well as its public one; both accept an input a `hostDirectives` entry
3187
+ exposes, which `setInputs` used to refuse with "It declares no inputs at all"; and both refuse a name
3188
+ the component does not declare, at the call, listing the ones it does. Angular's own answer to an
3189
+ undeclared name is an `NG0303` on the console and no change at all, so the spec used to fail several
3190
+ assertions later, on state nothing moved.
3191
+
2818
3192
  Two zoneless traps:
2819
3193
 
2820
3194
  - `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
@@ -2845,6 +3219,22 @@ writable and a write through it lands in `'local'`, `set` / `update` / `asReadon
2845
3219
  `true` while the resource is loading over a value that is defined, which is what Angular has answered
2846
3220
  since v20. Nothing is in flight, so there is nothing to await.
2847
3221
 
3222
+ **The double is no more forgiving than the real thing, in two places that used to hide a defect.**
3223
+ `value()` read after `fail(reason)` **throws** — a `ResourceValueError` carrying the reason on
3224
+ `cause` and naming the property — exactly as a real `ResourceRef` does, so a spec that read the value
3225
+ after arranging the error branch fails now instead of reading the last good value. Branch on
3226
+ `hasValue()` / `status()` first, or assert with `toHaveResourceError()`. And `reload()` answers
3227
+ `false` while the resource is `'idle'` or `'loading'`, as Angular's does, rather than a constant
3228
+ `true` — `expect(products.reload).toHaveBeenCalled()` is still the assertion to write; the return
3229
+ value is only for code that branches on it.
3230
+
3231
+ **The resource, focus and directive matchers throw on an argument of the wrong type.** They used to
3232
+ report `{ pass: false }`, which `.not` turns into a pass — so
3233
+ `expect(products.value()).not.toBeLoading()` (the value, not the resource),
3234
+ `expect(missingEl).not.toHaveFocus()` (a query that found nothing) and
3235
+ `expect(undefined).not.toHaveDirectiveApplied(X)` were three green assertions about nothing. Each
3236
+ now throws, naming what it received.
3237
+
2848
3238
  Per-file timing, to find which specs actually pay for `TestBed`:
2849
3239
 
2850
3240
  ```ts
@@ -2871,10 +3261,14 @@ wants **Angular >= 20** and a declared `@angular/platform-browser`; on 18 or 19
2871
3261
  loading, because the zoneless provider was still called
2872
3262
  `provideExperimentalZonelessChangeDetection` there.
2873
3263
 
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.
3264
+ It exports the core, `provideAutoSpy` / `injectSpy`, `renderShallow`, `createWithAutoSpies`,
3265
+ `setInputs`, `runEffect`, `settleResource`, `trackEffectRuns` / `trackRecomputations` and
3266
+ `stable` / `flushEffects` — nothing else from this section. The matcher registrars
3267
+ (`registerSignalMatchers`, `registerDirectiveMatchers`, `registerResourceMatchers`) need the runner's
3268
+ `expect.extend` and the TestBed diagnostics its suite-level hooks; the overrides, `extendWithAutoSpies`,
3269
+ `provideAutoSpyForToken`, `trackInjections`, `setupAngularTestEnv`, the stub factories and the
3270
+ `mock*Prop`, platform and dialog doubles are simply not routed to Bun. Import them from `/angular`
3271
+ under Vitest.
2878
3272
 
2879
3273
  ---
2880
3274
 
@@ -2895,7 +3289,9 @@ setupAngularTestEnv({
2895
3289
  ```
2896
3290
 
2897
3291
  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.
3292
+ which platform and which providers is not this library's decision. The mode is remembered **per
3293
+ worker**, so under `isolate: false` a run of files all in one mode initialises once: before, every
3294
+ spec file reset the environment and re-ran your initialiser.
2899
3295
 
2900
3296
  ---
2901
3297
 
@@ -2973,10 +3369,12 @@ chart.pointSelected.emit(2); // outputs are EventEmitters; a model() stays a mod
2973
3369
  Copied from `ɵcmp` / `ɵdir` / `ɵpipe`: the selector, every input under its public name (transforms
2974
3370
  included), every output, `exportAs`, a pipe's name and purity. Not copied: the template (the stub
2975
3371
  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.
3372
+ replaces it), host bindings, providers, lifecycle hooks, queries, and **`hostDirectives`** — so an
3373
+ input the real component exposes through one is not on the stub, even though `setInputs` accepts that
3374
+ name on the real thing. The second argument seeds members per instance — a method the parent calls
3375
+ through `viewChild`, a pipe's `transform` (identity by default). With `renderShallow`:
3376
+ `renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })` — the real child is
3377
+ dropped as a component import and the stub is the one child left.
2980
3378
 
2981
3379
  ### Patching a property of a spy
2982
3380
 
@@ -3107,6 +3505,11 @@ its constructor, and `tick()` in the `fakeAsync` test has to see those timers. U
3107
3505
  `installProxyZonePatch({ scope: 'callback' })` for `test.concurrent`, where two callbacks are in
3108
3506
  flight at once and would otherwise swap the same `ProxyZoneSpec` delegate under one another.
3109
3507
 
3508
+ Calling `installProxyZonePatch()` yourself is idempotent now: a second call is a no-op instead of
3509
+ wrapping the previous wrapper, and so is the undo it returns. Under `isolate: false` an explicit
3510
+ call in a setup file used to add a `Proxy` layer per spec file — two hundred layers at two hundred
3511
+ files, every `fakeAsync` callback paying for all of them.
3512
+
3110
3513
  **Invariant of this package, not a detail of one release:** `zone.js` is a **devDependency and only a
3111
3514
  devDependency** — never a dependency, never a peer, not even an optional one. Everything about zones
3112
3515
  lives behind this one subpath; no other entry reaches it, even transitively, and the module imports
@@ -3162,6 +3565,11 @@ their calls. **Do not rely on the import to install them**: it does so once per
3162
3565
  (`no-import-time-console-spies` reports it). Under `setupAutoSpy({ strayConsole })` the import installs
3163
3566
  nothing at all.
3164
3567
 
3568
+ The spies survive `vi.resetModules()`. A fresh copy of the module used to record the previous copy's
3569
+ spy as "the real `console.warn`", after which `restoreConsole()` installed that dead spy for the rest
3570
+ of the worker and every log from then on went nowhere. The real methods are kept on one shared table
3571
+ and a spy of another copy is refused as an original.
3572
+
3165
3573
  Import your runtime entry (`…/bun`, `…/node`) **before** `…/console`, or it registers the Vitest
3166
3574
  adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives a detached one.
3167
3575
 
@@ -3195,49 +3603,50 @@ export default [
3195
3603
  errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
3196
3604
  blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
3197
3605
 
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 |
3238
-
3239
- Thirty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3240
- `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
3606
+ | Rule | Level | Fix | Flags |
3607
+ | --------------------------------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3608
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
3609
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3610
+ | `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 |
3611
+ | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3612
+ | `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 }` |
3613
+ | `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 }` |
3614
+ | `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 }` |
3615
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
3616
+ | `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 |
3617
+ | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3618
+ | `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 |
3619
+ | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3620
+ | `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 |
3621
+ | `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 |
3622
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3623
+ | `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 |
3624
+ | `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 |
3625
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
3626
+ | `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 |
3627
+ | `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 |
3628
+ | `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` |
3629
+ | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3630
+ | `no-hand-assigned-global` | `error` | — | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages; the three observers stay with `prefer-observer-stub` |
3631
+ | `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 |
3632
+ | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3633
+ | `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` |
3634
+ | `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` |
3635
+ | `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 |
3636
+ | `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` |
3637
+ | `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 |
3638
+ | `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>` |
3639
+ | `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 |
3640
+ | `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 |
3641
+ | `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` |
3642
+ | `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 |
3643
+ | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
3644
+ | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
3645
+ | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3646
+ | `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 |
3647
+
3648
+ Thirty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3649
+ `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3241
3650
  `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3242
3651
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
3243
3652
  builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
@@ -3399,6 +3808,14 @@ directive. The report sits on the directive's line so that comment reaches it. `
3399
3808
  `failWith` and `throwWith` are not read: their parameter is `unknown`, so a directive there
3400
3809
  suppresses something other than the stub.
3401
3810
 
3811
+ **What the plugin costs is bounded by the file, not by what is in it.** Three rules used to re-read
3812
+ the file per finding-site rather than once: over this repository's 173 spec files the whole plugin
3813
+ takes **56 ms** where it took 93, and on a single 1.7 MB spec **68 ms** where it took 3 263. The
3814
+ rules answer the same way they did — the ordering rules (`no-inject-before-override`,
3815
+ `no-overridden-provider`) collect the `override*` and `resetTestingModule` positions in one pass and
3816
+ decide by range, `prefer-render-shallow` asks the template-read question once per file, and
3817
+ `no-redundant-smoke-test` indexes identifiers only where a smoke test exists to judge.
3818
+
3402
3819
  The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
3403
3820
  packages, which a subpath export can never be.
3404
3821
 
@@ -3410,9 +3827,14 @@ packages, which a subpath export can never be.
3410
3827
  | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3411
3828
  | `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
3829
  | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
3830
+ | `… 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) |
3831
+ | ``… 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) |
3832
+ | `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) |
3833
+ | `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
3834
  | `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
3835
  | `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
3836
  | `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) |
3837
+ | `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
3838
  | `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
3839
  | `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
3840
  | 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 +3844,7 @@ packages, which a subpath export can never be.
3422
3844
  | `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
3845
  | `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
3846
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
3847
+ | `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
3848
  | `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
3849
  | `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
3850
  | `[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 +3857,9 @@ packages, which a subpath export can never be.
3434
3857
  | `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
3858
  | `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
3859
  | `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) |
3860
+ | `.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) |
3861
+ | `@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) |
3862
+ | `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
3863
  | `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
3864
  | `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
3865
  | `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 +3891,7 @@ packages, which a subpath export can never be.
3465
3891
  | `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
3892
  | `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
3893
  | `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()` |
3894
+ | `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
3895
  | `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
3896
  | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
3471
3897
  | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
@@ -3480,6 +3906,11 @@ packages, which a subpath export can never be.
3480
3906
  | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
3481
3907
  | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
3482
3908
  | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
3909
+ | `adoptMock() needs a mock that can report its implementation` | a `node:test` `mock.fn()` handed to `adoptMock` | build the double with `createFunctionSpy()` and pass that to the module mock |
3910
+ | `Cannot spy on export "x". Module namespace is not configurable in ESM.` | `vi.spyOn(namespace, 'x')` under Vitest browser mode or a native ESM loader — the namespace is sealed | spy the class / prototype, or `vi.mock(path, { spy: true })` / `moduleNamespace(await importOriginal(), { passthrough: true })` |
3911
+ | `[Function] is not a spy or a call to a spy!` in a Storybook `play` | `expect` from `storybook/test` wraps a function with no own enumerable keys, and an auto-spy keeps its `mock*` methods on a shared prototype | import `expect` from `vitest` in that story file, or `setSpyEngine('runner')` from `/setup` |
3912
+ | `object is not iterable` destructuring a mocked hook tuple | an auto-mock stands for an object, and a Proxy double is not iterable | write the tuple — `mockReturnValue([data, false, null])`; auto-mocks are for the object a hook returns |
3913
+ | `The property "x" is not defined on the object` from `vi.spyOn(Class.prototype, 'x')` | `x` is an instance field (arrow function, signal), not a prototype method | `createSpyFromClass(C, { instanceMethodsToSpyOn: ['x'] })` |
3483
3914
  | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
3484
3915
  | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
3485
3916
  | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
@@ -3488,7 +3919,7 @@ packages, which a subpath export can never be.
3488
3919
  | `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
3920
  | `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
3921
  | 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()` |
3922
+ | `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
3923
  | 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
3924
  | 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
3925
  | `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 +3940,7 @@ packages, which a subpath export can never be.
3509
3940
  | `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
3941
  | 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
3942
  | `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 |
3943
+ | 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
3944
  | 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
3945
  | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
3515
3946
  | 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 +4044,28 @@ diagnostic and is free to change: print it, never assert on it.
3613
4044
  | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
3614
4045
  | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
3615
4046
  | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
4047
+ | `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
4048
+
4049
+ **A hand-rolled double trips `rxjs-x/finnish`, and a double from this package does not.** The rule
4050
+ reports any name whose type is an `Observable` **or whose call signature returns one**
4051
+ (`couldReturnObservable`). `vi.fn()` and `vi.spyOn()` hand back a callable mock carrying the
4052
+ signature of the method it stands for, so a double for a method returning an `Observable` reads to
4053
+ the rule as a stream and it asks for a `$` — on a spy, where the `$` would lie about what the
4054
+ variable holds. `functions: false` gives no relief: that option is about function _declarations_,
4055
+ not a variable holding one. A `Spy<T>` from `createSpyFromClass`, `provideAutoSpy` or
4056
+ `createAutoMock` is an ordinary object instead — the Observable sits on its members, not on the
4057
+ variable — so nothing is reported, and neither the name nor the assertion has to be bent. Measured
4058
+ on an Angular workspace of 1771 spec files: the rule named seven sites, every one a hand-rolled
4059
+ `vi.fn`/`vi.spyOn`, and none on a double built here.
4060
+
4061
+ The overrides bag is the one shape worth knowing in detail, because the repair is not a rename.
4062
+ Written inline, `provideAutoSpy(X, { overrides: { m: vi.fn(() => of(x)) } })` is invisible to the
4063
+ rule — an object literal inside a call is skipped. Hoisted into a `const` it is reported **per key**,
4064
+ and the key cannot be renamed: it is the method's name on `X`. What clears it is the annotation the
4065
+ bag should carry anyway, `const overrides: DeepPartial<X> = { … }` — the rule skips a literal whose
4066
+ variable is annotated, and the bag starts being checked against `X` where it is written rather than
4067
+ only at the call. For a spy whose name is genuinely free, `*Spy` plus a `names` exemption in the
4068
+ rule's own options says the same thing once for the whole repository.
3616
4069
 
3617
4070
  **The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
3618
4071
  when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
@@ -3624,6 +4077,21 @@ meant to show, with the test still green. Carry `vi.fn(() => x)` over as
3624
4077
  `mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
3625
4078
  anyone writing a codemod, because the rename looks like the safest edit in the file.
3626
4079
 
4080
+ ### Advice that circulates and is wrong on Vitest
4081
+
4082
+ Jest-era tutorials and cheat sheets repeat a handful of lines that fail on Vitest, or pass and leak.
4083
+ Each was checked on Vitest 5.0.0:
4084
+
4085
+ | Circulating advice | What happens | Write instead |
4086
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
4087
+ | `const load = vi.fn(); vi.mock('./api', () => ({ load }))` | `vi.mock` is hoisted above every import and `const`: `ReferenceError: Cannot access 'load' before initialization`, wrapped in `[vitest] There was an error when mocking a module` | `const mocks = vi.hoisted(() => ({ load: vi.fn() }))`, then `() => ({ load: mocks.load })` |
4088
+ | "name it `mockLoad` and the factory may read it" | Jest's exemption for `mock`-prefixed names does not exist in Vitest — same `ReferenceError` | `vi.hoisted` |
4089
+ | `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
4090
+ | `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
4091
+ | `import { userEvent } from '@testing-library/user-event'` | the named export exists only from 14.5.0; the default export works on every 14.x, and every 14.x method returns a promise | `import userEvent from '@testing-library/user-event'`; `const user = userEvent.setup(); await user.click(el)` |
4092
+ | `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
4093
+ | `global.fetch = vi.fn()` in a test | survives `vi.restoreAllMocks()` and `vi.unstubAllGlobals()`, and answers every later test of the file | `mockValueProp(globalThis, 'fetch', …)` or `vi.stubGlobal` + `unstubGlobals: true`; `blockNetwork()` to stay offline; the `no-hand-assigned-global` rule reports the bare form |
4094
+
3627
4095
  ---
3628
4096
 
3629
4097
  ## 19. Before you report success
@@ -3670,6 +4138,35 @@ with `as` still counts), the call must both begin and end a statement, and the p
3670
4138
  the installed major differs from the table's. Neither has a fixer; `doctor` still never writes.
3671
4139
  Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
3672
4140
 
4141
+ **Three things about the CLI that decide whether its answer means anything:**
4142
+
4143
+ - **An unknown flag is refused, exit 2, nothing runs.** `init --dryrun` used to write the files and
4144
+ `perf --gat` used to pass with no gate at all — a typo in CI that read as a clean result. The two
4145
+ stderr lines name the flag and list what the command accepts; `--cwd`, `--help` and `--version`
4146
+ work everywhere.
4147
+ - **The scan does not descend into a nested repository or a git worktree.** A tree carrying
4148
+ worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
4149
+ `codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
4150
+ it is a directory (a nested clone) or a file (a worktree). Past 50 000 files the scan still
4151
+ truncates, and `doctor` reports that as a `scan-cap-reached` warning (exit 1) rather than a clean
4152
+ result; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4153
+ - **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
4154
+ read is not a clean result. Absolute paths and `./`-style ones resolve against `--cwd`.
4155
+
4156
+ Exit codes, as the commands implement them: **0** is "ran, nothing to report"; **1** is "ran, and
4157
+ here is the finding" — `doctor` with an error, `init --check` with a stale block, `codemod` with any
4158
+ note, `perf --gate` over budget; **2** is "there was nothing to judge" — no command, an unknown
4159
+ command, an unknown flag, an unreadable `--only` / `--from` value, a path matching no file, and a
4160
+ `perf` run that measured nothing (including a red suite, which the gate will not judge at all).
4161
+
4162
+ **Reading the output from a script: `--format json`**, on `doctor` and on `perf`. One JSON document
4163
+ on stdout — `schema`, `exitCode`, `tally` (`errors`, `warnings`, `notes`), every finding with
4164
+ `check`, `severity`, `file`, `message`, `fix`; `perf` adds `run`, `budgets` and `gate.verdicts` (one row
4165
+ per candidate, `outcome` one of `confirmed`, `not reproduced`, `unconfirmed`, `single reading`,
4166
+ `over budget`). Parse that rather than the text: the text is wrapped to the terminal (80 columns in a
4167
+ pipe), groups one cause found in many files into one block, and ends in a tally line that starts
4168
+ with `N errors, N warnings, N notes`.
4169
+
3673
4170
  ### If you were asked why a suite is slow
3674
4171
 
3675
4172
  ```bash
@@ -3769,6 +4266,20 @@ migrated by hand. Run it after `--write`, and again after any manual clean-up. `
3769
4266
  select transforms by id, `--list` prints them. Full reference:
3770
4267
  <https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
3771
4268
 
4269
+ **Every rewrite is parsed before it is written.** The result goes through the project's own
4270
+ `typescript` and its diagnostics are compared with the original's; a file the run would have broken
4271
+ is reported as `codemod-broke-syntax` and **left exactly as it was**, so the run exits 1 with one
4272
+ file named rather than a tree that no longer compiles. Where `typescript` is not installed the check
4273
+ is skipped silently — it is a safety net, not an install instruction.
4274
+
4275
+ It visits JavaScript specs too — `*.spec.js`, `*.test.jsx`, the `.cjs` / `.mjs` forms — because a
4276
+ Jest suite that was never TypeScript is the suite with the most `jest.` in it. Two things it now
4277
+ reports instead of rewriting into something wrong: `jest.fn<R, [A]>()` / `jest.spyOn<…>()` in a file
4278
+ that imports from `@jest/globals` (`jest-mock-type-arguments` — `jest-mock` 29 already takes the
4279
+ whole function type, so transposing a second time produces a return type of a return type), and
4280
+ `.withArgs(…)` on a `vi.spyOn` chain (`jasmine-with-args-on-spy-on` — `vi.spyOn` has no
4281
+ `calledWith`, and renaming it onto one produces a method that does not exist).
4282
+
3772
4283
  Past 50 000 files the repository scan truncates and the run says so — _Nothing left to migrate_ off a
3773
4284
  truncated list is a claim about a tree the tool never looked at. `VITEST_AUTO_SPY_SCAN_CAP` raises
3774
4285
  the cap.
@@ -3826,6 +4337,27 @@ npx vitest-auto-spy codemod --from jasmine --verify # match the result, not th
3826
4337
  matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
3827
4338
  on `globalThis` — it is one explicit line per file, which the codemod later deletes.
3828
4339
 
4340
+ Four places where that surface now matches jasmine's rather than approximating it:
4341
+
4342
+ - **`spy.withArgs(…).and` carries the strategies that describe one argument list** — `stub()`,
4343
+ `throwError(…)`, `resolveTo(…)` and `returnValue(…)`, plus this library's own `resolveWith` /
4344
+ `nextWith` / `returnSubject` family. The three that install an _implementation_ —
4345
+ `callFake`, `callThrough`, `returnValues` — are present and **throw** a message naming the
4346
+ alternative, rather than arriving as `… is not a function`: an implementation answers every call,
4347
+ which is the opposite of configuring one argument list. Take the whole spy with `spy.and.callFake(…)`
4348
+ if that is what the line meant.
4349
+ - **`jasmine.mapContaining` compares keys with the runner's equality**, as jasmine does, so an
4350
+ asymmetric matcher as a key and an object key compared deeply both work. `Map.has` alone answered
4351
+ on reference identity and missed both.
4352
+ - **`jasmine.clock().install()` leaves `Date` real**, exactly as jasmine's does — the timers are
4353
+ faked, the clock is not. `jasmine.clock().mockDate()` is what takes `Date` over, and because that
4354
+ re-installs the fake clock it reports when callbacks were already scheduled and have just been
4355
+ dropped. Call `mockDate()` right after `install()`, before anything schedules a timer.
4356
+ - **`jasmine.createSpyObj`'s third argument builds spied accessors**, reachable through
4357
+ `Object.getOwnPropertyDescriptor(obj, name).get`. Reading still answers the seed, so nothing about
4358
+ a migrated spec changes; what is gained is moving the value mid-test and asserting that the code
4359
+ under test wrote it.
4360
+
3829
4361
  ### The renames, once the suite is green
3830
4362
 
3831
4363
  | jasmine | here |
@@ -3875,8 +4407,10 @@ on `globalThis` — it is one explicit line per file, which the codemod later de
3875
4407
  once naming both rather than silently swallowing the write.
3876
4408
 
3877
4409
  `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(…)`.
4410
+ shape, not a name. Use `async` + `await`; `no-done-callback` reports the parameter and any
4411
+ `done.fail(…)`. It reports the parameter that is _called_, handed on as an argument, or never used —
4412
+ not every named first parameter: `it('x', (ctx) => ctx.skip())` is Vitest's own `TestContext` read
4413
+ without destructuring, which is legal, and used to be reported as a `done` callback.
3880
4414
 
3881
4415
  On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
3882
4416
  importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a