vitest-auto-spy 5.33.0 → 5.35.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.
package/AGENTS.md CHANGED
@@ -21,19 +21,39 @@ package's skill in `.claude/skills/vitest-auto-spy/` is reported `stale` (and fa
21
21
  it and re-run `init`. Full table:
22
22
  <https://asdalexey.github.io/vitest-auto-spy/agents>.
23
23
 
24
- | Resource | Where |
25
- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
- | Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
27
- | Task recipes | <https://asdalexey.github.io/vitest-auto-spy/guides/mocking-classes> · `/guides/mocking-local-storage` · `/guides/mocking-prisma` · `/guides/storybook-angular` |
28
- | Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
29
- | Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
30
- | Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
31
- | Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
32
- | Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
24
+ | Resource | Where |
25
+ | --- | --- |
26
+ | Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
27
+ | Task recipes | <https://asdalexey.github.io/vitest-auto-spy/guides/mocking-classes> · `/guides/mocking-local-storage` · `/guides/mocking-prisma` · `/guides/storybook-angular` |
28
+ | Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
29
+ | Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
30
+ | Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
31
+ | Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
32
+ | Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
33
33
 
34
34
  **Read `dist/*.d.ts` before inventing a call.** Every export is typed and documented there, and the
35
35
  type is the authority when this file and the code disagree.
36
36
 
37
+ ## How to read this file
38
+
39
+ No task needs all of it. This file is the core; five long sections live next to it in `agent-docs/`,
40
+ each with a stub here that says when to read it. Read by section, not whole:
41
+
42
+ ```bash
43
+ D=node_modules/vitest-auto-spy
44
+ grep -n '^## ' "$D/AGENTS.md" # the map: every section with its line number
45
+ sed -n '<start>,<end>p' "$D/AGENTS.md" # one section, from its heading to the next one
46
+ grep -n '^### ' "$D/agent-docs/angular.md" # the map of a topic file
47
+ grep -n -F '<text of the error>' "$D/agent-docs/errors.md" # a failure: its row, with the fix
48
+ ```
49
+
50
+ | Read | Sections |
51
+ | --- | --- |
52
+ | Always, before writing a spec | §1 entry point, §2 factory, §3 the 90% recipe, §18 do not write this, §19 before you report success |
53
+ | When the task touches it | §4 return-type helpers, §5 `createSpyFromClass` configuration, §6 `Spy<T>` vs `T`, §7 resetting, §8 observables, §9 patching properties, §10 setup file (`agent-docs/setup.md`), §11 waiting, §12 doubles for what the code builds, §15 other adapters |
54
+ | Only for that stack | §13 Angular (`agent-docs/angular.md`), §14 `fakeAsync`, §16 ESLint plugin (`agent-docs/eslint.md`), §20 migrating off `jasmine-auto-spies` (`agent-docs/migration-jasmine.md`) |
55
+ | Never front to back | §17 Error → fix (`agent-docs/errors.md`): grep the message instead |
56
+
37
57
  ---
38
58
 
39
59
  ## 1. Pick the entry point first
@@ -41,37 +61,37 @@ type is the authority when this file and the code disagree.
41
61
  Each entry registers its mock adapter **on import**. Importing the wrong one leaves the wrong
42
62
  adapter installed and spies fail at runtime.
43
63
 
44
- | Runner / framework | Import from |
45
- | -------------------- | ----------------------------- |
46
- | Vitest (default) | `vitest-auto-spy` |
47
- | `bun test` | `vitest-auto-spy/bun` |
64
+ | Runner / framework | Import from |
65
+ | --- | --- |
66
+ | Vitest (default) | `vitest-auto-spy` |
67
+ | `bun test` | `vitest-auto-spy/bun` |
48
68
  | `bun test` + Angular | `vitest-auto-spy/bun-angular` |
49
- | `node --test` | `vitest-auto-spy/node` |
50
- | `rstest run` | `vitest-auto-spy/rstest` |
51
- | Angular + Vitest | `vitest-auto-spy/angular` |
52
- | NestJS | `vitest-auto-spy/nestjs` |
53
- | React | `vitest-auto-spy/react` |
54
- | Vue / Pinia | `vitest-auto-spy/vue` |
55
- | Svelte | `vitest-auto-spy/svelte` |
69
+ | `node --test` | `vitest-auto-spy/node` |
70
+ | `rstest run` | `vitest-auto-spy/rstest` |
71
+ | Angular + Vitest | `vitest-auto-spy/angular` |
72
+ | NestJS | `vitest-auto-spy/nestjs` |
73
+ | React | `vitest-auto-spy/react` |
74
+ | Vue / Pinia | `vitest-auto-spy/vue` |
75
+ | Svelte | `vitest-auto-spy/svelte` |
56
76
 
57
77
  Add-ons, orthogonal to the runner:
58
78
 
59
- | Add-on | Import | Needed for |
60
- | ------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
61
- | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, and in a file the `tsconfig` includes (§4) |
62
- | 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 |
63
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
64
- | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
65
- | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
66
- | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
67
- | 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 |
68
- | 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 5.21.0** so importing spies stops evaluating it |
69
- | 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 5.21.0** |
70
- | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 5.21.0** |
71
- | 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+ |
72
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
73
- | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
74
- | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
79
+ | Add-on | Import | Needed for |
80
+ | --- | --- | --- |
81
+ | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, in a setup file, a spec, or a `.d.ts` the `tsconfig` includes (§4) |
82
+ | 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 |
83
+ | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
84
+ | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
85
+ | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
86
+ | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
87
+ | 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 |
88
+ | 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 5.21.0** so importing spies stops evaluating it |
89
+ | 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 5.21.0** |
90
+ | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 5.21.0** |
91
+ | 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+ |
92
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
93
+ | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
94
+ | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
75
95
 
76
96
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
77
97
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
@@ -201,7 +221,7 @@ three ways back compose — `restoreSpiedInstance(instance)` for one object mid-
201
221
  `using spy = createSpyFromInstance(client)`, whose dispose **restores** rather than merely resets:
202
222
  the only sense disposal can have for an object the consumer owns. A frozen or sealed instance, and a
203
223
  member declared `configurable: false`, come back as this package's diagnostic rather than a bare
204
- `TypeError` (§17). Nothing else in the field does this — `vi.mockObject` is Vitest-only,
224
+ `TypeError` (§17) — except a writable, enumerable one, whose value is simply replaced in place. Nothing else in the field does this — `vi.mockObject` is Vitest-only,
205
225
  `sinon.createStubInstance` builds a new object from a constructor instead of patching the one you
206
226
  hold, and `bun:test` and `node:test` have nothing.
207
227
 
@@ -231,8 +251,10 @@ node's prototype chain, and past the component's own class that chain is the eng
231
251
  `Node.removeChild` calls an internal Symbol-keyed method on the child, and once discovery has
232
252
  patched it the strict guard refuses that call — the node cannot be removed from `document.body` at
233
253
  all, and the leak then fails later tests in the file far from the cause. Without `onlyMethodsToSpyOn`
234
- on a live `Node` or another `EventTarget`, the call now reports this itself, before patching
235
- anything — `warn` by default, or a throw right there under `setMisconfigurationReaction('throw')`.
254
+ on a live `Node`, the global `window` or another engine-provided event target (`XMLHttpRequest`,
255
+ `AbortSignal`; a class of your own that extends `EventTarget` does not count), the call now reports
256
+ this itself, before patching anything — `warn` by default, or a throw right there under
257
+ `setMisconfigurationReaction('throw')`.
236
258
  The bare-array shorthand does not silence it: `createSpyFromInstance(el, ['addEventListener'])`
237
259
  merges as `methodsToSpyOn`, the _additive_ list, so discovery still walks the whole node — only
238
260
  `{ onlyMethodsToSpyOn: [...] }` skips it. For a still-attached element, spy one property with
@@ -368,11 +390,11 @@ missing, and each produced a _silent_ wrong answer rather than an error — the
368
390
  this library can have, because a checking test becomes a non-checking one and only the proxy's
369
391
  source says so. Two are fixed; the third cannot be:
370
392
 
371
- | Operation | Before 3.5.0 | Now |
372
- | --------------------------------- | ------------------------------------------------- | -------------------------------------------------------- |
373
- | `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
374
- | `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
375
- | `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
393
+ | Operation | Before 3.5.0 | Now |
394
+ | --- | --- | --- |
395
+ | `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
396
+ | `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
397
+ | `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
376
398
 
377
399
  `ownKeys` cannot be completed: a type has no key list at runtime, which is the whole premise of
378
400
  these two factories. So a spec that installs a double by **copying it onto a real instance** —
@@ -389,12 +411,12 @@ probe too, at which point it stops being a double of `T` and becomes whatever wa
389
411
  Four names are therefore answered with `undefined` unless the spec seeds them, alongside `then` and
390
412
  every symbol, which always were:
391
413
 
392
- | Key | Probed by | The double became |
393
- | -------------- | --------------------------------------------- | ----------------- |
394
- | `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
395
- | `lift` | `isObservable`, with `subscribe` | an Observable |
396
- | `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
397
- | `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
414
+ | Key | Probed by | The double became |
415
+ | --- | --- | --- |
416
+ | `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
417
+ | `lift` | `isObservable`, with `subscribe` | an Observable |
418
+ | `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
419
+ | `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
398
420
 
399
421
  The one that cost an afternoon reads like nothing at all:
400
422
 
@@ -574,11 +596,11 @@ Four conventions that carry most of the value:
574
596
  Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementation`,
575
597
  `toHaveBeenCalledWith` and the rest all work as usual. On top of that:
576
598
 
577
- | Return type | Helpers added |
578
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
579
- | anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)`. On a `Promise` or `Observable` method the handle carries that row's helpers instead — `calledWith(id).nextWith(v)`, `.resolveWith(v)`; `.mockReturnValue` there is a `TS2339` |
580
- | `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
581
- | `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
599
+ | Return type | Helpers added |
600
+ | --- | --- |
601
+ | anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)`. On a `Promise` or `Observable` method the handle carries that row's helpers instead — `calledWith(id).nextWith(v)`, `.resolveWith(v)`; `.mockReturnValue` there is a `TS2339` |
602
+ | `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
603
+ | `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
582
604
 
583
605
  `Observable` **properties** (not just methods) get the same helpers — list them in
584
606
  `observablePropsToSpyOn`. The prop spy is a plain `Observable`: a `BehaviorSubject` member loses
@@ -601,10 +623,12 @@ augmentable interface, `AutoSpyRxjsTypes<T>`, which `/rxjs` fills in with `subje
601
623
  augment it yourself only to plug in a different subject type. If
602
624
  `const s: Subject<T> = spy.m.returnSubject()` fails to compile, the import is missing from the
603
625
  program the specs are checked in — usually a Vitest `setupFiles` entry that no `tsconfig`
