vitest-auto-spy 4.1.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 +116 -24
  2. package/README.md +189 -78
  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 +15 -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
@@ -261,20 +294,23 @@ xhr.send.mockImplementation(() => respond(asInstance(xhr)));
261
294
 
262
295
  ### Cost, so it stops being a question
263
296
 
264
- Building a spy is not a thing to optimise. On a ten-method class, Node 24, `npm run bench`:
265
- `createSpyFromClass` plus two methods called ~2.3 µs, plus all ten ~6 µs, `createAutoMock` plus four
266
- members ~1.7 µs, a `calledWith` lookup ~0.2 µs — five providers across two thousand tests is a
267
- hundredth of a second. Call the factory in `beforeEach` and look at
268
- `TestBed` instead. The only two settings that cost: `{ lazySpies: false }` gives up the laziness
269
- `provideAutoSpy` defaults to, and `autoSpyAccessors: true` walks the prototype chain uncached on
270
- every call — name the accessors instead.
297
+ Building a spy is not a thing to optimise. On a ten-method class, `npm run bench` on Node v24.19.0
298
+ and Vitest 4.1.11, measured 2026-09-04: `createSpyFromClass` plus two methods called **2.25 µs**,
299
+ plus all ten 5.83 µs, `createAutoMock` plus four members 1.79 µs, a `calledWith` lookup 0.21 µs —
300
+ five providers across two thousand tests is about two hundredths of a second. Call the factory in
301
+ `beforeEach` and look at `TestBed` instead. The only two settings that cost:
302
+ `{ lazySpies: false }` gives up the laziness `provideAutoSpy` defaults to, and
303
+ `autoSpyAccessors: true` walks the prototype chain uncached on every call — name the accessors
304
+ instead.
271
305
 
272
306
  Memory is the exception, and only on classes wide enough for it to matter. `lazySpies: true` still
273
307
  defines one accessor per method, and that placeholder is nearly all of what an untouched double
274
- retains: 101.6 kB on a 400-method class against 11.8 kB with `lazySpies: 'proxy'`, which answers
275
- every method from a single trap object instead (25 B per method against 253 B). It is opt-in because
276
- a `Proxy` cannot remove itself — +30 ns per read and +43 ns per call, forever — and it loses on a
277
- five-method service. Reach for it on generated API clients and ngrx facades under `isolate: false`,
308
+ retains: an untouched 100-method double holds 25 601 B against 4 097 B with `lazySpies: 'proxy'`,
309
+ which answers every method from a single trap object instead — 256 B per method against 41 B
310
+ (`npm run bench:memory`, 2026-09-04). It is opt-in because a `Proxy` cannot remove itself: the
311
+ default leaves a plain data property behind once a method materialises, while the proxy goes through
312
+ a trap on every read and every call for the life of the double, and on a narrow class it is also
313
+ slower to create. Reach for it on generated API clients and ngrx facades under `isolate: false`,
278
314
  where the doubles of a whole file are alive at once; leave the default everywhere else.
279
315
 
280
316
  ---
@@ -843,6 +879,12 @@ setupAutoSpy({ globalFakeTimers: true });
843
879
  setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
844
880
  ```
845
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
+
846
888
  **`expectEmission` subscribes when you call it, not when you await it**, and that is load-bearing
847
889
  rather than an implementation detail. It is what converts the test whose source has to be poked
848
890
  _after_ somebody is listening — a router event, a `Subject` the spec pushes into, anything that
@@ -2101,22 +2143,35 @@ thing to check by hand after the swap: Suites' double answers every property nam
2101
2143
  stubbed a since-renamed method was passing over nothing and will now fail. Full mapping:
2102
2144
  <https://asdalexey.github.io/vitest-auto-spy/migrating-suites>.
2103
2145
 
2104
- - **`renderShallow` has no single speed number, and quoting one is a bug.** Per render it ranges
2105
- from 1.2× (no children) to 16.2× (400 child instances) — `docs-site/core/performance.md`,
2106
- measured 2026-08-26. Per spec file, where imports and the `TestBed` module are also on the clock,
2107
- the published conversion is 1.7× over three specs with one of them a 0.8× regression. The two are
2108
- not interchangeable: the per-render ratio is the upper bound on what a file can gain. Any surface
2109
- that mentions the helper's speed quotes the range and links to
2110
- `/core/performance#_2-rendering-the-child-subtree`. A figure of **4.1×** appears in
2111
- `PRIORITIES.md` and is backed by nothing in this repository — do not publish it until someone
2112
- produces the measurement.
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
+
2157
+ - **`renderShallow` has no single speed number, and quoting one is a bug.** This repository commits
2158
+ no harness for the helper, so no surface publishes a ratio for it and none may be invented.
2159
+ Describe the shape instead: `renderShallow` is flat in the number of children because it never
2160
+ builds the subtree, while `TestBed.createComponent` scales linearly with it — so the win is
2161
+ however much markup the component under test happens to own, nothing on a leaf component and a
2162
+ great deal on a table or a dashboard. A per-render ratio would in any case be the upper bound on
2163
+ what a spec file can gain rather than a prediction of it, because a file also pays for imports,
2164
+ the `TestBed` module and the assertions. Link to
2165
+ `/core/performance#_2-rendering-the-child-subtree`. A speed figure for the helper also sits in
2166
+ `PRIORITIES.md`, backed by nothing in this repository — do not publish it until someone produces
2167
+ the measurement.
2113
2168
  - **Competitor defects are quoted from the published tarball, with a file, a line and a date.** The
