vitest-auto-spy 4.2.0 → 4.4.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.
Files changed (40) hide show
  1. package/AGENTS.md +101 -2
  2. package/README.md +147 -6
  3. package/dist/angular.d.ts +2 -2
  4. package/dist/angular.js +276 -7
  5. package/dist/bun-angular.d.ts +3 -3
  6. package/dist/bun-angular.js +6 -6
  7. package/dist/bun.d.ts +72 -2
  8. package/dist/bun.js +4 -4
  9. package/dist/{chunk-DSIYS6Y5.js → chunk-D5LDT3W4.js} +1 -1
  10. package/dist/{chunk-2ZG27FVL.js → chunk-EMASXQDZ.js} +1 -1
  11. package/dist/{chunk-4KK3ES4P.js → chunk-FFKN6QKI.js} +22 -0
  12. package/dist/{chunk-4KCYFAJX.js → chunk-FYFF67EN.js} +23 -7
  13. package/dist/{chunk-5URFSYDT.js → chunk-GAY6LNAM.js} +300 -6
  14. package/dist/{chunk-WQZ2RKS7.js → chunk-KYAJCFUA.js} +1 -1
  15. package/dist/{chunk-KEKKEIRG.js → chunk-Z4VRMCW2.js} +1 -1
  16. package/dist/cli.js +608 -16
  17. package/dist/console.js +2 -2
  18. package/dist/diagnostics.d.ts +31 -1
  19. package/dist/diagnostics.js +350 -1
  20. package/dist/{expect-emission-BDuFuqTb.d.ts → expect-emission-Dy5CvsF2.d.ts} +27 -25
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +444 -111
  23. package/dist/jasmine.js +4 -4
  24. package/dist/nestjs.js +5 -5
  25. package/dist/node.cjs +272 -111
  26. package/dist/node.d.cts +2 -0
  27. package/dist/node.d.ts +2 -2
  28. package/dist/node.js +17 -5
  29. package/dist/react.d.ts +2 -2
  30. package/dist/react.js +5 -5
  31. package/dist/setup.d.ts +69 -11
  32. package/dist/setup.js +91 -13
  33. package/dist/shared-state.js +4 -275
  34. package/dist/svelte.d.ts +2 -2
  35. package/dist/svelte.js +5 -5
  36. package/dist/vue.d.ts +2 -2
  37. package/dist/vue.js +6 -6
  38. package/dist/{zoneless-DGwkbWKm.d.ts → zoneless-LsBDcSNZ.d.ts} +1 -1
  39. package/package.json +6 -2
  40. package/skills/vitest-auto-spy/SKILL.md +66 -62
package/AGENTS.md CHANGED
@@ -88,6 +88,8 @@ Do you have a real class at runtime?
88
88
  ├── one spec, a couple of fields → createMock<T>(partial?) → T (no spies)
89
89
  └── many specs, one shared model → createFixtureFactory<T>(defaults) → (overrides?) => T
90
90
 
91
+ Object already built, other code already holds it?
92
+ → createSpyFromInstance(obj, config?) → Spy<T>, patched IN PLACE
91
93
  One standalone function? → createFunctionSpy<Fn>('name')
92
94
  Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
93
95
  → only a shape? mockConstructor<T>(() => instance)
@@ -106,6 +108,37 @@ noise scales with the number of them.
106
108
  `createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
107
109
  `no-type-assertion` lint rule without an `eslint-disable` on every fixture.
108
110
 
111
+ **`createSpyFromInstance(instance, config?)` is the one case where nothing is constructed.** Every
112
+ factory above builds the double, which is no help once the object exists and other code already
113
+ points at it — a service a factory built, a third-party client, a half-real `TestBed.inject(X)`. It
114
+ patches that object in place and hands the same object back typed as `Spy<T>`, so a closure, a DI
115
+ container or a live subscription that captured it first sees the spies:
116
+
117
+ ```ts
118
+ const client = new PaymentsClient(config); // real, already wired into the code under test
119
+ const spy = createSpyFromInstance(client);
120
+
121
+ spy.charge.calledWith(100).resolveWith({ ok: true });
122
+ await checkout.pay(100);
123
+
124
+ restoreSpiedInstance(client); // client.charge is the real method again
125
+ ```
126
+
127
+ Discovery is the object's own function-valued fields **plus** every prototype method up to but not
128
+ including `Object.prototype` — so an arrow-function property needs no `instanceMethodsToSpyOn` here,
129
+ and `hasOwnProperty` / `toString` are never replaced. The configuration is `createSpyFromClass`',
130
+ minus the two options that describe a double being built rather than an object being patched:
131
+ `lazySpies` (the members already exist) and `fillMissing` (an instance is not an erased `abstract`
132
+ declaration). Every write is journaled through the same mechanism as the `mock*Prop` helpers, so
133
+ three ways back compose — `restoreSpiedInstance(instance)` for one object mid-test,
134
+ `restoreMockedProps()` (which `setupAutoSpy()` already runs) for the sweep, and
135
+ `using spy = createSpyFromInstance(client)`, whose dispose **restores** rather than merely resets:
136
+ the only sense disposal can have for an object the consumer owns. A frozen or sealed instance, and a
137
+ member declared `configurable: false`, come back as this package's diagnostic rather than a bare
138
+ `TypeError` (§17). Nothing else in the field does this — `vi.mockObject` is Vitest-only,
139
+ `sinon.createStubInstance` builds a new object from a constructor instead of patching the one you
140
+ hold, and `bun:test` and `node:test` have nothing.
141
+
109
142
  **`createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` are for the model
110
143
  that more than one spec builds.** The difference from `createMock` is the `defaults` argument: it is
111
144
  a **complete** `T`, checked in full, in one place — so a field the model dropped fails there instead
@@ -846,6 +879,12 @@ setupAutoSpy({ globalFakeTimers: true });
846
879
  setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
847
880
  ```