604
- `include` covers. That is the _only_ breaking change in 4.0.0; the reason for it is that
605
- `dist/types-*.d.ts` used to open with `import { Observable, Subject } from 'rxjs'` and load 189 rxjs
606
- `.d.ts` files into every consumer's program (303 files against 114 without it), `import type`
607
- included — TypeScript resolves a type-only import the same way.
626
+ `include` covers, or, from `@angular/build:unit-test` 22.2.0, a plain `.ts` that only the spec
627
+ `tsconfig`'s `include` lists: that builder's program is the specs, the setup files and the `.d.ts`
628
+ files (angular-cli#34134), so put the import in one of those. That is the _only_ breaking change in
629
+ 4.0.0; the reason for it is that `dist/types-*.d.ts` used to open with
630
+ `import { Observable, Subject } from 'rxjs'` and load 189 rxjs `.d.ts` files into every consumer's
631
+ program (303 files against 114 without it), `import type` included — TypeScript resolves a type-only import the same way.
608
632
 
609
633
  ```ts
610
634
  // argument dispatch — other arguments return undefined
@@ -780,16 +804,16 @@ createSpyFromClass(MyService, {
780
804
  });
781
805
  ```
782
806
 
783
- | Key | Semantics |
784
- | ------------------------ | --------------------------------------------------------------------------------------------------------- |
785
- | `methodsToSpyOn` | **Additive**, as in `jest-auto-spies`. Same behaviour as `instanceMethodsToSpyOn`. |
786
- | `onlyMethodsToSpyOn` | **Exhaustive whitelist.** Skips discovery; anything not listed is absent. |
787
- | `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
788
- | `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
789
- | `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. `'proxy'` also changes _what holds the name_. |
790
- | `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
791
- | `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
792
- | `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
807
+ | Key | Semantics |
808
+ | --- | --- |
809
+ | `methodsToSpyOn` | **Additive**, as in `jest-auto-spies`. Same behaviour as `instanceMethodsToSpyOn`. |
810
+ | `onlyMethodsToSpyOn` | **Exhaustive whitelist.** Skips discovery; anything not listed is absent. |
811
+ | `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
812
+ | `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
813
+ | `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. `'proxy'` also changes _what holds the name_. |
814
+ | `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
815
+ | `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
816
+ | `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
793
817
 
794
818
  **`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
795
819
  the reference suite). Method discovery walks the _prototype chain_; a callable assigned to an
@@ -1100,12 +1124,12 @@ Both are the same object at runtime. `injectSpy(X)` already returns `Spy<X>`.
1100
1124
  The compiler reports this in four different ways, none of which contains both the words "spy" and
1101
1125
  "instance", which is why the fix is hard to find from the message alone:
1102
1126
 
1103
- | Message | Direction | Fix |
1104
- | ------------------------------------------------------------------------------------ | --------- | --------------------------------- |
1105
- | `TS2352: … 'accessorSpies' is missing in type 'Router'` | `T` → spy | `asSpy(TestBed.inject(Router))` |
1106
- | `TS2739` / `TS2740: Type 'Spy<X>' is missing the following properties from type 'X'` | spy → `T` | `asInstance(spy)` |
1107
- | `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` | spy → `T` | `asInstance(spy)` |
1108
- | `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not `Mocked<T>` |
1127
+ | Message | Direction | Fix |
1128
+ | --- | --- | --- |
1129
+ | `TS2352: … 'accessorSpies' is missing in type 'Router'` | `T` → spy | `asSpy(TestBed.inject(Router))` |
1130
+ | `TS2739` / `TS2740: Type 'Spy<X>' is missing the following properties from type 'X'` | spy → `T` | `asInstance(spy)` |
1131
+ | `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` | spy → `T` | `asInstance(spy)` |
1132
+ | `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not `Mocked<T>` |
1109
1133
 
1110
1134
  That last row is its own trap: Vitest's `Mocked<T>` keeps `T`'s **private** members, so the error
1111
1135
  lists private field names and reads as "the double is incomplete". It is not — the declaration is
@@ -1498,776 +1522,23 @@ The hand-written form fails in three ways that all surface in **someone else's f
1498
1522
 
1499
1523
  ## 10. Setup file
1500
1524
 
1501
- ```ts
1502
- // vitest.setup.ts
1503
- import 'vitest-auto-spy/rxjs';
1504
- // once — enables observable spies everywhere
1505
- import { setupAutoSpy } from 'vitest-auto-spy/setup';
1506
-
1507
- setupAutoSpy(); // { duplicateCopies: 'throw', restoreProps: true, restoreMocks: false }
1508
- ```
1509
-
1510
- `setupAutoSpy()` does three things: `restoreMockedProps()` in a global `afterEach`, a duplicate-install
1511
- check that fails the run, and (opt-in) `vi.restoreAllMocks()`. Turn on `restoreMocks: true` when the
1512
- suite runs with `isolate: false`.
1513
-
1514
- **The restore also runs from an `onTestFinished` net**, because the `afterEach` is not guaranteed
1515
- to. Vitest calls `afterEach` hooks in _reverse_ registration order, so the setup file's is the last
1516
- one, and a hook the spec file registered takes the chain down with it when it throws — the patches
1517
- then travel into the next test and the failure surfaces somewhere that never touched them. One spec
1518
- kept `afterEach(() => vi.restoreAllMocks())`; migrating it to `gettersToSpyOn` made the restored
1519
- getter return `undefined`, `ngOnDestroy` called it as a signal, the `TypeError` aborted the hook,
1520
- and a template error about a null profile appeared in a different `describe`. The net puts the
1521
- properties back and warns with the count and the cause. `countMockedProps()` is exported if you
1522
- would rather assert it: `afterEach(() => expect(countMockedProps()).toBe(0))`.
1523
-
1524
- **A `mock*Prop` patch written in a `describe` body or a `beforeAll` is reported**, because the sweep
1525
- undoes it after the test it was applied during and nothing puts it back: the first test passes and
1526
- every one after it reads the real member. `propsOutsideHooks` grades that — `'warn'` (default),
1527
- `'throw'`, `'off'` — and `reportPropsOutsideHooks(reaction)` sets the same dial for a suite that
1528
- wires its own hooks. Move the call into `beforeEach`.
1529
-
1530
- **Adopting `setupAutoSpy()` in a suite that restored its patches once per file** — one
1531
- `restoreMockedProps()` in an `afterAll` — changes a patch from "lives for the file" to "lives for one
1532
- test". On 176 files of one such suite, 3 broke, each a `mock*Prop` in a `describe` body or
1533
- `beforeAll`, and the `'warn'` report is easy to miss in a full run. Run the first time with
1534
- `setupAutoSpy({ propsOutsideHooks: 'throw' })`, fix what it names, then drop the option.
1535
-
1536
- **The one that only bites at scale:** with `isolate: false`, a `setTimeout` or
1537
- `requestAnimationFrame` a component schedules and never clears keeps running after its file is done,
1538
- and fires while the **next** file is mid-test. It is reported against that innocent file, as
1539
- `Schedulers cannot synchronously execute watches while scheduling`, `signal read during notification
1540
- phase`, or an unhandled rejection naming a component the failing file never imported. If you see any
1541
- of those, suspect the previous file, not the one that failed:
1542
-
1543
- ```ts
1544
- setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors in afterAll
1545
- ```
1546
-
1547
- The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
1548
- `cancelStrayTimers()` (returns how many it cancelled), `countStrayTimers()` and
1549
- `describeStrayTimers()` (each pending timer's `kind`, the `delay` of a timeout or interval, the spec
1550
- `file` that scheduled it, up to five `frames`, and `test` — the `suite > test` that scheduled it — or
1551
- `outsideTest: 'import' | 'hook'` when no test was running), all from `vitest-auto-spy/setup`. Use
1552
- `expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak fail rather than be tidied
1553
- away, or take the per-file report from the sweep itself — `timers` carries the same origins, so the
1554
- failure diff names the file and the scheduling call:
1555
-
1556
- ```ts
1557
- setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).toEqual([]) });
1558
- ```
1559
-
1560
- `frames` starts below the tracking wrapper and is up to forty frames deep, so a zone or an rxjs
1561
- scheduler in between does not hide the caller: project frames come first; when the call came from
1562
- dependencies only, their frames stand in. The package's own frames are never quoted, from source or
1563
- from `dist`. `onStrayTimers: 'throw'` fails the file with a message that names the file and lists
1564
- every stray as `setTimeout 300 ms, scheduled in "<test>" at <frames[0]>`, then the one fix; a handler
1565
- that prints its own message should print the same `kind`, `delay`, `file`, `test` and `frames[0]` —
1566
- they are what makes a report that only reproduces under a slow coverage run actionable the first
1567
- time.
1568
-
1569
- A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
1570
- the previous file is the one to fix.
1571
-
1572
- **`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
1573
- away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
1574
- the fake clock hands out bypasses the count entirely — which makes `expect(countStrayTimers()).toBe(0)`
1575
- vacuous in any file running on a frozen clock, `setupAutoSpy({ strayTimers: true, globalFakeTimers: true })`
1576
- included. Read `vi.getTimerCount()` for the fake clock's own backlog; the two answer different
1577
- questions and neither covers the other. A handle cleared by the number it coerces to
1578
- (`clearTimeout(+handle)`) or through its own `close()` is forgotten properly now, so those stopped
1579
- being counted as strays.
1580
-
1581
- **With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
1582
- arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
1583
- Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
1584
- reports **no leaks** for a file that leaks. Cancelling is still the right default — a callback firing
1585
- during a later file is the more expensive failure — so when both are on and no `onStrayTimers` is
1586
- given, the sweep prints to stderr the report `'throw'` fails with — the file, the first three timers
1587
- and the test that scheduled each — closed by one sentence saying they are missing from Vitest's
1588
- "Async Leaks" report; `onStrayTimers` gets all of them. Vitest's own report, with `strayTimers` off, points
1589
- its code frame at the `setTimeout` in the spec, because the library's own scheduler wrappers go
1590
- through `vi.defineHelper` and are dropped from the stack.
1591
-
1592
- **The one that keeps a suite green while it is wrong:** zone.js replaces the global `Promise`, and a
1593
- rejection nobody handled is drained into `console.error` and no further — it never reaches
1594
- `process.on('unhandledRejection')`, the channel Vitest watches, so the runner is never told and the
1595
- file still exits 0. `compileComponents().then(() => expect(…))`, an `async` helper called without
1596
- `await`, a `TypeError` thrown inside `import('…').then(…)` in production code: each of those is a
1597
- passing test with a line of stderr behind it. One migrated suite — 1688 spec files, 11 587 tests,
1598
- green — was hiding six of them, two being assertions that were simply false.
1599
-
1600
- ```ts
1601
- setupAutoSpy({ strayRejections: true }); // fail the test the swallowed rejection surfaced in
1602
- ```
1603
-
1604
- Off by default, and it needs zone.js already loaded — this package never imports it, so the setup
1605
- file does (`import 'zone.js';`) or the Angular builder does. Without it the call **throws** rather
1606
- than quietly watching nothing. Native, non-zone rejections already fail a Vitest run, and nothing
1607
- here touches them. The pieces are exported too — `trackStrayRejections()` (idempotent, returns the
1608
- undo), `flushStrayRejections()` (takes what was captured and starts again from empty) and
1609
- `countStrayRejections()`. The count is complete; the **reasons** retained are capped at 100, so a
1610
- file rejecting in a loop cannot hold its whole run's worth of stacks. A promise rejected with
1611
- `undefined` is reported as that rather than as `[object Object]`. The `no-floating-assertion` lint
1612
- rule catches the commonest shape before it ever runs (§16).
1613
-
1614
- **`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
1615
- about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
1616
- into `console.error` before `process.on('unhandledRejection')` would fire, so there is nothing for
1617
- that callback to filter. Use `onUnhandledError` to triage the native failures Vitest already reports;
1618
- use `strayRejections` to find the ones it never sees.
1619
-
1620
- A rejection the runner has **already** blamed the finished test for is not reported again. An
1621
- `async` test that fails an assertion leaves its own `AssertionError` in both places, so a red run
1622
- used to print two messages per failure and the second one sent the reader hunting for a defect that
1623
- was not there. What is left is what the check is for: the rejections that fail no test at all.
1624
-
1625
- **The one that gets slower the longer the run goes on:** every `vi.fn()` and `vi.spyOn()` is added
1626
- to one `Set` inside `@vitest/spy`, because that is what `vi.clearAllMocks()` walks, and nothing takes
1627
- anything out of it again. With `isolate: false` the set is created once per worker and only grows:
1628
- `clearMocks: true` then walks every mock of every file already run **before every single test**, and
1629
- the worker holds all of them at once — their recorded arguments included, and through those whole
1630
- component trees.
1631
-
1632
- ```ts
1633
- setupAutoSpy({ pruneMockRegistry: true }); // keep only the mocks that outlive a file
1634
- ```
1635
-
1636
- The part to understand before turning it on is what must **not** be pruned. A pruned mock is one
1637
- `clearMocks` can no longer see, so its calls accumulate silently — harmless for a mock that dies with
1638
- its file, a bug for the module-level `vi.fn()` in a shared `*.mock.ts` that six spec files import.
1639
- The split is therefore drawn where it is observable: what is already in the registry when a file's
1640
- hooks start was created while the module graph was evaluated and is kept; everything added after that
1641
- belongs to the file and goes when it ends. One case lands on the wrong side — a module first loaded
1642
- by a dynamic `import()` inside a test — and says so explicitly:
1643
-
1644
- ```ts
1645
- export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
1646
- ```
1647
-
1648
- The mocks it keeps, it also guards. Staying registered means `vi.resetAllMocks()` reaches them too —
1649
- it walks the same set and calls `mockReset()`, which puts an implementation back only when it was
1650
- passed to `vi.fn(implementation)`; behaviour chained on with `.mockReturnValue(…)` or
1651
- `.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a _different_ file later
1652
- in the same worker, inside application code, as `Cannot read properties of undefined` against a
1653
- shared double that spec never touched — and `vi.restoreAllMocks()` does not cause it (Vitest 4 walks
1654
- `MOCK_RESTORE` there, which only `vi.spyOn` writes to), so probing with that one comes back green and
1655
- sends the search the wrong way. The implementation a long-lived mock carried when it was classified
1656
- is therefore remembered and put back, in `beforeEach`, but only when it has gone missing.
1657
-
1658
- `trackMockRegistry()` installs the hooks on its own, `pruneMockRegistry()` is the one-shot sweep (it
1659
- returns how many went), `restoreLongLivedImplementations()` is that repair (it returns how many it
1660
- put back) and `getMockRegistrySize()` reports what is left.
1661
-
1662
- One entry is never pruned, and it is not one of yours: the Vitest adapter registers a single
1663
- `vi.fn()` whose `mockClear` sweeps this library's own spies. Dropping it would turn
1664
- `vi.clearAllMocks()` into a silent no-op for every double in the run, so it carries a mark the
1665
- pruner skips. Why the library needs a mock of its own for that is the next section.
1666
-
1667
- **The one that answers for every file that follows:** `vi.spyOn(localStorage, 'setItem')` puts the
1668
- mock on the storage as an own property, and happy-dom's `Storage` hands each instance out wrapped
1669
- in a Proxy whose `deleteProperty` trap only knows stored items. `mockRestore()` — and
1670
- `vi.restoreAllMocks()` walking the same path — deletes through that trap, comes back green, and
1671
- does nothing: the spy keeps recording, and a later spec's own `vi.spyOn` is handed the same mock
1672
- with the previous file's calls already in it, so "not to have been called" fails every other run.
1673
- Probing by running `vi.restoreAllMocks()` rules nothing out — it is the failing path itself. Writing
1674
- `Storage.prototype`'s method back over the spy does go through the proxy; that asymmetry, define
1675
- passes and delete does not, is the repair, and it is on by default:
1676
-
1677
- ```ts
1678
- setupAutoSpy(); // restoreStorageSpies: true — the sweep runs at the file boundary only
1679
- ```
1680
-
1681
- `restoreStorageSpies()` is the one-shot (it returns the storages it repaired); a suite that
1682
- deliberately keeps a spy on a storage method for a whole worker turns the option off. jsdom breaks
1683
- the other half of the same contract — its Storage proxy turns a `defineProperty` of a method into a
1684
- stored item — which is why the gate is `vi.isMockFunction` rather than "differs from the
1685
- prototype": a junk item is left alone, a mock is repaired.
1686
-
1687
- **The one that outlives its component:** a listener on `window` or `document` is not the
1688
- component's to take off. Overlays, portals and services register on the shared targets, and under
1689
- `isolate: false` whatever a file leaves registered fires during the next one — the same wrong-file
1690
- blame as a stray timer, with none of the visibility: nothing errors, the callback simply runs
1691
- against mocks and a DOM it was never written for.
1692
-
1693
- ```ts
1694
- setupAutoSpy({ strayListeners: true }); // wrap addEventListener, sweep what the file added
1695
- ```
1696
-
1697
- The split is the one `pruneMockRegistry` uses: a `beforeAll` marks the listeners already on
1698
- `window`/`document` — registered while the module graph was evaluated, a framework's one-time
1699
- initialisation — and the `afterAll` takes off everything added since. The pieces are exported too:
1700
- `trackStrayListeners()` (idempotent, returns the undo), `baselineStrayListeners()`,
1701
- `removeStrayListeners()` (returns how many), `countStrayListeners()` (throws before
1702
- `trackStrayListeners()` has run, like the timer counter) and `describeStrayListeners()` — each
1703
- stray's target, type, spec file, up to five frames and the same `test` / `outsideTest` as a timer,
1704
- the list `onStrayListeners({ removed, listeners })` receives when a file should fail instead:
1705
- `onStrayListeners: ({ removed }) => expect(removed).toBe(0)`, or `onStrayListeners: 'throw'`, whose
1706
- message names the file and lists each as `keydown on document, added in "<test>" at <frames[0]>`.
1707
- A listener has `type` and `target` where a timer has `kind` and `delay` — a handler that prints its own
1708
- message prints those fields instead.
1709
- With `strayTimers` on too, both reports run after both sweeps and both run even if the first throws —
1710
- a handler or `'throw'` alike; two failures surface as one `AggregateError`
1711
- (`2 file-end checks failed when a.spec.ts ended:`, each report a numbered block with its `Docs:`
1712
- line), so a throwing report never leaves a timer uncancelled. The stray-console file report runs in
1713
- the same sweep.
1714
- Under jsdom a listener registered with `{ once: true }` that already fired stays counted until
1715
- something removes it: the wrapper cannot observe the firing without breaking identity-based
1716
- `removeEventListener` from the code under test, and removing an already-fired listener is a no-op
1717
- anyway. happy-dom detaches it through the public `removeEventListener`, so there it leaves the count.
1718
-
1719
- **The one no registry tracks:** `vi.stubGlobal` is undone by `unstubGlobals` and
1720
- `vi.spyOn(globalThis, …)` by `restoreMocks`, but a plain assignment —
1721
- `global.ResizeObserver = stub` — is written straight into the shared worker and read by every later
1722
- file. `guardGlobals` names the _unrepairable_ case, a non-configurable redefine; this is the net
1723
- for the repairable one:
1724
-
1725
- ```ts
1726
- setupAutoSpy({ restoreGlobals: true }); // one snapshot per worker, restored at every file boundary
1727
- ```
1728
-
1729
- `captureGlobalBaseline()` takes the snapshot — the first call is the worker's truth, a later one
1730
- does nothing, so a re-capture can never launder a replacement into the baseline. `restoreGlobals()`
1731
- is the sweep, and returns the keys it changed. Two traps it steps around, both load-bearing. A DOM
1732
- environment installs window properties on `globalThis` as accessor pairs forwarding to an override
1733
- map, so `global.ResizeObserver = stub` leaves the descriptor untouched and the restore has to write
1734
- the captured value back through the same setter, not just re-define the descriptor. And the
1735
- library's own wrappers — `setTimeout` under `strayTimers`, `addEventListener` under
1736
- `strayListeners` — are marked as theirs, so the restore steps around them instead of uninstalling
1737
- the tracking at the first boundary. Added keys are never deleted (a framework that installs a
1738
- global at import time must not lose it), and the identity globals — `location`, `document`,
1739
- `window` and their kin — are never written back at all. A leftover fake clock comes off first,
1740
- because restoring timer descriptors under an installed fake breaks both.
1741
-
1742
- ### What a method spy is, and the one thing that differs from `vi.fn()`
1743
-
1744
- A method spy is **not** a `vi.fn()`. It is this library's own mock function: one shared prototype
1745
- carrying the whole `Mock` surface, call state allocated on the first call rather than at creation,
1746
- and no entry in any global registry. `vi.fn()` assigns some twenty-five closures as own properties
1747
- of every mock it makes and allocates six arrays up front — per method, on every double a spec
1748
- builds — and that cost is most of what a wide service used to pay for methods no test touches.
1749
-
1750
- Everything a spec can observe is the same, and the suite pins it by putting the two side by side:
1751
-
1752
- - `vi.isMockFunction(spy.load)`, `expect(spy.load).toHaveBeenCalledWith(…)`, `toHaveReturned`,
1753
- `toHaveResolved`, `toHaveBeenNthCalledWith` — every matcher reads `_isMockFunction`, `mock.*` and
1754
- `getMockName()`, all of which the spy implements with the runner's semantics.
1755
- - `spy.load.mock.calls` / `.results` / `.settledResults` / `.instances` / `.contexts` /
1756
- `.invocationCallOrder` / `.lastCall`, `mockReturnValue`, `mockResolvedValue`,
1757
- `mockImplementation(Once)`, `withImplementation`, `mockClear` / `mockReset` / `mockRestore`,
1758
- `mockName`, `using`.
1759
- - `vi.clearAllMocks()`, `vi.resetAllMocks()`, and the `clearMocks` / `mockReset` config keys, which
1760
- Vitest applies through those two functions.
1761
-
1762
- **The one difference.** `mock.invocationCallOrder` counts on this library's own scale, so
1763
- `expect(a).toHaveBeenCalledBefore(b)` is exact between two auto-spies and **wrong** between an
1764
- auto-spy and a hand-written `vi.fn()` — not an error, an answer, computed from two counters that
1765
- never met. `@vitest/spy` keeps its own in a module-private variable it neither exports nor lets
1766
- anything advance, so the scales cannot be reconciled after the fact; they drift apart by however many
1767
- calls of each kind the run has already made. Measured in a converted suite: a double reporting
1768
- `[164, 165, 167, 168, 169]` beside a `vi.fn()` reporting `[28]` for its single call in the same test,
1769
- under an assertion that had been passing — because `jest-auto-spies` built both halves with
1770
- `jest.fn()`, which did share one counter. Three earlier calls of one family are enough to invert the
1771
- verdict. Nothing else in the library or in Vitest reads that field. If a suite needs the comparison,
1772
- compare two doubles of the same family, or switch the whole run back to the runner's factory:
1773
-
1774
- ```ts
1775
- // vitest.setup.ts
1776
- import { setSpyEngine } from 'vitest-auto-spy/setup';
1777
-
1778
- setSpyEngine('runner'); // every double built afterwards is `vi.fn()` per method, as before 4.1
1779
- ```
1780
-
1781
- **On Vitest 5 the registry story changes, and this library follows it.** Vitest 5 keeps registered
1782
- mocks behind `WeakRef`s and its `clearAllMocks()` visits only the ones _called_ since the last
1783
- sweep — so a spy that is not a `vi.fn()` has to make itself reachable to that sweep deliberately. It
1784
- does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMocks: true` default
1785
- (new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
1786
- changes, and the peer range still starts at 2.1 — one install spans Vitest 2.1 through 5.x.
1787
-
1788
- What each config flag does to an auto-spy, measured on both spy engines: `clearMocks` empties the
1789
- calls and keeps the configuration; `mockReset` empties the calls and drops `mockReturnValue` /
1790
- `mockImplementation` but **keeps `calledWith` rules**; `restoreMocks` never touches an auto-spy (it
1791
- only undoes `vi.spyOn`). Do not claim the flags skip auto-spies — a sweep sentinel in the Vitest
1792
- adapter routes the first two. `resetAutoSpy(spy)` is the call that drops everything, `calledWith`
1793
- included. `mockReset()` on a `vi.spyOn(…)` runs the **real** method again (Vitest ≥ 3).
1794
-
1795
- What Vitest 5 does break is the runner's own doing, not this library's: with `clearMocks` on by
1796
- default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
1797
- zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
1798
-
1799
- **The one exception to "nothing in a spec changes".** `setupAutoSpy({ pruneMockRegistry: true })`
1800
- discovers long-lived mocks by walking the runner's registry, which Vitest 5 no longer exposes — the
1801
- automatic half is a no-op there. `keepMockRegistered(mock)` works on every version and is the fix;
1802
- recommend it whenever a suite on Vitest 5 loses a shared mock's implementation to
1803
- `vi.resetAllMocks()`.
1804
-
1805
- `getSpyEngine()` reports the current one, `'auto-spy'` is the default, and doubles already built keep
1806
- the engine they were built with. The switch is Vitest-only: on Bun and `node:test` the runner's own
1807
- matchers recognise only the runner's own mocks, so those adapters keep using them.
1808
-
1809
- Two more switches, both about the environment rather than the spies:
1810
-
1811
- ```ts
1812
- setupAutoSpy({ blockNetwork: true }); // fetch rejects, XHR fails, sendBeacon answers false
1813
- ```
1814
-
1815
- Both DOMs leak, through different holes. happy-dom implements `fetch`, so a component pulling a
1816
- remote asset really fetches it, nothing asserts on the response, and the aborts at teardown fail the
1817
- run with **no test named** — a green run exiting 1 with `DOMException [AbortError]`. jsdom
1818
- implements `XMLHttpRequest` in full, and the libraries that never left XHR (a VAST player pinging
1819
- every tracker through a hand-rolled one) reach the internet once per ping per test, printing jsdom's
1820
- `AggregateError at Object.dispatchError` for each connection that failed — so what a green run
1821
- prints depends on whether the machine has a route out.
1822
-
1823
- Every channel is closed by default; the object is for narrowing it:
1824
-
1825
- | option | default | what it does |
1826
- | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
1827
- | `fetch` | `true` | `fetch` rejects, naming the method, the URL and the test, and the `stubResponse` line that answers it |
1828
- | `xhr` | `'reject'` | `'reject'` fails the request (`status` 0, an `error` event); `'empty'` answers 200 with an empty body; `false` leaves XHR alone |
1829
- | `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one |
1830
-
1831
- `'reject'` is the default because it is what `fetch` does: the code takes its failure branch, which
1832
- is the branch a unit test should be asserting on. `'empty'` is for a request whose response nobody
1833
- reads — a tracker ping, an analytics beacon — where failing it only trades one kind of noise for
1834
- another:
1835
-
1836
- ```ts
1837
- setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setting
1838
- ```
1839
-
1840
- **Called twice, the last caller's mode wins.** The setup file installs from its own `beforeEach`,
1841
- which runs before the spec's, so a spec calling `blockNetwork({ xhr: 'reject' })` because the failure
1842
- branch is what it is testing used to be served the setup file's empty 200 and pass on the wrong
1843
- branch. The stubs are also installed once rather than re-journalled per test, so `restoreProps: false`
1844
- no longer grows the restore journal for the length of the run.
1845
-
1846
- A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
1847
- serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
1848
- and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
1849
- resolves it against the document origin, so a spec that reaches `/config`and passes is resting on
1850
- nothing listening on that port.`WebSocket`and`EventSource`are left alone: their failure is an
1851
- event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
1852
- a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
1853
-
1854
- - `stubResponse(init?)` (`/setup`) builds a real `Response` — never write `{ ok, json } as Response`.
1855
- Plain data in `body` goes out as JSON with `application/json`; an `ok` that disagrees with
1856
- `status` throws. A body can be read once: for a stub answering several calls use
1857
- `mockImplementation(async () => stubResponse(…))`, not `mockResolvedValue`.
1858
- - **`body: null` is the JSON literal, and only `undefined` or an omitted `body` send no body.** It
1859
- reverses what the field used to mean: `null` sent no body, so `.json()` threw
1860
- `Unexpected end of JSON input` on it while `0`, `false`, `[]` and `{}` round-tripped — the one
1861
- literal a reader had no reason to expect to be different, and the shape a backend answering
1862
- "nothing here" (an empty login, an absent profile) really sends. A spec that meant "no body"
1863
- drops the field. `{ body: null, status: 204 }` throws by name rather than letting the constructor
1864
- raise "Response with null body status cannot have body" about a field whose old meaning was the
1865
- opposite; 205 and 304 are the same.
1866
- - `blockNetwork` leaves `fetch` alone while `Symbol.for('fetch-interceptor')` is on `globalThis`
1867
- (MSW `setupServer`, nock 14), so MSW handlers keep answering; XHR stays blocked for what MSW does
1868
- not handle. MSW's `onUnhandledRequest: 'error'` exempts asset-looking URLs (`.svg`, `.json`,
1869
- fonts…); a last `http.all('*', () => HttpResponse.error())` is the hard floor. MSW's browser
1870
- `setupWorker` has not been checked.
1871
-
1872
- `restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
1873
- fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
1874
- the realm, not owned by `globalThis`), and with `isolate: false` the next file dies inside Vitest's
1875
- own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
1876
- the file in the stack is not the cause.
1877
-
1878
- `restoreWebStorage` is the other repair that is on by default. Vitest copies a DOM environment's
1879
- globals onto `globalThis` behind `if (k in global) return KEYS.includes(k)`, and neither
1880
- `localStorage` nor `sessionStorage` is in `KEYS` — both used to arrive only because Node put neither
1881
- on `globalThis`, so the first half of that condition was false. Node's own Web Storage made the key
1882
- exist, and the environment's storage stopped arriving: `setItem is not a function` on Node 25,
1883
- `undefined` on Node 26, under jsdom and happy-dom alike, because the filter runs before either. The
1884
- suite stays green until a spec touches storage, so this lands as "CI moved to a new Node and eleven
1885
- unrelated specs died". The repair writes a key, reads it back and removes it — a storage that
1886
- survives that is left alone, whoever implemented it — and it installs nothing in a `node`
1887
- environment, which is supposed to have no Web Storage at all.
1888
-
1889
- `hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
1890
- CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
1891
- `test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
1892
- own and defaults it to 10 000 ms. Carry a preset's `testTimeout: 30000` into the runner config and
1893
- stop there, and every hook in the suite silently runs on a third of what its tests get. Worse, the
1894
- failure is filed against the wrong thing: a `beforeEach` timeout is attributed to the **test**, with
1895
- the test's duration pinned at the limit, so the report reads `× should create 10045ms` and sends the
1896
- reader looking for ten seconds of work inside a body that never ran. The hint appends both numbers
1897
- and the field to set. It says nothing when the budgets agree — then the hook really is slow — and
1898
- nothing for `beforeEach(fn, 300)`, which chose its own limit. `beforeAll` is out of reach by
1899
- construction: its timeout is reported as a failed _suite_, every test is skipped and no `afterEach`
1900
- runs.
1901
-
1902
- **When migrating a runner config off Jest, set both timeouts side by side**, and treat the single
1903
- Jest number as belonging to both fields:
1904
-
1905
- ```ts
1906
- test: {
1907
- testTimeout: 30_000,
1908
- // Jest had one budget for both; Vitest defaults this to 10_000 on its own.
1909
- hookTimeout: 30_000,
1910
- }
1911
- ```
1912
-
1913
- `frozenClockHint` is the same seam aimed at the other half of a timeout. A frozen clock turns
1914
- waiting into waiting forever — `await new Promise(r => setTimeout(r, 10))` never resolves unless
1915
- something advances it — and the runner's own advice ("pass a timeout value as the last argument") is
1916
- the one repair that cannot work: the callback is not late, it is never scheduled to run. Under
1917
- `globalFakeTimers` nothing in the spec says the clock is fake at all, so the timeout lands in a file
1918
- that never mentions a timer. The hint reports `vi.isFakeTimers()` and `vi.getTimerCount()` — facts,
1919
- not a guess — and stays silent when the clock is real or its queue is empty.
1920
-
1921
- The shape that reaches this with no timer in sight is an HTTP spec. `setImmediate` is among the
1922
- globals `vi.useFakeTimers()` replaces by default, and Express ends a request that matched no route
1923
- through `finalhandler`, which schedules on `setImmediate`. So the 404 is never written and the test
1924
- dies on its timeout: in such a file, "the test hung" means _the route did not match_, not "the
1925
- server is slow". One thing this cannot see through — a spec whose own `afterEach` calls
1926
- `vi.useRealTimers()`, because hooks run in reverse registration order and the clock is real again by
1927
- the time the hint reads it. Nothing is reported then, rather than something wrong.
1928
-
1929
- `angularBuildHint` is the third read-only hint, and the only one that speaks before a test rather
1930
- than after one. `@angular/build` in `[22.1.5, 22.1.7)` builds the unit-test bundle with esbuild code
1931
- splitting off — every spec a self-contained bundle, `--coverage` growing by hundreds of megabytes
1932
- with no plateau (791 chunks / 596 MB on a 784-spec suite) — and the builder says nothing. The
1933
- `doctor` check `angular-build-splitting-off` reports it, but has to be run; this line is printed
1934
- from inside the affected run, to stderr, once per worker. The builder is recognised by the marker
1935
- its own `vitest-mock-patch` setup file sets (`Symbol.for('@angular/cli/vitest-mock-patch')` on
1936
- `globalThis`, before any user setup file), so a plain Vitest run reads nothing; under the builder
1937
- the version comes from the nearest `node_modules/@angular/build/package.json` above the working
1938
- directory. That read is the one place the library touches the disk — one file, read-only, through
1939
- `process.getBuiltinModule` so `/setup` still loads with no `process` — and `scripts/check-dist.mjs`
1940
- allows `node:fs` in `dist/setup.js` for exactly that. Silent outside the builder, outside the
1941
- window, and on a Node before `getBuiltinModule`. `setupAutoSpy({ angularBuildHint: false })` turns
1942
- it off.
1943
-
1944
- **Strict for one run:** `VITEST_AUTO_SPY_STRICT=1 npx vitest run <slice>` makes `setupAutoSpy()` arm
1945
- `strict` whatever the setup file passed (`0` turns it off, `survey` counts instead of throwing); a second `setupAutoSpy()` in an extra setup
1946
- file also works now, without grading the first call's network stubs as patched outside a hook.
1947
-
1948
- **Moving a suite to `strict`:** most of the churn is the same few methods configured file by file.
1949
- Run the suite once in survey mode, which refuses nothing and ends each file with the list strict mode
1950
- would have refused, most frequent first (`setupAutoSpy({ strict: 'survey' })` is the same in code):
1951
-
1952
- ```bash
1953
- VITEST_AUTO_SPY_STRICT=survey npx vitest run
1954
- ```
1955
-
1956
- The top of that list — `SvgIconService.getIcon`, `NotificationsService.open` — goes into the setup
1957
- file once, `registerAutoSpyDefaults(NotificationsService, { returns: { open: undefined } })`, and
1958
- every double of the class answers it; the tail is what each spec configures itself.
1959
-
1960
- **One setup file for the builder and for plain Vitest:** the builder initialises `TestBed` before any
1961
- setup file, so a second `initTestEnvironment()` throws "Cannot set base providers because it has
1962
- already been called". Guard it with `isAngularUnitTestBuilder()` from `/setup` — the same marker the
1963
- hint reads — and name the file in the target's `setupFiles`, which is the only way the builder runs
1964
- it (`doctor` reports `builder-setup-unreached` otherwise).
1965
-
1966
- One more field of the same family differs quietly and changes only the report:
1967
- `slowTestThreshold` is `5` in Jest (**seconds**) and `300` in Vitest (**milliseconds**), so a
1968
- migrated suite starts marking most of its files slow. That is a unit change, not a regression.
1969
-
1970
- Fake timers:
1971
-
1972
- ```ts
1973
- import { advanceTimers, setupFakeTimers } from 'vitest-auto-spy/setup';
1974
-
1975
- setupFakeTimers(); // install + restore, paired
1976
- await advanceTimers(5_000); // advance AND drain the microtasks a bare advanceTimersByTime leaves
1977
- ```
1978
-
1979
- Coming from a Jest project that had `fakeTimers: { enableGlobally: true }`, every one of its tests
1980
- was written against a frozen clock. Turning that back on file by file is a thousand edits; turn it
1981
- on once instead:
1982
-
1983
- ```ts
1984
- setupAutoSpy({ globalFakeTimers: true }); // or a vi.useFakeTimers() config
1985
- ```
1986
-
1987
- Both ends are guarded, which is the half a hand-written pair of hooks gets wrong: a spec that drives
1988
- the clock itself would otherwise reach a second `vi.useRealTimers()`, and that one leaves the
1989
- environment without `clearInterval` — which explodes during teardown of whichever file runs next.
1990
-
1991
- `globalFakeTimers` also keeps the clock fake **between** tests, and that half is not decoration: a
1992
- `beforeAll` inside a nested `describe` runs after the previous test's `afterEach`, so a
1993
- `beforeEach`-only pair leaves it on real timers and the block fails with `the timers APIs are not
1994
- mocked` without touching a timer itself. For one `describe` instead of the whole run:
1995
- `setupFakeTimers(config, { betweenTests: true })`.
1996
-
1997
- Whatever you turn on, the hooks belong to the spec file whose collection imported the setup module.
1998
- If something keeps that module in the cache across files — `@angular/build:unit-test` under
1999
- `--coverage` serves every test file as a wrapper around the built bundle, so the setup module is
2000
- never re-evaluated — only the first file of each worker gets them, and the rest fail somewhere
2001
- unrelated. Run coverage with `--isolate`, or call `setupAutoSpy()` from something evaluated per file.
2002
-
2003
- ### Freezing and counting the clock
2004
-
2005
- ```ts
2006
- import { mockNow, mockSystemTime, useCountingClock, withSystemTime } from 'vitest-auto-spy/setup';
2007
-
2008
- mockSystemTime('2025-04-30T00:00:00Z'); // works whether or not fakes are already installed
2009
- await withSystemTime('2025-04-30T00:00:00Z', async () => { … }); // scoped, restores itself
2010
- const clock = useCountingClock(); // Date.now() → 1, 2, 3 …, reset before every test
2011
- mockNow(() => nextTimestamp()); // any Date.now source, re-applied before every test
2012
- ```
2013
-
2014
- **An assertion that contains a date must set the clock.** Otherwise the expected string is computed
2015
- from `new Date()` and the test starts failing on its own some days after it was written — which
2016
- reads as a regression and is not one.
2017
-
2018
- `vi.spyOn(globalThis, 'Date')` is not the way. Fake timers already own that global, so it throws
2019
- `Date is not a constructor` with a stack in production code and no mention of timers.
2020
- `mockSystemTime` does the right thing either way.
2021
-
2022
- Two rules about which of these composes with which, both of them earned:
2023
-
2024
- - **`withSystemTime` puts the previous time back**, including when fake timers were already running.
2025
- Its undo used to be a no-op there, so the clock stayed where the callback left it for the rest of
2026
- the test.
2027
- - **`mockSystemTime()` installs `Date` and nothing else**, so `advanceTimers()` under it has nothing
2028
- to advance. It used to pass having done nothing; it now refuses and names `setupFakeTimers()`.
2029
- `setupFakeTimers(config)` installs the config it was given rather than deferring to whatever fakes
2030
- were already up — a nested call, a global pair plus a local one, and the `Date`-only case all now
2031
- end with the clock the call asked for, where the outer configuration used to win silently.
2032
-
2033
- `useCountingClock` exists because under fake timers every call inside one test reports the _same_
2034
- "now", so a spec that asserts on **order** or **duration** — analytics batches, tracing, a rate
2035
- limiter, a TTL cache — cannot express its expectation at all. Patching `Date.now` by hand does not
2036
- survive: `vi.useFakeTimers()` installs a fresh `Date` on every call, so a module-scope or `beforeAll`
2037
- patch is left on an object nothing reads, and the naive undo re-attaches a dead clock's `now` to the
2038
- live one.
2039
-
2040
- ### Asserting focus
2041
-
2042
- ```ts
2043
- registerFocusMatchers(); // once, in the setup file
2044
-
2045
- expect(fixture.nativeElement.querySelector('.play')).toHaveFocus();
2046
- ```
2047
-
2048
- The two idioms it replaces both fail unhelpfully: `expect(document.activeElement).toBe(el)` prints
2049
- two whole DOM subtrees, and `expect(el === document.activeElement).toBe(true)` prints
2050
- `expected false to deeply equal true`. The matcher separates the causes instead — the query found
2051
- nothing, the element is not in the document, focus is still on `<body>` (nothing claimed it), or
2052
- focus is on another element, which it names as `button#save.primary` rather than as a subtree.
2053
-
2054
- ### Shared fixtures are functions, not constants
2055
-
2056
- Under `isolate: false` a module is evaluated **once per worker**, so this is one set of spies shared
2057
- by every file that imports it, registered against whichever file got there first — and the others'
2058
- `clearMocks` never reaches them. The symptom is a 30-second timeout, in a different file each run.
2059
-
2060
- ```ts
2061
- // ❌ __mocks__/context.ts
2062
- export const mockActionContext = { actions: { navigateToSection: vi.fn() } };
2063
- export const checkoutProvider = { provide: CheckoutState, useValue: { load: vi.fn() } };
2064
-
2065
- // ✅
2066
- export const createActionContext = () => ({ actions: { navigateToSection: vi.fn() } });
2067
- export const createCheckoutProvider = () => ({ provide: CheckoutState, useValue: { load: vi.fn() } });
2068
- ```
2069
-
2070
- A spec file must **export nothing**: under `isolate: false` an exported spec file is imported by its
2071
- neighbours and loses its own suite. Put shared doubles in a `*.mock.ts` next to them, as factories.
2072
- The `no-shared-module-level-mock` lint rule (§16) finds these mechanically.
2073
-
2074
- ### A stub must be re-installed for every test
2075
-
2076
- Every stub this library installs is taken off again by `restoreMockedProps()` after each test — that
2077
- is what keeps it out of the next file. So a stub installed once at `describe` level, or in a
2078
- `beforeAll`, is gone from the second test on, and what fails is an assertion about the component
2079
- with the stub sitting ten lines above it, apparently in force. The same ordering bites the other way:
2080
- a project-wide setup file installs its defaults in a root `beforeEach`, and root hooks run **before**
2081
- a file's own — so a `beforeAll` in a spec loses to them silently, while a `beforeEach` wins.
2082
-
2083
- ```ts
2084
- import { installPerTest } from 'vitest-auto-spy/setup';
2085
-
2086
- const observers = installPerTest(() => stubIntersectionObserver({ autoEmit: true }));
2087
-
2088
- it('…', () => expect(observers().last.targets).toEqual([host]));
2089
- ```
2090
-
2091
- It hands back a **reader**, not the handle: the handle is a different object each test.
2092
-
2093
- ### Naming the file that sealed a global
2094
-
2095
- ```ts
2096
- setupAutoSpy({ guardGlobals: 'throw' }); // or 'warn' while a suite is being cleaned up
2097
- ```
1525
+ In [`agent-docs/setup.md`](./agent-docs/setup.md). Read it when the task touches `setupAutoSpy()` or the setup file, or when a guard failed a file: stray timers, console output, sealed globals, leftovers on `<body>`.
2098
1526
 
2099
- `Object.defineProperty(document, 'cookie', { value })` defaults `configurable` to `false`, so the
2100
- property can no longer be redefined _or_ deleted. Under `isolate: false` every later file in the
2101
- worker inherits it, and what fails is some library, every other run, with nothing naming the file
2102
- that did it. The guard compares `globalThis`, `document`, `navigator`, `location`, `screen` and the
2103
- DOM prototypes a Jest-era stub reaches for — `Element`, `HTMLElement`, `HTMLCanvasElement`,
2104
- `HTMLMediaElement`, `Node`, `EventTarget` — around every test, symbol keys included, and reports only
2105
- what appeared and cannot be removed.
1527
+ <!-- agents-outline: agent-docs/setup.md -->
2106
1528
 
2107
- **Its blind spot is an existing name redefined in place.** The guard reads a descriptor only for a
2108
- key that was not there before, so `Object.defineProperty(document, 'cookie', { value, configurable: false })`
2109
- over the `cookie` the environment already has is invisible to it: the name was in the snapshot, and
2110
- only its configurability changed. What it catches is the addition — the shape that actually breaks a
2111
- later file, because the next `defineProperty` of a name nothing expected is the one that throws.
2112
- The snapshot is taken once per file, in `beforeAll`, rather than per test, and the comparison is also
2113
- run after every `afterAll`, so a write made in a `beforeAll` or an `afterAll` is reported too.
1529
+ - What a method spy is, and the one thing that differs from `vi.fn()`
1530
+ - Freezing and counting the clock
1531
+ - Asserting focus
1532
+ - Shared fixtures are functions, not constants
1533
+ - A stub must be re-installed for every test
1534
+ - Naming the file that sealed a global
1535
+ - Naming the file that polluted `Object.prototype`
1536
+ - Naming the test that left an attribute on `<body>`
1537
+ - Failing on console output nothing absorbed
1538
+ - One grade for everything — `preset: 'strict'`
1539
+ - Hook order differs from Jest
2114
1540
 
2115
- ### Naming the file that polluted `Object.prototype`
2116
-
2117
- ```ts
2118
- setupAutoSpy(); // prototypePollution: 'throw' by default — 'warn' sweeps and only reports
2119
- ```
2120
-
2121
- Vitest walks a file's hooks with `for…in` in `mergeHooks`, so one own enumerable key on
2122
- `Object.prototype` is spread as if it were an array during **collect**:
2123
- `TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`, with no stack —
2124
- every frame of it is filtered out by `stackIgnorePatterns`. Under `isolate: false` the key outlives
2125
- its file, so **every later spec file in that worker fails to collect** while the run still reports
2126
- success: `145 failed | 1613 passed` over `11880 passed | 0 failed`, zero failing tests because those
2127
- files never ran. The guard compares `Object.prototype` / `Array.prototype` / `Function.prototype`
2128
- around every test, takes the key back off and names the file. The write is nearly always accidental:
2129
- `Object.getPrototypeOf(instance)` **is** `Object.prototype` when `instance` is an object literal from
2130
- a `useValue` provider or a test double — patch the prototype of the class the object came from.
2131
- `guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
2132
- that does not call `setupAutoSpy()`.
2133
-
2134
- **A key an earlier file left behind is taken back before the next one is collected.** The per-test
2135
- comparison cannot see a write made while a spec file was being imported or collected, or in its
2136
- `afterAll` — all three happen outside any test, and the file that then fails to collect is the
2137
- innocent one. `setupAutoSpy()` therefore also checks at the moment it is called, which is the one
2138
- seam Vitest leaves between files: the key is deleted, a line goes to **stderr** naming it, and
2139
- **nothing fails** — the file that would fail is not the file that wrote it. A key that refuses to be
2140
- deleted is adopted into the baseline rather than reported once per file for the rest of the run. The
2141
- in-file check still throws at its default grade; the file's own baseline is taken in `beforeAll` and
2142
- re-checked after every `afterAll`, so a write made in either is covered as well.
2143
-
2144
- ### Naming the test that left an attribute on `<body>`
2145
-
2146
- ```ts
2147
- setupAutoSpy({ documentPollution: 'throw' }); // default 'off'; 'throw' under preset: 'strict'
2148
- setupAutoSpy({ documentPollution: { reaction: 'throw', nodes: true, ignoreAttributes: [/^data-cdk-/], ignoreNodes: 'style' } });
2149
- ```
2150
-
2151
- Under `isolate: false` every spec file in a worker shares one document. An attribute a component set
2152
- on `<body>` — `renderer.setAttribute(document.body, 'data-reset-focus', '')` in an `effect` — that
2153
- nothing took off changes the branch a later file's code takes, so that file fails, only when the two
2154
- share a worker and never alone. The guard records the attributes of `<html>`, `<head>` and `<body>`
2155
- before each test and compares them from `onTestFinished` — **after** the TestBed destroyed the
2156
- fixtures in its own `afterEach`, so what a component removes in `ngOnDestroy` / `DestroyRef.onDestroy`
2157
- is never reported. Every attribute added, changed or removed is named with both values, put back,
2158
- and fails that test. An empty `class` or `style` reads as the attribute being absent, in both
2159
- directions — `classList.add` followed by `remove` leaves `class=""` where there was none and nothing
2160
- observable changed; any other empty value, `data-reset-focus=""` included, is still reported.
2161
- A change made in a `beforeAll` and never undone fails the file, checked from a
2162
- `beforeAll` cleanup after every `afterAll`. `nodes: true` also watches the child elements of `<head>`
2163
- and `<body>`; it is off by default because a module that injects a stylesheet on first import does
2164
- so once per worker. Blind spots: a write made while the spec file is imported, and a fixture kept
2165
- alive by `teardown: { destroyAfterEach: false }` (reported against the test that rendered it).
2166
- `guardDocumentPollution(option)` from `/setup` registers the same check on its own. Vitest only, like
2167
- every `/setup` guard — `bun:test`, `node:test` and Rstest have no setup entry to host it.
2168
-
2169
- ### Failing on console output nothing absorbed
2170
-
2171
- ```ts
2172
- setupAutoSpy({ strayConsole: 'throw' }); // default 'off'; 'warn' prints the same report without failing
2173
- ```
2174
-
2175
- Any call to a console method that writes (`log`, `info`, `warn`, `error`, `debug`, `trace`, `table`,
2176
- `dir`, `dirxml`, `timeLog`, `timeEnd`, `count`; `group` with a label; a falsy `assert`) made during a
2177
- test that nothing absorbed fails **that test**, quoting the method, the first three lines and the first
2178
- frame outside `node_modules`, and naming the spy that absorbs it:
2179
-
2180
- ```
2181
- [vitest-auto-spy] "CartService > reports a failed load" wrote to console.error 1 time and nothing absorbed it:
2182
- - console.error: Error: load failed
2183
- at CartService.load (src/app/cart.service.ts:41:15)
2184
- Absorb what the test expects — installConsoleSpies() in a beforeEach, then assert consoleErrorSpy — or fix the code if the output is a defect.
2185
- Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/setup#_16-console-output-nothing-absorbed
2186
- ```
2187
-
2188
- - **Absorbed** means the call never reached the console: a `/console` spy installed for the test or
2189
- the file, `vi.spyOn(console, m).mockImplementation(…)`, any replacement that does not call
2190
- through. A bare `vi.spyOn(console, m)` calls through and **counts** — the report says so and names
2191
- `.mockImplementation(() => undefined)`; a method with no `/console` spy (`table`, `dir`) gets a
2192
- silent `vi.spyOn` instead.
2193
- - **This library's own `'warn'`-grade reports are not stray console output.** `guardGlobals`,
2194
- `prototypePollution`, `unconfiguredReads`, `swallowedStrictCalls`, `misconfiguration`, the
2195
- duplicate-copy report, the skipped-teardown net and the `test.concurrent` notice write past the
2196
- guard's wrapper, so `'warn'` stays a warning under `strayConsole: 'throw'` instead of failing the
2197
- test it is advising about. A `vi.spyOn(console, 'warn')` a test installs still absorbs them — the
2198
- channel checks that the guard's own wrapper is the installed method and steps aside when anything
2199
- sits on top of it.
2200
- - Output **outside any test** — collection/import, `beforeAll` / `afterAll`, a callback after its
2201
- test ended — fails the **file** at its end, in the same sweep as the timer and listener reports. The
2202
- report names the phase — `while the file was being imported`, `in a beforeAll`,
2203
- `after a test had ended`, or `outside any test` when they mix — and gives that phase's advice: for
2204
- import-time output, the module and the line to fix. A recognised line (`NG0912`, any `NGxxxx`)
2205
- adds a `Likely cause:` block. It always names the file that wrote the output: when that file's end
2206
- never ran (its own `afterAll` threw), the next file reports it and says
2207
- `It is reported at the end of b.spec.ts: the file-end check of a.spec.ts did not run.`
2208
- - Every console method a test replaced is put back after it; one a file replaced, after the file.
2209
- - Under the guard, **importing `vitest-auto-spy/console` installs nothing** — call
2210
- `installConsoleSpies()` in a `beforeEach` (or at the top of the file). Spies an import installed
2211
- before the guard armed are taken off.
2212
- - `strayConsole: { allow: ['…', /…/] }` is the last resort, for a line from a dependency no spec can
2213
- reach — the import-time report offers it, with the pattern, only when the quoted frame is inside
2214
- `node_modules`. Output written to `process.stdout` / `process.stderr` directly, and jsdom's own
2215
- virtual console, is not seen. `NG0912` is not noise to allow: it means two copies of one component
2216
- in the bundle, fixed by importing the component from one place.
2217
- - The wrapper forwards every call unchanged, so Vitest's attribution and `onConsoleLog` still work.
2218
- `guardStrayConsole(reaction)` registers the same guard on its own.
2219
-
2220
- ### One grade for everything — `preset: 'strict'`
2221
-
2222
- ```ts
2223
- setupAutoSpy({ preset: 'strict' });
2224
- ```
2225
-
2226
- Sets `duplicateCopies`, `propsOutsideHooks`, `guardGlobals`, `prototypePollution`, `documentPollution`,
2227
- `strayConsole` and `misconfiguration` to `'throw'`, turns `strayTimers` on, and `strayRejections` on when zone.js is
2228
- loaded. An option passed alongside still wins. **Not** included: `strict` (strict doubles change what
2229
- an unconfigured call returns — a semantic switch, not a grade; the name was taken, hence `preset`) and
2230
- `unconfiguredReads`, its read side (survey with `onUnstubbedRead` before turning it on),
2231
- `blockNetwork` (changes the code under test), `restoreMocks` (drops `beforeAll` spies), failing on
2232
- stray-timer counts (the sweep fails the file from `afterAll`, and a callback scheduled after the
2233
- previous file's sweep is charged to the next — opt in with `onStrayTimers: 'throw'`, whose message
2234
- names the file and the test that scheduled each one, or `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`), and
2235
- `enableAngularDiagnostics()`, which lives in `/angular/diagnostics` — call it in the same setup file as the Angular half of strict.
2236
-
2237
- `misconfiguration: 'throw'` on its own makes the library's misuse reports — an `onlyMethodsToSpyOn`
2238
- typo, `gettersToSpyOn` naming a method, a `returns` key no spy answers to, `injectSpy` handed a real
2239
- instance, a `jasmine.DEFAULT_TIMEOUT_INTERVAL` write, `providedMethodNames`, a `nextWithValues()` on an
2240
- observable property something already subscribed to — throw at the call site,
2241
- every occurrence. The grade is process-wide and released after the file. The printed grade of the
2242
- `injectSpy` warning is de-duplicated per token **per spec file**, no longer per worker.
2243
-
2244
- `withoutStrayTimerTracking(work)` runs setup work whose timers `strayTimers` neither counts nor
2245
- cancels — jsdom answers every Web Storage write with a real `setTimeout(…, 0)`, and the library's own
2246
- storage probe runs under it.
2247
-
2248
- **`test.concurrent` earns one warning per worker, and it is worth reading rather than silencing.**
2249
- The per-test guards assume one test at a time: the document snapshot, the console window and the
2250
- unconfigured-read counter are opened and judged per test, so with two in flight a finding can be
2251
- charged to the other one — or cleared before anything sees it. The restores still run for every
2252
- test, and the skipped-teardown net now remembers per test rather than per file, which is the half
2253
- that was simply wrong. Run the files that need a guard sequentially, or keep `test.concurrent` for
2254
- files whose setup passes `strayConsole: 'off'`, `documentPollution: 'off'` and
2255
- `unconfiguredReads: 'off'`.
2256
-
2257
- **What `/setup` costs per test**, measured on happy-dom over 10 000 empty tests (2026-09-17, Node
2258
- 24): the default `setupAutoSpy()` **19 µs**, `guardGlobals` **59 µs** (it snapshots once per file
2259
- now, not once per test), `documentPollution` **23 µs** — it shares the `onTestFinished` the teardown
2260
- net registers anyway — and `preset: 'strict'` **67 µs**. `documentPollution: { nodes: true }` over a
2261
- `<head>` of 1000 children is **0.19 ms** per test rather than 7.1 ms, because the children are walked
2262
- as a sibling chain instead of through a live `HTMLCollection`; an `ignoreNodes` selector's answer for
2263
- an element is decided at snapshot time and reused for that test.
2264
-
2265
- ### Hook order differs from Jest
2266
-
2267
- Vitest runs `afterEach` hooks as a stack (innermost / last-registered first); Jest ran them in
2268
- declaration order. A ported suite where a spec's `afterEach` depends on a patch the setup file
2269
- installed needs `sequence: { hooks: 'list' }` in the Vitest config, or the setup file's teardown runs
2270
- first and the spec's hook operates on an already-restored environment.
1541
+ <!-- /agents-outline -->
2271
1542
 
2272
1543
  ---
2273
1544
 
@@ -2276,13 +1547,13 @@ first and the spec's hook operates on an already-restored environment.
2276
1547
  Under Jest these were hard to tell apart; under Vitest with a real bundler they are four separate
2277
1548
  mechanisms, and a test that waits on the wrong one fails with a message that names none of them.
2278
1549
 
2279
- | What is pending | What drives it | What does **not** |
2280
- | ------------------------------------------------- | -------------------------------------------------- | ----------------------------------------- |
2281
- | change detection | `fixture.detectChanges()` | anything `await`ed |
2282
- | effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
2283
- | timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
2284
- | a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
2285
- | an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
1550
+ | What is pending | What drives it | What does **not** |
1551
+ | --- | --- | --- |
1552
+ | change detection | `fixture.detectChanges()` | anything `await`ed |
1553
+ | effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
1554
+ | timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
1555
+ | a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
1556
+ | an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
2286
1557
 
2287
1558
  ```ts
2288
1559
  import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
@@ -2495,8 +1766,9 @@ per `flush()`. A callback that throws stops `flush()` (or propagates out of `req
2495
1766
  in `'immediate'` mode); pass `onError: (error) => { … }` to intercept it instead — rethrow from
2496
1767
  inside to keep the default for an error you did not mean to swallow. `stubElementRect` returns a real
2497
1768
  `DOMRect` per call, edges derived, plus the installed spy as `.getBoundingClientRect` for
2498
- `expect(el.getBoundingClientRect).toHaveBeenCalled()`, and the return value is still callable as the
2499
- undo. Both go through `mockValueProp`, so `restoreMockedProps()` (and `setupAutoSpy()` between tests)
1769
+ `expect(stub.getBoundingClientRect).toHaveBeenCalled()`, and the return value is still callable as the
1770
+ undo. The box is the spy's creation implementation, so `vi.resetAllMocks()` / `mockReset()` mid-test
1771
+ keeps it on both spy engines; Bun's `mockReset()` drops it (re-stub after a reset there). Both go through `mockValueProp`, so `restoreMockedProps()` (and `setupAutoSpy()` between tests)
2500
1772
  takes them off; `frames.restore()` does it sooner.
2501
1773
 
2502
1774
  ### A module mock that did nothing
@@ -2552,1316 +1824,38 @@ a fixture between the factory and the tests with `vi.hoisted()`.
2552
1824
 
2553
1825
  ## 13. Angular
2554
1826
 
2555
- ```ts
2556
- import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
2557
-
2558
- TestBed.configureTestingModule({
2559
- providers: [provideAutoSpy(MyService), provideAutoSpy(ApiService, { methodsToSpyOn: ['get'] })],
2560
- });
2561
-
2562
- const myService = injectSpy(MyService); // Spy<MyService>
2563
- ```
2564
-
2565
- `provideAutoSpy` defaults to `lazySpies: true` (the plain `createSpyFromClass` does not). Pass
2566
- `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
2567
- zone.js alike. The entry needs **Angular >= 20** (§1); on 16 or 17 it does not link at all, because
2568
- `ɵSIGNAL` is not there to import.
2569
-
2570
- **Niche Angular helpers ship in narrow companion subpaths, never in `/angular`** — the pattern of
2571
- `/angular-http`, `/angular-router` and `/signal-forms`, and since 5.21.0 of `/angular/diagnostics`,
2572
- `/angular/doubles` and `/angular/matchers`. An import of `/angular` evaluates its whole graph, so a
2573
- helper a suite names once in a setup file must not ride along with every `provideAutoSpy` import;
2574
- the diagnostics, doubles and matcher registrars left on that rule (`trackInjections` stays —
2575
- `createWithAutoSpies` needs its module).
2576
-
2577
- ### The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
2578
-
2579
- The block above, written once instead of once per dependency, with the types inferred rather than
2580
- declared:
2581
-
2582
- ```ts
2583
- import { test as base } from 'vitest';
2584
- import { extendWithAutoSpies } from 'vitest-auto-spy/angular';
2585
-
2586
- const test = extendWithAutoSpies(base, {
2587
- cart: CartService,
2588
- api: [ApiService, { returns: { get: of([]) } }],
2589
- passcode: PASSCODE_TOKEN,
2590
- });
2591
-
2592
- test('checks out', async ({ cart }) => {
2593
- cart.checkout.resolveWith(true);
2594
-
2595
- await expect(cart.checkout(1)).resolves.toBe(true);
2596
- });
2597
- ```
2598
-
2599
- No `let cart: Spy<CartService>` that is `undefined` between tests, and a test that never destructures
2600
- `api` never builds it. Entries are a class, `[Class, config]` with whatever `provideAutoSpy` takes,
2601
- or an `InjectionToken` (built from the token's own type, like `provideAutoSpyForToken`). Extra
2602
- providers — the component under test, a real service, `provideHttpClient()` — go in the third
2603
- argument and are registered in the same call, ahead of the generated ones, so a token named there
2604
- wins.
2605
-
2606
- **It takes the whole map at once, and that is a `TestBed` constraint rather than a typing one.** The
2607
- composing form — `base.extend('cart', …).extend('api', …)` — cannot work: fixtures resolve
2608
- independently, so `cart` would configure the testing module _and_ inject, which instantiates it, and
2609
- `api` would then reach `configureTestingModule` after instantiation and fail with Angular's own
2610
- "Cannot configure the test module when the test module has already been instantiated". A `beforeEach`
2611
- that configures the module further still composes, because it runs before any fixture resolves and
2612
- repeated `configureTestingModule` calls merge right up until the first injection. A `beforeEach` that
2613
- **injects** does not, and nothing can repair that from here.
2614
-
2615
- The token may be an **abstract class** — `abstract class LocalStorage extends AbstractStorage {}`,
2616
- the shape production provides with `useClass`. Its members are erased before they reach a prototype,
2617
- so there is nothing to discover; the factory notices and returns the `createAutoMock` proxy, which
2618
- answers every method of the declared type. `injectSpy(LocalStorage)` recognises it as an auto-spy
2619
- and stays quiet.
2620
-
2621
- **Seed the double in the provider, not in the `beforeEach` under it.** Both factories take both
2622
- halves — `returns` for what a spied method answers, `overrides` for a member that is not a method
2623
- result:
2624
-
2625
- ```ts
2626
- provideAutoSpy(FavoritesService, {
2627
- returns: { load: of([]) },
2628
- overrides: { savedItemsChanged$: of(undefined), favoriteItems: [] },
2629
- });
2630
-
2631
- provideAutoSpyForToken(PRODUCTS, undefined, { returns: { getProducts: of([]), getById: of(null) } });
2632
- ```
2633
-
2634
- A seeded `overrides` member is stored verbatim and is **no longer a spy**, so seed data there and
2635
- name methods in `returns` when they must stay assertable. The reason to prefer this over a second
2636
- statement is not brevity: the shortcut people take instead is an exported `const` provider carrying
2637
- the values, and under `isolate: false` that is one set of spies shared by every file that imports
2638
- it.
2639
-
2640
- **A member named in both wins as the seed**, whatever the seed is — a value, a plain function, a
2641
- `vi.fn()`. `returns` and `selfReturning` skip such a member rather than configuring it, which is the
2642
- precedence the merge already applies between a registration and a call site: a spec seeding `channel`
2643
- over `registerAutoSpyDefaults(LOGGER, { selfReturning: ['channel'] })` gets its own function back, not
2644
- the registered link.
2645
-
2646
- **`observablePropsToSpyOn` works on a token too**, and matters more there than on a class. A class
2647
- tells the factory which members are methods; a type does not, so _every_ unnamed key of a
2648
- token-driven double is a function spy — including an `Observable` property, which the code under
2649
- test then subscribes to as if it were a function, failing far from the double:
2650
-
2651
- ```ts
2652
- provideAutoSpyForToken(FAVORITES, undefined, { observablePropsToSpyOn: ['favorites$'] });
2653
- // …
2654
- injectSpy(FAVORITES).favorites$.nextWith([{ id: 1 }]);
2655
- ```
2656
-
2657
- A member also named in `overrides` keeps its seed — hand the double a real `Subject` there when the
2658
- spec drives the stream itself, and name it here when `nextWith` is what the spec wants. That is the
2659
- same precedence the class-based factory uses. Before 3.5.0 this option existed only on the class
2660
- path, and reaching a token with observable members meant going back to a hand-written double —
2661
- which is exactly what `prefer-provide-auto-spy` and `prefer-create-spy-from-class` exist to prevent.
2662
-
2663
- **Do not write a local `injectSpy`.** A wrapper of the shape
2664
- `TestBed.inject(token as never) as Spy<T>` — a double assertion, typed
2665
- `<T>(token: abstract new (...args: never[]) => T)` — is a common thing to find already in a
2666
- repository, and the library's is strictly wider: it takes `ClassType<T>`, an `InjectionToken<T>` and
2667
- an abstract constructor, warns when the injector hands back something that is not a spy, and has no
2668
- assertion for the project's lint rules to argue with. Two functions with the same name and different
2669
- signatures means the import order decides which one a file gets. Delete the local one, or re-export
2670
- the library's under that name.
2671
-
2672
- ### Signals — which helper depends on whose signal it is
2673
-
2674
- ```ts
2675
- // a DEPENDENCY's signal — name it, then configure the mock like any other
2676
- provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current'] });
2677
- injectSpy(ProjectStore).current.mockReturnValue({ id: 1 });
2678
-
2679
- // the CLASS UNDER TEST's own signal / computed / input — replace the field with a REAL signal
2680
- mockReadonlyProp(component, 'selected', signal(true));
2681
- mockReadonlyProp(component, 'items', signal([]));
2682
- mockReadonlyProp(component, 'host', signal({ nativeElement: element }));
2683
-
2684
- // a value that changes during the test — keep the signal, set it
2685
- const selected = signal(false);
2686
-
2687
- mockReadonlyProp(component, 'selected', selected);
2688
- selected.set(true); // every computed reading it updates
2689
- ```
2690
-
2691
- Pass a real `signal()`, not a `vi.fn()` returning a value — anything `computed()` downstream has to
2692
- recompute, and only a real signal notifies it.
2693
-
2694
- `mockSignalProp` is that pair in one call, and hands back the writable half:
2695
-
2696
- ```ts
2697
- import { mockSignalProp } from 'vitest-auto-spy/angular';
2698
-
2699
- const selected = mockSignalProp(component, 'selected', false);
2700
-
2701
- selected.set(true); // every computed reading it updates
2702
- ```
2703
-
2704
- Use it whenever the value has to change during the test. `mockReadonlyProp` stays right when the
2705
- value is fixed for the whole test and you never need the handle.
2706
-
2707
- A `protected` or TS-`private` signal is reached too: the key is checked against the public type
2708
- first, and a key that type does not describe takes the value's own type instead —
2709
- `mockSignalProp(component, 'saving', true)`. `mockResourceProp` works the same way. A public key never
2710
- falls through to that form, so its value is still checked.
2711
-
2712
- A `linkedSignal()` is a writable signal like any other, so `mockSignalProp` covers it and there is
2713
- no separate helper to go looking for.
2714
-
2715
- **An ngrx `signalStore()`'s state members go through `mockSignalProp` too**, on the store double —
2716
- `mockSignalProp(store, 'currentAngle', null)`, then `.set(angle)` mid-test. A state slice is a
2717
- signal the template reads during the first render; a method spy there answers `undefined`, and
2718
- `mockImplementation(() => value())` is re-read by nothing downstream. A nullable object slice is
2719
- typed `DeepSignal<A> | Signal<null>`: `overrides` accepts one `signal<A | null>()` for it, but only
2720
- `mockSignalProp` hands back the handle.
2721
-
2722
- **It patches as little as it can, and the order rule follows from that.** A member that is already
2723
- writable — `signal()`, `model()`, `linkedSignal()` — is **written through**, not replaced: Angular
2724
- links a consumer to the signal it read, not to the property it read it through, so replacing one
2725
- that anything has already read strands every `computed()`, `effect()` and template binding on the
2726
- old signal, silently and for the rest of the test. Writing through keeps all of them, keeps a
2727
- `model()`'s output half alive, and leaves nothing for `restoreMockedProps()` to undo — the value
2728
- stays where the spec left it.
2729
-
2730
- **A `signal().asReadonly()` member counts as writable.** The view a class exposes is a different
2731
- function from the signal behind it, but it is the same reactive node, so the value is set through the
2732
- node and the member the class publishes keeps working — whenever it was first read. That is the
2733
- common shape for a store's public state (`readonly items = this.#items.asReadonly()`), and it used to
2734
- be swapped, which meant a `computed()` that had already read it never saw the new value.
2735
-
2736
- One shape is still swapped: a `computed()` the class declares, and a member the double does not have
2737
- yet. **Patch those before anything reads them** — before the first `detectChanges()` /
2738
- `stable(fixture)`. A `computed()` a live consumer has already read is refused by name rather than
2739
- replaced where nothing would notice it — the refusal now names `computed()` specifically, and says
2740
- that driving the signal the computation reads is the way through. An `input()` is refused outright:
2741
- Angular sets an input through the input node rather than the property, so a replaced one breaks the
2742
- host's next write. Drive an input with `await setInputs(fixture, { … })`, or with
2743
- `renderShallow(Component, { inputs: { … } })` for the value it starts at.
2744
-
2745
- ### Observers the component constructs itself
2746
-
2747
- Do not assign `globalThis.IntersectionObserver` by hand: it stays assigned, and under
2748
- `isolate: false` the next file inherits it.
2749
-
2750
- ```ts
2751
- import { intersectionEntry, stubIntersectionObserver } from 'vitest-auto-spy/dom-stubs';
2752
-
2753
- const observers = stubIntersectionObserver(); // also stubResizeObserver / stubMutationObserver
2754
-
2755
- fixture.detectChanges(); // the component constructs it
2756
-
2757
- observers.last.emit([intersectionEntry(element, true)]); // one batch, as the browser delivers it
2758
- await fixture.whenStable();
2759
-
2760
- expect(observers.last.disconnected).toBe(true); // after the component is destroyed
2761
- ```
2762
-
2763
- `restoreMockedProps()` puts the real constructor back, so `setupAutoSpy()` covers the teardown.
2764
- `observers.last` throws if the code under test constructed nothing — render first, and install the
2765
- stub before the construction, not after.
2766
-
2767
- Install it in `beforeEach`, **never in `beforeAll`**: a shared setup file's root `beforeEach` runs
2768
- _after_ a file's `beforeAll`, so a stub installed there is overwritten by the setup file's default
2769
- before the test starts. The symptom is `expected "vi.fn()" to be called 2 times, but got 0 times`,
2770
- in a file where the mock class is ten lines above.
2771
-
2772
- Three more knobs, each for a shape that otherwise gets hand-rolled:
2773
-
2774
- ```ts
2775
- stubIntersectionObserver({ autoEmit: true }); // every observed target reports as visible, at once
2776
- observers.last.options; // the init object: { rootMargin, threshold, … }
2777
- observers.last.host; // the observer the code under test constructed — and its callback's 2nd argument
2778
- observers.last.emit([mutationRecord(host, { addedNodes: [span] })]);
2779
- observers.last.emit([resizeEntry(host, { width: 320 })]);
2780
- ```
2781
-
2782
- **The callback's second argument is the observer the code under test holds**, not an internal
2783
- record — so `(entries, observer) => observer.unobserve(entries[0].target)`, `observer.disconnect()`
2784
- and `observer.takeRecords()` all reach the thing the spec drives, which is what production code
2785
- written against the platform expects. `root`, `rootMargin` and `thresholds` read back off the init
2786
- the way the platform normalises them (`null`, `'0px 0px 0px 0px'`, `[0]` by default) rather than as
2787
- empty values.
2788
-
2789
- `autoEmit` is the mode a suite ported from Jest needs: there the global mock fired its callback with
2790
- `isIntersecting: true` immediately, so lazily-loading sections fetched their data during
2791
- `detectChanges()`. Against the default inert observer those specs assert on an empty component and
2792
- fail with something that has nothing to do with intersection.
2793
-
2794
- `mutationRecord()` exists because a `MutationRecord` cannot be written as an object literal at all —
2795
- `addedNodes` is a `NodeList`. Do not build one from a `DocumentFragment`: appending **moves** the
2796
- nodes, so the helper silently rips the element out of the fixture it was just asserted on.
2797
-
2798
- ### A component's own `providers` win, and the symptom is nowhere near the cause
2799
-
2800
- Worth reading before the rest of this section: it has now come up twice in one migration wave, and
2801
- both times the failure landed in a different file from its cause.
2802
-
2803
- `@Component({ providers: [DeleteAccountService] })` declares the provider on the **element**
2804
- injector, and a module-level `provideAutoSpy(DeleteAccountService)` in `configureTestingModule`
2805
- loses to it — so the component builds the **real** service. Nothing warns. What fails is whatever
2806
- the real service touches first: in the observed case a logger, with
2807
- `TypeError: Cannot read properties of undefined (reading 'pipe')`, which names neither the component
2808
- nor the provider nor the spy.
2809
-
2810
- Two things fix it, and which one depends on whether the double is wanted:
2811
-
2812
- ```ts
2813
- // keep a double, but put it where the component will look
2814
- const menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService);
2815
-
2816
- // or take the component's own provider away, so the module-level one is reached again
2817
- TestBed.overrideComponent(ProfileComponent, { remove: { providers: [DeleteAccountService] } });
2818
- ```
2819
-
2820
- `overrideComponentProvider` is the one to reach for by default — it also queues the component with
2821
- the TestBed compiler, which `overrideProvider` alone does not do. Reach for the `remove` form when
2822
- the module already provides the spy and the component's declaration is simply in the way.
2823
-
2824
- Under `renderShallow` the override goes in `beforeCreate`, which runs after the testing module is
2825
- configured and before the component is created:
2826
-
2827
- ```ts
2828
- let menu: Spy<NavigationBuilderService>;
2829
-
2830
- renderShallow(CatalogPageComponent, {
2831
- beforeCreate: () => {
2832
- menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService);
2833
- },
2834
- });
2835
- ```
2836
-
2837
- ### `injectSpy` cannot reach a component-level provider
2838
-
2839
- `injectSpy(X)` reads the **global** `TestBed` injector. A provider declared on the component
2840
- (`@Component({ providers: [...] })`) lives in the element injector, which `TestBed.inject` never
2841
- sees. Go through the fixture and re-view the result:
2842
-
2843
- ```ts
2844
- const player = asSpy(fixture.debugElement.injector.get(PlayerService));
2845
-
2846
- player.play.mockReturnValue(true);
2847
- ```
2848
-
2849
- To _replace_ it rather than read it, the provider has to be overridden — and `provideAutoSpy` cannot
2850
- do that, because a testing-module provider loses to one the component declares:
2851
-
2852
- ```ts
2853
- import { overrideAutoSpy, overrideComponentProvider } from 'vitest-auto-spy/angular';
2854
-
2855
- const menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService); // → Spy<NavigationBuilderService>
2856
-
2857
- // or, when the component is already in the testing module:
2858
- TestBed.configureTestingModule({ … }).overrideProvider(PaymentMethodService, overrideAutoSpy(PaymentMethodService));
2859
- ```
2860
-
2861
- `overrideProvider(X, provideAutoSpy(X))` is **not** broken, contrary to what this section used to
2862
- say: `provideAutoSpy` returns `{ provide, useValue }`, `overrideProvider` reads the `useValue` off
2863
- it and ignores the extra `provide`, and the spy is installed. `overrideAutoSpy` is the right call
2864
- because it says what it does and hands the spy back directly — not because the other form is a
2865
- no-op.
2866
-
2867
- The failure that is real: `overrideProvider` only reaches a component the TestBed compiler knows
2868
- about, so a standalone component instantiated through a parent's template needs to be in `imports`
2869
- first; `overrideComponentProvider` queues it.
2870
-
2871
- **`overrideComponentProvider` verifies itself**, always — not behind a diagnostics flag. On the next
2872
- `TestBed.createComponent` it asks the component's **own** injector for the token and throws when the
2873
- answer is not the spy it created, naming the component, the token and what was resolved instead. It
2874
- checks the **first** fixture only and stays silent when the component was not rendered (behind an
2875
- `@if`, a different host), so a throwaway fixture created first means "not checked", not "wrong". A
2876
- later `TestBed.overrideProvider(Token, …)` still wins; the check reports that but cannot prevent it.
2877
- `overrideAutoSpy` carries no verification of its own.
2878
-
2879
- Do **not** reach for `TestBed.overrideComponent` here — see the next subsection for why it is worse
2880
- than the problem it solves.
2881
-
2882
- ### An NgModule that contributes nothing
2883
-
2884
- Under an AOT test bundle (`@angular/build:unit-test`, and any builder that compiles specs the way it
2885
- compiles production code) `ɵɵsetNgModuleScope` is stripped, because only the TestBed reads it. Every
2886
- NgModule then has an empty `ɵmod.declarations` / `ɵmod.exports` at runtime. Nothing notices while
2887
- AOT is in charge — the flat dependency list is already baked into each `ɵcmp` — but the moment the
2888
- TestBed resolves a scope itself, through `imports: [SomeModule]` or through a JIT recompilation
2889
- after `overrideComponent`, it resolves it from nothing:
2890
-
2891
- ```
2892
- NG0303: Can't bind to 'appTruncate' since it isn't a known property of 'div'
2893
- NG0301: Export of name 'focusable' not found!
2894
- NG0304: 'ui-smart-row' is not a known element
2895
- (nothing at all — an attribute directive simply never instantiates)
2896
- ```
2897
-
2898
- None of the four names the module. Say so up front instead:
2899
-
2900
- ```ts
2901
- import { assertNgModuleScopes } from 'vitest-auto-spy/angular';
2902
-
2903
- assertNgModuleScopes(DirectivesModule, PipesModule); // throws, naming the module and the cause
2904
- ```
2905
-
2906
- Then declare what the spec needs in the TestBed module directly. Pass only modules you expect to
2907
- bring declarations — a providers-only module is legitimately empty.
2908
-
2909
- ### A component whose own definition has a hole in it
2910
-
2911
- The same bundle, one level down. A component's `providers`, `viewProviders` and compiled scope are
2912
- **baked into `ɵcmp` when its module executes**, not read at `createComponent` time — so a barrel
2913
- split into a chunk that has not run yet produces a definition with `undefined` in those lists.
2914
- Angular finds out much later, from inside itself, with a stack that names neither the barrel nor the
2915
- component:
2916
-
2917
- ```
2918
- TypeError: Cannot read properties of undefined (reading 'provide')
2919
- ❯ resolveProvider render3/di_setup.ts:95
2920
- ```
2921
-
2922
- Both obvious cures fail: `await import('@scope/lib')` at the top of `beforeEach` is already too late,
2923
- and a static import at the top of the spec does not fix the order this bundler emits. Worse, the spec
2924
- that breaks is one nobody touched — chunk boundaries move with file _contents_, so editing a type
2925
- next door is enough.
2926
-
2927
- ```ts
2928
- import { assertComponentDefIntact } from 'vitest-auto-spy/angular';
2929
-
2930
- assertComponentDefIntact(HoverMenuComponent); // HoverMenuComponent.ɵcmp.providers[0] is undefined
2931
- const fixture = TestBed.createComponent(HoverMenuComponent);
2932
- ```
2933
-
2934
- It walks the three lists, nested arrays and forward-reference thunks included, and answers the
2935
- related `Cannot read properties of undefined (reading 'ɵcmp')` from `imports: [Cmp]` as well — there
2936
- the class reference itself never arrived, and the message names the argument position. Directives
2937
- (`ɵdir`) are checked the same way. Neither this nor `assertNgModuleScopes` fixes the build; both
2938
- replace a stack inside `@angular/core` with a line naming what is missing.
2939
-
2940
- ### Four silent failures, as one setup line
2941
-
2942
- ```ts
2943
- // vitest.setup.ts — AFTER getTestBed().initTestEnvironment(…), because Vitest runs
2944
- // afterEach hooks in reverse registration order and this one must run before the teardown.
2945
- import { enableAngularDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
2946
-
2947
- enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders, pendingRequests, shadowedProviders }
2948
- ```
2949
-
2950
- | Member | Fails when |
2951
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2952
- | `ngModuleScopes` | a testing module imports an NgModule that contributes nothing at runtime |
2953
- | `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
2954
- | `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
2955
- | `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one; `{ ignoreCancelled: true }` forgives the ones the code under test cancelled |
2956
- | `shadowedProviders` | a double on the testing module loses to the component's own `providers` |
2957
-
2958
- Every member defaults to `true`, takes `false` to opt out, and a second call **replaces** the
2959
- selection (safe from anywhere, including inside a test). `disableAngularDiagnostics()` turns the
2960
- group off and leaves the TestBed timing instrumentation alone. `assertNoShadowedProviders(Component, fixture)`
2961
- and `assertNoPendingRequests({ ignoreCancelled }?)` are the same checks on their own, for a spec that renders through a
2962
- helper of its own or wants the HTTP one mid-test — reading takes the requests, so the group will not
2963
- re-report what you inspected.
2964
-
2965
- Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
2966
- `provideHttpClientTesting()` / `HttpClientTestingModule` — including one reached through a nested
2967
- module, which is the usual `imports: [SharedTestingModule]` shape and used to make `pendingRequests`
2968
- inert without saying so — and a project using neither is genuinely inert. `ngModuleScopes` only fires
2969
- on a module that contributes **nothing at all** — a providers-only module is legitimately
2970
- scope-empty, so a stripped scope that still has providers passes silently; hand-call
2971
- `assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
2972
- not fire when `declarations` is non-empty.
2973
-
2974
- **`getTestBed()` is covered as well as `TestBed`.** The instrumentation sits on the TestBed
2975
- _instance_ every static delegates to, so the configuration inspectors, `shadowedProviders` and the
2976
- `overrideComponentProvider` verification all see `getTestBed().configureTestingModule(…)` and
2977
- `getTestBed().createComponent(…)` — the shape a setup helper of your own usually writes — and
2978
- `overrideTemplate` is counted with the rest of the overrides. A suite on `getTestBed()` used to get
2979
- silence from every one of these.
2980
-
2981
- Two more things now judge what Angular ends up with rather than one call: `deadSchemas` tallies
2982
- `schemas` and `declarations` across **every** `configureTestingModule` of a test, because Angular
2983
- accumulates them — a schema added in a second call next to a component declared in the first is no
2984
- longer reported, and the reverse order is no longer silently passed — and a test that resets the
2985
- testing module twice reports the requests of every module it built, not only the last.
2986
-
2987
- `enableAngularDiagnostics()` and `mockSignalProp()` both check once per worker that the Angular
2988
- internals they read are still there, and **throw** naming the installed `@angular/core` version when
2989
- one has moved. Nothing about that is fixable from a spec: the throw tells you which shape went and
2990
- what stopped working, and the repair is a release of this package.
2991
-
2992
- ### `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
2993
-
2994
- ```ts
2995
- import { expectRequest, provideHttpTesting } from 'vitest-auto-spy/angular-http';
2996
-
2997
- TestBed.configureTestingModule({ providers: [...provideHttpTesting()] });
2998
-
2999
- const products = TestBed.runInInjectionContext(() => httpResource<Product[]>(() => '/api/products'));
3000
-
3001
- await expectRequest('/api/products').flush([product]);
3002
-
3003
- expect(products.value()).toEqual([product]); // no tick, no microtask, no detectChanges
3004
- ```
3005
-
3006
- `expectRequest` does the six steps a zoneless HTTP spec otherwise writes by hand: tick (an
3007
- `httpResource()` created in an injection context has issued **nothing** until something does), inject
3008
- the `HttpTestingController`, find the one matching request, flush it, let one microtask run so the
3009
- response reaches the resource, tick again so the view reading it is current. Miss the first and
3010
- `expectOne` reports a request that was never sent; miss the fifth and the assertion reads the
3011
- resource's **default** value and passes.
3012
-
3013
- | Call | Does |
3014
- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3015
- | `provideHttpTesting(opts?)` | `provideHttpClient()` + `provideHttpClientTesting()`; `{ verifyOnTeardown }` defaults to `true`, and takes `{ ignoreCancelled }` instead of `false` |
3016
- | `expectRequest(matcher, opts?)` | tick, then the one match — `.request`, `.flush(body, opts?)`, `.error(status, opts?)` |
3017
- | `expectNoRequest(matcher?, opts?)` | tick, then assert nothing matched; no argument means "nothing at all was requested" |
3018
- | `verifyNoPendingRequests(opts?)` | the teardown check by hand, for mid-test use or a suite with `verifyOnTeardown: false`; `{ ignoreCancelled: true }` forgives a request the code under test unsubscribed from |
3019
-
3020
- `matcher` is a URL (matched against either `url` or `urlWithParams`), a `RegExp` over
3021
- `urlWithParams`, or a predicate `(request) => boolean`. `{ method }` is case-insensitive.
3022
- `flush()` and `error()` are `async` because settling requires letting a microtask run — `await` them,
3023
- and the next line reads the settled value.
3024
-
3025
- Three things to know before reaching for it:
3026
-
3027
- - **It does not re-export the core** — nor does `/angular-router`; every other subpath does. It is a
3028
- companion to `vitest-auto-spy/angular`, which stays the import for spies, `TestBed` helpers and
3029
- `settleResource`.
3030
- Staying narrow is what keeps `@angular/common` — an **optional** peer — out of every other entry.
3031
- - **`settleResource` is unchanged and still the answer** whenever the wait is not tied to one
3032
- request: a `resource()` with an async loader, an `rxResource`, a reload, anything not HTTP.
3033
- - **`enableAngularDiagnostics({ pendingRequests })` is unchanged and cooperates.** Both take the open
3034
- requests with the one-shot `match(() => true)`, so an unanswered request is reported once. For a
3035
- suite using `provideHttpTesting()` everywhere the diagnostic is redundant; keep it on while any
3036
- file still configures HTTP testing by hand.
3037
-
3038
- `provideHttpTesting({ verifyOnTeardown })` reads which test is running off the runner's own state,
3039
- and where that state is absent — `bun:test`, `node:test`, anything that is not Vitest — the
3040
- end-of-test check cannot arm. It now says so once per worker instead of checking nothing quietly;
3041
- call `verifyNoPendingRequests()` yourself there.
3042
-
3043
- ### `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
3044
-
3045
- ```ts
3046
- import { injectActivatedRoute, provideActivatedRoute } from 'vitest-auto-spy/angular-router';
3047
-
3048
- TestBed.configureTestingModule({
3049
- providers: [provideActivatedRoute({ params: { id: '7' }, queryParams: { tab: 'reviews' } })],
3050
- });
3051
-
3052
- const fixture = TestBed.createComponent(ProductPage);
3053
-
3054
- injectActivatedRoute().setParams({ id: '8' }); // params + paramMap emit; snapshot.params already agrees
3055
- fixture.detectChanges();
3056
- ```
3057
-
3058
- **Do not hand-write an `ActivatedRoute` double, and do not `provideAutoSpy(ActivatedRoute)`.** Its
3059
- streams and its snapshot are instance fields: the spy has neither, and a `useValue` has whichever half
3060
- its author thought of — `route.paramMap.pipe(…)` throws on the `{ snapshot }` one, `route.snapshot`
3061
- is `undefined` on the `{ params: of(…) }` one, and a spec that updates one half by hand tests a route
3062
- no navigation produces. `provideActivatedRoute(init)` provides **Angular's own `ActivatedRoute`**
3063
- built over one record, so every stream, both `ParamMap`s and `snapshot` read the same values.
3064
-
3065
- | Call | Does |
3066
- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3067
- | `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `title`, `fragment`, `url`, `outlet`, `component`, `routeConfig`, `resolve` |
3068
- | `injectActivatedRoute(injector?)` | the handle: `.route`, `setParams`, `setQueryParams`, `setData`, `setFragment`, `setUrl`, `set({ … })` |
3069
- | `createActivatedRoute(init?)` | the same handle without a `TestBed` — for `new Page(route)`, or a guard given `route.snapshot` |
3070
-
3071
- A setter behaves like the router after a navigation: it **replaces** (not merges) that part, builds
3072
- a new snapshot first, then emits only the streams whose value changed (router equality: same keys,
3073
- arrays as sorted sets), in the order `queryParams`, `fragment`, `params`, `url`, `data`. Setting
3074
- an equal value emits nothing; a string `url` is split on `/`.
3075
-
3076
- Four things to know:
3077
-
3078
- - **List it after `provideRouter()`** — the later provider of `ActivatedRoute` wins, and
3079
- `injectActivatedRoute()` throws naming the one that did. With the real router present, the double
3080
- still works as `relativeTo` for `router.createUrlTree`.
3081
- - **One node, no tree.** `root` is the route itself, `parent` / `firstChild` are `null`. For
3082
- `route.parent.params` patch that member with `mockReadonlyProp`, or use `RouterTestingHarness`.
3083
- - **`title` is given, not resolved; no navigation, no input binding.** `provideActivatedRoute({
3084
- title: 'Product 7' })` answers `route.snapshot.title` — the double puts the string under the
3085
- router's own `RouteTitleKey`, read off the installed router, and does not run a `title: () => …`
3086
- resolver. `Router.navigate` does not move it (the setters do), and `withComponentInputBinding()`
3087
- is the outlet's job (`fixture.componentRef.setInput`).
3088
- - **Its own entry, like `/angular-http`.** It does not re-export the core, imports no runner, and is
3089
- the only file that reaches `@angular/router` — an optional peer. The `Location` double below wraps
3090
- `@angular/common/testing`, which `@angular/router` itself depends on, so it adds no peer.
3091
-
3092
- ### `Router` from one URL — `vitest-auto-spy/angular-router`
3093
-
3094
- ```ts
3095
- import { injectRouterDouble, provideRouterDouble } from 'vitest-auto-spy/angular-router';
3096
-
3097
- TestBed.configureTestingModule({ providers: [provideRouterDouble({ url: '/products/7' })] });
3098
-
3099
- const router = injectRouterDouble();
3100
-
3101
- router.emitNavigation('/products/8'); // events emits a NavigationEnd; router.url already reads it
3102
- expect(router.navigate).toHaveBeenCalledWith(['/checkout']);
3103
- ```
3104
-
3105
- **Do not hand-write `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate:
3106
- Promise.resolve(true) } })`** — the line 48 hand-rolled providers across two private suites all
3107
- converged on, and every part of it is a guess the component falls through: `of()` never emits again,
3108
- `url` is a string nobody updates, `routerState` is absent, `serializeUrl` throws on the first
3109
- redirect a guard builds. `provideRouterDouble(init)` keeps one URL and derives the rest.
3110
-
3111
- | Call | Does |
3112
- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
3113
- | `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
3114
- | `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)`, `setCurrentNavigation(navigation?)` |
3115
- | `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
3116
- | `collectRouterEvents(events)` | the recording of what `router.events` emits, `expect([[Class, url], …])` in one line — Angular's own integration-spec idiom |
3117
-
3118
- Unlike the route, this is **not** an instance of Angular's class: a real `Router` drags the whole
3119
- routing stack in. It is a structural double that answers `url` (serialized as the real router does),
3120
- `events` (a `BehaviorSubject` starting at the `NavigationEnd` that put it there), `navigate` and
3121
- `navigateByUrl` (spies resolving `true`), `serializeUrl` / `parseUrl` / `createUrlTree` (the
3122
- router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`), `routerState`
3123
- (Angular's own, its `snapshot.url` the URL) and `currentNavigation` / `getCurrentNavigation()` (the
3124
- navigation in flight, `null` while the router stands still). **Every other member `Router` declares
3125
- throws by name** rather than reading `undefined` — and that holds for the instance fields too, which
3126
- is why a hand-rolled double needs `instanceMethodsToSpyOn: ['currentNavigation']` and this one does
3127
- not.
3128
-
3129
- Five things to know:
3130
-
3131
- - **It does not navigate.** `navigate()` records the call and resolves `true`; it does not move
3132
- `url`. `setUrl()` and `emitNavigation()` move it; `RouterTestingHarness` over a real
3133
- `provideRouter()` is what tests a navigation.
3134
- - **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
3135
- `NavigationEnd` moves the URL with it; any other event does not. It resolves once the event is
3136
- delivered and a navigation it ended is back to `null` — the moment `navigate()` resolves in an
3137
- application; the work is synchronous, so ignoring the promise changes nothing.
3138
- - **The navigation in flight follows the events, and a terminal event ends it _after_ delivering
3139
- it.** `setCurrentNavigation({ extras: { state } })` puts one up and `setCurrentNavigation(null)`
3140
- ends it; a `NavigationStart` pushed through `emitNavigation()` starts one with that event's id,
3141
- URL and trigger, and a `NavigationEnd`, `NavigationCancel`, `NavigationError` or
3142
- `NavigationSkipped` drops it back to `null` once its subscribers have run. Angular's doc comment
3143
- — "the current navigation becomes to null after the NavigationEnd event is emitted" — means
3144
- _after_ literally: the router emits the terminal event from a `tap` with the navigation still in
3145
- flight and clears it in the `finalize` below, and `events` is a Subject, so a synchronous
3146
- subscriber runs between the two. **A component that reads `currentNavigation()` while handling a
3147
- `NavigationEnd` gets the navigation that just finished**, here and in production; it reads `null`
3148
- only once `navigate()` has resolved. Probed against a real `provideRouter()` on Angular 22.
3149
- - **Ordering does not matter here.** `Router` is `providedIn: 'root'` and `provideRouter()` does not
3150
- re-provide the token, so the double wins in either order — and a `TestBed` without it hands out a
3151
- real router, which is what `injectRouterDouble()` says by name.
3152
- - **It builds spies**, so a runtime entry has to be imported in the suite (`vitest-auto-spy/angular`
3153
- in the same file, or the setup file); the route double needs none.
3154
-
3155
- ### `Location` from Angular's own testing classes — `vitest-auto-spy/angular-router`
3156
-
3157
- ```ts
3158
- import { injectLocationDouble, provideLocationDouble } from 'vitest-auto-spy/angular-router';
3159
-
3160
- TestBed.configureTestingModule({ providers: [provideLocationDouble()] });
3161
-
3162
- const location = injectLocationDouble();
3163
-
3164
- location.go('/reports/7');
3165
- location.simulateUrlPop('/'); // the popstate no method call can cause
3166
- ```
3167
-
3168
- **A wrap, not a rival.** Angular ships the double — `SpyLocation` with a real history array, a
3169
- `urlChanges` journal for assertions, and `simulateUrlPop()` / `simulateHashChange()` for the
3170
- browser's half of the contract — and this entry puts it in the family shape of the route and the
3171
- router: one provider call, an `injectLocationDouble()` whose errors name the instance that won
3172
- (`Location` is `providedIn: 'root'`, so the quiet failure is the real `Location` nobody configured).
3173
- `createLocationDouble()` is the same without a `TestBed`. One asymmetry worth knowing: `back()` and
3174
- `forward()` wake the popstate subscribers but do not write `urlChanges` — the journal holds what the
3175
- app asked for, the subscribers carry what the browser did. And one deliberate difference from
3176
- Angular's `SpyLocation`: `path()` keeps the query of `go(path, query)` / `replaceState(path, query)`
3177
- (`/reports?tab=7`), as the real `Location.path()` does — `SpyLocation`'s own drops it.
3178
-
3179
- ### Signal forms — `vitest-auto-spy/signal-forms`
3180
-
3181
- ```ts
3182
- import { minLength, required } from '@angular/forms/signals';
3183
- import { createForm, registerFormMatchers } from 'vitest-auto-spy/signal-forms';
3184
-
3185
- registerFormMatchers(); // once, in the setup file
3186
-
3187
- const user = createForm({ email: '', name: '' }, (path) => {
3188
- required(path.email, { message: 'Email is required' });
3189
- minLength(path.name, 2);
3190
- });
3191
-
3192
- expect(user.email).toHaveFieldErrors([{ kind: 'required', message: 'Email is required' }]);
3193
-
3194
- user.email().value.set('ada@example.test');
3195
-
3196
- expect(user.email).toHaveFieldErrors([]);
3197
- ```
3198
-
3199
- Signal forms are stable from Angular 22, and a spec that touches one meets the same two things every
3200
- time. **`form()` injects**, so a call in a `beforeEach` throws `NG0203: The Injector token injection
3201
- failed` — a message about `inject()` that never says "form", whose repair is
3202
- `{ injector: TestBed.inject(Injector) }` or a `TestBed.runInInjectionContext` around the call.
3203
- **`errors()` answers `RequiredValidationError` instances**, each carrying a `fieldTree`
3204
- back-reference, so `toEqual([{ kind: 'required' }])` fails on a property nobody wrote and every
3205
- suite falls back to `errors().some((error) => error.kind === 'required')` — which passes just as
3206
- happily when the field has three other errors nobody expected.
3207
-
3208
- | Call | Does |
3209
- | --------------------------------------------- | ------------------------------------------------------------------------- |
3210
- | `createForm(model, schema?, options?)` | Angular's own `form()`, built in the `TestBed`'s injection context |
3211
- | `createForm(initialValue, schema?, options?)` | the same, with the model signal made for you |
3212
- | `registerFormMatchers()` | adds `expect(field).toHaveFieldErrors(kinds)` — the whole set, order-free |
3213
-
3214
- Four things to know:
3215
-
3216
- - **What comes back is Angular's `FieldTree`**, not a double: the states, the validators and the
3217
- write-through to the model are the framework's. A spec that passes its own `signal()` asserts on it
3218
- directly; a plain value gets a signal made here, and `user().value()` reads it back either way.
3219
- - **A `computed()` is refused by name** — a form writes into its model, and without the check a
3220
- derived signal would be wrapped as a value while every write vanished.
3221
- - **`options.injector` is for the validator that injects**: pass `fixture.debugElement.injector`
3222
- when the service a schema reaches for lives in a component's own `providers`.
3223
- - **`toHaveFieldErrors` compares the whole set**, by `kind`, and by `message` only where the spec
3224
- names one; `[]` is "no errors". A field tree and its state both read, so `expect(user.email)` and
3225
- `expect(user.email())` are the same assertion.
3226
-
3227
- **No `FieldTree` double is shipped, on purpose.** It would stand in for a form a component takes as
3228
- an input, and across the measured suites no component takes one: forms are built by the component
3229
- that owns them (assert through `component.form.tags()`), and custom controls — `FormValueControl<T>`,
3230
- `FormCheckboxControl` — are plain components whose contract is `value = model<T>()`, which is
3231
- `renderShallow` plus `setInputs`.
3232
-
3233
- ### `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
3234
-
3235
- ```ts
3236
- import { provideDocumentDouble, provideWindowDouble } from 'vitest-auto-spy/angular/doubles';
3237
-
3238
- TestBed.configureTestingModule({
3239
- providers: [
3240
- provideWindowDouble(WINDOW, { screen: { width: 1920, height: 1080 }, devicePixelRatio: 2 }),
3241
- provideDocumentDouble({ visibilityState: 'hidden' }),
3242
- ],
3243
- });
3244
- ```
3245
-
3246
- **Do not write `useValue: window`, and do not build a `mockDocument` from scratch.** These are the
3247
- two most hand-rolled providers in an Angular suite — 95 `window` ones and 70 `document` ones across
3248
- two private suites — and both hand-written shapes fail the same way. `useValue: window` isolates
3249
- nothing: what the test writes stays on the global for the rest of the worker. A double built from
3250
- scratch, whether a slice (`{ screen: { width: 1280, height: 720 } }`) or a `mockDocument` with one
3251
- hand-written `querySelector`, knows only the members its author thought of, so the component that
3252
- reads `screen.colorDepth` or calls `document.createElement` gets `undefined` and the spec fails
3253
- somewhere unrelated to what it was testing.
3254
-
3255
- These two merge the overrides **over the real jsdom object** instead: everything the spec did not
3256
- name — `location.href`, `getComputedStyle`, `addEventListener`, `document.createElement` — still
3257
- answers the way jsdom answers it.
3258
-
3259
- | Call | Does |
3260
- | --------------------------------------------------------------------- | ----------------------------------------------------------------- |
3261
- | `provideWindowDouble(token, overrides?)` | a `FactoryProvider` under the application's own window token |
3262
- | `provideDocumentDouble(overrides?, token?)` | the same under Angular's `DOCUMENT`, or under a token of your own |
3263
- | `createWindowDouble(overrides?)` / `createDocumentDouble(overrides?)` | the same doubles without a `TestBed` |
3264
-
3265
- Five things to know:
3266
-
3267
- - **The window helper needs your token.** Angular ships `DOCUMENT` (from `@angular/core` itself
3268
- since v20) and has never shipped a `WINDOW`: every application declares its own
3269
- `InjectionToken<Window>`. The helper is generic over the token's type, so an
3270
- `InjectionToken<AppWindow>` has that interface's members checked in the overrides too.
3271
- - **A plain `{ … }` merges, anything else replaces.** `{ screen: { width: 1920 } }` keeps
3272
- `screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole. The
3273
- slice goes three levels down, the way the merge does — `{ document: { location: { href: '' } } }` is
3274
- checked and merged member by member.
3275
- - **Nothing to restore.** The globals are never patched — the double is a view over them, and every
3276
- write and delete lands on the view, which is also how a spec moves a value mid-test:
3277
- `Object.assign(TestBed.inject(WINDOW), { scrollY: 40 })` (`Object.assign` because lib.dom declares
3278
- most of `Window` `readonly`). There is no handle and no `restoreMockedProps()` in this recipe.
3279
- - **`provideDocumentDouble` hands the double to Angular as well** — the renderer injects `DOCUMENT`
3280
- too, so replace `createElement` or `body` only where the spec means to.
3281
- - **`location` takes overrides like everything else**, though the platform declares its members
3282
- unforgeable: `{ location: { reload: vi.fn() } }` — the shape most hand-written window mocks are —
3283
- records the reload, leaves `location.href` real, and `win.location.href = '/next'` moves the
3284
- double rather than the address bar. `mockValueProp(TestBed.inject(WINDOW), 'innerWidth', 800)` and
3285
- its restore work on the double too, and neither touches the global.
3286
- - **Constructors come out unbound.** `win.Date`, `win.Promise`, `win.Object`, `win.Event` are handed
3287
- back as themselves rather than through a binding that loses the statics, so `win.Date.now()`,
3288
- `win.Promise.resolve()`, `win.Object.keys(x)` and `new win.Event('x')` work, and
3289
- `win.Event === window.Event` holds — which is what `instanceof` in production code needs.
3290
-
3291
- ### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
3292
-
3293
- ```ts
3294
- import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
3295
- import { expectEmission } from 'vitest-auto-spy/angular';
3296
- import { injectMatDialogRef, provideMatDialogData, provideMatDialogRef } from 'vitest-auto-spy/angular/doubles';
3297
-
3298
- TestBed.configureTestingModule({
3299
- providers: [provideMatDialogData<EditUserData>(MAT_DIALOG_DATA, { id: 7, name: 'Ada' }), provideMatDialogRef(MatDialogRef)],
3300
- });
3301
-
3302
- const dialog = injectMatDialogRef(MatDialogRef);
3303
-
3304
- TestBed.createComponent(EditUserDialog).componentInstance.save();
3305
-
3306
- expect(dialog.close).toHaveBeenCalledWith('saved');
3307
- await expect(expectEmission(dialog.ref.afterClosed())).resolves.toBe('saved');
3308
- ```
3309
-
3310
- **`@angular/material` is not a dependency of this package and must not become one** — it ships no
3311
- runtime dependencies at all. That is exactly why the token and the ref class are **arguments**:
3312
- `MatDialogRef` is the DI token, the shape the double is measured against and the type the result is
3313
- read off, so the spec's own Material import stays the only place it is named.
3314
-
3315
- **Do not hand-roll `{ close: vi.fn() }`.** 36 of these across two private suites are the same three
3316
- shapes, and the ref one fails twice. `close` is the only member anybody writes, so a component that
3317
- subscribes to `afterClosed()` dies on "is not a function"; the repair written next to it,
3318
- `afterClosed: () => of('saved')`, answers before anything closed the dialog — the spec then passes
3319
- whether or not `close()` was ever called.
3320
-
3321
- | Call | Does |
3322
- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
3323
- | `provideMatDialogData(token, data)` | a typed `{ provide, useValue }` — name the type argument and the data is checked against it |
3324
- | `provideMatDialogRef(RefClass, init?)` | a `FactoryProvider` — a fresh ref per injector; `init`: `closedWith`, `disableClose`, `componentInstance` |
3325
- | `injectMatDialogRef(RefClass, injector?)` | the handle: `.ref`, `.close` (the spy), `emitClose(result?)` |
3326
- | `createMatDialogRef(RefClass, init?)` | the same handle without a `TestBed` — and the ref a spied `MatDialog.open()` hands back |
3327
-
3328
- `MatDialog` itself needs no helper of its own: `provideAutoSpy(MatDialog)` already spies it, and the
3329
- ref double is the thing its `open()` was missing.
3330
-
3331
- ```ts
3332
- TestBed.configureTestingModule({ providers: [provideAutoSpy(MatDialog)] });
3333
-
3334
- injectSpy(MatDialog).open.mockReturnValue(createMatDialogRef(MatDialogRef, { closedWith: 'saved' }).ref);
3335
- ```
3336
-
3337
- Six things to know:
3338
-
3339
- - **`close` is the spy, and it is the same function the ref carries.** `expect(dialog.close)` and
3340
- `expect(TestBed.inject(MatDialogRef).close)` are one assertion. `emitClose(result?)` is the other
3341
- half — the user closing the dialog from outside, where the streams move and the spy records
3342
- nothing, and the only way to close with `undefined` (a dismissal), which `closedWith` cannot say.
3343
- - **`afterClosed()` is replayed, where Material's is a plain `Subject`.** The deliberate departure:
3344
- in a spec the assertion usually subscribes _after_ the component has closed the dialog, and a
3345
- `Subject` has nothing left to say by then. `beforeClosed()` is the same stream — the double closes
3346
- instantly, so there is no window between them, and `afterOpened()` has already emitted.
3347
- - **`MAT_DIALOG_DATA` is `InjectionToken<any>` in Material**, so a hand-written `useValue: null`
3348
- compiles for a component that reads `data.name`. `provideMatDialogData<EditUserData>(…)` checks the
3349
- object; a token of your own typed `InjectionToken<EditUserData>` checks it without naming anything.
3350
- With the type argument named the token parameter is `InjectionToken<unknown>`, so the call passes
3351
- `@typescript-eslint/no-unsafe-argument` — do not drop the type argument to silence that rule, it
3352
- only turns the data back into `any`. The value is handed out as it is, so build it per test rather than hoisting it to a constant.
3353
- - **Name the ref type as a type argument, not as an instantiation expression.**
3354
- `injectMatDialogRef(MatDialogRef<NameInputDialog, string>)` reads `Ref` off the class's `prototype`,
3355
- which Material types `MatDialogRef<any, any>`, so the handle comes back `any`-typed and assigning it
3356
- to a typed variable is `no-unsafe-assignment`. Write
3357
- `injectMatDialogRef<MatDialogRef<NameInputDialog, string>>(MatDialogRef)` — the same for
3358
- `createMatDialogRef` and `provideMatDialogRef`.
3359
- - **`componentInstance` is how the opener drives the dialog**, and it is the one member Material
3360
- keeps off the prototype, so a double that did not have it would read `undefined` rather than
3361
- fail. Hand the stand-in over and the opener works against it — checked, member by member, against
3362
- the component the ref class names:
3363
-
3364
- ```ts
3365
- const save = new EventEmitter<string>();
3366
- const dialog = createMatDialogRef<MatDialogRef<NameInputDialog, string>>(MatDialogRef, {
3367
- componentInstance: { save, isSaving: signal(false) },
3368
- });
3369
-
3370
- injectSpy(MatDialog).open.mockReturnValue(dialog.ref);
3371
- save.emit('Grace');
3372
-
3373
- expect(dialog.close).toHaveBeenCalledWith('Grace');
3374
- ```
3375
-
3376
- - **What it does not answer throws by name.** `backdropClick`, `keydownEvents`, `updateSize`,
3377
- `updatePosition`, `getState`, `componentRef` and `id` are the dialog doing its own work; the
3378
- double names them in the failure instead of reading `undefined`, and the real `MatDialogModule` is
3379
- what answers them. A `componentInstance` nobody handed over says so by name too, and names the
3380
- init field that fixes it.
3381
-
3382
- ### Which collaborators the code asked for — `trackInjections`
3383
-
3384
- Do **not** reach for `vi.mock('@app/services')` to answer "was this collaborator used". Register the
3385
- collaborators as provider factories and read back which ones DI constructed; a factory runs exactly
3386
- when something injects its token, and DI is a seam the bundler cannot remove.
3387
-
3388
- ```ts
3389
- import { trackInjections } from 'vitest-auto-spy/angular';
3390
-
3391
- // the same function on /nestjs
3392
-
3393
- const collaborators = trackInjections([FeatureFlagService, ANALYTICS_TOKEN]);
3394
-
3395
- TestBed.configureTestingModule({ providers: [CheckoutFacade, ...collaborators.providers] });
3396
- collaborators.get(FeatureFlagService).isOn.mockReturnValue(true);
3397
-
3398
- TestBed.inject(CheckoutFacade).start();
3399
-
3400
- expect(collaborators.names()).toEqual(['FeatureFlagService']); // analytics was never asked for
3401
- ```
3402
-
3403
- `providers`, `injectedTokens()` (in factory-run order), `names()`, `wasInjected(token)`,
3404
- `get<D>(token)` → `Spy<D>`, `reset()` (the record only — the doubles survive). A class token gets a
3405
- class spy, anything else a `createAutoMock()`; pass `{ double: () => … }` when a collaborator has to
3406
- be a real object. Doubles are built eagerly, so stub before the entry point runs; a factory runs once
3407
- per injector, so a token appears once per injector, not once per injection site.
3408
-
3409
- ### Never mock `@angular/core` to control an `effect()`
3410
-
3411
- Under the Angular unit-test builder the specs are bundled and `@angular/core` sits in a shared
3412
- chunk, so `vi.mock('@angular/core', …)` re-enters a chunk that is still initialising and fails with
3413
- `Cannot access '__vi_import_N__' before initialization`. The same applies to any module those shared
3414
- chunks depend on, and to `vi.mock()` with a relative path (`./`, `../`), which has no module
3415
- boundary left to replace once bundled.
3416
-
3417
- Assert the effect's **result** instead — set the signals it reads, let it run, check what it
3418
- produced:
3419
-
3420
- ```ts
3421
- mockReadonlyProp(component, 'state', signal(State.Selected));
3422
-
3423
- await stable(fixture);
3424
-
3425
- expect(component.icon()).toBe('favouritesFilled');
3426
- ```
3427
-
3428
- When the effect will never become dirty on its own — because its trigger is now a static signal —
3429
- run that one effect directly:
3430
-
3431
- ```ts
3432
- import { runEffect } from 'vitest-auto-spy/angular';
3433
-
3434
- runEffect(component.highlightEffect); // runs the body now, with the current signal values
3435
- ```
3436
-
3437
- `flushEffects()` runs everything currently dirty; `runEffect(ref)` runs one specific effect
3438
- regardless. It runs the previous run's cleanup first, the way Angular's own scheduler does, so an
3439
- `onCleanup` callback is observable and the node does not collect one closure per call. An effect
3440
- whose view has been torn down — or which was destroyed on its own — is refused rather than run:
3441
- after `fixture.destroy()` there is nothing production could still produce. Prefer asserting the
3442
- result where practical — `runEffect` reads Angular's reactive node, and throws with instructions if
3443
- a future version moves it.
3444
-
3445
- ### Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
3446
-
3447
- "The template read `total()` six times and the computation ran once" and "changing the filter did
3448
- not re-run the sync effect" are not observable from the outside: a `computed()` returns the same
3449
- value whether it was cached or recomputed. The usual workaround is a counter inside the computation,
3450
- which means editing production code to make a test possible.
3451
-
3452
- ```ts
3453
- import { trackEffectRuns, trackRecomputations } from 'vitest-auto-spy/angular';
3454
-
3455
- const recomputed = trackRecomputations(component.total);
3456
- const synced = trackEffectRuns(component.syncEffect);
3457
-
3458
- component.unrelatedFilter.set('open');
3459
- await stable(fixture);
3460
-
3461
- expect(component.total()).toBe(42);
3462
- expect(recomputed.count).toBe(0); // memoised: nothing it reads changed
3463
- expect(synced.count).toBe(0);
3464
-
3465
- recomputed.stop(); // and `restoreMockedProps()` — so `setupAutoSpy()` — takes both off anyway
3466
- ```
3467
-
3468
- Both return `{ count, stop() }`. `trackRecomputations` counts the **computation**, not the reads,
3469
- and takes a `computed()` or a `linkedSignal()` — a plain `signal()` has nothing to compute and is
3470
- refused by name. `trackEffectRuns` counts every run whoever asked for it: a flush, a `stable()`, a
3471
- `runEffect()` of the same effect. Both wrap one member of Angular's reactive node through
3472
- `mockValueProp`, so the undo is the journal every other stub in this package uses.
3473
-
3474
- ### ngrx `rxMethod`
3475
-
3476
- An `rxMethod` is a function with a `destroy` property. A bare mock has no `destroy`, so the
3477
- component's cleanup throws:
3478
-
3479
- ```ts
3480
- const load = Object.assign(vi.fn(), { destroy: vi.fn() });
3481
- ```
3482
-
3483
- Under `strict`, a call whose ref nobody reads is configured with `returns: { load: undefined }`. When
3484
- the code does read the ref (it calls `destroy`), seed one from the method's own type — ngrx does not
3485
- export `RxMethodRef`: `returns: { load: createAutoMock<ReturnType<Store['load']>>() }`.
3486
-
3487
- A `signalStore()` class keeps its methods and `rxMethod`s on the instance, not the prototype, so
3488
- discovery finds none of them. `provideAutoSpy(Store, { fillMissing: true, returns: { … } })` answers
3489
- every such member with a spy — `returns` and `strict` apply to them as to any method — instead of a
3490
- long `instanceMethodsToSpyOn` list.
3491
-
3492
- ```ts
3493
- // shallow rendering — configureTestingModule + NO_ERRORS_SCHEMA + overrideComponent, in one call
3494
- const { fixture, component } = renderShallow(TaskListComponent, {
3495
- providers: [provideAutoSpy(TaskService)],
3496
- inputs: { projectId: 42 }, // signal inputs take the VALUE, not the signal
3497
- });
3498
- // other options: imports, keepTemplate, keepModules, keepChildren, keepHostDirectives, template, beforeCreate, detectChanges
3499
- // COVERAGE: the override recompiles the component under JIT for the rest of the file (keepTemplate too),
3500
- // so its AOT template/host branches leave coverage — keep one real TestBed render first where they matter
3501
-
3502
- // change an input after the first render — one setInput per key, then the wait, in one await
3503
- await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
3504
- await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
3505
- // a fixture whose componentType carries no ɵcmp is refused by name too, not as a bare TypeError
3506
- // aliases resolve by CLASS FIELD name, and an input a hostDirectives entry exposes is accepted
3507
-
3508
- // a typed element instead of Angular's `any` — never `fixture.nativeElement as HTMLElement`
3509
- const host = hostElement(fixture); // HTMLElement, checked with instanceof
3510
- queryElement(fixture, '.close').click(); // HTMLElement; throws naming the selector when nothing matches
3511
- queryElement(fixture.debugElement, 'input[name=q]', HTMLInputElement).value; // typed; wrong type throws
3512
- // also on /bun-angular; queryElement takes an element found earlier as its root too
3513
- // a null source (debugElement.query() that matched nothing) throws saying so, not 'Cannot read properties of null'
3514
- expect(queryElement(fixture, '.title').textContent.trim()).toBe('Orders'); // present: read through queryElement
3515
- expect(host.querySelector('.empty-state')).toBeNull(); // absent: querySelector on the typed host, no cast
3516
- // NOT host.querySelector('.title')?.textContent.trim() — a miss is undefined, which toBeFalsy() accepts
3517
-
3518
- // build a class through DI, every unprovided token auto-spied
3519
- const { instance, spies } = createWithAutoSpies(CartService, {
3520
- providers: [{ provide: TaxService, useValue: realTax }], // explicit providers win
3521
- });
3522
- spies.get(PricingService).total.mockReturnValue(100);
3523
- // NOTE: Injector.create() — it does NOT accept EnvironmentProviders (provideHttpClient() etc.)
3524
-
3525
- // zoneless waiting
3526
- await stable(fixture); // flush effects, then await the fixture; fails at 2000 ms naming the cause
3527
- await stable(fixture, { timeout: 5000, label: 'the products fixture' });
3528
- flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
3529
-
3530
- // resources — one wait for httpResource(), resource() and rxResource()
3531
- flushEffects(); // an httpResource issues NO request until something ticks
3532
- httpTesting.expectOne('/api/products').flush([product]);
3533
- await settleResource(products, { label: 'the product resource' });
3534
- // a resource left 'idle' FAILS here — params() returned undefined and value() is still the default;
3535
- // { allowIdle: true } when that is the state under assertion, { turns } is spent exactly as reported
3536
-
3537
- // on a component the resource is a FIELD and renderShallow is the injection context — nothing else changes
3538
- const { component } = renderShallow(ProductListComponent, { providers: [...provideHttpTesting()], keepTemplate: true });
3539
- await expectRequest('/api/products?q=').flush([product]); // both from /angular-http; renderShallow already ticked
3540
- expect(component.products).toHaveResourceValue([product]);
3541
- // after changing a signal the params() computation reads: flushEffects() before the next expectOne/expectRequest
3542
-
3543
- // ...or skip the request entirely when it is not what the spec is about
3544
- const products = mockResourceProp(service, 'products', []); // { status: 'idle' } starts it unstarted
3545
- products.set([product]); // 'resolved' products.loading() products.fail('offline') products.idle()
3546
- service.products.set([edited]); // the component's own optimistic write — status → 'local'
3547
- expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, and re-issues nothing
3548
-
3549
- // signal assertions
3550
- registerSignalMatchers(); // once, in the setup file — /angular/matchers
3551
- expect(component.total).toHaveSignalValue(3);
3552
-
3553
- // resource assertions — value AND status, which is the whole point
3554
- registerResourceMatchers(); // once, in the setup file — /angular/matchers
3555
- expect(component.products).toBeLoading();
3556
- expect(component.products).toHaveResourceValue([product]);
3557
- expect(component.products).toHaveResourceError(/503/);
3558
- ```
3559
-
3560
- **`renderShallow({ inputs })` and `setInputs` resolve a name the same way**, which they did not
3561
- before. Both read the compiled definition, so an input renamed with an alias
3562
- (`@Input('chart-series') series` or `series = input(…, { alias: 'chartSeries' })`) is set by its
3563
- **class-field** name as well as its public one; both accept an input a `hostDirectives` entry
3564
- exposes, which `setInputs` used to refuse with "It declares no inputs at all"; and both refuse a name
3565
- the component does not declare, at the call, listing the ones it does. Angular's own answer to an
3566
- undeclared name is an `NG0303` on the console and no change at all, so the spec used to fail several
3567
- assertions later, on state nothing moved.
3568
-
3569
- Two zoneless traps:
3570
-
3571
- - `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
3572
- effects. Asserting right after it reads state that has not finished computing. Use `await stable(fixture)`.
3573
- - `expect(someSignal).toBeTruthy()` passes for **every** signal ever created — a signal is a
3574
- function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake — and a
3575
- spy, which it refuses instead of calling.
3576
-
3577
- And one resource trap, which is the same shape one level up: an `httpResource()` reports `loading`
3578
- with its **default** value until a tick _and_ a microtask after its response is flushed, so a spec
3579
- that asserts too early asserts the default and passes. `settleResource` fails instead of passing
3580
- emptily. Note the order — `flushEffects()` first (the request is issued there, not on creation),
3581
- then the flush, then the wait. The same trap wearing a different hat is a resource still `idle` when
3582
- the wait ends: its `params()` computation returned `undefined`, the loader never ran, and `value()`
3583
- is the default. `settleResource` refuses that too, naming the signal the computation reads;
3584
- `{ allowIdle: true }` is the opt-out for a spec whose subject is the idle state. Each round is a tick
3585
- plus a microtask, and from the third round an event-loop turn as well, so a loader on a real timer
3586
- settles rather than burning the budget in microseconds.
3587
-
3588
- `toHaveResourceValue` is the matcher form of that trap and the reason to prefer it over
3589
- `expect(products.value()).toEqual(...)`: it **fails an unresolved resource even when the default
3590
- value matches**, and names the status it was in. And when the request is not what the spec is about
3591
- at all, do not arrange one — `mockResourceProp(service, 'products', [])` replaces the property with
3592
- a double whose `set` / `fail` / `loading` / `idle` move it directly, built from real `signal()`s so a
3593
- `computed()` downstream still recomputes. The double carries the whole of `ResourceRef`: `value` is
3594
- writable and a write through it lands in `'local'`, `set` / `update` / `asReadonly` / `destroy` /
3595
- `snapshot` behave as Angular's own do, and `hasValue()` is value-based rather than status-based —
3596
- `true` while the resource is loading over a value that is defined, which is what Angular has answered
3597
- since v20. Nothing is in flight, so there is nothing to await.
3598
-
3599
- **The double is no more forgiving than the real thing, in two places that used to hide a defect.**
3600
- `value()` read after `fail(reason)` **throws** — a `ResourceValueError` carrying the reason on
3601
- `cause` and naming the property — exactly as a real `ResourceRef` does, so a spec that read the value
3602
- after arranging the error branch fails now instead of reading the last good value. Branch on
3603
- `hasValue()` / `status()` first, or assert with `toHaveResourceError()`. And `reload()` answers
3604
- `false` while the resource is `'idle'` or `'loading'`, as Angular's does, rather than a constant
3605
- `true` — `expect(products.reload).toHaveBeenCalled()` is still the assertion to write; the return
3606
- value is only for code that branches on it.
3607
-
3608
- **The resource, focus and directive matchers throw on an argument of the wrong type.** They used to
3609
- report `{ pass: false }`, which `.not` turns into a pass — so
3610
- `expect(products.value()).not.toBeLoading()` (the value, not the resource),
3611
- `expect(missingEl).not.toHaveFocus()` (a query that found nothing) and
3612
- `expect(undefined).not.toHaveDirectiveApplied(X)` were three green assertions about nothing. Each
3613
- now throws, naming what it received.
3614
-
3615
- Per-file timing, to find which specs actually pay for `TestBed`:
3616
-
3617
- ```ts
3618
- import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
3619
-
3620
- if (process.env['SPEC_TIMING']) {
3621
- enableTestBedDiagnostics();
3622
- }
3623
- ```
3624
-
3625
- ### Angular under `bun test`
3626
-
3627
- `vitest-auto-spy/bun-angular` is a **preload**, not a normal import — it installs a DOM, inlines
3628
- `templateUrl` / `styleUrls` through a `Bun.plugin` hook and boots a zoneless TestBed:
3629
-
3630
- ```toml
3631
- # bunfig.toml
3632
- [test]
3633
- preload = ["vitest-auto-spy/bun-angular"]
3634
- ```
3635
-
3636
- The preload imports `provideZonelessChangeDetection` and `platformBrowserTesting` as values, so it
3637
- wants **Angular >= 20** and a declared `@angular/platform-browser`; on 18 or 19 it throws while
3638
- loading, because the zoneless provider was still called
3639
- `provideExperimentalZonelessChangeDetection` there.
3640
-
3641
- It exports the core, `provideAutoSpy` / `injectSpy`, `renderShallow`, `createWithAutoSpies`,
3642
- `setInputs`, `runEffect`, `settleResource`, `trackEffectRuns` / `trackRecomputations` and
3643
- `stable` / `flushEffects` — nothing else from this section. The matcher registrars
3644
- (`registerSignalMatchers`, `registerDirectiveMatchers`, `registerResourceMatchers`) need the runner's
3645
- `expect.extend` and the TestBed diagnostics its suite-level hooks; the overrides, `extendWithAutoSpies`,
3646
- `provideAutoSpyForToken`, `trackInjections`, `setupAngularTestEnv`, the stub factories and the
3647
- `mock*Prop`, platform and dialog doubles are simply not routed to Bun. Import them from `/angular`
3648
- under Vitest, and the registrars, doubles and diagnostics from their companion entries
3649
- (`/angular/matchers`, `/angular/doubles`, `/angular/diagnostics`).
3650
-
3651
- ---
3652
-
3653
- ### Zone and zoneless spec files in one worker
3654
-
3655
- `TestBed.initTestEnvironment` may be called once per platform, and under `isolate: false` the
3656
- platform lives for the whole worker — so a repository migrating to zoneless gradually cannot express
3657
- itself in setup files: the second file the worker picks up in the other mode fails with `Cannot set
3658
- base providers because it has already been called`, naming neither file. `test.projects` does not
3659
- help; nothing promises a worker serves files of one project.
3660
-
3661
- ```ts
3662
- setupAngularTestEnv({
3663
- zoneless: (testPath) => testPath.includes('/libs/widgets/'),
3664
- initZone: setupZoneTestEnv,
3665
- initZoneless: setupZonelessTestEnv,
3666
- });
3667
- ```
3668
-
3669
- It resets the environment only when the mode actually changes, and the initialisers stay yours —
3670
- which platform and which providers is not this library's decision. The mode is remembered **per
3671
- worker**, so under `isolate: false` a run of files all in one mode initialises once: before, every
3672
- spec file reset the environment and re-ran your initialiser.
3673
-
3674
- ---
3675
-
3676
- ### A dependency behind an `InjectionToken`
3677
-
3678
- ```ts
3679
- providers: [provideAutoSpyForToken(PASSCODE_SERVICE_TOKEN)];
3680
- const passcode = injectSpy(PASSCODE_SERVICE_TOKEN); // Spy<PasscodeService>
3681
- ```
3682
-
3683
- A token typed with an interface has no class to read, so the habit is a `…Mock` class written in the
3684
- spec — after which `Spy<Mock>` and `Spy<Interface>` disagree and somebody casts. Do not write
3685
- `TestBed.inject<any>(TOKEN)`; both of these accept a token. And it is `provideAutoSpyForToken`, not
3686
- `provideAutoSpy`: the latter reads a class prototype, which a token does not have.
3687
-
3688
- **The second argument is not optional as often as it looks.** A spy answers `undefined` until it is
3689
- told otherwise, and that is fatal the moment the code under test _chains_ off it — a constructor
3690
- doing `inject(LOGGER).channel('auth').debug('…')` dies on the `.debug` of `undefined` before the
3691
- spec's first line runs, because nothing in production wrote `?.` there. Name the link:
3692
-
3693
- ```ts
3694
- provideAutoSpyForToken(LOGGER, undefined, { selfReturning: ['channel'] });
3695
- ```
3696
-
3697
- `channel` stays a spy (assertable, and configured under `strict`) and answers the double itself.
3698
- When every file wants it, register it once — `registerAutoSpyDefaults(LOGGER, { selfReturning:
3699
- ['channel'] })` from `vitest-auto-spy/angular` (§5) — and the provider is `provideAutoSpyForToken(LOGGER)`.
3700
-
3701
- For a chain more than one link long, `mockDeep<T>()` is the double that answers every level (§2).
3702
-
3703
- ### A host for a directive under test
3704
-
3705
- ```ts
3706
- const Host = createDirectiveHost({
3707
- template: `<div [appTruncate]="enabled"></div>`,
3708
- scope: [DirectivesModule], // the component's imports, NOT the TestBed's
3709
- props: { enabled: false },
3710
- });
3711
-
3712
- TestBed.configureTestingModule({ imports: [Host] });
3713
- ```
3714
-
3715
- The two halves of Angular disagree about where `imports` is resolved: on a `@Component` the AOT
3716
- compiler resolves it at build time and bakes the flat list into `ɵcmp`, so an NgModule there works;
3717
- on `TestBed.configureTestingModule` it is resolved at runtime from `ɵmod`, and `ɵɵsetNgModuleScope`
3718
- is not emitted into a test bundle, so the same line contributes nothing. A host written
3719
- `standalone: false` inside a spec is compiled outside any scope at all — no `NgClass`, no
3720
- `AsyncPipe`, nothing.
3721
-
3722
- `registerDirectiveMatchers()` adds `expect(fixture).toHaveDirectiveApplied(Directive, 'div')`, which
3723
- asserts the fact Angular reports three wrong ways (`NG0303` points at the module where the directive
3724
- _is_ declared; `NG0304` calls a missing directive a missing component; a bare attribute reports
3725
- nothing at all). `schemas: [NO_ERRORS_SCHEMA]` next to a standalone component is a dead entry —
3726
- schemas apply to a testing module's `declarations` only.
3727
-
3728
- ### A stand-in for a child — `createComponentStub`
3729
-
3730
- Do not hand-write `class MockChartComponent` with a copied selector: nothing checks the copy, so a
3731
- renamed input leaves the parent's binding pointing at nothing and the spec green. Build the stub
3732
- from the real class's compiled definition instead:
3733
-
3734
- ```ts
3735
- import { createComponentStub } from 'vitest-auto-spy/angular';
3736
-
3737
- const ChartStub = createComponentStub(ChartComponent); // also a directive or a pipe
3738
-
3739
- TestBed.configureTestingModule({ imports: [DashboardComponent] });
3740
- TestBed.overrideComponent(DashboardComponent, { remove: { imports: [ChartComponent] }, add: { imports: [ChartStub] } });
3741
-
3742
- const chart = fixture.debugElement.query(By.directive(ChartStub)).componentInstance;
3743
- expect(chart.series()).toEqual([1, 2, 3]); // a signal input stays a signal input
3744
- chart.pointSelected.emit(2); // outputs are EventEmitters; a model() stays a model
3745
- ```
3746
-
3747
- Copied from `ɵcmp` / `ɵdir` / `ɵpipe`: the selector, every input under its public name (transforms
3748
- included), every output, `exportAs`, a pipe's name and purity. Not copied: the template (the stub
3749
- renders one `<ng-content>` per slot the real one projects — `{ template }` in the third argument
3750
- replaces it), host bindings, providers, lifecycle hooks, queries, and **`hostDirectives`** — so an
3751
- input the real component exposes through one is not on the stub, even though `setInputs` accepts that
3752
- name on the real thing. The second argument seeds members per instance — a method the parent calls
3753
- through `viewChild`, a pipe's `transform` (identity by default). With `renderShallow`:
3754
- `renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })` — the real child is
3755
- dropped as a component import and the stub is the one child left.
3756
-
3757
- ### Patching a property of a spy
3758
-
3759
- `mockReadonlyProp` / `mockValueProp` / `mockSignalProp` accept the `Spy<T>` that `injectSpy` returns,
3760
- and type the value against the member's **own** type. For a signal-valued member prefer
3761
- `mockSignalProp(service, 'state', initial)` over `gettersToSpyOn`: a spied getter returns `undefined`
3762
- until configured, a real signal keeps everything downstream reactive.
3763
-
3764
- **A spec importing `@ngneat/spectator`** is on a package whose repository is a 404 and which does not
3765
- resolve on Angular 22 (it imports `@angular/platform-browser-dynamic/testing` without declaring it).
3766
- `createSpyObject` → `createSpyFromClass`, `mockProvider` → `provideAutoSpy`, `spectator.inject` →
3767
- `injectSpy`, `SpyObject<T>` → `Spy<T>`, `createComponentFactory` → `renderShallow`. There is no
3768
- replacement here for `spectator.query`, the event helpers or the DOM matchers — leave those on
3769
- `@testing-library/angular` rather than inventing an equivalent. Full mapping:
3770
- [Migrating from @ngneat/spectator](https://asdalexey.github.io/vitest-auto-spy/migrating-spectator).
3771
-
3772
- A suite coming off [`@suites/unit`](https://github.com/suites-dev/suites) maps one to one:
3773
- `TestBed.solitary(S).compile()` → `createNestUnit(S)`, `TestBed.sociable(S).expose(D).compile()` →
3774
- `createNestUnit(S, { expose: [D] })`, `unitRef.get` → `spies.get`, `.final(v)` →
3775
- `{ provide, useValue }`, and the `await` goes away because the graph is built synchronously. The one
3776
- thing to check by hand after the swap: Suites' double answers every property name, so a spec that
3777
- stubbed a since-renamed method was passing over nothing and will now fail. Full mapping:
3778
- <https://asdalexey.github.io/vitest-auto-spy/migrating-suites>.
3779
-
3780
- **A spec importing `@testing-library/angular` usually keeps it.** `render`, `screen` and the query
3781
- set are a different discipline and have no twin here. What overlaps is the secondary
3782
- `@testing-library/angular/vitest-utils` entry alone — 52 lines, four exports:
3783
- `createMock` → `createSpyFromClass`, `createMockWithValues` → `createSpyFromClass(X, { overrides })`,
3784
- `provideMock` → `provideAutoSpy`, `provideMockWithValues` → `provideAutoSpy(X, { overrides })` —
3785
- `overrides` and not `returns`, because those values replace the member rather than configure it. Its
3786
- walk assigns a mock only where `typeof descriptor?.value === 'function'`, so accessors are dropped
3787
- silently while its `Mock<T>` still types them callable, and it recurses past `Object.prototype`, so
3788
- `hasOwnProperty` and `toString` end up as `vi.fn()`. Full mapping, verified against the 19.4.2
3789
- tarball: <https://asdalexey.github.io/vitest-auto-spy/migrating-testing-library-angular>.
3790
-
3791
- - **`renderShallow` has no single speed number, and quoting one is a bug.** This repository commits
3792
- no harness for the helper, so no surface publishes a ratio for it and none may be invented.
3793
- Describe the shape instead: `renderShallow` is flat in the number of children because it never
3794
- builds the subtree, while `TestBed.createComponent` scales linearly with it — so the win is
3795
- however much markup the component under test happens to own, nothing on a leaf component and a
3796
- great deal on a table or a dashboard. A per-render ratio would in any case be the upper bound on
3797
- what a spec file can gain rather than a prediction of it, because a file also pays for imports,
3798
- the `TestBed` module and the assertions. Link to
3799
- `/core/performance#_2-rendering-the-child-subtree`. A speed figure for the helper also sits in
3800
- `PRIORITIES.md`, backed by nothing in this repository — do not publish it until someone produces
3801
- the measurement.
3802
- - **Competitor defects are quoted from the published tarball, with a file, a line and a date.** The
3803
- `@testing-library/angular` `createMock` claims in `docs-site/comparison.md` are pinned to
3804
- `fesm2022/testing-library-angular-vitest-utils.mjs` lines 14 and 18 at 19.4.2. Re-`npm pack` and
3805
- re-read before restating them against a new version; the "Where the numbers come from" box records
3806
- each re-verification date rather than replacing the previous one.
3807
- - **Head-to-head benchmark numbers are quoted from the measurement, not from memory.** `npm run
3808
- bench:vs` and `npm run bench:suite` (see `CONTRIBUTING.md` for setup) back the claims in
3809
- `core/performance.md`; point at that page rather than retyping a figure here, so it lives in one
3810
- place. Micro-benchmark ratios do not transfer to suite scale: double construction is on the order
3811
- of 1% of a test's cost, and an advantage that looks like several times faster per double can be
3812
- gone, or reversed, once a whole suite runs. Suite-scale wall-clock is noisy between invocations;
3813
- peak RSS reproduces far more tightly and is the safer claim. Never state a magnitude the source
3814
- marks as not established.
3815
-
3816
- `node:test` keeps every `mock.fn()` in one process-wide `MockTracker` for the life of the process.
3817
- On a long suite that is real memory — 20 000 spies of a 10-method class retained 124.5 MB on
3818
- Node v24.19.0. `trackNodeMocks()` puts this library's spies on a tracker it owns and replaces it per
3819
- test: 5.9 MB for the same run, against a 5.4 MB baseline.
3820
-
3821
- \```ts
3822
- import { before } from 'node:test';
3823
- import { trackNodeMocks } from 'vitest-auto-spy/node';
3824
-
3825
- before(() => trackNodeMocks()); // opt-in, idempotent, returns the undo
3826
-
3827
- // by hand, for a concurrent suite: pruneNodeMocks() → how many were dropped; countNodeMocks() → how many are held
3828
- \```
3829
-
3830
- It never calls `mock.reset()` — that would restore and forget the `mock.fn()` the spec made itself —
3831
- and it never throws: the class is reached through the undocumented `mock.constructor`, so the
3832
- constructed tracker is probed first and any failure leaves spies on `node:test`'s own tracker.
3833
- Spies created **before** the call stay there too. `mock.reset()` in `afterEach` is the fallback.
3834
-
3835
- **A `node:test` spy now prints under its method name.** `mock.fn()` takes no name and has no
3836
- `mockName()`, so a spy inherited the library's internal dispatcher and read back as
3837
- `[Function: dispatch]` everywhere. The adapter names the _implementation_ at creation and lets
3838
- `mock.fn()` carry that name onto the mock, which is where a `node:test` mock takes its `name` from;
3839
- `displayName` is set on the mock alongside it. The name survives `mock.reset()`, `mock.restore()`,
3840
- `resetCalls()` and a `mockImplementation()` swap, and it is what `node:assert` diffs, what
3841
- `util.inspect()` prints and what this package's own messages read. Do not "simplify" this into
3842
- `Object.defineProperty(mock, 'name', …)`: redefining `name` drops the function out of V8's fast map
3843
- and costs +206 B per mock against +65 B for naming at creation, measured over 200 000 mocks. `getMockName()` still does not exist there: read `spy.method.name`. Nothing labels a
3844
- mock in `node:test`'s reporter output on its own. Full account:
3845
- <https://asdalexey.github.io/vitest-auto-spy/runtimes/node#spy-names>.
3846
-
3847
- ### Rstest
3848
-
3849
- `vitest-auto-spy/rstest` drives the same core through `rstest.fn()` / `rstest.spyOn()`. Rstest
3850
- implements the Jest/Vitest mock surface, so **none of the `node:test` differences above apply**:
3851
- `spy.method.mock.calls[0]` is a bare argument array, the `mockReturnValue` family is native, and
3852
- `gettersToSpyOn` / `settersToSpyOn` go through `rstest.spyOn(obj, 'prop', 'get' | 'set')` rather than
3853
- the redefinition fallback. `rstest.clearAllMocks()`, `rstest.resetAllMocks()` and the
3854
- `clearMocks: true` / `resetMocks: true` config keys reach spies built by `createSpyFromClass` — the
3855
- entry plants one sentinel mock for it, and there is nothing to enable.
3856
-
3857
- Two things are not there. `vitest-auto-spy/setup` is wired to Vitest's hooks, so `setupAutoSpy()` and
3858
- the fake-timer helpers are Vitest-only — on Rstest, import `vitest-auto-spy/rstest` once in the setup
3859
- file, which is the part a setup file is for. `trackNodeMocks()` is `node:test`-only and is not needed:
3860
- Rstest drops its mock registry between files, like Vitest and Bun.
3861
-
3862
- Rstest is 0.x. Vitest stays the zero-config default; reach for this entry when the suite already runs
3863
- on Rstest, which is typically an Rspack project reusing its bundler config for tests. Full account:
3864
- <https://asdalexey.github.io/vitest-auto-spy/runtimes/rstest>.
1827
+ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that uses `TestBed`, signals, Angular HTTP, the router or an Angular double.
1828
+
1829
+ <!-- agents-outline: agent-docs/angular.md -->
1830
+
1831
+ - The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
1832
+ - Signals — which helper depends on whose signal it is
1833
+ - Observers the component constructs itself
1834
+ - A component's own `providers` win, and the symptom is nowhere near the cause
1835
+ - `injectSpy` cannot reach a component-level provider
1836
+ - An NgModule that contributes nothing
1837
+ - A component whose own definition has a hole in it
1838
+ - Four silent failures, as one setup line
1839
+ - `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
1840
+ - `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
1841
+ - `Router` from one URL — `vitest-auto-spy/angular-router`
1842
+ - `Location` from Angular's own testing classes — `vitest-auto-spy/angular-router`
1843
+ - Signal forms — `vitest-auto-spy/signal-forms`
1844
+ - `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
1845
+ - The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
1846
+ - Which collaborators the code asked for — `trackInjections`
1847
+ - Never mock `@angular/core` to control an `effect()`
1848
+ - Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
1849
+ - ngrx `rxMethod`
1850
+ - Angular under `bun test`
1851
+ - Zone and zoneless spec files in one worker
1852
+ - A dependency behind an `InjectionToken`
1853
+ - A host for a directive under test
1854
+ - A stand-in for a child — `createComponentStub`
1855
+ - Patching a property of a spy
1856
+ - Rstest
1857
+
1858
+ <!-- /agents-outline -->
3865
1859
 
3866
1860
  ---
3867
1861
 
@@ -3955,532 +1949,87 @@ adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives
3955
1949
 
3956
1950
  ## 16. ESLint plugin (flat config only)
3957
1951
 
3958
- ```js
3959
- import autoSpy from 'vitest-auto-spy/eslint-plugin';
3960
-
3961
- export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
3962
- ```
3963
-
3964
- The `files` glob has no default — too narrow and the plugin is silently inert
3965
- (`npx eslint --print-config a.spec.ts` settles it), too wide and `no-object-define-property` starts
3966
- reporting on application code that is entitled to it. To override a severity, spread the rule map
3967
- too: a bare `rules` key beside the spread config **replaces** it rather than merging, and nothing
3968
- reports that.
3969
-
3970
- ```js
3971
- export default [
3972
- {
3973
- files: ['**/*.spec.ts'],
3974
- ...autoSpy.configs.recommended,
3975
- rules: { ...autoSpy.configs.recommended.rules, 'vitest-auto-spy/prefer-native-spy-api': 'off' },
3976
- },
3977
- ];
3978
- ```
3979
-
3980
- `autoSpy.configs.typeErrors` is a second flat config holding the subset whose findings are compile
3981
- errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
3982
- blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
3983
- `autoSpy.configs.strict` is `recommended` with every rule at `error`, instead of mapping
3984
- `Object.keys(autoSpy.rules)` by hand.
3985
-
3986
- | Rule | Level | Fix | Flags |
3987
- | --------------------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3988
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
3989
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3990
- | `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 |
3991
- | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3992
- | `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 }` |
3993
- | `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 }` |
3994
- | `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 }` |
3995
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
3996
- | `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 |
3997
- | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3998
- | `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 |
3999
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
4000
- | `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 |
4001
- | `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 |
4002
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
4003
- | `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 |
4004
- | `no-reflect-member-access` | `error` | on a double | `Reflect.get(component, 'privateField')` / `Reflect.set(service, 'x', v)` on a subject the test holds — the second door out of `no-private-member-access`, and the one no compiler stands in: the key is an ordinary string argument, so a rename leaves the spec reading a property that is no longer there and `Reflect.set` writing a dead one nothing reads. `window` / `globalThis` (or a name declared as `Window` / `typeof globalThis`) and a computed key are never reported; a write onto a double this library built is reported apart and suggests `mockValueProp` |
4005
- | `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 |
4006
- | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
4007
- | `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 |
4008
- | `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 |
4009
- | `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` |
4010
- | `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
4011
- | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
4012
- | `no-hand-assigned-global` | `error` | `--fix` | 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, `stubWorker({ respond })` for `Worker`; the three observers stay with `prefer-observer-stub`; any value written into an imported object (`environment.production = true`) → `mockValueProp(environment, 'production', true)`, fixed in a test or `beforeEach` |
4013
- | `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or `createMock<Response>(…)` / `createAutoMock<Response>(…)` → `stubResponse({ body })` from `/setup`; the literal answers `undefined` for every member it does not list (`status`, `headers`, `text()`) and the cast is what makes that compile. `Response` must resolve to the **global**, so an Express handler's or a generated client's `Response` is never reported |
4014
- | `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 |
4015
- | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
4016
- | `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` |
4017
- | `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` |
4018
- | `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 |
4019
- | `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` |
4020
- | `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 |
4021
- | `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>` |
4022
- | `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 |
4023
- | `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 |
4024
- | `no-self-called-spy` | `error` | — | `vi.spyOn(obj, 'm')`, then `obj.m(…)` written by the test itself, then a positive `toHaveBeenCalled*` — the assertion is satisfied by the test's own line, so the test survives the deletion of the behaviour its title names. Order decides it: a call written **before** the spy is arrangement, and a `mockClear` between the call and the assertion, a negated matcher or arguments the call did not pass all silence it |
4025
- | `no-vacuous-absence-assertion` | `error` | — | a test **all** of whose assertions are satisfied by the stream never emitting. The carrier is a `const` / `let` declared in the test whose every write outside the declaration sits in a `subscribe` callback of that test (the assignment and the `push` forms both count), or a `vi.fn()` the test hands to `subscribe` and never calls itself. Silence satisfies an equality matcher repeating the initialiser's source text, `toBeUndefined` / `not.toBeDefined` on a `let` holding `undefined`, `toBeNull` on one holding `null`, `toBeFalsy` / `not.toBeTruthy` on any falsy literal, `toHaveLength(0)` on `[]` or `''`, and `not.toHaveBeenCalled` / `not.toHaveBeenCalledWith` / `toHaveBeenCalledTimes(0)` on any subject — read literally, so `toBeNull()` on a `let` with no initialiser is **not** reported. One `expect()` a silent source could fail silences the rule, which is what exempts the "nothing yet, trigger, now the value" shape; a test asserting through a local helper is skipped entirely → `await expectNoEmission(source$)`, or `expect(await expectEmission(source$))` |
4026
- | `prefer-settle-dynamic-import` | `error` | suggest | `await import('./thing')` and `import('./thing').then(…)` inside an `it` / `test` body or a `beforeEach` / `beforeAll` / `afterEach` / `afterAll` hook (the `.only`, `.skip` and `.each` spellings included) → `await settleDynamicImport(() => import('./thing'))`, which is the same load plus `flushEventLoop(1)` and returns the namespace, so the destructured form keeps reading the same way. The bare `await` waits for the **module** and not for the continuation of the code that was loading it, so the assertion runs a turn early and the test is green only while that continuation is short. Syntax only. Silent wherever a function of its own sits between the runner's callback and the import — a `vi.mock` factory, a lazy route's `loadComponent` / `loadChildren`, a callback handed to production code, `settleDynamicImport`'s own `() => import(…)` — and, by the same reading, silent on a spec-local `const load = async () => { await import('…') }`, which is written identically whether the spec calls it or the code under test does |
4027
- | `prefer-create-mock` | `warn` | suggest | an object literal under `as SomeType` — `{ id: '1', isOffline: false } as Device`, `<Device>{ … }` too → `createMock<Device>({ … })`. A cast asks whether the two types overlap rather than whether the value is one of them, so it passes a key the type does not declare (the excess-property check is skipped) **and** a required field the fixture never sets; both type gates stay silent, and the same object is then spread into the expected payload of a call assertion. Where the literal already sits in a typed slot the first repair is to delete the cast. Silent on `as const`, `as unknown` / `as any` and the double cast built from them, on a cast of anything but a literal, and on a literal inside one of this library's own factories. `warn`, because accepting the suggestion makes a drifted fixture red on the next type-check — the finding, and a migration taken file by file |
4028
- | `no-mock-cast` | `error` | suggest | a cast to Vitest's `Mock` / `MockInstance` over a **member access** — `TestBed.inject(S).m as Mock` → `injectSpy(S).m`, which the suggestion writes whenever the token is in view (directly, or through a `const` the file settled with a `TestBed.inject`). `Mock` with no parameters is `Mock<any>`: `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments. `(spy.m.mockReturnValue as Mock)(…)` gets its own message — the cast is on the member that installs the answer, so neither the value nor the method's return type is checked. The name must resolve to an import from `vitest` / `@rstest/core` / `bun:test` / `jest`, or to nothing at all, so a domain `Mock` is left alone |
4029
- | `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset the runner already performs between tests — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — standing where nothing the file wrote ran since the runner's own: the first statement of a `beforeEach` no other `beforeEach` precedes, or a clear as the last statement of an `afterEach` → delete it, and the hook with it where that is all it held. A restore or reset in `afterEach` / `afterAll` is never reported: after the last test it is what takes a spy off before the `afterAll` hooks. **Silent** until the flags are known: options `{ clearMocks, restoreMocks, mockReset }` (or `{ configFile }` naming the runner config) first, then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text. Flag matches call, never family — `restoreMocks` walks only the spies `vi.spyOn` installed. `--fix` only in a file whose only before-hook it is; a suggestion otherwise; never inside a test body |
4030
- | `no-unasserted-argument` | `warn` | — | a bare `expect(spy).toHaveBeenCalled()` on evidence from the file: the same subject asserted with `toHaveBeenCalledWith(…)` in another test, or a title containing `with` over a body whose every assertion is a bare call → `toHaveBeenCalledWith(…)` / `mustBeCalledWith(…)`. `not.toHaveBeenCalled()` and the counting matchers are never reported |
4031
- | `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 list those classes in `{ ignoreComponents: ['CardComponent'] }` (a spec naming one is left alone) or keep a `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
4032
- | `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 |
4033
- | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
4034
- | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
4035
- | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
4036
- | `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 |
4037
-
4038
- Forty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
4039
- `no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
4040
- `prefer-set-inputs`, `no-unasserted-argument` and `prefer-spy-on-own-method`**; six fix on their own, nineteen offer suggestions. Forty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
4041
- `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
4042
- rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
4043
- 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
4044
- consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The
4045
- eight `warn`s left are not judgements about how much a finding matters. `prefer-render-shallow` is
4046
- about the _kind_ of finding: every other rule names something wrong or dead, while this one names a
4047
- file that could render more cheaply, and `renderShallow` is a migration a suite either takes or does
4048
- not. At `error` the plugin would gate that migration — 491 findings across 398 of one consumer's
4049
- 1759 spec files — so `recommended` would exist to be overridden. `no-stub-class-double` and
4050
- `no-structural-double` (5.5.0) are about the _evidence_: both report the same drift
4051
- `prefer-create-spy-from-class` reports at `error`, but neither has a `provide:` beside it to settle
4052
- the question, so each decides on a heuristic — and a project that disagrees with the reading has to
4053
- be able to switch it off without losing the rule that reads a count. The counts are not the argument
4054
- and moved a long way inside the release: measured on the same 1759 files, the two started at 12
4055
- reports in 8 files and 115 in 74, and are 10 in 7 and 5 in 4 once `prefer-provide-auto-spy` learnt to
4056
- follow a name into a `useValue` — 112 of those doubles are handed to Angular DI one name away, where
4057
- a `provide:` settles it. That rule reports **154 times across 87 files** on the same suite, all at
4058
- `error`. `no-instance-lifecycle-spy` is on the evidence too: Angular never calls an instance spy on a
4059
- hook it read off the prototype, but a spec that calls `component.ngOnInit()` itself does, and so does
4060
- the injector for a service's `ngOnDestroy`. `prefer-create-mock` (5.23.0) is graded on what its
4061
- repair _costs_: the evidence is exact — the literal and the type it claims are written on the same
4062
- line — but accepting the suggestion hands that literal to the compiler, and on a 2 032-file consumer
4063
- that is 1 200 findings in 327 files going red on one day. `no-unasserted-argument` (5.23.0) is graded on what its
4064
- repair _needs_ rather than on its evidence: both of its readings are facts out of the file — a
4065
- subject some other test in the same file pins with `toHaveBeenCalledWith`, a title saying `with` over
4066
- a body whose every assertion is a bare call — but the repair is the argument list the test should
4067
- have named, which is the one thing the rule cannot supply. Every `error` here names an edit or names
4068
- a helper; this one names a question for the author. `prefer-spy-on-own-method` names a shorter spelling of a correct call — the
4069
- `createSpyFromInstance` it reports does exactly what the helper does — and its exact shapes carry a fix. Set any of the eight `warn`s to `'error'` once the batch is done. Three of
4070
- them can report on a _correct_ project, and only one has an option:
4071
- `jasmine-namespace-without-entry` takes `['error', { setupModules: ['./test-setup'] }]`, naming the
4072
- file where `enableJasmineCompat()` is called; `prefer-native-spy-api` goes `'off'` for as long as a
4073
- suite is still running on the jasmine bridge it reports; and `no-unregistered-inject-spy` has no
4074
- option and needs none — it stays silent unless the file calls `provideAutoSpy` and its whole
4075
- `providers` array is readable (a spread, an unknown factory, `createWithAutoSpies`, `renderShallow`
4076
- or `TestBed.overrideProvider` all silence it), so the residue is a scoped `'off'`. The last four are
4077
- for a suite mid-migration off `jasmine-auto-spies` (§20). `no-mocked-for-spy` only ever touches a
4078
- **type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
4079
- meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
4080
- the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
4081
- helper's parameter, `as unknown as Mocked<T>`. It declines where it cannot prove the rename (a
4082
- `Mocked` the file declares itself, a `Spy` that is already something else, `Mocked<{ a: Mock }>`
4083
- rather than a named type) and reports without a fix.
4084
-
4085
- **The autofix is narrower than that licence.** A declaration is decidable; what the name is
4086
- _assigned_ two lines below is not — `--fix` once renamed a declaration to `Spy<T>` and left an object
4087
- literal beneath it that the new type rejects, so `eslint --fix` reported clean and `tsc` failed
4088
- afterwards. The plain fix now survives only where the value came out of `createSpyFromClass`,
4089
- `createAutoMock`, `createMock`, `mockDeep`, `injectSpy`, `asSpy` and friends, or where the annotation
4090
- belongs to no variable (a parameter, a return type, an `as` expression). Everywhere else the same
4091
- edit is a **suggestion** — accept it together with the repair at the creation site, usually
4092
- `createAutoMock<T>()` in place of the literal. **After any `--fix` run over specs, run
4093
- `npx tsc --noEmit`**: a lint pass reporting clean is not evidence that the types still hold.
4094
-
4095
- `no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
4096
- **last** provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
4097
- auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out
4098
- (found on eight tokens of one file). It reads both spellings in either order and compares tokens as
4099
- source text. A **verbatim duplicate** was already being ignored by Angular, so it carries a
4100
- suggestion to delete the dead copy (a suggestion, never `--fix`: unattended deletions inside a
4101
- `providers` array are not something to find in a diff). When
4102
- the survivor is the **barer** of the two — `provideAutoSpy(X, { gettersToSpyOn: […] })` above a bare
4103
- `provideAutoSpy(X)` — there is no edit, because which one to keep is the question; move the
4104
- configuration onto the survivor or delete it. Both messages name the token and the surviving
4105
- provider's line. A `multi: true` registration is exempt on both sides:
4106
- Angular accumulates multi providers rather than keeping the last, so two of them for one token is
4107
- the feature — a spec asserting that two `BEFORE_INIT` hooks run in registration order needs both.
4108
- Multi mixed with plain is still reported, because Angular refuses that pair at runtime
4109
- (`Cannot mix multi providers and regular providers`). For the same reason `prefer-provide-auto-spy`
4110
- says nothing about a multi provider: `provideAutoSpy` takes no registration mode, so the replacement
4111
- it would ask for does not exist.
4112
-
4113
- `no-import-time-spread` is the one that fails **while the bundle loads**, on a tree whose every test
4114
- passes: `export const events = [...BaseEvents]` at module scope raises
4115
- `Spread syntax requires ...iterable[Symbol.iterator] to be a function` when a shared chunk is
4116
- evaluated while the binding it re-exports is still `undefined`. Safe under `tsc` and under a
4117
- browser's ESM loader, which is why nothing else catches it. A function body and an instance field are
4118
- not reported (they run later); a `static` field is. An **object** spread is reported through a second
4119
- message, `noImportTimeSpreadObject`, because that half fails without failing: `[...undefined]` and
4120
- `f(...undefined)` throw, `{ ...undefined }` is `{}`, so `{ ...SectionItemType, ...Local }` loads
4121
- clean and every key it meant to copy reads `undefined` for the rest of the run. One message cannot
4122
- carry both: a reader sent looking for `Spread syntax requires …` in an object spread finds no such
4123
- error in the log and takes the report for a false positive.
4124
-
4125
- `prefer-as-spy` is the one a migration meets in bulk: a `jest-auto-spies` suite writes
4126
- `TestBed.inject(X) as Spy<X>` once per injected double, and that cast fails here with `TS2352`.
4127
- `asSpy` is a typed identity function, so `--fix` keeps the assertion, carries the type arguments
4128
- across (inference answers `Spy<Service<any>>` for a generic class) and repairs the imports. A cast
4129
- that hops through `unknown` is left alone — the hop says the value is not a `T` — except after
4130
- `TestBed.inject(X)`, where the container returns `X` by construction and the hop was only silencing
4131
- `TS2352`. Neither rule is for the object under test: a service the spec exercises is not a double,
4132
- and typing it as the class is the repair there.
4133
-
4134
- `no-expect-in-subscribe` reports one shape and **three different edits**, and says which: the
4135
- subscription is the last thing the test does (invert it into `await firstValueFrom`); something
4136
- after it is what makes the stream emit (hold the promise — `const p = expectEmission(src$)`, fire
4137
- the trigger, `await p` — because inverting deadlocks); or the assertion is in the `error` branch
4138
- (`await expect(firstValueFrom(src$)).rejects.toMatchObject(…)`). It also counts assertions the
4139
- callback reaches through a helper it calls, which used to make `subscribe((d) => assertShape(d))`
4140
- invisible. `prefer-provide-auto-spy` reads `useFactory` as well as `useValue`, through the function
4141
- in the first case and not in the second — a factory's body is what DI ends up holding, while a
4142
- function inside a `useValue` is a lazily-built double, i.e. the fix. Three rules change behaviour
4143
- when applied — whether `injectSpy(X)` finds a spy is decided by a `provideAutoSpy(X)` usually
4144
- written in another file, `mockValueProp` leaves the property writable and configurable, and
4145
- `no-expect-in-subscribe` rewrites a whole test — so `prefer-inject-spy`,
4146
- `no-object-define-property` and `no-expect-in-subscribe` only ever suggest:
4147
-
4148
- ```ts
4149
- it('maps the products', () =>
4150
- // ❌ flagged, and a suggestion is offered
4151
- new Promise<void>((done) => {
4152
- service.getProducts(id).subscribe((products) => {
4153
- expect(products).toEqual(expected);
4154
- done();
4155
- });
4156
- }));
4157
-
4158
- it('maps the products', async () => {
4159
- // ✅ what accepting it produces
4160
- const products = await firstValueFrom(service.getProducts(id));
1952
+ In [`agent-docs/eslint.md`](./agent-docs/eslint.md). Read it when configuring `vitest-auto-spy/eslint-plugin` or fixing one of its reports.
4161
1953
 
4162
- expect(products).toEqual(expected);
4163
- });
4164
- ```
4165
-
4166
- That template was 111 of 133 violations in one migration batch. The suggestion appears only for the
4167
- exact frame above — one `subscribe` statement in the executor, one block-bodied callback, `done()`
4168
- mentioned once and standing last — and the report itself now counts assertions per `subscribe`
4169
- rather than one message per `expect`, which used to double the apparent size of the job.
4170
-
4171
- `prefer-inject-spy` reads both spellings of the same mistake, which is the point of the second one:
4172
-
4173
- ```ts
4174
- vi.spyOn(TestBed.inject(AppEventsService), 'announce'); // flagged, always was
4175
- const appEvents = TestBed.inject(AppEventsService);
4176
- const announceSpy = vi.spyOn(appEvents, 'announce'); // flagged now
4177
- ```
4178
-
4179
- The variable is resolved through the scope manager, so it has to be a `const`/`let` initialised
4180
- from `TestBed.inject(...)` and never assigned again — a name bound by an import, a parameter, or a
4181
- `let` that is reassigned is left alone.
4182
-
4183
- Five tokens are left alone whatever is spied on them: `ApplicationRef`, `DestroyRef`,
4184
- `EnvironmentInjector`, `HttpClient`, `Injector`. Each is a case where this rule's advice is
4185
- impossible or self-defeating — `{ provide: DestroyRef, useValue }` is silently ignored, because
4186
- `R3Injector.get()` short-circuits on `__NG_ENV_ID__` before it reads its providers; a spied
4187
- `ApplicationRef` has no `injector` to build a component with; `HttpClient` under
4188
- `provideHttpClientTesting()` is supposed to be real. A project adds its own with
4189
- `['error', { ignoreTokens: ['MapRendererService'] }]`, compared as source text, which extends the
4190
- five rather than replacing them.
4191
-
4192
- `no-inject-before-override` catches the trap this plugin's own advice sets. `TestBed.inject()` in a
4193
- `beforeEach` — the line you write once `provideAutoSpy(X)` has taken away the literal you used to
4194
- configure — instantiates the module, and every `TestBed.override*` afterwards throws, including one
4195
- written above it inside a `createComponent` helper. Configure the double after the overrides
4196
- (`injectSpy(X)` in the test), or keep the access lazy: `const api = () => injectSpy(Api)`. The check
4197
- is order-free by design, since a helper declared above the hook still runs after it.
4198
-
4199
- `no-ts-expect-error-on-double` reports the suppression, not the reason written after it. On one
4200
- 1759-file suite it reports 34 directives in 15 files and **every one carries a reason** — so an
4201
- escape for "a directive with a reason" would have silenced all of them. Four sat on overloaded
4202
- clients, which `overload` repairs (§5); seven blamed "the collapsed generic" for a fixture the real
4203
- instantiation rejects as well; nineteen hid a fixture or a production type that disagrees with the
4204
- declared one. Four were deliberate — a value outside the declared type, to reach a default branch —
4205
- and that is the case a per-line disable is for:
4206
- `// eslint-disable-next-line vitest-auto-spy/no-ts-expect-error-on-double -- <why>` above the
4207
- directive. The report sits on the directive's line so that comment reaches it. `rejectWith`,
4208
- `failWith` and `throwWith` are not read: their parameter is `unknown`, so a directive there
4209
- suppresses something other than the stub.
4210
-
4211
- **What the plugin costs is bounded by the file, not by what is in it.** Three rules used to re-read
4212
- the file per finding-site rather than once: over this repository's 173 spec files the whole plugin
4213
- takes **56 ms** where it took 93, and on a single 1.7 MB spec **68 ms** where it took 3 263. The
4214
- rules answer the same way they did — the ordering rules (`no-inject-before-override`,
4215
- `no-overridden-provider`) collect the `override*` and `resetTestingModule` positions in one pass and
4216
- decide by range, `prefer-render-shallow` asks the template-read question once per file, and
4217
- `no-redundant-smoke-test` indexes identifiers only where a smoke test exists to judge.
4218
-
4219
- **A rule message names what it found and one repair, and ends in `Docs: <url>`** — the rule's own
4220
- section of the rules page (`utilities/eslint-rules#<rule-name>`), which is also its `meta.docs.url`.
4221
- The reasoning a message leaves out lives in that section; read it before overriding a severity.
4222
-
4223
- The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
4224
- packages, which a subpath export can never be.
1954
+ <!-- agents-outline: agent-docs/eslint.md -->
1955
+ <!-- /agents-outline -->
4225
1956
 
