vitest-auto-spy 5.21.0 → 5.22.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 (60) hide show
  1. package/AGENTS.md +88 -23
  2. package/README.md +51 -36
  3. package/dist/angular-diagnostics.d.ts +1 -1
  4. package/dist/angular-diagnostics.js +8 -8
  5. package/dist/angular-doubles.d.ts +3 -1
  6. package/dist/angular-doubles.js +7 -7
  7. package/dist/angular-http.d.ts +14 -2
  8. package/dist/angular-http.js +17 -9
  9. package/dist/angular-router.d.ts +93 -6
  10. package/dist/angular-router.js +112 -15
  11. package/dist/angular.d.ts +1 -1
  12. package/dist/angular.js +1 -1
  13. package/dist/bun-angular.d.ts +2 -2
  14. package/dist/bun-angular.js +16 -16
  15. package/dist/bun.d.ts +103 -1
  16. package/dist/bun.js +9 -9
  17. package/dist/{chunk-3EV45V6W.js → chunk-45W5LBPY.js} +5 -5
  18. package/dist/{chunk-R3DF5ZEO.js → chunk-BBPYNKGB.js} +1 -1
  19. package/dist/{chunk-ITOFGQTX.js → chunk-CPLUHRHD.js} +1 -1
  20. package/dist/{chunk-I6JZIWIZ.js → chunk-DQ3SPSCH.js} +3 -3
  21. package/dist/{chunk-ZZSFAQT4.js → chunk-E5M7DBWB.js} +31 -7
  22. package/dist/{chunk-RPCKHTDN.js → chunk-JBV35SU3.js} +1 -1
  23. package/dist/{chunk-4SJY7ZHM.js → chunk-LZVIH54N.js} +2 -2
  24. package/dist/{chunk-GWEQZHMN.js → chunk-MBSPAUUD.js} +4 -4
  25. package/dist/{chunk-SA2QIER3.js → chunk-MG7LKE4R.js} +1 -1
  26. package/dist/{chunk-2KBFEQOV.js → chunk-MUZZBRUP.js} +2 -2
  27. package/dist/{chunk-JJVTDL5R.js → chunk-QO4BPR3V.js} +3 -0
  28. package/dist/{chunk-DPTWTLIG.js → chunk-U5NJJ4ZN.js} +1 -1
  29. package/dist/{chunk-BN2QV45R.js → chunk-V6TIK7XU.js} +2 -2
  30. package/dist/{chunk-NS3Y6AQB.js → chunk-WYYKUPXW.js} +1 -1
  31. package/dist/{chunk-QT5JIDOK.js → chunk-YEQQIKQR.js} +1 -1
  32. package/dist/{chunk-TQAYWU5J.js → chunk-ZCWAFGHX.js} +2 -2
  33. package/dist/cli.js +11 -2
  34. package/dist/console.js +7 -7
  35. package/dist/dom-stubs.js +1 -1
  36. package/dist/eslint-plugin.cjs +50 -2
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +26 -2
  39. package/dist/jasmine-compat.js +2 -2
  40. package/dist/jasmine.js +10 -10
  41. package/dist/nestjs.js +10 -10
  42. package/dist/node.cjs +26 -1
  43. package/dist/node.d.cts +1 -0
  44. package/dist/node.d.ts +1 -1
  45. package/dist/node.js +26 -2
  46. package/dist/react.d.ts +1 -1
  47. package/dist/react.js +26 -2
  48. package/dist/rstest.d.ts +1 -1
  49. package/dist/rstest.js +9 -9
  50. package/dist/rxjs.js +2 -2
  51. package/dist/setup.d.ts +12 -3
  52. package/dist/setup.js +9 -3
  53. package/dist/signal-forms.js +1 -1
  54. package/dist/svelte.d.ts +1 -1
  55. package/dist/svelte.js +26 -2
  56. package/dist/{track-signal-runs-C3V6OIGM.d.ts → track-signal-runs-BSVObkds.d.ts} +1 -1
  57. package/dist/vue.d.ts +1 -1
  58. package/dist/vue.js +26 -2
  59. package/package.json +3 -1
  60. package/skills/vitest-auto-spy/SKILL.md +16 -11
package/AGENTS.md CHANGED
@@ -62,9 +62,9 @@ Add-ons, orthogonal to the runner:
62
62
  | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
63
63
  | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
64
64
  | Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
65
- | Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular` like `/angular-http` — no core re-export; **moved off `/angular` in 6.0** so importing spies stops evaluating it |
66
- | Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio and the `Window`/`Document` platform doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box; **moved off `/angular` in 6.0** |
67
- | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 6.0** |
65
+ | Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular` like `/angular-http` — no core re-export; **moved off `/angular` in 5.21.0** so importing spies stops evaluating it |
66
+ | Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio and the `Window`/`Document` platform doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box; **moved off `/angular` in 5.21.0** |
67
+ | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 5.21.0** |
68
68
  | Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
69
69
  | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
70
70
  | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
@@ -1300,6 +1300,29 @@ line that triggers the source, because the `await` is the same statement as the
1300
1300
  the test deadlocks against a source that only emits once something pokes it. Hold the promise
1301
1301
  first, poke, then await.
1302
1302
 
1303
+ ### `createLog()` — the order between the spies
1304
+
1305
+ ```ts
1306
+ const log = createLog<'drop-cache' | 'flush-telemetry' | 'stop-engine'>();
1307
+
1308
+ engine.onShutdown(log.fn('drop-cache'));
1309
+ engine.onShutdown(log.fn('stop-engine'));
1310
+ engine.onShutdown(log.fn('flush-telemetry'));
1311
+
1312
+ engine.shutdown();
1313
+
1314
+ expect(log.result()).toBe('drop-cache; flush-telemetry; stop-engine'); // fails with the real order
1315
+ ```
1316
+
1317
+ Emission helpers answer a sequence _within one source_. The order of calls **across** collaborators
1318
+ is the gap `toHaveBeenCalled` papers over — three green checks that would accept the sequence
1319
+ backwards — and `toHaveBeenCalledBefore` covers pairwise. One journal the code under test writes into
1320
+ (`add`, `fn(value)` for a labelled callback, `clear`, `items`, `result()`) makes the sequence a
1321
+ single comparable value; `T` is a string union so a step nobody declared is a compile error. In
1322
+ Angular, provide it and let the component report its own lifecycle:
1323
+ `providers: [{ provide: PANEL_LOG, useValue: log }]`. Ported from Angular's own `Log`, which the
1324
+ framework keeps three copies of.
1325
+
1303
1326
  ---
