vitest-auto-spy 5.47.0 → 5.48.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,36 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.48.0] - 2026-09-28
14
+
15
+ ### Added
16
+
17
+ - **`hookRegisteringHelpers` on `vitest-auto-spy/eslint-plugin`** — the frozen list of the
18
+ library's public helpers that register `beforeEach` / `afterEach` / `beforeAll` / `afterAll` when
19
+ called (`useConsoleSpies`, `setupAutoSpy`, `setupFakeTimers`, `useCountingClock`, `mockNow`, the
20
+ `guard*` family, …), for `vitest/require-hook`'s `allowedFunctionCalls`:
21
+ `allowedFunctionCalls: [...autoSpy.hookRegisteringHelpers]`. A bare `useConsoleSpies();` in a
22
+ `describe` body no longer needs a hand-kept list; a spec walks the call graph of every public entry
23
+ and fails when a hook-registering export is missing from it. It is a property of the plugin
24
+ object, so `require()` and `import` both reach it. `vitest-auto-spy/eslint-plugin` is +0.42 kB
25
+ min+gzip (+1.0 %) for the list and the two rule changes.
26
+ - **`prefer-to-have-signal-value`** also reports `expect(sig()).toBeNull()` and `.toBeUndefined()`
27
+ on a signal read, `.not` included, and fixes them to `toHaveSignalValue(null)` /
28
+ `toHaveSignalValue(undefined)`.
29
+
30
+ ### Fixed
31
+
32
+ - **`prefer-to-have-signal-value`** no longer rewrites an identity check. `expect(list.items()).toBe(items)`
33
+ asserts the signal holds that very array, and the fix turned it into `toHaveSignalValue(items)`,
34
+ which compares deeply and passes for any equal copy. A `toBe` is now reported only when the
35
+ expected value is a primitive literal or the signal's type is primitive (`any` does not count);
36
+ `toEqual` and `toStrictEqual` are unchanged.
37
+ - **Docs**: `vitest-auto-spy/console` imports `vitest` itself (the fallback adapter and the
38
+ `useConsoleSpies()` hooks), so on `node:test` and Rstest it loads only where the `vitest` package
39
+ resolves; the console page, the README and `agent-docs/adapters.md` now say so. The README API
40
+ table and `agent-docs/angular.md` list `registerSignalMatchers({ strict })`, and the types list of
41
+ `/angular/matchers` names `RegisterSignalMatchersOptions`.
42
+
13
43
  ## [5.47.0] - 2026-09-28
14
44
 
15
45
  ### Added
@@ -6046,7 +6076,8 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
6046
6076
  This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
6047
6077
  [full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
6048
6078
 
6049
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.47.0...HEAD
6079
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.48.0...HEAD
6080
+ [5.48.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.47.0...v5.48.0
6050
6081
  [5.47.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.46.0...v5.47.0
6051
6082
  [5.46.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...v5.46.0
6052
6083
  [5.45.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.44.0...v5.45.0
package/README.md CHANGED
@@ -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
 
@@ -4017,7 +4020,7 @@ export can never be.
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 |
4019
4022
  | `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 |
4023
+ | `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
4024
  | `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
4025
  | `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
4026
  | `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
@@ -4360,7 +4363,7 @@ another, and the next one is in that same file.
4360
4363
  | `collectRouterEvents(events)` _(`/angular-router`)_ | The double's router events as one comparable value — `expect([[NavigationStart, '/checkout'], …])`, a class-and-URL pair per event |
4361
4364
  | `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
4365
  | `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)` |
4366
+ | `registerSignalMatchers(opts?)` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)`; `{ strict: true }` compares every assertion like `toStrictEqual` |
4364
4367
  | `trackRecomputations(signal)` / `trackEffectRuns(effectRef)` _(Angular)_ | Count what the reactive graph re-ran — `{ count, stop() }`, undone by `restoreMockedProps()` |
4365
4368
  | `provideWindowDouble(token, overrides?)` / `provideDocumentDouble(overrides?)` _(Angular)_ | A `window` / `document` merged over the real jsdom one; `createWindowDouble` / `createDocumentDouble` outside DI |
4366
4369
  | `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` |
@@ -56,7 +75,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
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 |
57
76
  | `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
77
  | `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 |
78
+ | `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
79
  | `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
80
  | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
62
81
  | `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` |
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.47.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) {
@@ -5853,7 +5905,8 @@ var plugin = {
5853
5905
  recommended: { plugins: {}, rules: recommendedRules },
5854
5906
  strict: { plugins: {}, rules: strictRules },
5855
5907
  typeErrors: { plugins: {}, rules: typeErrorRules }
5856
- }
5908
+ },
5909
+ hookRegisteringHelpers
5857
5910
  };
5858
5911
  plugin.configs.recommended.plugins[PLUGIN_NAME] = plugin;
5859
5912
  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.48.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
@@ -492,6 +494,11 @@ evidence that the types still hold. Where it cannot prove the rename it downgrad
492
494
  accept those together with the repair at the creation site, usually `createAutoMock<T>()` in place of
493
495
  an object literal.
494
496
 
497
+ **`vitest/require-hook` reports a bare `useConsoleSpies();` or `setupFakeTimers();`** in a
498
+ `describe` body. Do not wrap it in a hook or disable the line: spread
499
+ `autoSpy.hookRegisteringHelpers` from `vitest-auto-spy/eslint-plugin` into the rule's
500
+ `allowedFunctionCalls`.
501
+
495
502
  **`no-private-member-access`, `no-mistyped-use-value` and `no-unknown-use-value-key` are the three
496
503
  rules here that need type information.** The first reports
497
504
  `instance['privateMember']` (bracket access, which TypeScript does not visibility-check),