vitest-auto-spy 4.2.0 → 4.3.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 (39) hide show
  1. package/AGENTS.md +89 -2
  2. package/README.md +135 -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 +606 -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.js +4 -4
  32. package/dist/shared-state.js +4 -275
  33. package/dist/svelte.d.ts +2 -2
  34. package/dist/svelte.js +5 -5
  35. package/dist/vue.d.ts +2 -2
  36. package/dist/vue.js +6 -6
  37. package/dist/{zoneless-DGwkbWKm.d.ts → zoneless-LsBDcSNZ.d.ts} +1 -1
  38. package/package.json +6 -2
  39. package/skills/vitest-auto-spy/SKILL.md +4 -1
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
@@ -2104,6 +2143,17 @@ thing to check by hand after the swap: Suites' double answers every property nam
2104
2143
  stubbed a since-renamed method was passing over nothing and will now fail. Full mapping:
2105
2144
  <https://asdalexey.github.io/vitest-auto-spy/migrating-suites>.
2106
2145
 
2146
+ **A spec importing `@testing-library/angular` usually keeps it.** `render`, `screen` and the query
2147
+ set are a different discipline and have no twin here. What overlaps is the secondary
2148
+ `@testing-library/angular/vitest-utils` entry alone — 52 lines, four exports:
2149
+ `createMock` → `createSpyFromClass`, `createMockWithValues` → `createSpyFromClass(X, { overrides })`,
2150
+ `provideMock` → `provideAutoSpy`, `provideMockWithValues` → `provideAutoSpy(X, { overrides })` —
2151
+ `overrides` and not `returns`, because those values replace the member rather than configure it. Its
2152
+ walk assigns a mock only where `typeof descriptor?.value === 'function'`, so accessors are dropped
2153
+ silently while its `Mock<T>` still types them callable, and it recurses past `Object.prototype`, so
2154
+ `hasOwnProperty` and `toString` end up as `vi.fn()`. Full mapping, verified against the 19.4.2
2155
+ tarball: <https://asdalexey.github.io/vitest-auto-spy/migrating-testing-library-angular>.
2156
+
2107
2157
  - **`renderShallow` has no single speed number, and quoting one is a bug.** This repository commits
2108
2158
  no harness for the helper, so no surface publishes a ratio for it and none may be invented.
2109
2159
  Describe the shape instead: `renderShallow` is flat in the number of children because it never
@@ -2148,6 +2198,18 @@ and it never throws: the class is reached through the undocumented `mock.constru
2148
2198
  constructed tracker is probed first and any failure leaves spies on `node:test`'s own tracker.
2149
2199
  Spies created **before** the call stay there too. `mock.reset()` in `afterEach` is the fallback.
2150
2200
 
2201
+ **A `node:test` spy now prints under its method name.** `mock.fn()` takes no name and has no
2202
+ `mockName()`, so a spy inherited the library's internal dispatcher and read back as
2203
+ `[Function: dispatch]` everywhere. The adapter names the *implementation* at creation and lets
2204
+ `mock.fn()` carry that name onto the mock, which is where a `node:test` mock takes its `name` from;
2205
+ `displayName` is set on the mock alongside it. The name survives `mock.reset()`, `mock.restore()`,
2206
+ `resetCalls()` and a `mockImplementation()` swap, and it is what `node:assert` diffs, what
2207
+ `util.inspect()` prints and what this package's own messages read. Do not "simplify" this into
2208
+ `Object.defineProperty(mock, 'name', …)`: redefining `name` drops the function out of V8's fast map
2209
+ 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
2210
+ mock in `node:test`'s reporter output on its own. Full account:
2211
+ <https://asdalexey.github.io/vitest-auto-spy/runtimes/node#spy-names>.
2212
+
2151
2213
  ---
2152
2214
 
2153
2215
  ## 14. `fakeAsync` needs `vitest-auto-spy/zone`
@@ -2401,6 +2463,8 @@ packages, which a subpath export can never be.
2401
2463
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2402
2464
  | `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
