vitest-auto-spy 5.10.1 → 5.12.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
@@ -771,6 +771,15 @@ Precedence, first one set wins: the double's `onUnstubbedCall` → the double's
771
771
  and a `calledWith` configured after them is never consulted. After the last `mockReturnValueOnce`
772
772
  the queue empties back onto the library dispatch and the next call **is** reported as unstubbed —
773
773
  seed `mockReturnValue(...)` too when a `Once` sequence is meant to run out.
774
+ - **That pair is reported, in both orders.** `mockReturnValue` after a `calledWith` erases it;
775
+ `calledWith` after a `mockReturnValue` is dead on arrival. Neither fails on its own — the spec goes
776
+ green on a branch nobody configured — so the library says so as a misconfiguration (warn, or throw
777
+ under the `strict` preset). The whole family replaces: `mockImplementation`, `mockReturnValue`,
778
+ `mockReturnThis`, `mockThrow`, `mockResolvedValue`, `mockRejectedValue`. When both a fallback and a
779
+ per-argument value are wanted, the fallback goes in the spy's container — `returns:` where the
780
+ double is built, or `resolveWith` / `nextWith` / `failWith` — which a `calledWith` still wins over.
781
+ The report is this library's own spy engine's, so Vitest and Rstest have it; Bun, `node:test` and
782
+ `setSpyEngine('runner')` do not. The `Once` family is never reported.
774
783
  - **A throw something swallowed still fails the test.** Under `setupAutoSpy({ strict: true })` every
775
784
  strict throw is recorded, and one that a `try`/`catch` in the code under test or an RxJS error with
776
785
  no handler (its rethrow waits on a fake clock) kept from the test fails it after the test
@@ -1724,6 +1733,25 @@ Three rules worth stating outright, because each of them cost a day somewhere:
1724
1733
  teardown — a green run with `NG0205: Injector has already been destroyed` in "Unhandled Errors"
1725
1734
  and a non-zero exit code.
1726
1735
 
1736
+ **`stable()` and `flushEffects()` are not zoneless-only, and their tick runs inside the `NgZone` so
1737
+ that they are not.** `TestBed.createComponent` builds the component inside `ngZone.run(…)`, so an
1738
+ `effect()` its constructor registers records the zone's inner zone as its own. A bare `TestBed.tick()`
1739
+ from a test body runs in the runner's zone, so Angular hops back into the recorded one to run a
1740
+ **dirty** effect, and leaving that hop takes the zone from unstable to stable.
1741
+ `provideZoneChangeDetection()` — which `@angular/build:unit-test` installs for every suite that loads
1742
+ zone.js — answers `NgZone.onMicrotaskEmpty` with `ApplicationRef._tick()`, guarded against its own
1743
+ scheduler but not against `ApplicationRef._runningTick`, so the tick already on the stack is
1744
+ re-entered: `NG0101: ApplicationRef.tick is called recursively`. Angular hands that to `ErrorHandler`
1745
+ instead of throwing it at the call site, so a suite that does not fail on console output stays green
1746
+ with the change detection it asked for unfinished. Measured on an Angular 22 suite of 1771 spec files:
1747
+ 451 `componentRef.setInput` calls rewritten to `setInputs` turned **57 green files red** on `NG0101`,
1748
+ and none of them after the tick moved inside the zone. Under zoneless `NgZone` is a `NoopNgZone` whose
1749
+ `run` is a straight call, so it costs nothing there. One thing is genuinely new for a zone-based
1750
+ consumer: leaving the zone reports it stable, so the same subscriber ticks once more — counted on
1751
+ `ApplicationRef.afterTick`, one `flushEffects()` is 1 → 2 application ticks, which is what
1752
+ `fixture.detectChanges()` has always done in the same position. Do not "simplify" either helper back
1753
+ to a bare `TestBed.tick()`; `src/zone-tests/stable.zone-test.ts` is the guard.
1754
+
1727
1755
  `flushEventLoopUntil(isDone, { turns, label })` is the same thing with a condition and a budget —
