vitest-auto-spy 5.9.0 → 5.10.1

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
@@ -2345,27 +2345,41 @@ converged on, and every part of it is a guess the component falls through: `of()
2345
2345
  `url` is a string nobody updates, `routerState` is absent, `serializeUrl` throws on the first
2346
2346
  redirect a guard builds. `provideRouterDouble(init)` keeps one URL and derives the rest.
2347
2347
 
2348
- | Call | Does |
2349
- | ------------------------------- | ------------------------------------------------------------------------------------------------------------ |
2350
- | `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
2351
- | `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)` |
2352
- | `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
2348
+ | Call | Does |
2349
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
2350
+ | `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
2351
+ | `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)`, `setCurrentNavigation(navigation?)` |
2352
+ | `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
2353
2353
 
2354
2354
  Unlike the route, this is **not** an instance of Angular's class: a real `Router` drags the whole
2355
2355
  routing stack in. It is a structural double that answers `url` (serialized as the real router does),
2356
2356
  `events` (a `BehaviorSubject` starting at the `NavigationEnd` that put it there), `navigate` and
2357
2357
  `navigateByUrl` (spies resolving `true`), `serializeUrl` / `parseUrl` / `createUrlTree` (the
2358
- router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`) and `routerState`
2359
- (Angular's own, its `snapshot.url` the URL). **Every other member `Router` declares throws by name**
2360
- rather than reading `undefined`.
2358
+ router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`), `routerState`
2359
+ (Angular's own, its `snapshot.url` the URL) and `currentNavigation` / `getCurrentNavigation()` (the
2360
+ navigation in flight, `null` while the router stands still). **Every other member `Router` declares
2361
+ throws by name** rather than reading `undefined` — and that holds for the instance fields too, which
2362
+ is why a hand-rolled double needs `instanceMethodsToSpyOn: ['currentNavigation']` and this one does
2363
+ not.
2361
2364
 
2362
- Four things to know:
2365
+ Five things to know:
2363
2366
 
2364
2367
  - **It does not navigate.** `navigate()` records the call and resolves `true`; it does not move
2365
2368
  `url`. `setUrl()` and `emitNavigation()` move it; `RouterTestingHarness` over a real
2366
2369
  `provideRouter()` is what tests a navigation.
2367
2370
  - **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
2368
2371
  `NavigationEnd` moves the URL with it; any other event does not.
2372
+ - **The navigation in flight follows the events, and a terminal event ends it _after_ delivering
2373
+ it.** `setCurrentNavigation({ extras: { state } })` puts one up and `setCurrentNavigation(null)`
2374
+ ends it; a `NavigationStart` pushed through `emitNavigation()` starts one with that event's id,
2375
+ URL and trigger, and a `NavigationEnd`, `NavigationCancel`, `NavigationError` or
2376
+ `NavigationSkipped` drops it back to `null` once its subscribers have run. Angular's doc comment
2377
+ — "the current navigation becomes to null after the NavigationEnd event is emitted" — means
2378
+ _after_ literally: the router emits the terminal event from a `tap` with the navigation still in
2379
+ flight and clears it in the `finalize` below, and `events` is a Subject, so a synchronous
2380
+ subscriber runs between the two. **A component that reads `currentNavigation()` while handling a
2381
+ `NavigationEnd` gets the navigation that just finished**, here and in production; it reads `null`
2382
+ only once `navigate()` has resolved. Probed against a real `provideRouter()` on Angular 22.
2369
2383
  - **Ordering does not matter here.** `Router` is `providedIn: 'root'` and `provideRouter()` does not
2370
2384
  re-provide the token, so the double wins in either order — and a `TestBed` without it hands out a
2371
2385
  real router, which is what `injectRouterDouble()` says by name.
@@ -2465,7 +2479,9 @@ Five things to know:
2465
2479
  `InjectionToken<Window>`. The helper is generic over the token's type, so an
2466
2480
  `InjectionToken<AppWindow>` has that interface's members checked in the overrides too.
2467
2481
  - **A plain `{ … }` merges, anything else replaces.** `{ screen: { width: 1920 } }` keeps
2468
- `screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole.
2482
+ `screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole. The
2483
+ slice goes three levels down, the way the merge does — `{ document: { location: { href: '' } } }` is
2484
+ checked and merged member by member.
2469
2485
  - **Nothing to restore.** The globals are never patched — the double is a view over them, and every
2470
2486
  write and delete lands on the view, which is also how a spec moves a value mid-test:
2471
2487
  `Object.assign(TestBed.inject(WINDOW), { scrollY: 40 })` (`Object.assign` because lib.dom declares
@@ -3127,6 +3143,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3127
3143
  | `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(instance, 'ngOnInit')` (and `ngOnDestroy`, `ngDoCheck`, `ngAfterContent*`, `ngAfterView*`) — a view calls the hook read off the prototype, so the instance spy is never called and its stub never runs → `vi.spyOn(Cls.prototype, …)` before `createComponent`, or assert the effect. `ngOnChanges` is exempt: Angular calls it through the instance |
3128
3144
  | `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `returnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature → an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`; otherwise the fixture is the wrong shape, checked against `ReturnType<X['m']>`. A reason after the directive does not silence it; a deliberate out-of-type value keeps it under `eslint-disable-next-line … -- <why>` |
3129
3145
  | `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer it already fixes (`toBe` / `toEqual` / `toStrictEqual` against a literal; `toBeTruthy`, `toBeDefined`, `toBeNull`… for any literal) → assert on what the code produced, or `expect.fail(…)` for an unreachable branch |
3146
+ | `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — every statement of the body an `expect(x)` under `toBeTruthy` / `toBeDefined` / `toBeInstanceOf` (or their negated twins), weighed against the tests that run the same setup: the rest of the block, and everything the blocks nested in it declare → delete it; the suggestion removes the test and the blank line above it. Silent where that test is the block's only running one, and a skipped sibling does not count as proof |
3130
3147
  | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
3131
3148
  | `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`, through `TestBed`, `getTestBed()`, a chain of those, or a name holding one → drop the `await`, and the `async` of a hook that then awaits nothing else; the suggestion does both. Reads **no types**, so it reports without `parserOptions.project`, where `@typescript-eslint/await-thenable` cannot. `inject` and `runInInjectionContext` are never reported: each answers whatever the token or the callback holds, which can be a promise |
3132
3149
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
@@ -3134,8 +3151,8 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3134
3151
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3135
3152
  | `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 |
3136
3153
 
3137
- Thirty-six rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3138
- `no-stub-class-double`, `no-structural-double` and `no-instance-lifecycle-spy`**; three fix on their own, eleven offer suggestions. Thirty-three are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
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
3139
3156
  `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3140
3157
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
3141
3158
  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
@@ -3408,7 +3425,7 @@ packages, which a subpath export can never be.
3408
3425
  | `injectActivatedRoute(): nothing provides ActivatedRoute here` | no `ActivatedRoute` provider in the injector it read | `provideActivatedRoute({ … })` from `vitest-auto-spy/angular-router` in `providers`; `injectActivatedRoute(fixture.debugElement.injector)` for a component-level one (§13) |
3409
3426
  | `the ActivatedRoute here is … not one provideActivatedRoute() built` | a later provider of `ActivatedRoute` won — `provideRouter()`, `RouterModule`, a `useValue`, `provideAutoSpy(ActivatedRoute)` | list `provideActivatedRoute()` last, or drop the other provider (§13) |
3410
3427
  | `provideActivatedRoute: the installed @angular/router does not wire ActivatedRoute …` | a router major builds `ActivatedRoute` with different internal constructors | report it with the `@angular/router` version; provide the route by hand until then (§13) |
3411
- | `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `getCurrentNavigation`, `isActive`, `resetConfig` | the double covers `url`, `events`, `navigate`, `navigateByUrl`, `createUrlTree`, `serializeUrl`, `parseUrl` and `routerState`; past that, a real `provideRouter([])` with `RouterTestingHarness` (§13) |
3428
+ | `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `isActive`, `resetConfig`, `lastSuccessfulNavigation` | the double covers `url`, `events`, `navigate`, `navigateByUrl`, `createUrlTree`, `serializeUrl`, `parseUrl`, `routerState` and `currentNavigation` / `getCurrentNavigation()`; past that, a real `provideRouter([])` with `RouterTestingHarness` (§13) |
3412
3429
  | `injectRouterDouble(): the Router here is Angular's own …` | `Router` is `providedIn: 'root'`, so a `TestBed` without the double hands out a real one | add `provideRouterDouble({ … })` from `vitest-auto-spy/angular-router` to `providers` (§13) |
3413
3430
 
3414
3431
  **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
package/README.md CHANGED
@@ -65,7 +65,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
65
65
  `mock.reset()`, so a `mock.fn()` your spec made by hand is untouched.
66
66
  - 🌐 `httpResource()` and `HttpClient` in two lines — `provideHttpTesting()` + `await expectRequest(url).flush(body)` does the tick, the controller, the `expectOne`, the flush **and the settling**, so `resource.value()` reads on the next line; `@angular/common` stays an optional peer, confined to the `/angular-http` entry
67
67
  - 🧭 An `ActivatedRoute` that cannot disagree with itself — `provideActivatedRoute({ params, queryParams, data })` provides Angular's own route, its streams, `paramMap`s and snapshot built from one record, and `injectActivatedRoute().setParams(…)` moves them together the way a navigation does; `@angular/router` stays an optional peer, confined to the `/angular-router` entry
68
- - 🧱 The providers every suite hand-rolls, provided once — `provideRouterDouble({ url })` for a `Router` that derives `url`, `routerState` and `events` from one URL rather than four guesses; `provideWindowDouble(WINDOW, { screen })` and `provideDocumentDouble({ … })`, which merge what the spec names **over** the real jsdom object, so the member nobody thought of still answers; `provideMatDialogData(MAT_DIALOG_DATA, data)` and `provideMatDialogRef(MatDialogRef)` for the Material dialog trio, with Material itself staying out of this package's dependencies
68
+ - 🧱 The providers every suite hand-rolls, provided once — `provideRouterDouble({ url })` for a `Router` that derives `url`, `routerState`, `events` and `currentNavigation()` from one URL rather than four guesses; `provideWindowDouble(WINDOW, { screen })` and `provideDocumentDouble({ … })`, which merge what the spec names **over** the real jsdom object, so the member nobody thought of still answers; `provideMatDialogData(MAT_DIALOG_DATA, data)` and `provideMatDialogRef(MatDialogRef)` for the Material dialog trio, with Material itself staying out of this package's dependencies
69
69
  - 🧪 A signal graph you can assert on — `mockSignalProp` writes **through** a member that is already a `signal()`, `model()` or `linkedSignal()` instead of replacing it, so a computed, an effect or a template that read it first is still connected; `setInputs(fixture, { … })` changes an input mid-test; `trackRecomputations` / `trackEffectRuns` count what actually re-ran; `runEffect` runs the previous cleanup first, the way Angular's own scheduler does
70
70
  - 🪆 NestJS units from their own DI metadata — `createNestUnit(Target, { expose, providers })`, the solitary / sociable model of `@suites/unit` with the real prototype behind every spy and no `@nestjs` dependency
71
71
  - 🧱 The providers a testing module cannot reach — `overrideComponentProvider` (which verifies the override actually applied), `provideAutoSpyForToken`, `assertNgModuleScopes`, `assertComponentDefIntact`, `createDirectiveHost`, and `createComponentStub` for a child whose selector and inputs are read off the real one, so the stub cannot drift
@@ -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-six rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, eleven 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-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
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
@@ -2856,7 +2856,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
2856
2856
  | `runEffect(effectRef)` | `/angular` | Run one `effect()` body on demand, for an effect whose trigger a spec replaced with a static signal |
2857
2857
  | `setInputs(fixture, inputs, opts?)` | `/angular` | Change a component input mid-test — one `setInput` per name, checked against the compiled definition, then one wait |
2858
2858
  | `trackRecomputations(signal)` / `trackEffectRuns(ref)` | `/angular` | Count what the reactive graph actually did: `{ count, stop() }`, so "the filter change did not re-run the sync effect" is an assertion |
2859
- | `provideRouterDouble(init?)` / `injectRouterDouble()` | `/angular-router` | A `Router` derived from one URL — real `serializeUrl` / `createUrlTree`, `navigate` spies, `emitNavigation()` over a `BehaviorSubject` |
2859
+ | `provideRouterDouble(init?)` / `injectRouterDouble()` | `/angular-router` | A `Router` derived from one URL — real `serializeUrl` / `createUrlTree`, `navigate` spies, `emitNavigation()` over a `BehaviorSubject`, `currentNavigation()` and `setCurrentNavigation()` |
2860
2860
  | `createForm(model, schema?)` / `registerFormMatchers()` | `/signal-forms` | A signal form built where `form()` can inject, and `expect(field).toHaveFieldErrors(['required'])` over what it produced |
2861
2861
  | `provideWindowDouble(TOKEN, over?)` / `provideDocumentDouble(over?)` | `/angular` | A `window` / `document` merged **over** the real jsdom one, so members the spec never named still answer; the globals stay untouched |
2862
2862
  | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(Ref, init?)` | `/angular` | The Material dialog trio without `@angular/material` as a dependency — the token and the ref class are arguments, `afterClosed()` still answers after the close |
@@ -3424,6 +3424,7 @@ export can never be.
3424
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
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
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 |
3427
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 |
3428
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` |
3429
3430
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
@@ -3435,7 +3436,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
3435
3436
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
3436
3437
  are versioned together and stop being re-written in every project that installs the package.
3437
3438
 
3438
- **Three of the thirty-six fix on their own, eleven offer suggestions**, and the split is not about how hard
3439
+ **Three of the thirty-seven fix on their own, twelve offer suggestions**, and the split is not about how hard
3439
3440
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
3440
3441
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
3441
3442
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -3518,7 +3519,17 @@ reason is for. The report sits on the directive's line, so that comment reaches
3518
3519
  `no-constant-expect` reports `expect(true).toBe(true)` and its relatives: a value the spec spelled out,
3519
3520
  under a matcher whose answer that value already fixes. `@vitest/eslint-plugin` has no such rule. On
3520
3521
  the same suite it reports four — two tests with nothing else to assert and two that import a barrel
3521
- for coverage. `no-compile-components` says nothing until `['error', { builder: 'inline-resources' }]`
3522
+ for coverage.
3523
+
3524
+ `no-redundant-smoke-test` reports the other test that cannot fail: one whose whole body is
3525
+ `expect(pipe).toBeTruthy()`, standing in a block that already has tests which run the same
3526
+ `beforeEach` — a nullish subject fails one of those first, on the call it was making, rather than on a
3527
+ line named after construction. It is weighed against the tests that share its setup, the ones nested
3528
+ `describe`s declare included, and it says nothing where that smoke test is the only running test of
3529
+ its block. Measured over an Angular suite of 1771 spec files: 569 findings in 540 of them, 515 still carrying the
3530
+ `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
+
3532
+ `no-compile-components` says nothing until `['error', { builder: 'inline-resources' }]`
3522
3533
  states that the builder inlines `templateUrl` / `styleUrls`; then `compileComponents()` is a promise
3523
3534
  already settled, and a suggestion deletes it together with the `async` of a hook that awaits nothing
3524
3535
  else — 449 calls in 411 files there, 435 of them with the edit. Not every one of them, though: a
@@ -3573,7 +3584,7 @@ package's own — it needs its ESLint integration switched on.
3573
3584
  ### WebStorm and the other JetBrains IDEs
3574
3585
 
3575
3586
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
3576
- all run ESLint natively, so the thirty-six rules appear inline, in the **Problems** tool window, and
3587
+ all run ESLint natively, so the thirty-seven rules appear inline, in the **Problems** tool window, and
3577
3588
  under **Code → Inspect Code** for the whole project.
3578
3589
 
3579
3590
  ```js
@@ -3590,7 +3601,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
3590
3601
  the fixes and suggestions live.
3591
3602
 
3592
3603
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
3593
- and then keep a second copy of thirty-six rules, in Kotlin, in step with the TypeScript ones.
3604
+ and then keep a second copy of thirty-seven rules, in Kotlin, in step with the TypeScript ones.
3594
3605
 
3595
3606
  ### VS Code, Cursor, Windsurf, VSCodium
3596
3607
 
@@ -3700,7 +3711,7 @@ another, and the next one is in that same file.
3700
3711
  | `expectNoRequest(matcher?, opts?)` / `verifyNoPendingRequests()` _(`/angular-http`)_ | Assert that nothing was requested / that the test left nothing unanswered |
3701
3712
  | `provideActivatedRoute(init?)` / `injectActivatedRoute(injector?)` _(`/angular-router`)_ | An `ActivatedRoute` provider whose streams, `paramMap`s and snapshot share one record, and the handle whose setters move them together |
3702
3713
  | `createActivatedRoute(init?)` _(`/angular-router`)_ | The same double without a `TestBed` — `.route` plus the setters |
3703
- | `provideRouterDouble(init?)` / `injectRouterDouble(injector?)` / `createRouterDouble(init?)` _(`/angular-router`)_ | A `Router` derived from one URL — `navigate` spies, `setUrl`, `emitNavigation`, and the router's own URL serialization |
3714
+ | `provideRouterDouble(init?)` / `injectRouterDouble(injector?)` / `createRouterDouble(init?)` _(`/angular-router`)_ | A `Router` derived from one URL — `navigate` spies, `setUrl`, `emitNavigation`, `setCurrentNavigation`, and the router's own URL serialization |
3704
3715
  | `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()` |
3705
3716
  | `registerSignalMatchers()` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)` |
3706
3717
  | `trackRecomputations(signal)` / `trackEffectRuns(effectRef)` _(Angular)_ | Count what the reactive graph re-ran — `{ count, stop() }`, undone by `restoreMockedProps()` |
@@ -1,5 +1,5 @@
1
1
  import { Type, Injector, FactoryProvider } from '@angular/core';
2
- import { Params, Data, UrlSegment, ActivatedRoute, Route, Router, Event } from '@angular/router';
2
+ import { Params, Data, UrlSegment, ActivatedRoute, Route, Navigation, Router, Event } from '@angular/router';
3
3
  import { g as AddSpyMethodsByReturnTypes } from './types-D3RkJjc_.js';
4
4
  import 'vitest';
5
5
 
@@ -114,8 +114,9 @@ declare function injectActivatedRoute(injector?: Injector): ActivatedRouteDouble
114
114
  * So the double keeps one URL and derives the rest of it: `url` is that URL serialized the way the
115
115
  * real router serializes it, `routerState` is Angular's own `RouterState` over a root route holding
116
116
  * its query parameters and fragment, `events` is a `BehaviorSubject` that starts at the
117
- * `NavigationEnd` which put the router there, and `createUrlTree` / `serializeUrl` / `parseUrl` are
118
- * the router's real URL work rather than stubs.
117
+ * `NavigationEnd` which put the router there, `currentNavigation()` is `null` because a router
118
+ * standing at a URL is idle, and `createUrlTree` / `serializeUrl` / `parseUrl` are the router's real
119
+ * URL work rather than stubs.
119
120
  *
120
121
  * Navigation itself stays a spy, because a navigation in an application is asynchronous, runs
121
122
  * guards and can be cancelled: a double that moved its own URL on `navigate()` would be testing the
@@ -125,10 +126,18 @@ declare function injectActivatedRoute(injector?: Injector): ActivatedRouteDouble
125
126
  * reason: `@angular/router` is an optional peer, paid for by the suites that import this entry.
126
127
  */
127
128
 
128
- /** Where the router starts. One field, because everything else about a router is derived from it. */
129
+ /** What a spec names about the navigation in flight; every other field is derived from where the router stands. */
130
+ type NavigationInit = Partial<Navigation>;
131
+ /** Where the router starts: the URL it stands at, and whether a navigation is running. */
129
132
  interface RouterDoubleInit {
130
133
  /** The URL the router reports until the spec moves it. Serialized as the router would. Default `'/'`. */
131
134
  url?: string;
135
+ /**
136
+ * The navigation `currentNavigation()` and `getCurrentNavigation()` answer with. Default `null` —
137
+ * a router standing at a URL is idle, which is what the real one answers between navigations.
138
+ * Named here rather than only on the handle because a component reads it in a field initializer.
139
+ */
140
+ currentNavigation?: NavigationInit | null;
132
141
  }
133
142
  /** The handle a spec drives the router through. */
134
143
  interface RouterDouble {
@@ -140,6 +149,12 @@ interface RouterDouble {
140
149
  readonly navigateByUrl: AddSpyMethodsByReturnTypes<Router['navigateByUrl']>;
141
150
  /** Put the router at a URL: `url`, `routerState` and the root route move together. */
142
151
  setUrl(url: string): void;
152
+ /**
153
+ * Put a navigation in flight, or end it with `null`. What the spec names is kept; `id`,
154
+ * `initialUrl` and `extractedUrl` come from where the router stands, `trigger` is `'imperative'`
155
+ * and `extras` is empty.
156
+ */
157
+ setCurrentNavigation(navigation?: NavigationInit | null): void;
143
158
  /** Push an event through `router.events`. A `NavigationEnd`, or a URL, moves the URL with it. */
144
159
  emitNavigation(event?: Event | string): void;
145
160
  }
@@ -182,4 +197,4 @@ declare function provideRouterDouble(init?: RouterDoubleInit): FactoryProvider;
182
197
  */
183
198
  declare function injectRouterDouble(injector?: Injector): RouterDouble;
184
199
 
185
- export { type ActivatedRouteChange, type ActivatedRouteDouble, type ActivatedRouteInit, type RouterDouble, type RouterDoubleInit, createActivatedRoute, createRouterDouble, injectActivatedRoute, injectRouterDouble, provideActivatedRoute, provideRouterDouble };
200
+ export { type ActivatedRouteChange, type ActivatedRouteDouble, type ActivatedRouteInit, type NavigationInit, type RouterDouble, type RouterDoubleInit, createActivatedRoute, createRouterDouble, injectActivatedRoute, injectRouterDouble, provideActivatedRoute, provideRouterDouble };
@@ -2,8 +2,9 @@ import { createFunctionSpy } from './chunk-NQNHA3R4.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';
5
- import { Router, DefaultUrlSerializer, PRIMARY_OUTLET, ActivatedRoute, RouterState, NavigationEnd, ActivatedRouteSnapshot, RouterStateSnapshot, createUrlTreeFromSnapshot, UrlSegment } from '@angular/router';
5
+ import { Router, DefaultUrlSerializer, PRIMARY_OUTLET, ActivatedRoute, RouterState, NavigationEnd, ActivatedRouteSnapshot, RouterStateSnapshot, NavigationStart, NavigationCancel, NavigationError, NavigationSkipped, UrlSegment, createUrlTreeFromSnapshot } from '@angular/router';
6
6
  import { BehaviorSubject } from 'rxjs';
7
+ import { signal } from '@angular/core';
7
8
 
8
9
  var doubles = /* @__PURE__ */ new WeakMap();
9
10
  function toSegments(url) {
@@ -182,8 +183,16 @@ function injectActivatedRoute(injector) {
182
183
  }
183
184
  return double;
184
185
  }
185
- var COVERED = "url, events, navigate, navigateByUrl, createUrlTree, serializeUrl, parseUrl and routerState";
186
- var DECLARED = new Set(Object.getOwnPropertyNames(Router.prototype));
186
+ var COVERED = "url, events, navigate, navigateByUrl, createUrlTree, serializeUrl, parseUrl, routerState, currentNavigation and getCurrentNavigation";
187
+ var DECLARED_FIELDS = [
188
+ "navigated",
189
+ "routeReuseStrategy",
190
+ "onSameUrlNavigation",
191
+ "config",
192
+ "componentInputBindingEnabled",
193
+ "currentNavigation"
194
+ ];
195
+ var DECLARED = /* @__PURE__ */ new Set([...Object.getOwnPropertyNames(Router.prototype), ...DECLARED_FIELDS]);
187
196
  var doubles2 = /* @__PURE__ */ new WeakMap();
188
197
  var serializer = new DefaultUrlSerializer();
189
198
  function stateSnapshot(url, root) {
@@ -237,16 +246,39 @@ function createUrlState(initial) {
237
246
  }
238
247
  };
239
248
  }
240
- function createRouterDouble(init = {}) {
241
- const state = createUrlState(init.url ?? "/");
242
- const events = new BehaviorSubject(new NavigationEnd(1, state.url(), state.url()));
243
- const stream = events.asObservable();
244
- let navigationId = 1;
245
- const navigate = createFunctionSpy("Router.navigate");
246
- const navigateByUrl = createFunctionSpy("Router.navigateByUrl");
247
- navigate.resolveWith(true);
248
- navigateByUrl.resolveWith(true);
249
- const router = guardMissingMembers({
249
+ function startEvent(event, inFlight, navigationOf) {
250
+ if (!(event instanceof NavigationStart)) {
251
+ return void 0;
252
+ }
253
+ inFlight.set(
254
+ navigationOf({
255
+ id: event.id,
256
+ initialUrl: serializer.parse(event.url),
257
+ extractedUrl: serializer.parse(event.url),
258
+ trigger: event.navigationTrigger ?? "imperative"
259
+ })
260
+ );
261
+ return event.id;
262
+ }
263
+ function endsNavigation(event) {
264
+ return event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError || event instanceof NavigationSkipped;
265
+ }
266
+ function buildNavigation(state, id, named) {
267
+ return {
268
+ id,
269
+ initialUrl: state.tree(),
270
+ extractedUrl: state.tree(),
271
+ trigger: "imperative",
272
+ extras: {},
273
+ previousNavigation: null,
274
+ // A no-op rather than a spy: a spec that asserts the abort passes its own, and every other spec
275
+ // would be carrying a spy nothing reads.
276
+ abort: () => void 0,
277
+ ...named
278
+ };
279
+ }
280
+ function buildRouter(state, stream, parts) {
281
+ return guardMissingMembers({
250
282
  get url() {
251
283
  return state.url();
252
284
  },
@@ -256,8 +288,8 @@ function createRouterDouble(init = {}) {
256
288
  get routerState() {
257
289
  return state.routerState;
258
290
  },
259
- navigate,
260
- navigateByUrl,
291
+ navigate: parts.navigate,
292
+ navigateByUrl: parts.navigateByUrl,
261
293
  createUrlTree: (commands, extras = {}) => createUrlTreeFromSnapshot(
262
294
  (extras.relativeTo ?? state.routerState.root).snapshot,
263
295
  commands,
@@ -266,22 +298,47 @@ function createRouterDouble(init = {}) {
266
298
  ),
267
299
  serializeUrl: (target) => serializer.serialize(target),
268
300
  parseUrl: (target) => serializer.parse(target),
301
+ get currentNavigation() {
302
+ return parts.currentNavigation;
303
+ },
304
+ getCurrentNavigation: () => parts.currentNavigation(),
269
305
  // Inert, and present rather than guarded: Angular's injector reads `ngOnDestroy` off every
270
306
  // value it builds, so a double that threw on it would throw on `TestBed.resetTestingModule()`.
271
307
  ngOnDestroy: () => void 0,
272
308
  dispose: () => void 0
273
309
  });
310
+ }
311
+ function createRouterDouble(init = {}) {
312
+ const state = createUrlState(init.url ?? "/");
313
+ const events = new BehaviorSubject(new NavigationEnd(1, state.url(), state.url()));
314
+ const stream = events.asObservable();
315
+ let navigationId = 1;
316
+ const navigationOf = (named) => buildNavigation(state, navigationId, named);
317
+ const inFlight = signal(init.currentNavigation ? navigationOf(init.currentNavigation) : null);
318
+ const currentNavigation = inFlight.asReadonly();
319
+ const navigate = createFunctionSpy("Router.navigate");
320
+ const navigateByUrl = createFunctionSpy("Router.navigateByUrl");
321
+ navigate.resolveWith(true);
322
+ navigateByUrl.resolveWith(true);
323
+ const router = buildRouter(state, stream, { navigate, navigateByUrl, currentNavigation });
274
324
  const double = {
275
325
  router,
276
326
  navigate,
277
327
  navigateByUrl,
278
328
  setUrl: state.set,
329
+ setCurrentNavigation: (navigation) => {
330
+ inFlight.set(navigation === null ? null : navigationOf(navigation ?? {}));
331
+ },
279
332
  emitNavigation: (event) => {
280
333
  const next = typeof event === "object" ? event : new NavigationEnd(++navigationId, event ?? state.url(), event ?? state.url());
281
334
  if (next instanceof NavigationEnd) {
282
335
  state.set(next.urlAfterRedirects);
283
336
  }
337
+ navigationId = startEvent(next, inFlight, navigationOf) ?? navigationId;
284
338
  events.next(next);
339
+ if (endsNavigation(next)) {
340
+ inFlight.set(null);
341
+ }
285
342
  }
286
343
  };
287
344
  doubles2.set(router, double);
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-67EcdZwp.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-BoA6j-74.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';
@@ -841,12 +841,31 @@ declare function registerSignalMatchers(): void;
841
841
  * `InjectionToken<AppWindow>` gets its own members checked in the overrides too.
842
842
  */
843
843
 
844
+ /** How many levels of slicing the type follows. Beyond it a member takes its whole type. */
845
+ type Levels = [never, 0, 1, 2, 3];
846
+ /**
847
+ * One member's override: the member's own type, or a slice of it wherever it is a plain-ish object.
848
+ *
849
+ * A method is matched whole and **first**: `Partial<(…) => …>` is a mapped type over a function,
850
+ * which drops the call signature and leaves `{}` — a slice branch that accepts anything at all. An
851
+ * array is matched whole too: slicing one by index describes nothing a spec means.
852
+ */
853
+ type PlatformOverride<V, Depth extends number> = V extends (...args: never[]) => unknown ? V : V extends readonly unknown[] ? V : V extends object ? Depth extends 0 ? V : V | {
854
+ [K in keyof V]?: PlatformOverride<V[K], Levels[Depth]>;
855
+ } : V;
844
856
  /**
845
857
  * What a spec says about a platform object: a value per member, and a **slice** wherever the member
846
858
  * is itself an object — `{ screen: { width: 1920 } }` leaves `screen.colorDepth` real.
859
+ *
860
+ * The slice goes as deep as the merge does, three levels of it: the proxy re-merges every plain
861
+ * object it hands out, so `{ document: { location: { href: '' } } }` works at run time, and a type
862
+ * that stopped at the first level sent a spec that wrote it to `createMock<Location>()` or to a
863
+ * cast for no reason anybody could state. Depth is bounded because `Window` is recursive —
864
+ * `window.window` is a `Window` — and an unbounded mapped type over it is a bill every call site
865
+ * pays.
847
866
  */
848
867
  type PlatformOverrides<T> = {
849
- [K in keyof T]?: T[K] extends (...args: never[]) => unknown ? T[K] : T[K] extends object ? Partial<T[K]> | T[K] : T[K];
868
+ [K in keyof T]?: PlatformOverride<T[K], 3>;
850
869
  };
851
870
  /**
852
871
  * The `window` double without a `TestBed` — for a class built with `new`, or a plain function.
@@ -3,7 +3,7 @@ export { ArgCaptor, AsInstances, AssertMockedOptions, ConstructorMock, Construct
3
3
  export { A as AutoMockConfiguration, a as AutoSpyDefaultEntry, C as CallbackSubscribable, E as EmissionObserver, b as EmissionOptions, c as EmissionSource, S as SubscribableLike, d as autoMocked, e as clearAutoSpyDefaults, f as createAutoMock, g as expectCompletion, h as expectEmission, i as expectEmissions, j as expectError, k as expectNoEmission, r as registerAutoSpyDefaults, s as setEmissionTimeout } from './expect-emission-CmN9ADyN.js';
4
4
  export { c as createFunctionSpy, d as describeDuplicateCopies, g as getPackageCopies } from './package-identity-lm-gqZAR.js';
5
5
  export { A as AccessorImplementations, O as OutsideHookReaction, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as reportPropsOutsideHooks, e as restoreMockedProps } from './prop-mock-DnYNBNJL.js';
6
- export { 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, 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-67EcdZwp.js';
6
+ export { 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, 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';
7
7
  import 'vitest';
8
8
  import '@angular/core';
9
9
  import '@angular/core/testing';
package/dist/cli.js CHANGED
@@ -2453,7 +2453,7 @@ function checkForeignPragma(graph) {
2453
2453
  }
2454
2454
 
2455
2455
  // src/cli/checks/export-map.generated.ts
2456
- var EXPORT_MAP_VERSION = "5.9.0";
2456
+ var EXPORT_MAP_VERSION = "5.10.1";
2457
2457
  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-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";
2458
2458
  var EXPORTED_BY = {
2459
2459
  AccessorImplementations: "0 1 2 3 4 12 17 18 19",
@@ -2671,6 +2671,7 @@ var EXPORTED_BY = {
2671
2671
  mutationRecord: "7",
2672
2672
  MutationRecordInit: "7",
2673
2673
  narrow: "0 1 2 3 4 17 18 19",
2674
+ NavigationInit: "14",
2674
2675
  NestModuleRef: "16",
2675
2676
  NestUnit: "16",
2676
2677
  NestUnitClass: "16",
@@ -3484,7 +3485,9 @@ Options
3484
3485
  take (2), and the absolute floor under which it is noise
3485
3486
  (500 ms).
3486
3487
  --top <n> perf only. Rows in the "slowest files" and "slowest bodies"
3487
- tables. 0 turns them off.
3488
+ tables. 0 turns them off. Asked for explicitly, a run whose
3489
+ slowest file is under the one-second floor says so rather
3490
+ than printing nothing.
3488
3491
  --check init only. Write nothing; exit 1 if the block is out of date.
3489
3492
  --dry-run init only. Print what would change and write nothing.
3490
3493
  --uninstall init only. Remove the managed blocks and the files it created.
@@ -4389,6 +4392,10 @@ function formatHotspots(run, cwd, options = {}) {
4389
4392
  }
4390
4393
  return [...fileSection(files, totalTestMs(run, cwd), width), ...caseSection(caseHotspots(run, cwd, limit), width)].join("\n\n");
4391
4394
  }
4395
+ function hotspotFloorNote(run, cwd, floorMs = HOTSPOT_DEFAULTS.floorMs) {
4396
+ const [slowest] = fileHotspots(run, cwd, 1);
4397
+ return slowest === void 0 ? "No hotspot tables: no file in this run finished a test body, so there is nothing to rank." : `No hotspot tables: the slowest file spent ${formatMs(slowest.ms)} in its test bodies, under the ${formatMs(floorMs)} floor. Below it a ranking is the reader\u2019s attention spent on the machine rather than on a decision somebody made.`;
4398
+ }
4392
4399
  var UNIT_TEST_BUILDER2 = "@angular/build:unit-test";
4393
4400
  var WORKSPACE_FILES = ["angular.json", "workspace.json"];
4394
4401
  function targetsOf(project) {
@@ -4548,7 +4555,7 @@ var CONFIG_FILE = /(?:^|\/)vite(?:st)?[\w.-]*\.config\.[cm]?[jt]s$/;
4548
4555
  var NO_ISOLATION = /\bisolate\s*:\s*false/;
4549
4556
  var JSDOM = /\benvironment\s*:\s*["'`]jsdom["'`]/;
4550
4557
  var HAPPY_DOM = /happy-dom/;
4551
- var WORKER_CAP = /\bmaxWorkers\s*:/;
4558
+ var WORKER_CAP = /\bmaxWorkers\s*[,:=}]|\bmaxWorkers\s*$/m;
4552
4559
  var LARGE_RUN_MS = 6e4;