848
881
 
882
+ **The code frame these failures open is the spec line, not this package.** The helpers build their
883
+ error inside a `subscribe` or timer callback, so the stack is captured at helper entry and pinned on
884
+ when the failure is finally built; only errors these helpers make themselves are re-anchored, and
885
+ the error `expectError` resolves with keeps the stack of the code under test. Do not wrap them in
886
+ `vi.defineHelper` — its `__VITEST_HELPER__` frame lands last and Vitest then drops the stack whole.
887
+
849
888
  **`expectEmission` subscribes when you call it, not when you await it**, and that is load-bearing
850
889
  rather than an implementation detail. It is what converts the test whose source has to be poked
851
890
  _after_ somebody is listening — a router event, a `Subject` the spec pushes into, anything that
@@ -1144,6 +1183,17 @@ the realm, not owned by `globalThis`), and with `isolate: false` the next file d
1144
1183
  own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
1145
1184
  the file in the stack is not the cause.
1146
1185
 
1186
+ `restoreWebStorage` is the other repair that is on by default. Vitest copies a DOM environment's
1187
+ globals onto `globalThis` behind `if (k in global) return KEYS.includes(k)`, and neither
1188
+ `localStorage` nor `sessionStorage` is in `KEYS` — both used to arrive only because Node put neither
1189
+ on `globalThis`, so the first half of that condition was false. Node's own Web Storage made the key
1190
+ exist, and the environment's storage stopped arriving: `setItem is not a function` on Node 25,
1191
+ `undefined` on Node 26, under jsdom and happy-dom alike, because the filter runs before either. The
1192
+ suite stays green until a spec touches storage, so this lands as "CI moved to a new Node and eleven
1193
+ unrelated specs died". The repair writes a key, reads it back and removes it — a storage that
1194
+ survives that is left alone, whoever implemented it — and it installs nothing in a `node`
1195
+ environment, which is supposed to have no Web Storage at all.
1196
+
1147
1197
  `hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
1148
1198
  CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
1149
1199
  `test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
@@ -2104,6 +2154,17 @@ thing to check by hand after the swap: Suites' double answers every property nam
2104
2154
  stubbed a since-renamed method was passing over nothing and will now fail. Full mapping:
2105
2155
  <https://asdalexey.github.io/vitest-auto-spy/migrating-suites>.
2106
2156
 
2157
+ **A spec importing `@testing-library/angular` usually keeps it.** `render`, `screen` and the query
2158
+ set are a different discipline and have no twin here. What overlaps is the secondary
2159
+ `@testing-library/angular/vitest-utils` entry alone — 52 lines, four exports:
2160
+ `createMock` → `createSpyFromClass`, `createMockWithValues` → `createSpyFromClass(X, { overrides })`,
2161
+ `provideMock` → `provideAutoSpy`, `provideMockWithValues` → `provideAutoSpy(X, { overrides })` —
2162
+ `overrides` and not `returns`, because those values replace the member rather than configure it. Its
2163
+ walk assigns a mock only where `typeof descriptor?.value === 'function'`, so accessors are dropped
2164
+ silently while its `Mock<T>` still types them callable, and it recurses past `Object.prototype`, so
2165
+ `hasOwnProperty` and `toString` end up as `vi.fn()`. Full mapping, verified against the 19.4.2
2166
+ tarball: <https://asdalexey.github.io/vitest-auto-spy/migrating-testing-library-angular>.
2167
+
2107
2168
  - **`renderShallow` has no single speed number, and quoting one is a bug.** This repository commits
