vitest-auto-spy 4.6.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -66,6 +66,23 @@ Add-ons, orthogonal to the runner:
66
66
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
67
67
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
68
68
 
69
+ The rxjs peer is **>= 7.2**, not `>=7`. The observable layer used to pull `concatMap`, `delay`,
70
+ `switchMap`, `take`, `takeUntil` and `takeWhile` from `rxjs/operators`; rxjs 8 removes that deep path,
71
+ so the specifier moved to the root `rxjs` entry — which is where rxjs re-exported them in 7.2 — and
72
+ the floor moved with it. Any Angular project already satisfies it: Angular 16 through 22 all peer on
73
+ `^6.5.3 || ^7.4.0`.
74
+
75
+ The Angular entries need **Angular >= 20** — `@angular/core`, `@angular/common` and
76
+ `@angular/platform-browser`, all optional peers on the same range. Below 20 the failure is a link
77
+ error, not a missing helper: `/angular` imports `ɵSIGNAL` (Angular 18+) as a value on the first line
78
+ of its bundle, so on 16 and 17 the whole entry fails to load, and `/bun-angular` imports
79
+ `provideZonelessChangeDetection` (Angular 20+; called `provideExperimentalZonelessChangeDetection`
80
+ in 18 and 19), so on anything older the preload throws before the first spec. Angular 19 and
81
+ everything under it is out of Angular's own support window as well, so the floor cuts nothing that
82
+ still gets fixes. `@angular/platform-browser` is a declared peer since this major — `By` in the
83
+ directive matchers and `platformBrowserTesting()` in the Bun preload are both value imports of it,
84
+ and under pnpm's isolated layout it never resolved before.
85
+
69
86
  The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
70
87
  CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
71
88
  Vitest refuses to be required).
@@ -1135,7 +1152,7 @@ setSpyEngine('runner'); // every double built afterwards is `vi.fn()` per method
1135
1152
  ```
1136
1153
 
1137
1154
  **On Vitest 5 the registry story changes, and this library follows it.** Vitest 5 keeps registered
1138
- mocks behind `WeakRef`s and its `clearAllMocks()` visits only the ones *called* since the last
1155
+ mocks behind `WeakRef`s and its `clearAllMocks()` visits only the ones _called_ since the last
1139
1156
  sweep — so a spy that is not a `vi.fn()` has to make itself reachable to that sweep deliberately. It
1140
1157
  does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMocks: true` default