4553
4560
  var WORKER_RSS_MB = 155;
4554
4561
  function remainder(total) {
@@ -4820,6 +4827,18 @@ function recordBaseline(run, cwd, path, io) {
4820
4827
  perf baseline: recorded ${Object.keys(baseline.files).length} files at a median of ${formatMs(baseline.median)} into ${path}. Commit it \u2014 it is only worth what the next reader can diff.`
4821
4828
  );
4822
4829
  }
4830
+ function reportHotspots(run, cwd, top, io) {
4831
+ const hotspots = formatHotspots(run, cwd, top === void 0 ? {} : { limit: top });
4832
+ if (hotspots !== "") {
4833
+ io.out(`
4834
+ ${hotspots}`);
4835
+ return;
4836
+ }
4837
+ if (top !== void 0 && top > 0) {
4838
+ io.out(`
4839
+ ${hotspotFloorNote(run, cwd)}`);
4840
+ }
4841
+ }
4823
4842
  function renderPerf(source, profile, io, options = {}) {
4824
4843
  const gate = options.gate;
4825
4844
  if (!source.ok) {
@@ -4848,11 +4867,7 @@ function renderPerf(source, profile, io, options = {}) {
4848
4867
  `);
4849
4868
  }
4850
4869
  io.out(formatPhases(analysis.phases));