1304
1327
 
1305
1328
  ## 9. Patching properties (and putting them back)
@@ -1659,6 +1682,14 @@ a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the too
1659
1682
  Plain data in `body` goes out as JSON with `application/json`; an `ok` that disagrees with
1660
1683
  `status` throws. A body can be read once: for a stub answering several calls use
1661
1684
  `mockImplementation(async () => stubResponse(…))`, not `mockResolvedValue`.
1685
+ - **`body: null` is the JSON literal, and only `undefined` or an omitted `body` send no body.** It
1686
+ reverses what the field used to mean: `null` sent no body, so `.json()` threw
1687
+ `Unexpected end of JSON input` on it while `0`, `false`, `[]` and `{}` round-tripped — the one
1688
+ literal a reader had no reason to expect to be different, and the shape a backend answering
1689
+ "nothing here" (an empty login, an absent profile) really sends. A spec that meant "no body"
1690
+ drops the field. `{ body: null, status: 204 }` throws by name rather than letting the constructor
1691
+ raise "Response with null body status cannot have body" about a field whose old meaning was the
1692
+ opposite; 205 and 304 are the same.
1662
1693
  - `blockNetwork` leaves `fetch` alone while `Symbol.for('fetch-interceptor')` is on `globalThis`
1663
1694
  (MSW `setupServer`, nock 14), so MSW handlers keep answering; XHR stays blocked for what MSW does
1664
1695
  not handle. MSW's `onUnhandledRequest: 'error'` exempts asset-looking URLs (`.svg`, `.json`,
@@ -2286,7 +2317,7 @@ zone.js alike. The entry needs **Angular >= 20** (§1); on 16 or 17 it does not
2286
2317
  `ɵSIGNAL` is not there to import.
2287
2318
 
2288
2319
  **Niche Angular helpers ship in narrow companion subpaths, never in `/angular`** — the pattern of
2289
- `/angular-http`, `/angular-router` and `/signal-forms`, and since 6.0 of `/angular/diagnostics`,
2320
+ `/angular-http`, `/angular-router` and `/signal-forms`, and since 5.21.0 of `/angular/diagnostics`,
2290
2321
  `/angular/doubles` and `/angular/matchers`. An import of `/angular` evaluates its whole graph, so a
2291
2322
  helper a suite names once in a setup file must not ride along with every `provideAutoSpy` import;
