vitest-auto-spy 5.19.0 → 5.21.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 (68) hide show
  1. package/AGENTS.md +167 -34
  2. package/README.md +111 -57
  3. package/dist/angular-diagnostics.d.ts +168 -0
  4. package/dist/angular-diagnostics.js +326 -0
  5. package/dist/angular-doubles.d.ts +254 -0
  6. package/dist/angular-doubles.js +203 -0
  7. package/dist/angular-http.js +3 -27
  8. package/dist/angular-matchers.d.ts +96 -0
  9. package/dist/angular-matchers.js +145 -0
  10. package/dist/angular-router.d.ts +1 -1
  11. package/dist/angular-router.js +1 -1
  12. package/dist/angular.d.ts +10 -519
  13. package/dist/angular.js +169 -742
  14. package/dist/bun-angular.d.ts +6 -6
  15. package/dist/bun-angular.js +16 -52
  16. package/dist/bun.d.ts +88 -12
  17. package/dist/bun.js +7 -5
  18. package/dist/{chunk-3LU77DBV.js → chunk-2KBFEQOV.js} +6 -4
  19. package/dist/chunk-3EV45V6W.js +52 -0
  20. package/dist/{chunk-HMECO5PK.js → chunk-4SJY7ZHM.js} +22 -2
  21. package/dist/{chunk-LBQWJZPS.js → chunk-BN2QV45R.js} +2 -2
  22. package/dist/chunk-GWEQZHMN.js +189 -0
  23. package/dist/{chunk-2ERHG4NE.js → chunk-I6JZIWIZ.js} +129 -72
  24. package/dist/{chunk-TCGO3VCY.js → chunk-ITOFGQTX.js} +2 -30
  25. package/dist/chunk-KOR5OK4H.js +134 -0
  26. package/dist/chunk-NS3Y6AQB.js +44 -0
  27. package/dist/{chunk-CEDA5PUY.js → chunk-QT5JIDOK.js} +1 -1
  28. package/dist/chunk-RPCKHTDN.js +41 -0
  29. package/dist/chunk-SA2QIER3.js +31 -0
  30. package/dist/{chunk-4JTRYOL2.js → chunk-TQAYWU5J.js} +1 -1
  31. package/dist/{chunk-F3OQDSKX.js → chunk-ZZSFAQT4.js} +265 -170
  32. package/dist/cli.js +1140 -873
  33. package/dist/console.d.ts +1 -1
  34. package/dist/console.js +5 -4
  35. package/dist/dom-stubs.d.ts +1 -1
  36. package/dist/dom-stubs.js +66 -6
  37. package/dist/eslint-plugin.cjs +176 -107
  38. package/dist/{expect-emission-QuF55ViS.d.ts → expect-emission-S5asJaJP.d.ts} +10 -3
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.js +386 -30
  41. package/dist/jasmine-compat.d.ts +1 -1
  42. package/dist/jasmine.d.ts +2 -2
  43. package/dist/jasmine.js +6 -5
  44. package/dist/nestjs.d.ts +2 -2
  45. package/dist/nestjs.js +8 -7
  46. package/dist/node.cjs +381 -26
  47. package/dist/node.d.cts +1 -0
  48. package/dist/node.d.ts +5 -5
  49. package/dist/node.js +381 -27
  50. package/dist/{package-identity-C2L4fM0k.d.ts → package-identity-Cn7yncmJ.d.ts} +1 -1
  51. package/dist/{prop-mock-DMqE-ldi.d.ts → prop-mock-C--a8rpU.d.ts} +3 -1
  52. package/dist/react.d.ts +5 -5
  53. package/dist/react.js +386 -30
  54. package/dist/rstest.d.ts +5 -5
  55. package/dist/rstest.js +7 -5
  56. package/dist/rxjs.d.ts +2 -2
  57. package/dist/setup.d.ts +30 -4
  58. package/dist/setup.js +82 -7
  59. package/dist/svelte.d.ts +5 -5
  60. package/dist/svelte.js +386 -30
  61. package/dist/{track-injections-BfNI9fHy.d.ts → track-injections-Dig5t28T.d.ts} +1 -1
  62. package/dist/{track-signal-runs-C5nd4A4_.d.ts → track-signal-runs-C3V6OIGM.d.ts} +13 -4
  63. package/dist/{types-B7Wjo00D.d.ts → types-BM3BcWj1.d.ts} +25 -1
  64. package/dist/vue.d.ts +6 -6
  65. package/dist/vue.js +386 -30
  66. package/package.json +16 -1
  67. package/skills/vitest-auto-spy/SKILL.md +128 -97
  68. package/dist/chunk-X4BVCE4V.js +0 -78
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.
@@ -52,23 +53,31 @@ adapter installed and spies fail at runtime.
52
53
 
