vitest-auto-spy 4.6.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -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).
@@ -1485,6 +1502,14 @@ For the three observers, prefer the purpose-built stubs (§13). For `AbortContro
1485
1502
  in a jsdom run for a reason involving none of the three parties in the stack trace — use
1486
1503
  `stubAbortController()`.
1487
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
+
1488
1513
  ### `<video>` and `<audio>`
1489
1514
 
1490
1515
  jsdom implements them as a shell: `play()` throws, `duration` is `NaN` and is an accessor with no
@@ -1505,6 +1530,10 @@ expect(media.play).toHaveBeenCalledTimes(1);
1505
1530
 
1506
1531
  State is per element, so an ad and the content report different durations.
1507
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
+
1508
1537
  ### A module mock that did nothing
1509
1538
 
1510
1539
  `vi.mock()` is the one thing in a ported suite that fails **silently**. Under a bundler
@@ -1547,7 +1576,8 @@ const myService = injectSpy(MyService); // Spy<MyService>
1547
1576
 
1548
1577
  `provideAutoSpy` defaults to `lazySpies: true` (the plain `createSpyFromClass` does not). Pass
1549
1578
  `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
1550
- 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.
1551
1581
 
1552
1582
  ### The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
1553
1583
 
@@ -2072,6 +2102,11 @@ if (process.env['SPEC_TIMING']) {
2072
2102
  preload = ["vitest-auto-spy/bun-angular"]
2073
2103
  ```
2074
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
+
2075
2110
  It re-exports everything in this section except `registerSignalMatchers`,
2076
2111
  `registerResourceMatchers`, `mockSignalProp` / `mockResourceProp` and the TestBed diagnostics — the
2077
2112
  matchers and diagnostics need the runner's `expect.extend` and suite-level hooks, and the `mock*Prop`
@@ -2486,7 +2521,7 @@ packages, which a subpath export can never be.
2486
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) |
2487
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)` |
2488
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) |
2489
- | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
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` |
2490
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) |
2491
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) |
2492
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` |
@@ -2746,6 +2781,10 @@ migrated by hand. Run it after `--write`, and again after any manual clean-up. `
2746
2781
  select transforms by id, `--list` prints them. Full reference:
2747
2782
  <https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
2748
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
+
2749
2788
  ### If you are writing a codemod over specs
2750
2789
 
2751
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,7 +86,24 @@ 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 4.0
89
+ ## New in 5
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
+
106
+ ## New in 4
90
107
 
91
108
  A major with **one job**: take out of your project the weight this library was making it carry.
92
109
  Nothing was removed or renamed and no runtime behaviour changed — the whole cost is two import
@@ -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
 
@@ -242,11 +260,12 @@ shipped since 4.1 is worth another **−6.1 % on Vitest 4 and −8.1 % on Vitest
242
260
  every method with the runner's own `vi.fn()` (`setSpyEngine('runner')`). End to end, the slowest
243
261
  pairing to the fastest is 1473 ms → 1276 ms, **−13.4 %**.
244
262
 
245
- Node **≥ 18** is the library's own floor and it holds — the published output is ES2022 and every
246
- entry runs on 18. What moves the real minimum is the runner: **Vitest 4 cannot start on Node 18 at
247
- all**, because Vite 7 calls `crypto.hash` (added in Node 20.12) and the run dies with
248
- `TypeError: crypto.hash is not a function` before a spec loads. On Vitest ≤ 3 Node 18 is fine. Which
249
- 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
250
269
  [Performance → Which Node version](https://vitest-auto-spy.dev/core/performance#which-node-version):
251
270
  the break is between 22 and 24, where a cold import more than halves.
252
271
 
@@ -260,14 +279,54 @@ the published package roughly in half.
260
279
 
261
280
  ### Peer dependencies
262
281
 
263
- All peers are **provided by your project**; `rxjs` and `@angular/core` are **optional** — install
264
- them only for the matching entry point. The package itself has **zero runtime dependencies**.
265
-
266
- | Peer | Needed for | Optional? |
267
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------- |
268
- | `vitest` | the default runner | no |
269
- | `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 |
270
- | `@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.
271
330
 
272
331
  ## The CLI — `doctor`, `perf`, `codemod` and `init`
273
332
 
@@ -417,7 +476,9 @@ enough for `--from auto` to classify a file, for exactly the same reason — tha
417
476
 
418
477
  **Dry-run by default**, so the first thing a repository sees is a diff it can reject. `--write`
419
478
  applies it, `--only` / `--skip` select transforms by id, `--list` prints them together with the
420
- 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.
421
482
 
422
483
  What it deliberately does not do is guess. A `jest.*` member with no `vi` twin — `requireMock`,
423
484
  `replaceProperty`, `createMockFromModule`, `jest.setTimeout`, `requireActual` — is **left exactly as
@@ -1084,26 +1145,26 @@ the conditional types that pick helpers from a return type — see
1084
1145
  The library ships a framework-agnostic core plus runtime and framework layers, so a plain
1085
1146
  Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runtime bundle**:
1086
1147
 
1087
- | Import | Provides | Pulls in | Status |
1088
- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | :----: |
1089
- | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
1090
- | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) and `createObservableWithValues` | `rxjs` | ✅ |
1091
- | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, `mockResourceProp`, the `mock*Prop` helpers, signal & resource matchers, TestBed diagnostics | `@angular/core` | ✅ |
1092
- | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1093
- | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
1094
- | `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` | ✅ |
1095
- | `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` | ✅ |
1096
- | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
1097
- | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
1098
- | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
1099
- | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
1100
- | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
1101
- | `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` | ✅ |
1102
- | `vitest-auto-spy/jasmine-compat` | `enableJasmineCompat()` alone — the same `.and` / `.calls` layer, registering no adapter, for `bun test` and `node --test` | — (your runner) | ✅ |
1103
- | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` / `ObserverSpy` / `SubscriberSpy` — the `@hirez_io/observer-spy` surface, so `/rxjs` does not carry it | `rxjs` | ✅ |
1104
- | `vitest-auto-spy/setup` | `setupAutoSpy()` — property restore, duplicate-copy detection and mock-registry hygiene in one call; `setupFakeTimers()` / `advanceTimers()` | `vitest` | ✅ |
1105
- | `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`) | ✅ |
1106
- | `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`) | ✅ |
1107
1168
 
1108
1169
  ✅ all entry points published (see [Availability](#availability)).
1109
1170
 
@@ -1418,7 +1479,7 @@ Full write-up, methodology and the complete suite-scale tables:
1418
1479
 
1419
1480
  ### Reproducing the numbers
1420
1481
 
1421
- 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
1422
1483
  identical on Windows, macOS and Linux — same Node, same npm, same commands, nothing OS-specific to
1423
1484
  call out:
1424
1485
 
@@ -3197,7 +3258,7 @@ another, and the next one is in that same file.
3197
3258
  | `asInstance(spy)` / `asSpy(instance)` | The two named views between `Spy<T>` / `DeepMockProxy<T>` and `T`, instead of `as any` |
3198
3259
  | `createSpyClass(Class, config?)` | A spy that can be called with `new`; records `calls` and `instances` |
3199
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 |
3200
- | `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 |
3201
3262
  | `flushEventLoop(turns?)` / `settleDynamicImport(load, turns?)` | Real event-loop turns under fake timers; wait for a dynamic `import()` |
3202
3263
  | `autoMocked<T>(overrides?)` | `createAutoMock` typed as `T & Spy<T>` |
3203
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
  }