2292
2323
  the diagnostics, doubles and matcher registrars left on that rule (`trackInjections` stays —
@@ -2703,12 +2734,12 @@ response reaches the resource, tick again so the view reading it is current. Mis
2703
2734
  `expectOne` reports a request that was never sent; miss the fifth and the assertion reads the
2704
2735
  resource's **default** value and passes.
2705
2736
 
2706
- | Call | Does |
2707
- | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
2708
- | `provideHttpTesting(opts?)` | `provideHttpClient()` + `provideHttpClientTesting()`; `{ verifyOnTeardown }` defaults to `true` |
2709
- | `expectRequest(matcher, opts?)` | tick, then the one match — `.request`, `.flush(body, opts?)`, `.error(status, opts?)` |
2710
- | `expectNoRequest(matcher?, opts?)` | tick, then assert nothing matched; no argument means "nothing at all was requested" |
2711
- | `verifyNoPendingRequests()` | the teardown check by hand, for mid-test use or a suite with `verifyOnTeardown: false` |
2737
+ | Call | Does |
2738
+ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2739
+ | `provideHttpTesting(opts?)` | `provideHttpClient()` + `provideHttpClientTesting()`; `{ verifyOnTeardown }` defaults to `true`, and takes `{ ignoreCancelled }` instead of `false` |
2740
+ | `expectRequest(matcher, opts?)` | tick, then the one match — `.request`, `.flush(body, opts?)`, `.error(status, opts?)` |
2741
+ | `expectNoRequest(matcher?, opts?)` | tick, then assert nothing matched; no argument means "nothing at all was requested" |
2742
+ | `verifyNoPendingRequests(opts?)` | the teardown check by hand, for mid-test use or a suite with `verifyOnTeardown: false`; `{ ignoreCancelled: true }` forgives a request the code under test unsubscribed from |
2712
2743
 
2713
2744
  `matcher` is a URL (matched against either `url` or `urlWithParams`), a `RegExp` over
2714
2745
  `urlWithParams`, or a predicate `(request) => boolean`. `{ method }` is case-insensitive.
@@ -2755,11 +2786,11 @@ is `undefined` on the `{ params: of(…) }` one, and a spec that updates one hal
2755
2786
  no navigation produces. `provideActivatedRoute(init)` provides **Angular's own `ActivatedRoute`**
2756
2787
  built over one record, so every stream, both `ParamMap`s and `snapshot` read the same values.
2757
2788
 
2758
- | Call | Does |
2759
- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
2760
- | `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `fragment`, `url`, `outlet`, `component`, `routeConfig` |
2761
- | `injectActivatedRoute(injector?)` | the handle: `.route`, `setParams`, `setQueryParams`, `setData`, `setFragment`, `setUrl`, `set({ … })` |
2762
- | `createActivatedRoute(init?)` | the same handle without a `TestBed` — for `new Page(route)`, or a guard given `route.snapshot` |
2789
+ | Call | Does |
2790
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2791
+ | `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `title`, `fragment`, `url`, `outlet`, `component`, `routeConfig`, `resolve` |
2792
+ | `injectActivatedRoute(injector?)` | the handle: `.route`, `setParams`, `setQueryParams`, `setData`, `setFragment`, `setUrl`, `set({ … })` |
2793
+ | `createActivatedRoute(init?)` | the same handle without a `TestBed` — for `new Page(route)`, or a guard given `route.snapshot` |
2763
2794
 
2764
2795
  A setter behaves like the router after a navigation: it **replaces** (not merges) that part, builds
2765
2796
  a new snapshot first, then emits only the streams whose value changed (router equality: same keys,
@@ -2773,11 +2804,14 @@ Four things to know:
2773
2804
  still works as `relativeTo` for `router.createUrlTree`.
2774
2805
  - **One node, no tree.** `root` is the route itself, `parent` / `firstChild` are `null`. For
2775
2806
  `route.parent.params` patch that member with `mockReadonlyProp`, or use `RouterTestingHarness`.
2776
- - **No `title`, no navigation, no input binding** — `title` emits `undefined`, `Router.navigate`
2777
- does not move it (the setters do), and `withComponentInputBinding()` is the outlet's job
2778
- (`fixture.componentRef.setInput`).
2807
+ - **`title` is given, not resolved; no navigation, no input binding.** `provideActivatedRoute({
2808
+ title: 'Product 7' })` answers `route.snapshot.title` — the double puts the string under the
2809
+ router's own `RouteTitleKey`, read off the installed router, and does not run a `title: () => …`
2810
+ resolver. `Router.navigate` does not move it (the setters do), and `withComponentInputBinding()`
2811
+ is the outlet's job (`fixture.componentRef.setInput`).
2779
2812
  - **Its own entry, like `/angular-http`.** It does not re-export the core, imports no runner, and is
2780
- the only file that reaches `@angular/router` — an optional peer.
2813
+ the only file that reaches `@angular/router` — an optional peer. The `Location` double below wraps
2814
+ `@angular/common/testing`, which `@angular/router` itself depends on, so it adds no peer.
2781
2815
 
2782
2816
  ### `Router` from one URL — `vitest-auto-spy/angular-router`
2783
2817
 
@@ -2803,6 +2837,7 @@ redirect a guard builds. `provideRouterDouble(init)` keeps one URL and derives t
2803
2837
  | `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
2804
2838
  | `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)`, `setCurrentNavigation(navigation?)` |
2805
2839
  | `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
2840
+ | `collectRouterEvents(events)` | the recording of what `router.events` emits, `expect([[Class, url], …])` in one line — Angular's own integration-spec idiom |
2806
2841
 
2807
2842
  Unlike the route, this is **not** an instance of Angular's class: a real `Router` drags the whole
2808
2843
  routing stack in. It is a structural double that answers `url` (serialized as the real router does),
@@ -2821,7 +2856,9 @@ Five things to know:
2821
2856
  `url`. `setUrl()` and `emitNavigation()` move it; `RouterTestingHarness` over a real
2822
2857
  `provideRouter()` is what tests a navigation.
2823
2858
  - **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
2824
- `NavigationEnd` moves the URL with it; any other event does not.
2859
+ `NavigationEnd` moves the URL with it; any other event does not. It resolves once the event is
2860
+ delivered and a navigation it ended is back to `null` — the moment `navigate()` resolves in an
2861
+ application; the work is synchronous, so ignoring the promise changes nothing.
2825
2862
  - **The navigation in flight follows the events, and a terminal event ends it _after_ delivering
2826
2863
  it.** `setCurrentNavigation({ extras: { state } })` puts one up and `setCurrentNavigation(null)`
2827
2864
  ends it; a `NavigationStart` pushed through `emitNavigation()` starts one with that event's id,
@@ -2839,6 +2876,28 @@ Five things to know:
2839
2876
  - **It builds spies**, so a runtime entry has to be imported in the suite (`vitest-auto-spy/angular`
2840
2877
  in the same file, or the setup file); the route double needs none.
2841
2878
 
2879
+ ### `Location` from Angular's own testing classes — `vitest-auto-spy/angular-router`
2880
+
2881
+ ```ts
2882
+ import { injectLocationDouble, provideLocationDouble } from 'vitest-auto-spy/angular-router';
2883
+
2884
+ TestBed.configureTestingModule({ providers: [provideLocationDouble()] });
2885
+
2886
+ const location = injectLocationDouble();
2887
+
2888
+ location.go('/reports/7');
2889
+ location.simulateUrlPop('/'); // the popstate no method call can cause
2890
+ ```
2891
+
2892
+ **A wrap, not a rival.** Angular ships the double — `SpyLocation` with a real history array, a
2893
+ `urlChanges` journal for assertions, and `simulateUrlPop()` / `simulateHashChange()` for the
2894
+ browser's half of the contract — and this entry puts it in the family shape of the route and the
2895
+ router: one provider call, an `injectLocationDouble()` whose errors name the instance that won
2896
+ (`Location` is `providedIn: 'root'`, so the quiet failure is the real `Location` nobody configured).
2897
+ `createLocationDouble()` is the same without a `TestBed`. One asymmetry worth knowing: `back()` and
2898
+ `forward()` wake the popstate subscribers but do not write `urlChanges` — the journal holds what the
2899
+ app asked for, the subscribers carry what the browser did.
2900
+
2842
2901
  ### Signal forms — `vitest-auto-spy/signal-forms`
2843
2902
 
2844
2903
  ```ts
@@ -3640,6 +3699,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3640
3699
  | `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
3641
3700
  | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3642
3701
  | `no-hand-assigned-global` | `error` | — | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages; the three observers stay with `prefer-observer-stub` |
3702
+ | `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or `createMock<Response>(…)` / `createAutoMock<Response>(…)` → `stubResponse({ body })` from `/setup`; the literal answers `undefined` for every member it does not list (`status`, `headers`, `text()`) and the cast is what makes that compile. `Response` must resolve to the **global**, so an Express handler's or a generated client's `Response` is never reported |
3643
3703
  | `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 |
3644
3704
  | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3645
3705
  | `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` |
@@ -3657,7 +3717,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3657
3717
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3658
3718
  | `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 |
3659
3719
 
3660
- Thirty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3720
+ Forty rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3661
3721
  `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3662
3722
  `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3663
3723
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
@@ -3969,7 +4029,7 @@ packages, which a subpath export can never be.
3969
4029
  | 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 |
3970
4030
  | `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) |
