vitest-auto-spy 5.7.0 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/AGENTS.md +772 -223
  2. package/README.md +357 -139
  3. package/dist/angular-http.js +2 -2
  4. package/dist/angular-router.d.ts +185 -0
  5. package/dist/angular-router.js +314 -0
  6. package/dist/angular.d.ts +431 -33
  7. package/dist/angular.js +751 -120
  8. package/dist/bun-angular.d.ts +6 -6
  9. package/dist/bun-angular.js +170 -24
  10. package/dist/bun.d.ts +6 -128
  11. package/dist/bun.js +9 -8
  12. package/dist/{chunk-U53SDEP3.js → chunk-BDJYPKLB.js} +2 -2
  13. package/dist/chunk-DTZCPCMA.js +89 -0
  14. package/dist/{chunk-O5IKAXWI.js → chunk-F334GPEI.js} +62 -34
  15. package/dist/{chunk-4EJRJ3QP.js → chunk-FNOXP6VT.js} +1 -1
  16. package/dist/{chunk-ALFMIDWW.js → chunk-GNJLDNJS.js} +1 -1
  17. package/dist/{chunk-RYJDNCAY.js → chunk-GZLFVXCY.js} +2 -2
  18. package/dist/{chunk-374SMZYV.js → chunk-HXH2QXK2.js} +2 -2
  19. package/dist/chunk-HZPV3CHD.js +113 -0
  20. package/dist/{chunk-G73VE5ED.js → chunk-JOI3BLCL.js} +15 -11
  21. package/dist/{chunk-IW6ZHFTL.js → chunk-M6VMOOZ2.js} +1 -1
  22. package/dist/{chunk-2PBOTZ4S.js → chunk-N3T7KWWA.js} +2 -2
  23. package/dist/{chunk-6ER757ZM.js → chunk-NQNHA3R4.js} +6 -89
  24. package/dist/{chunk-B4Z3CFDJ.js → chunk-OTOCFH6B.js} +24 -24
  25. package/dist/{chunk-B45HLBA4.js → chunk-Q477VHHF.js} +3 -0
  26. package/dist/cli.js +1287 -260
  27. package/dist/console.d.ts +1 -1
  28. package/dist/console.js +6 -5
  29. package/dist/dom-stubs.d.ts +2 -1
  30. package/dist/dom-stubs.js +41 -1
  31. package/dist/eslint-plugin.cjs +423 -22
  32. package/dist/eslint-plugin.d.cts +22 -1
  33. package/dist/eslint-plugin.d.ts +22 -1
  34. package/dist/{expect-emission-H8lAQz2f.d.ts → expect-emission-CmN9ADyN.d.ts} +138 -3
  35. package/dist/index.d.ts +5 -5
  36. package/dist/index.js +138 -60
  37. package/dist/jasmine-compat.d.ts +1 -1
  38. package/dist/jasmine.d.ts +2 -2
  39. package/dist/jasmine.js +9 -8
  40. package/dist/nestjs.d.ts +2 -2
  41. package/dist/nestjs.js +10 -9
  42. package/dist/node.cjs +152 -72
  43. package/dist/node.d.ts +5 -5
  44. package/dist/node.js +8 -8
  45. package/dist/{package-identity-B3aSBJRI.d.ts → package-identity-lm-gqZAR.d.ts} +1 -1
  46. package/dist/perf-reporter.js +46 -3
  47. package/dist/{prop-mock-DSWMTtRE.d.ts → prop-mock-DnYNBNJL.d.ts} +1 -1
  48. package/dist/react.d.ts +5 -5
  49. package/dist/react.js +10 -9
  50. package/dist/rstest.d.ts +5 -5
  51. package/dist/rstest.js +9 -9
  52. package/dist/rxjs.d.ts +2 -2
  53. package/dist/rxjs.js +11 -4
  54. package/dist/setup.d.ts +24 -60
  55. package/dist/setup.js +36 -30
  56. package/dist/signal-forms.d.ts +83 -0
  57. package/dist/signal-forms.js +101 -0
  58. package/dist/svelte.d.ts +5 -5
  59. package/dist/svelte.js +10 -9
  60. package/dist/{track-injections-Bh7AZ2-0.d.ts → track-injections-CIHJWFpp.d.ts} +1 -1
  61. package/dist/{zoneless-CvgjkxKw.d.ts → track-signal-runs-67EcdZwp.d.ts} +183 -53
  62. package/dist/{types-DYGoBQGg.d.ts → types-D3RkJjc_.d.ts} +52 -2
  63. package/dist/vue.d.ts +6 -6
  64. package/dist/vue.js +11 -10
  65. package/dist/web-storage-dwQ9E7Lm.d.ts +83 -0
  66. package/package.json +23 -3
  67. package/skills/vitest-auto-spy/SKILL.md +142 -90
  68. package/dist/chunk-RZRKEUE6.js +0 -15
package/AGENTS.md CHANGED
@@ -52,17 +52,19 @@ adapter installed and spies fail at runtime.
52
52
 
53
53
  Add-ons, orthogonal to the runner:
54
54
 
55
- | Add-on | Import | Needed for |
56
- | ----------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
- | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, and in a file the `tsconfig` includes (§4) |
58
- | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
59
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
60
- | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
61
- | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
62
- | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
63
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
64
- | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
65
- | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
55
+ | Add-on | Import | Needed for |
56
+ | ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, and in a file the `tsconfig` includes (§4) |
58
+ | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
59
+ | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
60
+ | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
61
+ | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
62
+ | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
63
+ | Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
64
+ | Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
65
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
66
+ | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
67
+ | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
66
68
 
67
69
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
68
70
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
@@ -73,8 +75,8 @@ so the specifier moved to the root `rxjs` entry — which is where rxjs re-expor
73
75
  the floor moved with it. Any Angular project already satisfies it: Angular 16 through 22 all peer on
74
76
  `^6.5.3 || ^7.4.0`.
75
77
 
76
- The Angular entries need **Angular >= 20** — `@angular/core`, `@angular/common` and
77
- `@angular/platform-browser`, all optional peers on the same range. Below 20 the failure is a link
78
+ The Angular entries need **Angular >= 20** — `@angular/core`, `@angular/common`,
79
+ `@angular/platform-browser` and `@angular/router`, all optional peers on the same range. Below 20 the failure is a link
78
80
  error, not a missing helper: `/angular` imports `ɵSIGNAL` (Angular 18+) as a value on the first line
79
81
  of its bundle, so on 16 and 17 the whole entry fails to load, and `/bun-angular` imports