1141
1158
  (new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
@@ -1145,6 +1162,12 @@ What Vitest 5 does break is the runner's own doing, not this library's: with `cl
1145
1162
  default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
1146
1163
  zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
1147
1164
 
1165
+ **The one exception to "nothing in a spec changes".** `setupAutoSpy({ pruneMockRegistry: true })`
1166
+ discovers long-lived mocks by walking the runner's registry, which Vitest 5 no longer exposes — the
1167
+ automatic half is a no-op there. `keepMockRegistered(mock)` works on every version and is the fix;
1168
+ recommend it whenever a suite on Vitest 5 loses a shared mock's implementation to
1169
+ `vi.resetAllMocks()`.
1170
+
1148
1171
  `getSpyEngine()` reports the current one, `'auto-spy'` is the default, and doubles already built keep
1149
1172
  the engine they were built with. The switch is Vitest-only: on Bun and `node:test` the runner's own
1150
1173
  matchers recognise only the runner's own mocks, so those adapters keep using them.
@@ -1479,6 +1502,14 @@ For the three observers, prefer the purpose-built stubs (§13). For `AbortContro
1479
1502
  in a jsdom run for a reason involving none of the three parties in the stack trace — use
1480
1503
  `stubAbortController()`.
1481
1504
 
1505
+ The stub carries `AbortSignal.abort()`, `AbortSignal.timeout()` and `AbortSignal.any()` as well —
1506
+ the three statics are how modern code makes a signal without a controller, `fetch(url, { signal:
1507
+ AbortSignal.timeout(5_000) })` most of all. `timeout()` aborts through `setTimeout`, so
1508
+ `vi.useFakeTimers()` drives it exactly as it drives the platform's, and it aborts with a
1509
+ `TimeoutError` rather than an `AbortError` because that is the distinction the platform draws. A
1510
+ signal's `reason` is the platform's `DOMException`, so code branching on
1511
+ `signal.reason.name === 'AbortError'` takes the same branch it takes in a browser.
1512
+
1482
1513
  ### `<video>` and `<audio>`
1483
1514
 
1484
1515
  jsdom implements them as a shell: `play()` throws, `duration` is `NaN` and is an accessor with no
@@ -1499,6 +1530,10 @@ expect(media.play).toHaveBeenCalledTimes(1);
1499
1530
 
1500
1531
  State is per element, so an ad and the content report different durations.
1501
1532
 
1533
+ `currentTime` is a get/set pair, so a player restarting itself with `video.currentTime = 0` reaches
1534
+ the record and fires `timeupdate` too — `media.set()` is not the only way in, and the component's own
1535
+ handler runs where it used to stay unrun while the assertion read the new value.
1536
+
1502
1537
  ### A module mock that did nothing
1503
1538
 
1504
1539
  `vi.mock()` is the one thing in a ported suite that fails **silently**. Under a bundler
@@ -1541,7 +1576,8 @@ const myService = injectSpy(MyService); // Spy<MyService>
1541
1576
 
1542
1577
  `provideAutoSpy` defaults to `lazySpies: true` (the plain `createSpyFromClass` does not). Pass
1543
1578
  `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
1544
- zone.js alike.
1579
+ zone.js alike. The entry needs **Angular >= 20** (§1); on 16 or 17 it does not link at all, because
1580
+ `ɵSIGNAL` is not there to import.
1545
1581
 
1546
1582
  ### The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
1547
1583
 
@@ -2066,6 +2102,11 @@ if (process.env['SPEC_TIMING']) {
2066
2102
  preload = ["vitest-auto-spy/bun-angular"]
2067
2103
  ```
2068
2104
 
2105
+ The preload imports `provideZonelessChangeDetection` and `platformBrowserTesting` as values, so it
2106
+ wants **Angular >= 20** and a declared `@angular/platform-browser`; on 18 or 19 it throws while
2107
+ loading, because the zoneless provider was still called
2108
+ `provideExperimentalZonelessChangeDetection` there.
2109
+
2069
2110
  It re-exports everything in this section except `registerSignalMatchers`,
2070
2111
  `registerResourceMatchers`, `mockSignalProp` / `mockResourceProp` and the TestBed diagnostics — the
2071
2112
  matchers and diagnostics need the runner's `expect.extend` and suite-level hooks, and the `mock*Prop`
@@ -2222,7 +2263,7 @@ Spies created **before** the call stay there too. `mock.reset()` in `afterEach`
2222
2263
 
2223
2264
  **A `node:test` spy now prints under its method name.** `mock.fn()` takes no name and has no
2224
2265
  `mockName()`, so a spy inherited the library's internal dispatcher and read back as
2225
- `[Function: dispatch]` everywhere. The adapter names the *implementation* at creation and lets
2266
+ `[Function: dispatch]` everywhere. The adapter names the _implementation_ at creation and lets
2226
2267
  `mock.fn()` carry that name onto the mock, which is where a `node:test` mock takes its `name` from;
2227
2268
  `displayName` is set on the mock alongside it. The name survives `mock.reset()`, `mock.restore()`,
2228
2269
  `resetCalls()` and a `mockImplementation()` swap, and it is what `node:assert` diffs, what
@@ -2480,8 +2521,8 @@ packages, which a subpath export can never be.
2480
2521
  | `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
2481
2522
  | `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
2482
2523
  | a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
2483
- | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2484
- | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
2524
+ | `names method(s) that are not on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2525
+ | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
2485
2526
  | `toHaveBeenCalledTimes` reads `0` for a call an earlier test made, after moving to Vitest 5 | `clearMocks` defaults to `true` on Vitest 5, so `vi.clearAllMocks()` runs before every test | count the calls in a plain variable instead of reading the spy, or set `clearMocks: false` (§10) |
2486
2527
  | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2487
2528
  | `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
@@ -2509,7 +2550,7 @@ packages, which a subpath export can never be.
2509
2550
  | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2510
2551
  | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
2511
2552
  | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
2512
- | `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
2553
+ | `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
2513
2554
  | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
2514
2555
  | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2515
2556
  | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
@@ -2740,6 +2781,10 @@ migrated by hand. Run it after `--write`, and again after any manual clean-up. `
2740
2781
  select transforms by id, `--list` prints them. Full reference:
2741
2782
  <https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
2742
2783
 
2784
+ Past 50 000 files the repository scan truncates and the run says so — *Nothing left to migrate* off a
2785
+ truncated list is a claim about a tree the tool never looked at. `VITEST_AUTO_SPY_SCAN_CAP` raises
2786
+ the cap.
2787
+
2743
2788
  ### If you are writing a codemod over specs
2744
2789
 
2745
2790
  Two traps, both found the hard way on rxjs-heavy code.
package/README.md CHANGED
@@ -23,7 +23,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
23
23
  [![downloads per month](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2Flast-month%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2Fmonth)](https://www.npmjs.com/package/vitest-auto-spy)
24
24
  [![downloads over 18 months](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2F2026-06-21%3A2030-01-01%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2F18mo)](https://www.npmjs.com/package/vitest-auto-spy)
25
25
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
26
- [![minzipped size](https://img.shields.io/badge/minzip-15.0%20kB-brightgreen)](#install)
26
+ [![minzipped size](https://img.shields.io/badge/minzip-15.1%20kB-brightgreen)](#install)
27
27
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
28
28
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
29
29
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -86,6 +86,23 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
86
86
  - 🤖 Built for AI agents too — one `npx vitest-auto-spy init` writes the pointer into the files your agents actually read and specialises it for this repository, backed by an offline [`AGENTS.md`](#using-this-library-with-an-ai-agent) inside the package, a [per-agent map](#which-file-your-agent-reads) for **Claude Code**, **OpenAI Codex**, **GLM/z.ai**, **Cursor**, **Copilot**, **Gemini CLI** and the rest, `llms.txt` on the docs site, a Claude Code skill, and errors that name their own fix
87
87
  - 🟢 100% test coverage, **zero runtime dependencies** (in-tree arg serializer, no `javascript-stringify`)
88
88
 
89
+ ## New in 5.0
90
+
91
+ A major that changes **no helper, no option and no runtime behaviour**. What it changes is what the
92
+ package claims to run on, because both old peer ranges were claims the code could not keep — the
93
+ full list, with what to do about each, is in
94
+ [Upgrading to 5.0](https://asdalexey.github.io/vitest-auto-spy/upgrading-5).
95
+
96
+ | What you get | Verified |
97
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
98
+ | **The Angular range stops being false.** `>=16.0.0` admitted four majors on which an entry point cannot link at all: `ɵSIGNAL` is a value import and arrived in 18, `provideZonelessChangeDetection` in 20. The floor is now `>=20` | Angular 16 through 22 downloaded, their real `export { … }` lists parsed out of `fesm2022/*.mjs`; Angular 19 went EOL on 2026-05-19 |
99
+ | **`@angular/platform-browser` is a declared peer.** `/angular` has always imported `By` from it as a value, and under pnpm's isolated layout that never resolved | undeclared → optional peer on the same `>=20` range |
100
+ | **The rxjs range stops promising rxjs 8.** Six operators came from `rxjs/operators`, a path rxjs 8 removes; they now come from the root entry, where rxjs re-exported them in 7.2, and the floor moved with the specifier | read out of rxjs 7.2.0's own `dist/types/index.d.ts`; every Angular major from 16 to 22 already peers above it |
101
+ | **`flushEffects()` is one line.** The `ApplicationRef.tick()` fallback and the spec that deleted `TestBed.tick` at runtime to cover it are both gone | `TestBed.tick()` direct; coverage still 100 % |
102
+
103
+ For almost everyone the whole upgrade is a version number: every Angular still supported by Angular
104
+ satisfies the new floor, and only a project pinning `rxjs@7.0` or `7.1` on purpose has to move.
105
+
89
106
  ## New in 4.0
90
107
 
91
108
  A major with **one job**: take out of your project the weight this library was making it carry.
@@ -210,8 +227,9 @@ npm i -D vitest-auto-spy
210
227
 
211
228
  | Tool | Minimum |
212
229
  | ---------- | --------------------------------------------------------------------------------- |
213
- | Node.js | ≥ 18 for the library — in practice, whatever your runner needs |
230
+ | Node.js | ≥ 22 — 18 and 20 are EOL; CI exercises 22, 24 and 26 |
214
231
  | Vitest | ≥ 2.1 (required peer) — one install covers 2.1 through 5.x |
232
+ | Angular | ≥ 20 for the Angular entry points — optional peer, nothing else needs it |
215
233
  | Bun | ≥ 1.4 for `vitest-auto-spy/bun-angular`; any recent Bun for `vitest-auto-spy/bun` |
216
234
  | TypeScript | ≥ 4.7 for the typed helpers (plain JS works too, just untyped) |
217
235
 
@@ -228,7 +246,12 @@ mocks that were called, so a spy engine that is not `vi.fn()` had to make itself
228
246
  sweep; and the `Matchers` interface grew a second type parameter, so the bundled matchers declare
229
247
  themselves on Chai's `Assertion` instead and keep typing on 4 and 5 alike. The one thing Vitest 5
230
248
  can still break is its own doing: `clearMocks` is on by default there, so a spec asserting on a call
231
- recorded by an *earlier* test now reads zero. Record it in a counter, not in the spy.
249
+ recorded by an _earlier_ test now reads zero. Record it in a counter, not in the spy.
250
+
251
+ One feature of this package does need a line changed. `setupAutoSpy({ pruneMockRegistry: true })`
252
+ finds long-lived mocks by walking the runner's own registry, and Vitest 5 no longer exposes it — so
253
+ on Vitest 5 the automatic half does nothing and a mock that must survive `vi.resetAllMocks()` has to
254
+ say so: `keepMockRegistered(vi.fn().mockReturnThis())`. Everything after that mark behaves as before.
232
255
 
233
256
  Measured 2026-09-06 on 40 spec files, 800 tests, 400 spied methods per file, v8 coverage on,
234
257
  Node v24.19.0, median of five runs: the same suite takes **1383 ms on Vitest 4.1.11 and 1276 ms on
@@ -237,11 +260,12 @@ shipped since 4.1 is worth another **−6.1 % on Vitest 4 and −8.1 % on Vitest
237
260
  every method with the runner's own `vi.fn()` (`setSpyEngine('runner')`). End to end, the slowest
238
261
  pairing to the fastest is 1473 ms → 1276 ms, **−13.4 %**.
239
262
 
240
- Node **≥ 18** is the library's own floor and it holds — the published output is ES2022 and every
241
- entry runs on 18. What moves the real minimum is the runner: **Vitest 4 cannot start on Node 18 at
242
- all**, because Vite 7 calls `crypto.hash` (added in Node 20.12) and the run dies with
243
- `TypeError: crypto.hash is not a function` before a spec loads. On Vitest ≤ 3 Node 18 is fine. Which
244
- version to actually run is measured in
263
+ Node **≥ 22** is the library's own floor now — Node 18 and 20 are both past end of life, and every
264
+ runner this library supports already needed more: Vitest 4 pulls in Vite 7 (`^20.19.0 ||
265
+ >=22.12.0`) and calls `crypto.hash` (added in Node 20.12), so Node 18 died with `TypeError:
266
+ crypto.hash is not a function` before a spec loads. Vitest 5 raises its own floor to `^22.12.0 ||
267
+ ^24.0.0 || >=26.0.0`, and `@angular/build` 22 needs `^22.22.3 || ^24.15.0 || >=26.0.0`. CI tests
268
+ Node 22, 24 and 26, so 22 is what is actually exercised. Which version to run is measured in
245
269
  [Performance → Which Node version](https://vitest-auto-spy.dev/core/performance#which-node-version):
246
270
  the break is between 22 and 24, where a cold import more than halves.
247
271
 
@@ -255,14 +279,54 @@ the published package roughly in half.
255
279
 
256
280
  ### Peer dependencies
257
281
 
258
- All peers are **provided by your project**; `rxjs` and `@angular/core` are **optional** — install
259
- them only for the matching entry point. The package itself has **zero runtime dependencies**.
260
-
261
- | Peer | Needed for | Optional? |
262
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------- |
263
- | `vitest` | the default runner | no |
264
- | `rxjs` | `vitest-auto-spy/rxjs` observable spies — `>=7`, **no upper bound** (the rxjs 8 line included); not needed to type-check a spy since 4.0.0 | yes |
265
- | `@angular/core` | `vitest-auto-spy/angular` helpers | yes |
282
+ All peers are **provided by your project**; `rxjs` and the three `@angular/*` packages are
283
+ **optional** — install them only for the matching entry point. The package itself has **zero runtime
284
+ dependencies**.
285
+
286
+ | Peer | Needed for | Optional? |
287
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
288
+ | `vitest` | the default runner | no |
289
+ | `rxjs` | `vitest-auto-spy/rxjs` observable spies — `>=7.2`, **no upper bound** (the rxjs 8 line included); not needed to type-check a spy since 4.0.0 | yes |
290
+ | `@angular/core` | `vitest-auto-spy/angular` and `vitest-auto-spy/bun-angular` helpers — `>=20` | yes |
291
+ | `@angular/common` | `vitest-auto-spy/angular-http` — `>=20`, that entry only | yes |
292
+ | `@angular/platform-browser` | `By` and the directive matchers on `vitest-auto-spy/angular`, and the Bun preload's platform — `>=20` | yes |
293
+
294
+ **rxjs ≥ 7.2**, and that number moved because an import specifier did. The observable layer builds
295
+ its subjects out of six operators — `concatMap`, `delay`, `switchMap`, `take`, `takeUntil`,
296
+ `takeWhile` — and it took them from `rxjs/operators`, the legacy deep path. **rxjs 8 removes that
297
+ path entirely**, so the old open-ended `>=7.0.0` was promising a version the code could not have
298
+ served. The operators now come from the root `rxjs` entry, which is where rxjs itself re-exported
299
+ them in **7.2** — checked against rxjs 7.2.0's own `dist/types/index.d.ts`, not against its release
300
+ notes. `firstValueFrom`, which is what justified the old 7.0 floor, is still there; 7.2 is simply the
301
+ first version where every symbol this package imports exists at the specifier it imports it from.
302
+
303
+ It costs an Angular consumer nothing. Every Angular major from 16 to 22 declares its own rxjs peer
304
+ as `^6.5.3 || ^7.4.0`, which is already above what this asks — so only a project that pins `rxjs@7.0`
305
+ or `7.1` on purpose has anything to do.
306
+
307
+ **Angular ≥ 20**, and two symbols the shipped code imports **as values** are what set the number. A
308
+ missing value import is a link error, so the symptom is never one unavailable helper — it is an
309
+ entry that does not load:
310
+
311
+ - **`ɵSIGNAL` exists from Angular 18.** `runEffect()` reads it, and that import is the first line of
312
+ the `/angular` bundle, evaluated eagerly. On Angular 16 or 17 the entry fails to link, taking
313
+ `provideAutoSpy`, `injectSpy` and the rest with it — not just `runEffect`.
314
+ - **`provideZonelessChangeDetection` exists from Angular 20.** In 18 and 19 the same function was
315
+ named `provideExperimentalZonelessChangeDetection`; in 16 and 17 there was nothing to name.
316
+ `vitest-auto-spy/bun-angular` imports it by name, so below 20 `bun test` dies loading the preload,
317
+ before any spec file.
318
+
319
+ Angular 20 is also the oldest Angular that Angular still supports — six months active plus twelve
320
+ months LTS put 19 past end of life on **2026-05-19**, and 18, 17 and 16 before it. The technical
321
+ floor and the supported floor are the same number, so nothing still receiving fixes is cut off; the
322
+ range this replaces said `>=16.0.0`, which was a promise on versions where the main entry could not
323
+ link. No upper bound is set on purpose — one would force a release per Angular major and hand
324
+ `ERESOLVE` to whoever upgraded first.
325
+
326
+ `@angular/platform-browser` is declared for the first time here. `vitest-auto-spy/angular` has
327
+ always imported `By` from it as a value, and the Bun preload boots through `platformBrowserTesting()`.
328
+ Under npm's hoisted `node_modules` it resolved by accident — every Angular workspace has it — while
329
+ under pnpm's isolated layout it did not resolve at all. Declaring it makes the accident a contract.
266
330
 
267
331
  ## The CLI — `doctor`, `perf`, `codemod` and `init`
268
332
 
@@ -412,7 +476,9 @@ enough for `--from auto` to classify a file, for exactly the same reason — tha
412
476
 
413
477
  **Dry-run by default**, so the first thing a repository sees is a diff it can reject. `--write`
414
478
  applies it, `--only` / `--skip` select transforms by id, `--list` prints them together with the
415
- generated entry-point table.
479
+ generated entry-point table. Past 50 000 files the scan truncates and says so, because *Nothing left
480
+ to migrate* off a truncated list is a claim about a tree it never looked at;
481
+ `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
416
482
 
417
483
  What it deliberately does not do is guess. A `jest.*` member with no `vi` twin — `requireMock`,
418
484
  `replaceProperty`, `createMockFromModule`, `jest.setTimeout`, `requireActual` — is **left exactly as
@@ -1079,26 +1145,26 @@ the conditional types that pick helpers from a return type — see
1079
1145
  The library ships a framework-agnostic core plus runtime and framework layers, so a plain
1080
1146
  Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runtime bundle**:
1081
1147
 
1082
- | Import | Provides | Pulls in | Status |
1083
- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | :----: |
1084
- | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
1085
- | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) and `createObservableWithValues` | `rxjs` | ✅ |
1086
- | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, `mockResourceProp`, the `mock*Prop` helpers, signal & resource matchers, TestBed diagnostics | `@angular/core` | ✅ |
1087
- | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1088
- | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
1089
- | `vitest-auto-spy/bun-angular` | Angular's `TestBed` under `bun test` — DOM, JIT `templateUrl` resolution and a zoneless environment, from one preload | `bun:test`, `@angular/core` | ✅ |
1090
- | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()`, plus `trackNodeMocks()` — a private `MockTracker` so dropped spies are actually freed | `node:test` | ✅ |
1091
- | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
1092
- | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
1093
- | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
1094
- | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
1095
- | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
1096
- | `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` | ✅ |
1097
- | `vitest-auto-spy/jasmine-compat` | `enableJasmineCompat()` alone — the same `.and` / `.calls` layer, registering no adapter, for `bun test` and `node --test` | — (your runner) | ✅ |
1098
- | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` / `ObserverSpy` / `SubscriberSpy` — the `@hirez_io/observer-spy` surface, so `/rxjs` does not carry it | `rxjs` | ✅ |
1099
- | `vitest-auto-spy/setup` | `setupAutoSpy()` — property restore, duplicate-copy detection and mock-registry hygiene in one call; `setupFakeTimers()` / `advanceTimers()` | `vitest` | ✅ |
1100
- | `vitest-auto-spy/zone` | `fakeAsync` / `waitForAsync` on Vitest — the ProxyZone patch `zone.js/testing` does not ship. Reads the `zone.js` **you** loaded; imports none of it | — (your `zone.js`) | ✅ |
1101
- | `vitest-auto-spy/eslint-plugin` | the lint rules that steer a suite onto these helpers | — (your `eslint`) | ✅ |
1148
+ | Import | Provides | Pulls in | Status |
1149
+ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | :----: |
1150
+ | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
1151
+ | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) and `createObservableWithValues` | `rxjs` | ✅ |
1152
+ | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, `mockResourceProp`, the `mock*Prop` helpers, signal & resource matchers, TestBed diagnostics | `@angular/core`, `@angular/platform-browser` | ✅ |
1153
+ | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1154
+ | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
1155
+ | `vitest-auto-spy/bun-angular` | Angular's `TestBed` under `bun test` — DOM, JIT `templateUrl` resolution and a zoneless environment, from one preload | `bun:test`, `@angular/core`, `@angular/platform-browser` | ✅ |
1156
+ | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()`, plus `trackNodeMocks()` — a private `MockTracker` so dropped spies are actually freed | `node:test` | ✅ |
1157
+ | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
1158
+ | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
1159
+ | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
1160
+ | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
1161
+ | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
1162
+ | `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` | ✅ |
1163
+ | `vitest-auto-spy/jasmine-compat` | `enableJasmineCompat()` alone — the same `.and` / `.calls` layer, registering no adapter, for `bun test` and `node --test` | — (your runner) | ✅ |
1164
+ | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` / `ObserverSpy` / `SubscriberSpy` — the `@hirez_io/observer-spy` surface, so `/rxjs` does not carry it | `rxjs` | ✅ |
1165
+ | `vitest-auto-spy/setup` | `setupAutoSpy()` — property restore, duplicate-copy detection and mock-registry hygiene in one call; `setupFakeTimers()` / `advanceTimers()` | `vitest` | ✅ |
1166
+ | `vitest-auto-spy/zone` | `fakeAsync` / `waitForAsync` on Vitest — the ProxyZone patch `zone.js/testing` does not ship. Reads the `zone.js` **you** loaded; imports none of it | — (your `zone.js`) | ✅ |
1167
+ | `vitest-auto-spy/eslint-plugin` | the lint rules that steer a suite onto these helpers | — (your `eslint`) | ✅ |
1102
1168
 
1103
1169
  ✅ all entry points published (see [Availability](#availability)).
1104
1170
 
@@ -1341,26 +1407,26 @@ time budget — these cases allocate doubles by the tens of thousands, and GC co
1341
1407
  created rather than elapsed time, so a fixed time budget would have handed a faster arm more
1342
1408
  allocations and made it pay for its own speed.
1343
1409
 
1344
- **Every micro-benchmark table is this library's**, and the margin is against the *best* other arm in
1410
+ **Every micro-benchmark table is this library's**, and the margin is against the _best_ other arm in
1345
1411
  that table. The three class sizes are measured project profiles, not round numbers — across four
1346
1412
  private Angular suites (~2 700 spec files, 2 742 doubles built from a class) the median service has
1347
1413
  5-8 methods and the spec touches 1 of them:
1348
1414
 
1349
- | Case (micro) | 4.0 | 4.1 | best other arm | lead |
1350
- | --- | ---: | ---: | ---: | ---: |
1351
- | small project — 6 methods, 1 called | — | **1.42 µs** | 6.83 µs hand-written | 4.82x |
1352
- | medium project — 14 methods, 2 called | — | **2.67 µs** | 15.92 µs hand-written | 5.97x |
1353
- | large project — 45 methods, 2 called | — | **5.79 µs** | 52.79 µs hand-written | 9.11x |
1354
- | worst case — all 14 of 14 methods called | 18.92 µs (0.66x, a loss) | **8.17 µs** | 17.92 µs hand-written | 2.19x |
1355
- | worst case — all 45 of 45 methods called | 75.33 µs (0.71x, a loss) | **26.12 µs** | 62.04 µs hand-written | 2.37x |
1356
- | double from a type — 2 / 10 / 40 members | 3.58 / 18.17 / 72.88 µs (a loss) | **1.00 / 4.67 / 18.92 µs** | 2.79 / 13.83 / 56.79 µs vitest-mock-extended | 2.79-3.00x |
1357
- | deep double, 3 levels, leaf called | 8.83 µs (0.61x, a loss) | **2.29 µs** | 5.46 µs vitest-mock-extended | 2.38x |
1358
- | configure a return + 3 calls — class / type | 3.29 / 2.08 µs (type a loss) | **2.21 / 0.71 µs** | 16.38 µs hand-written / 1.58 µs @golevelup | 7.41x / 2.24x |
1359
- | `calledWith` dispatch, 2 configured + 1 miss | 0.54 µs (parity) | **0.17 µs** | 0.54 µs vitest-mock-extended | 3.25x |
1415
+ | Case (micro) | 4.0 | 4.1 | best other arm | lead |
1416
+ | -------------------------------------------- | -------------------------------: | -------------------------: | -------------------------------------------: | ------------: |
1417
+ | small project — 6 methods, 1 called | — | **1.42 µs** | 6.83 µs hand-written | 4.82x |
1418
+ | medium project — 14 methods, 2 called | — | **2.67 µs** | 15.92 µs hand-written | 5.97x |
1419
+ | large project — 45 methods, 2 called | — | **5.79 µs** | 52.79 µs hand-written | 9.11x |
1420
+ | worst case — all 14 of 14 methods called | 18.92 µs (0.66x, a loss) | **8.17 µs** | 17.92 µs hand-written | 2.19x |
1421
+ | worst case — all 45 of 45 methods called | 75.33 µs (0.71x, a loss) | **26.12 µs** | 62.04 µs hand-written | 2.37x |
1422
+ | double from a type — 2 / 10 / 40 members | 3.58 / 18.17 / 72.88 µs (a loss) | **1.00 / 4.67 / 18.92 µs** | 2.79 / 13.83 / 56.79 µs vitest-mock-extended | 2.79-3.00x |
1423
+ | deep double, 3 levels, leaf called | 8.83 µs (0.61x, a loss) | **2.29 µs** | 5.46 µs vitest-mock-extended | 2.38x |
1424
+ | configure a return + 3 calls — class / type | 3.29 / 2.08 µs (type a loss) | **2.21 / 0.71 µs** | 16.38 µs hand-written / 1.58 µs @golevelup | 7.41x / 2.24x |
1425
+ | `calledWith` dispatch, 2 configured + 1 miss | 0.54 µs (parity) | **0.17 µs** | 0.54 µs vitest-mock-extended | 3.25x |
1360
1426
 
1361
1427
  Six of those rows were losses and one was parity. What changed is that a method spy is no longer a
1362
1428
  `vi.fn()` — see [The spy engine](#the-spy-engine) — plus one thing that is not the engine: a
1363
- `calledWith(x)` of a single primitive argument used to be rendered into a string key on *every call*
1429
+ `calledWith(x)` of a single primitive argument used to be rendered into a string key on _every call_
1364
1430
  of that spy, where a `Map` keyed by the value does the same lookup with no allocation.
1365
1431
 
1366
1432
  The three class sizes changed in the same release, from round numbers to measured profiles, so the
@@ -1373,10 +1439,10 @@ vi.fn() per method` arm is in the table precisely so its size stays visible.
1373
1439
  the 4.1 build, 20-method class, `isolate: true`, coverage on, 3 rounds per cell after a discarded
1374
1440
  warm-up, medians of the three):
1375
1441
 
1376
- | Comparison | 1 000 tests | 3 000 tests | 10 000 tests |
1377
- | --- | ---: | ---: | ---: |
1378
- | vitest-auto-spy | 1.33 s | 2.86 s | 10.02 s |
1379
- | hand-written `vi.fn()` | 1.31 s (0.99×) | 2.85 s (1.00×) | 9.25 s (0.92×) |
1442
+ | Comparison | 1 000 tests | 3 000 tests | 10 000 tests |
1443
+ | --------------------------- | -------------: | -------------: | --------------: |
1444
+ | vitest-auto-spy | 1.33 s | 2.86 s | 10.02 s |
1445
+ | hand-written `vi.fn()` | 1.31 s (0.99×) | 2.85 s (1.00×) | 9.25 s (0.92×) |
1380
1446
  | @bugsplat/vitest-auto-spies | 2.00 s (1.50×) | 4.61 s (1.61×) | 15.45 s (1.54×) |
1381
1447
 
1382
1448
  Hand-written doubles are still cheaper — **about 3% at the median**, down from 10-15% before 4.1,
@@ -1387,11 +1453,11 @@ suite swamps it.
1387
1453
 
1388
1454
  **Memory, `isolate: false`, 100-method class, 10 000 tests, peak RSS:**
1389
1455
 
1390
- | Arm | Peak RSS |
1391
- | --- | ---: |
1392
- | hand-written `vi.fn()` | 6366 MB |
1393
- | vitest-auto-spy, default | 2103 MB |
1394
- | vitest-auto-spy, `lazySpies: 'proxy'` | 1851 MB |
1456
+ | Arm | Peak RSS |
1457
+ | ------------------------------------- | -------: |
1458
+ | hand-written `vi.fn()` | 6366 MB |
1459
+ | vitest-auto-spy, default | 2103 MB |
1460
+ | vitest-auto-spy, `lazySpies: 'proxy'` | 1851 MB |
1395
1461
 
1396
1462
  Hand-written doubles use 3.0x the default's peak RSS here — the difference between finishing and an
1397
1463
  OOM'd worker on a large suite.
@@ -1413,7 +1479,7 @@ Full write-up, methodology and the complete suite-scale tables:
1413
1479
 
1414
1480
  ### Reproducing the numbers
1415
1481
 
1416
- Node **>=18** (the project floor); the numbers above were captured on Node v24.19.0. The steps are
1482
+ Node **>=22** (the project floor); the numbers above were captured on Node v24.19.0. The steps are
1417
1483
  identical on Windows, macOS and Linux — same Node, same npm, same commands, nothing OS-specific to
1418
1484
  call out:
1419
1485
 
@@ -2508,7 +2574,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
2508
2574
  | `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
2509
2575
  | `setupAngularTestEnv(opts)` | `/angular` | Zone and zoneless spec files in one worker, switching platforms per file |
2510
2576
  | `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
2511
- | `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
2577
+ | `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
2512
2578
  | `trackMockRegistry()` / `keepMockRegistered(mock)` / `restoreLongLivedImplementations()` | `/setup` | Keep @vitest/spy's mock registry to the mocks that outlive a file; mark one the split would miss; put back an implementation a cross-file `vi.resetAllMocks()` dropped ([details](#test-run-hygiene)) |
2513
2579
  | `trackNodeMocks()` / `pruneNodeMocks()` / `countNodeMocks()` | `/node` | Give this library its own `node:test` `MockTracker` so a dropped spy is freed — 21× less retained heap; sweep by hand, and read the count back |
2514
2580
  | `setSpyEngine(engine)` / `getSpyEngine()` | `/setup` | Build method spies from this library's own mock (`'auto-spy'`, the default) or from `vi.fn()` (`'runner'`) ([details](#the-spy-engine)) |
@@ -2752,10 +2818,10 @@ expensive to diagnose when it is missing. The first three are on by default:
2752
2818
  so the 404 is never written and a routing mistake is reported as a slow test.
2753
2819
  11. **Web Storage the runner never handed over.** On by default, because it only ever repairs.
2754
2820
  Vitest copies a DOM environment's globals onto `globalThis` behind `if (k in global) return
2755
- KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
2821
+ KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
2756
2822
  arrive only because Node put neither on `globalThis`, so the first half was false. Node's own
2757
2823
  Web Storage made the key exist, and now the environment's storage never arrives: `setItem is
2758
- not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
2824
+ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
2759
2825
  filter runs before either. A suite stays green with this broken — only the specs that touch
2760
2826
  storage fail — so it arrives as "CI moved to a new Node and eleven unrelated specs died". The
2761
2827
  repair decides by using the storage rather than looking at it, replaces only one that cannot
@@ -2773,7 +2839,7 @@ expensive to diagnose when it is missing. The first three are on by default:
2773
2839
  | `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
2774
2840
  | `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
2775
2841
  | `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
2776
- | `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
2842
+ | `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
2777
2843
  | `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
2778
2844
  | `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
2779
2845
  | `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
@@ -3192,7 +3258,7 @@ another, and the next one is in that same file.
3192
3258
  | `asInstance(spy)` / `asSpy(instance)` | The two named views between `Spy<T>` / `DeepMockProxy<T>` and `T`, instead of `as any` |
3193
3259
  | `createSpyClass(Class, config?)` | A spy that can be called with `new`; records `calls` and `instances` |
3194
3260
  | `mockConstructor(factory, name?)` / `stubConstructor(obj, key, factory)` | A runner mock that is also a constructor — for a global or an SDK with no class at runtime |
3195
- | `stubAbortController()` | A realm-consistent `AbortController` / `AbortSignal` for jsdom + zone.js |
3261
+ | `stubAbortController()` | A realm-consistent `AbortController` / `AbortSignal` for jsdom + zone.js, statics (`abort`, `timeout`, `any`) included |
3196
3262
  | `flushEventLoop(turns?)` / `settleDynamicImport(load, turns?)` | Real event-loop turns under fake timers; wait for a dynamic `import()` |
3197
3263
  | `autoMocked<T>(overrides?)` | `createAutoMock` typed as `T & Spy<T>` |
3198
3264
  | `mockSystemTime` / `withSystemTime` / `mockNow` / `useCountingClock` _(`/setup`)_ | Clock control that survives fake timers being re-installed per test |
@@ -1,14 +1,25 @@
1
- import { flushEffects } from './chunk-5BPM3BLI.js';
1
+ import { flushEffects } from './chunk-ALFMIDWW.js';
2
2
  import { withDocs, DOCS_LINKS } from './chunk-B45HLBA4.js';
3
3
  import { provideHttpClient } from '@angular/common/http';
4
4
  import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
5
5
  import { TestBed } from '@angular/core/testing';
6
6
  import { afterEach } from 'vitest';
7
7
 
8
+ function verifyOnTeardown(check) {
9
+ try {
10
+ check();
11
+ } catch (error) {
12
+ TestBed.resetTestingModule();
13
+ throw error;
14
+ }
15
+ }
16
+ performance.now.bind(performance);
17
+
18
+ // src/lib/angular-http.ts
8
19
  var SETTLE_ROUNDS = 2;
9
- var verifyArmed = false;
20
+ var verifyPolicy = false;
10
21
  function provideHttpTesting(options = {}) {
11
- verifyArmed = options.verifyOnTeardown ?? true;
22
+ verifyPolicy = options.verifyOnTeardown ?? true;
12
23
  return [provideHttpClient(), provideHttpClientTesting()];
13
24
  }
14
25
  function describeRequest(request) {
@@ -136,11 +147,10 @@ Answer it (\`await expectRequest('/url').flush(body)\`), assert it is absent (\`
136
147
  );
137
148
  }
138
149
  afterEach(() => {
139
- if (!verifyArmed) {
150
+ if (!verifyPolicy) {
140
151
  return;
141
152
  }
142
- verifyArmed = false;
143
- verifyNoPendingRequests();
153
+ verifyOnTeardown(verifyNoPendingRequests);
144
154
  });
145
155
 
146
156
  export { expectNoRequest, expectRequest, provideHttpTesting, verifyNoPendingRequests };
package/dist/angular.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-LsBDcSNZ.js';
1
+ export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-BFq3ve6Q.js';
2
2
  import { InjectionToken, Provider, Type, Signal, WritableSignal } from '@angular/core';
3
3
  import { TestAPI } from 'vitest';
4
4
  import { C as ClassType, S as Spy, k as ClassSpyConfiguration, o as OnlyMethodKeysOf, g as AddSpyMethodsByReturnTypes } from './types-CQLPjw8z.js';
@@ -57,8 +57,8 @@ interface ExtendWithAutoSpiesOptions {
57
57
  * Providers registered alongside the spies, in the same `configureTestingModule` call — the
58
58
  * component under test, a real service the spec deliberately keeps, `provideHttpClient()`.
59
59
  *
60
- * They go in *before* the generated ones, so a provider named here wins over the auto-spy that
61
- * would otherwise be made for the same token.
60
+ * They go in *after* the generated ones, so a provider named here wins over the auto-spy that
61
+ * would otherwise be made for the same token — Angular resolves duplicate providers last-one-wins.
62
62
  */
63
63
  providers?: Provider[];
64
64
  }