3971
4031
  | `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) |
3972
- | `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) |
4032
+ | `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)`), forgive an unsubscribed one with `{ verifyOnTeardown: { ignoreCancelled: true } }`, or `{ verifyOnTeardown: false }` (§13) |
3973
4033
  | `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) |
3974
4034
  | `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) |
3975
4035
  | `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) |
@@ -4140,7 +4200,12 @@ the eaten glob below came from.
4140
4200
 
4141
4201
  Two of its checks are about this package's own names. `helper-from-wrong-entry` catches a named
4142
4202
  import taken from an entry that does not export it — `provideAutoSpy` from the root rather than
4143
- `/angular` or `/nestjs`, `expectRequest` from anywhere but `/angular-http`. `no-unawaited-helper`
4203
+ `/angular` or `/nestjs`, `expectRequest` from anywhere but `/angular-http`. It reads both directions
4204
+ of the 5.21.0 Angular split — one of the thirty-two moved names still taken from `/angular`, and a
4205
+ core name taken from `/angular/diagnostics`, `/angular/doubles` or `/angular/matchers`, which export
4206
+ none of them — which makes it what fails that upgrade rather than a compiler: a repository green on
4207
+ 5.20.0 exits 1 here with no test run, and the fix line names the companion to move to.
4208
+ `no-unawaited-helper`
4144
4209
  catches an `expectEmission` / `expectError` / `stable` / `flushEventLoop` called as a bare statement
4145
4210
  and dropped, so the promise settles after the test ended and its assertion reports into a later test
4146
4211
  or nowhere. Both resolve the name against a table **generated from this package's own `exports`
package/README.md CHANGED
@@ -23,7 +23,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
23
23
  [![downloads per month](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2Flast-month%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2Fmonth)](https://www.npmjs.com/package/vitest-auto-spy)
24
24
  [![downloads over 18 months](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2F2026-06-21%3A2030-01-01%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2F18mo)](https://www.npmjs.com/package/vitest-auto-spy)
25
25
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
26
- [![minzipped size](https://img.shields.io/badge/minzip-22.5%20kB-brightgreen)](#install)
26
+ [![minzipped size](https://img.shields.io/badge/minzip-22.6%20kB-brightgreen)](#install)
27
27
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
28
28
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
29
29
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -80,7 +80,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
80
80
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored; `moduleNamespace(await importOriginal(), { passthrough: true })` is Vitest's `{ spy: true }` with `calledWith` on top, on any runner
81
81
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
82
82
  - 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
83
- - 📏 Lint rules and one-line test-run hygiene — thirty-nine rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, thirteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade
83
+ - 📏 Lint rules and one-line test-run hygiene — forty rules in `vitest-auto-spy/eslint-plugin` (four `--fix`, thirteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade
84
84
  - 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
85
85
  - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
86
86
  - ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
@@ -1291,11 +1291,19 @@ runner's cleanups (`vi.restoreAllMocks()` restores spies, `vi.unstubAllGlobals()
1291
1291
  `vi.stubGlobal`), so the fake answers every later test of the file. The `no-hand-assigned-global` lint
1292
1292
  rule reports it. `vi.stubGlobal('fetch', …)` with `unstubGlobals: true` in the config is the runner's
1293
1293
  own equivalent. `stubResponse` builds a real `Response` from the environment's own constructor —
1294
- plain data goes out as JSON — and a body reads once, so answer with a fresh one per call
1294
+ plain data goes out as JSON, `null` included, while `undefined` and an omitted `body` send no body
1295
+ at all — and a body reads once, so answer with a fresh one per call
1295
1296
  (`vi.fn(async () => stubResponse(…))`) rather than `mockResolvedValue`. A spec that only has to stay