80
82
  `provideZonelessChangeDetection` (Angular 20+; called `provideExperimentalZonelessChangeDetection`
@@ -173,8 +175,8 @@ out, and the one that costs an afternoon otherwise. `mock.repo.user.find()` chai
173
175
  but the last is a _read_. A node that is **called** returns what it was configured to return, and by
174
176
  default that is `undefined`, so `mockDeep<AppLogger>().channel('app').info('x')` is a `TypeError` at
175
177
  the second call — while `DeepMockProxy<AppLogger>` types it perfectly, so nothing warns. Pass
176
- `{ selfReturning: true }` for a fluent API, or use `createAutoMock<T>()` with
177
- `channel.mockReturnThis()` when only one method chains:
178
+ `{ selfReturning: true }` for a fluent API, or use
179
+ `createAutoMock<T>(undefined, { selfReturning: ['channel'] })` when only one method chains:
178
180
 
179
181
  ```ts
180
182
  const logger = mockDeep<AppLogger>({}, { selfReturning: true });
@@ -557,6 +559,7 @@ createSpyFromClass(MyService, {
557
559
  autoSpyAccessors: true, // discover every accessor on the prototype chain
558
560
  lazySpies: true, // build each method spy on first access ('proxy' for very wide classes)
559
561
  returns: { getProducts: of([]) }, // what a spied METHOD answers
562
+ selfReturning: ['where'], // a method that answers the double itself, for a chained call
560
563
  overrides: { products$: subject }, // a member that is not a method result
561
564
  });
562
565
  ```
@@ -570,6 +573,7 @@ createSpyFromClass(MyService, {
570
573
  | `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. `'proxy'` also changes _what holds the name_. |
571
574
  | `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
572
575
  | `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
576
+ | `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
573
577
 
574
578
  **`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
575
579
  the reference suite). Method discovery walks the _prototype chain_; a callable assigned to an
@@ -603,7 +607,9 @@ would then take the wrong branch, silently, which is the exact failure mode the
603
607
  in §2 exists to remove. A loud `TypeError` on the spec's own line is the better of the two.
604
608
 
605
609
  For an ngrx `signalStore()`, prefer `createAutoMock<T>()` over listing every member: it mocks from
606
- the type, needs no prototype, and the list cannot fall behind the store.
610
+ the type, needs no prototype, and the list cannot fall behind the store. Its **signal** members want
611
+ `mockSignalProp`, not a method spy — a store's state is read during the first render, so a method spy
612
+ there answers `undefined` to the template.
607
613
 
608
614
  The symptom of getting this wrong is **a spy that is never called and no warning at all**: the
609
615
  additive lists exist precisely to name things the prototype does not have, so a typo in one cannot
@@ -707,6 +713,33 @@ registerAutoSpyDefaults([
707
713
  A later row for a class an earlier row already named replaces it, exactly as a second call would.
708
714
  `AutoSpyDefaultEntry<T>` is the row type, for a row built outside the literal.
709
715
 
716
+ **An `InjectionToken` is a key too — import `registerAutoSpyDefaults` from `vitest-auto-spy/angular`
717
+ for it.** Same registry, same merge; the core export takes a class only, because the core entry may
718
+ not mention Angular's types. `provideAutoSpyForToken(TOKEN)` reads the registration exactly as
719
+ `provideAutoSpy(Class)` reads a class's — both of its arguments are merged over it, and a registered
720
+ `returns` stays a default a later `calledWith` / `resolveWith` wins over:
721
+
722
+ ```ts
723
+ import { registerAutoSpyDefaults } from 'vitest-auto-spy/angular';
724
+
725
+ registerAutoSpyDefaults(LOGGER, { returns: { info: undefined, err: undefined }, selfReturning: ['channel'] });
726
+ registerAutoSpyDefaults(NAVIGATION, { overrides: { activeRow$: of({}) }, returns: { setFocus: undefined } });
727
+
728
+ providers: [provideAutoSpyForToken(LOGGER), provideAutoSpyForToken(NAVIGATION, { activeRow$: rows$ })];
729
+ ```
730
+
731
+ A token row holds `AutoSpyTokenDefaults<T>`: what `createAutoMock` takes (`returns`, `selfReturning`,
732
+ `observablePropsToSpyOn`, `strict`, `name`) plus `overrides`, the seeds `provideAutoSpyForToken` takes
733
+ second. Keys are checked against the token's `T`; a table may mix class rows and token rows;
734
+ `clearAutoSpyDefaults(TOKEN)` from the same entry drops one. Handing a token to the **core**
735
+ `registerAutoSpyDefaults` fails with `TS2345 … 'InjectionToken<X>' is not assignable to parameter of
736
+ type 'ClassType<unknown>'` — the fix is the import, not a cast.
737
+
738
+ **`selfReturning` names the methods that answer the double itself.** `returns` cannot say it — the
739
+ double does not exist when the literal is written — and without it a chained call dies on
740
+ `undefined`. It works on every factory and in a registration; a registered chain takes a link back out
741
+ through the call site's `returns`, since lists only ever union.
742
+
710
743
  ### `strict` — a method nobody configured throws instead of answering `undefined`
711
744
 
712
745
  ```ts
@@ -733,6 +766,7 @@ Precedence, first one set wins: the double's `onUnstubbedCall` → the double's
733
766
  - **`returns:` is a default in the spy's own container**: a `calledWith` / `resolveWith` / `failWith`
734
767
  configured later wins for its arguments or supersedes it, every other call still gets it, and
735
768
  `undefined` counts as configured. `returns: { save: undefined }` is how a `void` call is expected.
769
+ `selfReturning: ['channel']` is the same kind of default, and counts as configured too.
736
770
  - **`mockReturnValue` / `mockImplementation` replace the dispatch**, so they never reach the guard —
737
771
  and a `calledWith` configured after them is never consulted. After the last `mockReturnValueOnce`
738
772
  the queue empties back onto the library dispatch and the next call **is** reported as unstubbed —
@@ -745,6 +779,17 @@ Precedence, first one set wins: the double's `onUnstubbedCall` → the double's
745
779
  - **It does not reach** accessor spies, observable-property spies, `mockDeep` nodes, `console-spy`,
746
780
  `mockResourceProp`'s `reload` or a standalone `createFunctionSpy`. A strict double still answers
747
781
  `undefined` for an unconfigured getter or `items$`. `fillMissing` members **are** covered.
782
+ - **The getter and the stream are reported after the test instead** — a read cannot throw, because a
783
+ failure diff that prints the double reads it. `setupAutoSpy({ unconfiguredReads: 'warn' | 'throw' })`
784
+ (default `'off'`, not in the preset) reports a strict double's getter read that reached no
785
+ configuration and a subscription to a stream nothing fed **by the end of the test** — subscribe in
786
+ `beforeEach`, `nextWith` in the test is fine. Counted from setup's `beforeEach` to its `afterEach`,
787
+ so the spec's own hooks count. Configured means `accessorSpies.getters.x.mockReturnValue(…)` /
788
+ `mockImplementation`, `overrides: { x }` (call site or registration — a registered _list_ alone is
789
+ not), `mockReadonlyProp`; for a stream any of `nextWith` / `throwWith` / `complete` /
790
+ `returnSubject`. `undefined` meant: `mockReturnValue(undefined)`. `onUnstubbedRead({ className,
791
+ member, kind, count })` — suite-wide or per double, same precedence as `onUnstubbedCall` — takes the
792
+ findings instead of the report, from every double not `strict: false`: survey with it first.
748
793
  - **Angular lifecycle hooks are exempt.** `ngOnDestroy`, `ngOnInit`, `ngOnChanges`, `ngDoCheck` and the
749
794
  `ngAfter…` hooks answer `undefined` on a strict double: Angular calls `ngOnDestroy` itself on every
750
795
  provided value at teardown (a `createAutoMock` / `provideAutoSpyForToken` double always has one), and
@@ -832,6 +877,16 @@ const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); //
832
877
  const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
833
878
  ```
834
879
 
880
+ It applies to `createSpyFromClass` with a configuration too, in one combination: an accessor list
881
+ (or `overrides`) **and** `returns` on a generic class. TypeScript checks a generic class argument
882
+ after the configuration, reads `T` back from `gettersToSpyOn: ['remoteConfig']` as
883
+ `{ remoteConfig: any }`, and fails with `'isKeyEnabled' does not exist in type
884
+ 'MethodReturns<{ remoteConfig: any; }>'`. Spell it out — `createSpyFromClass<RemoteConfigService>(…)`.
885
+ `provideAutoSpy`, `overrideAutoSpy`, `overrideComponentProvider` and the class overload of
886
+ `registerAutoSpyDefaults` (`/angular`) take `T` from the class alone, so there the inferred form
887
+ compiles; the core `registerAutoSpyDefaults` needs the argument spelled out like
888
+ `createSpyFromClass`. A token key needs nothing: `T` comes from the `InjectionToken<T>` itself.
889
+
835
890
  **A declared default now reaches the double, and used not to.** `class RemoteConfigService<T =
836
891
  RemoteConfigDefaults>` handed to `createSpyFromClass` / `injectSpy` inferred `T` as `unknown`, so
837
892
  every member typed against it read as `unknown` on a class that had said exactly what it should be.
@@ -1610,7 +1665,8 @@ setupAutoSpy({ preset: 'strict' });
1610
1665
  Sets `duplicateCopies`, `propsOutsideHooks`, `guardGlobals`, `prototypePollution`, `strayConsole` and
1611
1666
  `misconfiguration` to `'throw'`, turns `strayTimers` on, and `strayRejections` on when zone.js is
1612
1667
  loaded. An option passed alongside still wins. **Not** included: `strict` (strict doubles change what
1613
- an unconfigured call returns — a semantic switch, not a grade; the name was taken, hence `preset`),
1668
+ an unconfigured call returns — a semantic switch, not a grade; the name was taken, hence `preset`) and
1669
+ `unconfiguredReads`, its read side (survey with `onUnstubbedRead` before turning it on),
1614
1670
  `blockNetwork` (changes the code under test), `restoreMocks` (drops `beforeAll` spies), failing on
1615
1671
  stray-timer counts (the sweep fails the file from `afterAll`, and a callback scheduled after the
1616
1672
  previous file's sweep is charged to the next — opt in with
@@ -1675,7 +1731,7 @@ never holds fails naming the `label` rather than hanging until the runner's time
1675
1731
 
1676
1732
  **Not for an Angular `resource()` / `httpResource()`.** Those need a change-detection _tick_, and
1677
1733
  this helper only takes event-loop turns — a resource awaited through it finishes the whole budget
1678
- having issued zero requests. `settleResource(resource, { turns, label })` from
1734
+ having issued zero requests. `settleResource(resource, { turns, label, allowIdle })` from
1679
1735
  `vitest-auto-spy/angular` is that wait.
1680
1736
 
1681
1737
  `flushEventLoop(turns?)` takes real event-loop turns even while the timers are faked, without
@@ -1750,6 +1806,34 @@ State is per element, so an ad and the content report different durations.
1750
1806
  the record and fires `timeupdate` too — `media.set()` is not the only way in, and the component's own
1751
1807
  handler runs where it used to stay unrun while the assertion read the new value.
1752
1808
 
1809
+ ### `localStorage` and `sessionStorage`
1810
+
1811
+ Do not write a `TestingStorage` class per project. `stubWebStorage()` installs an in-memory `Storage`
1812
+ for this test — `getItem` / `setItem` / `removeItem` / `clear` / `key` / `length`, string coercion
1813
+ as the platform does it — and `snapshot()` is the plain record to assert on:
1814
+
1815
+ ```ts
1816
+ import { type WebStorageStub, stubWebStorage } from 'vitest-auto-spy/dom-stubs';
1817
+
1818
+ let local: WebStorageStub;
1819
+
1820
+ beforeEach(() => {
1821
+ local = stubWebStorage('localStorage', { items: { token: 'abc' } }); // or 'sessionStorage'
1822
+ });
1823
+
1824
+ it('forgets the token on logout', () => {
1825
+ session.logout();
1826
+ expect(local.snapshot()).toEqual({});
1827
+ });
1828
+ ```
1829
+
1830
+ It goes through `mockValueProp`, so `restoreMockedProps()` (and `setupAutoSpy()` between tests) puts
1831
+ the previous storage back; install it in `beforeEach`. It also lands on `document.defaultView` when
1832
+ that is a separate object. It is not `restoreWebStorage()`: that one repairs a broken environment
1833
+ once and leaves a working storage alone; this one replaces whatever is there, for one test, and
1834
+ installs even in a `node` environment because the spec asked for it. Named-property access
1835
+ (`localStorage.token`, `Object.keys(localStorage)`) does not see the items — read through the API.
1836
+
1753
1837
  ### A module mock that did nothing
1754
1838
 
1755
1839
  `vi.mock()` is the one thing in a ported suite that fails **silently**. Under a bundler
@@ -1858,6 +1942,12 @@ statement is not brevity: the shortcut people take instead is an exported `const
1858
1942
  the values, and under `isolate: false` that is one set of spies shared by every file that imports
1859
1943
  it.
1860
1944
 
1945
+ **A member named in both wins as the seed**, whatever the seed is — a value, a plain function, a
1946
+ `vi.fn()`. `returns` and `selfReturning` skip such a member rather than configuring it, which is the
1947
+ precedence the merge already applies between a registration and a call site: a spec seeding `channel`
1948
+ over `registerAutoSpyDefaults(LOGGER, { selfReturning: ['channel'] })` gets its own function back, not
1949
+ the registered link.
1950
+
1861
1951
  **`observablePropsToSpyOn` works on a token too**, and matters more there than on a class. A class
1862
1952
  tells the factory which members are methods; a type does not, so _every_ unnamed key of a
1863
1953
  token-driven double is a function spy — including an `Observable` property, which the code under
@@ -1919,6 +2009,25 @@ selected.set(true); // every computed reading it updates
1919
2009
  Use it whenever the value has to change during the test. `mockReadonlyProp` stays right when the
1920
2010
  value is fixed for the whole test and you never need the handle.
1921
2011
 
2012
+ A `linkedSignal()` is a writable signal like any other, so `mockSignalProp` covers it and there is
2013
+ no separate helper to go looking for.
2014
+
2015
+ **It patches as little as it can, and the order rule follows from that.** A member that is already
2016
+ writable — `signal()`, `model()`, `linkedSignal()` — is **written through**, not replaced: Angular
2017
+ links a consumer to the signal it read, not to the property it read it through, so replacing one
2018
+ that anything has already read strands every `computed()`, `effect()` and template binding on the
2019
+ old signal, silently and for the rest of the test. Writing through keeps all of them, keeps a
2020
+ `model()`'s output half alive, and leaves nothing for `restoreMockedProps()` to undo — the value
2021
+ stays where the spec left it.
2022
+
2023
+ Two shapes are still swapped: a `computed()` the class declares, and a member the spy does not have
2024
+ yet. **Patch those before anything reads them** — before the first `detectChanges()` /
2025
+ `stable(fixture)`. A read-only signal a live consumer has already read is refused by name rather
2026
+ than replaced where nothing would notice it, and an `input()` is refused outright: Angular sets an
2027
+ input through the input node rather than the property, so a replaced one breaks the host's next
2028
+ write. Drive an input with `fixture.componentRef.setInput(name, value)`, or with
2029
+ `renderShallow(Component, { inputs: { … } })` for the value it starts at.
2030
+
1922
2031
  ### Observers the component constructs itself
1923
2032
 
1924
2033
  Do not assign `globalThis.IntersectionObserver` by hand: it stays assigned, and under
@@ -2160,8 +2269,9 @@ and the next line reads the settled value.
2160
2269
 
2161
2270
  Three things to know before reaching for it:
2162
2271
 
2163
- - **It does not re-export the core.** The one subpath that does not: it is a companion to
2164
- `vitest-auto-spy/angular`, which stays the import for spies, `TestBed` helpers and `settleResource`.
2272
+ - **It does not re-export the core** — nor does `/angular-router`; every other subpath does. It is a
2273
+ companion to `vitest-auto-spy/angular`, which stays the import for spies, `TestBed` helpers and
2274
+ `settleResource`.
2165
2275
  Staying narrow is what keeps `@angular/common` — an **optional** peer — out of every other entry.
2166
2276
  - **`settleResource` is unchanged and still the answer** whenever the wait is not tied to one
2167
2277
  request: a `resource()` with an async loader, an `rxResource`, a reload, anything not HTTP.
@@ -2170,6 +2280,286 @@ Three things to know before reaching for it:
2170
2280
  suite using `provideHttpTesting()` everywhere the diagnostic is redundant; keep it on while any
2171
2281
  file still configures HTTP testing by hand.
2172
2282
 
2283
+ ### `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
2284
+
2285
+ ```ts
2286
+ import { injectActivatedRoute, provideActivatedRoute } from 'vitest-auto-spy/angular-router';
2287
+
2288
+ TestBed.configureTestingModule({
2289
+ providers: [provideActivatedRoute({ params: { id: '7' }, queryParams: { tab: 'reviews' } })],
2290
+ });
2291
+
2292
+ const fixture = TestBed.createComponent(ProductPage);
2293
+
2294
+ injectActivatedRoute().setParams({ id: '8' }); // params + paramMap emit; snapshot.params already agrees
2295
+ fixture.detectChanges();
2296
+ ```
2297
+
2298
+ **Do not hand-write an `ActivatedRoute` double, and do not `provideAutoSpy(ActivatedRoute)`.** Its
2299
+ streams and its snapshot are instance fields: the spy has neither, and a `useValue` has whichever half
2300
+ its author thought of — `route.paramMap.pipe(…)` throws on the `{ snapshot }` one, `route.snapshot`
2301
+ is `undefined` on the `{ params: of(…) }` one, and a spec that updates one half by hand tests a route
2302
+ no navigation produces. `provideActivatedRoute(init)` provides **Angular's own `ActivatedRoute`**
2303
+ built over one record, so every stream, both `ParamMap`s and `snapshot` read the same values.
2304
+
2305
+ | Call | Does |
2306
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
2307
+ | `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `fragment`, `url`, `outlet`, `component`, `routeConfig` |
2308
+ | `injectActivatedRoute(injector?)` | the handle: `.route`, `setParams`, `setQueryParams`, `setData`, `setFragment`, `setUrl`, `set({ … })` |
2309
+ | `createActivatedRoute(init?)` | the same handle without a `TestBed` — for `new Page(route)`, or a guard given `route.snapshot` |
2310
+
2311
+ A setter behaves like the router after a navigation: it **replaces** (not merges) that part, builds
2312
+ a new snapshot first, then emits only the streams whose value changed (router equality: same keys,
2313
+ arrays as sorted sets), in the order `queryParams`, `fragment`, `params`, `url`, `data`. Setting
2314
+ an equal value emits nothing; a string `url` is split on `/`.
2315
+
2316
+ Four things to know:
2317
+
2318
+ - **List it after `provideRouter()`** — the later provider of `ActivatedRoute` wins, and
2319
+ `injectActivatedRoute()` throws naming the one that did. With the real router present, the double
2320
+ still works as `relativeTo` for `router.createUrlTree`.
2321
+ - **One node, no tree.** `root` is the route itself, `parent` / `firstChild` are `null`. For
2322
+ `route.parent.params` patch that member with `mockReadonlyProp`, or use `RouterTestingHarness`.
2323
+ - **No `title`, no navigation, no input binding** — `title` emits `undefined`, `Router.navigate`
2324
+ does not move it (the setters do), and `withComponentInputBinding()` is the outlet's job
2325
+ (`fixture.componentRef.setInput`).
2326
+ - **Its own entry, like `/angular-http`.** It does not re-export the core, imports no runner, and is
2327
+ the only file that reaches `@angular/router` — an optional peer.
2328
+
2329
+ ### `Router` from one URL — `vitest-auto-spy/angular-router`
2330
+
2331
+ ```ts
2332
+ import { injectRouterDouble, provideRouterDouble } from 'vitest-auto-spy/angular-router';
2333
+
2334
+ TestBed.configureTestingModule({ providers: [provideRouterDouble({ url: '/products/7' })] });
2335
+
2336
+ const router = injectRouterDouble();
2337
+
2338
+ router.emitNavigation('/products/8'); // events emits a NavigationEnd; router.url already reads it
2339
+ expect(router.navigate).toHaveBeenCalledWith(['/checkout']);
2340
+ ```
2341
+
2342
+ **Do not hand-write `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate:
2343
+ Promise.resolve(true) } })`** — the line 48 hand-rolled providers across two private suites all
2344
+ converged on, and every part of it is a guess the component falls through: `of()` never emits again,
2345
+ `url` is a string nobody updates, `routerState` is absent, `serializeUrl` throws on the first
2346
+ redirect a guard builds. `provideRouterDouble(init)` keeps one URL and derives the rest.
2347
+
2348
+ | Call | Does |
2349
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------ |
2350
+ | `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
2351
+ | `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)` |
2352
+ | `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
2353
+
2354
+ Unlike the route, this is **not** an instance of Angular's class: a real `Router` drags the whole
2355
+ routing stack in. It is a structural double that answers `url` (serialized as the real router does),
2356
+ `events` (a `BehaviorSubject` starting at the `NavigationEnd` that put it there), `navigate` and
2357
+ `navigateByUrl` (spies resolving `true`), `serializeUrl` / `parseUrl` / `createUrlTree` (the
2358
+ router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`) and `routerState`
2359
+ (Angular's own, its `snapshot.url` the URL). **Every other member `Router` declares throws by name**
2360
+ rather than reading `undefined`.
2361
+
2362
+ Four things to know:
2363
+
2364
+ - **It does not navigate.** `navigate()` records the call and resolves `true`; it does not move
2365
+ `url`. `setUrl()` and `emitNavigation()` move it; `RouterTestingHarness` over a real
2366
+ `provideRouter()` is what tests a navigation.
2367
+ - **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
2368
+ `NavigationEnd` moves the URL with it; any other event does not.
2369
+ - **Ordering does not matter here.** `Router` is `providedIn: 'root'` and `provideRouter()` does not
2370
+ re-provide the token, so the double wins in either order — and a `TestBed` without it hands out a
2371
+ real router, which is what `injectRouterDouble()` says by name.
2372
+ - **It builds spies**, so a runtime entry has to be imported in the suite (`vitest-auto-spy/angular`
2373
+ in the same file, or the setup file); the route double needs none.
2374
+
2375
+ ### Signal forms — `vitest-auto-spy/signal-forms`
2376
+
2377
+ ```ts
2378
+ import { minLength, required } from '@angular/forms/signals';
2379
+ import { createForm, registerFormMatchers } from 'vitest-auto-spy/signal-forms';
2380
+
2381
+ registerFormMatchers(); // once, in the setup file
2382
+
2383
+ const user = createForm({ email: '', name: '' }, (path) => {
2384
+ required(path.email, { message: 'Email is required' });
2385
+ minLength(path.name, 2);
2386
+ });
2387
+
2388
+ expect(user.email).toHaveFieldErrors([{ kind: 'required', message: 'Email is required' }]);
2389
+
2390
+ user.email().value.set('ada@example.test');
2391
+
2392
+ expect(user.email).toHaveFieldErrors([]);
2393
+ ```
2394
+
2395
+ Signal forms are stable from Angular 22, and a spec that touches one meets the same two things every
2396
+ time. **`form()` injects**, so a call in a `beforeEach` throws `NG0203: The Injector token injection
2397
+ failed` — a message about `inject()` that never says "form", whose repair is
2398
+ `{ injector: TestBed.inject(Injector) }` or a `TestBed.runInInjectionContext` around the call.
2399
+ **`errors()` answers `RequiredValidationError` instances**, each carrying a `fieldTree`
2400
+ back-reference, so `toEqual([{ kind: 'required' }])` fails on a property nobody wrote and every
2401
+ suite falls back to `errors().some((error) => error.kind === 'required')` — which passes just as
2402
+ happily when the field has three other errors nobody expected.
2403
+
2404
+ | Call | Does |
2405
+ | --------------------------------------------- | ------------------------------------------------------------------------- |
2406
+ | `createForm(model, schema?, options?)` | Angular's own `form()`, built in the `TestBed`'s injection context |
2407
+ | `createForm(initialValue, schema?, options?)` | the same, with the model signal made for you |
2408
+ | `registerFormMatchers()` | adds `expect(field).toHaveFieldErrors(kinds)` — the whole set, order-free |
2409
+
2410
+ Four things to know:
2411
+
2412
+ - **What comes back is Angular's `FieldTree`**, not a double: the states, the validators and the
2413
+ write-through to the model are the framework's. A spec that passes its own `signal()` asserts on it
2414
+ directly; a plain value gets a signal made here, and `user().value()` reads it back either way.
2415
+ - **A `computed()` is refused by name** — a form writes into its model, and without the check a
2416
+ derived signal would be wrapped as a value while every write vanished.
2417
+ - **`options.injector` is for the validator that injects**: pass `fixture.debugElement.injector`
2418
+ when the service a schema reaches for lives in a component's own `providers`.
2419
+ - **`toHaveFieldErrors` compares the whole set**, by `kind`, and by `message` only where the spec
2420
+ names one; `[]` is "no errors". A field tree and its state both read, so `expect(user.email)` and
2421
+ `expect(user.email())` are the same assertion.
2422
+
2423
+ **No `FieldTree` double is shipped, on purpose.** It would stand in for a form a component takes as
2424
+ an input, and across the measured suites no component takes one: forms are built by the component
2425
+ that owns them (assert through `component.form.tags()`), and custom controls — `FormValueControl<T>`,
2426
+ `FormCheckboxControl` — are plain components whose contract is `value = model<T>()`, which is
2427
+ `renderShallow` plus `setInputs`.
2428
+
2429
+ ### `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
2430
+
2431
+ ```ts
2432
+ import { provideDocumentDouble, provideWindowDouble } from 'vitest-auto-spy/angular';
2433
+
2434
+ TestBed.configureTestingModule({
2435
+ providers: [
2436
+ provideWindowDouble(WINDOW, { screen: { width: 1920, height: 1080 }, devicePixelRatio: 2 }),
2437
+ provideDocumentDouble({ visibilityState: 'hidden' }),
2438
+ ],
2439
+ });
2440
+ ```
2441
+
2442
+ **Do not write `useValue: window`, and do not build a `mockDocument` from scratch.** These are the
2443
+ two most hand-rolled providers in an Angular suite — 95 `window` ones and 70 `document` ones across
2444
+ two private suites — and both hand-written shapes fail the same way. `useValue: window` isolates
2445
+ nothing: what the test writes stays on the global for the rest of the worker. A double built from
2446
+ scratch, whether a slice (`{ screen: { width: 1280, height: 720 } }`) or a `mockDocument` with one
2447
+ hand-written `querySelector`, knows only the members its author thought of, so the component that
2448
+ reads `screen.colorDepth` or calls `document.createElement` gets `undefined` and the spec fails
2449
+ somewhere unrelated to what it was testing.
2450
+
2451
+ These two merge the overrides **over the real jsdom object** instead: everything the spec did not
2452
+ name — `location.href`, `getComputedStyle`, `addEventListener`, `document.createElement` — still
2453
+ answers the way jsdom answers it.
2454
+
2455
+ | Call | Does |
2456
+ | --------------------------------------------------------------------- | ----------------------------------------------------------------- |
2457
+ | `provideWindowDouble(token, overrides?)` | a `FactoryProvider` under the application's own window token |
2458
+ | `provideDocumentDouble(overrides?, token?)` | the same under Angular's `DOCUMENT`, or under a token of your own |
2459
+ | `createWindowDouble(overrides?)` / `createDocumentDouble(overrides?)` | the same doubles without a `TestBed` |
2460
+
2461
+ Five things to know:
2462
+
2463
+ - **The window helper needs your token.** Angular ships `DOCUMENT` (from `@angular/core` itself
2464
+ since v20) and has never shipped a `WINDOW`: every application declares its own
2465
+ `InjectionToken<Window>`. The helper is generic over the token's type, so an
2466
+ `InjectionToken<AppWindow>` has that interface's members checked in the overrides too.
2467
+ - **A plain `{ … }` merges, anything else replaces.** `{ screen: { width: 1920 } }` keeps
2468
+ `screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole.
2469
+ - **Nothing to restore.** The globals are never patched — the double is a view over them, and every
2470
+ write and delete lands on the view, which is also how a spec moves a value mid-test:
2471
+ `Object.assign(TestBed.inject(WINDOW), { scrollY: 40 })` (`Object.assign` because lib.dom declares
2472
+ most of `Window` `readonly`). There is no handle and no `restoreMockedProps()` in this recipe.
2473
+ - **`provideDocumentDouble` hands the double to Angular as well** — the renderer injects `DOCUMENT`
2474
+ too, so replace `createElement` or `body` only where the spec means to.
2475
+ - **`location` takes overrides like everything else**, though the platform declares its members
2476
+ unforgeable: `{ location: { reload: vi.fn() } }` — the shape most hand-written window mocks are —
2477
+ records the reload, leaves `location.href` real, and `win.location.href = '/next'` moves the
2478
+ double rather than the address bar. `mockValueProp(TestBed.inject(WINDOW), 'innerWidth', 800)` and
2479
+ its restore work on the double too, and neither touches the global.
2480
+
2481
+ ### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
2482
+
2483
+ ```ts
2484
+ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
2485
+ import { expectEmission, injectMatDialogRef, provideMatDialogData, provideMatDialogRef } from 'vitest-auto-spy/angular';
2486
+
2487
+ TestBed.configureTestingModule({
2488
+ providers: [provideMatDialogData<EditUserData>(MAT_DIALOG_DATA, { id: 7, name: 'Ada' }), provideMatDialogRef(MatDialogRef)],
2489
+ });
2490
+
2491
+ const dialog = injectMatDialogRef(MatDialogRef);
2492
+
2493
+ TestBed.createComponent(EditUserDialog).componentInstance.save();
2494
+
2495
+ expect(dialog.close).toHaveBeenCalledWith('saved');
2496
+ await expect(expectEmission(dialog.ref.afterClosed())).resolves.toBe('saved');
2497
+ ```
2498
+
2499
+ **`@angular/material` is not a dependency of this package and must not become one** — it ships no
2500
+ runtime dependencies at all. That is exactly why the token and the ref class are **arguments**:
2501
+ `MatDialogRef` is the DI token, the shape the double is measured against and the type the result is
2502
+ read off, so the spec's own Material import stays the only place it is named.
2503
+
2504
+ **Do not hand-roll `{ close: vi.fn() }`.** 36 of these across two private suites are the same three
2505
+ shapes, and the ref one fails twice. `close` is the only member anybody writes, so a component that
2506
+ subscribes to `afterClosed()` dies on "is not a function"; the repair written next to it,
2507
+ `afterClosed: () => of('saved')`, answers before anything closed the dialog — the spec then passes
2508
+ whether or not `close()` was ever called.
2509
+
2510
+ | Call | Does |
2511
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
2512
+ | `provideMatDialogData(token, data)` | a typed `{ provide, useValue }` — name the type argument and the data is checked against it |
2513
+ | `provideMatDialogRef(RefClass, init?)` | a `FactoryProvider` — a fresh ref per injector; `init`: `closedWith`, `disableClose`, `componentInstance` |
2514
+ | `injectMatDialogRef(RefClass, injector?)` | the handle: `.ref`, `.close` (the spy), `emitClose(result?)` |
2515
+ | `createMatDialogRef(RefClass, init?)` | the same handle without a `TestBed` — and the ref a spied `MatDialog.open()` hands back |
2516
+
2517
+ `MatDialog` itself needs no helper of its own: `provideAutoSpy(MatDialog)` already spies it, and the
2518
+ ref double is the thing its `open()` was missing.
2519
+
2520
+ ```ts
2521
+ TestBed.configureTestingModule({ providers: [provideAutoSpy(MatDialog)] });
2522
+
2523
+ injectSpy(MatDialog).open.mockReturnValue(createMatDialogRef(MatDialogRef, { closedWith: 'saved' }).ref);
2524
+ ```
2525
+
2526
+ Five things to know:
2527
+
2528
+ - **`close` is the spy, and it is the same function the ref carries.** `expect(dialog.close)` and
2529
+ `expect(TestBed.inject(MatDialogRef).close)` are one assertion. `emitClose(result?)` is the other
2530
+ half — the user closing the dialog from outside, where the streams move and the spy records
2531
+ nothing, and the only way to close with `undefined` (a dismissal), which `closedWith` cannot say.
2532
+ - **`afterClosed()` is replayed, where Material's is a plain `Subject`.** The deliberate departure:
2533
+ in a spec the assertion usually subscribes _after_ the component has closed the dialog, and a
2534
+ `Subject` has nothing left to say by then. `beforeClosed()` is the same stream — the double closes
2535
+ instantly, so there is no window between them, and `afterOpened()` has already emitted.
2536
+ - **`MAT_DIALOG_DATA` is `InjectionToken<any>` in Material**, so a hand-written `useValue: null`
2537
+ compiles for a component that reads `data.name`. `provideMatDialogData<EditUserData>(…)` checks the
2538
+ object; a token of your own typed `InjectionToken<EditUserData>` checks it without naming anything.
2539
+ The value is handed out as it is, so build it per test rather than hoisting it to a constant.
2540
+ - **`componentInstance` is how the opener drives the dialog**, and it is the one member Material
2541
+ keeps off the prototype, so a double that did not have it would read `undefined` rather than
2542
+ fail. Hand the stand-in over and the opener works against it — checked, member by member, against
2543
+ the component the ref class names:
2544
+
2545
+ ```ts
2546
+ const save = new EventEmitter<string>();
2547
+ const dialog = createMatDialogRef<MatDialogRef<NameInputDialog, string>>(MatDialogRef, {
2548
+ componentInstance: { save, isSaving: signal(false) },
2549
+ });
2550
+
2551
+ injectSpy(MatDialog).open.mockReturnValue(dialog.ref);
2552
+ save.emit('Grace');
2553
+
2554
+ expect(dialog.close).toHaveBeenCalledWith('Grace');
2555
+ ```
2556
+
2557
+ - **What it does not answer throws by name.** `backdropClick`, `keydownEvents`, `updateSize`,
2558
+ `updatePosition`, `getState`, `componentRef` and `id` are the dialog doing its own work; the
2559
+ double names them in the failure instead of reading `undefined`, and the real `MatDialogModule` is
2560
+ what answers them. A `componentInstance` nobody handed over says so by name too, and names the
2561
+ init field that fixes it.
2562
+
2173
2563
  ### Which collaborators the code asked for — `trackInjections`
2174
2564
 
2175
2565
  Do **not** reach for `vi.mock('@app/services')` to answer "was this collaborator used". Register the
@@ -2226,8 +2616,41 @@ runEffect(component.highlightEffect); // runs the body now, with the current sig
2226
2616
  ```
2227
2617
 
2228
2618
  `flushEffects()` runs everything currently dirty; `runEffect(ref)` runs one specific effect
2229
- regardless. Prefer asserting the result where practical — `runEffect` reads Angular's reactive node,
2230
- and throws with instructions if a future version moves it.
2619
+ regardless. It runs the previous run's cleanup first, the way Angular's own scheduler does, so an
2620
+ `onCleanup` callback is observable and the node does not collect one closure per call. An effect
2621
+ whose view has been torn down — or which was destroyed on its own — is refused rather than run:
2622
+ after `fixture.destroy()` there is nothing production could still produce. Prefer asserting the
2623
+ result where practical — `runEffect` reads Angular's reactive node, and throws with instructions if
2624
+ a future version moves it.
2625
+
2626
+ ### Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
2627
+
2628
+ "The template read `total()` six times and the computation ran once" and "changing the filter did
2629
+ not re-run the sync effect" are not observable from the outside: a `computed()` returns the same
2630
+ value whether it was cached or recomputed. The usual workaround is a counter inside the computation,
2631
+ which means editing production code to make a test possible.
2632
+
2633
+ ```ts
2634
+ import { trackEffectRuns, trackRecomputations } from 'vitest-auto-spy/angular';
2635
+
2636
+ const recomputed = trackRecomputations(component.total);
2637
+ const synced = trackEffectRuns(component.syncEffect);
2638
+
2639
+ component.unrelatedFilter.set('open');
2640
+ await stable(fixture);
2641
+
2642
+ expect(component.total()).toBe(42);
2643
+ expect(recomputed.count).toBe(0); // memoised: nothing it reads changed
2644
+ expect(synced.count).toBe(0);
2645
+
2646
+ recomputed.stop(); // and `restoreMockedProps()` — so `setupAutoSpy()` — takes both off anyway
2647
+ ```
2648
+
2649
+ Both return `{ count, stop() }`. `trackRecomputations` counts the **computation**, not the reads,
2650
+ and takes a `computed()` or a `linkedSignal()` — a plain `signal()` has nothing to compute and is
2651
+ refused by name. `trackEffectRuns` counts every run whoever asked for it: a flush, a `stable()`, a
2652
+ `runEffect()` of the same effect. Both wrap one member of Angular's reactive node through
2653
+ `mockValueProp`, so the undo is the journal every other stub in this package uses.
2231
2654
 
2232
2655
  ### ngrx `rxMethod`
2233
2656
 
@@ -2246,6 +2669,10 @@ const { fixture, component } = renderShallow(TaskListComponent, {
2246
2669
  });
2247
2670
  // other options: imports, keepTemplate, keepChildren, template, beforeCreate, detectChanges
2248
2671
 
2672
+ // change an input after the first render — one setInput per key, then the wait, in one await
2673
+ await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
2674
+ await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
2675
+
2249
2676
  // build a class through DI, every unprovided token auto-spied
2250
2677
  const { instance, spies } = createWithAutoSpies(CartService, {
2251
2678
  providers: [{ provide: TaxService, useValue: realTax }], // explicit providers win
@@ -2262,11 +2689,20 @@ flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
2262
2689
  flushEffects(); // an httpResource issues NO request until something ticks
2263
2690
  httpTesting.expectOne('/api/products').flush([product]);
2264
2691
  await settleResource(products, { label: 'the product resource' });
2692
+ // a resource left 'idle' FAILS here — params() returned undefined and value() is still the default;
2693
+ // { allowIdle: true } when that is the state under assertion, { turns } is spent exactly as reported
2694
+
2695
+ // on a component the resource is a FIELD and renderShallow is the injection context — nothing else changes
2696
+ const { component } = renderShallow(ProductListComponent, { providers: [...provideHttpTesting()], keepTemplate: true });
2697
+ await expectRequest('/api/products?q=').flush([product]); // both from /angular-http; renderShallow already ticked
2698
+ expect(component.products).toHaveResourceValue([product]);
2699
+ // after changing a signal the params() computation reads: flushEffects() before the next expectOne/expectRequest
2265
2700
 
2266
2701
  // ...or skip the request entirely when it is not what the spec is about
2267
- const products = mockResourceProp(service, 'products', []);
2268
- products.set([product]); // 'resolved' products.loading() products.fail('offline')
2269
- expect(products.reload).toHaveBeenCalled(); // reload is spied, and re-issues nothing
2702
+ const products = mockResourceProp(service, 'products', []); // { status: 'idle' } starts it unstarted
2703
+ products.set([product]); // 'resolved' products.loading() products.fail('offline') products.idle()
2704
+ service.products.set([edited]); // the component's own optimistic write — status → 'local'
2705
+ expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, and re-issues nothing
2270
2706
 
2271
2707
  // signal assertions
2272
2708
  registerSignalMatchers(); // once, in the setup file
@@ -2284,20 +2720,30 @@ Two zoneless traps:
2284
2720
  - `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
2285
2721
  effects. Asserting right after it reads state that has not finished computing. Use `await stable(fixture)`.
2286
2722
  - `expect(someSignal).toBeTruthy()` passes for **every** signal ever created — a signal is a
2287
- function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake.
2723
+ function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake — and a
2724
+ spy, which it refuses instead of calling.
2288
2725
 
2289
2726
  And one resource trap, which is the same shape one level up: an `httpResource()` reports `loading`
2290
2727
  with its **default** value until a tick _and_ a microtask after its response is flushed, so a spec
2291
2728
  that asserts too early asserts the default and passes. `settleResource` fails instead of passing
2292
2729
  emptily. Note the order — `flushEffects()` first (the request is issued there, not on creation),
2293
- then the flush, then the wait.
2730
+ then the flush, then the wait. The same trap wearing a different hat is a resource still `idle` when
2731
+ the wait ends: its `params()` computation returned `undefined`, the loader never ran, and `value()`
2732
+ is the default. `settleResource` refuses that too, naming the signal the computation reads;
2733
+ `{ allowIdle: true }` is the opt-out for a spec whose subject is the idle state. Each round is a tick
2734
+ plus a microtask, and from the third round an event-loop turn as well, so a loader on a real timer
2735
+ settles rather than burning the budget in microseconds.
2294
2736
 
2295
2737
  `toHaveResourceValue` is the matcher form of that trap and the reason to prefer it over
2296
2738
  `expect(products.value()).toEqual(...)`: it **fails an unresolved resource even when the default
2297
2739
  value matches**, and names the status it was in. And when the request is not what the spec is about
2298
2740
  at all, do not arrange one — `mockResourceProp(service, 'products', [])` replaces the property with
2299
- a double whose `set` / `fail` / `loading` move it directly, built from real `signal()`s so a
2300
- `computed()` downstream still recomputes. Nothing is in flight, so there is nothing to await.
2741
+ a double whose `set` / `fail` / `loading` / `idle` move it directly, built from real `signal()`s so a
2742
+ `computed()` downstream still recomputes. The double carries the whole of `ResourceRef`: `value` is
2743
+ writable and a write through it lands in `'local'`, `set` / `update` / `asReadonly` / `destroy` /
2744
+ `snapshot` behave as Angular's own do, and `hasValue()` is value-based rather than status-based —
2745
+ `true` while the resource is loading over a value that is defined, which is what Angular has answered
2746
+ since v20. Nothing is in flight, so there is nothing to await.
2301
2747
 
2302
2748
  Per-file timing, to find which specs actually pay for `TestBed`:
2303
2749
 
@@ -2368,12 +2814,16 @@ spec — after which `Spy<Mock>` and `Spy<Interface>` disagree and somebody cast
2368
2814
  **The second argument is not optional as often as it looks.** A spy answers `undefined` until it is
2369
2815
  told otherwise, and that is fatal the moment the code under test _chains_ off it — a constructor
2370
2816
  doing `inject(LOGGER).channel('auth').debug('…')` dies on the `.debug` of `undefined` before the
2371
- spec's first line runs, because nothing in production wrote `?.` there. Seed the link:
2817
+ spec's first line runs, because nothing in production wrote `?.` there. Name the link:
2372
2818
 
2373
2819
  ```ts
2374
- provideAutoSpyForToken(LOGGER, { channel: vi.fn().mockReturnThis() });
2820
+ provideAutoSpyForToken(LOGGER, undefined, { selfReturning: ['channel'] });
2375
2821
  ```
2376
2822
 
2823
+ `channel` stays a spy (assertable, and configured under `strict`) and answers the double itself.
2824
+ When every file wants it, register it once — `registerAutoSpyDefaults(LOGGER, { selfReturning:
2825
+ ['channel'] })` from `vitest-auto-spy/angular` (§5) — and the provider is `provideAutoSpyForToken(LOGGER)`.
2826
+
2377
2827
  For a chain more than one link long, `mockDeep<T>()` is the double that answers every level (§2).
2378
2828
 
2379
2829
  ### A host for a directive under test
@@ -2401,6 +2851,33 @@ _is_ declared; `NG0304` calls a missing directive a missing component; a bare at
2401
2851
  nothing at all). `schemas: [NO_ERRORS_SCHEMA]` next to a standalone component is a dead entry —
2402
2852
  schemas apply to a testing module's `declarations` only.
2403
2853
 
2854
+ ### A stand-in for a child — `createComponentStub`
2855
+
2856
+ Do not hand-write `class MockChartComponent` with a copied selector: nothing checks the copy, so a
2857
+ renamed input leaves the parent's binding pointing at nothing and the spec green. Build the stub
2858
+ from the real class's compiled definition instead:
2859
+
2860
+ ```ts
2861
+ import { createComponentStub } from 'vitest-auto-spy/angular';
2862
+
2863
+ const ChartStub = createComponentStub(ChartComponent); // also a directive or a pipe
2864
+
2865
+ TestBed.configureTestingModule({ imports: [DashboardComponent] });
2866
+ TestBed.overrideComponent(DashboardComponent, { remove: { imports: [ChartComponent] }, add: { imports: [ChartStub] } });
2867
+
2868
+ const chart = fixture.debugElement.query(By.directive(ChartStub)).componentInstance;
2869
+ expect(chart.series()).toEqual([1, 2, 3]); // a signal input stays a signal input
2870
+ chart.pointSelected.emit(2); // outputs are EventEmitters; a model() stays a model
2871
+ ```
2872
+
2873
+ Copied from `ɵcmp` / `ɵdir` / `ɵpipe`: the selector, every input under its public name (transforms
2874
+ included), every output, `exportAs`, a pipe's name and purity. Not copied: the template (the stub
2875
+ renders one `<ng-content>` per slot the real one projects — `{ template }` in the third argument
2876
+ replaces it), host bindings, providers, lifecycle hooks, queries. The second argument seeds members
2877
+ per instance — a method the parent calls through `viewChild`, a pipe's `transform` (identity by
2878
+ default). With `renderShallow`: `renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })`
2879
+ — the real child is dropped as a component import and the stub is the one child left.
2880
+
2404
2881
  ### Patching a property of a spy
2405
2882
 
2406
2883
  `mockReadonlyProp` / `mockValueProp` / `mockSignalProp` accept the `Spy<T>` that `injectSpy` returns,
@@ -2618,43 +3095,50 @@ export default [
2618
3095
  errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
2619
3096
  blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
2620
3097
 
2621
- | Rule | Level | Fix | Flags |
2622
- | --------------------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2623
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
2624
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
2625
- | `prefer-provide-auto-spy` | `error` | — | 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 |
2626
- | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
2627
- | `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 }` |
2628
- | `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 }` |
2629
- | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
2630
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
2631
- | `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 |
2632
- | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
2633
- | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
2634
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
2635
- | `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 |
2636
- | `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 |
2637
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
2638
- | `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 |
2639
- | `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 |
2640
- | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2641
- | `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 |
2642
- | `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 |
2643
- | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
2644
- | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
2645
- | `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` |
2646
- | `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` |
2647
- | `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 out of scope |
2648
- | `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 |
2649
- | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2650
- | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2651
- | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
2652
- | `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 |
2653
-
2654
- Thirty rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
2655
- `no-stub-class-double`, `no-structural-double` and `no-instance-lifecycle-spy`**; three fix on their own, nine offer suggestions. Twenty-eight are syntactic; `no-private-member-access` and `no-mistyped-use-value`
2656
- read types, and both report nothing at all without `parserOptions.project` / `projectService`
2657
- rather than guessing. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
3098
+ | Rule | Level | Fix | Flags |
3099
+ | --------------------------------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3100
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
3101
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3102
+ | `prefer-provide-auto-spy` | `error` | — | 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 |
3103
+ | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3104
+ | `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 }` |
3105
+ | `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 }` |
3106
+ | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
3107
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
3108
+ | `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 |
3109
+ | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3110
+ | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
3111
+ | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3112
+ | `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 |
3113
+ | `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 |
3114
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3115
+ | `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 |
3116
+ | `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 |
3117
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
3118
+ | `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 |
3119
+ | `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 |
3120
+ | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3121
+ | `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 |
3122
+ | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3123
+ | `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` |
3124
+ | `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` |
3125
+ | `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 |
3126
+ | `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` |
3127
+ | `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 |
3128
+ | `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>` |
3129
+ | `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 |
3130
+ | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
3131
+ | `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 |
3132
+ | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
3133
+ | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
3134
+ | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3135
+ | `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 |
3136
+
3137
+ Thirty-six rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3138
+ `no-stub-class-double`, `no-structural-double` and `no-instance-lifecycle-spy`**; three fix on their own, eleven offer suggestions. Thirty-three are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3139
+ `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3140
+ rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
3141
+ 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
2658
3142
  consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The
2659
3143
  four `warn`s left are not judgements about how much a finding matters. `prefer-render-shallow` is
2660
3144
  about the _kind_ of finding: every other rule names something wrong or dead, while this one names a
@@ -2787,6 +3271,18 @@ written above it inside a `createComponent` helper. Configure the double after t
2787
3271
  (`injectSpy(X)` in the test), or keep the access lazy: `const api = () => injectSpy(Api)`. The check
2788
3272
  is order-free by design, since a helper declared above the hook still runs after it.
2789
3273
 
3274
+ `no-ts-expect-error-on-double` reports the suppression, not the reason written after it. On one
3275
+ 1759-file suite it reports 34 directives in 15 files and **every one carries a reason** — so an
3276
+ escape for "a directive with a reason" would have silenced all of them. Four sat on overloaded
3277
+ clients, which `overload` repairs (§5); seven blamed "the collapsed generic" for a fixture the real
3278
+ instantiation rejects as well; nineteen hid a fixture or a production type that disagrees with the
3279
+ declared one. Four were deliberate — a value outside the declared type, to reach a default branch —
3280
+ and that is the case a per-line disable is for:
3281
+ `// eslint-disable-next-line vitest-auto-spy/no-ts-expect-error-on-double -- <why>` above the
3282
+ directive. The report sits on the directive's line so that comment reaches it. `rejectWith`,
3283
+ `failWith` and `throwWith` are not read: their parameter is `unknown`, so a directive there
3284
+ suppresses something other than the stub.
3285
+
2790
3286
  The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