53
54
  Add-ons, orthogonal to the runner:
54
55
 
55
- | Add-on | Import | Needed for |
56
- | ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, and in a file the `tsconfig` includes (§4) |
58
- | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
59
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
60
- | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
61
- | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
62
- | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
63
- | 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
- | 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
- | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
67
- | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
56
+ | Add-on | Import | Needed for |
57
+ | ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, and in a file the `tsconfig` includes (§4) |
59
+ | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
60
+ | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
61
+ | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
62
+ | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
63
+ | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
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 |
65
+ | Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular` like `/angular-http` — no core re-export; **moved off `/angular` in 6.0** so importing spies stops evaluating it |
66
+ | Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio and the `Window`/`Document` platform doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box; **moved off `/angular` in 6.0** |
67
+ | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 6.0** |
68
+ | 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+ |
69
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
70
+ | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
71
+ | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
68
72
 
69
73
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
70
74
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
71
75
 
76
+ `vitest-auto-spy/setup` is Vitest-only as well — `setupAutoSpy()` registers Vitest hooks. On
77
+ `bun test` nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy between tests;
78
+ put `afterEach(() => { restoreMockedProps(); mock.restore(); })` in a `--preload` file, and any
79
+ `mock.module()` there too (inside a test it swaps bindings after the original module already ran).
80
+
72
81
  The rxjs peer is **>= 7.2**, not `>=7`. The observable layer used to pull `concatMap`, `delay`,
73
82
  `switchMap`, `take`, `takeUntil` and `takeWhile` from `rxjs/operators`; rxjs 8 removes that deep path,
74
83
  so the specifier moved to the root `rxjs` entry — which is where rxjs re-exported them in 7.2 — and
@@ -139,7 +148,11 @@ noise scales with the number of them. Both take the same second argument (`retur
139
148
  `observablePropsToSpyOn`).
140
149
 
141
150
  `createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
142
- `no-type-assertion` lint rule without an `eslint-disable` on every fixture.
151
+ `no-type-assertion` lint rule without an `eslint-disable` on every fixture. `createMock<T>(undefined)`
152
+ is the same call as `createMock<T>()` and answers `{}`, **not** `undefined` — a forwarding helper
153
+ with an optional `overrides` parameter relies on that. A fixture that means "no value" passes
154
+ `undefined` itself: `getters.profile.mockReturnValue(undefined)`, never
155
+ `mockReturnValue(createMock<Profile>(undefined))`.
143
156
 
144
157
  **`createSpyFromInstance(instance, config?)` is the one case where nothing is constructed.** Every
145
158
  factory above builds the double, which is no help once the object exists and other code already
@@ -172,6 +185,17 @@ member declared `configurable: false`, come back as this package's diagnostic ra
172
185
  `sinon.createStubInstance` builds a new object from a constructor instead of patching the one you
173
186
  hold, and `bun:test` and `node:test` have nothing.
174
187
 
188
+ **`passthrough: true` keeps the patched object working.** Unconfigured methods run the real
189
+ implementation (with the instance as `this`) and are still recorded; any configuration takes the
190
+ whole method over and `resetAutoSpy` hands it back. The Angular shape is
191
+ `createSpyFromInstance(TestBed.inject(CartService), { passthrough: true })` — dependencies, `signal()`
192
+ fields, `ɵprov` and the real `ngOnDestroy` keep working. Lifecycle hooks, discovered callables with an
193
+ API of their own (signals) and discovered classes are left real rather than spied. Do **not** combine
194
+ it with `strict: true` or `onUnstubbedCall` on the same call — that throws; a suite-wide strict yields
195
+ to it. A `calledWith(1)` miss answers `undefined`, not the real method: configuring a method hands the
196
+ whole method over. Use it to assert an interaction on a real collaborator; use a plain double when
197
+ the test must not touch the real one.
198
+
175
199
  **`createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` are for the model
176
200
  that more than one spec builds.** The difference from `createMock` is the `defaults` argument: it is
177
201
  a **complete** `T`, checked in full, in one place — so a field the model dropped fails there instead
@@ -210,6 +234,16 @@ boot(asInstance(logger)); // → AppLogger, for the API under test
210
234
  asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // → the helpers
211
235
  ```