2114
2169
  `@testing-library/angular` `createMock` claims in `docs-site/comparison.md` are pinned to
2115
2170
  `fesm2022/testing-library-angular-vitest-utils.mjs` lines 14 and 18 at 19.4.2. Re-`npm pack` and
2116
2171
  re-read before restating them against a new version; the "Where the numbers come from" box records
2117
2172
  each re-verification date rather than replacing the previous one.
2118
2173
  - **Head-to-head benchmark numbers are quoted from the measurement, not from memory.** `npm run
2119
- bench:vs` and `npm run bench:suite` (see `CONTRIBUTING.md` for setup) back the claims in
2174
+ bench:vs` and `npm run bench:suite` (see `CONTRIBUTING.md` for setup) back the claims in
2120
2175
  `core/performance.md`; point at that page rather than retyping a figure here, so it lives in one
2121
2176
  place. Micro-benchmark ratios do not transfer to suite scale: double construction is on the order
2122
2177
  of 1% of a test's cost, and an advantage that looks like several times faster per double can be
@@ -2143,6 +2198,18 @@ and it never throws: the class is reached through the undocumented `mock.constru
2143
2198
  constructed tracker is probed first and any failure leaves spies on `node:test`'s own tracker.
2144
2199
  Spies created **before** the call stay there too. `mock.reset()` in `afterEach` is the fallback.
2145
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
+
2146
2213
  ---
2147
2214
 
2148
2215
  ## 14. `fakeAsync` needs `vitest-auto-spy/zone`
@@ -2396,6 +2463,8 @@ packages, which a subpath export can never be.
2396
2463
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2397
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) |
2398
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()` |
2399
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 |
2400
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) |
2401
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) |
@@ -2474,6 +2543,18 @@ nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` stil
2474
2543
  `mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
2475
2544
  never patched anything.
2476
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
+
2477
2558
  ---
2478
2559
 
2479
2560
  ## 18. Do not write this
@@ -2569,8 +2650,19 @@ config describes — `coverage.all` on a Vitest that stopped reading the key, a
2569
2650
  `coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
2570
2651
  recompiling it per file costs more than collecting the coverage does. It is
2571
2652
  worth one run after any large edit to a test suite — especially after a codemod, which is where
2572
- the eaten glob below came from. Full reference:
2573
- <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>.
2574
2666
 
2575
2667
  ### If you were asked why a suite is slow
2576
2668
 
@@ -2586,7 +2678,7 @@ totals are CPU time summed across every worker, not wall clock** — a report sh
2586
2678
  time` under `986ms wall clock` is not a bug, it is the work spread over workers.
2587
2679
 
2588
2680
  When `environment` dominates, the `perf-environment` finding names spec files that could run under
2589
- the `node` environment instead — but only the ones it could *prove* reach no DOM: the spec, its
2681
+ the `node` environment instead — but only the ones it could _prove_ reach no DOM: the spec, its
2590
2682
  configured setup files, and every repository module any of them imports were all read, none
2591
2683
  mentions a DOM name, and every package they import is on a short allowlist (`vitest`, `rxjs`,
2592
2684
  `date-fns`, `lodash`, `zod`, …). Anything it could not resolve is reported **undecided**, never