4226
1957
  ---
4227
1958
 
4228
1959
  ## 17. Error → fix
4229
1960
 
4230
- | Message contains | Cause | Fix |
4231
- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
4232
- | `No mock adapter registered` | no runtime entry was imported, or the wrong one — or the setup file that imports it never ran: an `@angular/build:unit-test` / `@nx/angular:unit-test` target runs only the `setupFiles` it names (`doctor` reports `builder-setup-unreached`) | the one import the message names — it detects the runner (Vitest, `bun:test`, `node:test`, Rstest) and links that runner's installation section; under the unit-test builder, name the setup file in the target's `setupFiles` |
4233
- | `No "X" export is defined on the "<m>" mock`, in a spec whose `vi.mock('<m>')` has no factory | another spec file mocked `<m>` with a factory, and under `isolate: false` Vitest hands that factory to a later automock or `{ spy: true }` mock of the same module (`doctor` reports `module-mock-leak`) | mock it the same way in both files, or `vi.resetModules()` in this file before importing it |
4234
- | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
4235
- | `… 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) |
4236
- | `expectEmission(x): no value within N ms (0 received)` | nothing triggered the stream — or, when the message says timers are fake, the callbacks feeding it wait on a clock this watchdog never advances | check the call that should make it emit and the spy feeding it (`nextWith`); under fake timers pass `{ advance: () => vi.advanceTimersByTime(ms) }` (§8) |
4237
- | ``… 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) |
4238
- | `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) |
4239
- | `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) |
4240
- | `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 |
4241
- | `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) |
4242
- | `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) |
4243
- | `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) |
4244
- | `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) |
4245
- | `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)` |
4246
- | 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) |
4247
- | `onlyMethodsToSpyOn names 'x' (did you mean 'y'?), not a method of X` | a typo, or a method the constructor assigns (an arrow property, a `signal()` field, a `signalStore()` method) | take the name the message suggests; with no suggestion it names `instanceMethodsToSpyOn: ['x']`, which adds to the discovered methods (§5) |
4248
- | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
4249
- | `toHaveBeenCalledTimes` reads `0` for a call an earlier test made, after moving to Vitest 5 | `clearMocks` defaults to `true` on Vitest 5, so `vi.clearAllMocks()` runs before every test | count the calls in a plain variable instead of reading the spy, or set `clearMocks: false` (§10) |
4250
- | `X.method is set up with mustBeCalledWith, and this call matches none of its configs` | the code called the spy with other arguments; with one config set up the line ends with the first that differs (`argument 2: expected 'eu', got 'us'`). `X.` appears on a strict double only | that is the assertion firing — fix the value the code under test passes, or configure that call too; relax to `calledWith` when other arguments are fine |
4251
- | `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 |
4252
- | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
4253
- | `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) |
4254
- | `X.method(…) was called; this strict double has nothing configured for it` | a strict double was asked for a method no line configured | paste the call the `Configure it in the test:` line names (`calledWith(args).mockReturnValue(…)`, or `mockReturnValue(…)` for any); `Called from` is the calling line; or `{ strict: false }` on that double (§5) |
4255
- | `X.member was read N times on a strict double and nothing configured it` (or `was subscribed to N times on a strict double and never emitted`) | `setupAutoSpy({ unconfiguredReads })`, after the test — the code under test read a strict double's spied getter and got `undefined`, or subscribed to its observable property and nothing ever emitted | the advice under each finding names the member: `accessorSpies.getters.url.mockReturnValue(…)` for a getter, `events.nextWith(…)` or `overrides: { events: new Subject() }` for a stream; `mockReturnValue(undefined)` when `undefined` is the answer meant (§5) |
4256
- | `[vitest-auto-spy] "…" wrote to console.<method> 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 | the report names the spy to assert (`installConsoleSpies()` in a `beforeEach`, then `consoleErrorSpy`, …), or says the `vi.spyOn` calls through and needs `.mockImplementation(() => undefined)`; or fix the code that printed. Importing a spy installs nothing under the guard |
4257
- | `… wrote to the console N time(s) outside any test` | output while the file was imported, in a `beforeAll` / `afterAll`, from a callback after its test ended, or from a test whose `afterEach` never ran — the frame names the line | move the work into a test and absorb it there; a callback firing late is a leak — clear it where it is scheduled |
4258
- | `… wrote to console.<method> N time(s) while the file was being imported` | a module the file imports wrote while it was evaluated (a static block, a decorator, a top-level call), or a `describe` body did — before any hook, so no spy absorbs it; it lands on the first file of the worker to import that module | fix the line the report names, reading the `Likely cause:` block (`NG0912`: two copies of one component in the bundle — import it from one place); `strayConsole: { allow: [...] }` only for a line inside `node_modules`, which the report offers itself |
4259
- | `It is reported at the end of b.spec.ts: the file-end check of a.spec.ts did not run.` | `a.spec.ts`'s own `afterAll` threw, so its file-end checks never ran and its console output was carried into the next file | fix that `afterAll` first — its error is in `a.spec.ts`'s output — then the output the report quotes, in the file it names |
4260
- | `"…" made N call(s) a strict double had nothing configured for, and the throw never reached the test` | `setupAutoSpy({ strict: true })` — a strict double threw, and a `catch` in the code under test or an RxJS subscriber with no error callback swallowed it; each call is quoted with its arguments and the line that made it | configure that method (`.mockReturnValue`, `.calledWith` or `returns`), or drain it with `takeStrictViolations()` when the test provokes it on purpose (§5) |
4261
- | `a.spec.ts left N timer(s) pending when it ended` | `onStrayTimers: 'throw'` — a timer outlived its file; each line names the test that scheduled it (or `while the file was imported`, `outside any test`), and it was cancelled | clear it in the test named — `clearTimeout`, unsubscribe, `fixture.destroy()` — or run it out with fake timers before the test ends (§10) |
4262
- | `a.spec.ts left N window/document listener(s) attached when it ended` | `onStrayListeners: 'throw'` — a listener outlived its file; each line names the test that added it, and it was removed | remove it in that test — `removeEventListener` with the same capture flag, an `AbortSignal`, `fixture.destroy()` (§10) |
4263
- | `N file-end checks failed when a.spec.ts ended` | several file-end reports failed together — stray timers, listeners, console output — and each numbered block is one of them | fix each block on its own; Vitest also lists every member error separately |
4264
- | `fetch is stubbed in unit tests — GET <url>. The test "…" requested it` | `blockNetwork` refused a request the named test made (a blocked `XMLHttpRequest` carries the same marker on `statusText`) | answer it in that test — `vi.spyOn(globalThis, 'fetch').mockResolvedValue(stubResponse({ body: … }))` — or `blockNetwork({ xhr: 'empty' })` for an XHR nobody reads (§10) |
4265
- | `X.member.nextWithValues() ran after something subscribed to X.member` | `nextWithValues` on an observable property publishes a new stream, which the subscriber already holding the old one never sees; `X.` appears on a strict double only | call it before the code under test subscribes, or push into the held stream with `nextWith()`; a throw under `setupAutoSpy({ misconfiguration: 'throw' })` |
4266
- | `assertMocked(…): x is the real function — … did not apply` (or `no export is a mock`) | the module mock did not reach the code under test; the message names the cause that fits the run — `isolate: false` with the module already loaded, or the code importing it through another path | under `isolate: false` mock it in the earlier file too or run this file isolated; otherwise `vi.mock` the specifier the code imports, or inject the dependency |
4267
- | `setupAutoSpy({ strict: true })` has no effect — no double throws, a suite with it on stays green | before this release the default lived in module scope, and `/setup` and the factory bundles each carried a copy; a 1759-file consumer ran 12 717 tests green with it on | upgrade — the default, `onUnstubbedCall` and `setSpyEngine()` now live on `globalThis` |
4268
- | a seeded getter reads `undefined`: `provideAutoSpy(X, { overrides: { flagsConfig } })` next to `gettersToSpyOn` or a `registerAutoSpyDefaults` row | before this release the seed was assigned into the spied accessor's setter, and the getter kept answering `undefined` | upgrade — `overrides` now seeds the getter spy; a later `accessorSpies.getters.x.mockReturnValue(…)` still wins |
4269
- | `Cannot read properties of undefined (reading 'name')` on `value.constructor.name`, with `createAutoMock<T>()` | `constructor` answered `undefined` on an auto-mock | upgrade — it answers `Object`, as every other double does; or seed it: `createAutoMock<T>({ constructor: … })` |
4270
- | `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
4271
- | `Cannot spy on this instance in place: it is not extensible` | `createSpyFromInstance` was handed a frozen or sealed object | spy it before it is frozen, or build a separate double with `createSpyFromClass` (§2) |
4272
- | `createSpyFromInstance(X): no onlyMethodsToSpyOn was given for a live DOM/BOM object` | discovery is about to walk the whole prototype chain of a live `Node` / `EventTarget`, engine internals included — the bare-array shorthand does not avoid this, since it merges as `methodsToSpyOn`, not `onlyMethodsToSpyOn` | list the methods the test needs: `{ onlyMethodsToSpyOn: [...] }`; for one method on a node that must keep living a DOM life, `mockValueProp` / `spyOnVoidMethod` instead (§2, live DOM node) |
4273
- | `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()` |
4274
- | `overrideComponentProvider(…): the override did not apply — C resolved T to …` | the component injects a different token, or something configured the token again after the call; the message says whether the real service or a different double won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
4275
- | `.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) |
4276
- | `@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) |
4277
- | `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) |
4278
- | `ngModuleScopes: X is imported into the testing module but contributes nothing` | `enableAngularDiagnostics({ ngModuleScopes })` — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the directives it exports are missing (`NG0303` / `NG0304`) | import the directives it exports directly, or declare them in the TestBed (§13) |
4279
- | `assertNgModuleScopes(): X has an empty runtime scope` | the same stripped `ɵɵsetNgModuleScope`, found by the manual check; a module with providers of its own may be providers-only, and the message says so | declare what the spec needs in the TestBed module directly; leave a providers-only module out of the call (§13) |
4280
- | `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) |
4281
- | `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) |
4282
- | `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
4283
- | `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
4284
- | `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
4285
- | `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
4286
- | `injectSpy(X): got a real X` (or `got a plain value, not an auto-spy` for an `InjectionToken`) | nothing in the testing module provides a double; a `console.warn` once per token and spec file, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` or `setupAutoSpy({ misconfiguration: 'throw' })` | the fix the message names: `provideAutoSpy(X)` in `providers`, or `{ provide: TOKEN, useValue: createAutoMock<T>() }` for a token; `TestBed.inject(X)` if the real thing is what the spec wants |
4287
- | `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
4288
- | `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
4289
- | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
4290
- | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
4291
- | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
4292
- | `cannot read "…" referenced by …: ENOENT at …` | a `templateUrl` / `styleUrl` path does not resolve; the errno (`ENOENT`, `EACCES`) and the resolved path say why | fix the path, relative to the component file |
4293
- | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade. A setup file that initialises the Angular test environment before the library loads splits Angular the same way — two copies, a second `TestBed`, and `No mock adapter registered` or overrides that never apply: `resolve.dedupe: ['@angular/core', '@angular/common', '@angular/platform-browser', '@angular/compiler', 'rxjs']` plus `test.server.deps.inline: ['vitest-auto-spy']` (Vitest 1 and up) leaves one copy of each |
4294
- | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
4295
- | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
4296
- | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
4297
- | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
4298
- | `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
4299
- | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
4300
- | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
4301
- | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
4302
- | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
4303
- | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
4304
- | a spy on a storage method still answering after `vi.restoreAllMocks()` probed green | happy-dom's Storage proxy deletes only stored items, so the restore behind `mockRestore()` never lands; a later spec's `vi.spyOn` gets the same mock with the old calls in it | on by default — `setupAutoSpy()` repairs it at the file boundary; `restoreStorageSpies()` on its own, or `restoreStorageSpies: false` for a worker-lifetime spy (§10) |
4305
- | a listener callback running against mocks and a DOM it was never written for, in a file that never added it | a `window`/`document` listener a previous file left registered, under `isolate: false` | `setupAutoSpy({ strayListeners: true })` — module-graph registrations stay, per-file ones go at the boundary (§10) |
4306
- | a global a previous file patched (`ResizeObserver` answering a stub) with no file named | a raw assignment no registry tracks — not `vi.stubGlobal`, not `vi.spyOn` | `setupAutoSpy({ restoreGlobals: true })` puts the worker's baseline back at every file boundary; `guardGlobals` for the non-configurable case (§10) |
4307
- | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
4308
- | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
4309
- | `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
4310
- | `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 |
4311
- | `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 |
4312
- | `mockSignalProp: '…' is an input() signal` | an `input()` replaced instead of set | `fixture.componentRef.setInput(name, value)`, or `renderShallow(C, { inputs: { … } })` |
4313
- | `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()` |
4314
- | `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 |
4315
- | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
4316
- | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
4317
- | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
4318
- | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
4319
- | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
4320
- | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
4321
- | `createComponentStub(): X carries no ɵcmp, ɵdir or ɵpipe to copy` | the class is not an Angular declaration, or the import resolved to `undefined` (a barrel split across chunks) | pass the `@Component` / `@Directive` / `@Pipe` class, imported from its own file (§13) |
4322
- | `'isKeyEnabled' does not exist in type 'MethodReturns<{ flagsConfig: any; }>'` | generic class: `createSpyFromClass(Config, { gettersToSpyOn: ['flagsConfig'], returns: { … } })` reads `T` back from the list, because the generic class argument is checked after the configuration | generic class: spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` from `/angular` infers it (§6) |
4323
- | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
4324
- | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
4325
- | 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 |
4326
- | 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 |
4327
- | `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({ … }))` |
4328
- | `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 |
4329
- | `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 })` |
4330
- | `[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` |
4331
- | `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 |
4332
- | `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'] })` |
4333
- | `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 |
4334
- | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
4335
- | 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…())` |
4336
- | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the test; then the `mock*Prop` helper the message names |
4337
- | a spec green alone that fails only in a full run, on something it reads off `<body>` / `<html>` (`querySelector('[data-…]')`, a class) | an earlier file in the worker left an attribute on the shared document | `setupAutoSpy({ documentPollution: 'throw' })` (in `preset: 'strict'`) names the test; take the attribute off in `ngOnDestroy` / `DestroyRef.onDestroy` or an `afterEach` |
4338
- | `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()` |
4339
- | `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 |
4340
- | 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 |
4341
- | `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" |
4342
- | 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 })` |
4343
- | 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 |
4344
- | `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 |
4345
- | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
4346
- | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
4347
- | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
4348
- | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` — never `@ts-expect-error`, which `no-ts-expect-error-on-double` reports (§16) |
4349
- | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
4350
- | `setInputs: … declares no input named …` (with a `Did you mean` for a near typo) | the key is not an input of that component — a typo, an input renamed since, or one of its own fields | take the suggestion, or set a name the message lists; a signal the component owns is moved through the signal (§13) |
4351
- | `setInputs: X carries no ɵcmp, so there are no inputs to set` (or `the component class is undefined`, `X is a @Pipe`) | the fixture's `componentType` is not a compiled `@Component` — a `@Pipe`, a class Angular never compiled, or `undefined` from a barrel split across chunks | pass the `@Component` class the fixture was created from; call a pipe's `transform()` directly; for `undefined` import the class from its own file (§13) |
4352
- | `TypeError: dependencies is not a function`, from `renderShallow({ keepTemplate: true })` | the compiled `ɵcmp.dependencies` is the flat array AOT emits when no cycle forced a factory, and the trim called it | upgrade — all three shapes (array, factory, `null`) are read; nothing to change in the spec |
4353
- | `renderShallow(X, { keepTemplate: true }): … is declared by Module, not standalone` (or `by an NgModule`) | an AOT dependency list: ngtsc flattened an imported `NgModule`'s exported declarations into it, and Angular takes only standalone declarations and `NgModule`s in `imports` | name that module in `keepModules: [Module]` and it is put back whole — the message names it when the spec imports it; or drop `keepTemplate` when the spec reads TypeScript state only |
4354
- | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
4355
- | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
4356
- | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
4357
- | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic and keeps the `readonly` of the source member — deliberately: on a member replaced by a **spied accessor** the assignment reaches the setter spy and the getter still answers `undefined`, so the type error is the loud version of a silent no-op | `mockValueProp(spy, 'X', value)` — it defines rather than assigns, so it works in both cases; `Mutable<Spy<T>>` is the opt-in for plain data members, and `Reflect.set` is **not** a way round it |
4358
- | `TS2345` from `mockReturnValue` / `mockImplementation` / `mockResolvedValue` on a double that used to compile | the mock surface is `MockInstance<Method>`, so the stub is now checked against the method's own return type | stub with what the method returns; if the double is meant to answer something else, the method's type or the spec's expectation is wrong |
4359
- | `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` on `mockReturnValue(double)` | 5.1.0: the stub is checked against the method, and a double of the return type is a `Spy<X>`, not an `X` | `mockReturnValue(asInstance(double))` (§6) |
4360
- | `with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties` on a stub | the stub's type spells an optional member `?: T` where the method's spells `?: T \| undefined` (or the reverse), or a fixture writes `key: undefined`; a `Subject` is invariant, so its type argument has to match exactly | `method.returnSubject()` for a subject typed from the method; omit the key instead of writing `undefined` |
4361
- | `Types of parameters 'x' and 'y' are incompatible` on `mockImplementation(…)` | 5.1.0: the implementation is checked against the method's parameters, and an annotation in it disagrees | drop the parameter annotations and let the method type them; narrow inside the body |
4362
- | `TS4111` / `TS2375` / `TS2322` inside a `mockImplementation` body that used to compile | before 5.1.0 the implementation's parameters were `any`, so nothing in its body was checked | fix the body — the error is ordinary once its parameters are typed |
4363
- | `TS2349: This expression is not callable` on a value read from `spy.mock.calls` | 5.1.0 types `mock.calls` from the method; a listener taken from `addEventListener`'s calls is `EventListenerOrEventListenerObject` | narrow before calling: `if (typeof listener === 'function') listener(event)` |
4364
- | `is not assignable to parameter of type 'RxMethod<…>'` (or another member type) on `mockReadonlyProp(x, 'm', vi.fn(impl))` | `vi.fn` infers its type from the member the helper replaces, so `impl` must return what the member returns | return the member's type — `Object.assign(vi.fn(), { destroy: vi.fn() })` for an `rxMethod` |
4365
- | `Argument of type '() => A \| null' is not assignable to parameter of type '(() => A) \| (() => null)'` | the member is a union of call signatures — `@ngrx/signals`' `DeepSignal<A> \| Signal<null>` for a nullable slice — and 5.1.0–5.24.0 typed its stub as the union of both halves | upgrade past 5.24.0, which types the stub as one signature returning the union |
4366
- | `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` |
4367
- | 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 |
4368
- | `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` |
4369
- | 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 |
4370
- | 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) |
4371
- | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
4372
- | 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) |
4373
- | `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | the stderr report names the file, the first three timers and the test that scheduled each; `onStrayTimers` gets every one in `timers` (§10) |
4374
- | `createNestUnit(X): parameter #n of Y is typed as \`Object\``(or`undefined — usually a circular import`) | the compiler emitted no runtime class for that parameter — interface, union, primitive, or a circular import | `@Inject(TOKEN)` plus a `providers` entry, or `@Optional()` to receive `undefined`; `@Inject(forwardRef(() => X))` for the import cycle |
4375
- | `createNestUnit(X): Y declares n constructor parameter(s) but carries no design:paramtypes metadata` | no `emitDecoratorMetadata` (esbuild / Vite), or `reflect-metadata` loaded after the class | tsc or SWC with `decoratorMetadata`, `@Injectable()`, `reflect-metadata` first; or `@Inject(X)` on every parameter |
4376
- | `createNestUnit(X): A -> B -> A is a cycle among the classes built for real` | two exposed / `useClass` classes depend on each other; the helper does not resolve `forwardRef` cycles | expose one side less, or provide it |
4377
- | `createNestUnit(X).spies.get(Y): the unit never asked for that token` | same guard as `createWithAutoSpies` — the spy would not be the one the unit uses | stub the token the unit actually injects (the message lists the auto-spied ones) |
4378
- | `createNestUnit(X).spies.get(Y): Y is in \`expose\`, so the unit got a real instance` | an exposed class is real, not a spy | read the spies of its own collaborators, or drop it from `expose` |
4379
- | `expect(component.ngOnInit).toHaveBeenCalled()` fails after `fixture.detectChanges()`, or a hook's `.mockImplementation` never runs | the spy was put on the **instance**; a view calls the hook it read off the class prototype when the component was created | `vi.spyOn(Cls.prototype, 'ngOnInit')` before `TestBed.createComponent`, or assert what the hook does — `no-instance-lifecycle-spy` (§16) |
4380
- | `TS2345: Argument of type 'InjectionToken<X>' is not assignable to parameter of type 'ClassType<unknown>'`, on `registerAutoSpyDefaults` | the core entry's `registerAutoSpyDefaults` takes a class only — it may not name Angular's token type | import `registerAutoSpyDefaults` (and `clearAutoSpyDefaults`) from `vitest-auto-spy/angular`, which also takes an `InjectionToken` (§5) |
4381
- | `returns names 'x', not a method of X` (or `which onlyMethodsToSpyOn left out`) | the name is not a method of the double — a typo, or a restricting `onlyMethodsToSpyOn` left it out; on a type-driven double, `then` and `constructor` are never spies; a method the constructor assigns (an ngrx `rxMethod`) is not on the prototype | the message names the fix: the close method it meant, adding it to `onlyMethodsToSpyOn`, or `instanceMethodsToSpyOn` for a constructor-assigned method; the value is silently never returned otherwise (§5) |
4382
- | a spec green over a `useValue` fixture whose key the code never reads — `queryParams$` on an `ActivatedRoute` | `useValue` is `any`, so a renamed or misspelled key of an object literal compiles | the member's real name — `no-unknown-use-value-key` (§16), with type information; or `provideAutoSpy(X, { overrides })`, which the compiler checks |
4383
- | a spec runs down the "browser" (or any flag-on) branch whatever its `useValue` says | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` — `useValue` is `any`, and an object is truthy | a value of the token's type (`useValue: false`) — `no-mistyped-use-value` (§16), with type information |
4384
- | `expectRequest: no request matched … Made instead: …` | the URL or predicate describes nothing the code under test sent | compare with the requests listed — they are the ones actually made; then fix the URL or the predicate (§13) |
4385
- | `expectRequest: no POST … — but GET … was made` | only the verb differs | pass `{ method }` for the verb the code sends, or fix the verb it sends (§13) |
4386
- | `expectRequest: no GET …?page=2 — but GET …?page=3 was made; the query differs` | the same path, another query string | name the query the code sends, or match the path alone: `expectRequest('/api/products')` (§13) |
4387
- | `expectRequest: no request matched … — nothing was requested at all` | an `httpResource()` whose `request()` returned no URL yet, or an `HttpClient` call nothing subscribed to | set the signal the resource reads, or subscribe to the call (§13) |
4388
- | `this TestBed has no HttpTestingController` | `expectRequest` / `expectNoRequest` on a module that never got the testing backend | `TestBed.configureTestingModule({ providers: [...provideHttpTesting()] })` from `vitest-auto-spy/angular-http` (§13) |
4389
- | `X was never answered (end of "…")` / `N requests were never answered` | `provideHttpTesting({ verifyOnTeardown })` or `enableAngularDiagnostics({ pendingRequests })` — the test left a request open; the message names its URL and the test | answer it with the line the message prints (`await expectRequest(url).flush(body)`, `controller.expectOne(url).flush(body)` under diagnostics), or assert its absence (`expectNoRequest(url)`); a cancelled one is forgiven with the `ignoreCancelled` opt-in the message names (§13) |
4390
- | `injectActivatedRoute(): nothing provides ActivatedRoute here` | no `ActivatedRoute` provider in the injector it read | `provideActivatedRoute({ … })` from `vitest-auto-spy/angular-router` in `providers`; `injectActivatedRoute(fixture.debugElement.injector)` for a component-level one (§13) |
4391
- | `the ActivatedRoute here is … not one provideActivatedRoute() built` | a later provider of `ActivatedRoute` won — `provideRouter()`, `RouterModule`, a `useValue`, `provideAutoSpy(ActivatedRoute)` | list `provideActivatedRoute()` last, or drop the other provider (§13) |
4392
- | `provideActivatedRoute: @angular/router <version> does not wire ActivatedRoute …` | a router major builds `ActivatedRoute` with different internal constructors | report it with the `@angular/router` version; provide the route by hand until then (§13) |
4393
- | `provideActivatedRoute({ title }): @angular/router <version> keeps the route title under a key …` | that router version stores the resolved title under a key this helper could not find | leave `title` out of the init for now, and report it with the `@angular/router` version (§13) |
4394
- | `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `isActive`, `resetConfig`, `lastSuccessfulNavigation` | the double covers `url`, `events`, `navigate`, `navigateByUrl`, `createUrlTree`, `serializeUrl`, `parseUrl`, `routerState` and `currentNavigation` / `getCurrentNavigation()`; past that, a real `provideRouter([])` with `RouterTestingHarness` (§13) |
4395
- | `injectRouterDouble(): the Router here is Angular's own …` | `Router` is `providedIn: 'root'`, so a `TestBed` without the double hands out a real one | add `provideRouterDouble({ … })` from `vitest-auto-spy/angular-router` to `providers` (§13) |
4396
- | `collectRouterEvents().expect(): the events differ at #N` (or `#N … never came`, `#N … was not expected`) | the recorded navigation events part from the expected ones there; the message prints both sequences under it | read the `Expected:` / `Recorded:` lines under it, then fix the expectation or the navigation the test drives (§13) |
4397
-
4398
- **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
4399
- nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
4400
- `gettersToSpyOn: ['X']`; a `mock*Prop` on a `createAutoMock` double changing nothing;
4401
- `gettersToSpyOn` rejecting a `Signal` key; `mockReadonlyProp` rejecting a real `signal()`;
4402
- `mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
4403
- never patched anything.
4404
-
4405
- **When the double is the suspect and no message names it, print it.**
4406
- `import { explainSpy } from 'vitest-auto-spy/diagnostics'` — it sits on that entry, not the root,
4407
- because ESM re-export is eager and a reader calls it at a breakpoint, not from a spec.
4408
- `console.log(explainSpy(spy))`
4409
- reports every spied member with its configured argument lists numbered and every recorded call
4410
- attributed to the config it hit — or to the default it fell through to, which is the shape of "the
4411
- method answered `undefined` and nothing said why". `explainSpy(spy, 'load')` narrows to one member,
4412
- `explainSpy(spy.load)` takes a bare function spy, `calledWith` and `mustBeCalledWith` share one
4413
- numbering, and it never throws — a foreign mock is reported as one in the text. The wording is a
4414
- diagnostic and is free to change: print it, never assert on it.
4415
- <https://asdalexey.github.io/vitest-auto-spy/utilities/explain-spy>.
1961
+ In [`agent-docs/errors.md`](./agent-docs/errors.md). A table from the text of a failure to its fix. Do not read it front to back — grep it by the message: `grep -n -F '<text of the error>' node_modules/vitest-auto-spy/agent-docs/errors.md`.
1962
+
1963
+ <!-- agents-outline: agent-docs/errors.md -->
1964
+ <!-- /agents-outline -->
4416
1965
 
4417
1966
  ---
4418
1967
 
4419
1968
  ## 18. Do not write this
4420
1969
 
4421
- | ❌ | ✅ |
4422
- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
4423
- | `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
4424
- | `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
4425
- | `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
4426
- | `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
4427
- | `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
4428
- | `{ provide: ActivatedRoute, useValue: { snapshot: { params } } }` | `provideActivatedRoute({ params })` — streams and snapshot from one record |
4429
- | `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate } })` | `provideRouterDouble({ url })` — one URL behind `url`, `routerState` and `events` |
4430
- | `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
4431
- | `vi.spyOn(component, 'ngOnInit')` after `createComponent` | `vi.spyOn(Cls.prototype, 'ngOnInit')` before it, or assert the effect |
4432
- | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` | `useValue: false` — a value of the type the token declares |
4433
- | `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
4434
- | `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
4435
- | `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
4436
- | `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
4437
- | `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
4438
- | `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
4439
- | `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
4440
- | `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
4441
- | hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
4442
- | `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
4443
- | a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
4444
- | a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
4445
- | `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
4446
- | `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
4447
- | `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
4448
- | `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
4449
- | `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
4450
- | `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
4451
- | a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
4452
- | an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
4453
- | `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
4454
- | `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
4455
- | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
4456
- | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
4457
- | `const x = a?.b; assert.exists(x); … x …` on every optional read | `narrow.defined(a?.b)` — it returns the value, so the narrowing sits in the expression |
4458
- | `expect(body).toBeInstanceOf(FormData); const form = body as FormData` | `narrow.instanceOf(body, FormData)` — the check and the type in one expression |
4459
- | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
4460
- | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
4461
- | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
4462
- | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
4463
- | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
4464
- | a bare `await import('…')` in a test body | `await settleDynamicImport(() => import('…'))` — `prefer-settle-dynamic-import` reports it |
4465
- | a capture whose only assertions are `toEqual([])` / `not.toHaveBeenCalled()` | `await expectNoEmission(source$)` — `no-vacuous-absence-assertion` reports it |
4466
- | `{ id: '1', isOffline: false } as Device` (a cast passes an excess key) | `createMock<Device>({ id: '1' })` — `prefer-create-mock` reports it |
4467
- | `TestBed.inject(S).m as Mock` (`Mock` is `Mock<any>`) | `injectSpy(S).m` — `no-mock-cast` reports it |
4468
- | `await fixture.whenRenderingDone()` | `await stable(fixture)` |
4469
- | an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
4470
- | `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
4471
- | `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
4472
- | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
4473
- | `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift |
4474
- | a `TestingStorage` class assigned to `globalThis.localStorage` | `stubWebStorage('localStorage')`, restored with the other patches |
4475
- | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
4476
- | `configureTestingModule` inside every `it()` | one per `describe` |
4477
- | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
4478
- | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
4479
- | `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
4480
- | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
4481
- | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
4482
- | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
4483
- | `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
1970
+ | ❌ | ✅ |
1971
+ | --- | --- |
1972
+ | `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
1973
+ | `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
1974
+ | `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
1975
+ | `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
1976
+ | `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
1977
+ | `{ provide: ActivatedRoute, useValue: { snapshot: { params } } }` | `provideActivatedRoute({ params })` — streams and snapshot from one record |
1978
+ | `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate } })` | `provideRouterDouble({ url })` — one URL behind `url`, `routerState` and `events` |
1979
+ | `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
1980
+ | `vi.spyOn(component, 'ngOnInit')` after `createComponent` | `vi.spyOn(Cls.prototype, 'ngOnInit')` before it, or assert the effect |
1981
+ | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` | `useValue: false` — a value of the type the token declares |
1982
+ | `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
1983
+ | `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
1984
+ | `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
1985
+ | `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
1986
+ | `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
1987
+ | `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
1988
+ | `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
1989
+ | `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
1990
+ | hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
1991
+ | `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
1992
+ | a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
1993
+ | a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
1994
+ | `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
1995
+ | `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
1996
+ | `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
1997
+ | `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
1998
+ | `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
1999
+ | `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
2000
+ | a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
2001
+ | an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
2002
+ | `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
2003
+ | `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
2004
+ | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
2005
+ | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
2006
+ | `const x = a?.b; assert.exists(x); … x …` on every optional read | `narrow.defined(a?.b)` — it returns the value, so the narrowing sits in the expression |
2007
+ | `expect(body).toBeInstanceOf(FormData); const form = body as FormData` | `narrow.instanceOf(body, FormData)` — the check and the type in one expression |
2008
+ | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
2009
+ | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
2010
+ | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
2011
+ | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
2012
+ | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
2013
+ | a bare `await import('…')` in a test body | `await settleDynamicImport(() => import('…'))` — `prefer-settle-dynamic-import` reports it |
2014
+ | a capture whose only assertions are `toEqual([])` / `not.toHaveBeenCalled()` | `await expectNoEmission(source$)` — `no-vacuous-absence-assertion` reports it |
2015
+ | `{ id: '1', isOffline: false } as Device` (a cast passes an excess key) | `createMock<Device>({ id: '1' })` — `prefer-create-mock` reports it |
2016
+ | `TestBed.inject(S).m as Mock` (`Mock` is `Mock<any>`) | `injectSpy(S).m` — `no-mock-cast` reports it |
2017
+ | `await fixture.whenRenderingDone()` | `await stable(fixture)` |
2018
+ | an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
2019
+ | `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
2020
+ | `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
2021
+ | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
2022
+ | `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift |
2023
+ | a `TestingStorage` class assigned to `globalThis.localStorage` | `stubWebStorage('localStorage')`, restored with the other patches |
2024
+ | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
2025
+ | `configureTestingModule` inside every `it()` | one per `describe` |
2026
+ | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
2027
+ | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
2028
+ | `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
2029
+ | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
2030
+ | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
2031
+ | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
2032
+ | `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
4484
2033
 
4485
2034
  **A hand-rolled double trips `rxjs-x/finnish`, and a double from this package does not.** The rule
4486
2035
  reports any name whose type is an `Observable` **or whose call signature returns one**
@@ -4518,15 +2067,15 @@ anyone writing a codemod, because the rename looks like the safest edit in the f
4518
2067
  Jest-era tutorials and cheat sheets repeat a handful of lines that fail on Vitest, or pass and leak.
4519
2068
  Each was checked on Vitest 5.0.0:
4520
2069
 
4521
- | Circulating advice | What happens | Write instead |
4522
- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
4523
- | `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 })` |
4524
- | "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` |
4525
- | `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
4526
- | `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
4527
- | `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)` |
4528
- | `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
4529
- | `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 |
2070
+ | Circulating advice | What happens | Write instead |
2071
+ | --- | --- | --- |
2072
+ | `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 })` |
2073
+ | "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` |
2074
+ | `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
2075
+ | `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
2076
+ | `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)` |
2077
+ | `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
2078
+ | `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 |
4530
2079
 