212
236
 
237
+ **Arrays and unmocked calls on `mockDeep`.** A member read by a numeric index (`api.items[0]`) is a
238
+ real `Array` of deep mocks from then on, and `map`, iteration and `toEqual` work on it. Read the
239
+ member again after the first index: a handle taken before the first index is still a node. To make a
240
+ call nobody configured fail, use `mockDeep<T>({}, { fallbackMockImplementation: () => { throw … } })`.
241
+ Options go in the **second** argument, not the first as in vitest-mock-extended. The precedence is
242
+ configuration > fallback > `selfReturning`. A `calledWith` miss answers `undefined`, not the
243
+ fallback; use `mustBeCalledWith` for "other arguments fail". For a callback API (`$transaction`), use
244
+ `method.mockImplementation((run) => run(asInstance(mock)))`; there is no option for it.
245
+ `vi.spyOn(mock.repo, 'find')` works on a member nobody has read and returns the node's own spy.
246
+
213
247
  `asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
214
248
  sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
215
249
 
@@ -394,6 +428,15 @@ from inside the placeholder's getter; the spy is kept beside the double instead,
394
428
  a stable mock and `mockReturnValue` on it works. On a merely sealed double an assignment still
395
429
  reaches the member.
396
430
 
431
+ `vi.spyOn(double, 'load')` on a method nobody has read yet works again. Vitest reads an accessor by
432
+ calling its getter with no receiver, and 5.19.0's shared getter answered that with
433
+ `TypeError: Invalid value used as weak map key`. The call now wraps a forwarder: a configured
434
+ `mockReturnValue` answers, an unconfigured call reaches the double's own spy (strict guard included),
435
+ and `mockRestore()` / `vi.restoreAllMocks()` hand back that same spy with the calls it recorded. It
436
+ is still redundant — the member already is a spy, so `double.load.mockReturnValue(…)` is the line to
437
+ write — and a wrapped method called off its double (`const { load } = double; load()`) throws a
438
+ message saying so.
439
+
397
440
  **Symbol-keyed methods are discovered and spied.** A method under a symbol the project owns —
398
441
  `[SERIALIZE]()`, `Symbol.for('app.render')` — is a method like any other, resets with the rest and
399
442
  shows up in `Reflect.ownKeys`. The runtime's own protocol symbols are deliberately left alone
@@ -816,7 +859,14 @@ walking the prototype chain would let one registration change doubles in files n
816
859
  A second registration for the same class replaces the first, because two of them in one suite is the
817
860
  drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
818
861
  `clearAutoSpyDefaults()` the lot. `createSpyFromInstance(obj)` reads the registration of the class
819
- `obj.constructor` names, merged the same way; an object literal resolves none.
862
+ `obj.constructor` names, merged the same way; an object literal resolves none. One exception, because
863
+ an instance is real: when the call site lists `onlyMethodsToSpyOn`, the rest of the object stays real,
864
+ so the registration contributes only `strict`, `onUnstubbedCall`, `onUnstubbedRead` and the
865
+ `returns` / `selfReturning` entries of the listed methods — not its accessor lists, its other method
866
+ lists or its `overrides`. `registerAutoSpyDefaults(Router, { gettersToSpyOn: ['url'] })` therefore
867
+ leaves `router.url` live under `createSpyFromInstance(router, { onlyMethodsToSpyOn: ['navigateByUrl'] })`.
868
+ A `returns` or `selfReturning` name the call site itself wrote for a method it left real is reported
869
+ as a misconfiguration and skipped.
820
870
 
821
871
  A setup file that registers more than a handful of classes can say them as one table instead of one
822
872
  call each. Rows apply in order, and each is checked against **its own** class — a key `Router` does
@@ -1012,6 +1062,15 @@ const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); //
1012
1062
  const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
1013
1063
  ```
