vitest-auto-spy 5.1.0 → 5.3.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 (56) hide show
  1. package/AGENTS.md +91 -7
  2. package/README.md +49 -13
  3. package/dist/angular.d.ts +28 -6
  4. package/dist/angular.js +201 -15
  5. package/dist/bun-angular.d.ts +5 -5
  6. package/dist/bun-angular.js +7 -8
  7. package/dist/bun.d.ts +81 -5
  8. package/dist/bun.js +5 -6
  9. package/dist/{chunk-Q74KQJWG.js → chunk-5TUDM4DS.js} +39 -1
  10. package/dist/chunk-ED4EVQKA.js +74 -0
  11. package/dist/{chunk-CWHCYCCQ.js → chunk-GOG2CJOY.js} +1 -1
  12. package/dist/{chunk-VX5DDHAS.js → chunk-J33KX6XT.js} +48 -3
  13. package/dist/{chunk-Y4YG7UOK.js → chunk-JJ2JA2E3.js} +1 -1
  14. package/dist/{chunk-CUVQUISO.js → chunk-KE3LEHPW.js} +1 -1
  15. package/dist/{chunk-5WEFPNUG.js → chunk-NHSUUN3D.js} +1 -1
  16. package/dist/{chunk-YZOB2KRF.js → chunk-URZIJ5CL.js} +3 -3
  17. package/dist/{chunk-ZLF2YBXV.js → chunk-XAS66AWX.js} +50 -2
  18. package/dist/cli.js +7 -1
  19. package/dist/console.d.ts +1 -1
  20. package/dist/console.js +3 -3
  21. package/dist/dom-stubs.d.ts +1 -1
  22. package/dist/dom-stubs.js +54 -6
  23. package/dist/eslint-plugin.cjs +320 -73
  24. package/dist/eslint-plugin.d.cts +52 -0
  25. package/dist/eslint-plugin.d.ts +52 -0
  26. package/dist/{expect-emission-BfCKHEzi.d.ts → expect-emission-DYtyyKM5.d.ts} +1 -1
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +143 -8
  29. package/dist/jasmine-compat.d.ts +1 -1
  30. package/dist/jasmine.d.ts +2 -2
  31. package/dist/jasmine.js +5 -5
  32. package/dist/nestjs.d.ts +2 -2
  33. package/dist/nestjs.js +6 -6
  34. package/dist/node.cjs +95 -2
  35. package/dist/node.d.cts +3 -0
  36. package/dist/node.d.ts +4 -4
  37. package/dist/node.js +5 -5
  38. package/dist/{prop-mock-DkSaUWiQ.d.ts → prop-mock-DmM-rPcG.d.ts} +44 -2
  39. package/dist/react.d.ts +4 -4
  40. package/dist/react.js +6 -6
  41. package/dist/rstest.d.ts +4 -4
  42. package/dist/rstest.js +16 -19
  43. package/dist/rxjs.d.ts +2 -2
  44. package/dist/setup.d.ts +13 -2
  45. package/dist/setup.js +7 -5
  46. package/dist/svelte.d.ts +4 -4
  47. package/dist/svelte.js +6 -6
  48. package/dist/{track-injections-CiYfR36E.d.ts → track-injections-D4TuS8yk.d.ts} +1 -1
  49. package/dist/{types-CroX5c53.d.ts → types-DTi4dIeN.d.ts} +31 -9
  50. package/dist/vue.d.ts +5 -5
  51. package/dist/vue.js +7 -7
  52. package/dist/{zoneless-C9ruGqto.d.ts → zoneless-BerZbxtu.d.ts} +16 -4
  53. package/package.json +3 -2
  54. package/skills/vitest-auto-spy/SKILL.md +34 -4
  55. package/dist/chunk-NQXXUJPS.js +0 -39
  56. package/dist/chunk-RQGMLVRO.js +0 -51
package/AGENTS.md CHANGED
@@ -634,6 +634,31 @@ Also true, and worth not re-deriving:
634
634
  mapping = createSpyFromClass(MgwMappingService); // no second type argument here