4851
- const hotspots = formatHotspots(source.run, profile.cwd, options.top === void 0 ? {} : { limit: options.top });
4852
- if (hotspots !== "") {
4853
- io.out(`
4854
- ${hotspots}`);
4855
- }
4870
+ reportHotspots(source.run, profile.cwd, options.top, io);
4856
4871
  reportFindings(analysis, io);
4857
4872
  if (options.baseline?.update === true) {
4858
4873
  recordBaseline(source.run, profile.cwd, options.baseline.path, io);
@@ -757,14 +757,22 @@ function factoryBody(context, value) {
757
757
  }
758
758
  var SPY_FACTORIES = /* @__PURE__ */ new Set([
759
759
  "autoMocked",
760
+ "createActivatedRoute",
760
761
  "createAutoMock",
762
+ "createDocumentDouble",
761
763
  "createMock",
764
+ "createRouterDouble",
762
765
  "createSpyClass",
763
766
  "createSpyFromClass",
767
+ "createWindowDouble",
764
768
  "mockConstructor",
765
769
  "mockDeep",
770
+ "provideActivatedRoute",
766
771
  "provideAutoSpy",
767
- "provideAutoSpyForToken"
772
+ "provideAutoSpyForToken",
773
+ "provideDocumentDouble",
774
+ "provideRouterDouble",
775
+ "provideWindowDouble"
768
776
  ]);
769
777
  function isFactoryCall(node) {
770
778
  return isCallExpression(node) && isIdentifier(node.callee) && SPY_FACTORIES.has(node.callee.name);
@@ -2067,6 +2075,206 @@ var preferProvideActivatedRoute = defineRule({
2067
2075
  })
2068
2076
  });