1014
1064
 
1065
+ `injectSpy(X)` without the argument keeps a declared default when the constructor does not take the
1066
+ type parameter. When it does — `constructor(public data: T, …)`, the shape of most modal refs — 5.19.0
1067
+ inferred `X<never>`, and with the typed `accessorSpies` bag `Spy<X<never>>` no longer assigns to
1068
+ `Spy<X<unknown>>`. Such a class is now read at its **constraint**: `ModalRef<T = unknown>` gives
1069
+ `Spy<ModalRef<unknown>>`, `ConfigService<T extends Config = Defaults>` gives
1070
+ `Spy<ConfigService<Config>>`. A constructor cannot hand TypeScript the default here, so spell the
1071
+ argument out whenever the default or a particular instantiation is what the spec means:
1072
+ `injectSpy<ConfigService>(ConfigService)`, `injectSpy<ModalRef<PurchaseOptions>>(ModalRef)`.
1073
+
1015
1074
  It applies to `createSpyFromClass` with a configuration too, in one combination: an accessor list
1016
1075
  (or `overrides`) **and** `returns` on a generic class. TypeScript checks a generic class argument
1017
1076
  after the configuration, reads `T` back from `gettersToSpyOn: ['remoteConfig']` as
@@ -1530,6 +1589,13 @@ does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMock
1530
1589
  (new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
1531
1590
  changes, and the peer range still starts at 2.1 — one install spans Vitest 2.1 through 5.x.
1532
1591
 
1592
+ What each config flag does to an auto-spy, measured on both spy engines: `clearMocks` empties the
1593
+ calls and keeps the configuration; `mockReset` empties the calls and drops `mockReturnValue` /
1594
+ `mockImplementation` but **keeps `calledWith` rules**; `restoreMocks` never touches an auto-spy (it
1595
+ only undoes `vi.spyOn`). Do not claim the flags skip auto-spies — a sweep sentinel in the Vitest
1596
+ adapter routes the first two. `resetAutoSpy(spy)` is the call that drops everything, `calledWith`
1597
+ included. `mockReset()` on a `vi.spyOn(…)` runs the **real** method again (Vitest ≥ 3).
1598
+
1533
1599
  What Vitest 5 does break is the runner's own doing, not this library's: with `clearMocks` on by
1534
1600
  default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
1535
1601
  zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
@@ -1589,6 +1655,16 @@ nothing listening on that port.`WebSocket`and`EventSource`are left alone: their
1589
1655
  event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
1590
1656
  a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
1591
1657
 
1658
+ - `stubResponse(init?)` (`/setup`) builds a real `Response` — never write `{ ok, json } as Response`.
1659
+ Plain data in `body` goes out as JSON with `application/json`; an `ok` that disagrees with
1660
+ `status` throws. A body can be read once: for a stub answering several calls use
1661
+ `mockImplementation(async () => stubResponse(…))`, not `mockResolvedValue`.
1662
+ - `blockNetwork` leaves `fetch` alone while `Symbol.for('fetch-interceptor')` is on `globalThis`
1663
+ (MSW `setupServer`, nock 14), so MSW handlers keep answering; XHR stays blocked for what MSW does
1664
+ not handle. MSW's `onUnhandledRequest: 'error'` exempts asset-looking URLs (`.svg`, `.json`,
1665
+ fonts…); a last `http.all('*', () => HttpResponse.error())` is the hard floor. MSW's browser
1666
+ `setupWorker` has not been checked.
1667
+
1592
1668
  `restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
1593
1669
  fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
1594
1670
  the realm, not owned by `globalThis`), and with `isolate: false` the next file dies inside Vitest's
@@ -1918,7 +1994,7 @@ an unconfigured call returns — a semantic switch, not a grade; the name was ta
1918
1994
  stray-timer counts (the sweep fails the file from `afterAll`, and a callback scheduled after the
1919
1995
  previous file's sweep is charged to the next — opt in with
1920
1996
  `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`, whose diff names each one's file), and
1921
- `enableAngularDiagnostics()`, which lives in `/angular` — call it in the same setup file as the Angular half of strict.
1997
+ `enableAngularDiagnostics()`, which lives in `/angular/diagnostics` — call it in the same setup file as the Angular half of strict.
1922
1998
 
1923
1999
  `misconfiguration: 'throw'` on its own makes the library's misuse reports — an `onlyMethodsToSpyOn`
1924
2000
  typo, `gettersToSpyOn` naming a method, a `returns` key no spy answers to, `injectSpy` handed a real
@@ -2168,6 +2244,24 @@ gives the probing dependency `dayjsStub`, where it used to be replaced by the na
2168
2244
  the default export silently became the wrong object. Only a factory without one gets
2169
2245
  `default: <the namespace>`, which is the interop shape it was there for; the return type follows.
2170
2246
 
2247
+ A factory's `vi.fn()` comes back typed as the real function, without `calledWith` or `resolveWith`.
2248
+ `adoptMock` takes it over in place — same object, history kept, typed from the export's signature —
2249
+ and an unconfigured call keeps answering what the mock answered before:
2250
+
2251
+ ```ts
2252
+ import { loadUser } from './api';
2253
+
2254
+ vi.mock('./api', () => ({ loadUser: vi.fn() }));
2255
+ adoptMock(loadUser).calledWith(7).resolveWith({ id: 7, name: 'Ada' });
2256
+ ```
2257
+
2258
+ To keep the real module and configure one case, spy it through:
2259
+ `vi.mock('./api', async (importOriginal) => moduleNamespace(await importOriginal(), { passthrough: true }))`.
2260
+ Every function export runs for real and is recorded until configured; classes and values stay real;
2261
+ calls one export makes to another inside the module are not recorded. A `vi.spyOn` / `{ spy: true }`
2262
+ mock calls an original it does not report, so adopting it makes an unconfigured call answer
2263
+ `undefined`; `node:test`'s `mock.fn()` is refused.
2264
+
2171
2265
  There is no `mockModule(…)` helper here, and there cannot be: Vitest hoists the literal `vi.mock`
2172
2266
  call, so a wrapper around it would be hoisted as a call to a function that does not exist yet. Share
2173
2267
  a fixture between the factory and the tests with `vi.hoisted()`.
@@ -2191,6 +2285,13 @@ const myService = injectSpy(MyService); // Spy<MyService>
2191
2285
  zone.js alike. The entry needs **Angular >= 20** (§1); on 16 or 17 it does not link at all, because
2192
2286
  `ɵSIGNAL` is not there to import.
2193
2287
 
2288
+ **Niche Angular helpers ship in narrow companion subpaths, never in `/angular`** — the pattern of
2289
+ `/angular-http`, `/angular-router` and `/signal-forms`, and since 6.0 of `/angular/diagnostics`,
2290
+ `/angular/doubles` and `/angular/matchers`. An import of `/angular` evaluates its whole graph, so a
2291
+ helper a suite names once in a setup file must not ride along with every `provideAutoSpy` import;
2292
+ the diagnostics, doubles and matcher registrars left on that rule (`trackInjections` stays —
2293
+ `createWithAutoSpies` needs its module).
2294
+
2194
2295
  ### The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
2195
2296
 
2196
2297
  The block above, written once instead of once per dependency, with the types inferred rather than
@@ -2534,7 +2635,7 @@ replace a stack inside `@angular/core` with a line naming what is missing.
2534
2635
  ```ts
