vitest-auto-spy 5.47.0 → 5.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -534,7 +534,7 @@ In [`agent-docs/adapters.md`](./agent-docs/adapters.md). Read it for NestJS, Vue
534
534
 
535
535
  ## 16. ESLint plugin (flat config only)
536
536
 
537
- In [`agent-docs/eslint.md`](./agent-docs/eslint.md). Read it when configuring `vitest-auto-spy/eslint-plugin` or fixing one of its reports.
537
+ In [`agent-docs/eslint.md`](./agent-docs/eslint.md). Read it when configuring `vitest-auto-spy/eslint-plugin` or fixing one of its reports. For `vitest/require-hook`, spread `autoSpy.hookRegisteringHelpers` into `allowedFunctionCalls` rather than keeping the names by hand.
538
538
 
539
539
  <!-- agents-outline: agent-docs/eslint.md -->
540
540
  <!-- /agents-outline -->
package/CHANGELOG.md CHANGED
@@ -10,6 +10,53 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.49.0] - 2026-09-28
14
+
15
+ ### Added
16
+
17
+ - **`no-real-component-provider`** in `vitest-auto-spy/eslint-plugin`, `error` in `recommended`:
18
+ `fixture.debugElement.injector.get(X)` / `fixture.componentRef.injector.get(X)` for a token nothing
19
+ in the file replaced with a double. That read is how a spec reaches a component-level provider, and
20
+ when nothing swapped it the fixture hands back the production class — the component spec then
21
+ drives the real store through HTTP and repeats the store's own spec. The message names the repair,
22
+ `overrideComponentProvider(Component, X)`. Tokens from `@angular/*`, classes the file renders
23
+ (`createComponent`, `imports`, `declarations`, `hostDirectives`), reads wrapped in `asSpy(…)` and
24
+ files that call `createWithAutoSpies` are left alone; `{ ignoreTokens }` keeps an intended
25
+ integration test real. Only the fixture's own injector is read, never a child's. Measured on three
26
+ consumers, 1078 spec files: 5 reports, all of them real. The plugin now ships fifty-one rules.
27
+ `vitest-auto-spy/eslint-plugin` is +0.78 kB min+gzip (+1.8 %) for the rule module and its
28
+ messages; no module is added to the entry's graph.
29
+
30
+ ## [5.48.0] - 2026-09-28
31
+
32
+ ### Added
33
+
34
+ - **`hookRegisteringHelpers` on `vitest-auto-spy/eslint-plugin`** — the frozen list of the
35
+ library's public helpers that register `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when
36
+ called (`useConsoleSpies`, `setupAutoSpy`, `setupFakeTimers`, `useCountingClock`, `mockNow`, the
37
+ `guard*` family, …), for `vitest/require-hook`'s `allowedFunctionCalls`:
38
+ `allowedFunctionCalls: [...autoSpy.hookRegisteringHelpers]`. A bare `useConsoleSpies();` in a
39
+ `describe` body no longer needs a hand-kept list; a spec walks the call graph of every public entry
40
+ and fails when a hook-registering export is missing from it. It is a property of the plugin
41
+ object, so `require()` and `import` both reach it. `vitest-auto-spy/eslint-plugin` is +0.42 kB
42
+ min+gzip (+1.0 %) for the list and the two rule changes.
43
+ - **`prefer-to-have-signal-value`** also reports `expect(sig()).toBeNull()` and `.toBeUndefined()`
44
+ on a signal read, `.not` included, and fixes them to `toHaveSignalValue(null)` /
45
+ `toHaveSignalValue(undefined)`.
46
+
47
+ ### Fixed
48
+
49
+ - **`prefer-to-have-signal-value`** no longer rewrites an identity check. `expect(list.items()).toBe(items)`
50
+ asserts the signal holds that very array, and the fix turned it into `toHaveSignalValue(items)`,
51
+ which compares deeply and passes for any equal copy. A `toBe` is now reported only when the
52
+ expected value is a primitive literal or the signal's type is primitive (`any` does not count);
53
+ `toEqual` and `toStrictEqual` are unchanged.
54
+ - **Docs**: `vitest-auto-spy/console` imports `vitest` itself (the fallback adapter and the
55
+ `useConsoleSpies()` hooks), so on `node:test` and Rstest it loads only where the `vitest` package
56
+ resolves; the console page, the README and `agent-docs/adapters.md` now say so. The README API
57
+ table and `agent-docs/angular.md` list `registerSignalMatchers({ strict })`, and the types list of
58
+ `/angular/matchers` names `RegisterSignalMatchersOptions`.
59
+
13
60
  ## [5.47.0] - 2026-09-28
14
61
 
15
62
  ### Added
@@ -6046,7 +6093,9 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
6046
6093
  This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