2465
  | `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 |
2466
+ | `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) |
2467
+ | `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
2468
  | `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
2469
  | `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
2470
  | `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) |
@@ -2479,6 +2543,18 @@ nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` stil
2479
2543
  `mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
2480
2544
  never patched anything.
2481
2545
 
2546
+ **When the double is the suspect and no message names it, print it.**
2547
+ `import { explainSpy } from 'vitest-auto-spy/diagnostics'` — it sits on that entry, not the root,
2548
+ because ESM re-export is eager and a reader calls it at a breakpoint, not from a spec.
2549
+ `console.log(explainSpy(spy))`
2550
+ reports every spied member with its configured argument lists numbered and every recorded call
2551
+ attributed to the config it hit — or to the default it fell through to, which is the shape of "the
2552
+ method answered `undefined` and nothing said why". `explainSpy(spy, 'load')` narrows to one member,
2553
+ `explainSpy(spy.load)` takes a bare function spy, `calledWith` and `mustBeCalledWith` share one
2554
+ numbering, and it never throws — a foreign mock is reported as one in the text. The wording is a
2555
+ diagnostic and is free to change: print it, never assert on it.
2556
+ <https://asdalexey.github.io/vitest-auto-spy/utilities/explain-spy>.
2557
+
2482
2558
  ---
2483
2559
 
2484
2560
  ## 18. Do not write this
@@ -2574,8 +2650,19 @@ config describes — `coverage.all` on a Vitest that stopped reading the key, a
2574
2650
  `coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
2575
2651
  recompiling it per file costs more than collecting the coverage does. It is
2576
2652
  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>.
2653
+ the eaten glob below came from.
2654
+
2655
+ Two of its checks are about this package's own names. `helper-from-wrong-entry` catches a named
2656
+ import taken from an entry that does not export it — `provideAutoSpy` from the root rather than
2657
+ `/angular` or `/nestjs`, `expectRequest` from anywhere but `/angular-http`. `no-unawaited-helper`
2658
+ catches an `expectEmission` / `expectError` / `stable` / `flushEventLoop` called as a bare statement
2659
+ and dropped, so the promise settles after the test ended and its assertion reports into a later test
2660
+ or nowhere. Both resolve the name against a table **generated from this package's own `exports`
2661
+ map**, which is why they are `doctor` checks and not lint rules: a per-file linter has none. Both
2662
+ are conservative — the callee must have been imported from this package in that same file (a rename
2663
+ with `as` still counts), the call must both begin and end a statement, and the pair goes quiet when
2664
+ the installed major differs from the table's. Neither has a fixer; `doctor` still never writes.
2665
+ Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
2579
2666
 
2580
2667
  ### If you were asked why a suite is slow
2581
2668
 
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 |
@@ -2477,6 +2552,47 @@ service.doWork();
2477
2552
  expect(fakeConsole.info).toHaveBeenCalledWith('done');
2478
2553
  ```
2479
2554
 