4531
2080
  ---
4532
2081
 
@@ -4559,8 +2108,25 @@ file an Angular or Nx unit-test target never runs because the target does not na
4559
2108
  its checks are about coverage, where the run is green and the report is simply not the one the
4560
2109
  config describes — `coverage.all` on a Vitest that stopped reading the key, a source-only
4561
2110
  `coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
4562
- recompiling it per file costs more than collecting the coverage does. It is
4563
- worth one run after any large edit to a test suite — especially after a codemod, which is where
2111
+ recompiling it per file costs more than collecting the coverage does. Two are about the Angular
2112
+ builder's version: `@angular/build` in `[22.1.5, 22.1.7)` (`angular-build-splitting-off`), and an
2113
+ `@analogjs/vite-plugin-angular` below 2.7.5 next to `@angular/build` 22.2 or newer
2114
+ (`analog-behind-angular-build`), which dies at startup with `TypeError: cache.has is not a function`.
2115
+ Five are about Vitest 5. On Vitest 5, `vitest-5-removed` reports what it took away and the run now
2116
+ trips over — an import from `vitest/reporters`, `vitest/coverage`, `vitest/environments`,
2117
+ `vitest/snapshot`, `vitest/runners`, `vitest/suite` or `vitest/mocker`, a `.sequential` chain,
2118
+ `--outputJson`/`--compare` in a script — as errors; on Vitest 4 the same lines are upgrade notes.
2119
+ `poolOptions` on Vitest 4 or newer is a warning, because Vitest runs without every option inside it.
2120
+ `vitest-5-deprecated` names the two renamed programmatic calls nothing warns about. `vitest-5-clear-mocks`
2121
+ says `clearMocks: true` restates the Vitest 5 default, and tells a Vitest 4 suite that never set it
2122
+ to price the change with `npx vitest run --clearMocks` first. `vitest-5-available` tells a Vitest 4
2123
+ repository what the upgrade buys — or names what holds it back: `@angular/build` below 22.2.0 beside
2124
+ a unit-test target, Analog below 2.7.5, `vite` below 6.4, or Node below 22.12 in `.nvmrc`,
2125
+ `.node-version`, CI or a private package's `engines`. `fs-module-cache-not-persisted` warns when
2126
+ `fsModuleCache` is on and no CI config caches its directory. Renamed config keys Vitest 5 still
2127
+ honours (`experimental.fsModuleCache`, `browser.isolate`, `cache.dir`, …) are left to Vitest's own
2128
+ deprecation line.
2129
+ It is worth one run after any large edit to a test suite — especially after a codemod, which is where
4564
2130
  the eaten glob below came from.
4565
2131
 
4566
2132
  Two of its checks are about this package's own names. `helper-from-wrong-entry` catches a named
@@ -4633,7 +2199,8 @@ npx vitest-auto-spy perf src/some/dir # a path is passed through to Vitest as a
4633
2199
 
4634
2200
  It reads Vitest's own per-file phase timings through `TestModule.diagnostic()` — nothing here
4635
2201
  parses terminal output — and reports six phases: `environment`, `import`, `tests`, `setup`,
4636
- `prepare` (all measured per file) and `transform` (measured once for the whole run). **The phase
2202
+ `prepare` (all measured per file) and `transform` (measured once for the whole run before Vitest 5,
2203
+ per file on 5, where it is the wait for Vite's transforms and `import`/`setup` are net of it). **The phase
4637
2204
  totals are CPU time summed across every worker, not wall clock** — a report showing `20.29s of CPU
