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