635
635
  ```
636
636
 
637
+ **The symptom that leads here says nothing about overloads**, which is why the option is hard to
638
+ find from the error. A stub of the real response shape is rejected on the spec's own line —
639
+ `TS2345: Argument of type 'Page' is not assignable to parameter of type 'HttpEvent<Page>'` — on
640
+ `nextWith(body)`, `resolveWith(body)`, `calledWith(…).returnValue(body)` or
641
+ `mockReturnValue(of(body))` against a generated `observe` client. Neither the double nor the stub
642
+ is wrong; both are being checked against the signature nobody calls. Reach for `overload`, never
643
+ for `@ts-expect-error`: one migration wrote sixty of those across twenty-five files before anyone
644
+ found this option, and each one stops checking the response shape that line exists to describe.
645
+
646
+ **Name the method rather than the whole double.** `'first'` on the type moves *every* overloaded
647
+ member, and on a wide type that breaks the ones nobody was fixing — `Spy<Response, { overload:
648
+ 'first' }>` for one method collected five `TS2769`s on `download`. `overload` also takes a map:
649
+
650
+ ```ts
651
+ let perf: Spy<Performance, { overload: { getEntriesByType: 'first' } }>;
652
+ ```
653
+
654
+ A name the type does not have never matches, so a rename leaves a dead entry rather than a red
655
+ build. The default stays `'last'` because "the useful signature" is not decidable from the type: on
656
+ a generated `observe` client it is the first, on a four-overload `api-mgw` client the last, and both
657
+ live in one suite. And **overload order is not always the author's** — `declare global` in a
658
+ third-party package appends to a global interface (`web-vitals` does exactly this to
659
+ `Performance.getEntriesByType`), so which signature is last depends on which packages are in the
660
+ program and can move on a dependency bump.
661
+
637
662
  **A getter that returns a `Signal<T>` goes in `instanceMethodsToSpyOn`**, not in `gettersToSpyOn`.
638
663
  `get isKidMode(): Signal<boolean> { return this._isKidMode.asReadonly(); }` is read as a property
639
664
  and called as a function, and the accessor route makes you write
@@ -643,6 +668,29 @@ class prototype, so nothing is being shadowed), and
643
668
  `service.isKidMode.mockReturnValue(false)` reads like every other member. `mockSignalProp` is the
644
669
  other answer when the value has to change during the test.
645
670
 
671
+ ### The composition belongs to the class — `registerAutoSpyDefaults`
672
+
673
+ A class is doubled the same way in every file that doubles it, so register that once instead of
674
+ repeating it:
675
+
676
+ ```ts
677
+ // vitest.setup.ts, once
678
+ registerAutoSpyDefaults(Router, { observablePropsToSpyOn: ['events'], gettersToSpyOn: ['url'] });
679
+
680
+ // every spec, from then on
681
+ provideAutoSpy(Router);
682
+ provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] }); // ADDS to the registration
683
+ ```
684
+
685
+ The call site is **merged** into the registration, not substituted for it: lists unioned
686
+ (registration first, no repeats), `returns` / `overrides` merged key by key with the call site
687
+ winning, scalars decided by the call site when it names the key. The bare-array form counts as
688
+ `{ methodsToSpyOn: [...] }`. Registration is by class identity, so a subclass inherits nothing —
689
+ walking the prototype chain would let one registration change doubles in files nobody was looking at.
690
+ A second registration for the same class replaces the first, because two of them in one suite is the
691
+ drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
692
+ `clearAutoSpyDefaults()` the lot.
693
+
646
694
  ### `strict` — a method nobody configured throws instead of answering `undefined`
647
695
 
648
696
  ```ts
@@ -752,6 +800,23 @@ const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); //
752
800
  const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