6047
6094
  [full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
6048
6095
 
6049
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.47.0...HEAD
6096
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.49.0...HEAD
6097
+ [5.49.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.48.0...v5.49.0
6098
+ [5.48.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.47.0...v5.48.0
6050
6099
  [5.47.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.46.0...v5.47.0
6051
6100
  [5.46.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...v5.46.0
6052
6101
  [5.45.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.44.0...v5.45.0
package/README.md CHANGED
@@ -81,7 +81,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
81
81
  - 🧩 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
82
82
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`, and `outOfType()` for the fixture that is outside its type on purpose
83
83
  - 🚚 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
84
- - 📏 Lint rules and one-line test-run hygiene — fifty rules in `vitest-auto-spy/eslint-plugin` (eight `--fix`, nineteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade, and under `isolate: false` `strayListeners` / `restoreGlobals` for the listeners and raw global assignments a file leaves the next one, `restoreStorageSpies` (on by default) for the storage spy happy-dom will not let `mockRestore()` remove
84
+ - 📏 Lint rules and one-line test-run hygiene — fifty-one rules in `vitest-auto-spy/eslint-plugin` (eight `--fix`, nineteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade, and under `isolate: false` `strayListeners` / `restoreGlobals` for the listeners and raw global assignments a file leaves the next one, `restoreStorageSpies` (on by default) for the storage spy happy-dom will not let `mockRestore()` remove
85
85
  - 🩺 [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
86
86
  - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-init-and-ng-test) — 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
87
87
  - ⏱️ [`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
@@ -890,7 +890,9 @@ it('reports a failed load', () => {
890
890
  `useConsoleSpies()` registers `beforeEach(installConsoleSpies)` and `afterEach(restoreConsole)` in
891
891
  the enclosing `describe` (or the file) and returns the spies, which are the same objects in every
892
892
  test. It registers Vitest hooks, which Bun resolves to its own; on `node:test` and Rstest write that
893
- pair in the runner's hooks yourself. Do not
893
+ pair in the runner's hooks yourself — the entry imports `vitest` itself, so there it loads only where
894
+ the `vitest` package resolves. Under `vitest/require-hook`, spread `autoSpy.hookRegisteringHelpers`
895
+ from `vitest-auto-spy/eslint-plugin` into its `allowedFunctionCalls`. Do not
894
896
  rely on the **import** to install them: it does so once per worker, so under `isolate: false` the
895
897
  spies go on in whichever file imported them first and silence every later file of the worker — on a
896
898
  1759-file Angular consumer, 32 of the 39 files importing the entry relied on that, and three files
@@ -2953,7 +2955,8 @@ expect(component.draft).toHaveSignalValue({}, { strict: false });
2953
2955
  ```
2954
2956
 
2955
2957
  The `prefer-to-have-signal-value` lint rule rewrites the inline spellings — `expect(component.total()).toBe(3)`
2956
- — into the matcher with a `--fix`.
2958
+ — into the matcher with a `--fix`. A `toBe` against an object-typed signal checks identity, which the
2959
+ deep matcher cannot keep, so the rule leaves it alone.
2957
2960
 
2958
2961
  #### Where a spec spends its time
2959
2962
 
@@ -4016,8 +4019,9 @@ export can never be.
4016
4019
  | `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 |
4017
4020
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
4018
4021
  | `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 |
4022
+ | `no-real-component-provider` | `error` | — | `fixture.debugElement.injector.get(X)` for a component-level provider nothing in the file replaced → the real store under a component spec; `overrideComponentProvider(Cmp, X)` |
4019
4023
  | `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 |
4020
- | `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(signal()).toBe(…)` → `expect(signal).toHaveSignalValue(…)`; the matcher names the signal in the failure and refuses a non-signal. `toStrictEqual` keeps `{ strict: true }`. **Type-aware** — the callee must be callable and carry Angular's signal brand — and silent without `parserOptions.project`; safe to autofix since 5.46.0 fixed the `Set`/`Map` comparison |
4024
+ | `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(signal()).toBe(…)` → `expect(signal).toHaveSignalValue(…)`; the matcher names the signal in the failure and refuses a non-signal. `toStrictEqual` keeps `{ strict: true }`, `toBeNull()` / `toBeUndefined()` become `toHaveSignalValue(null)` / `(undefined)`. **Type-aware** — the callee must be callable and carry Angular's signal brand — and silent without `parserOptions.project`; safe to autofix since 5.46.0 fixed the `Set`/`Map` comparison. A `toBe` against an object-typed signal is an identity check and is left alone |
4021
4025
  | `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` |
4022
4026
  | `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
4023
4027
  | `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
@@ -4051,7 +4055,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
4051
4055
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
4052
4056
  are versioned together and stop being re-written in every project that installs the package.
4053
4057
 
4054
- **Eight of the fifty fix on their own, nineteen offer suggestions**, and the split is not about how hard
4058
+ **Eight of the fifty-one fix on their own, nineteen offer suggestions**, and the split is not about how hard
4055
4059
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
4056
4060
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
4057
4061
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -4128,6 +4132,16 @@ a spread, an unknown provider factory, `createWithAutoSpies`, `renderShallow` or
4128
4132
  `TestBed.overrideProvider` silences the file. A token handed a plain `useValue` is left to
4129
4133
  `prefer-provide-auto-spy` — two rules on one line only teach people to disable both.
4130
4134
 
4135
+ `no-real-component-provider` catches `fixture.debugElement.injector.get(X)` — or
4136
+ `fixture.componentRef.injector.get(X)` — for a token nothing in the file replaced with a double.
4137
+ `injectSpy` cannot reach a provider the component declares itself, so a spec that wants that store
4138
+ reads it through the fixture, and when nothing swapped it the fixture hands back the production
4139
+ class: the component spec drives the real store through HTTP and repeats the store's own spec. The
4140
+ message names the repair, [`overrideComponentProvider(Cmp, X)`](#a-provider-the-component-declares-for-itself). Tokens
4141
+ from `@angular/*`, classes the file renders, reads wrapped in `asSpy(…)` and files that call
4142
+ `createWithAutoSpies` are left alone, and `{ ignoreTokens }` keeps an intended integration test real.
4143
+ On three consumers, 1078 spec files, it reports 5, every one of them real.
4144
+
4131
4145
  `no-ts-expect-error-on-double` reports `@ts-expect-error` / `@ts-ignore` above a double's
4132
4146
  configuration whatever reason follows the directive. On one 1759-file suite it reports 34, every one
4133
4147
  with a reason: four on overloaded clients ([the recipe](#how-to-mock-an-overloaded-method)), twenty-six
@@ -4222,7 +4236,7 @@ package's own — it needs its ESLint integration switched on.
4222
4236
  ### WebStorm and the other JetBrains IDEs
4223
4237
 
4224
4238
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
4225
- all run ESLint natively, so the fifty rules appear inline, in the **Problems** tool window, and
4239
+ all run ESLint natively, so the fifty-one rules appear inline, in the **Problems** tool window, and
4226
4240
  under **Code → Inspect Code** for the whole project.
4227
4241
 
4228
4242
  ```js
@@ -4239,7 +4253,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
4239
4253
  the fixes and suggestions live.
4240
4254
 
4241
4255
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
4242
- and then keep a second copy of fifty rules, in Kotlin, in step with the TypeScript ones.
4256
+ and then keep a second copy of fifty-one rules, in Kotlin, in step with the TypeScript ones.
4243
4257
 
4244
4258
  ### VS Code, Cursor, Windsurf, VSCodium
4245
4259
 
@@ -4360,7 +4374,7 @@ another, and the next one is in that same file.
4360
4374
  | `collectRouterEvents(events)` _(`/angular-router`)_ | The double's router events as one comparable value — `expect([[NavigationStart, '/checkout'], …])`, a class-and-URL pair per event |
4361
4375
  | `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 |
4362
4376
  | `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()` |
4363
- | `registerSignalMatchers()` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)` |
4377
+ | `registerSignalMatchers(opts?)` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)`; `{ strict: true }` compares every assertion like `toStrictEqual` |
4364
4378
  | `trackRecomputations(signal)` / `trackEffectRuns(effectRef)` _(Angular)_ | Count what the reactive graph re-ran — `{ count, stop() }`, undone by `restoreMockedProps()` |
4365
4379
  | `provideWindowDouble(token, overrides?)` / `provideDocumentDouble(overrides?)` _(Angular)_ | A `window` / `document` merged over the real jsdom one; `createWindowDouble` / `createDocumentDouble` outside DI |
4366
4380
  | `provideMatDialogData(token, data)` / `provideMatDialogRef(Ref, init?)` / `injectMatDialogRef(Ref)` _(Angular)_ | The Material dialog trio — the token and the ref class are arguments; `createMatDialogRef` builds the ref outside DI |
@@ -39,7 +39,9 @@ expect(consoleInfoSpy).toHaveBeenCalledWith('done'); // the output is silenced,
39
39
  ```
40
40
 
41
41
  It registers Vitest hooks (Bun resolves them to its own); on `node:test` and Rstest write
42
- `installConsoleSpies()` in a `beforeEach` and `restoreConsole()` in an `afterEach` yourself.
42
+ `installConsoleSpies()` in a `beforeEach` and `restoreConsole()` in an `afterEach` yourself. The entry
43
+ imports `vitest` itself, so there it loads only where the `vitest` package resolves. Under
44
+ `vitest/require-hook`, spread `autoSpy.hookRegisteringHelpers` into `allowedFunctionCalls` (§16).
43
45
 
44
46
  The exported `consoleInfoSpy` & co. are the same objects; `restoreConsole()` keeps them and clears
45
47
  their calls. **Do not rely on the import to install them**: it does so once per worker, so under
@@ -1063,8 +1063,8 @@ service.products.set([edited]); // the component's own optimistic write — stat
1063
1063
  expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, and re-issues nothing
1064
1064
 
1065
1065
  // signal assertions
1066
- registerSignalMatchers(); // once, in the setup file — /angular/matchers
1067
- expect(component.total).toHaveSignalValue(3); // toEqual semantics; { strict: true } for toStrictEqual's
1066
+ registerSignalMatchers(); // once, in the setup file — /angular/matchers; ({ strict: true }) makes the whole suite strict
1067
+ expect(component.total).toHaveSignalValue(3); // toEqual semantics; { strict: true } for toStrictEqual's, { strict: false } opts out
1068
1068
 
1069
1069
  // resource assertions — value AND status, which is the whole point
1070
1070
  registerResourceMatchers(); // once, in the setup file — /angular/matchers
@@ -32,6 +32,25 @@ blanket downgrade so those keep their severity; do not copy the two names into a
32
32
  `autoSpy.configs.strict` is `recommended` with every rule at `error`, instead of mapping
33
33
  `Object.keys(autoSpy.rules)` by hand.
34
34
 
35
+ `@vitest/eslint-plugin`'s `vitest/require-hook` reports a bare `useConsoleSpies();` or
36
+ `setupFakeTimers();` in a `describe` body, since it cannot know the call registers hooks.
37
+ `autoSpy.hookRegisteringHelpers` is the library's list of such helpers — every public function that
38
+ calls `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when it runs, kept in step with the
39
+ source by a spec — so spread it instead of maintaining the names by hand. Calls that register no
40
+ hook (`registerSignalMatchers()`, `trackStrayTimers()`) are not in it; add them yourself where a
41
+ linted file calls them at the top level.
42
+
43
+ ```js
44
+ export default [
45
+ {
46
+ files: ['**/*.spec.ts'],
47
+ rules: {
48
+ 'vitest/require-hook': ['error', { allowedFunctionCalls: [...autoSpy.hookRegisteringHelpers] }],
49
+ },
50
+ },
51
+ ];
52
+ ```
53
+
35
54
  | Rule | Level | Fix | Flags |
36
55
  | --- | --- | --- | --- |
37
56
  | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
@@ -54,9 +73,10 @@ blanket downgrade so those keep their severity; do not copy the two names into a
54
73
  | `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 |
55
74
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
56
75
  | `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 |
76
+ | `no-real-component-provider` | `error` | — | `fixture.debugElement.injector.get(X)` for a component-level provider nothing in the file replaced → the real store under a component spec; `overrideComponentProvider(Cmp, X)`; `{ ignoreTokens }` for an intended integration test |
57
77
  | `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 |
58
78
  | `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` |
59
- | `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(component.total()).toBe(3)` / `toEqual(…)` → `expect(component.total).toHaveSignalValue(3)`, `toStrictEqual` → `{ strict: true }`; the matcher names the signal in the failure and refuses a value that is not one. **Type-aware**: the signal is recognised by Angular's brand on its type, so methods, functions and getters are never reported, and without `parserOptions.project` it reports nothing |
79
+ | `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(component.total()).toBe(3)` / `toEqual(…)` → `expect(component.total).toHaveSignalValue(3)`, `toStrictEqual` → `{ strict: true }`, `toBeNull()` / `toBeUndefined()` → `toHaveSignalValue(null)` / `(undefined)`; the matcher names the signal in the failure and refuses a value that is not one. **Type-aware**: the signal is recognised by Angular's brand on its type, so methods, functions and getters are never reported, and without `parserOptions.project` it reports nothing. A `toBe` against an object-typed signal (`expect(list.items()).toBe(items)`) is an identity check the deep matcher would lose, so it is reported only when the expected value is a primitive literal or the signal's type is primitive |
60
80
  | `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
61
81
  | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
62
82
  | `no-hand-assigned-global` | `error` | `--fix` | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages, `stubWorker({ respond })` for `Worker`; the three observers stay with `prefer-observer-stub`; any value written into an imported object (`environment.production = true`) → `mockValueProp(environment, 'production', true)`, fixed in a test or `beforeEach` |
@@ -85,9 +105,9 @@ blanket downgrade so those keep their severity; do not copy the two names into a
85
105
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
86
106
  | `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 |
87
107
 
88
- Fifty rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
108
+ Fifty-one rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
89
109
  `no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
90
- `prefer-set-inputs`, `no-unasserted-argument`, `prefer-spy-on-own-method` and `prefer-to-have-signal-value`**; eight fix on their own, nineteen offer suggestions. Forty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
110
+ `prefer-set-inputs`, `no-unasserted-argument`, `prefer-spy-on-own-method` and `prefer-to-have-signal-value`**; eight fix on their own, nineteen offer suggestions. Forty-seven are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
91
111
  `no-unknown-use-value-key` and `prefer-to-have-signal-value` read types, and all four report nothing at all without `parserOptions.project` / `projectService`
92
112
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
93
113
  builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
package/dist/cli.js CHANGED
@@ -3865,7 +3865,7 @@ function checkForeignPragma(graph) {
3865
3865
  }
3866
3866
 
3867
3867
  // src/cli/checks/export-map.generated.ts
3868
- var EXPORT_MAP_VERSION = "5.46.0";
3868
+ var EXPORT_MAP_VERSION = "5.48.0";
3869
3869
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular/diagnostics vitest-auto-spy/angular/doubles vitest-auto-spy/angular/matchers vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin vitest-auto-spy/perf-reporter";
3870
3870
  var EXPORTED_BY = {
3871
3871
  AccessorImplementations: "0 1 2 3 4 12 20 21 22",
@@ -3,6 +3,25 @@
3
3
  var node_fs = require('node:fs');
4
4
  var node_path = require('node:path');
5
5
 
6
+ // src/lib/eslint/hook-registering-helpers.ts
7
+ var hookRegisteringHelpers = Object.freeze([
8
+ "enableAngularDiagnostics",
9
+ "enableTestBedDiagnostics",
10
+ "guardDocumentPollution",
11
+ "guardGlobalPatches",
12
+ "guardPrototypePollution",
13
+ "guardStrayConsole",
14
+ "installPerTest",
15
+ "mockNow",
16
+ "setupAngularTestEnv",
17
+ "setupAutoSpy",
18
+ "setupFakeTimers",
19
+ "trackMockRegistry",
20
+ "trackNodeMocks",
21
+ "useConsoleSpies",
22
+ "useCountingClock"
23
+ ]);
24
+
6
25
  // src/lib/eslint/rule-types.ts
7
26
  function isCast(node) {
8
27
  return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
@@ -4138,11 +4157,15 @@ var preferRenderShallow = defineRule({
4138
4157
 
4139
4158
  // src/lib/eslint/prefer-to-have-signal-value.ts
4140
4159
  var MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
4160
+ var VALUE_MATCHERS = /* @__PURE__ */ new Map([
4161
+ ["toBeNull", "null"],
4162
+ ["toBeUndefined", "undefined"]
4163
+ ]);
4141
4164
  var SIGNAL_BRAND = "\u0275SIGNAL";
4142
4165
  var SIGNAL_BRAND_PROPERTY = /^__@(?:SIGNAL|ɵSIGNAL|ɵWRITABLE_SIGNAL)@\d+$/;
4143
4166
  function signalAssertionOf(node) {
4144
4167
  const callee = node.callee;
4145
- if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property) || !MATCHERS.has(callee.property.name)) {
4168
+ if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property) || !(MATCHERS.has(callee.property.name) || VALUE_MATCHERS.has(callee.property.name))) {
4146
4169
  return void 0;
4147
4170
  }
4148
4171
  let expected = callee.object;
@@ -4164,9 +4187,37 @@ function readsSignal(context, node) {
4164
4187
  const signatures = type.getCallSignatures();
4165
4188
  return type.getProperties().some((property) => property.getName() === SIGNAL_BRAND || SIGNAL_BRAND_PROPERTY.test(property.getName())) && signatures.length > 0;
4166
4189
  }
4190
+ function isPrimitiveLiteral(node) {
4191
+ if (node === void 0) {
4192
+ return false;
4193
+ }
4194
+ switch (node.type) {
4195
+ case "Literal":
4196
+ return Reflect.get(node, "regex") === void 0;
4197
+ case "Identifier":
4198
+ return isIdentifier(node) && node.name === "undefined";
4199
+ case "UnaryExpression":
4200
+ return isPrimitiveLiteral(Reflect.get(node, "argument"));
4201
+ case "TemplateLiteral":
4202
+ return Reflect.get(node, "expressions")?.length === 0;
4203
+ default:
4204
+ return false;
4205
+ }
4206
+ }
4207
+ function primitiveValue({ checker, toTs }, received) {
4208
+ const value = checker.getTypeAtLocation(toTs.get(received));
4209
+ return checker.typeToString(value) !== "any" && isPrimitiveLike(checker, value);
4210
+ }
4211
+ function identityIsMoot(context, node, shape) {
4212
+ if (shape.matcher.name !== "toBe" || isPrimitiveLiteral(node.arguments.at(0))) {
4213
+ return true;
4214
+ }
4215
+ const services = checkerServices(context, PRIMITIVE_CHECKER_METHODS);
4216
+ return services !== void 0 && askChecker(() => primitiveValue(services, shape.received)) === true;
4217
+ }
4167
4218
  var preferToHaveSignalValue = defineRule({
4168
4219
  name: "prefer-to-have-signal-value",
4169
- description: "Assert a signal with toHaveSignalValue instead of reading it inline into toBe/toEqual",
4220
+ description: "Assert a signal with toHaveSignalValue instead of reading it inline into toBe/toEqual/toBeNull",
4170
4221
  messages: {
4171
4222
  preferToHaveSignalValue: "Assert the signal, not the value it happens to hold: `expect({{signal}}).toHaveSignalValue(\u2026)` keeps the signal's name in the failure output and refuses a value that is not a signal."
4172
4223
  },
@@ -4174,7 +4225,7 @@ var preferToHaveSignalValue = defineRule({
4174
4225
  create: (context) => ({
4175
4226
  CallExpression: (node) => {
4176
4227
  const shape = signalAssertionOf(node);
4177
- if (!shape || !readsSignal(context, shape.received.callee)) {
4228
+ if (!shape || !readsSignal(context, shape.received.callee) || !identityIsMoot(context, node, shape)) {
4178
4229
  return;
4179
4230
  }
4180
4231
  context.report({
@@ -4183,9 +4234,10 @@ var preferToHaveSignalValue = defineRule({
4183
4234
  data: { signal: excerpt(context, shape.received.callee, 40) },
4184
4235
  ...shape.received.typeArguments === void 0 && {
4185
4236
  fix: (fixer) => {
4237
+ const value = VALUE_MATCHERS.get(shape.matcher.name);
4186
4238
  const fixes = [
4187
4239
  fixer.replaceText(shape.received, context.sourceCode.getText(shape.received.callee)),
4188
- fixer.replaceText(shape.matcher, "toHaveSignalValue")
4240
+ value === void 0 ? fixer.replaceText(shape.matcher, "toHaveSignalValue") : fixer.replaceTextRange([shape.matcher.range[0], node.range[1]], `toHaveSignalValue(${value})`)
4189
4241
  ];
4190
4242
  const last = node.arguments.at(-1);
4191
4243
  if (shape.strict && last !== void 0) {
@@ -4577,6 +4629,120 @@ var preferProvideAutoSpy = defineRule({
4577
4629
  }
4578
4630
  });
4579
4631
 
4632
+ // src/lib/eslint/real-component-provider.ts
4633
+ var REPLACING_CALLS = /* @__PURE__ */ new Set([
4634
+ "createSpyFromClass",
4635
+ "overrideAutoSpy",
4636
+ "overrideComponent",
4637
+ "overrideComponentProvider",
4638
+ "overrideProvider",
4639
+ "provideAutoSpy"
4640
+ ]);
4641
+ var OPAQUE_HELPERS2 = /* @__PURE__ */ new Set(["createWithAutoSpies"]);
4642
+ var OWN_INJECTOR_HOLDERS = /* @__PURE__ */ new Set(["componentRef", "debugElement"]);
4643
+ var FRAMEWORK_SCOPE = "@angular/";
4644
+ var REAL_COMPONENT_PROVIDER_SCHEMA = [
4645
+ {
4646
+ type: "object",
4647
+ properties: { ignoreTokens: { type: "array", items: { type: "string" }, uniqueItems: true } },
4648
+ additionalProperties: false
4649
+ }
4650
+ ];
4651
+ function calleeName2(node) {
4652
+ return isIdentifier(node.callee) ? node.callee.name : memberName(node.callee);
4653
+ }
4654
+ var RENDERED_KEYS = /* @__PURE__ */ new Set(["declarations", "hostDirectives", "imports"]);
4655
+ function isRenderedList(node) {
4656
+ const name = propertyName(node.parent);
4657
+ return name !== void 0 && RENDERED_KEYS.has(name) && Reflect.get(node.parent, "value") === node;
4658
+ }
4659
+ function isReplacingArgument(node) {
4660
+ const call = node.parent;
4661
+ if (!isCallExpression(call) || !call.arguments.includes(node)) {
4662
+ return false;
4663
+ }
4664
+ const name = calleeName2(call);
4665
+ return name !== void 0 && REPLACING_CALLS.has(name);
4666
+ }
4667
+ function ownInjectorToken(node) {
4668
+ const [token] = node.arguments;
4669
+ if (!isMemberExpression(node.callee) || memberName(node.callee) !== "get" || !token || !isIdentifier(token)) {
4670
+ return void 0;
4671
+ }
4672
+ const injector = node.callee.object;
4673
+ if (!isMemberExpression(injector) || memberName(injector) !== "injector") {
4674
+ return void 0;
4675
+ }
4676
+ const holder = memberName(injector.object);
4677
+ return holder !== void 0 && OWN_INJECTOR_HOLDERS.has(holder) ? token : void 0;
4678
+ }
4679
+ function isClaimedSpy(node) {
4680
+ return isCallExpression(node.parent) && calleeName2(node.parent) === "asSpy";
4681
+ }
4682
+ function ignoredTokens(options) {
4683
+ const configured = Reflect.get(Object(options[0]), "ignoreTokens");
4684
+ return new Set(Array.isArray(configured) ? configured.filter((entry) => typeof entry === "string") : []);
4685
+ }
4686
+ function isFrameworkImport(node) {
4687
+ return node.type === "ImportDeclaration" && String(Reflect.get(Reflect.get(node, "source"), "value")).startsWith(FRAMEWORK_SCOPE);
4688
+ }
4689
+ function readCall2(node, scan) {
4690
+ const name = calleeName2(node);
4691
+ if (name !== void 0 && OPAQUE_HELPERS2.has(name)) {
4692
+ scan.opaque = true;
4693
+ }
4694
+ if (name === "createComponent") {
4695
+ node.arguments.filter(isIdentifier).forEach((argument) => scan.rendered.add(argument.name));
4696
+ }
4697
+ const token = ownInjectorToken(node);
4698
+ if (token && !isClaimedSpy(node)) {
4699
+ scan.reads.push(token);
4700
+ }
4701
+ }
4702
+ var noRealComponentProvider = defineRule({
4703
+ name: "no-real-component-provider",
4704
+ description: "Do not read a component's own provider from the fixture unless the file replaced it with a spy",
4705
+ schema: REAL_COMPONENT_PROVIDER_SCHEMA,
4706
+ messages: {
4707
+ noRealComponentProvider: "Nothing in this file replaces `{{token}}`, so the fixture hands back the real instance the component provides, and the spec tests it through the component. Install the spy where the component looks: `const {{variable}} = overrideComponentProvider(Component, {{token}});`, before the fixture is created. If the real one is meant, list `{{token}}` in `{ ignoreTokens }`."
4708
+ },
4709
+ create: (context) => {
4710
+ const scan = { reads: [], replaced: /* @__PURE__ */ new Set(), rendered: /* @__PURE__ */ new Set(), framework: /* @__PURE__ */ new Set(), opaque: false };
4711
+ const ignored = ignoredTokens(context.options);
4712
+ return {
4713
+ Property: (node) => {
4714
+ if (propertyName(node) === "provide" && isIdentifier(node.value)) {
4715
+ scan.replaced.add(node.value.name);
4716
+ }
4717
+ },
4718
+ Identifier: (node) => {
4719
+ if (hasAncestor(node, isReplacingArgument)) {
4720
+ scan.replaced.add(node.name);
4721
+ }
4722
+ if (hasAncestor(node, isRenderedList)) {
4723
+ scan.rendered.add(node.name);
4724
+ }
4725
+ if (hasAncestor(node, isFrameworkImport)) {
4726
+ scan.framework.add(node.name);
4727
+ }
4728
+ },
4729
+ CallExpression: (node) => {
4730
+ readCall2(node, scan);
4731
+ },
4732
+ "Program:exit": () => {
4733
+ if (scan.opaque) {
4734
+ return;
4735
+ }
4736
+ const skipped = [scan.replaced, scan.rendered, scan.framework, ignored];
4737
+ scan.reads.filter(({ name }) => !skipped.some((names) => names.has(name))).forEach((token) => {
4738
+ const variable = `${token.name.charAt(0).toLowerCase()}${token.name.slice(1)}`;
4739
+ context.report({ node: token, messageId: "noRealComponentProvider", data: { token: token.name, variable } });
4740
+ });
4741
+ }
4742
+ };
4743
+ }
4744
+ });
4745
+
4580
4746
  // src/lib/eslint/reflect-access.ts
4581
4747
  var HELPER3 = "mockValueProp";
4582
4748
  var REFLECT = /* @__PURE__ */ new Set(["Reflect"]);
@@ -5500,7 +5666,7 @@ function readCast(context, node) {
5500
5666
  context.report({ node, messageId: "castResponse", data: { cast: excerpt(context, node, 50) } });
5501
5667
  }
5502
5668
  }
5503
- function readCall2(context, node) {
5669
+ function readCall3(context, node) {
5504
5670
  const helper = isIdentifier(node.callee) ? node.callee.name : void 0;
5505
5671
  const argument = node.typeArguments?.params[0];
5506
5672
  if (helper === void 0 || !TYPE_ARGUMENT_DOUBLES.has(helper) || argument === void 0) {
@@ -5522,7 +5688,7 @@ var preferStubResponse = defineRule({
5522
5688
  },
5523
5689
  create: (context) => ({
5524
5690
  CallExpression: (node) => {
5525
- readCall2(context, node);
5691
+ readCall3(context, node);
5526
5692
  },
5527
5693
  [CAST_SELECTORS.join(", ")]: (node) => {
5528
5694
  readCast(context, node);
@@ -5758,6 +5924,7 @@ var rules = {
5758
5924
  "no-dead-schemas": noDeadSchemas,
5759
5925
  "no-import-time-spread": noImportTimeSpread,
5760
5926
  "no-unregistered-inject-spy": noUnregisteredInjectSpy,
5927
+ "no-real-component-provider": noRealComponentProvider,
5761
5928
  "prefer-render-shallow": preferRenderShallow,
5762
5929
  "prefer-to-have-signal-value": preferToHaveSignalValue,
5763
5930
  "prefer-observer-stub": preferObserverStub,
@@ -5810,6 +5977,7 @@ var recommendedRules = {
5810
5977
  [`${PLUGIN_NAME}/no-dead-schemas`]: "error",
5811
5978
  [`${PLUGIN_NAME}/no-import-time-spread`]: "error",
5812
5979
  [`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "error",
5980
+ [`${PLUGIN_NAME}/no-real-component-provider`]: "error",
5813
5981
  [`${PLUGIN_NAME}/prefer-render-shallow`]: "warn",
5814
5982
  [`${PLUGIN_NAME}/prefer-to-have-signal-value`]: "warn",
5815
5983
  [`${PLUGIN_NAME}/no-stub-class-double`]: "warn",
@@ -5853,7 +6021,8 @@ var plugin = {
5853
6021
  recommended: { plugins: {}, rules: recommendedRules },
5854
6022
  strict: { plugins: {}, rules: strictRules },
5855
6023
  typeErrors: { plugins: {}, rules: typeErrorRules }
5856
- }
6024
+ },
6025
+ hookRegisteringHelpers
5857
6026
  };
5858
6027
  plugin.configs.recommended.plugins[PLUGIN_NAME] = plugin;
5859
6028
  plugin.configs.strict.plugins[PLUGIN_NAME] = plugin;
@@ -227,29 +227,6 @@ interface RuleModule {
227
227
  create(context: RuleContext): RuleListener;
228
228
  }
229
229
 
230
- /**
231
- * `vitest-auto-spy/eslint-plugin` — the lint rules that keep a suite on the library's API.
232
- *
233
- * ```js
234
- * // eslint.config.js (flat config)
235
- * import autoSpy from 'vitest-auto-spy/eslint-plugin';
236
- *
237
- * export default [
238
- * { files: ['**\/*.spec.ts'], ...autoSpy.configs.recommended },
239
- * ];
240
- * ```
241
- *
242
- * Scope the config to spec files yourself: every rule here is about test code, and `Object.
243
- * defineProperty` or an object of `vi.fn()`s is perfectly reasonable in application code.
244
- *
245
- * `configs.strict` is `recommended` with every rule at `error`. `configs.typeErrors` is the subset whose
246
- * findings are compile errors — already `error` in `recommended` — to spread back over a blanket
247
- * downgrade while a large suite is being adopted.
248
- *
249
- * Flat config only. The legacy `.eslintrc` `plugins: ['…']` form resolves plugin names to
250
- * `eslint-plugin-*` packages, which a subpath export of this package can never be.
251
- */
252
-
253
230
  /** Severity map shared by the shipped configs. */
254
231
  type RuleSeverity = 'error' | 'off' | 'warn';
255
232
  /** A flat-config object: the plugin under its name, plus the rules it turns on. */
@@ -277,6 +254,11 @@ interface AutoSpyEslintPlugin {
277
254
  strict: FlatConfig;
278
255
  typeErrors: FlatConfig;
279
256
  };
257
+ /**
258
+ * The library's helpers that register `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when
259
+ * called, for `vitest/require-hook`'s `allowedFunctionCalls`.
260
+ */
261
+ hookRegisteringHelpers: readonly string[];
280
262
  }
281
263
  declare const plugin: AutoSpyEslintPlugin;
282
264
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.47.0",
3
+ "version": "5.49.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -384,7 +384,9 @@ it('loads', async () => {
384
384
  `toHaveSignalValue(v)` after `registerSignalMatchers()` from `vitest-auto-spy/angular/matchers`
385
385
  (`toEqual` semantics; `toHaveSignalValue(v, { strict: true })` for `toStrictEqual`'s, or
386
386
  `registerSignalMatchers({ strict: true })` for the whole suite). The type-aware
387
- `prefer-to-have-signal-value` lint rule rewrites `expect(sig()).toBe(v)` into the matcher.
387
+ `prefer-to-have-signal-value` lint rule rewrites `expect(sig()).toBe(v)`, `.toBeNull()` and
388
+ `.toBeUndefined()` into the matcher, except a `toBe` that checks an object's identity, which the
389
+ deep matcher cannot keep.
388
390
  - **Never assert a resource with `expect(r.value()).toEqual(...)` alone** — an unresolved resource
389
391
  still holds its _default_, so that passes while proving nothing. `toHaveResourceValue(v)` after
390
392
  `registerResourceMatchers()`, from the same `/angular/matchers` entry, fails unless the resource
@@ -408,7 +410,9 @@ it('loads', async () => {
408
410
  To control an `effect()`, set the signals it reads and assert what it produced.
409
411
  - **`injectSpy(X)` only reaches the global TestBed.** For a component-level provider use
410
412
  `asSpy(fixture.debugElement.injector.get(X))` to read it, or `overrideComponentProvider(Cmp, X)` to
411
- replace it — `provideAutoSpy` loses to a provider the component declares, silently.
413
+ replace it — `provideAutoSpy` loses to a provider the component declares, silently. Reading it
414
+ bare, with nothing in the file replacing it, hands back the real store and makes the component spec
415
+ re-test it; `no-real-component-provider` reports that.
412
416
  - **Declare `Spy<T>`, never Vitest's `Mocked<T>`** — `Mocked<T>` keeps the private members, so the
413
417
  assignment fails with a list of private field names that says nothing about the declaration being
414
418
  the problem.
@@ -485,13 +489,18 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
485
489
  Most of this library's guarantees are type-level, so a green run that does not type-check is not
486
490
  done. Report failures with their output rather than describing them as passing.
487
491
 
488
- **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The fifty rules in
492
+ **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The fifty-one rules in
489
493
  `vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
490
494
  to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
491
495
  evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
492
496
  accept those together with the repair at the creation site, usually `createAutoMock<T>()` in place of
493
497
  an object literal.
494
498
 
499
+ **`vitest/require-hook` reports a bare `useConsoleSpies();` or `setupFakeTimers();`** in a
500
+ `describe` body. Do not wrap it in a hook or disable the line: spread
501
+ `autoSpy.hookRegisteringHelpers` from `vitest-auto-spy/eslint-plugin` into the rule's
502
+ `allowedFunctionCalls`.
503
+
495
504
  **`no-private-member-access`, `no-mistyped-use-value` and `no-unknown-use-value-key` are the three
496
505
  rules here that need type information.** The first reports
497
506
  `instance['privateMember']` (bracket access, which TypeScript does not visibility-check),