1296
1297
  off the network wants [`blockNetwork()`](#test-run-hygiene), which leaves `fetch` to MSW's interceptor
1297
1298
  when `server.listen()` installed one.
1298
1299
 
1300
+ `body: null` is a **behaviour change**: it used to send no body, so `.json()` threw
1301
+ `Unexpected end of JSON input` on the one JSON literal that did not round-trip while `0`, `false`,
1302
+ `[]` and `{}` all did. It is now the literal — which is what a backend answering "nothing here"
1303
+ sends — and "no body" is `undefined` or an omitted field, the spelling every caller already used.
1304
+ A spec that wanted the old reading drops the field; `{ body: null, status: 204 }` throws by name,
1305
+ since those statuses carry no body.
1306
+
1299
1307
  ### How to mock: the console
1300
1308
 
1301
1309
  A test that expects output absorbs it, and asserts on it:
@@ -1442,34 +1450,34 @@ the conditional types that pick helpers from a return type — see
1442
1450
  The library ships a framework-agnostic core plus runtime and framework layers, so a plain
1443
1451
  Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runtime bundle**:
1444
1452
 
1445
- | Import | Provides | Pulls in | Status |
1446
- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | :----: |
1447
- | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
1448
- | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) and `createObservableWithValues` | `rxjs` | ✅ |
1449
- | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `setInputs`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, `mockResourceProp`, the `mock*Prop` helpers, `trackRecomputations` / `trackEffectRuns` — the diagnostics, doubles and matchers below moved off this entry in 6.0 | `@angular/core` | ✅ |
1450
- | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family — a companion to `/angular`, no core re-export; moved off `/angular` in 6.0 | `@angular/core` | ✅ |
1451
- | `vitest-auto-spy/angular/doubles` | the `window` / `document` and Material-dialog doubles; registers the Vitest adapter, so its doubles spy out of the box; moved off `/angular` in 6.0 | `@angular/core` | ✅ |
1452
- | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` — a companion to `/angular`, no core re-export; moved off `/angular` in 6.0 | `@angular/core`, `@angular/platform-browser` | ✅ |
1453
- | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1454
- | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute`, `createActivatedRoute` — Angular's own `ActivatedRoute` over one record, streams and snapshot moved together; `provideRouterDouble` / `injectRouterDouble` for the `Router` beside it | `@angular/router` | ✅ |
1455
- | `vitest-auto-spy/signal-forms` | `createForm` — Angular's own `form()` built where it can inject — and `registerFormMatchers()` for `toHaveFieldErrors` | `@angular/forms` (>=22) | ✅ |
1456
- | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
1457
- | `vitest-auto-spy/bun-angular` | Angular's `TestBed` under `bun test` — DOM, JIT `templateUrl` resolution and a zoneless environment, from one preload | `bun:test`, `@angular/core`, `@angular/platform-browser` | ✅ |
1458
- | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()`, plus `trackNodeMocks()` — a private `MockTracker` so dropped spies are actually freed | `node:test` | ✅ |
1459
- | `vitest-auto-spy/rstest` | the same core, driven by Rstest's `rstest.fn()` / `rstest.spyOn()` | `@rstest/core` | ✅ |
1460
- | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
1461
- | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
1462
- | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
1463
- | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
1464
- | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
1465
- | `vitest-auto-spy/jasmine` | the drop-in surface for a `jasmine-auto-spies` suite — `.and` / `.calls` / `.withArgs` on every spy, `createSpyObj`, the `jasmine` namespace, `registerJasmineMatchers` | `vitest` | ✅ |
1466
- | `vitest-auto-spy/jasmine-compat` | `enableJasmineCompat()` alone — the same `.and` / `.calls` layer, registering no adapter, for `bun test` and `node --test` | — (your runner) | ✅ |
1467
- | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` / `ObserverSpy` / `SubscriberSpy` — the `@hirez_io/observer-spy` surface, so `/rxjs` does not carry it | `rxjs` | ✅ |
1468
- | `vitest-auto-spy/dom-stubs` | the globals a component builds for itself — `stubIntersectionObserver`, `stubResizeObserver`, `stubMutationObserver`, `stubObserver`, `stubMediaElement`, `stubAbortController`, the entry builders, and `stubWebStorage` with its `snapshot()`. On the root entry until 4.0.0 | — | ✅ |
1469
- | `vitest-auto-spy/diagnostics` | `compareTestRuns` / `summarizeTestRun` / `formatTestRunComparison`, `diffByField` and `explainSpy` — the two reports a counter cannot give; pure functions, so a plain Node script can import this one too. On the root entry until 4.0.0 | — | ✅ |
1470
- | `vitest-auto-spy/setup` | `setupAutoSpy()` — property restore, duplicate-copy detection and mock-registry hygiene in one call; `setupFakeTimers()` / `advanceTimers()` | `vitest` | ✅ |
1471
- | `vitest-auto-spy/zone` | `fakeAsync` / `waitForAsync` on Vitest — the ProxyZone patch `zone.js/testing` does not ship. Reads the `zone.js` **you** loaded; imports none of it | — (your `zone.js`) | ✅ |
1472
- | `vitest-auto-spy/eslint-plugin` | the lint rules that steer a suite onto these helpers | — (your `eslint`) | ✅ |
1453
+ | Import | Provides | Pulls in | Status |
1454
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | :----: |
1455
+ | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
1456
+ | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) and `createObservableWithValues` | `rxjs` | ✅ |
1457
+ | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `setInputs`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, `mockResourceProp`, the `mock*Prop` helpers, `trackRecomputations` / `trackEffectRuns` — the diagnostics, doubles and matchers below moved off this entry in 5.21.0 | `@angular/core` | ✅ |
1458
+ | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family — a companion to `/angular`, no core re-export; moved off `/angular` in 5.21.0 | `@angular/core` | ✅ |
1459
+ | `vitest-auto-spy/angular/doubles` | the `window` / `document` and Material-dialog doubles; registers the Vitest adapter, so its doubles spy out of the box; moved off `/angular` in 5.21.0 | `@angular/core` | ✅ |
1460
+ | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` — a companion to `/angular`, no core re-export; moved off `/angular` in 5.21.0 | `@angular/core`, `@angular/platform-browser` | ✅ |
1461
+ | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1462
+ | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute`, `createActivatedRoute` — Angular's own `ActivatedRoute` over one record, streams and snapshot moved together; `provideRouterDouble` / `injectRouterDouble` for the `Router` beside it, with `collectRouterEvents()` asserting the events that follow; `provideLocationDouble` wrapping the `SpyLocation` Angular ships | `@angular/router`, `@angular/common` | ✅ |
1463
+ | `vitest-auto-spy/signal-forms` | `createForm` — Angular's own `form()` built where it can inject — and `registerFormMatchers()` for `toHaveFieldErrors` | `@angular/forms` (>=22) | ✅ |
1464
+ | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
1465
+ | `vitest-auto-spy/bun-angular` | Angular's `TestBed` under `bun test` — DOM, JIT `templateUrl` resolution and a zoneless environment, from one preload | `bun:test`, `@angular/core`, `@angular/platform-browser` | ✅ |
1466
+ | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()`, plus `trackNodeMocks()` — a private `MockTracker` so dropped spies are actually freed | `node:test` | ✅ |
1467
+ | `vitest-auto-spy/rstest` | the same core, driven by Rstest's `rstest.fn()` / `rstest.spyOn()` | `@rstest/core` | ✅ |
1468
+ | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
1469
+ | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
1470
+ | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
1471
+ | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
1472
+ | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
1473
+ | `vitest-auto-spy/jasmine` | the drop-in surface for a `jasmine-auto-spies` suite — `.and` / `.calls` / `.withArgs` on every spy, `createSpyObj`, the `jasmine` namespace, `registerJasmineMatchers` | `vitest` | ✅ |
1474
+ | `vitest-auto-spy/jasmine-compat` | `enableJasmineCompat()` alone — the same `.and` / `.calls` layer, registering no adapter, for `bun test` and `node --test` | — (your runner) | ✅ |
1475
+ | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` / `ObserverSpy` / `SubscriberSpy` — the `@hirez_io/observer-spy` surface, so `/rxjs` does not carry it | `rxjs` | ✅ |
1476
+ | `vitest-auto-spy/dom-stubs` | the globals a component builds for itself — `stubIntersectionObserver`, `stubResizeObserver`, `stubMutationObserver`, `stubObserver`, `stubMediaElement`, `stubAbortController`, the entry builders, and `stubWebStorage` with its `snapshot()`. On the root entry until 4.0.0 | — | ✅ |
1477
+ | `vitest-auto-spy/diagnostics` | `compareTestRuns` / `summarizeTestRun` / `formatTestRunComparison`, `diffByField` and `explainSpy` — the two reports a counter cannot give; pure functions, so a plain Node script can import this one too. On the root entry until 4.0.0 | — | ✅ |
1478
+ | `vitest-auto-spy/setup` | `setupAutoSpy()` — property restore, duplicate-copy detection and mock-registry hygiene in one call; `setupFakeTimers()` / `advanceTimers()` | `vitest` | ✅ |
1479
+ | `vitest-auto-spy/zone` | `fakeAsync` / `waitForAsync` on Vitest — the ProxyZone patch `zone.js/testing` does not ship. Reads the `zone.js` **you** loaded; imports none of it | — (your `zone.js`) | ✅ |
1480
+ | `vitest-auto-spy/eslint-plugin` | the lint rules that steer a suite onto these helpers | — (your `eslint`) | ✅ |
1473
1481
 