753
801
  ```
754
802
 
803
+ **A declared default now reaches the double, and used not to.** `class RemoteConfigService<T =
804
+ RemoteConfigDefaults>` handed to `createSpyFromClass` / `injectSpy` inferred `T` as `unknown`, so
805
+ every member typed against it read as `unknown` on a class that had said exactly what it should be.
806
+ Two shapes caused it, both fixed: the `& { [key: string]: any }` intersection `ClassType<T>` used to
807
+ carry (an index signature makes inference drop the default) and the union in `injectSpy`'s token
808
+ parameter (a union does too — a class now matches a bare construct-signature overload first).
809
+ Spelling the argument out still works and is still the answer when the class has **no** default.
810
+
811
+ **A generic *method* is a different thing, and it does collapse.** `show<T, U>(component: Type<T>,
812
+ data: U)` on a modal or factory service reaches `Spy<T>` through `Parameters` / `ReturnType`, which
813
+ instantiate the method's own type parameters — so the double types as `show(component: Type<unknown>,
814
+ data: unknown)`. No mapped type in TypeScript can preserve a generic signature, so this is a limit
815
+ rather than a defect, and it is usually harmless: `unknown` accepts every argument and every seeded
816
+ return, so `spy.show.calledWith(MyModal, data).mockReturnValue(ref)` compiles. It bites only where
817
+ the instantiated type appears in a **contravariant** position; there, name the shape on a declaration
818
+ of your own and assign the factory's result to it.
819
+
755
820
  ---
756
821
 
757
822
  ## 7. Resetting
@@ -1016,6 +1081,12 @@ and a template error about a null profile appeared in a different `describe`. Th
1016
1081
  properties back and warns with the count and the cause. `countMockedProps()` is exported if you
1017
1082
  would rather assert it: `afterEach(() => expect(countMockedProps()).toBe(0))`.
1018
1083
 
1084
+ **A `mock*Prop` patch written in a `describe` body or a `beforeAll` is reported**, because the sweep
1085
+ undoes it after the test it was applied during and nothing puts it back: the first test passes and
1086
+ every one after it reads the real member. `propsOutsideHooks` grades that — `'warn'` (default),
1087
+ `'throw'`, `'off'` — and `reportPropsOutsideHooks(reaction)` sets the same dial for a suite that
1088
+ wires its own hooks. Move the call into `beforeEach`.
1089
+
1019
1090
  **The one that only bites at scale:** with `isolate: false`, a `setTimeout` or
1020
1091
  `requestAnimationFrame` a component schedules and never clears keeps running after its file is done,
1021
1092
  and fires while the **next** file is mid-test. It is reported against that innocent file, as
@@ -1900,11 +1971,13 @@ enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders,
1900
1971
  | `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
1901
1972
  | `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
1902
1973
  | `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one |
1974
+ | `shadowedProviders` | a double on the testing module loses to the component's own `providers` |
1903
1975
 
1904
1976
  Every member defaults to `true`, takes `false` to opt out, and a second call **replaces** the
1905
1977
  selection (safe from anywhere, including inside a test). `disableAngularDiagnostics()` turns the
1906
- group off and leaves the TestBed timing instrumentation alone. `assertNoPendingRequests()` is the
1907
- HTTP check on its own, for use mid-test — reading takes the requests, so the group will not
1978
+ group off and leaves the TestBed timing instrumentation alone. `assertNoShadowedProviders(Component, fixture)`
1979
+ and `assertNoPendingRequests()` are the same checks on their own, for a spec that renders through a
1980
+ helper of its own or wants the HTTP one mid-test — reading takes the requests, so the group will not
1908
1981
  re-report what you inspected.
1909
1982
 
1910
1983
  Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
@@ -2407,18 +2480,29 @@ blanket downgrade so those keep their severity; do not copy the two names into a
2407
2480
  | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
2408
2481
  | `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 |
2409
2482
  | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
2410
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2483
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
2484
+ | `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 |
2485
+ | `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 |
2411
2486
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2412
2487
  | `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 |