2108
2169
  no harness for the helper, so no surface publishes a ratio for it and none may be invented.
2109
2170
  Describe the shape instead: `renderShallow` is flat in the number of children because it never
@@ -2148,6 +2209,18 @@ and it never throws: the class is reached through the undocumented `mock.constru
2148
2209
  constructed tracker is probed first and any failure leaves spies on `node:test`'s own tracker.
2149
2210
  Spies created **before** the call stay there too. `mock.reset()` in `afterEach` is the fallback.
2150
2211
 
2212
+ **A `node:test` spy now prints under its method name.** `mock.fn()` takes no name and has no
2213
+ `mockName()`, so a spy inherited the library's internal dispatcher and read back as
2214
+ `[Function: dispatch]` everywhere. The adapter names the *implementation* at creation and lets
2215
+ `mock.fn()` carry that name onto the mock, which is where a `node:test` mock takes its `name` from;
2216
+ `displayName` is set on the mock alongside it. The name survives `mock.reset()`, `mock.restore()`,
2217
+ `resetCalls()` and a `mockImplementation()` swap, and it is what `node:assert` diffs, what
2218
+ `util.inspect()` prints and what this package's own messages read. Do not "simplify" this into
2219
+ `Object.defineProperty(mock, 'name', …)`: redefining `name` drops the function out of V8's fast map
2220
+ and costs +206 B per mock against +65 B for naming at creation, measured over 200 000 mocks. `getMockName()` still does not exist there: read `spy.method.name`. Nothing labels a
2221
+ mock in `node:test`'s reporter output on its own. Full account:
2222
+ <https://asdalexey.github.io/vitest-auto-spy/runtimes/node#spy-names>.
2223
+
2151
2224
  ---
2152
2225
 
2153
2226
  ## 14. `fakeAsync` needs `vitest-auto-spy/zone`
@@ -2401,6 +2474,8 @@ packages, which a subpath export can never be.
2401
2474
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2402
2475
  | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
2403
2476
  | `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
2477
+ | `Cannot spy on this instance in place: it is not extensible` | `createSpyFromInstance` was handed a frozen or sealed object | spy it before it is frozen, or build a separate double with `createSpyFromClass` (§2) |
2478
+ | `Cannot mock the property 'X': it is not configurable` | the member was defined with `configurable: false`, so it cannot be redefined | members replaced before it are still restorable — `restoreSpiedInstance(instance)` or `restoreMockedProps()` |
2404
2479
  | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2405
2480
  | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2406
2481
  | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
@@ -2420,6 +2495,7 @@ packages, which a subpath export can never be.
2420
2495
  | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2421
2496
  | `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 |
2422
2497
  | `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) |
2498
+ | `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) |
2423
2499
  | `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) |
2424
2500
  | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2425
2501
  | 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) |
@@ -2479,6 +2555,18 @@ nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` stil
2479
2555
  `mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
2480
2556
  never patched anything.
2481
2557
 
2558
+ **When the double is the suspect and no message names it, print it.**
2559
+ `import { explainSpy } from 'vitest-auto-spy/diagnostics'` — it sits on that entry, not the root,
2560
+ because ESM re-export is eager and a reader calls it at a breakpoint, not from a spec.
2561
+ `console.log(explainSpy(spy))`
2562
+ reports every spied member with its configured argument lists numbered and every recorded call
2563
+ attributed to the config it hit — or to the default it fell through to, which is the shape of "the
2564
+ method answered `undefined` and nothing said why". `explainSpy(spy, 'load')` narrows to one member,
2565
+ `explainSpy(spy.load)` takes a bare function spy, `calledWith` and `mustBeCalledWith` share one
2566
+ numbering, and it never throws — a foreign mock is reported as one in the text. The wording is a
2567
+ diagnostic and is free to change: print it, never assert on it.
2568
+ <https://asdalexey.github.io/vitest-auto-spy/utilities/explain-spy>.
2569
+
2482
2570
  ---
2483
2571
 
2484
2572
  ## 18. Do not write this
