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.
- package/AGENTS.md +116 -24
- package/README.md +189 -78
- package/dist/angular.d.ts +2 -2
- package/dist/angular.js +276 -7
- package/dist/bun-angular.d.ts +3 -3
- package/dist/bun-angular.js +6 -6
- package/dist/bun.d.ts +72 -2
- package/dist/bun.js +4 -4
- package/dist/{chunk-DSIYS6Y5.js → chunk-D5LDT3W4.js} +1 -1
- package/dist/{chunk-2ZG27FVL.js → chunk-EMASXQDZ.js} +1 -1
- package/dist/{chunk-4KK3ES4P.js → chunk-FFKN6QKI.js} +22 -0
- package/dist/{chunk-4KCYFAJX.js → chunk-FYFF67EN.js} +23 -7
- package/dist/{chunk-5URFSYDT.js → chunk-GAY6LNAM.js} +300 -6
- package/dist/{chunk-WQZ2RKS7.js → chunk-KYAJCFUA.js} +1 -1
- package/dist/{chunk-KEKKEIRG.js → chunk-Z4VRMCW2.js} +1 -1
- package/dist/cli.js +606 -16
- package/dist/console.js +2 -2
- package/dist/diagnostics.d.ts +31 -1
- package/dist/diagnostics.js +350 -1
- package/dist/{expect-emission-BDuFuqTb.d.ts → expect-emission-Dy5CvsF2.d.ts} +27 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +444 -111
- package/dist/jasmine.js +4 -4
- package/dist/nestjs.js +5 -5
- package/dist/node.cjs +272 -111
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +2 -2
- package/dist/node.js +17 -5
- package/dist/react.d.ts +2 -2
- package/dist/react.js +5 -5
- package/dist/setup.js +4 -4
- package/dist/shared-state.js +4 -275
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +5 -5
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +6 -6
- package/dist/{zoneless-DGwkbWKm.d.ts → zoneless-LsBDcSNZ.d.ts} +1 -1
- package/package.json +15 -2
- 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,
|
|
265
|
-
`createSpyFromClass` plus two methods called
|
|
266
|
-
members
|
|
267
|
-
|
|
268
|
-
`TestBed` instead. The only two settings that cost:
|
|
269
|
-
`
|
|
270
|
-
every call — name the accessors
|
|
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:
|
|
275
|
-
every method from a single trap object instead
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
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
|
-
|
|
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.
|
|
2573
|
-
|
|
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
|
|
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
|