2413
- | `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2488
+ | `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 |
2489
+ | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
2414
2490
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2415
2491
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2416
2492
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
2417
2493
  | `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 |
2418
2494
 
2419
- Nineteen rules, **every one an `error` since 4.0.0**; three fix on their own, seven offer
2420
- suggestions. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
2421
- consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. Three of
2495
+ Twenty-three rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`**; three fix on
2496
+ their own, eight offer suggestions. Twenty-two are syntactic; `no-private-member-access` is the one
2497
+ that reads types, and it reports nothing at all without `parserOptions.project` / `projectService`
2498
+ rather than guessing. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
2499
+ consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The one
2500
+ `warn` left is not a judgement about how much that finding matters but about what kind of finding it
2501
+ is: every other rule names something wrong or dead, while `prefer-render-shallow` names a file that
2502
+ could render more cheaply, and `renderShallow` is a migration a suite either takes or does not. At
2503
+ `error` the plugin would gate that migration — 491 findings across 398 of one consumer's 1759 spec
2504
+ files — so `recommended` would exist to be overridden. Set it to `'error'` once the project has decided
2505
+ to make the move. Three of
2422
2506
  them can report on a _correct_ project, and only one has an option:
2423
2507
  `jasmine-namespace-without-entry` takes `['error', { setupModules: ['./test-setup'] }]`, naming the