2791
3287
  packages, which a subpath export can never be.
2792
3288
 
@@ -2794,108 +3290,126 @@ packages, which a subpath export can never be.
2794
3290
 
2795
3291
  ## 17. Error → fix
2796
3292
 
2797
- | Message contains | Cause | Fix |
2798
- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2799
- | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
2800
- | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
2801
- | `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 |
2802
- | `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) |
2803
- | `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) |
2804
- | `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) |
2805
- | `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)` |
2806
- | 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) |
2807
- | `names method(s) that are not on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2808
- | `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) |
2809
- | `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) |
2810
- | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2811
- | `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 |
2812
- | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2813
- | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
2814
- | `[vitest-auto-spy] "…" wrote to the console N time(s) and nothing absorbed it` | `setupAutoSpy({ strayConsole })` — the test printed, and nothing that does not call through stood on `console`: no `/console` spy installed for it, no `mockImplementation`. A bare `vi.spyOn(console, m)` calls through, and a `console.warn` from this library counts too| absorb and assert: `installConsoleSpies()` in a `beforeEach`, then `expect(consoleErrorSpy)…`; or fix the code that printed. Importing a spy installs nothing under the guard. `strayConsole: { allow: [...] }` only for environment noise no spec can reach |
2815
- | `… 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 |
2816
- | `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` |
2817
- | a seeded getter reads `undefined`: `provideAutoSpy(X, { overrides: { remoteConfig } })` 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 |
2818
- | `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: … })` |
2819
- | `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 |
2820
- | `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) |
2821
- | `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()` |
2822
- | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2823
- | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2824
- | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
2825
- | `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) |
2826
- | `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) |
2827
- | `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) |
2828
- | `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) |
2829
- | `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) |
2830
- | `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) |
2831
- | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` once per token and spec file, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` or `setupAutoSpy({ misconfiguration: 'throw' })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
2832
- | `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) |
2833
- | `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) |
2834
- | `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 })` |
2835
- | 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 |
2836
- | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
2837
- | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
2838
- | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
2839
- | `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) |
2840
- | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2841
- | `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 |
2842
- | `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) |
2843
- | `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) |
2844
- | `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) |
2845
- | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2846
- | 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) |
2847
- | 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) |
2848
- | `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 |
2849
- | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
2850
- | `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)` |
2851
- | `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 |
2852
- | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
2853
- | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
2854
- | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
2855
- | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
2856
- | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
2857
- | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
2858
- | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
2859
- | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
2860
- | 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 |
2861
- | 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 |
2862
- | `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({ … }))` |
2863
- | `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 |
2864
- | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
2865
- | 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…())` |
2866
- | 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 file; then `mockValueProp` |
2867
- | 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 |
2868
- | `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()` |
2869
- | 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 })` |
2870
- | 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 |
2871
- | 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 |
2872
- | `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 |
2873
- | `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) |
2874
- | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
2875
- | `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) |
2876
- | `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` |
2877
- | `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) |
2878
- | `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` |
2879
- | `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 |
2880
- | `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 |
2881
- | `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` |
2882
- | 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 |
2883
- | `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` |
2884
- | 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 |
2885
- | 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) |
2886
- | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
2887
- | 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) |
2888
- | `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | the warning on stderr says how many were taken and where the first three were scheduled; `onStrayTimers` gets every one in `timers` (§10) |
2889
- | `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 |
2890
- | `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 |
2891
- | `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 |
2892
- | `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) |
2893
- | `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` |
2894
- | `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) |
2895
- | 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 |
2896
- | `no request matched …` (with the requests that were made listed) | the URL, verb or predicate does not describe anything the code under test sent, or nothing subscribed | read the list in the message — it is the requests that were actually made; then fix the URL, add `{ method }`, or subscribe (§13) |
2897
- | `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) |
2898
- | `the test ended with N unanswered request(s)` | `provideHttpTesting({ verifyOnTeardown })` found a request the spec never flushed | answer it (`await expectRequest(url).flush(body)`), assert its absence (`expectNoRequest(url)`), or `{ verifyOnTeardown: false }` (§13) |
3293
+ | Message contains | Cause | Fix |
3294
+ | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3295
+ | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
3296
+ | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
3297
+ | `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 |
3298
+ | `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) |
3299
+ | `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) |
3300
+ | `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) |
3301
+ | `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)` |
3302
+ | 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) |
3303
+ | `names method(s) that are not on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
3304
+ | `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) |
3305
+ | `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) |
3306
+ | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
3307
+ | `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 |
3308
+ | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
3309
+ | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
3310
+ | `X.member was read N times and nothing configured it, and strict mode is on` (or `was subscribed to N times and nothing fed it`), after the test | `setupAutoSpy({ unconfiguredReads })` — the code under test read a strict double's spied getter and got `undefined`, or subscribed to its observable property and nothing ever emitted | configure it: `accessorSpies.getters.member.mockReturnValue(…)`, `overrides: { member }`, `mockReadonlyProp`; `nextWith` / `returnSubject` / `complete` for a stream; `mockReturnValue(undefined)` when `undefined` is the answer meant; `{ strict: false }` on that double (§5) |
3311
+ | `[vitest-auto-spy] "…" wrote to the console N time(s) and nothing absorbed it` | `setupAutoSpy({ strayConsole })` — the test printed, and nothing that does not call through stood on `console`: no `/console` spy installed for it, no `mockImplementation`. A bare `vi.spyOn(console, m)` calls through, and a `console.warn` from this library counts too | absorb and assert: `installConsoleSpies()` in a `beforeEach`, then `expect(consoleErrorSpy)…`; or fix the code that printed. Importing a spy installs nothing under the guard. `strayConsole: { allow: [...] }` only for environment noise no spec can reach |
3312
+ | `… 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 |
3313
+ | `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` |
3314
+ | a seeded getter reads `undefined`: `provideAutoSpy(X, { overrides: { remoteConfig } })` 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 |
3315
+ | `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: … })` |
3316
+ | `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 |
3317
+ | `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) |
3318
+ | `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()` |
3319
+ | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
3320
+ | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
3321
+ | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
3322
+ | `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) |
3323
+ | `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) |
3324
+ | `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) |
3325
+ | `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) |
3326
+ | `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) |
3327
+ | `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) |
3328
+ | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` once per token and spec file, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` or `setupAutoSpy({ misconfiguration: 'throw' })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
3329
+ | `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) |
3330
+ | `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) |
3331
+ | `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 })` |
3332
+ | 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 |
3333
+ | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
3334
+ | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
3335
+ | 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 |
3336
+ | `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) |
3337
+ | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
3338
+ | `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 |
3339
+ | `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) |
3340
+ | `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) |
3341
+ | `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) |
3342
+ | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
3343
+ | 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) |
3344
+ | 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) |
3345
+ | `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 |
3346
+ | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
3347
+ | `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)` |
3348
+ | `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 |
3349
+ | `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 |
3350
+ | `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 |
3351
+ | `mockSignalProp: '…' is an input() signal` | an `input()` replaced instead of set | `fixture.componentRef.setInput(name, value)`, or `renderShallow(C, { inputs: { … } })` |
3352
+ | `mockSignalProp: '…' is a read-only signal something has already read` | a `computed()` member swapped after the first render | patch before the first `detectChanges()` / `stable(fixture)`, or expose the member as a writable `signal()` |
3353
+ | `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 |
3354
+ | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
3355
+ | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
3356
+ | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
3357
+ | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
3358
+ | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
3359
+ | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
3360
+ | `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) |
3361
+ | `'isKeyEnabled' does not exist in type 'MethodReturns<{ remoteConfig: any; }>'` | generic class: `createSpyFromClass(Config, { gettersToSpyOn: ['remoteConfig'], 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) |
3362
+ | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
3363
+ | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
3364
+ | 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 |
3365
+ | 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 |
3366
+ | `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({ … }))` |
3367
+ | `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 |
3368
+ | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
3369
+ | 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…())` |
3370
+ | 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 file; then `mockValueProp` |
3371
+ | 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 |
3372
+ | `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()` |
3373
+ | 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 })` |
3374
+ | 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 |
3375
+ | `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 |
3376
+ | 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 |
3377
+ | `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 |
3378
+ | `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) |
3379
+ | `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) |
3380
+ | `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) |
3381
+ | `setInputs: … declares no input named …` | the key is not an input of that component — a typo, an input renamed since, or one of its own fields | set the names the message lists; a signal the component owns is moved through the signal (§13) |
3382
+ | `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` |
3383
+ | `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) |
3384
+ | `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` |
3385
+ | `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 |
3386
+ | `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 |
3387
+ | `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` |
3388
+ | 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 |
3389
+ | `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` |
3390
+ | 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 |
3391
+ | 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) |
3392
+ | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
3393
+ | 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) |
3394
+ | `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | the warning on stderr says how many were taken and where the first three were scheduled; `onStrayTimers` gets every one in `timers` (§10) |
3395
+ | `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 |
3396
+ | `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 |
3397
+ | `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 |
3398
+ | `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) |
3399
+ | `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` |
3400
+ | `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) |
3401
+ | `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) |
3402
+ | `selfReturning names 'x', which is not a spied method` | 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 | fix the name or the whitelist; the value is silently never returned otherwise (§5) |
3403
+ | 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 |
3404
+ | 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 |
3405
+ | `no request matched …` (with the requests that were made listed) | the URL, verb or predicate does not describe anything the code under test sent, or nothing subscribed | read the list in the message — it is the requests that were actually made; then fix the URL, add `{ method }`, or subscribe (§13) |
3406
+ | `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) |
3407
+ | `the test ended with N unanswered request(s)` | `provideHttpTesting({ verifyOnTeardown })` found a request the spec never flushed | answer it (`await expectRequest(url).flush(body)`), assert its absence (`expectNoRequest(url)`), or `{ verifyOnTeardown: false }` (§13) |
3408
+ | `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) |
3409
+ | `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) |
3410
+ | `provideActivatedRoute: the installed @angular/router 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) |
3411
+ | `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `getCurrentNavigation`, `isActive`, `resetConfig` | the double covers `url`, `events`, `navigate`, `navigateByUrl`, `createUrlTree`, `serializeUrl`, `parseUrl` and `routerState`; past that, a real `provideRouter([])` with `RouterTestingHarness` (§13) |
3412
+ | `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) |
2899
3413
 
2900
3414
  **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
2901
3415
  nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
@@ -2920,58 +3434,62 @@ diagnostic and is free to change: print it, never assert on it.
2920
3434
 
2921
3435
  ## 18. Do not write this
2922
3436
 
2923
- | ❌ | ✅ |
2924
- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
2925
- | `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
2926
- | `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
2927
- | `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
2928
- | `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
2929
- | `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
2930
- | `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
2931
- | `vi.spyOn(component, 'ngOnInit')` after `createComponent` | `vi.spyOn(Cls.prototype, 'ngOnInit')` before it, or assert the effect |
2932
- | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` | `useValue: false` — a value of the type the token declares |
2933
- | `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
2934
- | `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
2935
- | `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
2936
- | `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
2937
- | `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
2938
- | `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
2939
- | `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
2940
- | `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
2941
- | hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
2942
- | `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
2943
- | a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
2944
- | a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
2945
- | `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
2946
- | `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
2947
- | `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
2948
- | `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
2949
- | `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
2950
- | `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
2951
- | a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
2952
- | an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
2953
- | `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
2954
- | `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
2955
- | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
2956
- | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
2957
- | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
2958
- | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
2959
- | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
2960
- | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
2961
- | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
2962
- | `await fixture.whenRenderingDone()` | `await stable(fixture)` |
2963
- | an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
2964
- | `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
2965
- | `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
2966
- | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
2967
- | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
2968
- | `configureTestingModule` inside every `it()` | one per `describe` |
2969
- | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
2970
- | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
2971
- | `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
2972
- | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
2973
- | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
2974
- | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
3437
+ | ❌ | ✅ |
3438
+ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
3439
+ | `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
3440
+ | `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
3441
+ | `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
3442
+ | `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
3443
+ | `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
3444
+ | `{ provide: ActivatedRoute, useValue: { snapshot: { params } } }` | `provideActivatedRoute({ params })` — streams and snapshot from one record |
3445
+ | `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate } })` | `provideRouterDouble({ url })` — one URL behind `url`, `routerState` and `events` |
3446
+ | `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
3447
+ | `vi.spyOn(component, 'ngOnInit')` after `createComponent` | `vi.spyOn(Cls.prototype, 'ngOnInit')` before it, or assert the effect |
3448
+ | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` | `useValue: false` — a value of the type the token declares |
3449
+ | `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
3450
+ | `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
3451
+ | `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
3452
+ | `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
3453
+ | `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
3454
+ | `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
3455
+ | `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
3456
+ | `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
3457
+ | hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
3458
+ | `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
3459
+ | a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
3460
+ | a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
3461
+ | `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
3462
+ | `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
3463
+ | `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
3464
+ | `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
3465
+ | `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
3466
+ | `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
3467
+ | a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
3468
+ | an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
3469
+ | `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
3470
+ | `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
3471
+ | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
3472
+ | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
3473
+ | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
3474
+ | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
3475
+ | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
3476
+ | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
3477
+ | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
3478
+ | `await fixture.whenRenderingDone()` | `await stable(fixture)` |
3479
+ | an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
3480
+ | `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
3481
+ | `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
3482
+ | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
3483
+ | `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift |
3484
+ | a `TestingStorage` class assigned to `globalThis.localStorage` | `stubWebStorage('localStorage')`, restored with the other patches |
3485
+ | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
3486
+ | `configureTestingModule` inside every `it()` | one per `describe` |
3487
+ | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
3488
+ | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
3489
+ | `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
3490
+ | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
3491
+ | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
3492
+ | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
2975
3493
 