2069
2077
 
2078
+ // src/lib/eslint/smoke-test.ts
2079
+ var TESTS = /* @__PURE__ */ new Set(["fit", "it", "test", "xit", "xtest"]);
2080
+ var SKIPPED_TESTS = /* @__PURE__ */ new Set(["xit", "xtest"]);
2081
+ var SKIPPED_MEMBERS = /* @__PURE__ */ new Set(["skip", "todo"]);
2082
+ var EXISTS = /* @__PURE__ */ new Set(["toBeDefined", "toBeInstanceOf", "toBeTruthy"]);
2083
+ var MISSING = /* @__PURE__ */ new Set(["toBeFalsy", "toBeNull", "toBeUndefined"]);
2084
+ function calleeChain(node) {
2085
+ const members = [];
2086
+ let current = node.callee;
2087
+ for (; ; ) {
2088
+ if (isIdentifier(current)) {
2089
+ return { members, root: current.name };
2090
+ }
2091
+ if (isMemberExpression(current)) {
2092
+ const name = memberName(current);
2093
+ if (name === void 0) {
2094
+ return void 0;
2095
+ }
2096
+ members.unshift(name);
2097
+ current = current.object;
2098
+ continue;
2099
+ }
2100
+ if (!isCallExpression(current)) {
2101
+ return void 0;
2102
+ }
2103
+ current = current.callee;
2104
+ }
2105
+ }
2106
+ var TITLES = /* @__PURE__ */ new Set(["Literal", "TemplateLiteral"]);
2107
+ function testCall(node) {
2108
+ const chain = calleeChain(node);
2109
+ const [title] = node.arguments;
2110
+ if (!chain || !TESTS.has(chain.root) || !title || !TITLES.has(title.type)) {
2111
+ return void 0;
2112
+ }
2113
+ return { skipped: SKIPPED_TESTS.has(chain.root) || chain.members.some((member) => SKIPPED_MEMBERS.has(member)) };
2114
+ }
2115
+ var SUBJECT_BUILDERS = /* @__PURE__ */ new Set(["get", "inject"]);
2116
+ function containsCall(node) {
2117
+ let current = node;
2118
+ while (isMemberExpression(current)) {
2119
+ if (isCallExpression(current.object)) {
2120
+ return true;
2121
+ }
2122
+ current = current.object;
2123
+ }
2124
+ return isCallExpression(current);
2125
+ }
2126
+ function namesSubject(subject, matcher) {
2127
+ if (isIdentifier(subject)) {
2128
+ return true;
2129
+ }
2130
+ if (isMemberExpression(subject)) {
2131
+ return !containsCall(subject);
2132
+ }
2133
+ if (!isCallExpression(subject)) {
2134
+ return false;
2135
+ }
2136
+ if (matcher === "toBeInstanceOf") {
2137
+ return false;
2138
+ }
2139
+ if (isIdentifier(subject.callee)) {
2140
+ return subject.arguments.length === 0;
2141
+ }
2142
+ const [token, ...rest] = subject.arguments;
2143
+ return isMemberExpression(subject.callee) && SUBJECT_BUILDERS.has(memberName(subject.callee) ?? "") && !containsCall(subject.callee.object) && rest.length === 0 && token !== void 0 && (isIdentifier(token) || isMemberExpression(token) && !containsCall(token));
2144
+ }
2145
+ function subjectReference(subject, context) {
2146
+ if (isIdentifier(subject) || isMemberExpression(subject)) {
2147
+ return context.sourceCode.getText(subject);
2148
+ }
2149
+ let current = subject;
2150
+ while (isCallExpression(current) || isMemberExpression(current)) {
2151
+ current = isCallExpression(current) ? current.callee : current.object;
2152
+ }
2153
+ return isIdentifier(current) ? current.name : void 0;
2154
+ }
2155
+ function existenceSubject(expression) {
2156
+ if (!isCallExpression(expression) || !isMemberExpression(expression.callee)) {
2157
+ return void 0;
2158
+ }
2159
+ const matcher = memberName(expression.callee);
2160
+ let target = expression.callee.object;
2161
+ let negated = false;
2162
+ while (isMemberExpression(target) && memberName(target) === "not") {
2163
+ negated = !negated;
2164
+ target = target.object;
2165
+ }
2166
+ if (!matcher || !isCallExpression(target) || !isIdentifier(target.callee) || target.callee.name !== "expect") {
2167
+ return void 0;
2168
+ }
2169
+ const [subject] = target.arguments;
2170
+ if (!subject || !(negated ? MISSING.has(matcher) : EXISTS.has(matcher))) {
2171
+ return void 0;
2172
+ }
2173
+ return namesSubject(subject, matcher) ? subject : void 0;
2174
+ }
2175
+ function existenceOnly(callback) {
2176
+ const body = callback.body;
2177
+ if (!isBlockStatement(body)) {
2178
+ return existenceSubject(body);
2179
+ }
2180
+ const subjects = body.body.map((statement) => isExpressionStatement(statement) ? existenceSubject(statement.expression) : void 0);
2181
+ return subjects.length > 0 && subjects.every(Boolean) ? subjects[0] : void 0;
2182
+ }
2183
+ function removal2(context, statement) {
2184
+ return (fixer) => {
2185
+ const text = context.sourceCode.getText();
2186
+ let start = statement.range[0];
2187
+ while (start > 0 && /\s/.test(text.charAt(start - 1))) {
2188
+ start -= 1;
2189
+ }
2190
+ return fixer.replaceTextRange([start, statement.range[1]], "");
2191
+ };
2192
+ }
2193
+ function recordName(names, node, context) {
2194
+ const text = context.sourceCode.getText(node);
2195
+ for (let scope = enclosingFunction(node); scope; scope = enclosingFunction(scope.parent)) {
2196
+ const seen = names.get(scope) ?? /* @__PURE__ */ new Set();
2197
+ seen.add(text);
2198
+ names.set(scope, seen);
2199
+ }
2200
+ }
2201
+ function countNested(blocks) {
2202
+ blocks.forEach((block, scope) => {
2203
+ let current = scope;
2204
+ for (; ; ) {
2205
+ const above = blocks.get(current);
2206
+ if (above) {
2207
+ above.below += block.proving;
2208
+ above.running.push(...block.running);
2209
+ }
2210
+ if (current === void 0) {
2211
+ return;
2212
+ }
2213
+ current = enclosingFunction(current.parent);
2214
+ }
2215
+ });
2216
+ }
2217
+ function reportBlock(context, block, names) {
2218
+ if (block.below === 0) {
2219
+ return;
2220
+ }
2221
+ const referenced = new Set(block.running.flatMap((callback) => [...names.get(callback) ?? []]));
2222
+ block.smoke.forEach(({ node, subject }) => {
2223
+ const reference = subjectReference(subject, context);
2224
+ if (reference !== void 0 && !referenced.has(reference)) {
2225
+ return;
2226
+ }
2227
+ const data = {
2228
+ siblings: block.below === 1 ? "another test under the same setup already runs against it" : `${block.below} other tests under the same setup already run against it`,
2229
+ subject: context.sourceCode.getText(subject)
2230
+ };
2231
+ const statement = node.parent;
2232
+ const report = { data, messageId: "noRedundantSmokeTest", node };
2233
+ context.report(
2234
+ isExpressionStatement(statement) ? { ...report, suggest: [{ desc: "Remove this test", fix: removal2(context, statement) }] } : report
2235
+ );
2236
+ });
2237
+ }
2238
+ var noRedundantSmokeTest = defineRule({
2239
+ anchor: "-a-promise-a-test-forgets-to-await",
2240
+ description: "Delete a test whose whole body asserts that the subject exists, where the block already has tests that use it",
2241
+ hasSuggestions: true,
2242
+ messages: {
2243
+ noRedundantSmokeTest: "The whole of this test is `{{subject}}` existing, and {{siblings}}: they run the same `beforeEach`, so a subject that came back nullish fails them first, and names what it was doing when it did. This one cannot fail on its own and covers nothing \u2014 it is a green line in the report standing in for a test. Delete it. If the construction is what the spec is about \u2014 a factory that rejects a bad config, a constructor that reads an optional token \u2014 assert on that instead: `expect(() => new Subject(null)).toThrow(\u2026)`, or on the value the subject produced."
2244
+ },
2245
+ create: (context) => {
2246
+ const blocks = /* @__PURE__ */ new Map();
2247
+ const names = /* @__PURE__ */ new Map();
2248
+ return {
2249
+ Identifier: (node) => recordName(names, node, context),
2250
+ MemberExpression: (node) => recordName(names, node, context),
2251
+ CallExpression: (node) => {
2252
+ const test = testCall(node);
2253
+ if (!test) {
2254
+ return;
2255
+ }
2256
+ const scope = enclosingFunction(node);
2257
+ const block = blocks.get(scope) ?? { below: 0, proving: 0, running: [], smoke: [] };
2258
+ const callback = node.arguments.find(isFunctionNode);
2259
+ const subject = callback && existenceOnly(callback);
2260
+ if (subject) {
2261
+ block.smoke.push({ node, subject });
2262
+ } else if (!test.skipped) {
2263
+ block.proving += 1;
2264
+ if (callback) {
2265
+ block.running.push(callback);
2266
+ }
2267
+ }
2268
+ blocks.set(scope, block);
2269
+ },
2270
+ "Program:exit": () => {
2271
+ countNested(blocks);
2272
+ blocks.forEach((block) => reportBlock(context, block, names));
2273
+ }
2274
+ };
2275
+ }
2276
+ });
2277
+
2070
2278
  // src/lib/eslint/stub-class.ts