2535
2636
  // vitest.setup.ts — AFTER getTestBed().initTestEnvironment(…), because Vitest runs
2536
2637
  // afterEach hooks in reverse registration order and this one must run before the teardown.
2537
- import { enableAngularDiagnostics } from 'vitest-auto-spy/angular';
2638
+ import { enableAngularDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
2538
2639
 
2539
2640
  enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders, pendingRequests }
2540
2641
  ```
@@ -2795,7 +2896,7 @@ that owns them (assert through `component.form.tags()`), and custom controls —
2795
2896
  ### `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
2796
2897
 
2797
2898
  ```ts
2798
- import { provideDocumentDouble, provideWindowDouble } from 'vitest-auto-spy/angular';
2899
+ import { provideDocumentDouble, provideWindowDouble } from 'vitest-auto-spy/angular/doubles';
2799
2900
 
2800
2901
  TestBed.configureTestingModule({
2801
2902
  providers: [
@@ -2854,7 +2955,8 @@ Five things to know:
2854
2955
 
2855
2956
  ```ts
2856
2957
  import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
2857
- import { expectEmission, injectMatDialogRef, provideMatDialogData, provideMatDialogRef } from 'vitest-auto-spy/angular';
2958
+ import { expectEmission } from 'vitest-auto-spy/angular';
2959
+ import { injectMatDialogRef, provideMatDialogData, provideMatDialogRef } from 'vitest-auto-spy/angular/doubles';
2858
2960
 