2976
3494
  **The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
2977
3495
  when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
@@ -3032,7 +3550,7 @@ Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
3032
3550
  ### If you were asked why a suite is slow
3033
3551
 
3034
3552
  ```bash
3035
- npx vitest-auto-spy perf # runs the suite once and reports; always exits 0
3553
+ npx vitest-auto-spy perf # runs the suite once and reports; exits 0 without --gate
3036
3554
  npx vitest-auto-spy perf src/some/dir # a path is passed through to Vitest as a file filter
3037
3555
  ```
3038
3556
 
@@ -3066,6 +3584,37 @@ is the default, resident memory measured at 1.42 GB plus ~155 MB per worker, and
3066
3584
  about 2.8 % of wall clock. Do not quote a worker count as universally right — it is a property of
3067
3585
  the machine.
3068
3586
 
3587
+ **A bare `vitest run` is not every repository's suite, and `perf` refuses to pretend otherwise.**
3588
+ Where the suite is assembled by something else — an Angular builder, an Nx target, a script that
3589
+ generates a config per project — there is no root `vite(st).config.*`, the Vitest defaults sweep up
3590
+ every `*.spec.*` in the tree with no globals and no aliases, and every file fails to collect. The
3591
+ phase table that comes out of that is real seconds spent on nothing: measured on one such workspace,
3592
+ 1 830 files, 29 s of wall clock and **zero** test bodies executed. `perf` checks for that shape
3593
+ before it runs anything, and refuses any report in which no body finished (exit 2). Measure the
3594
+ repository's own command instead:
3595
+
3596
+ ```bash
3597
+ npx vitest-auto-spy perf --command 'npm test' # measure that command
3598
+ npx vitest-auto-spy perf --command 'npm test -- {paths:--include=}' --gate
3599
+ ```
3600
+
3601
+ `--command` puts `VITEST_AUTO_SPY_PERF_OUT` and `VITEST_AUTO_SPY_PERF_REPORTER` in that command's
3602
+ environment; the configuration it reaches attaches the reporter itself (`reporters: perf ===
3603
+ undefined ? ['default'] : ['default', perf]`), and the reporter writes nothing at all when the first
3604
+ variable is unset, so it can be declared permanently. `{paths}` / `{paths:<prefix>}` is where the
3605
+ files of a confirmation pass go.
3606
+
3607
+ **`--gate` is the only part of this command that fails anything**, and three rules keep it honest.
3608
+ It judges the `tests` phase alone, because the other five are the harness and the machine rather than
3609
+ anybody's code. A file is over budget only when it is over both an absolute floor and `--factor` ×
3610
+ the median file **of the same run**, which is what makes the verdict the same on a loaded CI runner
3611
+ and on an idle laptop. And every candidate is re-measured on its own before it may fail anything: a
3612
+ file that is fast when it has the machine to itself is reported as _not reproduced_, an `info` rather
3613
+ than a finding. Without a way to re-measure, findings are warnings that fail nothing unless
3614
+ `--no-confirm` says one reading is enough. Exit `1` is "over budget", exit `2` is "there was nothing
3615
+ to judge" — including a red suite, which the gate will not judge at all, since a failed test is
3616
+ measured until its timeout and 30 s of timeout looks exactly like 30 s of slow code.
3617
+
3069
3618
  `--json <path>` re-analyses a report an earlier `--out <path>` run wrote, instead of running Vitest
3070
3619
  again. Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
3071
3620