2071
2279
  function countStubFields(node) {
2072
2280
  return node.body.body.filter((member) => isStubField(member)).length;
@@ -2274,10 +2482,13 @@ function answersTheTestBed(context, node, seen) {
2274
2482
  const link = memberName(node.callee);
2275
2483
  return link !== void 0 && ANSWERS_THE_TEST_BED.has(link) && answersTheTestBed(context, node.callee.object, seen);
2276
2484
  }
2277
- function removal2(context, awaited) {
2485
+ function answerOf(member) {
2486
+ return ANSWERS_A_FIXTURE.has(member) ? "the `ComponentFixture`" : "the TestBed";
2487
+ }
2488
+ function removal3(context, awaited, answers) {
2278
2489
  const callback = asyncOnlyFor(awaited);
2279
2490
  return {
2280
- desc: "Remove the await \u2014 the TestBed call answers the TestBed, not a promise",
2491
+ desc: `Remove the await \u2014 the TestBed call answers ${answers}, not a promise`,
2281
2492
  fix: (fixer) => {
2282
2493
  const fixes = [fixer.replaceText(awaited, context.sourceCode.getText(awaited.argument))];
2283
2494
  if (callback) {
@@ -2292,7 +2503,7 @@ var noSyncTestbedAwait = defineRule({
2292
2503
  description: "Drop the await on a TestBed call that answers the TestBed or a fixture rather than a promise",
2293
2504
  hasSuggestions: true,
2294
2505
  messages: {
2295
- noSyncTestbedAwait: "`{{member}}(\u2026)` answers the TestBed, not a promise, so this `await` waits for nothing. Every `configureTestingModule` and `override*` returns `TestBed` itself \u2014 that is what lets them chain \u2014 and `createComponent` / `getLastFixture` return the `ComponentFixture`; awaiting one of those only resumes the hook a microtask later, while making a synchronous setup read as an asynchronous one. Drop the `await`, and the `async` of a `beforeEach` / `beforeAll` / `afterEach` / `afterAll` / `it` / `test` callback that then awaits nothing else \u2014 the hook is not asynchronous any more, and leaving the `async` behind is the half `@typescript-eslint/require-await` would still have to report. The TestBed calls that really do return a promise keep their `await`: `compileComponents()`, and on a fixture `whenStable()`, `whenRenderingDone()` and `getDeferBlocks()`. `TestBed.inject(TOKEN)` and `TestBed.runInInjectionContext(fn)` are never reported \u2014 each answers whatever the token or the callback holds, which is a promise often enough to be awaited on purpose."
2506
+ noSyncTestbedAwait: "`{{member}}(\u2026)` answers {{answers}}, not a promise, so this `await` waits for nothing. Every `configureTestingModule` and `override*` returns `TestBed` itself \u2014 that is what lets them chain \u2014 and `createComponent` / `getLastFixture` return the `ComponentFixture`; awaiting one of those only resumes the hook a microtask later, while making a synchronous setup read as an asynchronous one. Drop the `await`, and the `async` of a `beforeEach` / `beforeAll` / `afterEach` / `afterAll` / `it` / `test` callback that then awaits nothing else \u2014 the hook is not asynchronous any more, and leaving the `async` behind is the half `@typescript-eslint/require-await` would still have to report. The TestBed calls that really do return a promise keep their `await`: `compileComponents()`, and on a fixture `whenStable()`, `whenRenderingDone()` and `getDeferBlocks()`. `TestBed.inject(TOKEN)` and `TestBed.runInInjectionContext(fn)` are never reported \u2014 each answers whatever the token or the callback holds, which is a promise often enough to be awaited on purpose."
2296
2507
  },
2297
2508
  create: (context) => ({
2298
2509
  AwaitExpression: (node) => {
@@ -2307,7 +2518,8 @@ var noSyncTestbedAwait = defineRule({
2307
2518
  if (!answersTheTestBed(context, argument.callee.object, /* @__PURE__ */ new Set())) {
2308
2519
  return;
2309
2520
  }
2310
- context.report({ node, messageId: "noSyncTestbedAwait", data: { member }, suggest: [removal2(context, node)] });
2521
+ const answers = answerOf(member);
2522
+ context.report({ node, messageId: "noSyncTestbedAwait", data: { member, answers }, suggest: [removal3(context, node, answers)] });
2311
2523
  }
2312
2524
  })
2313
2525
  });
@@ -2924,6 +3136,7 @@ var rules = {
2924
3136
  "no-constant-expect": noConstantExpect,
2925
3137
  "no-compile-components": noCompileComponents,
2926
3138
  "no-sync-testbed-await": noSyncTestbedAwait,
3139
+ "no-redundant-smoke-test": noRedundantSmokeTest,
2927
3140
  ...jasmineRules
2928
3141
  };
2929
3142
 
@@ -2962,6 +3175,7 @@ var recommendedRules = {
2962
3175
  [`${PLUGIN_NAME}/no-constant-expect`]: "error",
2963
3176
  [`${PLUGIN_NAME}/no-compile-components`]: "error",
2964
3177
  [`${PLUGIN_NAME}/no-sync-testbed-await`]: "error",
3178
+ [`${PLUGIN_NAME}/no-redundant-smoke-test`]: "error",
2965
3179
  [`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
2966
3180
  [`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
2967
3181
  [`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
@@ -207,8 +207,18 @@ declare function createWithAutoSpies<T>(target: Type<T>, options?: CreateWithAut
207
207
  * signals and DI — everything such a spec actually reads.
208
208
  */
209
209
 
210
- /** The value `componentRef.setInput` expects for a member — signal inputs are set with the value, not the signal. */
211
- type InputValue<Member> = Member extends InputSignalWithTransform<unknown, infer Write> ? Write : Member extends InputSignal<infer Value> ? Value : Member extends ModelSignal<infer Value> ? Value : Member;
210
+ /**
211
+ * The value `componentRef.setInput` expects for a member — signal inputs are set with the value, not
212
+ * the signal.
213
+ *
214
+ * The read type in the pattern is what makes a transform input work.
215
+ * `input(false, { transform: booleanAttribute })` is an `InputSignalWithTransform<boolean, unknown>`,
216
+ * and against a pattern whose read type is fixed to `unknown` that match **fails**: the node behind
217
+ * the signal carries the read type in both directions, so a wider one is not a supertype, and
218
+ * `never` matches nothing at all. The member fell through to itself, and the spec was told to pass
219
+ * an `InputSignalWithTransform` where the component takes a boolean.
220
+ */
221
+ type InputValue<Member> = Member extends InputSignalWithTransform<any, infer Write> ? Write : Member extends InputSignal<infer Value> ? Value : Member extends ModelSignal<infer Value> ? Value : Member;
212
222
  /** The inputs of a component, keyed as declared but typed as the values `setInput` takes. */
213
223
  type ComponentInputs<T> = {
214
224
  [Key in keyof T]?: InputValue<T[Key]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.9.0",
3
+ "version": "5.10.1",
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",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vitest-auto-spy
3
- description: Write or fix tests that use vitest-auto-spy — typed spies on Vitest, bun:test, node:test and Rstest. Use when a spec imports `vitest-auto-spy` or a subpath (`/angular`, `/bun-angular`, `/bun`, `/node`, `/rstest`, `/rxjs`, `/nestjs`, `/jasmine`, `/setup`, `/zone`, `/eslint-plugin`), the user names createSpyFromClass, createAutoMock, createMock, mockDeep, createSpyObj, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, createDirectiveHost, overrideComponentProvider, enableAngularDiagnostics, trackInjections, runEffect, settleResource, httpResource, mockResourceProp, mockSignalProp, mockReadonlyProp, registerAutoSpyDefaults, provideActivatedRoute, createComponentStub, stubWebStorage, assertNoShadowedProviders, createSpyFromInstance, explainSpy, expectEmission, setupAutoSpy, takeStrictViolations, guardStrayConsole, setSpyEngine, assertMocked, Spy<T>, calledWith, mustBeCalledWith, onlyMethodsToSpyOn, onUnstubbedCall, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator (createSpyObject, mockProvider, SpectatorService), or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not on the class prototype", "strict mode is on", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController", "localStorage.setItem is not a function" or "Spy<T> is not assignable".
3
+ description: Write or fix tests that use vitest-auto-spy — typed spies on Vitest, bun:test, node:test and Rstest. Use when a spec imports `vitest-auto-spy` or a subpath (`/angular`, `/bun-angular`, `/bun`, `/node`, `/rstest`, `/rxjs`, `/nestjs`, `/jasmine`, `/setup`, `/zone`, `/eslint-plugin`), the user names createSpyFromClass, createAutoMock, createMock, mockDeep, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, createDirectiveHost, overrideComponentProvider, enableAngularDiagnostics, trackInjections, runEffect, settleResource, mockResourceProp, mockSignalProp, mockReadonlyProp, registerAutoSpyDefaults, provideActivatedRoute, setCurrentNavigation, createComponentStub, stubWebStorage, assertNoShadowedProviders, createSpyFromInstance, explainSpy, expectEmission, setupAutoSpy, takeStrictViolations, guardStrayConsole, setSpyEngine, assertMocked, Spy<T>, calledWith, mustBeCalledWith, onlyMethodsToSpyOn, onUnstubbedCall, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator (createSpyObject, mockProvider, SpectatorService), or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not on the class prototype", "strict mode is on", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController", "localStorage.setItem is not a function" or "Spy<T> is not assignable".
4
4
  ---
5
5
 
6
6
  # vitest-auto-spy
@@ -221,6 +221,7 @@ it('loads', async () => {
221
221
  | "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` |
222
222
  | an input that has to change after the first render | `await setInputs(fixture, { … })` — one `setInput` per name, one wait |
223
223
  | a component that navigates, or reads `router.url` | `provideRouterDouble({ url })` + `injectRouterDouble()` — `/angular-router` |
224
+ | a component that reads `router.currentNavigation()` for `extras.state` or `trigger` | `setCurrentNavigation({ extras: { state } })` on the same handle — no `instanceMethodsToSpyOn` |
224
225
  | a `window` or `document` behind a DI token | `provideWindowDouble(WINDOW, { screen })` / `provideDocumentDouble({ … })` |
225
226
  | `MAT_DIALOG_DATA` and `MatDialogRef` provided by hand | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(MatDialogRef)` + `injectMatDialogRef(Ref)` |
226
227
  | asserting a `computed()` did **not** recompute | `trackRecomputations(sig)` / `trackEffectRuns(ref)` — `{ count, stop() }` |
@@ -360,7 +361,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
360
361
  Most of this library's guarantees are type-level, so a green run that does not type-check is not
361
362
  done. Report failures with their output rather than describing them as passing.
362
363
 
363
- **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The thirty-six rules in
364
+ **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The thirty-seven rules in
364
365
  `vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
365
366
  to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
366
367
  evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
@@ -393,7 +394,11 @@ else is a fixture of the wrong shape — check it against `ReturnType<X['m']>` a
393
394
  with `createMock<…>()`. A value outside the declared type on purpose keeps the directive under
394
395
  `// eslint-disable-next-line vitest-auto-spy/no-ts-expect-error-on-double -- <why>`.
395
396
  `no-constant-expect` reports `expect(true).toBe(true)` and its relatives — assert on what the code
396
- produced, or `expect.fail(…)` for a branch the test must not reach. `no-compile-components` is silent
397
+ produced, or `expect.fail(…)` for a branch the test must not reach.
398
+ `no-redundant-smoke-test` reports the generated `it('should create', () => expect(x).toBeTruthy())`
399
+ where the block — its nested `describe`s counted — already has tests that run the same `beforeEach`:
400
+ they fail first on a subject that came back nullish, and say what they were doing. Delete it; keep it
401
+ only where it is the block's one running test, or make it assert the construction itself. `no-compile-components` is silent
397
402
  until `['error', { builder: 'inline-resources' }]`; with it, drop `compileComponents()` and the
398
403
  `async` it forced — except for a component whose template holds a `@defer` block, which ships async
399
404
  class metadata that the call resolves whatever the builder did. Keep that one behind