2859
2961
  TestBed.configureTestingModule({
2860
2962
  providers: [provideMatDialogData<EditUserData>(MAT_DIALOG_DATA, { id: 7, name: 'Ada' }), provideMatDialogRef(MatDialogRef)],
@@ -3079,11 +3181,11 @@ service.products.set([edited]); // the component's own optimistic write — stat
3079
3181
  expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, and re-issues nothing
3080
3182
 
3081
3183
  // signal assertions
3082
- registerSignalMatchers(); // once, in the setup file
3184
+ registerSignalMatchers(); // once, in the setup file — /angular/matchers
3083
3185
  expect(component.total).toHaveSignalValue(3);
3084
3186
 
3085
3187
  // resource assertions — value AND status, which is the whole point
3086
- registerResourceMatchers(); // once, in the setup file
3188
+ registerResourceMatchers(); // once, in the setup file — /angular/matchers
3087
3189
  expect(component.products).toBeLoading();
3088
3190
  expect(component.products).toHaveResourceValue([product]);
3089
3191
  expect(component.products).toHaveResourceError(/503/);
@@ -3147,7 +3249,7 @@ now throws, naming what it received.
3147
3249
  Per-file timing, to find which specs actually pay for `TestBed`:
3148
3250
 
3149
3251
  ```ts
3150
- import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular';
3252
+ import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
3151
3253
 
3152
3254
  if (process.env['SPEC_TIMING']) {
3153
3255
  enableTestBedDiagnostics();
@@ -3177,7 +3279,8 @@ It exports the core, `provideAutoSpy` / `injectSpy`, `renderShallow`, `createWit
3177
3279
  `expect.extend` and the TestBed diagnostics its suite-level hooks; the overrides, `extendWithAutoSpies`,
3178
3280
  `provideAutoSpyForToken`, `trackInjections`, `setupAngularTestEnv`, the stub factories and the
3179
3281
  `mock*Prop`, platform and dialog doubles are simply not routed to Bun. Import them from `/angular`
3180
- under Vitest.
3282
+ under Vitest, and the registrars, doubles and diagnostics from their companion entries
3283
+ (`/angular/matchers`, `/angular/doubles`, `/angular/diagnostics`).
3181
3284
 
3182
3285
  ---
3183
3286
 
@@ -3536,6 +3639,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3536
3639
  | `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
3537
3640
  | `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
3538
3641
  | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3642
+ | `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` |
3539
3643
  | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory` / `useExisting`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3540
3644
  | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3541
3645
  | `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` |
@@ -3553,8 +3657,8 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3553
3657
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3554
3658
  | `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 |
3555
3659
 
3556
- Thirty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3557
- `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-five are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3660
+ Thirty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3661
+ `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
3558
3662
  `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3559
3663
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
3560
3664
  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
@@ -3814,6 +3918,11 @@ packages, which a subpath export can never be.
3814
3918
  | 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 |
3815
3919
  | 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 |
3816
3920
  | `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({ … }))` |
3921
+ | `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 |
3922
+ | `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 })` |
3923
+ | `[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` |
3924
+ | `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 |
3925
+ | `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'] })` |
3817
3926
  | `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 |
3818
3927
  | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
3819
3928
  | 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…())` |
@@ -3980,6 +4089,21 @@ meant to show, with the test still green. Carry `vi.fn(() => x)` over as
3980
4089
  `mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
3981
4090
  anyone writing a codemod, because the rename looks like the safest edit in the file.
3982
4091
 
4092
+ ### Advice that circulates and is wrong on Vitest
4093
+
4094
+ Jest-era tutorials and cheat sheets repeat a handful of lines that fail on Vitest, or pass and leak.
4095
+ Each was checked on Vitest 5.0.0:
4096
+
4097
+ | Circulating advice | What happens | Write instead |
4098
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
4099
+ | `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 })` |
4100
+ | "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` |
4101
+ | `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
4102
+ | `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
4103
+ | `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)` |
4104
+ | `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
4105
+ | `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 |
4106
+
3983
4107
  ---