1728
1756
  for a chunk becoming reachable, an SDK reporting itself ready, a queue draining. Use it instead of a
1729
1757
  hand-tuned turn count: the count depends on the dependency, not on the spec, and a condition that
@@ -2688,6 +2716,7 @@ const { fixture, component } = renderShallow(TaskListComponent, {
2688
2716
  // change an input after the first render — one setInput per key, then the wait, in one await
2689
2717
  await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
2690
2718
  await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
2719
+ // a fixture whose componentType carries no ɵcmp is refused by name too, not as a bare TypeError
2691
2720
 
2692
2721
  // build a class through DI, every unprovided token auto-spied
2693
2722
  const { instance, spies } = createWithAutoSpies(CartService, {
@@ -3115,7 +3144,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3115
3144
  | --------------------------------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3116
3145
  | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
3117
3146
  | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3118
- | `prefer-provide-auto-spy` | `error` | — | a hand-rolled `useValue`, `useFactory`, `useClass` or `useExisting` (also `useValue: new StubMock()`), in a provider **or** a `TestBed.overrideProvider(X, { … })` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`. A name in the slot is followed one step, through an initialiser or a single `beforeEach` assignment |
3147
+ | `prefer-provide-auto-spy` | `error` | fix | a hand-rolled `useValue`, `useFactory`, `useClass` or `useExisting` (also `useValue: new StubMock()`), in a provider **or** a `TestBed.overrideProvider(X, { … })` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`. A name in the slot is followed one step, through an initialiser or a single `beforeEach` assignment. `{ provide: X, useValue: createSpyFromClass(X, config) }` — the factory's own body, written out — is reported and **rewritten** to `provideAutoSpy(X, config)`; a double read from a _different_ class (`{ provide: LocalStorage, useValue: createSpyFromClass(BaseLocalStorage) }`) is not, since the abstract token has no prototype for `provideAutoSpy` to read |
3119
3148
  | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3120
3149
  | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy` and delete the stub; a decorated, `implements`-ing, `extends`-ing or unnamed class is exempt — option: `{ minRunnerFns }` |
3121
3150
  | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`; a bare `let fn: Mock` is a callback and never reported — option: `{ minRunnerFns }` |
@@ -3133,6 +3162,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3133
3162
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
3134
3163
  | `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 |
3135
3164
  | `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 |
3165
+ | `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` |
3136
3166
  | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3137
3167
  | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory` / `useExisting`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3138
3168
  | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
@@ -3151,8 +3181,8 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3151
3181
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3152
3182
  | `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 |
3153
3183
 
3154
- Thirty-seven rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3155
- `no-stub-class-double`, `no-structural-double` and `no-instance-lifecycle-spy`**; three fix on their own, twelve offer suggestions. Thirty-four are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3184
+ Thirty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3185
+ `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-five are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3156
3186
  `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3157
3187
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
3158
3188
  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
@@ -3396,6 +3426,8 @@ packages, which a subpath export can never be.
3396
3426
  | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` — never `@ts-expect-error`, which `no-ts-expect-error-on-double` reports (§16) |
3397
3427
  | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
3398
3428
  | `setInputs: … declares no input named …` | the key is not an input of that component — a typo, an input renamed since, or one of its own fields | set the names the message lists; a signal the component owns is moved through the signal (§13) |
3429
+ | `setInputs: X carries no ɵcmp, so there are no inputs to set` | the fixture's `componentType` is not a compiled `@Component` — a `@Directive` or `@Pipe` class, or one Angular never compiled (a barrel import that resolved to `undefined`) | pass a component fixture; a directive takes its inputs through the host element that applies it (`createDirectiveHost`), and for `undefined` import the class from its own file (§13) |
3430
+ | `TypeError: dependencies is not a function`, from `renderShallow({ keepTemplate: true })` | the compiled `ɵcmp.dependencies` is the flat array AOT emits when no cycle forced a factory, and the trim called it | upgrade — all three shapes (array, factory, `null`) are read; nothing to change in the spec |
3399
3431
  | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
3400
3432
  | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
3401
3433
  | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
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-17.2%20kB-brightgreen)](#install)
26
+ [![minzipped size](https://img.shields.io/badge/minzip-17.5%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)
@@ -79,7 +79,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
79
79
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
80
80
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
81
81
  - 🚚 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
82
- - 📏 Lint rules and one-line test-run hygiene — thirty-seven rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, twelve suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade
82
+ - 📏 Lint rules and one-line test-run hygiene — thirty-eight rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, thirteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade
83
83
  - 🩺 [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
84
84
  - 🔎 [`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
85
85
  - ⏱️ [`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
@@ -2091,6 +2091,17 @@ myService.getName.mustBeCalledWith(1).mockReturnValue('Fake Name');
2091
2091
  expect(() => myService.getName(2)).toThrow();
2092
2092
  ```
2093
2093
 
2094
+ **The first line and the second do not layer.** `mockReturnValue` and its family
2095
+ (`mockImplementation`, `mockResolvedValue`, `mockRejectedValue`, `mockThrow`, `mockReturnThis`)
2096
+ install an implementation on the host mock, and the dispatch that reads a `calledWith` chain **is**
2097
+ the implementation they replace — so the one written later wins outright and the other decides
2098
+ nothing, in either order, without failing. Both orders are now reported as a misconfiguration (a
2099
+ warning, a throw under the `strict` preset). Where both are wanted, the fallback goes in the spy's
2100
+ own container, which a chain still wins over: the `returns` option where the double is built, or
2101
+ `resolveWith` / `nextWith` / `failWith`. The report comes from this library's own spy engine — so
2102
+ Vitest and Rstest, not Bun or `node:test`, and not under `setSpyEngine('runner')` — and the `Once`
2103
+ family is never reported, because its queue drains back onto the dispatch.
2104
+
2094
2105
  The failure prints both sides, because the diagnosis is the comparison rather than either half of
2095
2106
  it — and every configured call when there is more than one, matchers included, so a config that
2096
2107
  never matched is visible instead of inferred:
@@ -2371,8 +2382,14 @@ which shallow rendering touches.
2371
2382
  override keeps the component's own `imports` minus the child _components_, so the template renders
2372
2383
  with its own pipes and directives working while every child component in it resolves to nothing
2373
2384
  under `NO_ERRORS_SCHEMA`. (Before 5.4.0 the whole scope was dropped, which made a pipe in a kept
2374
- template throw `NG0302` and an attribute directive silently never apply.) A child re-exported by an
2375
- imported `NgModule` still renders — the module is kept whole. Reach for it when the spec reads
2385
+ template throw `NG0302` and an attribute directive silently never apply.) Those imports are read off
2386
+ `ɵcmp`, where the compiler leaves them in one of three shapes — the flat array, a factory returning
2387
+ it, or `null` for a component that imports nothing — and which one is not decided by JIT against
2388
+ AOT: JIT always emits the factory, AOT emits the array unless a cycle between two components forces
2389
+ it to defer the read. All three are handled; calling the array unconditionally was a
2390
+ `TypeError: dependencies is not a function` on an AOT-compiled component with no cycle in its
2391
+ imports. A child re-exported by an imported `NgModule` still renders — the module is kept whole.
2392
+ Reach for it when the spec reads
2376
2393
  something the template creates, and keep `keepChildren` for the handful of children it genuinely
2377
2394
  needs resolvable. Full write-up:
2378
2395
  [Performance](https://asdalexey.github.io/vitest-auto-spy/core/performance#_2-rendering-the-child-subtree).
@@ -2417,8 +2434,20 @@ flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
2417
2434
  `fixture.detectChanges()` runs a single change-detection pass and does **not** flush pending
2418
2435
  effects, so an assertion right after it reads state that has not finished computing. In a zoneless
2419
2436
  app the state that matters is signal-derived and effects are what move it forward. `stable` does
2420
- both, in the right order; `flushEffects` prefers `TestBed.tick()` (Angular ≥ 20) and falls back to
2421
- `ApplicationRef.tick()`.
2437
+ both, in the right order; `flushEffects` is `TestBed.tick()`, run inside the `NgZone`.
2438
+
2439
+ **Both work under zone.js, and the zone is why the tick is wrapped.** The name says zoneless and the
2440
+ helpers are written for it, but nothing in them is — `stable` and `setInputs` are the same two lines
2441
+ in a zone-based suite. `TestBed.createComponent` builds the component inside `ngZone.run(…)`, so an
2442
+ `effect()` its constructor registers records the zone's inner zone; a tick started from a test body
2443
+ runs in the runner's zone, hops back to run a **dirty** effect, and leaving that hop reports the zone
2444
+ stable mid-tick. `provideZoneChangeDetection()` — which the Angular CLI's unit-test builder installs
2445
+ for every suite that loads zone.js — answers that with `ApplicationRef._tick()`, guarded against its
2446
+ own scheduler but not against the tick already on the stack, which is
2447
+ `NG0101: ApplicationRef.tick is called recursively`. Running the tick inside the zone removes the hop.
2448
+ Measured on an Angular 22 suite of 1771 spec files: 451 `componentRef.setInput` calls rewritten to
2449
+ `setInputs` turned **57 green files red** on `NG0101`, and none of them after the fix. Under zoneless
2450
+ `NgZone` is a `NoopNgZone` whose `run` is a straight call, so it costs nothing there.
2422
2451
 
2423
2452
  The wait is bounded: `stable` gives the fixture **2000 ms** and then throws the cause, instead of
2424
2453
  letting the runner report a 5 s file-level timeout that names neither the helper nor the fixture.
@@ -3336,7 +3365,7 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
3336
3365
  export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
3337
3366
  ```
3338
3367
 
3339
- **Every rule is an `error` since 4.0.0, bar four.** The config used to grade them `error` / `warn` /
3368
+ **Every rule is an `error` since 4.0.0, bar five.** The config used to grade them `error` / `warn` /
3340
3369
  `off`, which chose for you how much each finding mattered; a `warn` in a repository that does not read
3341
3370
  lint output is `off` with extra noise, and which findings block a merge is one line of config either
3342
3371
  way. To turn one down, spread the rule map as well — a bare `rules` key beside the spread config
@@ -3392,51 +3421,52 @@ object of `vi.fn()`s is perfectly reasonable in application code. Flat config on
3392
3421
  `.eslintrc` `plugins: ['…']` form resolves names to `eslint-plugin-*` packages, which a subpath
3393
3422
  export can never be.
3394
3423
 
3395
- | Rule | Recommended | Fix | Flags |
3396
- | --------------------------------- | :---------: | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3397
- | `prefer-provide-auto-spy` | `error` | — | a hand-rolled `useValue`, `useFactory` **or** `useClass` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
3398
- | `prefer-create-spy-from-class` | `error` | — | an object literal of two or more `vi.fn()`s → `createSpyFromClass` / `createAutoMock`, unless it is a factory's own seed |
3399
- | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy`, the stub class deleted |
3400
- | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()` |
3401
- | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, in one step or two → `injectSpy(X).m` |
3402
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3403
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside a `subscribe()` callback → `expectEmission` / `firstValueFrom` |
3404
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory that returns it |
3405
- | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the value assigned is not one of this library's factories |
3406
- | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3407
- | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
3408
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3409
- | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement of its own — a stub nobody continued, asserting nothing |
3410
- | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
3411
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3412
- | `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` |
3413
- | `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 |
3414
- | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
3415
- | `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 |
3416
- | `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 |
3417
- | `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
3418
- | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3419
- | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and still prints → `installConsoleSpies()` from `vitest-auto-spy/console`, or `.mockImplementation(() => undefined)` |
3420
- | `no-console-in-spec` | `error` | — | a spec that calls `console.x(…)` itself, or replaces a method with `console.x = …`, which nothing puts back — under `isolate: false` every later file inherits it |
3421
- | `no-import-time-console-spies` | `error` | — | an import of `vitest-auto-spy/console` in a file that never calls `installConsoleSpies()` — the import installs once per worker and, under `isolate: false`, silences every later file |
3422
- | `no-mistyped-use-value` | `error` | — | `{ provide: TOKEN, useValue }` whose value is not assignable to the primitive `TOKEN` declares — `useValue` is `any`, so `{}` for an `InjectionToken<boolean>` compiles and is truthy; **type-aware**, and silent without `parserOptions.project` |
3423
- | `no-unknown-use-value-key` | `error` | — | a key of an object `useValue` literal the provided type does not have — `T` of an `InjectionToken<T>`, or a class's instance; keys only, never the values; silent on `any` / `unknown`, an index signature, a spread and `multi: true`; **type-aware** |
3424
- | `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(component, 'ngOnInit')` and the other hooks a view reads off the prototype — Angular never calls the instance spy → spy on `Cls.prototype` before the component is created, or assert the hook's effect |
3425
- | `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature — an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`, anything else is a fixture of the wrong shape |
3426
- | `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer that value already fixes → assert on what the code produced, or `expect.fail(…)` for a branch the test must not reach |
3427
- | `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — a test whose whole body asks whether the subject is there, in a block whose other tests run the same `beforeEach` and would fail first, naming what they were doing → delete it; the suggestion does. A block whose only running test is that one is left alone |
3428
- | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled; **silent** until `{ builder: 'inline-resources' }` says so, because a JIT setup that loads resources at run time needs the call. One exception the rule cannot see: a component whose template holds a `@defer` block ships async class metadata, so the call stays load-bearing there |
3429
- | `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, every `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`; none of them is a promise, so the `await` waits for nothing and the `async` it forced on the hook awaits nothing either. The suggestion drops both. Reads no types, so it reports without `parserOptions.project` |
3430
- | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
3431
- | `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
3432
- | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
3433
- | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — the last mile off the jasmine shim |
3424
+ | Rule | Recommended | Fix | Flags |
3425
+ | --------------------------------- | :---------: | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3426
+ | `prefer-provide-auto-spy` | `error` | fix | a hand-rolled `useValue`, `useFactory` **or** `useClass` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`; and `{ provide: X, useValue: createSpyFromClass(X) }` — the factory written out — which it rewrites |
3427
+ | `prefer-create-spy-from-class` | `error` | — | an object literal of two or more `vi.fn()`s → `createSpyFromClass` / `createAutoMock`, unless it is a factory's own seed |
3428
+ | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy`, the stub class deleted |
3429
+ | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()` |
3430
+ | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, in one step or two → `injectSpy(X).m` |
3431
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3432
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside a `subscribe()` callback → `expectEmission` / `firstValueFrom` |
3433
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory that returns it |
3434
+ | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the value assigned is not one of this library's factories |
3435
+ | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
3436
+ | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
3437
+ | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
3438
+ | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement of its own — a stub nobody continued, asserting nothing |
3439
+ | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
3440
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
3441
+ | `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` |
3442
+ | `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 |
3443
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
3444
+ | `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 |
3445
+ | `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 |
3446
+ | `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` |
3447
+ | `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
3448
+ | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3449
+ | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and still prints → `installConsoleSpies()` from `vitest-auto-spy/console`, or `.mockImplementation(() => undefined)` |
3450
+ | `no-console-in-spec` | `error` | — | a spec that calls `console.x(…)` itself, or replaces a method with `console.x = …`, which nothing puts back — under `isolate: false` every later file inherits it |
3451
+ | `no-import-time-console-spies` | `error` | — | an import of `vitest-auto-spy/console` in a file that never calls `installConsoleSpies()` — the import installs once per worker and, under `isolate: false`, silences every later file |
3452
+ | `no-mistyped-use-value` | `error` | — | `{ provide: TOKEN, useValue }` whose value is not assignable to the primitive `TOKEN` declares — `useValue` is `any`, so `{}` for an `InjectionToken<boolean>` compiles and is truthy; **type-aware**, and silent without `parserOptions.project` |
3453
+ | `no-unknown-use-value-key` | `error` | — | a key of an object `useValue` literal the provided type does not have — `T` of an `InjectionToken<T>`, or a class's instance; keys only, never the values; silent on `any` / `unknown`, an index signature, a spread and `multi: true`; **type-aware** |
3454
+ | `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(component, 'ngOnInit')` and the other hooks a view reads off the prototype — Angular never calls the instance spy → spy on `Cls.prototype` before the component is created, or assert the hook's effect |
3455
+ | `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature — an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`, anything else is a fixture of the wrong shape |
3456
+ | `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer that value already fixes → assert on what the code produced, or `expect.fail(…)` for a branch the test must not reach |
3457
+ | `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — a test whose whole body asks whether the subject is there, in a block whose other tests run the same `beforeEach` and would fail first, naming what they were doing → delete it; the suggestion does. A block whose only running test is that one is left alone |
3458
+ | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled; **silent** until `{ builder: 'inline-resources' }` says so, because a JIT setup that loads resources at run time needs the call. One exception the rule cannot see: a component whose template holds a `@defer` block ships async class metadata, so the call stays load-bearing there |
3459
+ | `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, every `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`; none of them is a promise, so the `await` waits for nothing and the `async` it forced on the hook awaits nothing either. The suggestion drops both. Reads no types, so it reports without `parserOptions.project` |
3460
+ | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
3461
+ | `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
3462
+ | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
3463
+ | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — the last mile off the jasmine shim |
3434
3464
 
3435
3465
  Every message ends with a link to the matching [recipe](#how-to-mock): a rule that only says
3436
3466
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
3437
3467
  are versioned together and stop being re-written in every project that installs the package.
3438
3468
 
3439
- **Three of the thirty-seven fix on their own, twelve offer suggestions**, and the split is not about how hard
3469
+ **Four of the thirty-eight fix on their own, thirteen offer suggestions**, and the split is not about how hard
3440
3470
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
3441
3471
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
3442
3472
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -3529,6 +3559,17 @@ line named after construction. It is weighed against the tests that share its se
3529
3559
  its block. Measured over an Angular suite of 1771 spec files: 569 findings in 540 of them, 515 still carrying the
3530
3560
  `should create` / `should be created` title `ng generate` wrote; on a second suite of 845 files, 97 in 87. The suggestion deletes the test together with the blank line above it.
3531
3561
 
3562
+ `prefer-set-inputs` reports a run of `fixture.componentRef.setInput('name', value)` on one fixture.
3563
+ Angular checks that name against nothing: an input the component does not declare answers with an
3564
+ `NG0303` on the console and **no change at all**, so the spec fails several lines later, on state
3565
+ nothing moved. `setInputs(fixture, { … })` resolves every key against the compiled definition before
3566
+ it writes the first one and types the value — on the 1771-file suite, rewriting the 650 calls it can
3567
+ rewrite turned 72 drifted fixtures into compile errors across 21 files. It reports 504 times in 140
3568
+ files there, and the edit is a **suggestion**: `setInputs` awaits `stable()`, whose `TestBed.tick()`
3569
+ re-enters the tick zone.js schedules for itself, and accepting all 451 of them turned 57 of 105 green
3570
+ files red on `NG0101: ApplicationRef.tick is called recursively`. That is also why it ships as a
3571
+ `warn`: the finding is a fact, the repair is a migration a project takes file by file.
3572
+
3532
3573
  `no-compile-components` says nothing until `['error', { builder: 'inline-resources' }]`
3533
3574
  states that the builder inlines `templateUrl` / `styleUrls`; then `compileComponents()` is a promise
3534
3575
  already settled, and a suggestion deletes it together with the `async` of a hook that awaits nothing
@@ -3584,7 +3625,7 @@ package's own — it needs its ESLint integration switched on.
3584
3625
  ### WebStorm and the other JetBrains IDEs
3585
3626
 
3586
3627
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
3587
- all run ESLint natively, so the thirty-seven rules appear inline, in the **Problems** tool window, and
3628
+ all run ESLint natively, so the thirty-eight rules appear inline, in the **Problems** tool window, and
3588
3629
  under **Code → Inspect Code** for the whole project.
3589
3630
 
3590
3631
  ```js
@@ -3601,7 +3642,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
3601
3642
  the fixes and suggestions live.
3602
3643
 
3603
3644
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
3604
- and then keep a second copy of thirty-seven rules, in Kotlin, in step with the TypeScript ones.
3645
+ and then keep a second copy of thirty-eight rules, in Kotlin, in step with the TypeScript ones.
3605
3646
 
3606
3647
  ### VS Code, Cursor, Windsurf, VSCodium
3607
3648
 
@@ -1,4 +1,4 @@
1
- import { flushEffects } from './chunk-GNJLDNJS.js';
1
+ import { flushEffects } from './chunk-M5BULP7X.js';
2
2
  import { withDocs, DOCS_LINKS } from './chunk-Q477VHHF.js';
3
3
  import { provideHttpClient } from '@angular/common/http';
4
4
  import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
@@ -1,4 +1,4 @@
1
- import { createFunctionSpy } from './chunk-NQNHA3R4.js';
1
+ import { createFunctionSpy } from './chunk-MUJTPXMJ.js';
2
2
  import './chunk-OTOCFH6B.js';
3
3
  import { withDocs, DOCS_LINKS } from './chunk-Q477VHHF.js';
4
4
  import { TestBed } from '@angular/core/testing';
package/dist/angular.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { o as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, d as RunCounter, S as SettleResourceOptions, e as ShallowRender, f as SpyRegistry, g as StableOptions, h as createWithAutoSpies, i as flushEffects, j as injectSpy, p as provideAutoSpy, q as provideAutoSpyForToken, r as renderShallow, k as runEffect, s as setInputs, l as settleResource, m as stable, t as trackEffectRuns, n as trackRecomputations } from './track-signal-runs-BoA6j-74.js';
1
+ export { o as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, d as RunCounter, S as SettleResourceOptions, e as ShallowRender, f as SpyRegistry, g as StableOptions, h as createWithAutoSpies, i as flushEffects, j as injectSpy, p as provideAutoSpy, q as provideAutoSpyForToken, r as renderShallow, k as runEffect, s as setInputs, l as settleResource, m as stable, t as trackEffectRuns, n as trackRecomputations } from './track-signal-runs-C-DgtCj9.js';
2
2
  import { InjectionToken, Provider, Type, WritableSignal, Signal, ProviderToken, FactoryProvider, AbstractType, Injector, ValueProvider } from '@angular/core';
3
3
  import { A as AutoMockConfiguration, D as DeepReadonly } from './expect-emission-CmN9ADyN.js';
4
4
  export { C as CallbackSubscribable, E as EmissionObserver, b as EmissionOptions, c as EmissionSource, S as SubscribableLike, g as expectCompletion, h as expectEmission, i as expectEmissions, j as expectError, k as expectNoEmission, s as setEmissionTimeout } from './expect-emission-CmN9ADyN.js';