2424
2508
  file where `enableJasmineCompat()` is called; `prefer-native-spy-api` goes `'off'` for as long as a
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-15.1%20kB-brightgreen)](#install)
26
+ [![minzipped size](https://img.shields.io/badge/minzip-15.8%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)
@@ -76,7 +76,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
76
76
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
77
77
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
78
78
  - 🚚 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
79
- - 📏 Lint rules and one-line test-run hygiene — twenty rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, eight suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
79
+ - 📏 Lint rules and one-line test-run hygiene — twenty-three rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, eight suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
80
80
  - 🩺 [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
81
81
  - 🔎 [`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
82
82
  - ⏱️ [`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
@@ -1662,6 +1662,27 @@ createSpyFromClass(MyService, {
1662
1662
  });
1663
1663
  ```
1664
1664
 
1665
+ ### The composition belongs to the class — `registerAutoSpyDefaults`
1666
+
1667
+ A spy's composition is a fact about the class rather than about the spec: `Router` needs `events`
1668
+ spied as an Observable property and `url` as a getter wherever it is doubled. Register it once and
1669
+ every call site starts from it.
1670
+
1671
+ ```ts
1672
+ // vitest-setup.ts, once
1673
+ registerAutoSpyDefaults(Router, { observablePropsToSpyOn: ['events'], gettersToSpyOn: ['url'] });
1674
+
1675
+ // every spec, from then on
1676
+ provideAutoSpy(Router);
1677
+ provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] }); // adds, does not replace
1678
+ ```
1679
+
1680
+ The call site is **merged** into the registration rather than replacing it: lists unioned,
1681
+ `returns` / `overrides` merged key by key, scalars decided by the call site when it names the key.
1682
+ Registration is by class identity — a subclass inherits nothing — and a second registration for one
1683
+ class replaces the first, because two of them in one suite is the drift this removes.
1684
+ `clearAutoSpyDefaults(Class)` drops one, `clearAutoSpyDefaults()` the lot.
1685
+
1665
1686
  ### Spying instance-assigned callables (`signal()`, arrow props, `signalStore()`)
1666
1687
 
1667
1688
  `createSpyFromClass` discovers methods by walking the **prototype**, so a callable assigned on the
@@ -2834,6 +2855,7 @@ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom al
2834
2855
  | --------------------- | --------- | ------------------------------------------------------------------------------------- |
2835
2856
  | `duplicateCopies` | `'throw'` | `'warn'` to report without failing, `'off'` to skip the check |
2836
2857
  | `restoreProps` | `true` | `restoreMockedProps()` in a global `afterEach` |
2858
+ | `propsOutsideHooks` | `'warn'` | Name a `mock*Prop` patch made in a `describe` body or `beforeAll` — it survives one test; `'throw'`, `'off'`. `reportPropsOutsideHooks(reaction)` sets the same dial without the setup helper |
2837
2859
  | `restoreMocks` | `false` | `vi.restoreAllMocks()` in a global `afterEach` — turn on for `isolate: false` |
2838
2860
  | `strayTimers` | `false` | Cancel timeouts, intervals and frames that outlive their file |
2839
2861
  | `onStrayTimers` | — | Takes the per-file count the sweep cancelled — see the note on `--detect-async-leaks` |
@@ -3004,11 +3026,11 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
3004
3026
  export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
3005
3027
  ```
3006
3028
 
3007
- **Every rule is an `error` since 4.0.0.** The config used to grade them `error` / `warn` / `off`,
3008
- which chose for you how much each finding mattered; a `warn` in a repository that does not read lint
3009
- output is `off` with extra noise, and which findings block a merge is one line of config either way.
3010
- To turn one down, spread the rule map as well — a bare `rules` key beside the spread config replaces
3011
- it rather than merging, silently:
3029
+ **Every rule is an `error` since 4.0.0, bar one.** The config used to grade them `error` / `warn` /
3030
+ `off`, which chose for you how much each finding mattered; a `warn` in a repository that does not read
3031
+ lint output is `off` with extra noise, and which findings block a merge is one line of config either
3032
+ way. To turn one down, spread the rule map as well — a bare `rules` key beside the spread config
3033
+ replaces it rather than merging, silently:
3012
3034
 
3013
3035
  ```js
3014
3036
  export default [
@@ -3020,6 +3042,16 @@ export default [
3020
3042
  ];
3021
3043
  ```
3022
3044
 
3045
+ The one exception is `prefer-render-shallow`, which ships as a **`warn`**, and the reason is the kind
3046
+ of thing it says rather than how much it matters. Every other rule here names something wrong or dead
3047
+ — a double that drifts from its class, an assertion that never runs, a provider the container already
3048
+ dropped, a schema guarding nothing. That one names a file that could render more cheaply, which is a
3049
+ choice a suite makes and not a defect in the file: moving onto `renderShallow` is a migration a
3050
+ project either takes or does not, and at `error` the plugin would be gating it. On the 1759-file suite
3051
+ it was measured against the rule reports 491 times across 398 files — a first run every consumer would
3052
+ have to answer with a project-wide `warn` of its own. A project that has decided to make the move sets
3053
+ it to `'error'` in the same one line that turns the others down.
3054
+
3023
3055
  A second config, `autoSpy.configs.typeErrors`, holds the subset whose findings are compile errors
3024
3056
  (`prefer-as-spy` is `TS2352`, `no-mocked-for-spy` is `TS2322`). Spread it **after** a blanket
3025
3057
  downgrade so those keep their severity — the recipe is a spread rather than rule names copied into
@@ -3048,10 +3080,13 @@ export can never be.
3048
3080
  | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3049
3081
  | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement of its own — a stub nobody continued, asserting nothing |
3050
3082
  | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
3051
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
3083
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3084
+ | `no-private-member-access` | `error` | — | `instance['privateMember']`, `(instance as any).privateMember` and `vi.spyOn(Object.getPrototypeOf(x), 'm')` — the three spellings that get past `private`; **type-aware**, and silent without `parserOptions.project` |
3085
+ | `no-dead-schemas` | `error` | — | `schemas` on a testing module that declares nothing — a charm protecting nobody, and one that starts working the day `declarations` arrive |
3052
3086
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
3053
3087
  | `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 |
3054
- | `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
3088
+ | `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 |
3089
+ | `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
3055
3090
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
3056
3091
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
3057
3092
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
@@ -3061,7 +3096,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
3061
3096
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
3062
3097
  are versioned together and stop being re-written in every project that installs the package.
3063
3098
 
3064
- **Three of the twenty fix on their own, eight offer suggestions**, and the split is not about how hard
3099
+ **Three of the twenty-three fix on their own, eight offer suggestions**, and the split is not about how hard
3065
3100
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
3066
3101
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
3067
3102
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -3165,7 +3200,7 @@ package's own — it needs its ESLint integration switched on.
3165
3200
  ### WebStorm and the other JetBrains IDEs
3166
3201
 
3167
3202
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
3168
- all run ESLint natively, so the twenty rules appear inline, in the **Problems** tool window, and
3203
+ all run ESLint natively, so the twenty-three rules appear inline, in the **Problems** tool window, and
3169
3204
  under **Code → Inspect Code** for the whole project.
3170
3205
 
3171
3206
  ```js
@@ -3182,7 +3217,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
3182
3217
  the fixes and suggestions live.
3183
3218
 
3184
3219
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
3185
- and then keep a second copy of twenty rules, in Kotlin, in step with the TypeScript ones.
3220
+ and then keep a second copy of twenty-three rules, in Kotlin, in step with the TypeScript ones.
3186
3221
 
3187
3222
  ### VS Code, Cursor, Windsurf, VSCodium
3188
3223
 
@@ -3273,9 +3308,10 @@ another, and the next one is in that same file.
3273
3308
  | `mockSystemTime` / `withSystemTime` / `mockNow` / `useCountingClock` _(`/setup`)_ | Clock control that survives fake timers being re-installed per test |
3274
3309
  | `registerFocusMatchers()` _(`/setup`)_ | Adds `expect(el).toHaveFocus()` |
3275
3310
  | `overrideAutoSpy` / `overrideComponentProvider` / `assertNgModuleScopes` / `assertComponentDefIntact` _(Angular)_ | Override a component-level provider — and verify on the next fixture that the override applied; diagnose an NgModule with an empty runtime scope |
3276
- | `enableAngularDiagnostics(opts?)` / `disableAngularDiagnostics()` / `assertNoPendingRequests()` _(Angular)_ | Dead NgModule imports, dead `schemas`, an unspied provider and unflushed HTTP requests, as failures |
3311
+ | `enableAngularDiagnostics(opts?)` / `disableAngularDiagnostics()` / `assertNoPendingRequests()` / `assertNoShadowedProviders()` _(Angular)_ | `ngModuleScopes`, `deadSchemas`, `unspiedProviders`, `shadowedProviders` and `pendingRequests`: a dead NgModule import, `schemas` that can never apply, an unspied provider, a double the component's own `providers` shadow, and unflushed HTTP requests — each as a failure |
3277
3312
  | `trackInjections(tokens, opts?)` _(Angular, NestJS)_ | Providers that record which collaborators DI constructed, with an auto-spy behind each token |
3278
3313
  | `setupAutoSpy(opts?)` _(`/setup`)_ | Property restore + duplicate-copy detection + mock-registry hygiene, in one call |
3314
+ | `registerAutoSpyDefaults(Class, config)` / `clearAutoSpyDefaults(Class?)` | A class's spy composition registered once, merged into every `createSpyFromClass(X)` / `provideAutoSpy(X)` from then on |
3279
3315
  | `setupFakeTimers(config?)` / `advanceTimers(ms?)` _(`/setup`)_ | Paired fake-timer install/restore, and an advance that also settles queued microtasks |
3280
3316
  | `describeDuplicateCopies()` / `getPackageCopies()` | The duplicate-install report, and the copies behind it |
3281
3317
  | `renderShallow(Component, opts?)` _(Angular)_ | `TestBed` component, minus its children and (by default) its template |
package/dist/angular.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-C9ruGqto.js';
1
+ export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-BerZbxtu.js';
2
2
  import { InjectionToken, Provider, Type, Signal, WritableSignal } from '@angular/core';
3
3
  import { TestAPI } from 'vitest';
4
- import { C as ClassType, S as Spy, k as ClassSpyConfiguration, o as OnlyMethodKeysOf, g as AddSpyMethodsByReturnTypes } from './types-CroX5c53.js';
5
- export { I as InjectionLog, T as TrackInjectionsOptions, a as TrackedProvider, t as trackInjections } from './track-injections-CiYfR36E.js';
6
- export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-DkSaUWiQ.js';
7
- export { C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-BfCKHEzi.js';
4
+ import { C as ClassType, S as Spy, k as ClassSpyConfiguration, o as OnlyMethodKeysOf, g as AddSpyMethodsByReturnTypes } from './types-DTi4dIeN.js';
5
+ export { I as InjectionLog, T as TrackInjectionsOptions, a as TrackedProvider, t as trackInjections } from './track-injections-D4TuS8yk.js';
6
+ export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, e as restoreMockedProps } from './prop-mock-DmM-rPcG.js';
7
+ export { C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-DYtyyKM5.js';
8
8
  import '@angular/core/testing';
9
9
 
10
10
  /**
@@ -245,6 +245,11 @@ interface AngularDiagnosticsOptions {
245
245
  unspiedProviders?: boolean;
246
246
  /** Fail a test that ends with unflushed `HttpTestingController` requests. */
247
247
  pendingRequests?: boolean;
248
+ /**
249
+ * Fail when a double registered on the testing module loses to the component's own `providers`,
250
+ * so the component under test is running against the real service.
251
+ */
252
+ shadowedProviders?: boolean;
248
253
  }
249
254
  /**
250
255
  * Fail when the `HttpTestingController` this test configured is still holding requests.
@@ -263,6 +268,23 @@ interface AngularDiagnosticsOptions {
263
268
  * ```
264
269
  */
265
270
  declare function assertNoPendingRequests(): void;
271
+ /**
272
+ * Fail when a double this test registered on the testing module never reached `component`.
273
+ *
274
+ * `shadowedProviders` calls this on every fixture; it is exported for the same reason
275
+ * {@link assertNoPendingRequests} is — a spec that builds its component through a helper of its own
276
+ * can ask directly, and a check that can only be reached through one code path is a check that
277
+ * stops running the day that path changes.
278
+ *
279
+ * A no-op when the fixture never rendered `component`, and when everything it resolved is a double.
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * const fixture = renderThroughOurHelper(CartComponent);
284
+ * assertNoShadowedProviders(CartComponent, fixture); // the doubles really are the ones in play
285
+ * ```
286
+ */
287
+ declare function assertNoShadowedProviders(component: unknown, fixture: unknown): void;
266
288
  /**
267
289
  * Turn the group on. Every member defaults to `true`; pass `false` to leave one out.
268
290
  *
@@ -709,4 +731,4 @@ declare function reportSpecTiming(timing: SpecTiming): void;
709
731
  */
710
732
  declare function enableTestBedDiagnostics(options?: TestBedDiagnosticsOptions): void;
711
733
 
712
- export { type AngularDiagnosticsOptions, type AngularTestEnvMode, type AngularTestEnvOptions, type AutoSpyFixture, type AutoSpyOverride, type DirectiveHostOptions, type ExtendWithAutoSpiesOptions, type MockedResource, type ResourceDouble, type ResourceDoubleStatus, type ResourceLike, type SignalLike, type SpecTiming, type SpiedFixtures, type TestBedDiagnosticsOptions, assertComponentDefIntact, assertNgModuleScopes, assertNoPendingRequests, createDirectiveHost, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, extendWithAutoSpies, formatSpecTiming, getTestBedTiming, instrumentTestBed, mockResourceProp, mockSignalProp, overrideAutoSpy, overrideComponentProvider, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, reportSpecTiming, setupAngularTestEnv };
734
+ export { type AngularDiagnosticsOptions, type AngularTestEnvMode, type AngularTestEnvOptions, type AutoSpyFixture, type AutoSpyOverride, type DirectiveHostOptions, type ExtendWithAutoSpiesOptions, type MockedResource, type ResourceDouble, type ResourceDoubleStatus, type ResourceLike, type SignalLike, type SpecTiming, type SpiedFixtures, type TestBedDiagnosticsOptions, assertComponentDefIntact, assertNgModuleScopes, assertNoPendingRequests, assertNoShadowedProviders, createDirectiveHost, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, extendWithAutoSpies, formatSpecTiming, getTestBedTiming, instrumentTestBed, mockResourceProp, mockSignalProp, overrideAutoSpy, overrideComponentProvider, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, reportSpecTiming, setupAngularTestEnv };