3984
4108
 
3985
4109
  ## 19. Before you report success
@@ -4036,7 +4160,8 @@ Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
4036
4160
  worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
4037
4161
  `codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
4038
4162
  it is a directory (a nested clone) or a file (a worktree). Past 50 000 files the scan still
4039
- truncates and says so; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4163
+ truncates, and `doctor` reports that as a `scan-cap-reached` warning (exit 1) rather than a clean
4164
+ result; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4040
4165
  - **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
4041
4166
  read is not a clean result. Absolute paths and `./`-style ones resolve against `--cwd`.
4042
4167
 
@@ -4046,6 +4171,14 @@ note, `perf --gate` over budget; **2** is "there was nothing to judge" — no co
4046
4171
  command, an unknown flag, an unreadable `--only` / `--from` value, a path matching no file, and a
4047
4172
  `perf` run that measured nothing (including a red suite, which the gate will not judge at all).
4048
4173
 
4174
+ **Reading the output from a script: `--format json`**, on `doctor` and on `perf`. One JSON document
4175
+ on stdout — `schema`, `exitCode`, `tally` (`errors`, `warnings`, `notes`), every finding with
4176
+ `check`, `severity`, `file`, `message`, `fix`; `perf` adds `run`, `budgets` and `gate.verdicts` (one row
4177
+ per candidate, `outcome` one of `confirmed`, `not reproduced`, `unconfirmed`, `single reading`,
4178
+ `over budget`). Parse that rather than the text: the text is wrapped to the terminal (80 columns in a
4179
+ pipe), groups one cause found in many files into one block, and ends in a tally line that starts
4180
+ with `N errors, N warnings, N notes`.
4181
+
4049
4182
  ### If you were asked why a suite is slow
4050
4183
 
4051
4184
  ```bash