vitest-auto-spy 5.44.0 → 5.45.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/CHANGELOG.md +61 -2
- package/README.md +56 -55
- package/agent-docs/angular.md +6 -2
- package/agent-docs/errors.md +2 -2
- package/agent-docs/eslint.md +1 -1
- package/agent-docs/setup.md +7 -3
- package/dist/angular-diagnostics.js +14 -3
- package/dist/angular-http.js +8 -3
- package/dist/{chunk-JFOJFCVQ.js → chunk-3JL2YWIG.js} +2 -1
- package/dist/cli.js +2 -2
- package/dist/eslint-plugin.cjs +33 -6
- package/package.json +2 -1
- package/skills/vitest-auto-spy/SKILL.md +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,64 @@ The latest released version here must always match the one published on
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [5.45.0] - 2026-09-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`no-redundant-mock-reset` takes `configFlags`: the flags a factory-built `configFile` sets
|
|
18
|
+
beyond its text.** The rule reads `configFile` as text, so a config such as
|
|
19
|
+
`export default createProjectConfig(…)` that sets `clearMocks: true` inside the factory showed no
|
|
20
|
+
flag. The only way to state it was the inline `clearMocks: true`, which the Angular unit-test
|
|
21
|
+
builder check does not narrow. On Vitest 4, with an `@angular/build:unit-test` /
|
|
22
|
+
`@nx/angular:unit-test` target that names no `runnerConfig`, that reported a clear the builder
|
|
23
|
+
run still needs. `{ configFile, configFlags: { clearMocks?, mockReset?, restoreMocks? } }` reads
|
|
24
|
+
those flags as if the file wrote them: they override its text and apply only to the runs that
|
|
25
|
+
load that file (plain `vitest run`, and a target whose `runnerConfig` resolves to it). A target
|
|
26
|
+
without `runnerConfig` still runs on Vitest's defaults. `configFlags` without `configFile` is a
|
|
27
|
+
configuration error. Inline flags still win and are still not narrowed. The fix line of `doctor`'s
|
|
28
|
+
`mock-reset-config-unread` now suggests `{ configFile, configFlags: { clearMocks: true } }`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **`pendingRequests` answers with `expectRequest` in a suite built on `provideHttpTesting()`.**
|
|
33
|
+
An unanswered request in such a suite is usually reported by `enableAngularDiagnostics({
|
|
34
|
+
pendingRequests })` / `assertNoPendingRequests()`, whose fix line read "Answer it in the spec:
|
|
35
|
+
`controller.expectOne('/api/unflushed').flush(body)`" — a controller these specs never inject. The
|
|
36
|
+
line is now `await expectRequest('/api/unflushed').flush(body)` (with `, { method: 'POST' }` when
|
|
37
|
+
two open requests share a URL), the same one `provideHttpTesting()`'s own teardown check gives.
|
|
38
|
+
`provideHttpTesting()` registers a marker provider the diagnostics entry reads from the
|
|
39
|
+
configuration, so `vitest-auto-spy/angular/diagnostics` still does not import `@angular/common`;
|
|
40
|
+
a module configured with `provideHttpClientTesting()` or `HttpClientTestingModule` keeps the
|
|
41
|
+
`controller.expectOne(...)` line.
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- **The `provideHttpTesting({ interceptors })` example in `agent-docs/angular.md` no longer fails
|
|
46
|
+
under `strayTimers`.** It answered `error(401)` to a bare `.subscribe()`, which rxjs rethrows as
|
|
47
|
+
an unhandled Observable error, and 5.44.0 fails the test on that. The example now subscribes with
|
|
48
|
+
an error callback and asserts what it received.
|
|
49
|
+
- **`flushUnhandledObservableErrors()` returns `{ error, outsideTest }` for a rethrow scheduled
|
|
50
|
+
outside a test**, beside `{ error, test }`; the README, the setup page and `agent-docs/setup.md`
|
|
51
|
+
named only the second. `agent-docs/errors.md` also lists the file-level forms of the message
|
|
52
|
+
(`Unhandled Observable errors`, `outside any test`, `while the file was imported`).
|
|
53
|
+
- **The 5.44.0 entry on `strayConsole` routing called `'throw'` the `setupAutoSpy` default.** The
|
|
54
|
+
default is `'off'`; `preset: 'strict'` sets `'throw'`. `agent-docs/setup.md` now also says both
|
|
55
|
+
environment lines get a `Likely cause:`, how `allow` lets one through, and that routing is off
|
|
56
|
+
under `disableConsoleIntercept`.
|
|
57
|
+
- `StrayTimersOptions` is named in `agent-docs/setup.md`; `consoleOutput()` is listed with the
|
|
58
|
+
`/console` entry in the README; `runner-dom-differs-from-builder` names `vite.config.*` too.
|
|
59
|
+
- The skill gains a row for an interceptor under test (`provideHttpTesting({ interceptors, features })`
|
|
60
|
+
instead of `provideHttpClient(withInterceptors([...]))` + `provideHttpClientTesting()`) and names
|
|
61
|
+
the two runner-parity checks in its `doctor` paragraph.
|
|
62
|
+
|
|
63
|
+
### Internal
|
|
64
|
+
|
|
65
|
+
- **`undici` is a devDependency.** The `strayTimers` spec that fetches through the `undici` package
|
|
66
|
+
loaded jsdom's copy with `createRequire(import.meta.resolve('jsdom'))`, so a jsdom release that
|
|
67
|
+
dropped or moved the dependency would have broken the suite with nothing in `package.json` saying
|
|
68
|
+
why. It is now declared at the version the lockfile already held (8.10.2), and imported after the
|
|
69
|
+
`Symbol.dispose` shim it needs on Node 22.
|
|
70
|
+
|
|
13
71
|
## [5.44.0] - 2026-09-27
|
|
14
72
|
|
|
15
73
|
### Added
|
|
@@ -78,7 +136,7 @@ The latest released version here must always match the one published on
|
|
|
78
136
|
reached stderr of a green run, past the guard and past every console spy. The guard now routes that
|
|
79
137
|
console into the one it watches: the line fails the test that caused it at the spec's frame, a
|
|
80
138
|
`/console` spy or `vi.spyOn(…).mockImplementation` absorbs it (and can assert it), and both lines
|
|
81
|
-
get a `Likely cause:`. A suite under `strayConsole: 'throw'` (
|
|
139
|
+
get a `Likely cause:`. A suite under `strayConsole: 'throw'` (set by `preset: 'strict'`) that had
|
|
82
140
|
such lines on stderr now fails on them — that output is what the guard promises to catch. Fix the
|
|
83
141
|
cause (stub the API, mock the navigation, absorb it with a console spy), or let a line through with
|
|
84
142
|
`strayConsole: { allow: ['Not implemented: navigation'] }`. Routing is off when Vitest does not
|
|
@@ -5864,7 +5922,8 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
|
|
|
5864
5922
|
This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
|
|
5865
5923
|
[full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
|
|
5866
5924
|
|
|
5867
|
-
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.
|
|
5925
|
+
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...HEAD
|
|
5926
|
+
[5.45.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.44.0...v5.45.0
|
|
5868
5927
|
[5.44.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.43.0...v5.44.0
|
|
5869
5928
|
[5.43.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.42.0...v5.43.0
|
|
5870
5929
|
[5.42.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.41.0...v5.42.0
|
package/README.md
CHANGED
|
@@ -1063,7 +1063,7 @@ error tsconfig-glob-matches-nothing libs/users/tsconfig.spec.json
|
|
|
1063
1063
|
| `analog-module-cache-inline-styles` | `fsModuleCache` on under the Analog plugin with a component that has inline `styles` — the warm run fails on `virtual:angular:jit:style:inline` |
|
|
1064
1064
|
| `angular-build-istanbul-module-cache` | istanbul coverage under the unit-test builder (not in `browsers`) without `fsModuleCache` — up to −46 %; v8 beat istanbul with the cache ~2x. Info |
|
|
1065
1065
|
| `angular-build-happy-dom` | jsdom only because `happy-dom` is not installed; the builder picks it by itself from 21 — −4.6 % wall, −6 % RSS. Info |
|
|
1066
|
-
| `runner-dom-differs-from-builder` | A `vitest.config.*` on `environment: 'jsdom'` beside a builder target that runs happy-dom — two DOMs for one suite; happy-dom was −20 % Duration
|
|
1066
|
+
| `runner-dom-differs-from-builder` | A `vitest.config.*` / `vite.config.*` on `environment: 'jsdom'` beside a builder target that runs happy-dom — two DOMs for one suite; happy-dom was −20 % Duration |
|
|
1067
1067
|
| `analog-testbed-laxer-than-builder` | Analog `setupTestBed()` without `errorOnUnknownElements` / `errorOnUnknownProperties`, which the builder's TestBed turns on |
|
|
1068
1068
|
| `coverage-all-removed` | `coverage.all` in a config, on a Vitest that stopped reading the key |
|
|
1069
1069
|
| `coverage-include-misses-bundle` | A `coverage.include` of sources only, in a runner config over a bundle |
|
|
@@ -1592,7 +1592,7 @@ Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runti
|
|
|
1592
1592
|
| `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
|
|
1593
1593
|
| `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
|
|
1594
1594
|
| `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
|
|
1595
|
-
| `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import
|
|
1595
|
+
| `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import; `consoleOutput()` for all of it at once | `vitest` | ✅ |
|
|
1596
1596
|
| `vitest-auto-spy/jasmine` | the drop-in surface for a `jasmine-auto-spies` suite — `.and` / `.calls` / `.withArgs` on every spy, `createSpyObj`, the `jasmine` namespace, `registerJasmineMatchers` | `vitest` | ✅ |
|
|
1597
1597
|
| `vitest-auto-spy/jasmine-compat` | `enableJasmineCompat()` alone — the same `.and` / `.calls` layer, registering no adapter, for `bun test` and `node --test` | — (your runner) | ✅ |
|
|
1598
1598
|
| `vitest-auto-spy/observer-spy` | `subscribeSpyTo` / `ObserverSpy` / `SubscriberSpy` — the `@hirez_io/observer-spy` surface, so `/rxjs` does not carry it | `rxjs` | ✅ |
|
|
@@ -3339,7 +3339,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
3339
3339
|
| `guardStrayConsole(reaction)` | `/setup` | Fail a test that wrote to the console without absorbing it, and a file that wrote outside any test ([details](#test-run-hygiene)) |
|
|
3340
3340
|
| `withoutStrayTimerTracking(work)` | `/setup` | Run setup work whose timers the stray-timer tracker neither counts nor cancels — jsdom schedules one per Web Storage write |
|
|
3341
3341
|
| `describeStrayTimers()` | `/setup` | Every timer still pending, with its kind and delay, the spec file that scheduled it and the scheduling frames — the list `onStrayTimers` gets. Blind to anything scheduled under fake timers — `vi.useFakeTimers()` assigns over the wrapper |
|
|
3342
|
-
| `flushUnhandledObservableErrors()` | `/setup` | Run rxjs's pending rethrows of Observable errors nothing handled, now, and return `{ error, test }` for each — the check `strayTimers` runs after every test, which fails the test with `Unhandled Observable error`
|
|
3342
|
+
| `flushUnhandledObservableErrors()` | `/setup` | Run rxjs's pending rethrows of Observable errors nothing handled, now, and return `{ error, test }` (or `outsideTest`) for each — the check `strayTimers` runs after every test, which fails the test with `Unhandled Observable error` |
|
|
3343
3343
|
| `guardPrototypePollution(reaction)` | `/setup` | Name the test that left a key on `Object.prototype` — it stops later files collecting ([details](#test-run-hygiene)) |
|
|
3344
3344
|
| `guardDocumentPollution(option)` | `/setup` | Name the test that left an attribute on `<html>` / `<body>`, and put it back ([details](#test-run-hygiene)) |
|
|
3345
3345
|
| `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
|
|
@@ -3394,7 +3394,8 @@ expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
|
3394
3394
|
Housekeeping: `resetConsoleSpies()` clears the recorded calls between tests (Vitest's
|
|
3395
3395
|
`clearMocks: true` already does that automatically), `restoreConsole()` puts the original
|
|
3396
3396
|
methods back and keeps the spies, so the exports stay live, and `installConsoleSpies()` puts the same
|
|
3397
|
-
spies back on after a restore.
|
|
3397
|
+
spies back on after a restore. `consoleOutput()` is everything the spies recorded as one value,
|
|
3398
|
+
`{ info: [['done']] }`, only the channels written to — for an exact `toStrictEqual`.
|
|
3398
3399
|
|
|
3399
3400
|
They survive `vi.resetModules()`. A fresh copy of the module used to record the previous copy's spy
|
|
3400
3401
|
as "the real `console.warn`", after which `restoreConsole()` installed that dead spy for the rest of
|
|
@@ -3981,57 +3982,57 @@ object of `vi.fn()`s is perfectly reasonable in application code. Flat config on
|
|
|
3981
3982
|
`.eslintrc` `plugins: ['…']` form resolves names to `eslint-plugin-*` packages, which a subpath
|
|
3982
3983
|
export can never be.
|
|
3983
3984
|
|
|
3984
|
-
| Rule | Recommended | Fix | Flags
|
|
3985
|
-
| --------------------------------- | :---------: | ----------------- |
|
|
3986
|
-
| `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. On the `ActivatedRoute` token the advice is `provideActivatedRoute()` from `/angular-router` instead, with a message of its own
|
|
3987
|
-
| `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
|
|
3988
|
-
| `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy`, the stub class deleted
|
|
3989
|
-
| `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`
|
|
3990
|
-
| `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, in one step or two → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }`
|
|
3991
|
-
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp`
|
|
3992
|
-
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside a `subscribe()` callback → `expectEmission` / `firstValueFrom`
|
|
3993
|
-
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory that returns it
|
|
3994
|
-
| `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
|
|
3995
|
-
| `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all
|
|
3996
|
-
| `no-done-callback` | `error` | — | a first parameter of a test or hook that is **called**, **passed on** as an argument, or never used → `async` + an awaited assertion, and `done.fail(…)` at the call site. A parameter read only as `ctx.skip()` / `ctx.task` is Vitest's `TestContext` and is left alone, destructured or not
|
|
3997
|
-
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)`
|
|
3998
|
-
| `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement of its own — a stub nobody continued, asserting nothing
|
|
3999
|
-
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted
|
|
4000
|
-
| `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*`
|
|
4001
|
-
| `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`
|
|
4002
|
-
| `no-reflect-member-access` | `error` | on a double | `Reflect.get(component, 'privateField')` / `Reflect.set(service, 'x', v)` on a subject the test holds — the second door out of `no-private-member-access`, and the one no compiler stands in: the key is an ordinary string argument, so a rename leaves the spec reading a property that is no longer there and `Reflect.set` writing a dead one nothing reads. `window` / `globalThis` and a computed key are never reported; a write onto a double this library built is reported apart and suggests `mockValueProp`
|
|
4003
|
-
| `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
|
|
4004
|
-
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads
|
|
4005
|
-
| `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
|
|
4006
|
-
| `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
|
|
4007
|
-
| `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`
|
|
4008
|
-
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double
|
|
4009
|
-
| `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs
|
|
4010
|
-
| `no-hand-assigned-global` | `error` | `--fix` | `global.fetch = vi.fn(…)` and any other double assigned to a global with no restore in a teardown hook → `mockValueProp(globalThis, …)` / `vi.stubGlobal` + `unstubGlobals`, `blockNetwork()` for a spec that only stays offline, `stubWebStorage()` for the storages; any value written into an imported object (`environment.production = true`) → `mockValueProp(environment, 'production', true)`, fixed in a test or `beforeEach`
|
|
4011
|
-
| `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or a `createMock<Response>(…)` / `createAutoMock<Response>(…)` call → `stubResponse({ body })` from `/setup`, which builds the platform's own and answers every member; `Response` has to resolve to the global, so an Express handler's or a generated client's envelope of the same name is left alone
|
|
4012
|
-
| `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
|
|
4013
|
-
| `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)`
|
|
4014
|
-
| `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
|
|
4015
|
-
| `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
|
|
4016
|
-
| `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`
|
|
4017
|
-
| `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**
|
|
4018
|
-
| `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
|
|
4019
|
-
| `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
|
|
4020
|
-
| `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
|
|
4021
|
-
| `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
|
|
4022
|
-
| `no-self-called-spy` | `error` | — | `vi.spyOn(obj, 'm')`, then `obj.m(…)` written by the test itself, then a positive `toHaveBeenCalled*` — the assertion is satisfied by the test's own line, so the test survives the deletion of the behaviour its title names. Order decides it: a call written **before** the spy is arrangement, and a `mockClear` between the call and the assertion, a negated matcher or arguments the call did not pass all silence it
|
|
4023
|
-
| `no-vacuous-absence-assertion` | `error` | — | a test whose **every** assertion holds on "nothing happened" — a value only a `subscribe` callback writes, or a `vi.fn()` handed to `subscribe`, under a matcher that repeats the declaration (`let x = []` … `expect(x).toEqual([])`) or asserts absence (`toBeUndefined`, `toBeNull`, `toBeFalsy`, `not.toHaveBeenCalled`, `toHaveBeenCalledTimes(0)`) → `await expectNoEmission(source$)` when silence is the claim, `expect(await expectEmission(source$)).toEqual([])` when the empty result is. Silent as soon as one assertion in the test could fail on silence, which is what leaves the "not yet, trigger, now yes" shape alone
|
|
4024
|
-
| `prefer-settle-dynamic-import` | `error` | suggest | `await import('./thing')` — or `import('./thing').then(…)` — written in a test body or a hook to settle a module the code under test lazy-loads → `await settleDynamicImport(() => import('./thing'))`. Awaiting the specifier waits for the **module**; the handler's own continuation, the lines after _its_ `await`, is still queued behind it, so the assertion reads the state one turn early. The suggestion wraps the `import()` and imports the helper. Reported only where the innermost enclosing function is the runner's own callback, which is what exempts a `vi.mock` factory, a lazy route's `loadComponent` and a callback the spec hands to production code
|
|
4025
|
-
| `prefer-create-mock` | `warn` | suggest | an object literal under `as SomeType` — `{ id: '1', isOffline: false } as Device`, and the `<Device>{ … }` spelling → `createMock<Device>({ … })`, which takes a `DeepPartial<Device>` and answers a `Device`. A cast asks whether the two types overlap, so it passes a key the type does not declare **and** a required field the fixture never sets; both type gates stay silent and the fixture is then spread into an expected payload. Silent on `as const`, on `as unknown` / `as any` and the double cast built from them, on a cast of anything that is not a literal, and on a literal inside one of this library's own factories. `warn`, not `error`: the repair makes a drifted fixture red on the next type-check, which is the finding and also a migration taken file by file
|
|
4026
|
-
| `no-mock-cast` | `error` | suggest | `TestBed.inject(S).m as Mock` — a cast to Vitest's `Mock` / `MockInstance` over a **member access** → `injectSpy(S).m`, which the suggestion writes when the token is in view. `Mock` with no parameters is `Mock<any>`, so `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments; the worse form, `(spy.m.mockReturnValue as Mock)(…)`, gets a message of its own because the cast sits on the member that installs the answer. The name has to resolve to a runner's import, so a domain `Mock` is never reported
|
|
4027
|
-
| `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset **the runner already performs** — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — first in a `beforeEach` no other `beforeEach` precedes, or a clear closing an `afterEach` → delete it. **Silent until** the runner's flags are known: `['error', { clearMocks: true }]` and siblings, `{ configFile }`, or a `vitest.config.*` / `vite.config.*` above the file, read as text; a factory or `mergeConfig` in another module is not read through, and `setupAutoSpy({ restoreMocks })` is not the runner's flag. The flag has to match the call: `restoreMocks` never reaches a plain `vi.fn()`. `--fix` only where it is the file's only before-hook, a suggestion elsewhere, **never** in a test body
|
|
4028
|
-
| `no-unasserted-argument` | `warn` | — | a bare `expect(spy).toHaveBeenCalled()` where the file itself says the arguments are the point: the same subject is pinned with `toHaveBeenCalledWith(…)` in another test of the file, or the test's title says `with` and its body asserts nothing else → name the arguments. Narrower than `vitest/prefer-called-with` on purpose — 175 findings against its 1941 on the same suite
|
|
4029
|
-
| `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
|
|
4030
|
-
| `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`
|
|
4031
|
-
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere
|
|
4032
|
-
| `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest
|
|
4033
|
-
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time
|
|
4034
|
-
| `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
|
|
3985
|
+
| Rule | Recommended | Fix | Flags |
|
|
3986
|
+
| --------------------------------- | :---------: | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3987
|
+
| `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. On the `ActivatedRoute` token the advice is `provideActivatedRoute()` from `/angular-router` instead, with a message of its own |
|
|
3988
|
+
| `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 |
|
|
3989
|
+
| `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy`, the stub class deleted |
|
|
3990
|
+
| `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()` |
|
|
3991
|
+
| `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, in one step or two → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }` |
|
|
3992
|
+
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
3993
|
+
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside a `subscribe()` callback → `expectEmission` / `firstValueFrom` |
|
|
3994
|
+
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory that returns it |
|
|
3995
|
+
| `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 |
|
|
3996
|
+
| `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
|
|
3997
|
+
| `no-done-callback` | `error` | — | a first parameter of a test or hook that is **called**, **passed on** as an argument, or never used → `async` + an awaited assertion, and `done.fail(…)` at the call site. A parameter read only as `ctx.skip()` / `ctx.task` is Vitest's `TestContext` and is left alone, destructured or not |
|
|
3998
|
+
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
3999
|
+
| `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement of its own — a stub nobody continued, asserting nothing |
|
|
4000
|
+
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
|
|
4001
|
+
| `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
|
|
4002
|
+
| `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` |
|
|
4003
|
+
| `no-reflect-member-access` | `error` | on a double | `Reflect.get(component, 'privateField')` / `Reflect.set(service, 'x', v)` on a subject the test holds — the second door out of `no-private-member-access`, and the one no compiler stands in: the key is an ordinary string argument, so a rename leaves the spec reading a property that is no longer there and `Reflect.set` writing a dead one nothing reads. `window` / `globalThis` and a computed key are never reported; a write onto a double this library built is reported apart and suggests `mockValueProp` |
|
|
4004
|
+
| `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 |
|
|
4005
|
+
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
|
|
4006
|
+
| `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 |
|
|
4007
|
+
| `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 |
|
|
4008
|
+
| `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` |
|
|
4009
|
+
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
|
|
4010
|
+
| `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
|
|
4011
|
+
| `no-hand-assigned-global` | `error` | `--fix` | `global.fetch = vi.fn(…)` and any other double assigned to a global with no restore in a teardown hook → `mockValueProp(globalThis, …)` / `vi.stubGlobal` + `unstubGlobals`, `blockNetwork()` for a spec that only stays offline, `stubWebStorage()` for the storages; any value written into an imported object (`environment.production = true`) → `mockValueProp(environment, 'production', true)`, fixed in a test or `beforeEach` |
|
|
4012
|
+
| `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or a `createMock<Response>(…)` / `createAutoMock<Response>(…)` call → `stubResponse({ body })` from `/setup`, which builds the platform's own and answers every member; `Response` has to resolve to the global, so an Express handler's or a generated client's envelope of the same name is left alone |
|
|
4013
|
+
| `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 |
|
|
4014
|
+
| `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)` |
|
|
4015
|
+
| `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 |
|
|
4016
|
+
| `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 |
|
|
4017
|
+
| `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` |
|
|
4018
|
+
| `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** |
|
|
4019
|
+
| `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 |
|
|
4020
|
+
| `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 |
|
|
4021
|
+
| `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 |
|
|
4022
|
+
| `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 |
|
|
4023
|
+
| `no-self-called-spy` | `error` | — | `vi.spyOn(obj, 'm')`, then `obj.m(…)` written by the test itself, then a positive `toHaveBeenCalled*` — the assertion is satisfied by the test's own line, so the test survives the deletion of the behaviour its title names. Order decides it: a call written **before** the spy is arrangement, and a `mockClear` between the call and the assertion, a negated matcher or arguments the call did not pass all silence it |
|
|
4024
|
+
| `no-vacuous-absence-assertion` | `error` | — | a test whose **every** assertion holds on "nothing happened" — a value only a `subscribe` callback writes, or a `vi.fn()` handed to `subscribe`, under a matcher that repeats the declaration (`let x = []` … `expect(x).toEqual([])`) or asserts absence (`toBeUndefined`, `toBeNull`, `toBeFalsy`, `not.toHaveBeenCalled`, `toHaveBeenCalledTimes(0)`) → `await expectNoEmission(source$)` when silence is the claim, `expect(await expectEmission(source$)).toEqual([])` when the empty result is. Silent as soon as one assertion in the test could fail on silence, which is what leaves the "not yet, trigger, now yes" shape alone |
|
|
4025
|
+
| `prefer-settle-dynamic-import` | `error` | suggest | `await import('./thing')` — or `import('./thing').then(…)` — written in a test body or a hook to settle a module the code under test lazy-loads → `await settleDynamicImport(() => import('./thing'))`. Awaiting the specifier waits for the **module**; the handler's own continuation, the lines after _its_ `await`, is still queued behind it, so the assertion reads the state one turn early. The suggestion wraps the `import()` and imports the helper. Reported only where the innermost enclosing function is the runner's own callback, which is what exempts a `vi.mock` factory, a lazy route's `loadComponent` and a callback the spec hands to production code |
|
|
4026
|
+
| `prefer-create-mock` | `warn` | suggest | an object literal under `as SomeType` — `{ id: '1', isOffline: false } as Device`, and the `<Device>{ … }` spelling → `createMock<Device>({ … })`, which takes a `DeepPartial<Device>` and answers a `Device`. A cast asks whether the two types overlap, so it passes a key the type does not declare **and** a required field the fixture never sets; both type gates stay silent and the fixture is then spread into an expected payload. Silent on `as const`, on `as unknown` / `as any` and the double cast built from them, on a cast of anything that is not a literal, and on a literal inside one of this library's own factories. `warn`, not `error`: the repair makes a drifted fixture red on the next type-check, which is the finding and also a migration taken file by file |
|
|
4027
|
+
| `no-mock-cast` | `error` | suggest | `TestBed.inject(S).m as Mock` — a cast to Vitest's `Mock` / `MockInstance` over a **member access** → `injectSpy(S).m`, which the suggestion writes when the token is in view. `Mock` with no parameters is `Mock<any>`, so `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments; the worse form, `(spy.m.mockReturnValue as Mock)(…)`, gets a message of its own because the cast sits on the member that installs the answer. The name has to resolve to a runner's import, so a domain `Mock` is never reported |
|
|
4028
|
+
| `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset **the runner already performs** — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — first in a `beforeEach` no other `beforeEach` precedes, or a clear closing an `afterEach` → delete it. **Silent until** the runner's flags are known: `['error', { clearMocks: true }]` and siblings, `{ configFile }`, or a `vitest.config.*` / `vite.config.*` above the file, read as text; a factory or `mergeConfig` in another module is not read through (name its flags as `{ configFile, configFlags: { clearMocks: true } }`), and `setupAutoSpy({ restoreMocks })` is not the runner's flag. The flag has to match the call: `restoreMocks` never reaches a plain `vi.fn()`. `--fix` only where it is the file's only before-hook, a suggestion elsewhere, **never** in a test body |
|
|
4029
|
+
| `no-unasserted-argument` | `warn` | — | a bare `expect(spy).toHaveBeenCalled()` where the file itself says the arguments are the point: the same subject is pinned with `toHaveBeenCalledWith(…)` in another test of the file, or the test's title says `with` and its body asserts nothing else → name the arguments. Narrower than `vitest/prefer-called-with` on purpose — 175 findings against its 1941 on the same suite |
|
|
4030
|
+
| `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 |
|
|
4031
|
+
| `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` |
|
|
4032
|
+
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
|
|
4033
|
+
| `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
|
|
4034
|
+
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
|
|
4035
|
+
| `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 |
|
|
4035
4036
|
|
|
4036
4037
|
Every message ends with a link to the matching [recipe](#how-to-mock): a rule that only says
|
|
4037
4038
|
"don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
|
package/agent-docs/angular.md
CHANGED
|
@@ -489,11 +489,15 @@ TestBed.configureTestingModule({
|
|
|
489
489
|
providers: [...provideHttpTesting({ interceptors: [authInterceptor], features: [withInterceptorsFromDi()] })],
|
|
490
490
|
});
|
|
491
491
|
|
|
492
|
-
|
|
492
|
+
let failure: unknown;
|
|
493
|
+
TestBed.inject(HttpClient)
|
|
494
|
+
.get('/api/me')
|
|
495
|
+
.subscribe({ error: (error: unknown) => (failure = error) });
|
|
493
496
|
const pending = expectRequest('/api/me'); // the request as the interceptors sent it
|
|
494
497
|
|
|
495
498
|
expect(pending.request.headers.get('Authorization')).toBe('Bearer token');
|
|
496
|
-
await pending.error(401);
|
|
499
|
+
await pending.error(401);
|
|
500
|
+
expect(failure).toBeInstanceOf(HttpErrorResponse); // or whatever the interceptor mapped it to
|
|
497
501
|
```
|
|
498
502
|
|
|
499
503
|
`interceptors` run in array order, before any from `features`. Every feature goes into one
|
package/agent-docs/errors.md
CHANGED
|
@@ -37,7 +37,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
|
|
|
37
37
|
| `It is reported at the end of b.spec.ts: the file-end check of a.spec.ts did not run.` | `a.spec.ts`'s own `afterAll` threw, so its file-end checks never ran and its console output was carried into the next file | fix that `afterAll` first — its error is in `a.spec.ts`'s output — then the output the report quotes, in the file it names |
|
|
38
38
|
| `"…" made N call(s) a strict double had nothing configured for, and the throw never reached the test` | `setupAutoSpy({ strict: true })` — a strict double threw, and a `catch` in the code under test or an RxJS subscriber with no error callback swallowed it; each call is quoted with its arguments and the line that made it | configure that method (`.mockReturnValue`, `.calledWith` or `returns`), or drain it with `takeStrictViolations()` when the test provokes it on purpose (§5) |
|
|
39
39
|
| `a.spec.ts left N timer(s) pending when it ended` | `onStrayTimers: 'throw'` — a timer outlived its file; each line names the test that scheduled it (or `while the file was imported`, `outside any test`), and it was cancelled | clear it in the test named — `clearTimeout`, unsubscribe, `fixture.destroy()` — or run it out with fake timers before the test ends (§10) |
|
|
40
|
-
| `Unhandled Observable error in "<test>"` | `strayTimers` on — rxjs rethrew an error no subscriber handled; the test named is the one that scheduled the rethrow, and the original error is the `cause
|
|
40
|
+
| `Unhandled Observable error in "<test>"` (or `errors`, `outside any test`, `while the file was imported`) | `strayTimers` on — rxjs rethrew an error no subscriber handled; the test named is the one that scheduled the rethrow, and the original error is the `cause`; one scheduled outside a test fails the file at its end | handle it where the stream is subscribed — an error callback or `catchError` — or assert it: `await expect(firstValueFrom(x$)).rejects.toThrow(…)` (§10) |
|
|
41
41
|
| `a.spec.ts left N window/document listener(s) attached when it ended` | `onStrayListeners: 'throw'` — a listener outlived its file; each line names the test that added it, and it was removed | remove it in that test — `removeEventListener` with the same capture flag, an `AbortSignal`, `fixture.destroy()` (§10) |
|
|
42
42
|
| `N file-end checks failed when a.spec.ts ended` | several file-end reports failed together — stray timers, listeners, console output — and each numbered block is one of them | fix each block on its own; Vitest also lists every member error separately |
|
|
43
43
|
| `fetch is stubbed in unit tests — GET <url>. The test "…" requested it` | `blockNetwork` refused a request the named test made (a blocked `XMLHttpRequest` carries the same marker on `statusText`) | answer it in that test — `vi.spyOn(globalThis, 'fetch').mockResolvedValue(stubResponse({ body: … }))` — or `blockNetwork({ xhr: 'empty' })` for an XHR nobody reads (§10) |
|
|
@@ -168,7 +168,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
|
|
|
168
168
|
| `expectRequest: no GET …?page=2 — but GET …?page=3 was made; the query differs` | the same path, another query string | name the query the code sends, or match the path alone: `expectRequest('/api/products')` (§13) |
|
|
169
169
|
| `expectRequest: no request matched … — nothing was requested at all` | an `httpResource()` whose `request()` returned no URL yet, or an `HttpClient` call nothing subscribed to | set the signal the resource reads, or subscribe to the call (§13) |
|
|
170
170
|
| `this TestBed has no HttpTestingController` | `expectRequest` / `expectNoRequest` on a module that never got the testing backend | `TestBed.configureTestingModule({ providers: [...provideHttpTesting()] })` from `vitest-auto-spy/angular-http` (§13) |
|
|
171
|
-
| `X was never answered (end of "…")` / `N requests were never answered` | `provideHttpTesting({ verifyOnTeardown })` or `enableAngularDiagnostics({ pendingRequests })` — the test left a request open; the message names its URL and the test | answer it with the line the message prints (`await expectRequest(url).flush(body)`, `controller.expectOne(url).flush(body)` under diagnostics), or assert its absence (`expectNoRequest(url)`); a cancelled one is forgiven with the `ignoreCancelled` opt-in the message names (§13) |
|
|
171
|
+
| `X was never answered (end of "…")` / `N requests were never answered` | `provideHttpTesting({ verifyOnTeardown })` or `enableAngularDiagnostics({ pendingRequests })` — the test left a request open; the message names its URL and the test | answer it with the line the message prints (`await expectRequest(url).flush(body)`, `controller.expectOne(url).flush(body)` under diagnostics in a module built on `provideHttpClientTesting()`), or assert its absence (`expectNoRequest(url)`); a cancelled one is forgiven with the `ignoreCancelled` opt-in the message names (§13) |
|
|
172
172
|
| `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) |
|
|
173
173
|
| `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) |
|
|
174
174
|
| `provideActivatedRoute: @angular/router <version> 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) |
|
package/agent-docs/eslint.md
CHANGED
|
@@ -75,7 +75,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
75
75
|
| `prefer-settle-dynamic-import` | `error` | suggest | `await import('./thing')` and `import('./thing').then(…)` inside an `it` / `test` body or a `beforeEach` / `beforeAll` / `afterEach` / `afterAll` hook (the `.only`, `.skip` and `.each` spellings included) → `await settleDynamicImport(() => import('./thing'))`, which is the same load plus `flushEventLoop(1)` and returns the namespace, so the destructured form keeps reading the same way. The bare `await` waits for the **module** and not for the continuation of the code that was loading it, so the assertion runs a turn early and the test is green only while that continuation is short. Syntax only. Silent wherever a function of its own sits between the runner's callback and the import — a `vi.mock` factory, a lazy route's `loadComponent` / `loadChildren`, a callback handed to production code, `settleDynamicImport`'s own `() => import(…)` — and, by the same reading, silent on a spec-local `const load = async () => { await import('…') }`, which is written identically whether the spec calls it or the code under test does |
|
|
76
76
|
| `prefer-create-mock` | `warn` | suggest | an object literal under `as SomeType` — `{ id: '1', isOffline: false } as Device`, `<Device>{ … }` too → `createMock<Device>({ … })`. A cast asks whether the two types overlap rather than whether the value is one of them, so it passes a key the type does not declare (the excess-property check is skipped) **and** a required field the fixture never sets; both type gates stay silent, and the same object is then spread into the expected payload of a call assertion. Where the literal already sits in a typed slot the first repair is to delete the cast. Silent on `as const`, `as unknown` / `as any` and the double cast built from them, on a cast of anything but a literal, and on a literal inside one of this library's own factories. `warn`, because accepting the suggestion makes a drifted fixture red on the next type-check — the finding, and a migration taken file by file |
|
|
77
77
|
| `no-mock-cast` | `error` | suggest | a cast to Vitest's `Mock` / `MockInstance` over a **member access** — `TestBed.inject(S).m as Mock` → `injectSpy(S).m`, which the suggestion writes whenever the token is in view (directly, or through a `const` the file settled with a `TestBed.inject`). `Mock` with no parameters is `Mock<any>`: `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments. `(spy.m.mockReturnValue as Mock)(…)` gets its own message — the cast is on the member that installs the answer, so neither the value nor the method's return type is checked. The name must resolve to an import from `vitest` / `@rstest/core` / `bun:test` / `jest`, or to nothing at all, so a domain `Mock` is left alone |
|
|
78
|
-
| `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset the runner already performs between tests — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — standing where nothing the file wrote ran since the runner's own: the first statement of a `beforeEach` no other `beforeEach` precedes, or a clear as the last statement of an `afterEach` → delete it, and the hook with it where that is all it held. A restore or reset in `afterEach` / `afterAll` is never reported: after the last test it is what takes a spy off before the `afterAll` hooks. **Silent** until the flags are known: options `{ clearMocks, restoreMocks, mockReset }` (or `{ configFile }` naming the runner config) first, then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text; a config that leaves `clearMocks` out counts it on from Vitest 5 (its default). Where an `@angular/build:unit-test` / `@nx/angular:unit-test` target (found in `angular.json` / `project.json` above the file) also serves the spec, a config flag counts only if that target's `runnerConfig` applies it too — without `runnerConfig` the builder runs Vitest's defaults, so `restoreMocks` / `mockReset` from `vitest.config.ts` do not make a reset dead there, while a clear stays reported on Vitest 5; the rule's inline flags are not narrowed. A config built by a factory or `mergeConfig` from another module is not read through —
|
|
78
|
+
| `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset the runner already performs between tests — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — standing where nothing the file wrote ran since the runner's own: the first statement of a `beforeEach` no other `beforeEach` precedes, or a clear as the last statement of an `afterEach` → delete it, and the hook with it where that is all it held. A restore or reset in `afterEach` / `afterAll` is never reported: after the last test it is what takes a spy off before the `afterAll` hooks. **Silent** until the flags are known: options `{ clearMocks, restoreMocks, mockReset }` (or `{ configFile }` naming the runner config) first, then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text; a config that leaves `clearMocks` out counts it on from Vitest 5 (its default). Where an `@angular/build:unit-test` / `@nx/angular:unit-test` target (found in `angular.json` / `project.json` above the file) also serves the spec, a config flag counts only if that target's `runnerConfig` applies it too — without `runnerConfig` the builder runs Vitest's defaults, so `restoreMocks` / `mockReset` from `vitest.config.ts` do not make a reset dead there, while a clear stays reported on Vitest 5; the rule's inline flags are not narrowed. A config built by a factory or `mergeConfig` from another module is not read through — name its flags as `{ configFile, configFlags: { clearMocks: true } }`, read as if the file set them, so the builder narrowing still applies (`configFlags` needs `configFile`; `doctor` notes the case as `mock-reset-config-unread`). `setupAutoSpy({ restoreMocks: true })` is not the runner's `restoreMocks` (it restores in an `afterEach`, after the test) — never pass the rule a flag only the setup file sets. Flag matches call, never family — `restoreMocks` walks only the spies `vi.spyOn` installed. `--fix` only in a file whose only before-hook it is; a suggestion otherwise; never inside a test body |
|
|
79
79
|
| `no-unasserted-argument` | `warn` | — | a bare `expect(spy).toHaveBeenCalled()` on evidence from the file: the same subject asserted with `toHaveBeenCalledWith(…)` in another test, or a title containing `with` over a body whose every assertion is a bare call → `toHaveBeenCalledWith(…)` / `mustBeCalledWith(…)`. `not.toHaveBeenCalled()` and the counting matchers are never reported |
|
|
80
80
|
| `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 list those classes in `{ ignoreComponents: ['CardComponent'] }` (a spec naming one is left alone) or keep a `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
|
|
81
81
|
| `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 |
|
package/agent-docs/setup.md
CHANGED
|
@@ -80,8 +80,9 @@ the previous file is the one to fix.
|
|
|
80
80
|
outside this package and zone.js is undici is never counted, reported or cancelled. So a file that
|
|
81
81
|
fetches from a server it cannot close passes `onStrayTimers: 'throw'` with the check on — do not turn
|
|
82
82
|
`strayTimers` off for it. For another dependency's housekeeping, `strayTimers: { ignore: [/some-sdk[/\\]poll/, 'heartbeat.js'] }`
|
|
83
|
-
(or `trackStrayTimers(undefined, { ignore })
|
|
84
|
-
stack; an ignored timer is not cancelled either, and the
|
|
83
|
+
(or `trackStrayTimers(undefined, { ignore })`; the type is `StrayTimersOptions` from `/setup`) searches
|
|
84
|
+
each substring or RegExp in the scheduling stack; an ignored timer is not cancelled either, and the
|
|
85
|
+
last call's list is the one in force.
|
|
85
86
|
|
|
86
87
|
**An Observable error nothing handled fails its test, not the file.** rxjs rethrows it from
|
|
87
88
|
`setTimeout(() => { throw error })`, which used to surface only as a stray `setTimeout 0 ms, scheduled
|
|
@@ -90,7 +91,8 @@ and fails the test with `Unhandled Observable error in "<test>"`, quoting `<name
|
|
|
90
91
|
error as `cause`; one scheduled outside a test fails the file at the sweep. The fake clock is read too while
|
|
91
92
|
it is still installed (`globalFakeTimers` is); a rethrow `config.onUnhandledError` takes reports
|
|
92
93
|
nothing. Fix it where the stream is subscribed — an error callback or `catchError` — or assert the
|
|
93
|
-
error. `flushUnhandledObservableErrors()` is the same check by hand
|
|
94
|
+
error. `flushUnhandledObservableErrors()` is the same check by hand: it returns `{ error, test }` (or
|
|
95
|
+
`{ error, outsideTest }`) for each rethrow that threw.
|
|
94
96
|
|
|
95
97
|
**`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
|
|
96
98
|
away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
|
|
@@ -755,6 +757,8 @@ Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/setup#_16-console-ou
|
|
|
755
757
|
console, captured before Vitest swapped `globalThis.console`; the guard routes it into the watched
|
|
756
758
|
one, so `NotSupportedError … Iframe page loading is disabled` or `Not implemented: navigation`
|
|
757
759
|
fails the test that caused it, and a `/console` spy absorbs it (`consoleErrorSpy` can assert it).
|
|
760
|
+
Both lines get a `Likely cause:`; `allow: ['Not implemented: navigation']` lets one through. Routing
|
|
761
|
+
is off when Vitest does not intercept the console (`disableConsoleIntercept`).
|
|
758
762
|
- **An iframe that must never load under happy-dom:** no happy-dom 20 setting is both silent and
|
|
759
763
|
unloaded (`navigation.disableChildFrameNavigation` dispatches `load` itself). Either
|
|
760
764
|
`// @vitest-environment-options {"settings":{"disableIframePageLoading":true}}` plus
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { pendingRequestsReport, assertAngularInternals, instrumentTestBed, onTestingModuleConfigured, onComponentCreated, beforeTestBedReset, verifyOnTeardown } from './chunk-
|
|
2
|
-
export { disableTestBedDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, reportSpecTiming } from './chunk-
|
|
1
|
+
import { pendingRequestsReport, assertAngularInternals, instrumentTestBed, onTestingModuleConfigured, onComponentCreated, HTTP_TESTING_BRAND, beforeTestBedReset, verifyOnTeardown } from './chunk-3JL2YWIG.js';
|
|
2
|
+
export { disableTestBedDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, reportSpecTiming } from './chunk-3JL2YWIG.js';
|
|
3
3
|
import { failOnUnspiedProvider } from './chunk-FQ6NT2IN.js';
|
|
4
4
|
import './chunk-GG7BQKEK.js';
|
|
5
5
|
import './chunk-MNY4ZHEQ.js';
|
|
@@ -158,6 +158,11 @@ function findControllerTokenInImports(imports, seen) {
|
|
|
158
158
|
const wrapped = readProperty(imports, "ngModule");
|
|
159
159
|
return findControllerToken(readProperty(imports, "providers")) ?? findControllerTokenInModule(wrapped ?? imports, seen);
|
|
160
160
|
}
|
|
161
|
+
function configuredByProvideHttpTesting(config) {
|
|
162
|
+
const flat = [];
|
|
163
|
+
flattenProviders(readProperty(config, "providers"), flat);
|
|
164
|
+
return flat.some((provider) => readProperty(provider, "useValue") === HTTP_TESTING_BRAND);
|
|
165
|
+
}
|
|
161
166
|
function readControllerToken(config) {
|
|
162
167
|
const fromProviders = findControllerToken(readProperty(config, "providers"));
|
|
163
168
|
if (fromProviders !== void 0) {
|
|
@@ -183,6 +188,7 @@ function takeOpenRequests(controller) {
|
|
|
183
188
|
return open.map(describeRequest);
|
|
184
189
|
}
|
|
185
190
|
var controllerToken;
|
|
191
|
+
var httpTestingInUse = false;
|
|
186
192
|
var openAtReset = [];
|
|
187
193
|
function injectFromModule(token) {
|
|
188
194
|
const testBed = getTestBed();
|
|
@@ -200,7 +206,7 @@ function checkPendingRequests(options, test) {
|
|
|
200
206
|
const open = ignoreCancelled ? taken.filter((request) => !request.cancelled) : taken;
|
|
201
207
|
const report = pendingRequestsReport(open, {
|
|
202
208
|
when: test === void 0 ? "when assertNoPendingRequests() ran" : `end of "${taskName(test)}"`,
|
|
203
|
-
answer: ({ method, urlWithParams }, withMethod) => withMethod ? `controller.expectOne({ method: '${method}', url: '${urlWithParams}' }).flush(body)` : `controller.expectOne('${urlWithParams}').flush(body)`,
|
|
209
|
+
answer: ({ method, urlWithParams }, withMethod) => httpTestingInUse ? `await expectRequest('${urlWithParams}'${withMethod ? `, { method: '${method}' }` : ""}).flush(body)` : withMethod ? `controller.expectOne({ method: '${method}', url: '${urlWithParams}' }).flush(body)` : `controller.expectOne('${urlWithParams}').flush(body)`,
|
|
204
210
|
ignoreCancelled: test === void 0 ? "assertNoPendingRequests({ ignoreCancelled: true })" : "enableAngularDiagnostics({ pendingRequests: { ignoreCancelled: true } })"
|
|
205
211
|
});
|
|
206
212
|
if (report !== void 0) {
|
|
@@ -272,6 +278,7 @@ function registerPerTestHooks() {
|
|
|
272
278
|
if (preparedTest !== task) {
|
|
273
279
|
preparedTest = task;
|
|
274
280
|
controllerToken = void 0;
|
|
281
|
+
httpTestingInUse = false;
|
|
275
282
|
openAtReset.length = 0;
|
|
276
283
|
moduleDoubles.clear();
|
|
277
284
|
resetTally();
|
|
@@ -311,6 +318,9 @@ function enableAngularDiagnostics(options = {}) {
|
|
|
311
318
|
if (selection.pendingRequests && controllerToken === void 0) {
|
|
312
319
|
controllerToken = readControllerToken(config);
|
|
313
320
|
}
|
|
321
|
+
if (selection.pendingRequests && configuredByProvideHttpTesting(config)) {
|
|
322
|
+
httpTestingInUse = true;
|
|
323
|
+
}
|
|
314
324
|
if (selection.shadowedProviders) {
|
|
315
325
|
collectModuleDoubles(config);
|
|
316
326
|
}
|
|
@@ -331,6 +341,7 @@ function disableAngularDiagnostics() {
|
|
|
331
341
|
removeComponentInspector = void 0;
|
|
332
342
|
moduleDoubles.clear();
|
|
333
343
|
controllerToken = void 0;
|
|
344
|
+
httpTestingInUse = false;
|
|
334
345
|
openAtReset.length = 0;
|
|
335
346
|
resetTally();
|
|
336
347
|
failOnUnspiedProvider(false);
|
package/dist/angular-http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertAngularInternals, pendingRequestsReport, verifyOnTeardown, beforeTestBedReset } from './chunk-
|
|
1
|
+
import { assertAngularInternals, HTTP_TESTING_BRAND, pendingRequestsReport, verifyOnTeardown, beforeTestBedReset } from './chunk-3JL2YWIG.js';
|
|
2
2
|
import { flushEffects } from './chunk-UH3KWLVL.js';
|
|
3
3
|
import './chunk-GG7BQKEK.js';
|
|
4
4
|
import './chunk-HVNKTPKG.js';
|
|
@@ -7,7 +7,7 @@ import { angularHttpNoRequest, angularHttpPending, angularHttpController, angula
|
|
|
7
7
|
import { withDocs } from './chunk-ENCEEH23.js';
|
|
8
8
|
import { withInterceptors, provideHttpClient } from '@angular/common/http';
|
|
9
9
|
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
|
10
|
-
import { provideEnvironmentInitializer } from '@angular/core';
|
|
10
|
+
import { InjectionToken, provideEnvironmentInitializer } from '@angular/core';
|
|
11
11
|
import { TestBed, getTestBed } from '@angular/core/testing';
|
|
12
12
|
import { onTestFinished } from 'vitest';
|
|
13
13
|
|
|
@@ -67,10 +67,15 @@ function armVerification(ignoreCancelled) {
|
|
|
67
67
|
verifyOnTeardown(() => verifyPending(ignoreCancelled, Object(current)));
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
+
var HTTP_TESTING_MARK = new InjectionToken("vitest-auto-spy provideHttpTesting");
|
|
70
71
|
function provideHttpTesting(options = {}) {
|
|
71
72
|
assertAngularInternals();
|
|
72
73
|
const features = options.interceptors === void 0 ? [] : [withInterceptors([...options.interceptors])];
|
|
73
|
-
const providers = [
|
|
74
|
+
const providers = [
|
|
75
|
+
provideHttpClient(...features, ...options.features ?? []),
|
|
76
|
+
provideHttpClientTesting(),
|
|
77
|
+
{ provide: HTTP_TESTING_MARK, useValue: HTTP_TESTING_BRAND }
|
|
78
|
+
];
|
|
74
79
|
const verify = options.verifyOnTeardown;
|
|
75
80
|
if (verify === false) {
|
|
76
81
|
return providers;
|
|
@@ -146,6 +146,7 @@ function assertAngularInternals() {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// src/lib/angular-pending-requests.ts
|
|
149
|
+
var HTTP_TESTING_BRAND = /* @__PURE__ */ Symbol.for("vitest-auto-spy.provide-http-testing");
|
|
149
150
|
function describe(request) {
|
|
150
151
|
return `${request.method} ${request.urlWithParams}`;
|
|
151
152
|
}
|
|
@@ -185,4 +186,4 @@ function beforeTestBedReset(wrapped, beforeReset) {
|
|
|
185
186
|
});
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
export { assertAngularInternals, beforeTestBedReset, disableTestBedDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, onComponentCreated, onTestingModuleConfigured, pendingRequestsReport, reportSpecTiming, verifyOnTeardown };
|
|
189
|
+
export { HTTP_TESTING_BRAND, assertAngularInternals, beforeTestBedReset, disableTestBedDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, onComponentCreated, onTestingModuleConfigured, pendingRequestsReport, reportSpecTiming, verifyOnTeardown };
|
package/dist/cli.js
CHANGED
|
@@ -3865,7 +3865,7 @@ function checkForeignPragma(graph) {
|
|
|
3865
3865
|
}
|
|
3866
3866
|
|
|
3867
3867
|
// src/cli/checks/export-map.generated.ts
|
|
3868
|
-
var EXPORT_MAP_VERSION = "5.
|
|
3868
|
+
var EXPORT_MAP_VERSION = "5.44.0";
|
|
3869
3869
|
var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular/diagnostics vitest-auto-spy/angular/doubles vitest-auto-spy/angular/matchers vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin vitest-auto-spy/perf-reporter";
|
|
3870
3870
|
var EXPORTED_BY = {
|
|
3871
3871
|
AccessorImplementations: "0 1 2 3 4 12 20 21 22",
|
|
@@ -4570,7 +4570,7 @@ function checkMockResetConfig(profile, graph) {
|
|
|
4570
4570
|
severity: "info",
|
|
4571
4571
|
file,
|
|
4572
4572
|
message: `\`no-redundant-mock-reset\` reads \`${configFile}\`, whose default export is a \`${callee}(\u2026)\` call: the rule reads the file as text, finds no \`clearMocks\` / \`mockReset\` / \`restoreMocks\`, and decides on the runner's defaults instead of what \`${callee}\` sets.`,
|
|
4573
|
-
fix: "Write the flags that config ends up with
|
|
4573
|
+
fix: "Write the flags that config ends up with as `configFlags` in the rule options, e.g. `{ configFile, configFlags: { clearMocks: true } }`; they are read as if the file wrote them, so a builder target that does not load it is still narrowed."
|
|
4574
4574
|
});
|
|
4575
4575
|
}
|
|
4576
4576
|
}
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -2850,21 +2850,38 @@ function meet(left, right) {
|
|
|
2850
2850
|
clearByDefault: clearMocks && left.clearByDefault && right.clearByDefault
|
|
2851
2851
|
};
|
|
2852
2852
|
}
|
|
2853
|
-
function underBuilders(context, found) {
|
|
2854
|
-
const read = (config) =>
|
|
2853
|
+
function underBuilders(context, found, named) {
|
|
2854
|
+
const read = (config) => {
|
|
2855
|
+
if (config === void 0 || !node_fs.existsSync(config)) {
|
|
2856
|
+
return NO_CONFIG;
|
|
2857
|
+
}
|
|
2858
|
+
return config === named?.path ? named.flags : flagsIn(node_fs.readFileSync(config, "utf8"));
|
|
2859
|
+
};
|
|
2855
2860
|
return builderConfigs(context.filename).reduce((flags, config) => meet(flags, settled(context, read(config))), found);
|
|
2856
2861
|
}
|
|
2857
2862
|
function isResetOptions(value) {
|
|
2858
2863
|
return typeof value === "object" && value !== null;
|
|
2859
2864
|
}
|
|
2860
|
-
function namedConfig(context, configFile) {
|
|
2865
|
+
function namedConfig(context, configFile, extra = {}) {
|
|
2861
2866
|
const path = node_path.resolve(context.cwd, configFile);
|
|
2862
2867
|
if (!node_fs.existsSync(path)) {
|
|
2863
2868
|
throw new Error(
|
|
2864
2869
|
`[vitest-auto-spy] no-redundant-mock-reset: the configFile option names ${path}, which does not exist. Point it at the runner config that sets clearMocks / restoreMocks / mockReset, relative to the directory ESLint runs in.`
|
|
2865
2870
|
);
|
|
2866
2871
|
}
|
|
2867
|
-
|
|
2872
|
+
const text = flagsIn(node_fs.readFileSync(path, "utf8"));
|
|
2873
|
+
return {
|
|
2874
|
+
path,
|
|
2875
|
+
flags: {
|
|
2876
|
+
clearMocks: extra.clearMocks ?? text.clearMocks,
|
|
2877
|
+
mockReset: extra.mockReset ?? text.mockReset,
|
|
2878
|
+
restoreMocks: extra.restoreMocks ?? text.restoreMocks
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
function fromConfigFile(context, configFile, extra) {
|
|
2883
|
+
const named = namedConfig(context, configFile, extra);
|
|
2884
|
+
return underBuilders(context, settled(context, named.flags), named);
|
|
2868
2885
|
}
|
|
2869
2886
|
function runnerResets(context) {
|
|
2870
2887
|
const [options] = context.options;
|
|
@@ -2872,7 +2889,7 @@ function runnerResets(context) {
|
|
|
2872
2889
|
const found = nearest(node_path.dirname(context.filename), configCache, configIn);
|
|
2873
2890
|
return found === null ? void 0 : underBuilders(context, settled(context, found));
|
|
2874
2891
|
}
|
|
2875
|
-
const base = options.configFile === void 0 ? { clearMocks: false, mockReset: false, restoreMocks: false, clearByDefault: false } :
|
|
2892
|
+
const base = options.configFile === void 0 ? { clearMocks: false, mockReset: false, restoreMocks: false, clearByDefault: false } : fromConfigFile(context, options.configFile, options.configFlags);
|
|
2876
2893
|
return {
|
|
2877
2894
|
clearMocks: options.clearMocks ?? base.clearMocks,
|
|
2878
2895
|
mockReset: options.mockReset ?? base.mockReset,
|
|
@@ -3004,10 +3021,20 @@ var noRedundantMockReset = defineRule({
|
|
|
3004
3021
|
properties: {
|
|
3005
3022
|
clearMocks: { type: "boolean" },
|
|
3006
3023
|
configFile: { type: "string" },
|
|
3024
|
+
configFlags: {
|
|
3025
|
+
type: "object",
|
|
3026
|
+
properties: {
|
|
3027
|
+
clearMocks: { type: "boolean" },
|
|
3028
|
+
mockReset: { type: "boolean" },
|
|
3029
|
+
restoreMocks: { type: "boolean" }
|
|
3030
|
+
},
|
|
3031
|
+
additionalProperties: false
|
|
3032
|
+
},
|
|
3007
3033
|
mockReset: { type: "boolean" },
|
|
3008
3034
|
restoreMocks: { type: "boolean" }
|
|
3009
3035
|
},
|
|
3010
|
-
additionalProperties: false
|
|
3036
|
+
additionalProperties: false,
|
|
3037
|
+
dependencies: { configFlags: ["configFile"] }
|
|
3011
3038
|
}
|
|
3012
3039
|
],
|
|
3013
3040
|
messages: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.45.0",
|
|
4
4
|
"description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auto-mock",
|
|
@@ -396,6 +396,7 @@
|
|
|
396
396
|
"rxjs": "^7.8.0",
|
|
397
397
|
"tsup": "^8.3.0",
|
|
398
398
|
"typescript": "^6.0.3",
|
|
399
|
+
"undici": "^8.10.2",
|
|
399
400
|
"vitest": "^5.0.0",
|
|
400
401
|
"zone.js": "^0.16.2"
|
|
401
402
|
}
|
|
@@ -188,6 +188,7 @@ it('loads', async () => {
|
|
|
188
188
|
| a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — writes through a writable one, `asReadonly()` included; an `input()` is refused |
|
|
189
189
|
| a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading`; `value()` after `fail()` throws |
|
|
190
190
|
| the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
|
|
191
|
+
| an interceptor under test: `provideHttpClient(withInterceptors([...]))` + `provideHttpClientTesting()` | `provideHttpTesting({ interceptors: [fn], features: [withInterceptorsFromDi()] })` — keeps the end-of-test check |
|
|
191
192
|
| a component or service that reads `ActivatedRoute` | `provideActivatedRoute({ params })`, then `injectActivatedRoute().setParams(…)` — `/angular-router` |
|
|
192
193
|
| a guard or a class built with `new` that takes the route | `createActivatedRoute({ params })` — the same handle, no `TestBed` |
|
|
193
194
|
| a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker |
|
|
@@ -559,7 +560,7 @@ from `vitest-auto-spy/angular-router` rather than at `provideAutoSpy`.
|
|
|
559
560
|
|
|
560
561
|
**`no-self-called-spy` reports a test that calls the spied method itself and then asserts that it was called** — `vi.spyOn(obj, 'm')`, then `obj.m(…)` written by the test body after it, then a positive `toHaveBeenCalled*`. Such a test proves that `emit` calls `emit`: delete the template binding or subscription its title names and it stays green. Drive the real trigger instead and keep the assertion. Order decides it, so a call written **before** the spy is arrangement and is never reported; neither is a negated matcher or `toHaveBeenCalledTimes(0)`, a call whose record a `mockClear` / `vi.clearAllMocks()` drops before the assertion reads it, an assertion whose arguments the call did not pass, a spy installed in a hook, or a call made from inside a callback.
|
|
561
562
|
|
|
562
|
-
**`no-redundant-mock-reset` reports a mock reset in a hook that the runner already performs** — `vi.clearAllMocks()` / `vi.resetAllMocks()` / `vi.restoreAllMocks()` and the per-mock `mockClear()` / `mockReset()` / `mockRestore()` — as the first statement of a `beforeEach` that no other `beforeEach` precedes, or a clear as the last statement of an `afterEach`. Vitest resets in `onBeforeTryTask`, ahead of every `beforeEach` chain and never after a test, so a reset that anything ran before, or a restore in `afterEach` / `afterAll` (the only thing that takes the last test's spies off before the `afterAll` hooks), is left alone. **It is silent until it knows the configuration**: options first — `['error', { clearMocks: true, restoreMocks: true }]`, or `{ configFile: 'path/to/runner.config.ts' }` for a runner config the search cannot find — then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text; with neither, it reports nothing. A config that leaves `clearMocks` out counts it on from Vitest 5, whose default it is; a config a factory or `mergeConfig` assembles in another module is not read through, so
|
|
563
|
+
**`no-redundant-mock-reset` reports a mock reset in a hook that the runner already performs** — `vi.clearAllMocks()` / `vi.resetAllMocks()` / `vi.restoreAllMocks()` and the per-mock `mockClear()` / `mockReset()` / `mockRestore()` — as the first statement of a `beforeEach` that no other `beforeEach` precedes, or a clear as the last statement of an `afterEach`. Vitest resets in `onBeforeTryTask`, ahead of every `beforeEach` chain and never after a test, so a reset that anything ran before, or a restore in `afterEach` / `afterAll` (the only thing that takes the last test's spies off before the `afterAll` hooks), is left alone. **It is silent until it knows the configuration**: options first — `['error', { clearMocks: true, restoreMocks: true }]`, or `{ configFile: 'path/to/runner.config.ts' }` for a runner config the search cannot find — then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text; with neither, it reports nothing. A config that leaves `clearMocks` out counts it on from Vitest 5, whose default it is; a config a factory or `mergeConfig` assembles in another module is not read through, so name its flags in `configFlags` — `{ configFile: 'vitest.config.ts', configFlags: { clearMocks: true } }`, read as if the file wrote them. Pass only what the runner config sets: `setupAutoSpy({ restoreMocks: true })` restores in an `afterEach`, after the test, which is not the runner's `restoreMocks`. The flag has to match the call, not the family: `restoreMocks` walks only the spies `vi.spyOn` installed, so it makes a `vi.clearAllMocks()` dead only where the receiver is such a spy. `--fix` deletes the line only in a file whose only before-hook it is, and offers a suggestion everywhere else; a reset in the middle of a test body is never reported, because there it separates two arrangements inside one test.
|
|
563
564
|
|
|
564
565
|
**`no-unasserted-argument` reports a bare `expect(spy).toHaveBeenCalled()` where the file itself says the arguments are the point** — either the same subject is pinned with `toHaveBeenCalledWith(…)` in another test of the file, or the test's title says `with` and its body asserts nothing but bare calls. Narrower than `vitest/prefer-called-with` on purpose: on one 2032-file suite that reports 1941 times in 360 files and this one 175 in 90. `not.toHaveBeenCalled()` and the counting matchers are never reported. `warn` rather than `error`, because the repair is the argument list the test should have named — `toHaveBeenCalledWith(…)`, `toHaveBeenCalledExactlyOnceWith(…)`, or `mustBeCalledWith(…)` where the double is configured — and that is the one thing the rule cannot write.
|
|
565
566
|
|
|
@@ -606,5 +607,6 @@ decides on a heuristic too: a spec that calls the hook itself does reach the ins
|
|
|
606
607
|
|
|
607
608
|
`doctor` is read-only. It reports what neither the runner nor the compiler can: a `tsconfig`
|
|
608
609
|
`include` pattern that matches no file, a production module importing a spec, a spec importing
|
|
609
|
-
another spec, a foreign runner's `@jest-environment` pragma,
|
|
610
|
-
that
|
|
610
|
+
another spec, a foreign runner's `@jest-environment` pragma, config left behind for a runner that
|
|
611
|
+
is gone, and a `vitest run` that tests another world than `ng test` / `nx test` (a jsdom runner config
|
|
612
|
+
beside a happy-dom builder target, Analog's `setupTestBed()` without the builder's strict flags).
|