2555
+ ### Why a spy answered what it did — `explainSpy`
2556
+
2557
+ `mustBeCalledWith` already prints wanted next to actual — but only on the call that breaks. While a
2558
+ test is red for some _other_ reason, the question is different: which of my configs did this call
2559
+ hit, and which one never fired at all. `explainSpy` answers it on demand.
2560
+
2561
+ ```ts
2562
+ import { explainSpy } from 'vitest-auto-spy/diagnostics';
2563
+
2564
+ console.log(explainSpy(users)); // every spied member; explainSpy(users, 'load') for one, explainSpy(users.load) for a bare spy
2565
+ ```
2566
+
2567
+ ```text
2568
+ [vitest-auto-spy] explainSpy
2569
+
2570
+ load — 3 calls, 2 configured
2571
+ configured:
2572
+ #1 calledWith(1)
2573
+ #2 calledWith(Any<String>)
2574
+ calls:
2575
+ #1 load(1) -> matched #1
2576
+ #2 load(2) -> no configured arguments matched; the default value was used
2577
+ #3 load('ada') -> matched #2
2578
+
2579
+ save — 1 call, nothing configured
2580
+ calls:
2581
+ #1 save('ada')
2582
+
2583
+ remove — never called, 1 configured
2584
+ configured:
2585
+ #1 mustBeCalledWith(9)
2586
+ ```
2587
+
2588
+ `calledWith` and `mustBeCalledWith` share one numbering, so a call names a single number whichever
2589
+ chain answered it. An accessor spy appears as `get name` / `set name`, read from the double's
2590
+ `accessorSpies` bag so that naming one never invokes the live accessor. It never throws — a foreign
2591
+ mock, or anything that is not one of this library's doubles, is reported as one in the text, because
2592
+ this is reached from a spec that is already failing. The wording is a diagnostic, not an API: print
2593
+ it, never assert on it. Full reference:
2594
+ [explainSpy](https://asdalexey.github.io/vitest-auto-spy/utilities/explain-spy).
2595
+
2480
2596
  ## Observable assertions
2481
2597
 
2482
2598
  `expect(...)` inside a `subscribe()` callback is the most common way to write a test that passes
@@ -2517,6 +2633,16 @@ saved$ did not emit within 1000 ms (0 emission(s) received). Either the stream n
2517
2633
  the trigger and any provider spy feeding it — or it is slower than the timeout; …
2518
2634
  ```
2519
2635
 
2636
+ **The code frame opens your spec line, not this package.** These helpers build their failure inside
2637
+ a `subscribe` or timer callback, long after the call returned, so the stack the runner used to see
2638
+ started in `node_modules/vitest-auto-spy/…` and carried no spec frame at all. The stack is now
2639
+ captured at helper entry, before anything subscribes, and pinned onto the failure when it is finally
2640
+ built — so the frame the reporter shows is the `await expectEmission(…)` line. Only errors these
2641
+ helpers build themselves are re-anchored: the error `expectError` resolves with belongs to the code
2642
+ under test and keeps the stack it was created with. `vi.defineHelper`, which covers the helpers that
2643
+ throw while the caller's frame is still live, cannot serve these — its `__VITEST_HELPER__` frame
2644
+ lands last and Vitest then drops the stack whole, code frame included.
2645
+
2520
2646
  | Option | Default | Notes |
2521
2647
  | --------- | ------- | ------------------------------------------------------------ |
2522
2648
  | `timeout` | `1000` | Milliseconds to wait. `0` waits indefinitely |
@@ -3015,6 +3141,8 @@ another, and the next one is in that same file.
3015
3141
  | Export | Description |
3016
3142
  | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
3017
3143
  | `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy<T>` from a class |
3144
+ | `createSpyFromInstance(instance, methodsOrConfig?)` | Patch an object the test already holds **in place**, so everything that captured it sees the spies |
3145
+ | `restoreSpiedInstance(instance)` | Put one spied instance back — `restoreMockedProps()` and `using` do the same, and all three compose |
3018
3146
  | `createAutoMock<T>(overrides?, config?)` | Build a `Spy<T>` from a **type/interface** alone (Proxy, no class); `{ returns, observablePropsToSpyOn }` configure it |
3019
3147
  | `createMock<T>(partial?)` | Build a plain, spy-free `T` from the fields a test seeds — for data shapes the code under test reads |
3020
3148
  | `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 +3191,7 @@ another, and the next one is in that same file.
3063
3191
  | `guardGlobalPatches(reaction)` / `installPerTest(install)` _(`/setup`)_ | Name the test that sealed a global property; re-install a stub before every test |
3064
3192
  | `consoleDebugSpy` … `consoleWarnSpy` _(`/console`)_ | Silent typed spies replacing the global `console` methods on import |
3065
3193
  | `installConsoleSpies()` / `resetConsoleSpies()` / `restoreConsole()` | Install / clear / undo the console spies |
3194
+ | `explainSpy(spy, method?)` _(`/diagnostics`)_ | Every configured argument list next to every recorded call, attributed to the config it hit |
3066
3195
  | `errorHandler` | The `mustBeCalledWith` argument-mismatch error helper |
3067
3196
 
3068
3197
  **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
  /**