4638
2205
  time` under `986ms wall clock` is not a bug, it is the work spread over workers.
4639
2206
 
@@ -4665,6 +2232,23 @@ about 2.8 % of wall clock. The finding itself counts this machine's cores
4665
2232
  figures above are in the docs section, not in the message. Do not quote a worker count as
4666
2233
  universally right — it is a property of the machine.
4667
2234
 
2235
+ **On Vitest 5, `perf` reads what Vitest 5 reports; an older Vitest prints exactly what it did before.**
2236
+ The advice reads the configuration Vitest resolved (`isolate`, `pool`, `maxWorkers`, `environment`,
2237
+ `fsModuleCache`) rather than the config text, and never advises against an option you set
2238
+ explicitly. `perf-transform` fires when the wait for Vite's transforms is 30 % or more of the CPU time
2239
+ with the module cache off, and advises `fsModuleCache: true` — kept between CI pipelines, or it only
2240
+ helps locally. `perf-long-pole` names the file still running alone after every other lane went idle,
2241
+ when that tail is at least 2 s and 30 % of the span. `perf-isolation` adds the workers spawned and
2242
+ their summed start-up, with an "at least" wall-clock saving; `perf-workers` counts the lanes the run
2243
+ used rather than the machine's cores; under `isolate: false`, `perf-heap` lists what each file added
2244
+ to its lane's heap. Environment time is counted once per lane (`concurrencyId`) and value — not per
2245
+ `workerId`, which Vitest 5 renews for every file even in a reused worker. A finding about a switch
2246
+ also prints `perf-vitest-doctor`: confirm it with `npx vitest doctor`, Vitest's own A/B runner, not
2247
+ this package's `doctor`. A bare run on Vitest 5 passes `--experimental.diagnostics=false` so Vitest's
2248
+ own hints do not repeat these. The reporter rewrites a `partial: true` report every ~2 s while the run
2249
+ goes, so a killed or timed-out run still leaves one: `perf` prints it under a warning and the gate
2250
+ refuses it like a red run.
2251
+
4668
2252
  **A bare `vitest run` is not every repository's suite, and `perf` refuses to pretend otherwise.**
4669
2253
  Where the suite is assembled by something else — an Angular builder, an Nx target, a script that
4670
2254
  generates a config per project — there is no root `vite(st).config.*`, the Vitest defaults sweep up
@@ -4700,7 +2284,7 @@ budget and nothing else. And every candidate is re-measured on its own before it
4700
2284
  file that is fast when it has the machine to itself is reported as _not reproduced_, an `info` rather
4701
2285
  than a finding. Without a way to re-measure, findings are warnings that fail nothing unless
4702
2286
  `--no-confirm` says one reading is enough. Exit `1` is "over budget", exit `2` is "there was nothing
4703
- to judge" — including a red suite, which the gate will not judge at all, since a failed test is
2287
+ to judge" — including a red or unfinished (`partial`) suite, which the gate will not judge at all, since a failed test is
4704
2288
  measured until its timeout and 30 s of timeout looks exactly like 30 s of slow code.
4705
2289
 
4706
2290
  `--json <path>` re-analyses a report an earlier `--out <path>` run wrote, instead of running Vitest
@@ -4767,156 +2351,12 @@ the comments before and after and compare against `HEAD`.
4767
2351
 
4768
2352
  ## 20. Migrating a suite off `jasmine-auto-spies`
4769
2353
 
4770
- `jasmine-auto-spies` and `jest-auto-spies` are the same library twice — both thin layers over
4771
- `@hirez_io/auto-spies-core`, with identical configuration keys (`methodsToSpyOn`,
4772
- `observablePropsToSpyOn`, `gettersToSpyOn`, `settersToSpyOn`) and identical helper names.
4773
- **Exactly one thing differs**: upstream parks its async helpers behind `.and`, because that is where
4774
- jasmine keeps its own spy strategies.
4775
-
4776
- ```ts
4777
- spy.load.and.nextWith(account); // jasmine-auto-spies
4778
- spy.load.nextWith(account); // jest-auto-spies, and this library
4779
- ```
4780
-
4781
- **Land it green first, rewrite second.** `vitest-auto-spy/jasmine` puts `.and`, `.calls` and
4782
- `.withArgs` back on every spy, so the import specifier is the only edit needed to get the suite
4783
- running — and the first red run then has one candidate cause instead of two:
4784
-
4785
- ```diff
4786
- - import { createSpyFromClass, provideAutoSpy, type Spy } from 'jasmine-auto-spies';
4787
- + import { createSpyFromClass, provideAutoSpy, type Spy } from 'vitest-auto-spy/jasmine';
4788
- ```
4789
-
4790
- ```bash
4791
- npx vitest-auto-spy codemod --from jasmine # dry run
4792
- npx vitest-auto-spy codemod --from jasmine --write # apply, then drop the import
4793
- npx vitest-auto-spy codemod --from jasmine --verify # match the result, not the diff
4794
- ```
2354
+ In [`agent-docs/migration-jasmine.md`](./agent-docs/migration-jasmine.md). Read it when moving a suite off `jasmine-auto-spies`.
4795
2355
 
4796
- `import { jasmine } from 'vitest-auto-spy/jasmine'` restores the whole `jasmine` namespace
4797
- (`objectContaining`, `any`, `createSpyObj`, `clock()`, `addMatchers`, and the eight asymmetric
4798
- matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
4799
- on `globalThis` — it is one explicit line per file, which the codemod later deletes.
4800
-
4801
- Four places where that surface now matches jasmine's rather than approximating it:
4802
-
4803
- - **`spy.withArgs(…).and` carries the strategies that describe one argument list** — `stub()`,
4804
- `throwError(…)`, `resolveTo(…)` and `returnValue(…)`, plus this library's own `resolveWith` /
4805
- `nextWith` / `returnSubject` family. The three that install an _implementation_ —
4806
- `callFake`, `callThrough`, `returnValues` — are present and **throw** a message naming the
4807
- alternative, rather than arriving as `… is not a function`: an implementation answers every call,
4808
- which is the opposite of configuring one argument list. Take the whole spy with `spy.and.callFake(…)`
4809
- if that is what the line meant.
4810
- - **`jasmine.mapContaining` compares keys with the runner's equality**, as jasmine does, so an
4811
- asymmetric matcher as a key and an object key compared deeply both work. `Map.has` alone answered
4812
- on reference identity and missed both.
4813
- - **`jasmine.clock().install()` leaves `Date` real**, exactly as jasmine's does — the timers are
4814
- faked, the clock is not. `jasmine.clock().mockDate()` is what takes `Date` over, and because that
4815
- re-installs the fake clock it reports when callbacks were already scheduled and have just been
4816
- dropped. Call `mockDate()` right after `install()`, before anything schedules a timer.
4817
- - **`jasmine.createSpyObj`'s third argument builds spied accessors**, reachable through
4818
- `Object.getOwnPropertyDescriptor(obj, name).get`. Reading still answers the seed, so nothing about
4819
- a migrated spec changes; what is gained is moving the value mid-test and asserting that the code
4820
- under test wrote it.
4821
-
4822
- ### The renames, once the suite is green
4823
-
4824
- | jasmine | here |
4825
- | ------------------------------------------ | ---------------------------------------------------------- |
4826
- | `spy.m.and.returnValue(v)` | `spy.m.mockReturnValue(v)` |
4827
- | `spy.m.and.callFake(fn)` | `spy.m.mockImplementation(fn)` |
4828
- | `spy.m.and.stub()` | `spy.m.mockImplementation(() => undefined)` |
4829
- | `spy.m.and.returnValues(a, b)` | `.mockReturnValueOnce(a).mockReturnValueOnce(b)` |
4830
- | `spy.m.and.throwError('boom')` | `.mockImplementation(() => { throw new Error('boom'); })` |
4831
- | `spy.m.and.resolveTo(v)` | `spy.m.mockResolvedValue(v)` |
4832
- | `spy.m.and.nextWith(v)` / `resolveWith(v)` | `spy.m.nextWith(v)` / `spy.m.resolveWith(v)` — drop `.and` |
4833
- | `spy.m.withArgs(a).and.returnValue(v)` | `spy.m.calledWith(a).mockReturnValue(v)` |
4834
- | `spy.m.calls.count()` / `argsFor(i)` | `spy.m.mock.calls.length` / `spy.m.mock.calls[i]` |
4835
- | `spy.m.calls.reset()` | `spy.m.mockClear()` |
4836
- | `jasmine.createSpy('n')` | `vi.fn()` — or `createFunctionSpy<F>('n')`, which is typed |
4837
- | `jasmine.any(X)` / `objectContaining({…})` | `expect.any(X)` / `expect.objectContaining({…})` |
4838
- | `jasmine.clock().tick(n)` | `vi.advanceTimersByTime(n)` — or `await advanceTimers(n)` |
4839
- | `jasmine.SpyObj<T>` / `jasmine.Spy` | `Spy<T>` from this package / `Mock` from `vitest` |
4840
- | `expect(x).toBeTrue()` / `toHaveSize(n)` | `.toBe(true)` / `.toHaveLength(n)` |
4841
- | `expect(spy).toHaveBeenCalledOnceWith(a)` | `.toHaveBeenCalledExactlyOnceWith(a)` |
4842
- | `fail(msg)` | `expect.fail(msg)` — there is no `vi.fail` |
4843
-
4844
- ### Four traps, all of them silent
4845
-
4846
- 1. **`spyOn(o, 'm')` inverts.** jasmine's `spyOn` installs a **stub**; `vi.spyOn` **calls through**.
4847
- A bare rename compiles, passes, and starts running the real implementation inside every spec that
4848
- installed the spy in order to stop it. Write
4849
- `vi.spyOn(o, 'm').mockImplementation(() => undefined)` — the codemod appends exactly that, and a
4850
- file whose only jasmine construct is `spyOn(` is deliberately **not** auto-detected: that suite
4851
- has to say `--from jasmine` out loud.
4852
- 2. **`.withContext('msg')` does not throw — it loses the message.** Vitest's chai layer ships an
4853
- `@internal` method of that name expecting a _flags object_; handed a string it walks the string's
4854
- character indices, sets nonsense flags and returns the assertion. The chain runs, the failure
4855
- reads `AssertionError: expected 2 to be 3`, and the label is gone. Write
4856
- `expect(actual, 'msg').toBe(expected)`.
4857
- 3. **`.calls.saveArgumentsByValue()` is a no-op here.** No runner in this family copies call
4858
- arguments. The call still runs, nothing fails, and a spec that relied on it silently starts
4859
- asserting on post-mutation state. Take the copy at call time, inside a `mockImplementation`.
4860
- 4. **`.and.callThrough()` means something different.** Upstream had no original to call through to
4861
- and silently answered `undefined`; here it restores **this library's own dispatch**, so a
4862
- `calledWith` chain decides the value again. The codemod leaves it byte-for-byte and names the
4863
- line, because there is no expression it could become.
4864
-
4865
- `jasmine.DEFAULT_TIMEOUT_INTERVAL` is a config setting, not a statement: set **both**
4866
- `testTimeout` (default 5 000 ms) and `hookTimeout` (default 10 000 ms), or
4867
- `vi.setConfig({ testTimeout: n, hookTimeout: n })` per file. Assigning to it on the namespace warns
4868
- once naming both rather than silently swallowing the write.
4869
-
4870
- `it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
4871
- shape, not a name. Use `async` + `await`; `no-done-callback` reports the parameter and any
4872
- `done.fail(…)`. It reports the parameter that is _called_, handed on as an argument, or never used —
4873
- not every named first parameter: `it('x', (ctx) => ctx.skip())` is Vitest's own `TestContext` read
4874
- without destructuring, which is legal, and used to be reported as a `done` callback.
4875
-
4876
- On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
4877
- importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a
4878
- setup file — spies built **before** the call do not get the namespaces. Observables still come from
4879
- `vitest-auto-spy/rxjs`. A project that never imports the entry pays one `undefined` check per spy
4880
- and ships none of the code.
4881
-
4882
- ### `@hirez_io/observer-spy` comes with it
4883
-
4884
- That package sits beside `jasmine-auto-spies` in almost every suite that has one, and is the larger
4885
- of the two by an order of magnitude. `vitest-auto-spy/rxjs` exports the same surface —
4886
- `subscribeSpyTo(source$, config?)`, `SubscriberSpy<T>`, `ObserverSpy<T>`, `ObserverSpyConfig` — so a
4887
- migration does not have to rewrite every stream assertion in the same commit:
2356
+ <!-- agents-outline: agent-docs/migration-jasmine.md -->
4888
2357
 
4889
- ```ts
4890
- import { subscribeSpyTo } from 'vitest-auto-spy/observer-spy';
4891
-
4892
- const spy = subscribeSpyTo(service.load());
4893
-
4894
- expect(spy.getValues()).toEqual(['a', 'b']);
4895
- expect(spy.receivedComplete()).toBe(true);
4896
- ```
2358
+ - The renames, once the suite is green
2359
+ - Four traps, all of them silent
2360
+ - `@hirez_io/observer-spy` comes with it
4897
2361
 
4898
- Four departures from upstream, each closing a defect: `getValues()` returns a **copy** (upstream
4899
- hands back its live array) and is typed `T[]` (upstream: `any[]`); `getFirstValue()` /
4900
- `getValueAt(i)` **throw** on an empty spy rather than returning `undefined` from a `T` signature;
4901
- and an **unexpected** error is thrown by the value readers, carrying the original as `cause`, rather
4902
- than rethrown out of the observer — under rxjs 7 that rethrow goes through `reportUnhandledError`
4903
- and never reaches the subscribing line. `{ expectErrors: true }` keeps the readers open, as upstream.
4904
-
4905
- `autoUnsubscribe()`, `queueForAutoUnsubscribe()` and `fakeTime()` are **not implemented**. Use
4906
- `using spy = subscribeSpyTo(source$)` — `SubscriberSpy` is disposable — and `setupFakeTimers()` with
4907
- `await advanceTimers(ms)`, or rxjs's `TestScheduler` directly.
4908
-
4909
- **It is a bridge, and the destination differs in kind.** observer-spy is synchronous inspection:
4910
- subscribe, let things happen, then read. A stream that never emits leaves a spy with no values, so a
4911
- spec reading `getValues()` gets `[]`, asserts about it, and passes having observed nothing. §8's
4912
- `expectEmission` / `expectEmissions` invert that — the assertion _is_ the await, and silence is a
4913
- failure with a watchdog. Land the suite green on `subscribeSpyTo`, then move the assertions over.
4914
-
4915
- Full mapping, including what upstream cannot do at all:
4916
- <https://asdalexey.github.io/vitest-auto-spy/migrating-jasmine>.
4917
-
4918
- An Angular suite that came through `ng generate @schematics/angular:refactor-jasmine-vitest` rather
4919
- than through `jasmine-auto-spies` is a different input — a `MockedObject<T>` literal per double and
4920
- `// TODO: vitest-migration:` comments on the `createSpyObj` calls it could not expand. That diff is
4921
- covered at <https://asdalexey.github.io/vitest-auto-spy/migrating-angular-schematic>; the answer to
4922
- all three TODO categories is `createSpyFromClass(Api)`, which reads the prototype, not the call site.
2362
+ <!-- /agents-outline -->