@@ -2574,8 +2662,19 @@ config describes — `coverage.all` on a Vitest that stopped reading the key, a
2574
2662
  `coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
2575
2663
  recompiling it per file costs more than collecting the coverage does. It is
2576
2664
  worth one run after any large edit to a test suite — especially after a codemod, which is where
2577
- the eaten glob below came from. Full reference:
2578
- <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
2665
+ the eaten glob below came from.
2666
+
2667
+ Two of its checks are about this package's own names. `helper-from-wrong-entry` catches a named
2668
+ import taken from an entry that does not export it — `provideAutoSpy` from the root rather than
2669
+ `/angular` or `/nestjs`, `expectRequest` from anywhere but `/angular-http`. `no-unawaited-helper`
2670
+ catches an `expectEmission` / `expectError` / `stable` / `flushEventLoop` called as a bare statement
2671
+ and dropped, so the promise settles after the test ended and its assertion reports into a later test
2672
+ or nowhere. Both resolve the name against a table **generated from this package's own `exports`
2673
+ map**, which is why they are `doctor` checks and not lint rules: a per-file linter has none. Both
2674
+ are conservative — the callee must have been imported from this package in that same file (a rename
2675
+ with `as` still counts), the call must both begin and end a statement, and the pair goes quiet when
2676
+ the installed major differs from the table's. Neither has a fixer; `doctor` still never writes.
2677
+ Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
2579
2678
 
2580
2679
  ### If you were asked why a suite is slow
2581
2680
 
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-14.3%20kB-brightgreen)](#install)
26
+ [![minzipped size](https://img.shields.io/badge/minzip-15.0%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)
@@ -48,10 +48,11 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
48
48
 
49
49
  - 🧪 Reads a class and generates a typed spy for **every** method — no hand-written `vi.fn()` lists
50
50
  - 🧬 Or mock from a **type/interface** alone — `createAutoMock<T>()`, no class required
51
+ - 🪛 Or patch the object you already hold, in place — `createSpyFromInstance(client)` spies a real instance without replacing it, so a closure or a DI container that captured it first sees the spies; `restoreSpiedInstance` puts it back
51
52
  - 🌐 One `MockAdapter` core — **Vitest**, **Bun** and **`node:test`**, identical API on each
52
53
  - 🧩 Framework recipes: **Angular**, **NestJS**, **React**, **Vue/Pinia** and **Svelte**
53
54
  - 🎯 Return-type-aware helpers — sync, `Promise`, and `Observable` all get the right API
54
- - 🔀 `calledWith` / `mustBeCalledWith` argument dispatch
55
+ - 🔀 `calledWith` / `mustBeCalledWith` argument dispatch — and `explainSpy(spy)` prints every configured list next to every recorded call, before anything has failed
55
56
  - 📡 First-class RxJS `Observable` spying (`nextWith`, `nextWithValues`, `throwWith`, …)
56
57
  - ⚙️ Getter / setter spies via `accessorSpies` — **including on Bun**, where `bun:test`'s own `spyOn(obj, 'prop', 'get')` throws _"does not support accessor properties yet"_ (verified on Bun 1.4.0); no library that generates a double from a class or a type has accessor spies on any runner
57
58
  - 🧰 DI & mocking utilities — `provideAutoSpy` / `injectSpy` (Angular, NestJS, Vue), `createFunctionSpy`, `mockReadonlyProp` for signals
@@ -74,7 +75,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
74
75
  - 🌀 `fakeAsync` / `waitForAsync` on Vitest — one import of `vitest-auto-spy/zone`; zone.js stays out of every other entry
75
76
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
76
77
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
77
- - 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses
78
+ - 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
78
79
  - 📏 Lint rules and one-line test-run hygiene — nineteen rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, seven suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
79
80
  - 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
80
81
  - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
@@ -125,6 +126,7 @@ includes — that one import is what keeps `returnSubject()` typed as rxjs's own
125
126
  - [How to mock](#how-to-mock)
126
127
  - [A service behind Angular DI](#how-to-mock-a-service-behind-angular-di)
127
128
  - [A service without DI](#how-to-mock-a-service-without-di)
129
+ - [An object the test already holds](#how-to-mock-an-object-the-test-already-holds)
128
130
  - [Reading a spy back from DI](#how-to-mock-reading-a-spy-back-from-di)
129
131
  - [A whole class's dependencies at once](#how-to-mock-a-whole-classs-dependencies-at-once)
130
132
  - [A readonly property or a signal](#how-to-mock-a-readonly-property-or-a-signal)
@@ -147,6 +149,7 @@ includes — that one import is what keeps `returnSubject()` typed as rxjs's own
147
149
  - [Migrating from jest-auto-spies](#migrating-from-jest-auto-spies)
148
150
  - [Migrating from jasmine-auto-spies](#migrating-from-jasmine-auto-spies)
149
151
  - [Migrating from @ngneat/spectator](https://asdalexey.github.io/vitest-auto-spy/migrating-spectator)
152
+ - [Migrating from @testing-library/angular](https://asdalexey.github.io/vitest-auto-spy/migrating-testing-library-angular)
150
153
  - [Configuration](#configuration)
151
154
  - [Spying instance-assigned callables (`signal()`, arrow props, `signalStore()`)](#spying-instance-assigned-callables-signal-arrow-props-signalstore)
152
155
  - [Strict doubles — fail on a method nobody configured](#strict-doubles--fail-on-a-method-nobody-configured)
@@ -176,6 +179,7 @@ includes — that one import is what keeps `returnSubject()` typed as rxjs's own
176
179
  - [Which factory, and what it costs](#which-factory-and-what-it-costs)
177
180
  - [Utilities](#utilities)
178
181
  - [Console spies — `vitest-auto-spy/console`](#console-spies--vitest-auto-spyconsole)
182
+ - [Why a spy answered what it did — `explainSpy`](#why-a-spy-answered-what-it-did--explainspy)
179
183
  - [Observable assertions](#observable-assertions)
180
184
  - [Test-run hygiene](#test-run-hygiene)
181
185
  - [Fake timers](#fake-timers)
@@ -290,6 +294,8 @@ error tsconfig-glob-matches-nothing libs/users/tsconfig.spec.json
290
294
  | `coverage-include-recompiles-globs` | A coverage scope large enough that `picomatch` recompiling it per file costs more than the coverage. Info |
291
295
  | `jasmine-era-project` | `jasmine-core`, `@types/jasmine`, `karma.conf.*` or `@hirez_io/observer-spy` still installed. Info, not an error |
292
296
  | `no-agent-instructions` | No instruction file names the package. A note, not an error |
297
+ | `helper-from-wrong-entry` | A named import taken from an entry that does not export it — `provideAutoSpy` from the root |
298
+ | `no-unawaited-helper` | An `expectEmission` / `stable` / `flushEventLoop` call dropped as a bare statement, so nothing awaits it |
293
299
 
294
300
  The check that motivated the tool: a spec showing `Cannot find name 'vi'` in the editor while
295
301
  `tsc --noEmit` reported zero errors. A migration codemod editing `include` had eaten a `/**`,
@@ -298,6 +304,16 @@ spec tsconfigs still covered their specs.
298
304
 
299
305
  `doctor` never writes. There is no `--fix`.
300
306
 
307
+ The last two checks resolve a **name**, and both read the same table: the entry each helper is
308
+ exported from, generated from this package's own `exports` map by compiling the barrels and reading
309
+ the symbols back out of the compiler. That table is why they are `doctor` checks and not lint rules
310
+ — a per-file linter has none. Both are conservative in the same way: the callee has to have been
311
+ imported from this package in that same file (a rename with `as` still counts), `no-unawaited-helper`
312
+ reports only a call that both begins and ends a statement, and the pair goes quiet when the installed
313
+ major differs from the table's, because a helper moves between entries only in a major. Over this
314
+ repository's own 755 source files they report nothing outside their own fixtures. Neither has a
315
+ fixer.
316
+
301
317
  ### `perf` — where the CPU time actually goes
302
318
 
303
319
  Vitest prints one summary line per run with a phase breakdown; it is the only place those numbers
@@ -720,6 +736,48 @@ const gateway = createAutoMock<PaymentGateway>(); // from a type/interface alone
720
736
  The constructor is never called, so no side effects run. `createAutoMock<T>()` is for the case
721
737
  where there is no runtime class to read — an interface, a type alias, an imported `.d.ts`.
722
738
 
739
+ ### How to mock: an object the test already holds
740
+
741
+ Every factory above _constructs_ the double, which is no help once the object exists and other code
742
+ already points at it — a service a factory built, a third-party client, a half-real
743
+ `TestBed.inject(X)`. `createSpyFromInstance` patches that object **in place**:
744
+
745
+ ```ts
746
+ import { createSpyFromInstance, restoreSpiedInstance } from 'vitest-auto-spy';
747
+
748
+ const client = new PaymentsClient(config); // real, and already wired into the code under test
749
+ const spy = createSpyFromInstance(client); // same object, now typed as Spy<PaymentsClient>
750
+
751
+ spy.charge.calledWith(100).resolveWith({ ok: true });
752
+ await checkout.pay(100);
753
+
754
+ expect(spy.charge).toHaveBeenCalledWith(100);
755
+ restoreSpiedInstance(client); // client.charge is the real method again
756
+ ```
757
+
758
+ Mutating rather than copying is the whole point: anything that captured the object before the spec
759
+ ran — a closure, a DI container, a live subscription — sees the spies. Discovery is the object's own
760
+ function-valued fields _plus_ every prototype method up to but not including `Object.prototype`, so
761
+ an arrow-function property needs no `instanceMethodsToSpyOn` here and `hasOwnProperty` / `toString`
762
+ are never replaced. The configuration is `createSpyFromClass`' — `methodsToSpyOn`,
763
+ `onlyMethodsToSpyOn`, `instanceMethodsToSpyOn`, the array shorthand, `observablePropsToSpyOn`,
764
+ `gettersToSpyOn`, `settersToSpyOn`, `autoSpyAccessors`, `returns`, `overrides`, `strict`,
765
+ `onUnstubbedCall` — minus the two that describe a double being built rather than an object being
766
+ patched: `lazySpies` (the members already exist) and `fillMissing` (an instance is not an erased
767
+ `abstract` declaration).
768
+
769
+ Every write is journaled through the same mechanism as the [`mock*Prop` helpers](#how-to-mock-a-readonly-property-or-a-signal),
770
+ so there are three ways back and they compose: `restoreSpiedInstance(instance)` for one object mid-test,
771
+ `restoreMockedProps()` for the sweep `setupAutoSpy()` already runs, and
772
+ `using spy = createSpyFromInstance(client)`, whose dispose **restores** rather than merely resets —
773
+ the only sense disposal can have for an object the consumer owns. A frozen or sealed instance, and a
774
+ member defined with `configurable: false`, are reported with the object described and the repair
775
+ named rather than as a bare `TypeError`.
776
+
777
+ Nothing else in the field does this: `vi.mockObject` is Vitest-only, `sinon.createStubInstance`
778
+ builds a new object from a constructor instead of patching the one you hold, and `bun:test` and
779
+ `node:test` have nothing.
780
+
723
781
  ### How to mock: reading a spy back from DI
724
782
 
725
783
  ```ts
@@ -1037,11 +1095,19 @@ Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runti
1037
1095
  > file while every Angular consumer imports `/angular` alongside it. The root now reaches the loader
1038
1096
  > as **2 modules instead of 8** and `/angular` as **2 instead of 10** — measured at ~0.8 ms less per
1039
1097
  > spec file for the root and ~1.0 ms for an Angular consumer, at a cost of ~120 kB in a dev-only
1040
- > dependency that never reaches a production bundle. The four modules that hold process-wide state
1098
+ > dependency that never reaches a production bundle. The five modules that hold process-wide state —
1099
+ > `mock-adapter`, `observable-support`, `jasmine-support`, `package-identity` and `emission-timeout` —
1041
1100
  > stay in one shared `dist/shared-state.js` that every ESM entry imports, so there is still exactly
1042
1101
  > one mock-adapter registry — two copies of it is what `No mock adapter registered` and
1043
- > `Observable spies require rxjs` used to mean. Only these two entries: de-chunking them all
1044
- > costs +429 kB and duplicates the registries.
1102
+ > `Observable spies require rxjs` used to mean. Only the **state** goes in, never the code that reads
1103
+ > it: `expect-emission` used to be the member, and it dragged its 10 kB of pure helper into every
1104
+ > entry that imports the shared file. Splitting it so that only its `defaultTimeoutMs` cell —
1105
+ > `emission-timeout` — is pinned, while the helper is code-split normally, took **−10.0 kB** off the
1106
+ > module graph of `/rxjs`, `/console`, `/nestjs`, `/setup`, `/jasmine`, `/jasmine-compat` and
1107
+ > `/dom-stubs`, none of which export an emission helper, and **−2.5 kB** off the eight entries that
1108
+ > do — module count unchanged on every entry, `/setup` min+gzip 12 092 → 12 072 B, measured
1109
+ > 2026-09-04. Only these two entries: de-chunking them all costs +429 kB and duplicates the
1110
+ > registries.
1045
1111
 
1046
1112
  ```ts
1047
1113
  import { createSpyFromClass } from 'vitest-auto-spy';
@@ -1481,6 +1547,14 @@ is `createNestUnit(S)` and `TestBed.sociable(S).expose(D).compile()` is
1481
1547
  refuses, is in
1482
1548
  [Migrating from @suites/unit](https://asdalexey.github.io/vitest-auto-spy/migrating-suites).
1483
1549
 
1550
+ On [`@testing-library/angular`](https://github.com/testing-library/angular-testing-library) there is
1551
+ one page here that tells you to **keep** the library you came from. `render`, `screen` and the query
1552
+ set have no twin in this package and should not move; what overlaps is the secondary
1553
+ `/vitest-utils` entry point alone — 52 lines, four exports, `createMock` → `createSpyFromClass` and
1554
+ `provideMock` → `provideAutoSpy`, with the eager walk and the two defects
1555
+ [above](#testing-libraryangular-vitest-utils) as the reason to make the swap:
1556
+ [Migrating from @testing-library/angular](https://asdalexey.github.io/vitest-auto-spy/migrating-testing-library-angular).
1557
+
1484
1558
  ## Configuration
1485
1559
 
1486
1560
  ```ts
@@ -2390,6 +2464,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
2390
2464
  | `moduleNamespace(exports, opts?)` | core | The `vi.mock` factory result an interop probe recognises — `default` + `__esModule` in place |
2391
2465
  | `diffByField(actual, expected)` | core | Which field of an array of records moved, and in how many elements — the diff the reporter collapses |
2392
2466
  | `captureArg<T>()` | core | Take hold of a callback or config the code under test built, instead of describing its shape — assertions only, never `calledWith` |
2467
+ | `explainSpy(spy, method?)` | `/diagnostics` | Every configured argument list next to every recorded call, each attributed to the config it hit — before anything failed |
2393
2468
  | `asInstances(...spies)` | core | `asInstance` for a whole argument list — one edit against one compiler error, not five |
2394
2469
  | `narrow(value, guard)` / `narrow.byKey` / `narrow.observable` | core | The branch of a union a test knows it got, failing with the shape the value actually had |
2395
2470
  | `withOverrides(model, overrides?)` | core | A fixture from a model instance: its getters read once, as data — a spread drops them |
@@ -2417,6 +2492,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
2417
2492
  | `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
2418
2493
  | `setupAngularTestEnv(opts)` | `/angular` | Zone and zoneless spec files in one worker, switching platforms per file |
2419
2494
  | `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
2495
+ | `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
2420
2496
  | `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)) |
2421
2497
  | `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 |
2422
2498
  | `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)) |
@@ -2477,6 +2553,47 @@ service.doWork();
2477
2553
  expect(fakeConsole.info).toHaveBeenCalledWith('done');
2478
2554
  ```
2479
2555
 
2556
+ ### Why a spy answered what it did — `explainSpy`
2557
+
2558
+ `mustBeCalledWith` already prints wanted next to actual — but only on the call that breaks. While a
2559
+ test is red for some _other_ reason, the question is different: which of my configs did this call
2560
+ hit, and which one never fired at all. `explainSpy` answers it on demand.
2561
+
2562
+ ```ts
2563
+ import { explainSpy } from 'vitest-auto-spy/diagnostics';
2564
+
2565
+ console.log(explainSpy(users)); // every spied member; explainSpy(users, 'load') for one, explainSpy(users.load) for a bare spy
2566
+ ```
2567
+
2568
+ ```text
2569
+ [vitest-auto-spy] explainSpy
2570
+
2571
+ load — 3 calls, 2 configured
2572
+ configured:
2573
+ #1 calledWith(1)
2574
+ #2 calledWith(Any<String>)
2575
+ calls:
2576
+ #1 load(1) -> matched #1
2577
+ #2 load(2) -> no configured arguments matched; the default value was used
2578
+ #3 load('ada') -> matched #2
2579
+
2580
+ save — 1 call, nothing configured
2581
+ calls:
2582
+ #1 save('ada')
2583
+
2584
+ remove — never called, 1 configured
2585
+ configured:
2586
+ #1 mustBeCalledWith(9)
2587
+ ```
2588
+
2589
+ `calledWith` and `mustBeCalledWith` share one numbering, so a call names a single number whichever
2590
+ chain answered it. An accessor spy appears as `get name` / `set name`, read from the double's
2591
+ `accessorSpies` bag so that naming one never invokes the live accessor. It never throws — a foreign
2592
+ mock, or anything that is not one of this library's doubles, is reported as one in the text, because
2593
+ this is reached from a spec that is already failing. The wording is a diagnostic, not an API: print
2594
+ it, never assert on it. Full reference:
2595
+ [explainSpy](https://asdalexey.github.io/vitest-auto-spy/utilities/explain-spy).
2596
+
2480
2597
  ## Observable assertions
2481
2598
 
2482
2599
  `expect(...)` inside a `subscribe()` callback is the most common way to write a test that passes
@@ -2517,6 +2634,16 @@ saved$ did not emit within 1000 ms (0 emission(s) received). Either the stream n
2517
2634
  the trigger and any provider spy feeding it — or it is slower than the timeout; …
2518
2635
  ```
2519
2636
 
2637
+ **The code frame opens your spec line, not this package.** These helpers build their failure inside
2638
+ a `subscribe` or timer callback, long after the call returned, so the stack the runner used to see
2639
+ started in `node_modules/vitest-auto-spy/…` and carried no spec frame at all. The stack is now
2640
+ captured at helper entry, before anything subscribes, and pinned onto the failure when it is finally
2641
+ built — so the frame the reporter shows is the `await expectEmission(…)` line. Only errors these
2642
+ helpers build themselves are re-anchored: the error `expectError` resolves with belongs to the code
2643
+ under test and keeps the stack it was created with. `vi.defineHelper`, which covers the helpers that
2644
+ throw while the caller's frame is still live, cannot serve these — its `__VITEST_HELPER__` frame
2645
+ lands last and Vitest then drops the stack whole, code frame included.
2646
+
2520
2647
  | Option | Default | Notes |
2521
2648
  | --------- | ------- | ------------------------------------------------------------ |
2522
2649
  | `timeout` | `1000` | Milliseconds to wait. `0` waits indefinitely |
@@ -2607,6 +2734,16 @@ expensive to diagnose when it is missing. The first three are on by default:
2607
2734
  which is a fact rather than a guess, and names the `setImmediate` case that reaches this with no
2608
2735
  timer in sight: a request matching no Express route is ended by `finalhandler` on `setImmediate`,
2609
2736
  so the 404 is never written and a routing mistake is reported as a slow test.
2737
+ 11. **Web Storage the runner never handed over.** On by default, because it only ever repairs.
2738
+ Vitest copies a DOM environment's globals onto `globalThis` behind `if (k in global) return
2739
+ KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
2740
+ arrive only because Node put neither on `globalThis`, so the first half was false. Node's own
2741
+ Web Storage made the key exist, and now the environment's storage never arrives: `setItem is
2742
+ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
2743
+ filter runs before either. A suite stays green with this broken — only the specs that touch
2744
+ storage fail — so it arrives as "CI moved to a new Node and eleven unrelated specs died". The
2745
+ repair decides by using the storage rather than looking at it, replaces only one that cannot
2746
+ keep a value, and installs nothing at all in a `node` environment.
2610
2747
 
2611
2748
  | Option | Default | Notes |
2612
2749
  | --------------------- | --------- | ------------------------------------------------------------------------------------- |
@@ -2620,6 +2757,7 @@ expensive to diagnose when it is missing. The first three are on by default:
2620
2757
  | `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
2621
2758
  | `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
2622
2759
  | `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
2760
+ | `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
2623
2761
  | `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
2624
2762
  | `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
2625
2763
  | `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
@@ -3015,6 +3153,8 @@ another, and the next one is in that same file.
3015
3153
  | Export | Description |
3016
3154
  | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
3017
3155
  | `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy<T>` from a class |
3156
+ | `createSpyFromInstance(instance, methodsOrConfig?)` | Patch an object the test already holds **in place**, so everything that captured it sees the spies |
3157
+ | `restoreSpiedInstance(instance)` | Put one spied instance back — `restoreMockedProps()` and `using` do the same, and all three compose |
3018
3158
  | `createAutoMock<T>(overrides?, config?)` | Build a `Spy<T>` from a **type/interface** alone (Proxy, no class); `{ returns, observablePropsToSpyOn }` configure it |
3019
3159
  | `createMock<T>(partial?)` | Build a plain, spy-free `T` from the fields a test seeds — for data shapes the code under test reads |
3020
3160
  | `createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` | A model written out and checked once, stamped into a fresh copy per test — for the fixture eight specs would otherwise each keep a copy of |
@@ -3063,6 +3203,7 @@ another, and the next one is in that same file.
3063
3203
  | `guardGlobalPatches(reaction)` / `installPerTest(install)` _(`/setup`)_ | Name the test that sealed a global property; re-install a stub before every test |
3064
3204
  | `consoleDebugSpy` … `consoleWarnSpy` _(`/console`)_ | Silent typed spies replacing the global `console` methods on import |
3065
3205
  | `installConsoleSpies()` / `resetConsoleSpies()` / `restoreConsole()` | Install / clear / undo the console spies |
3206
+ | `explainSpy(spy, method?)` _(`/diagnostics`)_ | Every configured argument list next to every recorded call, attributed to the config it hit |
3066
3207
  | `errorHandler` | The `mustBeCalledWith` argument-mismatch error helper |
3067
3208
 
3068
3209
  **Spied sync method:** `mockReturnValue`, `calledWith(...)`, `mustBeCalledWith(...)` — `calledWith`
package/dist/angular.d.ts CHANGED
@@ -1,10 +1,10 @@
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-DGwkbWKm.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-LsBDcSNZ.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';
5
5
  export { I as InjectionLog, T as TrackInjectionsOptions, a as TrackedProvider, t as trackInjections } from './track-injections-CAqxzlhV.js';
6
6
  export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-Dp2ZMFqj.js';
7
- export { C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-BDuFuqTb.js';
7
+ export { C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-Dy5CvsF2.js';
8
8
  import '@angular/core/testing';
9
9
 
10
10
  /**