1474
1482
  ✅ all entry points published (see [Availability](#availability)).
1475
1483
 
@@ -3099,6 +3107,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
3099
3107
  | `stubMediaElement(opts?)` | `/dom-stubs` | A `<video>` / `<audio>` that plays, reports a duration and fires the media events jsdom never does |
3100
3108
  | `assertMocked(namespace, opts?)` | core | Fail when the `vi.mock()` a spec relies on silently did not apply (a bundled alias, `isolate: false`) |
3101
3109
  | `moduleNamespace(exports, opts?)` | core | The `vi.mock` factory result an interop probe recognises — `default` + `__esModule` in place — a `default` the factory spells out is kept; `{ passthrough: true }` spies every function export through to the real one |
3110
+ | `createLog<Step>()` | core | The order of calls **across** collaborators as one comparable value — `add`, `fn(value)` for a labelled callback, `clear`, `items`, `result()`; `Step` is a string union, so a step nobody declared is a compile error rather than a typo the journal records |
3102
3111
  | `diffByField(actual, expected)` | `/diagnostics` | Which field of an array of records moved, and in how many elements — the diff the reporter collapses |
3103
3112
  | `captureArg<T>(options?)` | core | Take hold of a callback or config the code under test built, instead of describing its shape — assertions only, never `calledWith`; `{ where }` records only what it accepts and fails the position on anything else ([details](#taking-hold-of-an-argument--capturearg)) |
3104
3113
  | `explainSpy(spy, method?)` | `/diagnostics` | Every configured argument list next to every recorded call, each attributed to the config it hit — before anything failed |
@@ -3127,11 +3136,13 @@ single-purpose utility you can pick up independently — they all ride on the sa
3127
3136
  | `setInputs(fixture, inputs, opts?)` | `/angular` | Change a component input mid-test — one `setInput` per name, checked against the compiled definition, then one wait |
3128
3137
  | `trackRecomputations(signal)` / `trackEffectRuns(ref)` | `/angular` | Count what the reactive graph actually did: `{ count, stop() }`, so "the filter change did not re-run the sync effect" is an assertion |
3129
3138
  | `provideRouterDouble(init?)` / `injectRouterDouble()` | `/angular-router` | A `Router` derived from one URL — real `serializeUrl` / `createUrlTree`, `navigate` spies, `emitNavigation()` over a `BehaviorSubject`, `currentNavigation()` and `setCurrentNavigation()` |
3139
+ | `collectRouterEvents(events)` | `/angular-router` | The router events that followed, as one comparable value — `expect([[NavigationStart, '/checkout'], [NavigationEnd, '/checkout']])`, a class-and-URL pair per event, failing with the event that was there instead |
3140
+ | `provideLocationDouble()` / `injectLocationDouble()` / `createLocationDouble()` | `/angular-router` | Angular's own `SpyLocation` and `MockLocationStrategy` in one call — a real history, the `urlChanges` journal, `simulateUrlPop()` / `simulateHashChange()` for the browser's half of the contract; `Location` is `providedIn: 'root'`, so a spec that forgets the provider silently drives the platform's real one |
3130
3141
  | `createForm(model, schema?)` / `registerFormMatchers()` | `/signal-forms` | A signal form built where `form()` can inject, and `expect(field).toHaveFieldErrors(['required'])` over what it produced |
3131
3142
  | `provideWindowDouble(TOKEN, over?)` / `provideDocumentDouble(over?)` | `/angular/doubles` | A `window` / `document` merged **over** the real jsdom one, so members the spec never named still answer; the globals stay untouched |
3132
3143
  | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(Ref, init?)` | `/angular/doubles` | The Material dialog trio without `@angular/material` as a dependency — the token and the ref class are arguments, `afterClosed()` still answers after the close |
3133
3144
  | `blockNetwork(options?)` | `/setup` | Close `fetch`, `XMLHttpRequest` and `sendBeacon`, naming what was requested; called twice, the last caller's mode wins; `fetch` is left to an applied MSW / nock interceptor ([details](#test-run-hygiene)) |
3134
- | `stubResponse(init?)` | `/setup` | A real `Response` for a stubbed `fetch` — `body` (plain data → JSON), `status`, `ok`, `statusText`, `headers`, `url`; no cast ([recipe](#how-to-mock-fetch-and-other-globals)) |
3145
+ | `stubResponse(init?)` | `/setup` | A real `Response` for a stubbed `fetch` — `body` (plain data and `null` → JSON, `undefined` → no body), `status`, `ok`, `statusText`, `headers`, `url`; no cast ([recipe](#how-to-mock-fetch-and-other-globals)) |
3135
3146
  | `trackStrayRejections()` / `flushStrayRejections()` / `countStrayRejections()` | `/setup` | Read back the promise rejections zone.js swallowed into `console.error`, so one can fail a test ([details](#test-run-hygiene)) |
3136
3147
  | `trackStrayTimers()` / `cancelStrayTimers()` / `countStrayTimers()` | `/setup` | Timers a file left pending: record every timeout, interval and frame it hands out, cancel what is still outstanding when the file ends (and how many that was), or count what stands and fail on it — the two halves `setupAutoSpy({ strayTimers: true })` installs ([details](#test-run-hygiene)) |
3137
3148
  | `guardGlobalPatches(reaction)` | `/setup` | Name the test that redefined a property of `document` / `navigator` / `globalThis` as non-configurable |
@@ -3786,6 +3797,7 @@ export can never be.
3786
3797
  | `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })`, which resolves every name against the compiled definition before it writes one — an undeclared name is an `NG0303` on the console and no change at all — and types the value. The suggestion collapses the run, drops a `detectChanges()` under it and makes the callback `async`; it is offered rather than applied, because `stable()` ticks and a zone.js suite can answer that with `NG0101` |
3787
3798
  | `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
3788
3799
  | `no-hand-assigned-global` | `error` | — | `global.fetch = vi.fn(…)` and any other double assigned to a global with no restore in a teardown hook → `mockValueProp(globalThis, …)` / `vi.stubGlobal` + `unstubGlobals`, `blockNetwork()` for a spec that only stays offline, `stubWebStorage()` for the storages |
3800
+ | `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or a `createMock<Response>(…)` / `createAutoMock<Response>(…)` call → `stubResponse({ body })` from `/setup`, which builds the platform's own and answers every member; `Response` has to resolve to the global, so an Express handler's or a generated client's envelope of the same name is left alone |
3789
3801
  | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3790
3802
  | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and still prints → `installConsoleSpies()` from `vitest-auto-spy/console`, or `.mockImplementation(() => undefined)` |
3791
3803
  | `no-console-in-spec` | `error` | — | a spec that calls `console.x(…)` itself, or replaces a method with `console.x = …`, which nothing puts back — under `isolate: false` every later file inherits it |
@@ -3807,7 +3819,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
3807
3819
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
3808
3820
  are versioned together and stop being re-written in every project that installs the package.
3809
3821
 
3810
- **Four of the thirty-nine fix on their own, thirteen offer suggestions**, and the split is not about how hard
3822
+ **Four of the forty fix on their own, thirteen offer suggestions**, and the split is not about how hard
3811
3823
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
3812
3824
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
3813
3825
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -3978,7 +3990,7 @@ package's own — it needs its ESLint integration switched on.
3978
3990
  ### WebStorm and the other JetBrains IDEs
3979
3991
 
3980
3992
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
3981
- all run ESLint natively, so the thirty-nine rules appear inline, in the **Problems** tool window, and
3993
+ all run ESLint natively, so the forty rules appear inline, in the **Problems** tool window, and
3982
3994
  under **Code → Inspect Code** for the whole project.
3983
3995
 
3984
3996
  ```js
@@ -3995,7 +4007,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
3995
4007
  the fixes and suggestions live.
3996
4008
 
3997
4009
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
3998
- and then keep a second copy of thirty-nine rules, in Kotlin, in step with the TypeScript ones.
4010
+ and then keep a second copy of forty rules, in Kotlin, in step with the TypeScript ones.
3999
4011
 
4000
4012
  ### VS Code, Cursor, Windsurf, VSCodium
4001
4013
 
@@ -4103,10 +4115,12 @@ another, and the next one is in that same file.
4103
4115
  | `stable(fixture, opts?)` / `flushEffects()` _(Angular)_ | Zoneless waiting: flush effects, then await the fixture, with a 2 s budget that names the cause |
4104
4116
  | `settleResource(resource, opts?)` _(Angular)_ | Tick until an `httpResource()` / `resource()` / `rxResource()` leaves `loading` |
4105
4117
  | `provideHttpTesting(opts?)` / `expectRequest(matcher, opts?)` _(`/angular-http`)_ | `provideHttpClient()` + `provideHttpClientTesting()` in one spread; find the one matching request and `flush` / `error` it with the settling included |
4106
- | `expectNoRequest(matcher?, opts?)` / `verifyNoPendingRequests()` _(`/angular-http`)_ | Assert that nothing was requested / that the test left nothing unanswered |
4118
+ | `expectNoRequest(matcher?, opts?)` / `verifyNoPendingRequests(opts?)` _(`/angular-http`)_ | Assert that nothing was requested / that the test left nothing unanswered; `{ ignoreCancelled: true }` — also accepted by `provideHttpTesting({ verifyOnTeardown })` — forgives a request the code under test unsubscribed from |
4107
4119
  | `provideActivatedRoute(init?)` / `injectActivatedRoute(injector?)` _(`/angular-router`)_ | An `ActivatedRoute` provider whose streams, `paramMap`s and snapshot share one record, and the handle whose setters move them together |
4108
4120
  | `createActivatedRoute(init?)` _(`/angular-router`)_ | The same double without a `TestBed` — `.route` plus the setters |
4109
4121
  | `provideRouterDouble(init?)` / `injectRouterDouble(injector?)` / `createRouterDouble(init?)` _(`/angular-router`)_ | A `Router` derived from one URL — `navigate` spies, `setUrl`, `emitNavigation`, `setCurrentNavigation`, and the router's own URL serialization |
4122
+ | `collectRouterEvents(events)` _(`/angular-router`)_ | The double's router events as one comparable value — `expect([[NavigationStart, '/checkout'], …])`, a class-and-URL pair per event |
4123
+ | `provideLocationDouble()` / `injectLocationDouble(injector?)` / `createLocationDouble()` _(`/angular-router`)_ | Angular's own `SpyLocation` and `MockLocationStrategy` in one call — a real history, the `urlChanges` journal, `simulateUrlPop()` for the popstate no method call can cause |
4110
4124
  | `createForm(model, schema?, options?)` / `registerFormMatchers()` _(`/signal-forms`)_ | Angular's own signal `form()`, built in the `TestBed`'s injection context, and the `toHaveFieldErrors` matcher over its `errors()` |
4111
4125
  | `registerSignalMatchers()` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)` |
4112
4126
  | `trackRecomputations(signal)` / `trackEffectRuns(effectRef)` _(Angular)_ | Count what the reactive graph re-ran — `{ count, stop() }`, undone by `restoreMockedProps()` |
@@ -4117,6 +4131,7 @@ another, and the next one is in that same file.
4117
4131
  | `stubMediaElement(opts?)` | A `<video>` / `<audio>` that plays, reports a duration and fires the media events |
4118
4132
  | `assertMocked(namespace, opts?)` / `moduleNamespace(exports, opts?)` | Prove a `vi.mock()` applied; give its factory the shape an interop probe recognises, or spy the real module through (`passthrough`) |
4119
4133
  | `flushEventLoopUntil(isDone, opts?)` | Real event-loop turns until a condition holds, with a budget instead of a hang |
4134
+ | `createLog<Step>()` | The order of calls across collaborators as one comparable value — `add`, `fn(value)`, `items`, `result()`; the step union makes an undeclared step a compile error |
4120
4135
  | `diffByField(actual, expected)` | Which field of an array of records moved, and in how many elements |
4121
4136
  | `guardGlobalPatches(reaction)` / `installPerTest(install)` _(`/setup`)_ | Name the test that sealed a global property; re-install a stub before every test |
4122
4137
  | `guardStrayConsole(reaction)` / `withoutStrayTimerTracking(work)` _(`/setup`)_ | Fail a test on console output nothing absorbed; keep setup work's timers out of the stray-timer count |
@@ -156,7 +156,7 @@ declare function reportSpecTiming(timing: SpecTiming): void;
156
156
  *
157
157
  * ```ts
158
158
  * // vitest.setup.ts
159
- * import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular';
159
+ * import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
160
160
  *
161
161
  * if (process.env['SPEC_TIMING']) {
162
162
  * enableTestBedDiagnostics();
@@ -1,14 +1,14 @@
1
1
  import { instrumentTestBed, onTestingModuleConfigured, onComponentCreated, beforeTestBedReset, verifyOnTeardown } from './chunk-KOR5OK4H.js';
2
2
  export { disableTestBedDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, reportSpecTiming } from './chunk-KOR5OK4H.js';
3
- import { failOnUnspiedProvider } from './chunk-3EV45V6W.js';
4
- import { assertAngularInternals } from './chunk-NS3Y6AQB.js';
5
- import './chunk-GWEQZHMN.js';
6
- import './chunk-I6JZIWIZ.js';
7
- import './chunk-SA2QIER3.js';
3
+ import { failOnUnspiedProvider } from './chunk-45W5LBPY.js';
4
+ import { assertAngularInternals } from './chunk-WYYKUPXW.js';
5
+ import './chunk-MBSPAUUD.js';
6
+ import './chunk-DQ3SPSCH.js';
7
+ import './chunk-MG7LKE4R.js';
8
8
  import './chunk-PC3FAZT3.js';
9
- import './chunk-4SJY7ZHM.js';
10
- import { isAutoSpyLike } from './chunk-R3DF5ZEO.js';
11
- import { DOCS_LINKS } from './chunk-JJVTDL5R.js';
9
+ import './chunk-LZVIH54N.js';
10
+ import { isAutoSpyLike } from './chunk-BBPYNKGB.js';
11
+ import { DOCS_LINKS } from './chunk-QO4BPR3V.js';
12
12
  import { withDocs } from './chunk-TZ7DRSRD.js';
13
13
  import { getTestBed } from '@angular/core/testing';
14
14
  import { beforeEach, afterEach } from 'vitest';
@@ -19,7 +19,9 @@ import 'vitest';
19
19
  * is still typed against the token the spec passed and the result against the `close()` of the class
20
20
  * it passed.
21
21
  *
22
- * rxjs is imported here and nowhere else under `vitest-auto-spy/angular`, deliberately: a component
22
+ * rxjs is imported here and, bar `/angular-router`, nowhere else in the Angular entry family — and
23
+ * since the split moved this module to `vitest-auto-spy/angular/doubles` it no longer reaches
24
+ * `vitest-auto-spy/angular` at all. Deliberately, in either place: a component
23
25
  * pipes `afterClosed()`, so the stream has to be a real `Observable` rather than something
24
26
  * subscribable. `@angular/core` has rxjs as a peer dependency of its own, so a suite that can import
25
27
  * this entry already has it.
@@ -1,12 +1,12 @@
1
- import { useVitestAdapter } from './chunk-BN2QV45R.js';
2
- import './chunk-2KBFEQOV.js';
1
+ import { useVitestAdapter } from './chunk-V6TIK7XU.js';
2
+ import './chunk-MUZZBRUP.js';
3
3
  import './chunk-YHDBRBGU.js';
4
- import './chunk-ITOFGQTX.js';
5
- import './chunk-SA2QIER3.js';
4
+ import './chunk-CPLUHRHD.js';
5
+ import './chunk-MG7LKE4R.js';
6
6
  import './chunk-PC3FAZT3.js';
7
- import { createFunctionSpy } from './chunk-4SJY7ZHM.js';
8
- import './chunk-R3DF5ZEO.js';
9
- import { DOCS_LINKS } from './chunk-JJVTDL5R.js';
7
+ import { createFunctionSpy } from './chunk-LZVIH54N.js';
8
+ import './chunk-BBPYNKGB.js';
9
+ import { DOCS_LINKS } from './chunk-QO4BPR3V.js';
10
10
  import { withDocs } from './chunk-TZ7DRSRD.js';
11
11
  import { TestBed } from '@angular/core/testing';
12
12
  import { ReplaySubject } from 'rxjs';