vitest-auto-spy 5.35.0 → 5.37.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 +58 -1831
- package/CHANGELOG.md +177 -3281
- package/README.md +885 -872
- package/agent-docs/adapters.md +58 -0
- package/agent-docs/angular.md +7 -1
- package/agent-docs/anti-patterns.md +115 -0
- package/agent-docs/checklist.md +273 -0
- package/agent-docs/configuration.md +318 -0
- package/agent-docs/doubles.md +212 -0
- package/agent-docs/errors.md +3 -2
- package/agent-docs/eslint.md +2 -2
- package/agent-docs/factories.md +381 -0
- package/agent-docs/migration-jasmine.md +1 -1
- package/agent-docs/observables.md +192 -0
- package/agent-docs/properties.md +93 -0
- package/agent-docs/return-helpers.md +194 -0
- package/agent-docs/setup.md +24 -18
- package/agent-docs/waiting.md +70 -0
- package/dist/angular-diagnostics.js +10 -10
- package/dist/angular-doubles.js +8 -8
- package/dist/angular-http.js +4 -4
- package/dist/angular-matchers.js +9 -6
- package/dist/angular-router.js +5 -5
- package/dist/angular.d.ts +1 -1
- package/dist/angular.js +86 -3499
- package/dist/bun-angular.d.ts +1 -1
- package/dist/bun-angular.js +16 -16
- package/dist/bun.js +9 -9
- package/dist/{chunk-GHP66ADY.js → chunk-2KBRDNIB.js} +4 -4
- package/dist/{chunk-ABJTCB5O.js → chunk-2TZCJ3MM.js} +2 -2
- package/dist/{chunk-7C6PVODW.js → chunk-3ZBRBPOL.js} +1 -1
- package/dist/{chunk-UMBYWKKX.js → chunk-6KP5QMMV.js} +8 -4
- package/dist/{chunk-4HLOO2VK.js → chunk-6VPSS4BA.js} +12 -8
- package/dist/{chunk-4YZ7PIXM.js → chunk-B6C3K27C.js} +187 -57
- package/dist/{chunk-ON4SQZQL.js → chunk-BS432KB2.js} +1 -1
- package/dist/{chunk-NVCLX43M.js → chunk-DBC6526L.js} +1 -1
- package/dist/{chunk-HCBVNAMT.js → chunk-FOL5RH22.js} +1 -1
- package/dist/{chunk-IH7NDMVG.js → chunk-FWXDSPZD.js} +3 -3
- package/dist/{chunk-C5H7JCPE.js → chunk-GG7BQKEK.js} +1 -1
- package/dist/{chunk-4XDGO2IR.js → chunk-GKSVYPZQ.js} +1 -1
- package/dist/{chunk-TQXPN6GV.js → chunk-Q7L4AWI7.js} +1 -1
- package/dist/{chunk-4AQIDYTD.js → chunk-QDBRZQJO.js} +1 -1
- package/dist/{chunk-LGGUCVT5.js → chunk-RYFF55PV.js} +13 -5
- package/dist/{chunk-YVH6M22H.js → chunk-TXDBDMIL.js} +1 -1
- package/dist/chunk-YNXQ2WFH.js +23 -0
- package/dist/cli.js +40 -14
- package/dist/console.js +7 -7
- package/dist/dom-stubs.js +202 -131
- package/dist/eslint-plugin.cjs +2165 -2143
- package/dist/eslint-plugin.d.cts +1 -0
- package/dist/eslint-plugin.d.ts +15 -3
- package/dist/index.js +266 -143
- package/dist/jasmine-compat.js +2 -2
- package/dist/jasmine.js +10 -10
- package/dist/nestjs.js +10 -10
- package/dist/node.cjs +22 -6
- package/dist/node.js +22 -6
- package/dist/react.js +4 -4423
- package/dist/rstest.js +10 -55
- package/dist/rxjs.js +2 -2
- package/dist/setup.d.ts +106 -117
- package/dist/setup.js +1064 -741
- package/dist/signal-forms.js +1 -1
- package/dist/svelte.js +4 -4423
- package/dist/{track-signal-runs-BuJk_Yi8.d.ts → track-signal-runs-C7SrW3Ub.d.ts} +3 -1
- package/dist/vue.js +6 -4423
- package/dist/zone.js +1 -1
- package/package.json +5 -3
- package/skills/vitest-auto-spy/SKILL.md +8 -9
- package/dist/chunk-DMW7ROVM.js +0 -82
- /package/dist/{chunk-DN4MV4FB.js → chunk-5FQZGJLJ.js} +0 -0
package/AGENTS.md
CHANGED
|
@@ -36,23 +36,24 @@ type is the authority when this file and the code disagree.
|
|
|
36
36
|
|
|
37
37
|
## How to read this file
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
This file is the map, small enough to read whole. Sections §1, §3, §6, §7 and §14 are here; every
|
|
40
|
+
other section sits in its own file under `agent-docs/`, with a stub below that says when to read it
|
|
41
|
+
and lists its subsections. Section numbers are the same in every file, so "§5" means
|
|
42
|
+
`agent-docs/configuration.md` wherever it is cited.
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
45
|
D=node_modules/vitest-auto-spy
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
grep -n '^### ' "$D/agent-docs/angular.md" # the map of a topic file
|
|
46
|
+
cat "$D/agent-docs/factories.md" # one topic file, whole
|
|
47
|
+
grep -n '^### ' "$D/agent-docs/angular.md" # the map of a long topic file
|
|
47
48
|
grep -n -F '<text of the error>' "$D/agent-docs/errors.md" # a failure: its row, with the fix
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
| Read | Sections |
|
|
51
52
|
| --- | --- |
|
|
52
|
-
| Always, before writing a spec | §1 entry point, §2 factory, §3 the 90% recipe, §18 do not write this, §19 before you report success |
|
|
53
|
-
| When the task touches it | §4 return-type helpers, §5 `createSpyFromClass` configuration, §6 `Spy<T>` vs `T`, §7 resetting, §8 observables, §9 patching properties, §10 setup file (`agent-docs/setup.md`), §11 waiting, §12 doubles for what the code builds, §15 other adapters |
|
|
53
|
+
| Always, before writing a spec | §1 entry point, §2 factory (`agent-docs/factories.md`), §3 the 90% recipe, §18 do not write this (`agent-docs/anti-patterns.md`), §19 before you report success (`agent-docs/checklist.md`) |
|
|
54
|
+
| When the task touches it | §4 return-type helpers (`agent-docs/return-helpers.md`), §5 `createSpyFromClass` configuration (`agent-docs/configuration.md`), §6 `Spy<T>` vs `T`, §7 resetting, §8 observables (`agent-docs/observables.md`), §9 patching properties (`agent-docs/properties.md`), §10 setup file (`agent-docs/setup.md`), §11 waiting (`agent-docs/waiting.md`), §12 doubles for what the code builds (`agent-docs/doubles.md`), §15 other adapters (`agent-docs/adapters.md`) |
|
|
54
55
|
| Only for that stack | §13 Angular (`agent-docs/angular.md`), §14 `fakeAsync`, §16 ESLint plugin (`agent-docs/eslint.md`), §20 migrating off `jasmine-auto-spies` (`agent-docs/migration-jasmine.md`) |
|
|
55
|
-
| Never front to back | §17 Error → fix (`agent-docs/errors.md`): grep the message
|
|
56
|
+
| Never front to back | §17 Error → fix (`agent-docs/errors.md`), §10 setup file and §13 Angular: grep the message or list the `###` headings first |
|
|
56
57
|
|
|
57
58
|
---
|
|
58
59
|
|
|
@@ -93,7 +94,7 @@ Add-ons, orthogonal to the runner:
|
|
|
93
94
|
| Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
|
|
94
95
|
| jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
|
|
95
96
|
|
|
96
|
-
`vitest-auto-spy/jasmine` is Vitest-only, because
|
|
97
|
+
`vitest-auto-spy/jasmine` is Vitest-only, because its types are Vitest's. On `bun test` and
|
|
97
98
|
`node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
|
|
98
99
|
|
|
99
100
|
`vitest-auto-spy/setup` is Vitest-only as well — `setupAutoSpy()` registers Vitest hooks. On
|
|
@@ -138,381 +139,15 @@ want it as a devDependency anyway; freeing that is a type change and waits for a
|
|
|
138
139
|
|
|
139
140
|
## 2. Pick the factory
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
Do you have a real class at runtime?
|
|
143
|
-
├── yes → createSpyFromClass(Class, config?) → Spy<T>
|
|
144
|
-
│ (an `abstract class` DI token counts — see below)
|
|
145
|
-
└── no → Is the double CALLED by the code under test?
|
|
146
|
-
├── yes, and it is INJECTED (DI, a field) → createAutoMock<T>(overrides?) → Spy<T>
|
|
147
|
-
├── yes, and it is an ARGUMENT of the function under test, asserted on
|
|
148
|
-
│ → autoMocked<T>(overrides?) → T & Spy<T>
|
|
149
|
-
├── yes, and reads chain (a.b.c()) → mockDeep<T>(overrides?) → DeepMockProxy<T>
|
|
150
|
-
├── yes, and CALLS chain (a.b().c()) → mockDeep<T>({}, { selfReturning: true })
|
|
151
|
-
└── no, it is only READ (DTO, config, route snapshot)
|
|
152
|
-
├── one spec, a couple of fields → createMock<T>(partial?) → T (no spies)
|
|
153
|
-
└── many specs, one shared model → createFixtureFactory<T>(defaults) → (overrides?) => T
|
|
154
|
-
|
|
155
|
-
Object already built, other code already holds it?
|
|
156
|
-
→ createSpyFromInstance(obj, config?) → Spy<T>, patched IN PLACE
|
|
157
|
-
One standalone function? → createFunctionSpy<Fn>('name') → FunctionSpy<Fn>
|
|
158
|
-
Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
|
|
159
|
-
→ only a shape? mockConstructor<T>(() => instance)
|
|
160
|
-
→ on a global? stubConstructor(globalThis, 'Image', factory)
|
|
161
|
-
(a vi.fn() rejects `new` — see §12)
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
`createAutoMock` and `autoMocked` build the same object; they differ only in the type you get back,
|
|
165
|
-
and the question that decides it is **how the double travels**. Through DI, it arrives as `Spy<T>`
|
|
166
|
-
and is only ever asserted on — `createAutoMock`. Handed to the function under test as an argument
|
|
167
|
-
(`checkEndpoint(url, logger)`, `applyPreferences(target, …)`, `setLocalOverrides(storage, …)`),
|
|
168
|
-
it has to satisfy `T` at the call site _and_ expose the spy helpers at the assertion, and
|
|
169
|
-
`autoMocked<T>()` is that intersection — otherwise every call site needs an `asInstance()` and the
|
|
170
|
-
noise scales with the number of them. Both take the same second argument (`returns`, `name`, `strict`,
|
|
171
|
-
`observablePropsToSpyOn`). A `let` assigned in `beforeEach` is declared `AutoMocked<T>`,
|
|
172
|
-
not with the intersection spelt out. `returns` also takes an optional method written as a property,
|
|
173
|
-
`getMinZoom?: (() => number) | undefined` — the way Leaflet and other third-party typings declare
|
|
174
|
-
them.
|
|
175
|
-
|
|
176
|
-
`createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
|
|
177
|
-
`no-type-assertion` lint rule without an `eslint-disable` on every fixture. `createMock<T>(undefined)`
|
|
178
|
-
is the same call as `createMock<T>()` and answers `{}`, **not** `undefined` — a forwarding helper
|
|
179
|
-
with an optional `overrides` parameter relies on that. A fixture that means "no value" passes
|
|
180
|
-
`undefined` itself: `getters.profile.mockReturnValue(undefined)`, never
|
|
181
|
-
`mockReturnValue(createMock<Profile>(undefined))`.
|
|
182
|
-
|
|
183
|
-
A fixture **outside** its type on purpose — the `null` or array a backend sends where the type says
|
|
184
|
-
object, a payload that has to reach a runtime guard — is `outOfType<T>(value)`: nothing is checked,
|
|
185
|
-
the call site says so, and `prefer-create-mock` / `no-ts-expect-error-on-double` accept it where they
|
|
186
|
-
would report `as unknown as T` or a `@ts-expect-error`. It takes its type from the slot, so
|
|
187
|
-
`mockValueProp(job, 'status', outOfType('UNKNOWN'))` stays on the key-checked overload. Everything else
|
|
188
|
-
stays `createMock<T>()`.
|
|
189
|
-
On a strict double, `spy.m.mockReturnValue(outOfType(undefined))` reaches a defensive
|
|
190
|
-
`?? of(null)` branch of a method typed to return an `Observable`, and counts as configured.
|
|
191
|
-
|
|
192
|
-
Under `strict`, `returns: { open: undefined }` marks a method configured whose answer nobody reads
|
|
193
|
-
(an ngrx `rxMethod` ref, a snack-bar ref) — accepted for any return type. It is also how a
|
|
194
|
-
type-only double under `strict` takes its void calls, one key each:
|
|
195
|
-
`createAutoMock<CanvasRenderingContext2D>(undefined, { strict: true, returns: { save: undefined, restore: undefined } })`.
|
|
196
|
-
|
|
197
|
-
**`createSpyFromInstance(instance, config?)` is the one case where nothing is constructed.** Every
|
|
198
|
-
factory above builds the double, which is no help once the object exists and other code already
|
|
199
|
-
points at it — a service a factory built, a third-party client, a half-real `TestBed.inject(X)`. It
|
|
200
|
-
patches that object in place and hands the same object back typed as `Spy<T>`, so a closure, a DI
|
|
201
|
-
container or a live subscription that captured it first sees the spies:
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
const client = new PaymentsClient(config); // real, already wired into the code under test
|
|
205
|
-
const spy = createSpyFromInstance(client);
|
|
206
|
-
|
|
207
|
-
spy.charge.calledWith(100).resolveWith({ ok: true });
|
|
208
|
-
await checkout.pay(100);
|
|
209
|
-
|
|
210
|
-
restoreSpiedInstance(client); // client.charge is the real method again
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Discovery is the object's own function-valued fields **plus** every prototype method up to but not
|
|
214
|
-
including `Object.prototype` — so an arrow-function property needs no `instanceMethodsToSpyOn` here,
|
|
215
|
-
and `hasOwnProperty` / `toString` are never replaced. The configuration is `createSpyFromClass`',
|
|
216
|
-
minus the two options that describe a double being built rather than an object being patched:
|
|
217
|
-
`lazySpies` (the members already exist) and `fillMissing` (an instance is not an erased `abstract`
|
|
218
|
-
declaration). Every write is journaled through the same mechanism as the `mock*Prop` helpers, so
|
|
219
|
-
three ways back compose — `restoreSpiedInstance(instance)` for one object mid-test,
|
|
220
|
-
`restoreMockedProps()` (which `setupAutoSpy()` already runs) for the sweep, and
|
|
221
|
-
`using spy = createSpyFromInstance(client)`, whose dispose **restores** rather than merely resets:
|
|
222
|
-
the only sense disposal can have for an object the consumer owns. A frozen or sealed instance, and a
|
|
223
|
-
member declared `configurable: false`, come back as this package's diagnostic rather than a bare
|
|
224
|
-
`TypeError` (§17) — except a writable, enumerable one, whose value is simply replaced in place. Nothing else in the field does this — `vi.mockObject` is Vitest-only,
|
|
225
|
-
`sinon.createStubInstance` builds a new object from a constructor instead of patching the one you
|
|
226
|
-
hold, and `bun:test` and `node:test` have nothing.
|
|
227
|
-
|
|
228
|
-
**`passthrough: true` keeps the patched object working.** Unconfigured methods run the real
|
|
229
|
-
implementation (with the instance as `this`) and are still recorded; any configuration takes the
|
|
230
|
-
whole method over and `resetAutoSpy` hands it back. The Angular shape is
|
|
231
|
-
`createSpyFromInstance(TestBed.inject(CartService), { passthrough: true })` — dependencies, `signal()`
|
|
232
|
-
fields, `ɵprov` and the real `ngOnDestroy` keep working. Lifecycle hooks, discovered callables with an
|
|
233
|
-
API of their own (signals) and discovered classes are left real rather than spied. Do **not** combine
|
|
234
|
-
it with `strict: true` or `onUnstubbedCall` on the same call — that throws; a suite-wide strict yields
|
|
235
|
-
to it. A `calledWith(1)` miss answers `undefined`, not the real method: configuring a method hands the
|
|
236
|
-
whole method over. Use it to assert an interaction on a real collaborator; use a plain double when
|
|
237
|
-
the test must not touch the real one.
|
|
238
|
-
|
|
239
|
-
**`spyOnOwnMethod(sut, 'method')` is that whitelist-plus-passthrough shape packed into one call.**
|
|
240
|
-
`createSpyFromInstance(sut, { onlyMethodsToSpyOn: ['method'], passthrough: true }).method` is
|
|
241
|
-
verbose for the most common thing it says — _observe one method of the object under test and let it
|
|
242
|
-
run_ — so the helper says exactly that and hands back the single spy. It is also the drop-in for a
|
|
243
|
-
bare `vi.spyOn(component, 'method')` where a preset's `no-restricted-properties` bans `vi.spyOn`:
|
|
244
|
-
same default semantics, record and call through, and `restoreSpiedInstance` puts it back. It is a
|
|
245
|
-
core export — `import { spyOnOwnMethod } from 'vitest-auto-spy'`, or the runner entry on
|
|
246
|
-
`bun test` / `node --test` — while `vitest-auto-spy/angular` and `vitest-auto-spy/nestjs` have no
|
|
247
|
-
core re-export, so those suites import it from the root; `spyOnVoidMethod` below is the same.
|
|
248
|
-
|
|
249
|
-
**Do not `createSpyFromInstance` a DOM node that must keep living a DOM life.** Discovery walks the
|
|
250
|
-
node's prototype chain, and past the component's own class that chain is the engine's: happy-dom's
|
|
251
|
-
`Node.removeChild` calls an internal Symbol-keyed method on the child, and once discovery has
|
|
252
|
-
patched it the strict guard refuses that call — the node cannot be removed from `document.body` at
|
|
253
|
-
all, and the leak then fails later tests in the file far from the cause. Without `onlyMethodsToSpyOn`
|
|
254
|
-
on a live `Node`, the global `window` or another engine-provided event target (`XMLHttpRequest`,
|
|
255
|
-
`AbortSignal`; a class of your own that extends `EventTarget` does not count), the call now reports
|
|
256
|
-
this itself, before patching anything — `warn` by default, or a throw right there under
|
|
257
|
-
`setMisconfigurationReaction('throw')`.
|
|
258
|
-
The bare-array shorthand does not silence it: `createSpyFromInstance(el, ['addEventListener'])`
|
|
259
|
-
merges as `methodsToSpyOn`, the _additive_ list, so discovery still walks the whole node — only
|
|
260
|
-
`{ onlyMethodsToSpyOn: [...] }` skips it. For a still-attached element, spy one property with
|
|
261
|
-
`mockValueProp(el, 'addEventListener', vi.fn())` (§9) — the node stays otherwise real and removable.
|
|
262
|
-
For a native void method the handler is meant to call (`preventDefault`, `stopPropagation`, `focus`),
|
|
263
|
-
`spyOnVoidMethod(event, 'preventDefault')` packs the whitelist and the
|
|
264
|
-
`returns: { preventDefault: undefined }` seed a strict suite otherwise needs, naming the method once
|
|
265
|
-
instead of twice.
|
|
266
|
-
|
|
267
|
-
**`createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` are for the model
|
|
268
|
-
that more than one spec builds.** The difference from `createMock` is the `defaults` argument: it is
|
|
269
|
-
a **complete** `T`, checked in full, in one place — so a field the model dropped fails there instead
|
|
270
|
-
of in eight copies of a hundred-line literal. Overrides are deep-partial-checked and merge leaf by
|
|
271
|
-
leaf; an overridden array replaces the default one. Every call hands back a fresh object and the
|
|
272
|
-
defaults are copied at build time, which is what keeps one test's mutation out of the next test —
|
|
273
|
-
across files, under `isolate: false`. The copy is deep through plain objects and arrays only: a
|
|
274
|
-
`Date`, a `Map` or a class instance travels by reference, because rebuilding it would strip its
|
|
275
|
-
prototype. Defaults that are a class instance with getters go through `withOverrides()` first.
|
|
276
|
-
|
|
277
|
-
**`mockDeep` builds depth on property access, not on calls** — the distinction the tree now spells
|
|
278
|
-
out, and the one that costs an afternoon otherwise. `mock.repo.user.find()` chains because every hop
|
|
279
|
-
but the last is a _read_. A node that is **called** returns what it was configured to return, and by
|
|
280
|
-
default that is `undefined`, so `mockDeep<AppLogger>().channel('app').info('x')` is a `TypeError` at
|
|
281
|
-
the second call — while `DeepMockProxy<AppLogger>` types it perfectly, so nothing warns. Pass
|
|
282
|
-
`{ selfReturning: true }` for a fluent API, or use
|
|
283
|
-
`createAutoMock<T>(undefined, { selfReturning: ['channel'] })` when only one method chains:
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
const logger = mockDeep<AppLogger>({}, { selfReturning: true });
|
|
287
|
-
|
|
288
|
-
logger.channel('app').info('started');
|
|
289
|
-
expect(logger.channel('app').info).toHaveBeenCalledWith('started');
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Both bridges exist, and which one you need depends on the direction. What a self-returning **call**
|
|
293
|
-
hands back is typed as the _declared_ return type, not as a spy — `asSpy<T>(…)` when the helpers are
|
|
294
|
-
needed. The **whole mock** is a `DeepMockProxy<T>`, which is not assignable to `T` for the same
|
|
295
|
-
reason `Spy<T>` is not (a mapped type cannot see private members) — `asInstance(…)` when it has to
|
|
296
|
-
go somewhere typed against the real thing:
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
const logger = mockDeep<AppLogger>({}, { selfReturning: true });
|
|
300
|
-
|
|
301
|
-
boot(asInstance(logger)); // → AppLogger, for the API under test
|
|
302
|
-
asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // → the helpers
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
**Arrays and unmocked calls on `mockDeep`.** A member read by a numeric index (`api.items[0]`) is a
|
|
306
|
-
real `Array` of deep mocks from then on, and `map`, iteration and `toEqual` work on it. Read the
|
|
307
|
-
member again after the first index: a handle taken before the first index is still a node. To make a
|
|
308
|
-
call nobody configured fail, use `mockDeep<T>({}, { fallbackMockImplementation: () => { throw … } })`.
|
|
309
|
-
Options go in the **second** argument, not the first as in vitest-mock-extended. The precedence is
|
|
310
|
-
configuration > fallback > `selfReturning`. A `calledWith` miss answers `undefined`, not the
|
|
311
|
-
fallback; use `mustBeCalledWith` for "other arguments fail". For a callback API (`$transaction`), use
|
|
312
|
-
`method.mockImplementation((run) => run(asInstance(mock)))`; there is no option for it.
|
|
313
|
-
`vi.spyOn(mock.repo, 'find')` works on a member nobody has read and returns the node's own spy.
|
|
314
|
-
|
|
315
|
-
`asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
|
|
316
|
-
sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
|
|
317
|
-
|
|
318
|
-
**A deep node asks what the spy surface is on every read**, rather than remembering the answer from
|
|
319
|
-
the first deep mock of the worker. That matters under `isolate: false`, where the surface grows
|
|
320
|
-
mid-run: `import 'vitest-auto-spy/rxjs'` in a later file adds `nextWith` and friends, and
|
|
321
|
-
`setSpyEngine` swaps the whole prototype. With the answer cached, `deep.feed.items.nextWith(1)` in
|
|
322
|
-
every double built after that resolved to a **child node** — callable, recorded, emitting nothing —
|
|
323
|
-
while the spec waited on a stream that was never fed.
|
|
324
|
-
|
|
325
|
-
**`selfReturning: true` chains a factory, not a `return this` builder**, and the difference decides
|
|
326
|
-
where the calls are recorded. A called node answers _itself_, not the object the method was read
|
|
327
|
-
off, so every hop moves one level deeper: `editor.chain().focus().insertContent('text')` records
|
|
328
|
-
`insertContent` on `chain.focus`, while the `chain` handle the spec is holding still has none — and
|
|
329
|
-
`expect(chain.insertContent).toHaveBeenCalled()` reports nothing although the chain ran. Either walk
|
|
330
|
-
the same path in the assertion, or, for an API where every command answers the **same** object
|
|
331
|
-
(tiptap's `ChainedCommands`, a query builder, a `mockReturnThis()` chain), build that object with
|
|
332
|
-
`createAutoMock` instead, where `selfReturning` names methods that answer one double:
|
|
333
|
-
|
|
334
|
-
```ts
|
|
335
|
-
const chain = createAutoMock<ChainedCommands>(undefined, { selfReturning: ['focus', 'insertContent'] });
|
|
336
|
-
const editor = createAutoMock<Editor>(undefined, { returns: { chain: asInstance(chain) } });
|
|
337
|
-
|
|
338
|
-
editor.chain().focus().insertContent('text').run();
|
|
339
|
-
expect(chain.insertContent).toHaveBeenCalledWith('text'); // one double, so the assertion is the obvious one
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**An `abstract class` is a class.** `abstract class LocalStorage extends AbstractStorage {}`,
|
|
343
|
-
provided in production as `{ provide: LocalStorage, useClass: BrowserLocalStorage }`, is the
|
|
344
|
-
standard Angular DI-token idiom, and `provideAutoSpy(LocalStorage)` / `createSpyFromClass(LocalStorage)`
|
|
345
|
-
take it — type and runtime both. Abstract members are erased before they reach a prototype, so there
|
|
346
|
-
is nothing to read there; when discovery comes back empty the factory hands back the `createAutoMock`
|
|
347
|
-
proxy instead of an empty object, and every method answers. Nothing to configure, and no reason to
|
|
348
|
-
reach for `{ provide: X, useValue: createAutoMock<X>() }` by hand any more.
|
|
349
|
-
|
|
350
|
-
That holds while the class is **fully** abstract. One concrete member — a helper, a getter — and
|
|
351
|
-
discovery is no longer empty, the fallback does not fire, and every `abstract` member is missing
|
|
352
|
-
while `Spy<T>` types it as present: the read is `undefined` and the call dies as
|
|
353
|
-
`… is not a function` in production code. Pass `{ fillMissing: true }` there
|
|
354
|
-
(`provideAutoSpy(LocalStorage, { fillMissing: true })`), which answers a name the prototype never
|
|
355
|
-
carried with a spy. It is opt-in because `abstract` is erased at runtime — filling every unknown key
|
|
356
|
-
by default would silence a real typo on every concrete class.
|
|
357
|
-
|
|
358
|
-
**`overrides: { key: undefined }` is a seed, not an omission**, and the difference is load-bearing.
|
|
359
|
-
`createAutoMock` reads its seed with `Reflect.ownKeys`, so a key written out with an explicit
|
|
360
|
-
`undefined` **is** in the store: reading it answers `undefined`. Leave it out and the same read
|
|
361
|
-
materialises a _function spy_ — which is truthy, and sends `if (this.lastFocus)` down the branch the
|
|
362
|
-
spec was trying to close:
|
|
363
|
-
|
|
364
|
-
```ts
|
|
365
|
-
createAutoMock<NavigationService>({ currentFocus: undefined, navRoot: undefined, selectors: 'button, a' });
|
|
366
|
-
// ^ "this member is data, and there is none" — not the same as omitting it
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
This is the way to say "the member exists and is empty", and it is worth writing even when it looks
|
|
370
|
-
redundant.
|
|
371
|
-
|
|
372
|
-
**A getter in `overrides` stays a getter.** The seed is kept as a descriptor rather than read once
|
|
373
|
-
while the double is assembled, so it runs at every read, with the double as `this` — and a seed
|
|
374
|
-
written to **throw** ("this global is missing on this platform") fails where the code under test
|
|
375
|
-
reads the member, not where the provider literal is evaluated. A `{ set }` seed is kept the same way
|
|
376
|
-
and takes the write. Both used to be flattened as the double was built, which turned a throwing
|
|
377
|
-
seed into a failure during `TestBed.configureTestingModule`, three frames from the branch under test.
|
|
378
|
-
|
|
379
|
-
A **registration** is no exception, and it was the half that stayed broken one release longer: the
|
|
380
|
-
merge that puts `registerAutoSpyDefaults` under a call site copied both sides with a spread, so a
|
|
381
|
-
seeded getter was flattened on any class or token the registry knows — whether or not the
|
|
382
|
-
registration names that key — while the same seed stayed live on one it does not. The merge copies
|
|
383
|
-
descriptors now, and the seed behaves the same either way.
|
|
384
|
-
|
|
385
|
-
### What a Proxy-backed double cannot do
|
|
386
|
-
|
|
387
|
-
`createAutoMock` and `mockDeep` build a Proxy, not an object, and there is one place where the
|
|
388
|
-
difference shows: a Proxy answers only the operations its handler traps. Three of them used to be
|
|
389
|
-
missing, and each produced a _silent_ wrong answer rather than an error — the worst failure mode
|
|
390
|
-
this library can have, because a checking test becomes a non-checking one and only the proxy's
|
|
391
|
-
source says so. Two are fixed; the third cannot be:
|
|
392
|
-
|
|
393
|
-
| Operation | Before 3.5.0 | Now |
|
|
394
|
-
| --- | --- | --- |
|
|
395
|
-
| `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
|
|
396
|
-
| `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
|
|
397
|
-
| `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
|
|
398
|
-
|
|
399
|
-
`ownKeys` cannot be completed: a type has no key list at runtime, which is the whole premise of
|
|
400
|
-
these two factories. So a spec that installs a double by **copying it onto a real instance** —
|
|
401
|
-
`Object.assign(player, engineDouble)` — gets whichever members happened to be touched first, and
|
|
402
|
-
every other call goes to the real implementation, silently. Use `createSpyFromClass` there: it
|
|
403
|
-
returns an ordinary object whose method keys are enumerable (lazy accessors, but enumerable), so
|
|
404
|
-
the copy is complete.
|
|
405
|
-
|
|
406
|
-
### It answers everything, so it must not answer _these_
|
|
407
|
-
|
|
408
|
-
The same premise cuts the other way. A library that is handed an object and has to decide **what
|
|
409
|
-
kind of thing it is** asks by probing a key — and a double that answers every property answers the
|
|
410
|
-
probe too, at which point it stops being a double of `T` and becomes whatever was being looked for.
|
|
411
|
-
Four names are therefore answered with `undefined` unless the spec seeds them, alongside `then` and
|
|
412
|
-
every symbol, which always were:
|
|
413
|
-
|
|
414
|
-
| Key | Probed by | The double became |
|
|
415
|
-
| --- | --- | --- |
|
|
416
|
-
| `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
|
|
417
|
-
| `lift` | `isObservable`, with `subscribe` | an Observable |
|
|
418
|
-
| `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
|
|
419
|
-
| `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
|
|
420
|
-
|
|
421
|
-
The one that cost an afternoon reads like nothing at all:
|
|
422
|
-
|
|
423
|
-
```ts
|
|
424
|
-
of(autoMocked<AnimationItem>()); // an Observable that never emits
|
|
425
|
-
```
|
|
142
|
+
In [`agent-docs/factories.md`](./agent-docs/factories.md). Read it before writing any double: the decision tree from "what do you have" to the factory, and what a Proxy-backed `createAutoMock` cannot do.
|
|
426
143
|
|
|
427
|
-
|
|
428
|
-
the whole double was eaten as one, `of()` was left with an empty argument list, and the emission was
|
|
429
|
-
scheduled onto a spy that does nothing. The component under test kept its `null`, and what failed
|
|
430
|
-
was an assertion about an unrelated `emit()` three concerns away — nothing in the failure mentions
|
|
431
|
-
`of`. The workaround people find is `from([double])`; it is not needed any more.
|
|
144
|
+
<!-- agents-outline: agent-docs/factories.md -->
|
|
432
145
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
on its own fools nothing: `from(double)` now fails with rxjs's own _"You provided an invalid object
|
|
437
|
-
where a stream was expected"_, loudly and in the right file.
|
|
146
|
+
- What a Proxy-backed double cannot do
|
|
147
|
+
- It answers everything, so it must not answer _these_
|
|
148
|
+
- Cost, so it stops being a question
|
|
438
149
|
|
|
439
|
-
|
|
440
|
-
after the seed store:
|
|
441
|
-
|
|
442
|
-
```ts
|
|
443
|
-
createAutoMock<TaskScheduler>({ schedule: vi.fn() });
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
That is the trade the deny-list makes: without a seed the member is absent and the failure is an
|
|
447
|
-
immediate `TypeError: … is not a function` at the call site, instead of a silent one in another
|
|
448
|
-
file. A key is only added to that list with an observed mechanic behind it — never because the name
|
|
449
|
-
sounds protocol-ish — because every entry costs somebody the ability to mock a member of that name
|
|
450
|
-
without seeding it.
|
|
451
|
-
|
|
452
|
-
The tree asks whether the double is _called_, and there is a second question worth asking: whether
|
|
453
|
-
the code under test **writes to it**. `createAutoMock` is a proxy with a `set` trap over the same
|
|
454
|
-
cache its `get` trap answers from, so an assignment sticks and is read back — which makes it the
|
|
455
|
-
double for a DOM-ish object a library drives by assigning handlers, where a hand-written fake is
|
|
456
|
-
otherwise the only option:
|
|
457
|
-
|
|
458
|
-
```ts
|
|
459
|
-
const xhr = createAutoMock<XhrLike>({ status: 0, timeout: 0, onload: null, onerror: null });
|
|
460
|
-
|
|
461
|
-
xhr.send.mockImplementation(() => respond(asInstance(xhr)));
|
|
462
|
-
// production does `xhr.onload = () => resolve(xhr.status !== 0)` — the proxy remembers it
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
---
|
|
466
|
-
|
|
467
|
-
### Cost, so it stops being a question
|
|
468
|
-
|
|
469
|
-
Building a spy is not a thing to optimise. On a ten-method class, `npm run bench` on Node v24.19.0
|
|
470
|
-
and Vitest 4.1.11, measured 2026-09-04: `createSpyFromClass` plus two methods called **2.25 µs**,
|
|
471
|
-
plus all ten 5.83 µs, `createAutoMock` plus four members 1.79 µs, a `calledWith` lookup 0.21 µs —
|
|
472
|
-
five providers across two thousand tests is about two hundredths of a second. Call the factory in
|
|
473
|
-
`beforeEach` and look at `TestBed` instead. The only two settings that cost:
|
|
474
|
-
`{ lazySpies: false }` gives up the laziness `provideAutoSpy` defaults to, and
|
|
475
|
-
`autoSpyAccessors: true` walks the prototype chain uncached on every call — name the accessors
|
|
476
|
-
instead.
|
|
477
|
-
|
|
478
|
-
Memory used to be the exception, and it no longer is for the default. `lazySpies: true` still
|
|
479
|
-
defines one property per method, but the accessor pair behind it is now **shared by method name**
|
|
480
|
-
across every double in the process, so the placeholder is no longer what an untouched double
|
|
481
|
-
retains: an untouched 100-method double holds **215 B** where it held 25 593, and a 300-method one
|
|
482
|
-
284 B where it held 70 165 (`npm run bench:memory`, 2026-09-17, Node 24). The price is paid at
|
|
483
|
-
construction on very wide classes — building a 300-method double costs about 28 % more — and
|
|
484
|
-
materialising every method of one is about 26 % cheaper in exchange.
|
|
485
|
-
|
|
486
|
-
That inverts the case for `lazySpies: 'proxy'`, which answers every method from a single trap
|
|
487
|
-
object: it now retains **4 090 B** against the default's 215 B at 100 methods — 19× more — and still
|
|
488
|
-
pays a trap on every read (53 ns against 7 ns) for the life of the double, because a `Proxy` cannot
|
|
489
|
-
remove itself while the default leaves a plain data property behind once a method materialises. What
|
|
490
|
-
is left of its advantage is build time on a wide class. The option is unchanged and still supported;
|
|
491
|
-
there is no longer a memory reason to reach for it.
|
|
492
|
-
|
|
493
|
-
A frozen or sealed double is fine now. `Object.freeze(spy)` — a deep-freeze fixture helper, a
|
|
494
|
-
dev-mode state guard — used to make the first read of any method throw `Cannot redefine property`
|
|
495
|
-
from inside the placeholder's getter; the spy is kept beside the double instead, so the read answers
|
|
496
|
-
a stable mock and `mockReturnValue` on it works. On a merely sealed double an assignment still
|
|
497
|
-
reaches the member.
|
|
498
|
-
|
|
499
|
-
`vi.spyOn(double, 'load')` on a method nobody has read yet works again. Vitest reads an accessor by
|
|
500
|
-
calling its getter with no receiver, and 5.19.0's shared getter answered that with
|
|
501
|
-
`TypeError: Invalid value used as weak map key`. The call now wraps a forwarder: a configured
|
|
502
|
-
`mockReturnValue` answers, an unconfigured call reaches the double's own spy (strict guard included),
|
|
503
|
-
and `mockRestore()` / `vi.restoreAllMocks()` hand back that same spy with the calls it recorded. It
|
|
504
|
-
is still redundant — the member already is a spy, so `double.load.mockReturnValue(…)` is the line to
|
|
505
|
-
write — and a wrapped method called off its double (`const { load } = double; load()`) throws a
|
|
506
|
-
message saying so.
|
|
507
|
-
|
|
508
|
-
**Symbol-keyed methods are discovered and spied.** A method under a symbol the project owns —
|
|
509
|
-
`[SERIALIZE]()`, `Symbol.for('app.render')` — is a method like any other, resets with the rest and
|
|
510
|
-
shows up in `Reflect.ownKeys`. The runtime's own protocol symbols are deliberately left alone
|
|
511
|
-
(everything on `Symbol` itself — `Symbol.iterator`, `Symbol.dispose`, `Symbol.toPrimitive` and the
|
|
512
|
-
rest — plus `Symbol.for('nodejs.util.inspect.custom')`), because a double that answers those stops
|
|
513
|
-
being a double of `T` and starts impersonating an iterable or a disposable. Discovery covers methods
|
|
514
|
-
only: a symbol-keyed **getter or setter** is not found by `autoSpyAccessors`, so patch one with
|
|
515
|
-
`mockAccessorsProp` (§9).
|
|
150
|
+
<!-- /agents-outline -->
|
|
516
151
|
|
|
517
152
|
---
|
|
518
153
|
|
|
@@ -593,511 +228,24 @@ Four conventions that carry most of the value:
|
|
|
593
228
|
|
|
594
229
|
## 4. Helpers a spied method earns from its return type
|
|
595
230
|
|
|
596
|
-
|
|
597
|
-
`toHaveBeenCalledWith` and the rest all work as usual. On top of that:
|
|
598
|
-
|
|
599
|
-
| Return type | Helpers added |
|
|
600
|
-
| --- | --- |
|
|
601
|
-
| anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)`. On a `Promise` or `Observable` method the handle carries that row's helpers instead — `calledWith(id).nextWith(v)`, `.resolveWith(v)`; `.mockReturnValue` there is a `TS2339` |
|
|
602
|
-
| `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
|
|
603
|
-
| `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
|
|
604
|
-
|
|
605
|
-
`Observable` **properties** (not just methods) get the same helpers — list them in
|
|
606
|
-
`observablePropsToSpyOn`. The prop spy is a plain `Observable`: a `BehaviorSubject` member loses
|
|
607
|
-
`.value` / `getValue()`, and code that reads them gets `undefined`. Seed a real one instead —
|
|
608
|
-
`overrides: { isActive$: new BehaviorSubject(false) }` — and drive it with `.next()`; the same holds
|
|
609
|
-
for a stream the test pushes into itself.
|
|
610
|
-
|
|
611
|
-
**What counts as an `Observable` is structural, and no declaration names rxjs (4.0.0).** A member
|
|
612
|
-
earns the observable bundle when its type satisfies the exported `ObservableLike<T>` — `subscribe`
|
|
613
|
-
plus a promise-returning `forEach(next)`:
|
|
614
|
-
rxjs's `Observable`, every `Subject`, Angular's `EventEmitter` — and, new in 4.0.0, an `Observable`
|
|
615
|
-
from a _second copy_ of rxjs in the tree, which used to fall through to the plain-spy branch and
|
|
616
|
-
produce `nextWith is not a function` with nothing pointing at the duplicate. `Promise`, arrays,
|
|
617
|
-
`Signal` and Angular's `OutputEmitterRef` are not observables and do not earn it.
|
|
618
|
-
|
|
619
|
-
`returnSubject()` and `nextWithPerCall()` return `SubjectOf<T>` — rxjs's own `Subject<T>` wherever
|
|
620
|
-
`import 'vitest-auto-spy/rxjs'` is in the TypeScript program, the structural `SubjectLike<T>`
|
|
621
|
-
(`next` / `error` / `complete` / `asObservable` / `closed`) where it is not. The switch is one
|
|
622
|
-
augmentable interface, `AutoSpyRxjsTypes<T>`, which `/rxjs` fills in with `subject: Subject<T>`;
|
|
623
|
-
augment it yourself only to plug in a different subject type. If
|
|
624
|
-
`const s: Subject<T> = spy.m.returnSubject()` fails to compile, the import is missing from the
|
|
625
|
-
program the specs are checked in — usually a Vitest `setupFiles` entry that no `tsconfig`
|
|
626
|
-
`include` covers, or, from `@angular/build:unit-test` 22.2.0, a plain `.ts` that only the spec
|
|
627
|
-
`tsconfig`'s `include` lists: that builder's program is the specs, the setup files and the `.d.ts`
|
|
628
|
-
files (angular-cli#34134), so put the import in one of those. That is the _only_ breaking change in
|
|
629
|
-
4.0.0; the reason for it is that `dist/types-*.d.ts` used to open with
|
|
630
|
-
`import { Observable, Subject } from 'rxjs'` and load 189 rxjs `.d.ts` files into every consumer's
|
|
631
|
-
program (303 files against 114 without it), `import type` included — TypeScript resolves a type-only import the same way.
|
|
632
|
-
|
|
633
|
-
```ts
|
|
634
|
-
// argument dispatch — other arguments return undefined
|
|
635
|
-
users.getName.calledWith(1).mockReturnValue('Ada');
|
|
636
|
-
// argument enforcement — other arguments throw
|
|
637
|
-
users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
|
|
638
|
-
// asymmetric matchers work in both, at any depth
|
|
639
|
-
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
|
|
640
|
-
users.save.calledWith({ id: expect.any(Number), tags: [expect.any(String)] }).mockReturnValue(true);
|
|
641
|
-
// re-registering the same arguments replaces the answer — matcher arguments included
|
|
642
|
-
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(false);
|
|
643
|
-
|
|
644
|
-
// promises
|
|
645
|
-
users.load.resolveWith({ id: 1 });
|
|
646
|
-
users.load.rejectWith('FAKE ERROR');
|
|
647
|
-
users.load.resolveWithPerCall([{ value: a }, { value: b }]);
|
|
648
|
-
expect(users.load.mock.settledResults).toEqual([{ type: 'fulfilled', value: { id: 1 } }]);
|
|
649
|
-
|
|
650
|
-
// observables — requires `import 'vitest-auto-spy/rxjs'` once
|
|
651
|
-
feed.items$.nextWith([item]); // emit, stream stays open
|
|
652
|
-
feed.items$.nextOneTimeWith([item]); // emit once, then complete
|
|
653
|
-
feed.items$.nextWithValues([{ value: a }, { value: b, delay: 100 }, { complete: true }]);
|
|
654
|
-
const [first$, second$] = feed.watch$.nextWithPerCall([{ value: 'a' }, { value: 'b', doNotComplete: true }]);
|
|
655
|
-
feed.items$.throwWith('FAKE ERROR');
|
|
656
|
-
const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the helpers miss
|
|
657
|
-
|
|
658
|
-
// throwing — `failWith`, on a spy of any return type
|
|
659
|
-
cart.checkout.failWith(new HttpErrorResponse({ status: 500 })); // every call throws
|
|
660
|
-
cart.checkout.calledWith(BAD_ID).failWith(new Error('unknown cart')); // only these arguments throw
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
`failWith` is the sync counterpart of `rejectWith`, and it is **not** called `throwWith` — that name
|
|
664
|
-
belongs to the observable helper above, which errors the stream. At runtime every spy carries every
|
|
665
|
-
bundle; only the return type in `Spy<T>` tells them apart, so one name for both would mean whichever
|
|
666
|
-
is attached last silently wins.
|
|
667
|
-
|
|
668
|
-
On Vitest, `mockThrow` / `mockThrowOnce` (4.1) do the spy-level half of this too. `failWith` exists
|
|
669
|
-
because Bun and `node:test` ship neither, and because **no** runtime can make one `calledWith` chain
|
|
670
|
-
throw while its siblings answer normally — `mockImplementation` replaces the whole dispatch, which is
|
|
671
|
-
the opposite of configuring one set of arguments. A `failWith` supersedes a `resolveWith` /
|
|
672
|
-
`nextWith` / per-call batch configured before it, and is superseded by one configured after, so the
|
|
673
|
-
outcome never depends on the order the spec happens to be written in.
|
|
674
|
-
|
|
675
|
-
An exact argument list is matched before the asymmetric configs, and those are tried in
|
|
676
|
-
registration order — a narrow config written first keeps its calls. Two matchers count as the same
|
|
677
|
-
argument when they accept the same values (same matcher class, sample and inversion), which is what
|
|
678
|
-
makes a second `calledWith(1, expect.anything())` an override rather than a second config sitting
|
|
679
|
-
behind the first. A hand-rolled `{ asymmetricMatch }` object is compared by identity instead: its
|
|
680
|
-
verdict is a closure, so only re-registering that same instance overrides.
|
|
681
|
-
|
|
682
|
-
**A matcher counts wherever it sits, not only at the top level.** A config argument holding a
|
|
683
|
-
matcher — or a function — anywhere inside it is compared structurally rather than as data:
|
|
684
|
-
`calledWith({ id: expect.any(Number) })`, `calledWith([expect.any(String)])`, a matcher inside a
|
|
685
|
-
`Map` value or a `Set` member. The same comparison decides the rest of an argument's shape, so it is
|
|
686
|
-
worth knowing what it treats as equal: `Map` and `Set` are compared **without regard to insertion
|
|
687
|
-
order**, a `Date` by its time, a `RegExp` by its source and flags, an `Error` by its `name` and
|
|
688
|
-
`message` plus its own enumerable fields, a function by identity, and symbol-keyed properties
|
|
689
|
-
participate like string ones. Cycles are handled, so a component graph with a back-edge is a legal
|
|
690
|
-
argument. `mustBeCalledWith` uses the same map, and its `Wanted:` line renders a matcher as
|
|
691
|
-
`Any<Number>` rather than as the object it serialises to. With one config set up, its first line
|
|
692
|
-
names the first argument that differs — `argument 2: expected 'eu', got 'us'` — and it names the
|
|
693
|
-
class only on a strict double (`UserService.getName`), the method alone otherwise.
|
|
694
|
-
|
|
695
|
-
Each `calledWith(...)` / `mustBeCalledWith(...)` call hands back **its own** handle, so a chain kept
|
|
696
|
-
in a variable stays attached to the arguments it was written with:
|
|
697
|
-
|
|
698
|
-
```ts
|
|
699
|
-
const found = users.load.calledWith(1);
|
|
700
|
-
users.load.calledWith(2).mockReturnValue(undefined);
|
|
701
|
-
|
|
702
|
-
found.mockReturnValue({ id: 1 }); // configures 1, not 2
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
`new` on a method spy works: `new sdk.Client()` — the shape `createAutoMock<{ Client: typeof Client }>()`
|
|
706
|
-
and `mockDeep` produce — hands back the instance, or the object a `calledWith(...).mockReturnValue(...)`
|
|
707
|
-
configured for those arguments.
|
|
708
|
-
|
|
709
|
-
`mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
|
|
710
|
-
all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
|
|
711
|
-
|
|
712
|
-
When the argument worth asserting on is one the **code under test built** — a callback, a config
|
|
713
|
-
object, an `AbortSignal` — describing its shape is the wrong tool. `expect.any(Function)` says a
|
|
714
|
-
function was passed; `captureArg` hands it to you so the test can call it:
|
|
715
|
-
|
|
716
|
-
```ts
|
|
717
|
-
import { captureArg } from 'vitest-auto-spy';
|
|
718
|
-
|
|
719
|
-
const onDone = captureArg<() => void>();
|
|
720
|
-
|
|
721
|
-
expect(notifier.subscribe).toHaveBeenCalledWith('ready', onDone);
|
|
722
|
-
|
|
723
|
-
onDone.value(); // and now exercise what was passed
|
|
724
|
-
expect(component.finished()).toBe(true);
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
`.values` holds every value the captor was **offered**, oldest first — candidates, not matches. A
|
|
728
|
-
captor in position 0 of `toHaveBeenCalledWith(captor, 3)` is asked about the first argument of every
|
|
729
|
-
call the runner tries, including the ones the `3` then rejects, so `.values` can be longer than the
|
|
730
|
-
set of calls the assertion accepted. `.captured` asks whether anything was recorded without
|
|
731
|
-
triggering the "nothing was captured" throw that reading `.value` raises; `.reset()` lets one captor
|
|
732
|
-
serve two phases.
|
|
733
|
-
|
|
734
|
-
`captureArg({ where })` narrows both halves at once — the filter decides what is recorded **and**
|
|
735
|
-
whether that position matches at all, so a rejected candidate leaves no entry in `.values` and the
|
|
736
|
-
whole expectation fails on it:
|
|
737
|
-
|
|
738
|
-
```ts
|
|
739
|
-
const config = captureArg<RequestInit>({ where: (value) => (value as RequestInit)?.method === 'POST' });
|
|
740
|
-
|
|
741
|
-
expect(fetchSpy).toHaveBeenCalledWith(url, config); // only the POST call satisfies this
|
|
742
|
-
expect(config.value.headers).toEqual({ 'x-trace': '1' });
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
The filter receives the raw argument as `unknown`, so narrow it yourself. **Assertions only** — a
|
|
746
|
-
captor without `where` matches every value, so putting one in `calledWith` would configure a return
|
|
747
|
-
for every call, which is `mockReturnValue` spelled less clearly, and `calledWith` is typed to the
|
|
748
|
-
method's own parameters so it will not compile anyway.
|
|
231
|
+
In [`agent-docs/return-helpers.md`](./agent-docs/return-helpers.md). Read it when stubbing a return value: `calledWith`, `resolveWith`, `nextWith`, `returnSubject` and the rest, by return type.
|
|
749
232
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
buffer outliving the test that filled it, and both were silent:
|
|
753
|
-
|
|
754
|
-
```ts
|
|
755
|
-
// test 1
|
|
756
|
-
service.createTransition.nextWith(uri); // buffered
|
|
757
|
-
|
|
758
|
-
// test 2 — the failure path is the point of this test
|
|
759
|
-
service.createTransition.throwWith(error); // subscriber gets `uri` FIRST, then the error
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
The code under test therefore ran the **success** branch on stale data, and the error branch arrived
|
|
763
|
-
one emission late. The second: `error()` and `complete()` close a Subject permanently, so a later
|
|
764
|
-
`nextWith` on that spy pushed into a dead subject and emitted nothing at all. Both are fixed —
|
|
765
|
-
`resetAutoSpy(spy)` now drops the subject, and a terminated one is replaced on the next
|
|
766
|
-
configuration. That holds for a subject a spec closed **itself**, too:
|
|
767
|
-
`spy.items$.returnSubject().complete()` marks the stream closed, so the next `nextWith` opens a new
|
|
768
|
-
one rather than disappearing.
|
|
769
|
-
|
|
770
|
-
`nextWithValues` on an observable **property** builds a new stream, which a subscriber that already
|
|
771
|
-
holds the old one never sees — the spec's values go nowhere and the assertion below reads the
|
|
772
|
-
initial state. That case is reported rather than passing silently —
|
|
773
|
-
`Feed.items$.nextWithValues() ran after something subscribed to Feed.items$` on a strict double, the
|
|
774
|
-
member name alone otherwise — once per property as a warning, and at every such call as a throw under
|
|
775
|
-
`setupAutoSpy({ misconfiguration: 'throw' })`. `nextWith` pushes into the subject the current
|
|
776
|
-
subscriber is on and is the one to reach for mid-test.
|
|
777
|
-
|
|
778
|
-
What that does **not** change: `vi.clearAllMocks()` and `clearMocks: true` still cannot reach it,
|
|
779
|
-
for the same reason they cannot reach a `calledWith` chain — that state lives in this library's
|
|
780
|
-
closures, not on the runner's mock. So when a spy outlives a test — a TestBed built in `beforeAll`,
|
|
781
|
-
a spy hoisted to `describe` scope — put `resetAutoSpy(spy)` in `beforeEach`. Inside one test the
|
|
782
|
-
sequence `nextWith(a)` then `throwWith(e)` still means "emit a, then fail"; only a reset or a
|
|
783
|
-
terminal call starts a new stream.
|
|
233
|
+
<!-- agents-outline: agent-docs/return-helpers.md -->
|
|
234
|
+
<!-- /agents-outline -->
|
|
784
235
|
|
|
785
236
|
---
|
|
786
237
|
|
|
787
238
|
## 5. `createSpyFromClass` configuration
|
|
788
239
|
|
|
789
|
-
|
|
790
|
-
createSpyFromClass(MyService); // every method on the prototype chain
|
|
791
|
-
createSpyFromClass(MyService, ['reload', 'count']); // those two ADDED to the discovered ones
|
|
792
|
-
createSpyFromClass(MyService, {
|
|
793
|
-
methodsToSpyOn: ['reload'], // ADDS (jest-auto-spies semantics)
|
|
794
|
-
onlyMethodsToSpyOn: ['getName'], // RESTRICTS — skips prototype discovery
|
|
795
|
-
instanceMethodsToSpyOn: ['reload'], // ADDS; same behaviour, clearer name
|
|
796
|
-
observablePropsToSpyOn: ['products$'],
|
|
797
|
-
gettersToSpyOn: ['userName'],
|
|
798
|
-
settersToSpyOn: ['userName'],
|
|
799
|
-
autoSpyAccessors: true, // discover every accessor on the prototype chain
|
|
800
|
-
lazySpies: true, // build each method spy on first access ('proxy' for very wide classes)
|
|
801
|
-
returns: { getProducts: of([]) }, // what a spied METHOD answers
|
|
802
|
-
selfReturning: ['where'], // a method that answers the double itself, for a chained call
|
|
803
|
-
overrides: { products$: subject }, // a member that is not a method result
|
|
804
|
-
});
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
| Key | Semantics |
|
|
808
|
-
| --- | --- |
|
|
809
|
-
| `methodsToSpyOn` | **Additive**, as in `jest-auto-spies`. Same behaviour as `instanceMethodsToSpyOn`. |
|
|
810
|
-
| `onlyMethodsToSpyOn` | **Exhaustive whitelist.** Skips discovery; anything not listed is absent. |
|
|
811
|
-
| `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
|
|
812
|
-
| `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
|
|
813
|
-
| `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. `'proxy'` also changes _what holds the name_. |
|
|
814
|
-
| `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
|
|
815
|
-
| `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
|
|
816
|
-
| `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
|
|
817
|
-
|
|
818
|
-
**`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
|
|
819
|
-
the reference suite). Method discovery walks the _prototype chain_; a callable assigned to an
|
|
820
|
-
**instance field** is invisible to it:
|
|
821
|
-
|
|
822
|
-
- an Angular `signal()` / `computed()` field — the dominant case in a signals codebase
|
|
823
|
-
- an arrow-function property — `readonly reload = (): void => {}`
|
|
824
|
-
- anything on an ngrx `signalStore()`, which puts **everything** on the instance
|
|
825
|
-
- **members Angular's own classes moved onto the instance** — `Router.currentNavigation` in
|
|
826
|
-
Angular 20 is `currentNavigation = this.navigationTransitions.currentNavigation.asReadonly()`
|
|
827
|
-
|
|
828
|
-
```ts
|
|
829
|
-
createSpyFromClass(TaskStore, { instanceMethodsToSpyOn: ['count', 'reload'] });
|
|
830
|
-
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current', 'isEmpty'] });
|
|
831
|
-
provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] });
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
**The failure this produces says nothing about any of it.** The member is simply not on the spy, so
|
|
835
|
-
the next line reads `undefined` and configuring it throws:
|
|
836
|
-
|
|
837
|
-
```
|
|
838
|
-
TypeError: Cannot read properties of undefined (reading 'mockReturnValue')
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
There is no better message to be had at runtime, and it is worth saying why rather than leaving it
|
|
842
|
-
looking like an oversight. Instance fields do not exist until a constructor has run, and this
|
|
843
|
-
library never constructs the class — that is what makes a spy safe to build from a service whose
|
|
844
|
-
constructor talks to the network. The only alternative would be to answer an unknown member with
|
|
845
|
-
_something_, and that something would be truthy: `if (service.optionalThing)` in the code under test
|
|
846
|
-
would then take the wrong branch, silently, which is the exact failure mode the protocol deny-list
|
|
847
|
-
in §2 exists to remove. A loud `TypeError` on the spec's own line is the better of the two.
|
|
848
|
-
|
|
849
|
-
For an ngrx `signalStore()`, prefer `createAutoMock<T>()` over listing every member: it mocks from
|
|
850
|
-
the type, needs no prototype, and the list cannot fall behind the store. Its **signal** members want
|
|
851
|
-
`mockSignalProp`, not a method spy — a store's state is read during the first render, so a method spy
|
|
852
|
-
there answers `undefined` to the template.
|
|
853
|
-
|
|
854
|
-
The symptom of getting this wrong is **a spy that is never called and no warning at all**: the
|
|
855
|
-
additive lists exist precisely to name things the prototype does not have, so a typo in one cannot
|
|
856
|
-
be told apart from an instance field and stays silent.
|
|
857
|
-
|
|
858
|
-
Only `onlyMethodsToSpyOn` warns, because only a restricting list can be silently destructive — a
|
|
859
|
-
misspelling there leaves the real method unspied, and the code under test then calls something that
|
|
860
|
-
is not there:
|
|
861
|
-
`[vitest-auto-spy] createSpyFromClass(CartService): onlyMethodsToSpyOn names 'lod' (did you mean 'load'?), not a method of CartService.`
|
|
862
|
-
A name with nothing close gets `instanceMethodsToSpyOn: ['x']` as the fix instead.
|
|
863
|
-
|
|
864
|
-
Also true, and worth not re-deriving:
|
|
865
|
-
|
|
866
|
-
- **Inherited methods are spied** — discovery walks the whole chain (`Object.prototype` excluded).
|
|
867
|
-
- **Constructor bodies never run.** The spy is assembled from the prototype.
|
|
868
|
-
- **Abstract classes are accepted**, type and runtime both — `ClassType<T>` carries an abstract
|
|
869
|
-
construct signature, and when the prototype turns out to be empty (abstract members are erased
|
|
870
|
-
before emit) the factory hands back the `createAutoMock` proxy instead of an empty object. Do
|
|
871
|
-
**not** pass a concrete subclass instead; this file used to say so, and it was wrong twice over.
|
|
872
|
-
- **An overloaded method is not collapsed.** The worry that `Spy<T>` types every generated
|
|
873
|
-
`api-gateway` client against its last signature does not hold: a four-overload
|
|
874
|
-
`ContentApiService.getItemsBySlug` types as it should, and hand-written `{ m: vi.fn() }` doubles
|
|
875
|
-
for those services convert with no changes to the assertions. When the _first_ signature is the
|
|
876
|
-
useful one, name it on the **declaration only** — the factory's result assigns to it, so the type
|
|
877
|
-
argument is not written twice:
|
|
878
|
-
|
|
879
|
-
```ts
|
|
880
|
-
let mapping: Spy<ContentMappingService, { overload: 'first' }>;
|
|
881
|
-
|
|
882
|
-
mapping = createSpyFromClass(ContentMappingService); // no second type argument here
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
**The symptom that leads here says nothing about overloads**, which is why the option is hard to
|
|
886
|
-
find from the error. A stub of the real response shape is rejected on the spec's own line —
|
|
887
|
-
`TS2345: Argument of type 'Page' is not assignable to parameter of type 'HttpEvent<Page>'` — on
|
|
888
|
-
`nextWith(body)`, `resolveWith(body)`, `calledWith(…).returnValue(body)` or
|
|
889
|
-
`mockReturnValue(of(body))` against a generated `observe` client. Neither the double nor the stub
|
|
890
|
-
is wrong; both are being checked against the signature nobody calls. Reach for `overload`, never
|
|
891
|
-
for `@ts-expect-error`: one migration wrote sixty of those across twenty-five files before anyone
|
|
892
|
-
found this option, and each one stops checking the response shape that line exists to describe.
|
|
893
|
-
|
|
894
|
-
**Name the method rather than the whole double.** `'first'` on the type moves _every_ overloaded
|
|
895
|
-
member, and on a wide type that breaks the ones nobody was fixing — `Spy<Response, { overload:
|
|
896
|
-
'first' }>` for one method collected five `TS2769`s on `download`. `overload` also takes a map:
|
|
897
|
-
|
|
898
|
-
```ts
|
|
899
|
-
let perf: Spy<Performance, { overload: { getEntriesByType: 'first' } }>;
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
A name the type does not have never matches, so a rename leaves a dead entry rather than a red
|
|
903
|
-
build. The default stays `'last'` because "the useful signature" is not decidable from the type: on
|
|
904
|
-
a generated `observe` client it is the first, on a four-overload `api-gateway` client the last, and both
|
|
905
|
-
live in one suite. And **overload order is not always the author's** — `declare global` in a
|
|
906
|
-
third-party package appends to a global interface (`web-vitals` does exactly this to
|
|
907
|
-
`Performance.getEntriesByType`), so which signature is last depends on which packages are in the
|
|
908
|
-
program and can move on a dependency bump.
|
|
909
|
-
|
|
910
|
-
**A getter that returns a `Signal<T>` goes in `instanceMethodsToSpyOn`**, not in `gettersToSpyOn`.
|
|
911
|
-
`get isSafeMode(): Signal<boolean> { return this._isSafeMode.asReadonly(); }` is read as a property
|
|
912
|
-
and called as a function, and the accessor route makes you write
|
|
913
|
-
`accessorSpies.getters.isSafeMode.mockReturnValue(signal(false))` — two levels deeper than the value
|
|
914
|
-
in question. Naming it as an instance method puts a plain spy at that key (the spy object has no
|
|
915
|
-
class prototype, so nothing is being shadowed), and
|
|
916
|
-
`service.isSafeMode.mockReturnValue(false)` reads like every other member. `mockSignalProp` is the
|
|
917
|
-
other answer when the value has to change during the test.
|
|
918
|
-
|
|
919
|
-
### The composition belongs to the class — `registerAutoSpyDefaults`
|
|
920
|
-
|
|
921
|
-
A class is doubled the same way in every file that doubles it, so register that once instead of
|
|
922
|
-
repeating it:
|
|
923
|
-
|
|
924
|
-
```ts
|
|
925
|
-
// vitest.setup.ts, once
|
|
926
|
-
registerAutoSpyDefaults(Router, { observablePropsToSpyOn: ['events'], gettersToSpyOn: ['url'] });
|
|
927
|
-
|
|
928
|
-
// every spec, from then on
|
|
929
|
-
provideAutoSpy(Router);
|
|
930
|
-
provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] }); // ADDS to the registration
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
The call site is **merged** into the registration, not substituted for it: lists unioned
|
|
934
|
-
(registration first, no repeats), `returns` / `overrides` merged key by key with the call site
|
|
935
|
-
winning, scalars decided by the call site when it names the key. The bare-array form counts as
|
|
936
|
-
`{ methodsToSpyOn: [...] }`. Registration is by class identity, so a subclass inherits nothing —
|
|
937
|
-
walking the prototype chain would let one registration change doubles in files nobody was looking at.
|
|
938
|
-
A second registration for the same class replaces the first, because two of them in one suite is the
|
|
939
|
-
drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
|
|
940
|
-
`clearAutoSpyDefaults()` the lot. `createSpyFromInstance(obj)` reads the registration of the class
|
|
941
|
-
`obj.constructor` names, merged the same way; an object literal resolves none. One exception, because
|
|
942
|
-
an instance is real: when the call site lists `onlyMethodsToSpyOn`, the rest of the object stays real,
|
|
943
|
-
so the registration contributes only `strict`, `onUnstubbedCall`, `onUnstubbedRead` and the
|
|
944
|
-
`returns` / `selfReturning` entries of the listed methods — not its accessor lists, its other method
|
|
945
|
-
lists or its `overrides`. `registerAutoSpyDefaults(Router, { gettersToSpyOn: ['url'] })` therefore
|
|
946
|
-
leaves `router.url` live under `createSpyFromInstance(router, { onlyMethodsToSpyOn: ['navigateByUrl'] })`.
|
|
947
|
-
A `returns` or `selfReturning` name the call site itself wrote for a method it left real is reported
|
|
948
|
-
as a misconfiguration and skipped.
|
|
949
|
-
|
|
950
|
-
A setup file that registers more than a handful of classes can say them as one table instead of one
|
|
951
|
-
call each. Rows apply in order, and each is checked against **its own** class — a key `Router` does
|
|
952
|
-
not carry fails on that row, naming `Router`'s members and nothing else:
|
|
953
|
-
|
|
954
|
-
```ts
|
|
955
|
-
registerAutoSpyDefaults([
|
|
956
|
-
[Router, { observablePropsToSpyOn: ['events'], gettersToSpyOn: ['url'] }],
|
|
957
|
-
[AppEventsService, { instanceMethodsToSpyOn: ['announce'] }],
|
|
958
|
-
[BaseLocalStorage, { instanceMethodsToSpyOn: ['getItem', 'setItem'] }],
|
|
959
|
-
]);
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
A later row for a class an earlier row already named replaces it, exactly as a second call would.
|
|
963
|
-
`AutoSpyDefaultEntry<T>` is the row type, for a row built outside the literal.
|
|
964
|
-
|
|
965
|
-
**An `InjectionToken` is a key too — import `registerAutoSpyDefaults` from `vitest-auto-spy/angular`
|
|
966
|
-
for it.** Same registry, same merge; the core export takes a class only, because the core entry may
|
|
967
|
-
not mention Angular's types. `provideAutoSpyForToken(TOKEN)` reads the registration exactly as
|
|
968
|
-
`provideAutoSpy(Class)` reads a class's — both of its arguments are merged over it, and a registered
|
|
969
|
-
`returns` stays a default a later `calledWith` / `resolveWith` wins over:
|
|
970
|
-
|
|
971
|
-
```ts
|
|
972
|
-
import { registerAutoSpyDefaults } from 'vitest-auto-spy/angular';
|
|
973
|
-
|
|
974
|
-
registerAutoSpyDefaults(LOGGER, { returns: { info: undefined, err: undefined }, selfReturning: ['channel'] });
|
|
975
|
-
registerAutoSpyDefaults(NAVIGATION, { overrides: { activeRow$: of({}) }, returns: { setFocus: undefined } });
|
|
976
|
-
|
|
977
|
-
providers: [provideAutoSpyForToken(LOGGER), provideAutoSpyForToken(NAVIGATION, { activeRow$: rows$ })];
|
|
978
|
-
```
|
|
979
|
-
|
|
980
|
-
A token row holds `AutoSpyTokenDefaults<T>`: what `createAutoMock` takes (`returns`, `selfReturning`,
|
|
981
|
-
`observablePropsToSpyOn`, `strict`, `name`) plus `overrides`, the seeds `provideAutoSpyForToken` takes
|
|
982
|
-
second. Keys are checked against the token's `T`; a table may mix class rows and token rows;
|
|
983
|
-
`clearAutoSpyDefaults(TOKEN)` from the same entry drops one. Handing a token to the **core**
|
|
984
|
-
`registerAutoSpyDefaults` fails with `TS2345 … 'InjectionToken<X>' is not assignable to parameter of
|
|
985
|
-
type 'ClassType<unknown>'` — the fix is the import, not a cast.
|
|
986
|
-
|
|
987
|
-
**`selfReturning` names the methods that answer the double itself.** `returns` cannot say it — the
|
|
988
|
-
double does not exist when the literal is written — and without it a chained call dies on
|
|
989
|
-
`undefined`. It works on every factory and in a registration; a registered chain takes a link back out
|
|
990
|
-
through the call site's `returns`, since lists only ever union.
|
|
991
|
-
|
|
992
|
-
### `strict` — a method nobody configured throws instead of answering `undefined`
|
|
993
|
-
|
|
994
|
-
```ts
|
|
995
|
-
const users = createSpyFromClass(UserService, { strict: true });
|
|
996
|
-
|
|
997
|
-
users.load.resolveWith([]);
|
|
998
|
-
users.currentTenant(); // throws here, not four frames later inside the component
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
```
|
|
1002
|
-
[vitest-auto-spy] Cart.checkout(1, 'now') was called; this strict double has nothing configured for it.
|
|
1003
|
-
Called from src/app/cart.component.ts:41:12
|
|
1004
|
-
Configure it in the test: cart.checkout.calledWith(1, 'now').mockReturnValue(…) for these arguments, or .mockReturnValue(…) for any — .resolveWith(…) / .nextWith(…) when it returns a Promise / Observable.
|
|
1005
|
-
Docs: https://asdalexey.github.io/vitest-auto-spy/core/strict-mode#the-message
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
Also on `createAutoMock`, `provideAutoSpy`, and suite-wide as `setupAutoSpy({ strict: true })` — which
|
|
1009
|
-
reaches a double whichever bundle of the package built it: the default lives on `globalThis`. Before
|
|
1010
|
-
this release it lived in module scope, and a setup file's `strict: true` reached no double a spec built.
|
|
1011
|
-
Precedence, first one set wins: the double's `onUnstubbedCall` → the double's explicit **`strict: false`**
|
|
1012
|
-
(the only way to exempt one double from a suite-wide default, a global handler included) → the global
|
|
1013
|
-
`onUnstubbedCall` → the double's `strict: true` → the global `strict`.
|
|
1014
|
-
|
|
1015
|
-
- **It is not argument-level.** A `calledWith(1, 'now')` chain says the method is stubbed, so
|
|
1016
|
-
`checkout(9, 'later')` still answers `undefined`. Use `mustBeCalledWith` for that.
|
|
1017
|
-
- **`returns:` is a default in the spy's own container**: a `calledWith` / `resolveWith` / `failWith`
|
|
1018
|
-
configured later wins for its arguments or supersedes it, every other call still gets it, and
|
|
1019
|
-
`undefined` counts as configured. `returns: { save: undefined }` is how a `void` call is expected.
|
|
1020
|
-
`selfReturning: ['channel']` is the same kind of default, and counts as configured too.
|
|
1021
|
-
- **`mockReturnValue` / `mockImplementation` replace the dispatch**, so they never reach the guard —
|
|
1022
|
-
and a `calledWith` configured after them is never consulted. After the last `mockReturnValueOnce`
|
|
1023
|
-
the queue empties back onto the library dispatch and the next call **is** reported as unstubbed —
|
|
1024
|
-
seed `mockReturnValue(...)` too when a `Once` sequence is meant to run out.
|
|
1025
|
-
- **That pair is reported, in both orders.** `mockReturnValue` after a `calledWith` erases it;
|
|
1026
|
-
`calledWith` after a `mockReturnValue` is dead on arrival. Neither fails on its own — the spec goes
|
|
1027
|
-
green on a branch nobody configured — so the library says so as a misconfiguration (warn, or throw
|
|
1028
|
-
under the `strict` preset). The whole family replaces: `mockImplementation`, `mockReturnValue`,
|
|
1029
|
-
`mockReturnThis`, `mockThrow`, `mockResolvedValue`, `mockRejectedValue`. When both a fallback and a
|
|
1030
|
-
per-argument value are wanted, the fallback goes in the spy's container — `returns:` where the
|
|
1031
|
-
double is built, or `resolveWith` / `nextWith` / `failWith` — which a `calledWith` still wins over.
|
|
1032
|
-
The report is this library's own spy engine's, so Vitest and Rstest have it; Bun, `node:test` and
|
|
1033
|
-
`setSpyEngine('runner')` do not. The `Once` family is never reported.
|
|
1034
|
-
- **A throw something swallowed still fails the test.** Under `setupAutoSpy({ strict: true })` every
|
|
1035
|
-
strict throw is recorded, and one that a `try`/`catch` in the code under test or an RxJS error with
|
|
1036
|
-
no handler (its rethrow waits on a fake clock) kept from the test fails it after the test
|
|
1037
|
-
(`swallowedStrictCalls`, `'throw'` by default with `strict: true`). The report quotes each call
|
|
1038
|
-
with its arguments and the line that made it, and says what swallowed it — a `catch` in the code
|
|
1039
|
-
under test, or an RxJS subscriber with no error callback. Provoking one on purpose:
|
|
1040
|
-
`expect(() => cart.total()).toThrow(…)` then `expect(takeStrictViolations()).toHaveLength(1)`.
|
|
1041
|
-
- **It does not reach** accessor spies, observable-property spies, `mockDeep` nodes, `console-spy`,
|
|
1042
|
-
`mockResourceProp`'s `reload` or a standalone `createFunctionSpy`. A strict double still answers
|
|
1043
|
-
`undefined` for an unconfigured getter or `items$`. `fillMissing` members **are** covered.
|
|
1044
|
-
- **The getter and the stream are reported after the test instead** — a read cannot throw, because a
|
|
1045
|
-
failure diff that prints the double reads it. `setupAutoSpy({ unconfiguredReads: 'warn' | 'throw' })`
|
|
1046
|
-
(default `'off'`, not in the preset) reports a strict double's getter read that reached no
|
|
1047
|
-
configuration and a subscription to a stream nothing fed **by the end of the test** — subscribe in
|
|
1048
|
-
`beforeEach`, `nextWith` in the test is fine. Counted from setup's `beforeEach` to its `afterEach`,
|
|
1049
|
-
so the spec's own hooks count. Configured means `accessorSpies.getters.x.mockReturnValue(…)` /
|
|
1050
|
-
`mockImplementation`, `overrides: { x }` (call site or registration — a registered _list_ alone is
|
|
1051
|
-
not), `mockReadonlyProp`; for a stream any of `nextWith` / `throwWith` / `complete` /
|
|
1052
|
-
`returnSubject`. `undefined` meant: `mockReturnValue(undefined)`. `onUnstubbedRead({ className,
|
|
1053
|
-
member, kind, count })` — suite-wide or per double, same precedence as `onUnstubbedCall` — takes the
|
|
1054
|
-
findings instead of the report, from every double not `strict: false`: survey with it first.
|
|
1055
|
-
- **Angular lifecycle hooks are exempt.** `ngOnDestroy`, `ngOnInit`, `ngOnChanges`, `ngDoCheck` and the
|
|
1056
|
-
`ngAfter…` hooks answer `undefined` on a strict double: Angular calls `ngOnDestroy` itself on every
|
|
1057
|
-
provided value at teardown (a `createAutoMock` / `provideAutoSpyForToken` double always has one), and
|
|
1058
|
-
a throw there broke the teardown for every test after it. The calls are still recorded.
|
|
1059
|
-
- A class instance or DOM node in the call the message quotes prints as `[ClassName]`, data up to 200
|
|
1060
|
-
characters per argument; `provideAutoSpyForToken` names the token (`createAutoMock` takes `name`).
|
|
1061
|
-
- Use `onUnstubbedCall` to survey a suite before turning the throw on — it is handed
|
|
1062
|
-
`{ className, method, args }` and whatever it returns is what the call answers, so a handler that
|
|
1063
|
-
pushes to an array records the gap without failing anything.
|
|
1064
|
-
|
|
1065
|
-
### Getters and setters live in `accessorSpies`
|
|
1066
|
-
|
|
1067
|
-
```ts
|
|
1068
|
-
const settings = createSpyFromClass(SettingsService, { gettersToSpyOn: ['theme'], settersToSpyOn: ['theme'] });
|
|
1069
|
-
|
|
1070
|
-
settings.accessorSpies.getters.theme.mockReturnValue('dark');
|
|
1071
|
-
expect(settings.theme).toBe('dark'); // the property itself stays typed as `string`
|
|
1072
|
-
|
|
1073
|
-
settings.theme = 'light';
|
|
1074
|
-
expect(settings.accessorSpies.setters.theme).toHaveBeenCalledWith('light');
|
|
1075
|
-
```
|
|
1076
|
-
|
|
1077
|
-
**Naming one half gets you the pair, when the class declares a pair.** `gettersToSpyOn: ['theme']`
|
|
1078
|
-
on a class with both a getter and a setter installs both spies — mirroring reads the prototype
|
|
1079
|
-
descriptor, so it only ever adds what the class already has, and a read-only member stays read-only.
|
|
1080
|
-
Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs: the write vanished,
|
|
1081
|
-
`accessorSpies.setters.theme` was `undefined`, and the failure read
|
|
1082
|
-
`Cannot read properties of undefined` three steps from the configuration behind it.
|
|
1083
|
-
|
|
1084
|
-
The bag is typed over every key of `T` unless the lists are repeated in the options type argument —
|
|
1085
|
-
`createSpyFromClass<Settings, { gettersToSpyOn: ['theme'] }>(Settings, { gettersToSpyOn: ['theme'] })`
|
|
1086
|
-
keys both halves by exactly the configured names, so `accessorSpies.setters.other` is a compile error
|
|
1087
|
-
instead of an `undefined` at run time. Use it when a spec reaches into the bag by name; a
|
|
1088
|
-
non-literal `string[]` falls back to the every-key bag.
|
|
240
|
+
In [`agent-docs/configuration.md`](./agent-docs/configuration.md). Read it when passing a config to `createSpyFromClass`: which lists add and which restrict, `registerAutoSpyDefaults`, `strict`, getters and setters.
|
|
1089
241
|
|
|
1090
|
-
|
|
1091
|
-
spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
|
|
1092
|
-
factory, which is the part that is otherwise found by trial:
|
|
242
|
+
<!-- agents-outline: agent-docs/configuration.md -->
|
|
1093
243
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
244
|
+
- The composition belongs to the class — `registerAutoSpyDefaults`
|
|
245
|
+
- `strict` — a method nobody configured throws instead of answering `undefined`
|
|
246
|
+
- Getters and setters live in `accessorSpies`
|
|
1097
247
|
|
|
1098
|
-
|
|
1099
|
-
until it is configured, while `mockReadonlyProp(component, 'items', signal([]))` keeps every
|
|
1100
|
-
`computed()` and `effect()` downstream of it reactive (§9).
|
|
248
|
+
<!-- /agents-outline -->
|
|
1101
249
|
|
|
1102
250
|
---
|
|
1103
251
|
|
|
@@ -1239,284 +387,25 @@ across realms.
|
|
|
1239
387
|
|
|
1240
388
|
## 8. Observable assertions (core entry — no rxjs needed)
|
|
1241
389
|
|
|
1242
|
-
`
|
|
1243
|
-
emits, the callback never runs and nothing is asserted. Invert it — **the assertion is the `await`**:
|
|
390
|
+
In [`agent-docs/observables.md`](./agent-docs/observables.md). Read it when asserting what a stream emitted, and for `createLog()`, the order between spies.
|
|
1244
391
|
|
|
1245
|
-
|
|
1246
|
-
import { expectAllEmissions, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission } from 'vitest-auto-spy';
|
|
1247
|
-
|
|
1248
|
-
await expect(expectEmission(component.visible$)).resolves.toBe(true); // the first VALUE, not a list
|
|
1249
|
-
await expect(expectEmission(tasks$)).resolves.toEqual({ id: 1 }); // the task itself, not `[task]`
|
|
1250
|
-
await expect(expectEmissions(source$, 3)).resolves.toEqual([1, 2, 3]); // the list is this one
|
|
1251
|
-
await expectNoEmission(source$, { timeout: 50 });
|
|
1252
|
-
await expectCompletion(service.purgeCache()); // "it finished" — the value is not the point
|
|
1253
|
-
await expect(expectAllEmissions(source$)).resolves.toEqual([1, 2]); // every value, and none after
|
|
1254
|
-
```
|
|
392
|
+
<!-- agents-outline: agent-docs/observables.md -->
|
|
1255
393
|
|
|
1256
|
-
|
|
1257
|
-
wait is a quiet window rather than a watchdog). `0` and `Infinity` both mean **no watchdog** — the
|
|
1258
|
-
wait then runs to the runner's own test timeout, which is the trade. The source is duck-typed, so rxjs `Observable`s,
|
|
1259
|
-
`Subject`s, Angular `toObservable()` results, Angular `output()` (`OutputEmitterRef`, whose
|
|
1260
|
-
`subscribe` takes a bare callback) and hand-rolled subscribables all work — and every helper infers
|
|
1261
|
-
the emitted type, so `expectEmission(of(1))` is a `Promise<number>`.
|
|
1262
|
-
|
|
1263
|
-
**A `void` stream calls its listener with one argument, `undefined`.** `output<void>().emit()` and
|
|
1264
|
-
`Subject<void>.next()` both do, so a `vi.fn()` subscribed to one records `[undefined]`, and
|
|
1265
|
-
`expect(listener).toHaveBeenCalledExactlyOnceWith()` fails on `[] vs [undefined]` — the form a
|
|
1266
|
-
`prefer-called-with` autofix used to write. The assertion is the emission itself:
|
|
1267
|
-
`await expect(expectEmission(component.closed)).resolves.toBeUndefined()`, or
|
|
1268
|
-
`subscribeSpyTo(subject$).getValuesLength()` to count. A listener kept anyway is typed
|
|
1269
|
-
`vi.fn<() => void>()` (`(value: void) => void` trips `no-invalid-void-type`) and asserted
|
|
1270
|
-
`toHaveBeenCalledExactlyOnceWith(undefined)`.
|
|
1271
|
-
|
|
1272
|
-
`expectCompletion` is the one to reach for on a stream whose value is not the point — a save, a
|
|
1273
|
-
purge, an `Observable<void>`, a `Subject` a teardown closes. `firstValueFrom` rejects such a stream
|
|
1274
|
-
with rxjs's `EmptyError`, and the workaround people arrive at,
|
|
1275
|
-
`lastValueFrom(x, { defaultValue: undefined })`, reads as though the default were the interesting
|
|
1276
|
-
part. Emissions do not fail it: it asserts termination, nothing about what came before.
|
|
1277
|
-
|
|
1278
|
-
**To assert that production code pushed into a stream, do not use `observablePropsToSpyOn`.** That
|
|
1279
|
-
option points the other way: it gives the spec `nextWith` so it can _feed_ the double. When the
|
|
1280
|
-
question is whether the code under test called `next` on a property, the double needs a real
|
|
1281
|
-
`Subject` and a spy on its method:
|
|
1282
|
-
|
|
1283
|
-
```ts
|
|
1284
|
-
const forceRequery$ = new Subject<number>();
|
|
1285
|
-
|
|
1286
|
-
mockValueProp(state, 'reloadAndSeekTo$', forceRequery$);
|
|
1287
|
-
const next = spyOnOwnMethod(forceRequery$, 'next');
|
|
1288
|
-
|
|
1289
|
-
service.seek(1000);
|
|
1290
|
-
expect(next).toHaveBeenCalledWith(1000);
|
|
1291
|
-
```
|
|
1292
|
-
|
|
1293
|
-
`spyOnOwnMethod` is the same record-and-call-through a bare `vi.spyOn` gives, and it is the form to
|
|
1294
|
-
reach for when a preset bans `vi.spyOn` outright (`no-restricted-properties`): the emission still
|
|
1295
|
-
reaches subscribers, because the real `next` runs.
|
|
1296
|
-
|
|
1297
|
-
`Spy<T>` types an Observable property as `AddObservableSpyMethods<O> & T[K]`, so `next` is there on
|
|
1298
|
-
the type either way — which is exactly why this is worth saying: the code compiles against the spy
|
|
1299
|
-
surface and asserts nothing.
|
|
1300
|
-
|
|
1301
|
-
**When the error _is_ the assertion, use `expectError`.** The other helpers wrap a stream failure in
|
|
1302
|
-
a new `Error` whose message names the stream — right for reporting an unexpected failure, useless
|
|
1303
|
-
when the failure is the subject. `expectError` resolves _with_ the error, exactly as it was thrown:
|
|
1304
|
-
|
|
1305
|
-
```ts
|
|
1306
|
-
await expect(expectError(service.load())).resolves.toBe(originalError);
|
|
1307
|
-
expect(await expectError(process$)).toBeInstanceOf(UpstreamStatusError);
|
|
1308
|
-
expect((await expectError(account$)) as Error).toHaveProperty('message', 'websso fail');
|
|
1309
|
-
```
|
|
1310
|
-
|
|
1311
|
-
It waits for the error however late it arrives, and fails — naming the stream — if the stream
|
|
1312
|
-
completes or stays quiet instead. The wrapped failures of the other helpers now also carry the
|
|
1313
|
-
original on `cause`, so `rejects.toMatchObject({ cause: original })` works; prefer `expectError`,
|
|
1314
|
-
which needs no unwrapping. `firstValueFrom(source$).rejects` remains fine too.
|
|
1315
|
-
|
|
1316
|
-
**Which emission counts** — `skip` and `until`, for the stream whose first value is always stale:
|
|
1317
|
-
|
|
1318
|
-
```ts
|
|
1319
|
-
await expect(expectEmission(isXl$, { skip: 1 })).resolves.toBe(true); // a shareReplay / BehaviorSubject
|
|
1320
|
-
await expect(expectEmission(currentParams$, { until: (p) => p.channelId === expected })).resolves.toEqual(…);
|
|
1321
|
-
```
|
|
1322
|
-
|
|
1323
|
-
Both say in the assertion what `source$.pipe(skip(1))` / `pipe(filter(…))` say in the source, and
|
|
1324
|
-
they keep the diagnosis: emissions that do not match are still counted, so a failure reads
|
|
1325
|
-
`4 emission(s) received` rather than `0` and tells "the wrong thing fired" apart from "nothing
|
|
1326
|
-
fired".
|
|
1327
|
-
|
|
1328
|
-
**`advance` closes the window between subscribing and awaiting.** A stream driven by a
|
|
1329
|
-
`debounceTime`, a retry or a poll needs the clock moved _after_ something is listening, and `await`
|
|
1330
|
-
gives control away before the next statement runs:
|
|
1331
|
-
|
|
1332
|
-
```ts
|
|
1333
|
-
await expect(expectEmission(purchased$, { advance: () => vi.runAllTimers() })).resolves.toBe(false);
|
|
1334
|
-
```
|
|
394
|
+
- `createLog()` — the order between the spies
|
|
1335
395
|
|
|
1336
|
-
|
|
1337
|
-
breaks silently the moment somebody adds an `await` one line above it. It is a callback rather than
|
|
1338
|
-
an `advanceTimers: true` flag because these helpers are in the core entry, which contains no test
|
|
1339
|
-
runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`. A throw out of
|
|
1340
|
-
`advance` — or out of an `until` predicate — is reported as itself and tears the subscription down,
|
|
1341
|
-
rather than being lost while the wait runs on to the timeout.
|
|
1342
|
-
|
|
1343
|
-
**These helpers subscribe as a subscriber, so a synchronous source stops at the value that settles
|
|
1344
|
-
the wait.** `expectEmission(from([1, 2, 3]).pipe(tap(spy)))` calls `tap` once, not three times, so
|
|
1345
|
-
`expect(spy).toHaveBeenCalledTimes(1)` is honest and a `finalize` runs at the stop; before, the
|
|
1346
|
-
producer ran to completion before anything could unsubscribe, and an endless synchronous source
|
|
1347
|
-
(`of(1).pipe(repeat())`) hung the worker instead of resolving. `expectEmissions(source$, 3)` stops
|
|
1348
|
-
at the third. A source that cannot be subscribed to at all is reported by name now, with a separate
|
|
1349
|
-
hint when what was passed is a promise.
|
|
1350
|
-
|
|
1351
|
-
`expectEmissions(source$, 0)` throws at the call — a count below 1 is not something a stream can
|
|
1352
|
-
satisfy, and the message names `expectNoEmission` instead. A suite that wrote
|
|
1353
|
-
`expectEmissions(s, expected.length)` with an empty expectation is the one this changes.
|
|
1354
|
-
|
|
1355
|
-
**The watchdog runs on real time, on purpose — even under fake timers, and even under zone.js.** A
|
|
1356
|
-
virtual one would race the timers the spec advances: `expectEmission(source$, { timeout: 200 })`
|
|
1357
|
-
followed by `vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the
|
|
1358
|
-
spec was about to advance into. Inside `fakeAsync` that used to happen anyway, because zone.js
|
|
1359
|
-
patches the global `setTimeout` and the watchdog was scheduled onto the virtual queue: a
|
|
1360
|
-
`tick(1_500)` towards a `debounceTime(2_000)` rejected the wait it was advancing. The timer is taken
|
|
1361
|
-
from `__zone_symbol__setTimeout` now, so it is outside the zone and `tick()` cannot reach it.
|
|
1362
|
-
|
|
1363
|
-
The cost is that in a suite with global fake timers a _failing_ assertion spends a real second. Do
|
|
1364
|
-
**not** answer that with `{ timeout: 0 }` at every call site — that disables the watchdog, and the
|
|
1365
|
-
next silent stream hangs to the runner's own timeout with nothing useful in the message. Lower the
|
|
1366
|
-
default once instead:
|
|
1367
|
-
|
|
1368
|
-
```ts
|
|
1369
|
-
// vitest.setup.ts
|
|
1370
|
-
import { setEmissionTimeout } from 'vitest-auto-spy';
|
|
1371
|
-
import { setupAutoSpy } from 'vitest-auto-spy/setup';
|
|
1372
|
-
|
|
1373
|
-
setupAutoSpy({ globalFakeTimers: true });
|
|
1374
|
-
setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
|
|
1375
|
-
```
|
|
1376
|
-
|
|
1377
|
-
`setEmissionTimeout` takes `0` (no watchdog) and `Infinity` (the same), and **throws** on `NaN` and
|
|
1378
|
-
on a negative number rather than installing a default that fails every wait. The commonest way to
|
|
1379
|
-
reach it is arithmetic: `setEmissionTimeout(Number(process.env.EMISSION_TIMEOUT))` with the variable
|
|
1380
|
-
unset.
|
|
1381
|
-
|
|
1382
|
-
**The code frame these failures open is the spec line, not this package.** The helpers build their
|
|
1383
|
-
error inside a `subscribe` or timer callback, so the stack is captured at helper entry and pinned on
|
|
1384
|
-
when the failure is finally built; only errors these helpers make themselves are re-anchored, and
|
|
1385
|
-
the error `expectError` resolves with keeps the stack of the code under test. Do not wrap them in
|
|
1386
|
-
`vi.defineHelper` — its `__VITEST_HELPER__` frame lands last and Vitest then drops the stack whole.
|
|
1387
|
-
|
|
1388
|
-
**`expectEmission` subscribes when you call it, not when you await it**, and that is load-bearing
|
|
1389
|
-
rather than an implementation detail. It is what converts the test whose source has to be poked
|
|
1390
|
-
_after_ somebody is listening — a router event, a `Subject` the spec pushes into, anything that
|
|
1391
|
-
does not replay:
|
|
1392
|
-
|
|
1393
|
-
```ts
|
|
1394
|
-
const breadcrumbs = expectEmission(service.buildDynamicBreadcrumbs({ root })); // subscribed already
|
|
1395
|
-
|
|
1396
|
-
router.events.nextWith(navigationEnd); // …so this emission is not missed
|
|
1397
|
-
|
|
1398
|
-
await expect(breadcrumbs).resolves.toEqual([…]);
|
|
1399
|
-
```
|
|
1400
|
-
|
|
1401
|
-
`firstValueFrom` cannot do this half: it also subscribes eagerly, but there is nowhere to put the
|
|
1402
|
-
line that triggers the source, because the `await` is the same statement as the subscription — so
|
|
1403
|
-
the test deadlocks against a source that only emits once something pokes it. Hold the promise
|
|
1404
|
-
first, poke, then await.
|
|
1405
|
-
|
|
1406
|
-
### `createLog()` — the order between the spies
|
|
1407
|
-
|
|
1408
|
-
```ts
|
|
1409
|
-
const log = createLog<'drop-cache' | 'flush-telemetry' | 'stop-engine'>();
|
|
1410
|
-
|
|
1411
|
-
engine.onShutdown(log.fn('drop-cache'));
|
|
1412
|
-
engine.onShutdown(log.fn('stop-engine'));
|
|
1413
|
-
engine.onShutdown(log.fn('flush-telemetry'));
|
|
1414
|
-
|
|
1415
|
-
engine.shutdown();
|
|
1416
|
-
|
|
1417
|
-
expect(log.result()).toBe('drop-cache; flush-telemetry; stop-engine'); // fails with the real order
|
|
1418
|
-
```
|
|
1419
|
-
|
|
1420
|
-
Emission helpers answer a sequence _within one source_. The order of calls **across** collaborators
|
|
1421
|
-
is the gap `toHaveBeenCalled` papers over — three green checks that would accept the sequence
|
|
1422
|
-
backwards — and `toHaveBeenCalledBefore` covers pairwise. One journal the code under test writes into
|
|
1423
|
-
(`add`, `fn(value)` for a labelled callback, `clear`, `items`, `result()`) makes the sequence a
|
|
1424
|
-
single comparable value; `T` is a string union so a step nobody declared is a compile error. In
|
|
1425
|
-
Angular, provide it and let the component report its own lifecycle:
|
|
1426
|
-
`providers: [{ provide: PANEL_LOG, useValue: log }]`. Ported from Angular's own `Log`, which the
|
|
1427
|
-
framework keeps three copies of.
|
|
396
|
+
<!-- /agents-outline -->
|
|
1428
397
|
|
|
1429
398
|
---
|
|
1430
399
|
|
|
1431
400
|
## 9. Patching properties (and putting them back)
|
|
1432
401
|
|
|
1433
|
-
|
|
1434
|
-
import { mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from 'vitest-auto-spy';
|
|
1435
|
-
|
|
1436
|
-
mockReadonlyProp(service, 'isReady', true); // static value, signals included
|
|
1437
|
-
mockReadonlyPropGetter(service, 'label', () => 'A'); // dynamic getter
|
|
1438
|
-
mockValueProp(service, 'retries', 3); // plain writable value
|
|
1439
|
-
mockAccessorsProp(service, 'theme'); // spied get + set
|
|
1440
|
-
|
|
1441
|
-
restoreMockedProps(); // put every patch back; each helper also returns its own undo
|
|
1442
|
-
```
|
|
1443
|
-
|
|
1444
|
-
`vi.restoreAllMocks()` does **not** undo these — it knows about spies, not about redefined
|
|
1445
|
-
properties. Never use bare `Object.defineProperty` in a spec: nothing restores the original
|
|
1446
|
-
descriptor, and under `isolate: false` the patch leaks into the next file.
|
|
1447
|
-
|
|
1448
|
-
**They work on `createAutoMock` and `mockDeep` doubles too** — which they did not until 3.5.0.
|
|
1449
|
-
Both are Proxies, all four helpers are built on `Object.defineProperty`, and neither Proxy trapped
|
|
1450
|
-
it: the patch landed on the Proxy's own target, the `get` trap never looked there, nothing threw,
|
|
1451
|
-
and the test carried on reading the old value. If you have seen a spec build a double by hand —
|
|
1452
|
-
real getters plus a `createFunctionSpy` per method — this is usually why.
|
|
1453
|
-
|
|
1454
|
-
**The second overload is a normal tool, not a last resort.** Each helper has a checked overload
|
|
1455
|
-
(`K extends keyof T`) and a `(object, property: PropertyKey, value: unknown)` one behind it, and
|
|
1456
|
-
the JSDoc calls the latter the one for members the public type does not describe. In practice it
|
|
1457
|
-
carries about half of the real calls, all of them legitimate. It cannot reach a JS `#private` field —
|
|
1458
|
-
no property key can — and `'x' as keyof T` over one patches a new, unrelated key that nothing reads:
|
|
1459
|
-
|
|
1460
|
-
```ts
|
|
1461
|
-
mockValueProp(router, 'routerState', { snapshot: { url: '/home' } }); // a partial fixture of a fat type
|
|
1462
|
-
mockValueProp(window, 'AudioContext', undefined); // "this platform does not ship the API"
|
|
1463
|
-
mockValueProp(transitionEvent, 'propertyName', 'opacity'); // a field a synthetic DOM event lacks
|
|
1464
|
-
mockValueProp(spy, 'products$', new Subject()); // a member the double does not have at all
|
|
1465
|
-
```
|
|
1466
|
-
|
|
1467
|
-
The last one is worth knowing on its own: patching a key the object never had **works and is undone
|
|
1468
|
-
correctly** — the journal records the _absence_ of a descriptor and puts it back by deleting the
|
|
1469
|
-
property. That is how you add an Observable member that `provideAutoSpy` did not create because
|
|
1470
|
-
`observablePropsToSpyOn` was not passed.
|
|
1471
|
-
|
|
1472
|
-
What the second overload costs is the property-name check, so a typo in the name compiles. Nothing
|
|
1473
|
-
checks the _value_ on either overload; that is deliberate, and the partial fixture above is why.
|
|
1474
|
-
|
|
1475
|
-
**A read the constructor does cannot be seeded by any of these.** They patch an object that already
|
|
1476
|
-
exists, so the earliest they can run is after the double is built — and a component that reads
|
|
1477
|
-
`service.paymentParams.offer` in a field initializer or in its constructor has already read it by the
|
|
1478
|
-
time `TestBed.createComponent` returns:
|
|
1479
|
-
|
|
1480
|
-
```ts
|
|
1481
|
-
fixture = TestBed.createComponent(PaymentComponent); // ← TypeError thrown here, inside the component
|
|
1482
|
-
mockReadonlyProp(paymentService, 'paymentParams', params); // never reached
|
|
1483
|
-
```
|
|
1484
|
-
|
|
1485
|
-
Nothing warns, because nothing of this library runs: the throw is a plain `TypeError` from the
|
|
1486
|
-
component's own line, and the stack names the component rather than the seeding that is missing. The
|
|
1487
|
-
only place early enough is the registration:
|
|
402
|
+
In [`agent-docs/properties.md`](./agent-docs/properties.md). Read it when a test overrides a getter, a readonly property, a signal property or a DOM object property and has to put it back.
|
|
1488
403
|
|
|
1489
|
-
|
|
1490
|
-
providers: [provideAutoSpy(PaymentService, { overrides: { paymentParams: params } })];
|
|
1491
|
-
providers: [provideAutoSpyForToken(STATE_TOKEN, { snapshot })]; // a token's second argument does the same
|
|
1492
|
-
```
|
|
1493
|
-
|
|
1494
|
-
Three migration shards found this independently, each of them after the failure had pointed at the
|
|
1495
|
-
service. A `mock*Prop` after the render is right for everything a template or a method reads later —
|
|
1496
|
-
which is most of them — and wrong only for a constructor-time read.
|
|
1497
|
-
|
|
1498
|
-
### Properties of DOM objects — the same helpers, and the reason to look for them
|
|
1499
|
-
|
|
1500
|
-
`document.fullscreenElement`, `document.visibilityState`, `document.cookie`, `navigator.userAgent`,
|
|
1501
|
-
`element.scrollHeight`: half the patching a browser suite does is on objects, not on `globalThis`,
|
|
1502
|
-
so the "globals go through `stubGlobal`" rule does not cover it. `mockValueProp` does — it is the
|
|
1503
|
-
port of `jest.replaceProperty`, and a project that never used that one walks straight past it.
|
|
404
|
+
<!-- agents-outline: agent-docs/properties.md -->
|
|
1504
405
|
|
|
1505
|
-
|
|
1506
|
-
mockValueProp(document, 'fullscreenElement', videoElement);
|
|
1507
|
-
mockValueProp(navigator, 'userAgent', 'Tizen 6.0');
|
|
1508
|
-
mockValueProp(element, 'scrollHeight', 400);
|
|
1509
|
-
```
|
|
1510
|
-
|
|
1511
|
-
The hand-written form fails in three ways that all surface in **someone else's file**:
|
|
406
|
+
- Properties of DOM objects — the same helpers, and the reason to look for them
|
|
1512
407
|
|
|
1513
|
-
-
|
|
1514
|
-
can never be changed or removed again — for the rest of the worker;
|
|
1515
|
-
- the undo is written as the last line of the test, so a failing assertion skips it;
|
|
1516
|
-
- the real property is an **accessor on the prototype** (`document.fullscreenElement` is one), the
|
|
1517
|
-
patch writes a `value` over it, and "put the old descriptor back" is not the correct undo —
|
|
1518
|
-
deleting the own property is. `mockValueProp` records what was actually there and does the right
|
|
1519
|
-
one.
|
|
408
|
+
<!-- /agents-outline -->
|
|
1520
409
|
|
|
1521
410
|
---
|
|
1522
411
|
|
|
@@ -1544,281 +433,25 @@ In [`agent-docs/setup.md`](./agent-docs/setup.md). Read it when the task touches
|
|
|
1544
433
|
|
|
1545
434
|
## 11. Waiting: four queues, and which tool drives each
|
|
1546
435
|
|
|
1547
|
-
|
|
1548
|
-
mechanisms, and a test that waits on the wrong one fails with a message that names none of them.
|
|
1549
|
-
|
|
1550
|
-
| What is pending | What drives it | What does **not** |
|
|
1551
|
-
| --- | --- | --- |
|
|
1552
|
-
| change detection | `fixture.detectChanges()` | anything `await`ed |
|
|
1553
|
-
| effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
|
|
1554
|
-
| timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
|
|
1555
|
-
| a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
|
|
1556
|
-
| an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
|
|
1557
|
-
|
|
1558
|
-
```ts
|
|
1559
|
-
import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
|
|
1560
|
-
|
|
1561
|
-
fixture.debugElement.query(By.css('.open')).nativeElement.click(); // production code: await import(…)
|
|
1562
|
-
await settleDynamicImport(() => import('./profile-select.modal'));
|
|
1563
|
-
```
|
|
436
|
+
In [`agent-docs/waiting.md`](./agent-docs/waiting.md). Read it when a test waits for something: change detection, effects, timers, microtasks and dynamic imports are four queues, each with its own tool.
|
|
1564
437
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
- **`afterNextRender` does not run on `detectChanges()`.** A component that fills a form there is
|
|
1568
|
-
still empty when the assertion reads it. `await stable(fixture)` (or `await fixture.whenStable()`)
|
|
1569
|
-
is what runs the after-render phase.
|
|
1570
|
-
- **`fixture.whenRenderingDone()` is not a stronger `whenStable()`.** With an animation renderer
|
|
1571
|
-
installed it degrades to `Promise.resolve()`. Use `stable(fixture)`.
|
|
1572
|
-
- **`fakeAsync` / `tick()` / `flushMicrotasks()` never reach the module loader.** Spinning
|
|
1573
|
-
`await Promise.resolve()` ten times looks like it works and instead lands the continuation after
|
|
1574
|
-
teardown — a green run with `NG0205: Injector has already been destroyed` in "Unhandled Errors"
|
|
1575
|
-
and a non-zero exit code.
|
|
1576
|
-
|
|
1577
|
-
**`stable()` and `flushEffects()` are not zoneless-only, and their tick runs inside the `NgZone` so
|
|
1578
|
-
that they are not.** `TestBed.createComponent` builds the component inside `ngZone.run(…)`, so an
|
|
1579
|
-
`effect()` its constructor registers records the zone's inner zone as its own. A bare `TestBed.tick()`
|
|
1580
|
-
from a test body runs in the runner's zone, so Angular hops back into the recorded one to run a
|
|
1581
|
-
**dirty** effect, and leaving that hop takes the zone from unstable to stable.
|
|
1582
|
-
`provideZoneChangeDetection()` — which `@angular/build:unit-test` installs for every suite that loads
|
|
1583
|
-
zone.js — answers `NgZone.onMicrotaskEmpty` with `ApplicationRef._tick()`, guarded against its own
|
|
1584
|
-
scheduler but not against `ApplicationRef._runningTick`, so the tick already on the stack is
|
|
1585
|
-
re-entered: `NG0101: ApplicationRef.tick is called recursively`. Angular hands that to `ErrorHandler`
|
|
1586
|
-
instead of throwing it at the call site, so a suite that does not fail on console output stays green
|
|
1587
|
-
with the change detection it asked for unfinished. Measured on an Angular 22 suite of 1771 spec files:
|
|
1588
|
-
451 `componentRef.setInput` calls rewritten to `setInputs` turned **57 green files red** on `NG0101`,
|
|
1589
|
-
and none of them after the tick moved inside the zone. Under zoneless `NgZone` is a `NoopNgZone` whose
|
|
1590
|
-
`run` is a straight call, so it costs nothing there. One thing is genuinely new for a zone-based
|
|
1591
|
-
consumer: leaving the zone reports it stable, so the same subscriber ticks once more — counted on
|
|
1592
|
-
`ApplicationRef.afterTick`, one `flushEffects()` is 1 → 2 application ticks, which is what
|
|
1593
|
-
`fixture.detectChanges()` has always done in the same position. Do not "simplify" either helper back
|
|
1594
|
-
to a bare `TestBed.tick()`; `src/zone-tests/stable.zone-test.ts` is the guard.
|
|
1595
|
-
|
|
1596
|
-
`flushEventLoopUntil(isDone, { turns, label })` is the same thing with a condition and a budget —
|
|
1597
|
-
for a chunk becoming reachable, an SDK reporting itself ready, a queue draining. Use it instead of a
|
|
1598
|
-
hand-tuned turn count: the count depends on the dependency, not on the spec, and a condition that
|
|
1599
|
-
never holds fails naming the `label` rather than hanging until the runner's timeout. For real I/O —
|
|
1600
|
-
a socket round-trip, a child process — pass `{ timeoutMs, label }` instead of `turns`: it polls the
|
|
1601
|
-
real clock every 10 ms, unaffected by fake timers, and replaces a hand-rolled `waitFor(predicate, ms)`.
|
|
1602
|
-
|
|
1603
|
-
**Not for an Angular `resource()` / `httpResource()`.** Those need a change-detection _tick_, and
|
|
1604
|
-
this helper only takes event-loop turns — a resource awaited through it finishes the whole budget
|
|
1605
|
-
having issued zero requests. `settleResource(resource, { turns, label, allowIdle })` from
|
|
1606
|
-
`vitest-auto-spy/angular` is that wait.
|
|
1607
|
-
|
|
1608
|
-
`flushEventLoop(turns?)` takes real event-loop turns even while the timers are faked, without
|
|
1609
|
-
touching the clock. It is the honest name for the `await vi.advanceTimersByTimeAsync(0)` trick,
|
|
1610
|
-
which reads as "move the timers" in a test that has no timers and gets deleted as noise.
|
|
438
|
+
<!-- agents-outline: agent-docs/waiting.md -->
|
|
439
|
+
<!-- /agents-outline -->
|
|
1611
440
|
|
|
1612
441
|
---
|
|
1613
442
|
|
|
1614
443
|
## 12. Doubles for what the code builds itself
|
|
1615
444
|
|
|
1616
|
-
|
|
1617
|
-
an implementation that is itself constructible, and **an arrow function is not**: the call is
|
|
1618
|
-
recorded, the body never runs, `new` hands back an empty object. The warning Vitest prints
|
|
1619
|
-
("the mock did not use 'function' or 'class'") is nowhere near the failure, which arrives as
|
|
1620
|
-
`TypeError: (cb) => {…} is not a constructor` with a stack **in production code** — or as a green
|
|
1621
|
-
test for the wrong reason, when the resulting `undefined` is swallowed by a `catch`.
|
|
1622
|
-
|
|
1623
|
-
```ts
|
|
1624
|
-
import { createSpyClass, mockConstructor, stubConstructor } from 'vitest-auto-spy';
|
|
1625
|
-
|
|
1626
|
-
// a real class exists → full auto-spy instances
|
|
1627
|
-
mockValueProp(globalThis, 'Worker', createSpyClass(BackgroundWorker));
|
|
1628
|
-
|
|
1629
|
-
// only a type / a shape exists → a runner mock that is also a constructor
|
|
1630
|
-
const LicenseClient = mockConstructor<LicenseClient>(() => ({ prepareRequest: vi.fn() }));
|
|
445
|
+
In [`agent-docs/doubles.md`](./agent-docs/doubles.md). Read it when production code does `new Foo()`, touches `<video>`, `localStorage`, animation frames or element boxes, or a module mock did nothing.
|
|
1631
446
|
|
|
1632
|
-
|
|
1633
|
-
const Image = stubConstructor(globalThis, 'Image', () => ({ src: '' }));
|
|
1634
|
-
|
|
1635
|
-
tracker.ping();
|
|
1636
|
-
expect(Image).toHaveBeenCalledTimes(1);
|
|
1637
|
-
expect(Image.instances[0].src).toBe('https://tns.example/hit');
|
|
1638
|
-
```
|
|
1639
|
-
|
|
1640
|
-
`mockConstructor` stays a runner mock, so `toHaveBeenCalledWith` / `mockClear` work as usual, and it
|
|
1641
|
-
throws a named error if it is ever called **without** `new`. `stubConstructor` installs through
|
|
1642
|
-
`mockValueProp`, so `restoreMockedProps()` puts the platform's constructor back.
|
|
1643
|
-
|
|
1644
|
-
**Static members are opt-in — `createSpyClass(Class, config, { statics: true })`.** Production code
|
|
1645
|
-
that reaches the class rather than an instance (`BackgroundWorker.isSupported()`,
|
|
1646
|
-
`Client.fromToken(t)`, a `VERSION` constant) finds nothing on the double otherwise, and the failure
|
|
1647
|
-
is a `TypeError` on the class, not on the spy:
|
|
1648
|
-
|
|
1649
|
-
```ts
|
|
1650
|
-
const WorkerSpy = createSpyClass(BackgroundWorker, undefined, { statics: true });
|
|
1651
|
-
|
|
1652
|
-
WorkerSpy.isSupported.mockReturnValue(true); // needs a cast — see below
|
|
1653
|
-
```
|
|
1654
|
-
|
|
1655
|
-
What the option copies, walking the class's own prototype chain so inherited statics come too: a
|
|
1656
|
-
static **function** becomes a full spy of its own, a static **data member** is copied by value, and a
|
|
1657
|
-
static **accessor** is skipped and never evaluated — a getter that reads configuration or touches the
|
|
1658
|
-
network must not run because a double was built. `prototype`, `length`, `name`, `caller`,
|
|
1659
|
-
`arguments` and the double's own `calls` / `instances` are never overwritten. It is off by default
|
|
1660
|
-
for that last reason: a class with a static named `calls` would otherwise shadow the construction
|
|
1661
|
-
log. **The statics are not typed** — the return type is still `ConstructorSpy<T>`, so reading one
|
|
1662
|
-
needs a cast at the spec; type them and this note goes.
|
|
1663
|
-
|
|
1664
|
-
For the three observers, prefer the purpose-built stubs (§13). For a `Worker` the code builds with
|
|
1665
|
-
`new Worker(new URL(…, import.meta.url))`, prefer `stubWorker({ respond })` from `/dom-stubs`: an
|
|
1666
|
-
`EventTarget` whose listeners all receive the reply, answered on a microtask after `postMessage`
|
|
1667
|
-
returns, with `last.messages`, `last.emit(data)` and `last.fail(error)` on the handle. For `AbortController` — which breaks
|
|
1668
|
-
in a jsdom run for a reason involving none of the three parties in the stack trace — use
|
|
1669
|
-
`stubAbortController()`.
|
|
1670
|
-
|
|
1671
|
-
The stub carries `AbortSignal.abort()`, `AbortSignal.timeout()` and `AbortSignal.any()` as well —
|
|
1672
|
-
the three statics are how modern code makes a signal without a controller, `fetch(url, { signal:
|
|
1673
|
-
AbortSignal.timeout(5_000) })` most of all. `timeout()` aborts through `setTimeout`, so
|
|
1674
|
-
`vi.useFakeTimers()` drives it exactly as it drives the platform's, and it aborts with a
|
|
1675
|
-
`TimeoutError` rather than an `AbortError` because that is the distinction the platform draws. A
|
|
1676
|
-
signal's `reason` is the platform's `DOMException`, so code branching on
|
|
1677
|
-
`signal.reason.name === 'AbortError'` takes the same branch it takes in a browser.
|
|
1678
|
-
|
|
1679
|
-
### `<video>` and `<audio>`
|
|
1680
|
-
|
|
1681
|
-
jsdom implements them as a shell: `play()` throws, `duration` is `NaN` and is an accessor with no
|
|
1682
|
-
setter, `canPlayType()` answers `''` for everything, `readyState` never leaves 0, `error` is not on
|
|
1683
|
-
the prototype. `stubMediaElement()` patches the prototype (so it covers an element production code
|
|
1684
|
-
creates itself) and, crucially, **fires the event that goes with each change** — production code
|
|
1685
|
-
listens for `durationchange` / `timeupdate` / `ended`, and assigning the field alone leaves those
|
|
1686
|
-
handlers unrun:
|
|
1687
|
-
|
|
1688
|
-
```ts
|
|
1689
|
-
const media = stubMediaElement({ duration: 120 });
|
|
1690
|
-
|
|
1691
|
-
media.set(video, { readyState: 1 }); // → loadedmetadata
|
|
1692
|
-
media.set(video, { currentTime: 119 }); // → timeupdate
|
|
1693
|
-
media.set(video, { ended: true }); // → pause, then ended — and the element is paused
|
|
1694
|
-
expect(media.play).toHaveBeenCalledTimes(1);
|
|
1695
|
-
```
|
|
1696
|
-
|
|
1697
|
-
State is per element, so an ad and the content report different durations — and per **install**, so
|
|
1698
|
-
an element held in module scope or left in the document under `isolate: false` takes the new
|
|
1699
|
-
install's `duration` rather than the previous one's. `set({ ended: true })` pauses the element and
|
|
1700
|
-
fires `pause` before `ended`, as the platform does: an element that is both ended and playing is a
|
|
1701
|
-
state no browser produces, and a player listening for `pause` was never told.
|
|
1702
|
-
|
|
1703
|
-
`currentTime` is a get/set pair, so a player restarting itself with `video.currentTime = 0` reaches
|
|
1704
|
-
the record and fires `timeupdate` too — `media.set()` is not the only way in, and the component's own
|
|
1705
|
-
handler runs where it used to stay unrun while the assertion read the new value.
|
|
1706
|
-
|
|
1707
|
-
### `localStorage` and `sessionStorage`
|
|
1708
|
-
|
|
1709
|
-
Do not write a `TestingStorage` class per project. `stubWebStorage()` installs an in-memory `Storage`
|
|
1710
|
-
for this test — `getItem` / `setItem` / `removeItem` / `clear` / `key` / `length`, string coercion
|
|
1711
|
-
as the platform does it — and `snapshot()` is the plain record to assert on:
|
|
1712
|
-
|
|
1713
|
-
```ts
|
|
1714
|
-
import { type WebStorageStub, stubWebStorage } from 'vitest-auto-spy/dom-stubs';
|
|
447
|
+
<!-- agents-outline: agent-docs/doubles.md -->
|
|
1715
448
|
|
|
1716
|
-
|
|
449
|
+
- `<video>` and `<audio>`
|
|
450
|
+
- `localStorage` and `sessionStorage`
|
|
451
|
+
- Animation frames and element boxes
|
|
452
|
+
- A module mock that did nothing
|
|
1717
453
|
|
|
1718
|
-
|
|
1719
|
-
local = stubWebStorage('localStorage', { items: { token: 'abc' } }); // or 'sessionStorage'
|
|
1720
|
-
});
|
|
1721
|
-
|
|
1722
|
-
it('forgets the token on logout', () => {
|
|
1723
|
-
session.logout();
|
|
1724
|
-
expect(local.snapshot()).toEqual({});
|
|
1725
|
-
});
|
|
1726
|
-
```
|
|
1727
|
-
|
|
1728
|
-
Writes in the middle of a test go through `.storage`, the installed `Storage` itself:
|
|
1729
|
-
`local.storage.setItem('token', 'xyz')`. The stub is not a `Storage`; `local.setItem` is a `TS2339`.
|
|
1730
|
-
|
|
1731
|
-
It goes through `mockValueProp`, so `restoreMockedProps()` (and `setupAutoSpy()` between tests) puts
|
|
1732
|
-
the previous storage back; install it in `beforeEach`. It also lands on `document.defaultView` when
|
|
1733
|
-
that is a separate object. It is not `restoreWebStorage()`: that one repairs a broken environment
|
|
1734
|
-
once and leaves a working storage alone; this one replaces whatever is there, for one test, and
|
|
1735
|
-
installs even in a `node` environment because the spec asked for it. Named-property access
|
|
1736
|
-
(`localStorage.token`, `Object.keys(localStorage)`) does not see the items — read through the API.
|
|
1737
|
-
|
|
1738
|
-
### Animation frames and element boxes
|
|
1739
|
-
|
|
1740
|
-
Do not assign `window.requestAnimationFrame` by hand, and do not stub `getBoundingClientRect` with a
|
|
1741
|
-
partial literal. Both have a stub on `/dom-stubs`:
|
|
1742
|
-
|
|
1743
|
-
```ts
|
|
1744
|
-
import { stubAnimationFrame, stubElementRect } from 'vitest-auto-spy/dom-stubs';
|
|
1745
|
-
|
|
1746
|
-
beforeEach(() => {
|
|
1747
|
-
stubAnimationFrame(); // 'immediate': the callback runs before requestAnimationFrame returns
|
|
1748
|
-
stubElementRect(container, { width: 800, height: 600 }); // x / y default to 0
|
|
1749
|
-
});
|
|
1750
|
-
|
|
1751
|
-
it('scrolls after the next frame', () => {
|
|
1752
|
-
const frames = stubAnimationFrame({ mode: 'queued' });
|
|
1753
|
-
|
|
1754
|
-
list.scrollToSelected();
|
|
1755
|
-
expect(frames.pending).toBe(1);
|
|
1756
|
-
|
|
1757
|
-
frames.flush(); // one frame; the timestamp defaults to performance.now()
|
|
1758
|
-
expect(list.scrolled).toBe(true);
|
|
1759
|
-
});
|
|
1760
|
-
```
|
|
1761
|
-
|
|
1762
|
-
`stubAnimationFrame` stubs `cancelAnimationFrame` too, and hands both spies back as
|
|
1763
|
-
`frames.requestAnimationFrame` / `frames.cancelAnimationFrame`. A frame requested from inside a
|
|
1764
|
-
running frame is queued in either mode, so a loop that requests its own next frame advances one step
|
|
1765
|
-
per `flush()`. A callback that throws stops `flush()` (or propagates out of `requestAnimationFrame`
|
|
1766
|
-
in `'immediate'` mode); pass `onError: (error) => { … }` to intercept it instead — rethrow from
|
|
1767
|
-
inside to keep the default for an error you did not mean to swallow. `stubElementRect` returns a real
|
|
1768
|
-
`DOMRect` per call, edges derived, plus the installed spy as `.getBoundingClientRect` for
|
|
1769
|
-
`expect(stub.getBoundingClientRect).toHaveBeenCalled()`, and the return value is still callable as the
|
|
1770
|
-
undo. The box is the spy's creation implementation, so `vi.resetAllMocks()` / `mockReset()` mid-test
|
|
1771
|
-
keeps it on both spy engines; Bun's `mockReset()` drops it (re-stub after a reset there). Both go through `mockValueProp`, so `restoreMockedProps()` (and `setupAutoSpy()` between tests)
|
|
1772
|
-
takes them off; `frames.restore()` does it sooner.
|
|
1773
|
-
|
|
1774
|
-
### A module mock that did nothing
|
|
1775
|
-
|
|
1776
|
-
`vi.mock()` is the one thing in a ported suite that fails **silently**. Under a bundler
|
|
1777
|
-
(`@angular/build:unit-test`, a pre-built `vite-node` entry) a workspace alias or a barrel is already
|
|
1778
|
-
inlined when the mock would be installed, so the real implementation runs and the test either passes
|
|
1779
|
-
for the wrong reason or fails somewhere unrelated.
|
|
1780
|
-
|
|
1781
|
-
```ts
|
|
1782
|
-
import * as engine from '@app/pricing-engine';
|
|
1783
|
-
|
|
1784
|
-
vi.mock('@app/pricing-engine');
|
|
1785
|
-
beforeEach(() => assertMocked(engine, { specifier: '@app/pricing-engine', exports: ['createEngine'] }));
|
|
1786
|
-
```
|
|
1787
|
-
|
|
1788
|
-
And when a mocked dependency probes itself with `mod.default ?? mod` — every package that ships both
|
|
1789
|
-
CJS and ESM does — a factory of bare named exports throws `No "default" export is defined on the
|
|
1790
|
-
mock` from **inside that dependency**. `moduleNamespace` is the shape it expects:
|
|
1791
|
-
|
|
1792
|
-
```ts
|
|
1793
|
-
vi.mock('shaka-player', () => moduleNamespace({ Player: mockConstructor(() => playerStub) }));
|
|
1794
|
-
```
|
|
1795
|
-
|
|
1796
|
-
A factory that spells out its own `default` **keeps it** — `moduleNamespace({ default: dayjsStub, utc })`
|
|
1797
|
-
gives the probing dependency `dayjsStub`, where it used to be replaced by the namespace itself and
|
|
1798
|
-
the default export silently became the wrong object. Only a factory without one gets
|
|
1799
|
-
`default: <the namespace>`, which is the interop shape it was there for; the return type follows.
|
|
1800
|
-
|
|
1801
|
-
A factory's `vi.fn()` comes back typed as the real function, without `calledWith` or `resolveWith`.
|
|
1802
|
-
`adoptMock` takes it over in place — same object, history kept, typed from the export's signature —
|
|
1803
|
-
and an unconfigured call keeps answering what the mock answered before:
|
|
1804
|
-
|
|
1805
|
-
```ts
|
|
1806
|
-
import { loadUser } from './api';
|
|
1807
|
-
|
|
1808
|
-
vi.mock('./api', () => ({ loadUser: vi.fn() }));
|
|
1809
|
-
adoptMock(loadUser).calledWith(7).resolveWith({ id: 7, name: 'Ada' });
|
|
1810
|
-
```
|
|
1811
|
-
|
|
1812
|
-
To keep the real module and configure one case, spy it through:
|
|
1813
|
-
`vi.mock('./api', async (importOriginal) => moduleNamespace(await importOriginal(), { passthrough: true }))`.
|
|
1814
|
-
Every function export runs for real and is recorded until configured; classes and values stay real;
|
|
1815
|
-
calls one export makes to another inside the module are not recorded. A `vi.spyOn` / `{ spy: true }`
|
|
1816
|
-
mock calls an original it does not report, so adopting it makes an unconfigured call answer
|
|
1817
|
-
`undefined`; `node:test`'s `mock.fn()` is refused.
|
|
1818
|
-
|
|
1819
|
-
There is no `mockModule(…)` helper here, and there cannot be: Vitest hoists the literal `vi.mock`
|
|
1820
|
-
call, so a wrapper around it would be hoisted as a call to a function that does not exist yet. Share
|
|
1821
|
-
a fixture between the factory and the tests with `vi.hoisted()`.
|
|
454
|
+
<!-- /agents-outline -->
|
|
1822
455
|
|
|
1823
456
|
---
|
|
1824
457
|
|
|
@@ -1892,58 +525,10 @@ convenient re-export from the root: it would quietly hand zone.js to every zonel
|
|
|
1892
525
|
|
|
1893
526
|
## 15. Other adapters
|
|
1894
527
|
|
|
1895
|
-
|
|
1896
|
-
// NestJS
|
|
1897
|
-
import { createNestUnit, injectSpy, provideAutoSpy } from 'vitest-auto-spy/nestjs';
|
|
1898
|
-
const moduleRef = await Test.createTestingModule({ providers: [provideAutoSpy(MyService)] }).compile();
|
|
1899
|
-
const spy = injectSpy(moduleRef, MyService);
|
|
1900
|
-
// or no module at all: the unit built from its DI metadata, every unprovided token spied
|
|
1901
|
-
const { unit, spies } = createNestUnit(CartService, { expose: [PricingService], providers: [{ provide: 'CONFIG', useValue: cfg }] });
|
|
1902
|
-
spies.get(TaxService).rate.mockReturnValue(0.2); // spies.get refuses a token the unit never asked for, and an exposed class
|
|
1903
|
-
spies.autoSpiedTokens(); // what nothing provided — the list the refusal message prints; spies.exposedTokens() is what `expose` actually built
|
|
1904
|
-
// needs what Nest needs: emitDecoratorMetadata (tsc / SWC, not esbuild) and reflect-metadata loaded first
|
|
1905
|
-
|
|
1906
|
-
// Vue / Pinia — provideAutoSpy(token, Class, methodsOrConfig?) returns a `global.provide` map
|
|
1907
|
-
import { provideAutoSpy } from 'vitest-auto-spy/vue';
|
|
1908
|
-
const provide = provideAutoSpy(UserServiceKey, UserService);
|
|
1909
|
-
provide[UserServiceKey].getName.mockReturnValue('Ada');
|
|
1910
|
-
mount(Greeting, { global: { provide } });
|
|
1911
|
-
// a setup-store (`defineStore('x', () => …)`) is not a class — use createAutoMock<T>() there
|
|
1912
|
-
|
|
1913
|
-
// React / Svelte — the core API, re-exported with the right adapter registered
|
|
1914
|
-
import { createSpyFromClass } from 'vitest-auto-spy/react';
|
|
1915
|
-
```
|
|
528
|
+
In [`agent-docs/adapters.md`](./agent-docs/adapters.md). Read it for NestJS, Vue, React, Svelte, `bun test`, `node:test`, Rstest and the console entry.
|
|
1916
529
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
and take them off after it:
|
|
1920
|
-
|
|
1921
|
-
```ts
|
|
1922
|
-
import { type ConsoleSpies, installConsoleSpies, restoreConsole } from 'vitest-auto-spy/console';
|
|
1923
|
-
|
|
1924
|
-
let consoleSpies: ConsoleSpies;
|
|
1925
|
-
|
|
1926
|
-
beforeEach(() => {
|
|
1927
|
-
consoleSpies = installConsoleSpies();
|
|
1928
|
-
});
|
|
1929
|
-
afterEach(() => restoreConsole());
|
|
1930
|
-
|
|
1931
|
-
expect(consoleSpies.consoleInfoSpy).toHaveBeenCalledWith('done'); // the output is silenced, not printed
|
|
1932
|
-
```
|
|
1933
|
-
|
|
1934
|
-
The exported `consoleInfoSpy` & co. are the same objects; `restoreConsole()` keeps them and clears
|
|
1935
|
-
their calls. **Do not rely on the import to install them**: it does so once per worker, so under
|
|
1936
|
-
`isolate: false` the spies go on in whichever file imported them first and silence every later file
|
|
1937
|
-
(`no-import-time-console-spies` reports it). Under `setupAutoSpy({ strayConsole })` the import installs
|
|
1938
|
-
nothing at all.
|
|
1939
|
-
|
|
1940
|
-
The spies survive `vi.resetModules()`. A fresh copy of the module used to record the previous copy's
|
|
1941
|
-
spy as "the real `console.warn`", after which `restoreConsole()` installed that dead spy for the rest
|
|
1942
|
-
of the worker and every log from then on went nowhere. The real methods are kept on one shared table
|
|
1943
|
-
and a spy of another copy is refused as an original.
|
|
1944
|
-
|
|
1945
|
-
Import your runtime entry (`…/bun`, `…/node`) **before** `…/console`, or it registers the Vitest
|
|
1946
|
-
adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives a detached one.
|
|
530
|
+
<!-- agents-outline: agent-docs/adapters.md -->
|
|
531
|
+
<!-- /agents-outline -->
|
|
1947
532
|
|
|
1948
533
|
---
|
|
1949
534
|
|
|
@@ -1967,385 +552,27 @@ In [`agent-docs/errors.md`](./agent-docs/errors.md). A table from the text of a
|
|
|
1967
552
|
|
|
1968
553
|
## 18. Do not write this
|
|
1969
554
|
|
|
1970
|
-
|
|
1971
|
-
| --- | --- |
|
|
1972
|
-
| `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
|
|
1973
|
-
| `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
|
|
1974
|
-
| `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
|
|
1975
|
-
| `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
|
|
1976
|
-
| `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
|
|
1977
|
-
| `{ provide: ActivatedRoute, useValue: { snapshot: { params } } }` | `provideActivatedRoute({ params })` — streams and snapshot from one record |
|
|
1978
|
-
| `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate } })` | `provideRouterDouble({ url })` — one URL behind `url`, `routerState` and `events` |
|
|
1979
|
-
| `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
|
|
1980
|
-
| `vi.spyOn(component, 'ngOnInit')` after `createComponent` | `vi.spyOn(Cls.prototype, 'ngOnInit')` before it, or assert the effect |
|
|
1981
|
-
| `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` | `useValue: false` — a value of the type the token declares |
|
|
1982
|
-
| `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
|
|
1983
|
-
| `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
|
|
1984
|
-
| `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
|
|
1985
|
-
| `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
|
|
1986
|
-
| `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
|
|
1987
|
-
| `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
|
|
1988
|
-
| `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
|
|
1989
|
-
| `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
|
|
1990
|
-
| hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
|
|
1991
|
-
| `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
|
|
1992
|
-
| a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
|
|
1993
|
-
| a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
|
|
1994
|
-
| `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
|
|
1995
|
-
| `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
|
|
1996
|
-
| `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
|
|
1997
|
-
| `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
|
|
1998
|
-
| `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
|
|
1999
|
-
| `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
|
|
2000
|
-
| a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
|
|
2001
|
-
| an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
|
|
2002
|
-
| `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
|
|
2003
|
-
| `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
|
|
2004
|
-
| `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
|
|
2005
|
-
| `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
|
|
2006
|
-
| `const x = a?.b; assert.exists(x); … x …` on every optional read | `narrow.defined(a?.b)` — it returns the value, so the narrowing sits in the expression |
|
|
2007
|
-
| `expect(body).toBeInstanceOf(FormData); const form = body as FormData` | `narrow.instanceOf(body, FormData)` — the check and the type in one expression |
|
|
2008
|
-
| five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
|
|
2009
|
-
| `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
|
|
2010
|
-
| `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
|
|
2011
|
-
| `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
|
|
2012
|
-
| ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
|
|
2013
|
-
| a bare `await import('…')` in a test body | `await settleDynamicImport(() => import('…'))` — `prefer-settle-dynamic-import` reports it |
|
|
2014
|
-
| a capture whose only assertions are `toEqual([])` / `not.toHaveBeenCalled()` | `await expectNoEmission(source$)` — `no-vacuous-absence-assertion` reports it |
|
|
2015
|
-
| `{ id: '1', isOffline: false } as Device` (a cast passes an excess key) | `createMock<Device>({ id: '1' })` — `prefer-create-mock` reports it |
|
|
2016
|
-
| `TestBed.inject(S).m as Mock` (`Mock` is `Mock<any>`) | `injectSpy(S).m` — `no-mock-cast` reports it |
|
|
2017
|
-
| `await fixture.whenRenderingDone()` | `await stable(fixture)` |
|
|
2018
|
-
| an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
|
|
2019
|
-
| `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
|
|
2020
|
-
| `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
|
|
2021
|
-
| `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
|
|
2022
|
-
| `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift |
|
|
2023
|
-
| a `TestingStorage` class assigned to `globalThis.localStorage` | `stubWebStorage('localStorage')`, restored with the other patches |
|
|
2024
|
-
| an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
|
|
2025
|
-
| `configureTestingModule` inside every `it()` | one per `describe` |
|
|
2026
|
-
| `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
|
|
2027
|
-
| a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
|
|
2028
|
-
| `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
|
|
2029
|
-
| a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
|
|
2030
|
-
| `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
|
|
2031
|
-
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
|
|
2032
|
-
| `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
|
|
2033
|
-
|
|
2034
|
-
**A hand-rolled double trips `rxjs-x/finnish`, and a double from this package does not.** The rule
|
|
2035
|
-
reports any name whose type is an `Observable` **or whose call signature returns one**
|
|
2036
|
-
(`couldReturnObservable`). `vi.fn()` and `vi.spyOn()` hand back a callable mock carrying the
|
|
2037
|
-
signature of the method it stands for, so a double for a method returning an `Observable` reads to
|
|
2038
|
-
the rule as a stream and it asks for a `$` — on a spy, where the `$` would lie about what the
|
|
2039
|
-
variable holds. `functions: false` gives no relief: that option is about function _declarations_,
|
|
2040
|
-
not a variable holding one. A `Spy<T>` from `createSpyFromClass`, `provideAutoSpy` or
|
|
2041
|
-
`createAutoMock` is an ordinary object instead — the Observable sits on its members, not on the
|
|
2042
|
-
variable — so nothing is reported, and neither the name nor the assertion has to be bent. Measured
|
|
2043
|
-
on an Angular workspace of 1771 spec files: the rule named seven sites, every one a hand-rolled
|
|
2044
|
-
`vi.fn`/`vi.spyOn`, and none on a double built here.
|
|
2045
|
-
|
|
2046
|
-
The overrides bag is the one shape worth knowing in detail, because the repair is not a rename.
|
|
2047
|
-
Written inline, `provideAutoSpy(X, { overrides: { m: vi.fn(() => of(x)) } })` is invisible to the
|
|
2048
|
-
rule — an object literal inside a call is skipped. Hoisted into a `const` it is reported **per key**,
|
|
2049
|
-
and the key cannot be renamed: it is the method's name on `X`. What clears it is the annotation the
|
|
2050
|
-
bag should carry anyway, `const overrides: DeepPartial<X> = { … }` — the rule skips a literal whose
|
|
2051
|
-
variable is annotated, and the bag starts being checked against `X` where it is written rather than
|
|
2052
|
-
only at the call. For a spy whose name is genuinely free, `*Spy` plus a `names` exemption in the
|
|
2053
|
-
rule's own options says the same thing once for the whole repository.
|
|
2054
|
-
|
|
2055
|
-
**The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
|
|
2056
|
-
when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
|
|
2057
|
-
_configured_. They are indistinguishable until the test reassigns `x` — and the commonest reason to
|
|
2058
|
-
do that is a fresh `Subject` after the previous one has been `error()`ed or completed, which is
|
|
2059
|
-
exactly the case a suite is testing when it reassigns. The double then keeps handing out the dead
|
|
2060
|
-
one: in one spec the service received a completed subject and silently skipped the modal it was
|
|
2061
|
-
meant to show, with the test still green. Carry `vi.fn(() => x)` over as
|
|
2062
|
-
`mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
|
|
2063
|
-
anyone writing a codemod, because the rename looks like the safest edit in the file.
|
|
2064
|
-
|
|
2065
|
-
### Advice that circulates and is wrong on Vitest
|
|
2066
|
-
|
|
2067
|
-
Jest-era tutorials and cheat sheets repeat a handful of lines that fail on Vitest, or pass and leak.
|
|
2068
|
-
Each was checked on Vitest 5.0.0:
|
|
2069
|
-
|
|
2070
|
-
| Circulating advice | What happens | Write instead |
|
|
2071
|
-
| --- | --- | --- |
|
|
2072
|
-
| `const load = vi.fn(); vi.mock('./api', () => ({ load }))` | `vi.mock` is hoisted above every import and `const`: `ReferenceError: Cannot access 'load' before initialization`, wrapped in `[vitest] There was an error when mocking a module` | `const mocks = vi.hoisted(() => ({ load: vi.fn() }))`, then `() => ({ load: mocks.load })` |
|
|
2073
|
-
| "name it `mockLoad` and the factory may read it" | Jest's exemption for `mock`-prefixed names does not exist in Vitest — same `ReferenceError` | `vi.hoisted` |
|
|
2074
|
-
| `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
|
|
2075
|
-
| `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
|
|
2076
|
-
| `import { userEvent } from '@testing-library/user-event'` | the named export exists only from 14.5.0; the default export works on every 14.x, and every 14.x method returns a promise | `import userEvent from '@testing-library/user-event'`; `const user = userEvent.setup(); await user.click(el)` |
|
|
2077
|
-
| `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
|
|
2078
|
-
| `global.fetch = vi.fn()` in a test | survives `vi.restoreAllMocks()` and `vi.unstubAllGlobals()`, and answers every later test of the file | `mockValueProp(globalThis, 'fetch', …)` or `vi.stubGlobal` + `unstubGlobals: true`; `blockNetwork()` to stay offline; the `no-hand-assigned-global` rule reports the bare form |
|
|
2079
|
-
|
|
2080
|
-
---
|
|
2081
|
-
|
|
2082
|
-
## 19. Before you report success
|
|
2083
|
-
|
|
2084
|
-
Run what the project actually has — check its `package.json` first.
|
|
555
|
+
In [`agent-docs/anti-patterns.md`](./agent-docs/anti-patterns.md). Read it before writing a spec: the table of patterns to avoid with what to write instead, and advice that circulates and is wrong on Vitest.
|
|
2085
556
|
|
|
2086
|
-
|
|
2087
|
-
npx vitest run path/to/file.spec.ts # or: bun test path/to/file.test.ts
|
|
2088
|
-
npx tsc --noEmit # Spy<T> mistakes are compile errors, not runtime ones
|
|
2089
|
-
npx vitest-auto-spy doctor # suite-level defects that never fail a run
|
|
2090
|
-
```
|
|
2091
|
-
|
|
2092
|
-
**If you are an agent, add `--reporter=agent`** (Vitest 4.1). It is Vitest's own reporter, added for
|
|
2093
|
-
exactly this: the same failures, without the passing-test roll call and the repeated banners that
|
|
2094
|
-
make a run's output expensive to read and expensive to carry.
|
|
2095
|
-
|
|
2096
|
-
```bash
|
|
2097
|
-
npx vitest run --reporter=agent path/to/file.spec.ts
|
|
2098
|
-
```
|
|
557
|
+
<!-- agents-outline: agent-docs/anti-patterns.md -->
|
|
2099
558
|
|
|
2100
|
-
|
|
2101
|
-
suite that runs green but does not type-check is not done.
|
|
2102
|
-
|
|
2103
|
-
`doctor` is read-only and finds what a green run cannot: a `tsconfig` `include` pattern that
|
|
2104
|
-
matches no file (so it type-checks nothing while `tsc --noEmit` still reports success), a
|
|
2105
|
-
production module importing a `*.spec.ts`, a spec importing another spec, a `@jest-environment`
|
|
2106
|
-
pragma the runner never reads, configuration left behind for a runner that is gone, and a setup
|
|
2107
|
-
file an Angular or Nx unit-test target never runs because the target does not name it. Three of
|
|
2108
|
-
its checks are about coverage, where the run is green and the report is simply not the one the
|
|
2109
|
-
config describes — `coverage.all` on a Vitest that stopped reading the key, a source-only
|
|
2110
|
-
`coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
|
|
2111
|
-
recompiling it per file costs more than collecting the coverage does. Two are about the Angular
|
|
2112
|
-
builder's version: `@angular/build` in `[22.1.5, 22.1.7)` (`angular-build-splitting-off`), and an
|
|
2113
|
-
`@analogjs/vite-plugin-angular` below 2.7.5 next to `@angular/build` 22.2 or newer
|
|
2114
|
-
(`analog-behind-angular-build`), which dies at startup with `TypeError: cache.has is not a function`.
|
|
2115
|
-
Five are about Vitest 5. On Vitest 5, `vitest-5-removed` reports what it took away and the run now
|
|
2116
|
-
trips over — an import from `vitest/reporters`, `vitest/coverage`, `vitest/environments`,
|
|
2117
|
-
`vitest/snapshot`, `vitest/runners`, `vitest/suite` or `vitest/mocker`, a `.sequential` chain,
|
|
2118
|
-
`--outputJson`/`--compare` in a script — as errors; on Vitest 4 the same lines are upgrade notes.
|
|
2119
|
-
`poolOptions` on Vitest 4 or newer is a warning, because Vitest runs without every option inside it.
|
|
2120
|
-
`vitest-5-deprecated` names the two renamed programmatic calls nothing warns about. `vitest-5-clear-mocks`
|
|
2121
|
-
says `clearMocks: true` restates the Vitest 5 default, and tells a Vitest 4 suite that never set it
|
|
2122
|
-
to price the change with `npx vitest run --clearMocks` first. `vitest-5-available` tells a Vitest 4
|
|
2123
|
-
repository what the upgrade buys — or names what holds it back: `@angular/build` below 22.2.0 beside
|
|
2124
|
-
a unit-test target, Analog below 2.7.5, `vite` below 6.4, or Node below 22.12 in `.nvmrc`,
|
|
2125
|
-
`.node-version`, CI or a private package's `engines`. `fs-module-cache-not-persisted` warns when
|
|
2126
|
-
`fsModuleCache` is on and no CI config caches its directory. Renamed config keys Vitest 5 still
|
|
2127
|
-
honours (`experimental.fsModuleCache`, `browser.isolate`, `cache.dir`, …) are left to Vitest's own
|
|
2128
|
-
deprecation line.
|
|
2129
|
-
It is worth one run after any large edit to a test suite — especially after a codemod, which is where
|
|
2130
|
-
the eaten glob below came from.
|
|
2131
|
-
|
|
2132
|
-
Two of its checks are about this package's own names. `helper-from-wrong-entry` catches a named
|
|
2133
|
-
import taken from an entry that does not export it — `provideAutoSpy` from the root rather than
|
|
2134
|
-
`/angular` or `/nestjs`, `expectRequest` from anywhere but `/angular-http`. It reads both directions
|
|
2135
|
-
of the 5.21.0 Angular split — one of the thirty-two moved names still taken from `/angular`, and a
|
|
2136
|
-
core name taken from `/angular/diagnostics`, `/angular/doubles` or `/angular/matchers`, which export
|
|
2137
|
-
none of them — which makes it what fails that upgrade rather than a compiler: a repository green on
|
|
2138
|
-
5.20.0 exits 1 here with no test run, and the fix line names the companion to move to.
|
|
2139
|
-
`no-unawaited-helper`
|
|
2140
|
-
catches an `expectEmission` / `expectError` / `stable` / `flushEventLoop` called as a bare statement
|
|
2141
|
-
and dropped, so the promise settles after the test ended and its assertion reports into a later test
|
|
2142
|
-
or nowhere. Both resolve the name against a table **generated from this package's own `exports`
|
|
2143
|
-
map**, which is why they are `doctor` checks and not lint rules: a per-file linter has none. Both
|
|
2144
|
-
are conservative — the callee must have been imported from this package in that same file (a rename
|
|
2145
|
-
with `as` still counts), the call must both begin and end a statement, and the pair goes quiet when
|
|
2146
|
-
the installed major differs from the table's. Neither has a fixer; `doctor` still never writes.
|
|
2147
|
-
Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
2148
|
-
|
|
2149
|
-
**Three things about the CLI that decide whether its answer means anything:**
|
|
2150
|
-
|
|
2151
|
-
- **An unknown flag is refused, exit 2, nothing runs.** `init --dryrun` used to write the files and
|
|
2152
|
-
`perf --gat` used to pass with no gate at all — a typo in CI that read as a clean result. The two
|
|
2153
|
-
stderr lines name the flag and list what the command accepts; `--cwd`, `--help` and `--version`
|
|
2154
|
-
work everywhere.
|
|
2155
|
-
- **The scan does not descend into a nested repository or a git worktree.** A tree carrying
|
|
2156
|
-
worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
|
|
2157
|
-
`codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
|
|
2158
|
-
it is a directory (a nested clone) or a file (a worktree). Directories git ignores are skipped too
|
|
2159
|
-
— every `.gitignore` from the scan root down, `.git/info/exclude` and the per-user
|
|
2160
|
-
`core.excludesFile`, directories only, so an ignored file is still listed. Past 50 000 files the scan still
|
|
2161
|
-
truncates, and `doctor` reports that as a `scan-cap-reached` warning (exit 1) rather than a clean
|
|
2162
|
-
result; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
|
|
2163
|
-
- **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
|
|
2164
|
-
read is not a clean result. Absolute paths and `./`-style ones resolve against `--cwd`.
|
|
2165
|
-
|
|
2166
|
-
Exit codes, as the commands implement them: **0** is "ran, nothing to report"; **1** is "ran, and
|
|
2167
|
-
here is the finding" — `doctor` with an error, `init --check` with a stale block, `codemod` with any
|
|
2168
|
-
note, `perf --gate` over budget; **2** is "there was nothing to judge" — no command, an unknown
|
|
2169
|
-
command, an unknown flag, a flag value it cannot use (an `--min-severity` word it does not know, a
|
|
2170
|
-
number flag given a word or a negative number, a value flag with nothing after it, an `--ignore` id
|
|
2171
|
-
`doctor` has no check for), an unreadable `--only` / `--from` value, a path matching no file, and a
|
|
2172
|
-
`perf` run that measured nothing (including a red suite, which the gate will not judge at all). An
|
|
2173
|
-
invalid flag value stops the command before it does anything: the message says so (`Nothing ran.`),
|
|
2174
|
-
and a typo in a command, a flag, `--only` or `--ignore` gets a `Did you mean`.
|
|
2175
|
-
|
|
2176
|
-
**Reading the output from a script: `--format json`**, on `doctor` and on `perf`. One JSON document
|
|
2177
|
-
on stdout — `schema`, `exitCode`, `tally` (`errors`, `warnings`, `notes`), every finding with
|
|
2178
|
-
`check`, `severity`, `file`, `message`, `fix`; `perf` adds `run` (with `slowestFiles`, the `--top` slowest files per phase), `budgets` and `gate.verdicts` (one row
|
|
2179
|
-
per candidate, `outcome` one of `confirmed`, `not reproduced`, `unconfirmed`, `single reading`,
|
|
2180
|
-
`over budget`). Parse that rather than the text: the text is wrapped to the terminal (80 columns in a
|
|
2181
|
-
pipe), groups one cause found in many files into one block, and ends in a tally line that starts
|
|
2182
|
-
with `N errors, N warnings, N notes`. In the text, every finding ends in a `Docs:` line — its own
|
|
2183
|
-
section of the CLI page (`utilities/cli#<check-id>`), or of the codemod page for a codemod note. `--format markdown` renders the same document as tables — for a
|
|
2184
|
-
merge request note or a job summary, not for parsing.
|
|
2185
|
-
|
|
2186
|
-
**`doctor --ignore <check,…>`** leaves the named checks out of the report, the tally, the exit code
|
|
2187
|
-
and the `--code-quality` file. Use it only for a finding the repository has answered in a way
|
|
2188
|
-
`doctor` cannot see — `no-agent-instructions` in CI where the instruction files are kept out of git,
|
|
2189
|
-
`angular-build-splitting-off` under a patched builder. An id `doctor` has no check for is exit 2,
|
|
2190
|
-
with the closest id suggested. `--min-severity` hides findings from the text
|
|
2191
|
-
only; `--ignore` removes them.
|
|
2192
|
-
|
|
2193
|
-
### If you were asked why a suite is slow
|
|
559
|
+
- Advice that circulates and is wrong on Vitest
|
|
2194
560
|
|
|
2195
|
-
|
|
2196
|
-
npx vitest-auto-spy perf # runs the suite once and reports; exits 0 without --gate
|
|
2197
|
-
npx vitest-auto-spy perf src/some/dir # a path is passed through to Vitest as a file filter
|
|
2198
|
-
```
|
|
2199
|
-
|
|
2200
|
-
It reads Vitest's own per-file phase timings through `TestModule.diagnostic()` — nothing here
|
|
2201
|
-
parses terminal output — and reports six phases: `environment`, `import`, `tests`, `setup`,
|
|
2202
|
-
`prepare` (all measured per file) and `transform` (measured once for the whole run before Vitest 5,
|
|
2203
|
-
per file on 5, where it is the wait for Vite's transforms and `import`/`setup` are net of it). **The phase
|
|
2204
|
-
totals are CPU time summed across every worker, not wall clock** — a report showing `20.29s of CPU
|
|
2205
|
-
time` under `986ms wall clock` is not a bug, it is the work spread over workers.
|
|
2206
|
-
|
|
2207
|
-
When `environment` dominates, the `perf-environment` finding names spec files that could run under
|
|
2208
|
-
the `node` environment instead — but only the ones it could _prove_ reach no DOM: the spec, its
|
|
2209
|
-
configured setup files, and every repository module any of them imports were all read, none
|
|
2210
|
-
mentions a DOM name, and every package they import is on a short allowlist (`vitest`, `rxjs`,
|
|
2211
|
-
`date-fns`, `lodash`, `zod`, …). Anything it could not resolve is reported **undecided**, never
|
|
2212
|
-
assumed safe — do not treat an undecided file as a candidate, and do not add packages to that list
|
|
2213
|
-
yourself; a false positive breaks someone's suite on `document is not defined`. When `import`
|
|
2214
|
-
dominates, `perf-import` names specs that reach their subject through a barrel (`index`/`public-api`
|
|
2215
|
-
re-exporting a whole directory). When `environment` + `setup` + `prepare` together dominate,
|
|
2216
|
-
`perf-isolation` suggests `isolate: false` in the config it names, kept only if peak memory stays
|
|
2217
|
-
acceptable; its `Docs:` line leads to the check's section of the CLI page, which links this
|
|
2218
|
-
package's own memory measurements of that trade — read them before recommending the flag, since it
|
|
2219
|
-
raises peak memory.
|
|
2220
|
-
|
|
2221
|
-
Two more findings are about settings rather than files. `perf-environment-engine` fires when
|
|
2222
|
-
`environment` dominates and a `vite(st).config.*` names `jsdom` while nothing in those configs
|
|
2223
|
-
mentions `happy-dom`: measured on this package's own 117-file Angular suite, `happy-dom` is 23.2 s of
|
|
2224
|
-
user CPU against jsdom's 26.5 s, and on a spec that builds a DOM and does nothing else the gap is
|
|
2225
|
-
253 ms against 119 ms per file. It is a swap, not a flag — `happy-dom` implements less of the
|
|
2226
|
-
platform — so take one project at a time; the finding names the config that sets `jsdom`, and the
|
|
2227
|
-
numbers stay in the docs section. `perf-workers` fires on a run over a minute of summed CPU
|
|
2228
|
-
that declares no `maxWorkers`, and it is the one finding here about **memory**: one worker per core
|
|
2229
|
-
is the default, resident memory measured at 1.42 GB plus ~155 MB per worker, and a cap of four costs
|
|
2230
|
-
about 2.8 % of wall clock. The finding itself counts this machine's cores
|
|
2231
|
-
(`os.availableParallelism()`) and suggests half of them as `maxWorkers` in the config it names; the
|
|
2232
|
-
figures above are in the docs section, not in the message. Do not quote a worker count as
|
|
2233
|
-
universally right — it is a property of the machine.
|
|
2234
|
-
|
|
2235
|
-
**On Vitest 5, `perf` reads what Vitest 5 reports; an older Vitest prints exactly what it did before.**
|
|
2236
|
-
The advice reads the configuration Vitest resolved (`isolate`, `pool`, `maxWorkers`, `environment`,
|
|
2237
|
-
`fsModuleCache`) rather than the config text, and never advises against an option you set
|
|
2238
|
-
explicitly. `perf-transform` fires when the wait for Vite's transforms is 30 % or more of the CPU time
|
|
2239
|
-
with the module cache off, and advises `fsModuleCache: true` — kept between CI pipelines, or it only
|
|
2240
|
-
helps locally. `perf-long-pole` names the file still running alone after every other lane went idle,
|
|
2241
|
-
when that tail is at least 2 s and 30 % of the span. `perf-isolation` adds the workers spawned and
|
|
2242
|
-
their summed start-up, with an "at least" wall-clock saving; `perf-workers` counts the lanes the run
|
|
2243
|
-
used rather than the machine's cores; under `isolate: false`, `perf-heap` lists what each file added
|
|
2244
|
-
to its lane's heap. Environment time is counted once per lane (`concurrencyId`) and value — not per
|
|
2245
|
-
`workerId`, which Vitest 5 renews for every file even in a reused worker. A finding about a switch
|
|
2246
|
-
also prints `perf-vitest-doctor`: confirm it with `npx vitest doctor`, Vitest's own A/B runner, not
|
|
2247
|
-
this package's `doctor`. A bare run on Vitest 5 passes `--experimental.diagnostics=false` so Vitest's
|
|
2248
|
-
own hints do not repeat these. The reporter rewrites a `partial: true` report every ~2 s while the run
|
|
2249
|
-
goes, so a killed or timed-out run still leaves one: `perf` prints it under a warning and the gate
|
|
2250
|
-
refuses it like a red run.
|
|
2251
|
-
|
|
2252
|
-
**A bare `vitest run` is not every repository's suite, and `perf` refuses to pretend otherwise.**
|
|
2253
|
-
Where the suite is assembled by something else — an Angular builder, an Nx target, a script that
|
|
2254
|
-
generates a config per project — there is no root `vite(st).config.*`, the Vitest defaults sweep up
|
|
2255
|
-
every `*.spec.*` in the tree with no globals and no aliases, and every file fails to collect. The
|
|
2256
|
-
phase table that comes out of that is real seconds spent on nothing: measured on one such workspace,
|
|
2257
|
-
1 830 files, 29 s of wall clock and **zero** test bodies executed. `perf` checks for that shape
|
|
2258
|
-
before it runs anything, and refuses any report in which no body finished (exit 2). Measure the
|
|
2259
|
-
repository's own command instead:
|
|
561
|
+
<!-- /agents-outline -->
|
|
2260
562
|
|
|
2261
|
-
|
|
2262
|
-
npx vitest-auto-spy perf --command 'npm test' # measure that command
|
|
2263
|
-
npx vitest-auto-spy perf --command 'npm test -- {paths:--include=}' --gate
|
|
2264
|
-
```
|
|
563
|
+
---
|
|
2265
564
|
|
|
2266
|
-
|
|
2267
|
-
environment; the configuration it reaches attaches the reporter itself (`reporters: perf ===
|
|
2268
|
-
undefined ? ['default'] : ['default', perf]`), and the reporter writes nothing at all when the first
|
|
2269
|
-
variable is unset, so it can be declared permanently — by name, `vitest-auto-spy/perf-reporter`. `{paths}` / `{paths:<prefix>}` is where the
|
|
2270
|
-
files of a confirmation pass go.
|
|
2271
|
-
|
|
2272
|
-
**`--gate` is the only part of this command that fails anything**, and three rules keep it honest.
|
|
2273
|
-
It judges the `tests` phase alone, because the other five are the harness and the machine rather than
|
|
2274
|
-
anybody's code. A file budget is counted in the median test **of the same run** — the largest of
|
|
2275
|
-
`--max-file-tests` (2 000) median tests, `--factor` (10) × the median test for each test in the file,
|
|
2276
|
-
and a `--max-file-ms` (5 000) floor that can only spare a file — which is what makes the verdict the
|
|
2277
|
-
same on a loaded CI runner and on an idle laptop, and what keeps a large file of ordinary tests out of
|
|
2278
|
-
it: the rule it replaced, `--factor` × the median **file**, flagged 0 files of a 2 023-file consumer
|
|
2279
|
-
suite at ×1 slowdown and 19 at ×9, a 209-test service spec at 5 ms a test among them. A confirmed finding
|
|
2280
|
-
also says why: the confirmation pass records a CPU profile of each suspect file and every one of its
|
|
2281
|
-
test bodies, and the gate prints the slowest tests, the share in hooks against bodies, and where the
|
|
2282
|
-
time went in the spec, in your code and by package. The report ends in two tables of what is over
|
|
2283
|
-
budget and nothing else. And every candidate is re-measured on its own before it may fail anything: a
|
|
2284
|
-
file that is fast when it has the machine to itself is reported as _not reproduced_, an `info` rather
|
|
2285
|
-
than a finding. Without a way to re-measure, findings are warnings that fail nothing unless
|
|
2286
|
-
`--no-confirm` says one reading is enough. Exit `1` is "over budget", exit `2` is "there was nothing
|
|
2287
|
-
to judge" — including a red or unfinished (`partial`) suite, which the gate will not judge at all, since a failed test is
|
|
2288
|
-
measured until its timeout and 30 s of timeout looks exactly like 30 s of slow code.
|
|
2289
|
-
|
|
2290
|
-
`--json <path>` re-analyses a report an earlier `--out <path>` run wrote, instead of running Vitest
|
|
2291
|
-
again. Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
2292
|
-
|
|
2293
|
-
### Migrating a suite off `jest-auto-spies` — run the codemod, then verify it
|
|
565
|
+
## 19. Before you report success
|
|
2294
566
|
|
|
2295
|
-
|
|
2296
|
-
npx vitest-auto-spy codemod # dry run: prints the diff, writes nothing
|
|
2297
|
-
npx vitest-auto-spy codemod --write # apply
|
|
2298
|
-
npx vitest-auto-spy codemod --verify # exits 1 on anything the transforms should have removed
|
|
2299
|
-
```
|
|
567
|
+
In [`agent-docs/checklist.md`](./agent-docs/checklist.md). Read it before reporting a task done: what to run, and the checks for a slow suite, a `jest-auto-spies` migration and a codemod over specs.
|
|
2300
568
|
|
|
2301
|
-
|
|
2302
|
-
(it reads the installed package's export map) and it transposes `jest.Mock<R, [A]>` into the single
|
|
2303
|
-
call signature Vitest takes — a plain rename compiles into the **reverse** meaning and nothing fails
|
|
2304
|
-
until a call site disagrees. It leaves a `jest.*` member with no `vi` twin (`requireMock`,
|
|
2305
|
-
`replaceProperty`, `createMockFromModule`, `jest.setTimeout`, `requireActual`) exactly as it was and
|
|
2306
|
-
reports what to do instead, rather than guessing.
|
|
2307
|
-
|
|
2308
|
-
`--verify` matches the **result** against the patterns the codemod removes, so it also catches what
|
|
2309
|
-
the transforms declined to enter (a template literal, an unbalanced bracket) and a file somebody
|
|
2310
|
-
migrated by hand. Run it after `--write`, and again after any manual clean-up. `--only` / `--skip`
|
|
2311
|
-
select transforms by id, `--list` prints them. Full reference:
|
|
2312
|
-
<https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
|
|
2313
|
-
|
|
2314
|
-
**Every rewrite is parsed before it is written.** The result goes through the project's own
|
|
2315
|
-
`typescript` and its diagnostics are compared with the original's; a file the run would have broken
|
|
2316
|
-
is reported as `codemod-broke-syntax` and **left exactly as it was**, so the run exits 1 with one
|
|
2317
|
-
file named rather than a tree that no longer compiles. Where `typescript` is not installed the check
|
|
2318
|
-
is skipped silently — it is a safety net, not an install instruction.
|
|
2319
|
-
|
|
2320
|
-
It visits JavaScript specs too — `*.spec.js`, `*.test.jsx`, the `.cjs` / `.mjs` forms — because a
|
|
2321
|
-
Jest suite that was never TypeScript is the suite with the most `jest.` in it. Two things it now
|
|
2322
|
-
reports instead of rewriting into something wrong: `jest.fn<R, [A]>()` / `jest.spyOn<…>()` in a file
|
|
2323
|
-
that imports from `@jest/globals` (`jest-mock-type-arguments` — `jest-mock` 29 already takes the
|
|
2324
|
-
whole function type, so transposing a second time produces a return type of a return type), and
|
|
2325
|
-
`.withArgs(…)` on a `vi.spyOn` chain (`jasmine-with-args-on-spy-on` — `vi.spyOn` has no
|
|
2326
|
-
`calledWith`, and renaming it onto one produces a method that does not exist).
|
|
2327
|
-
|
|
2328
|
-
Past 50 000 files the repository scan truncates and the run says so — _Nothing left to migrate_ off a
|
|
2329
|
-
truncated list is a claim about a tree the tool never looked at. `VITEST_AUTO_SPY_SCAN_CAP` raises
|
|
2330
|
-
the cap.
|
|
2331
|
-
|
|
2332
|
-
### If you are writing a codemod over specs
|
|
2333
|
-
|
|
2334
|
-
Two traps, both found the hard way on rxjs-heavy code.
|
|
2335
|
-
|
|
2336
|
-
**`String.prototype.replace` interprets `$` in the replacement.** `$&`, `` $` ``, `$'` and `$n` are
|
|
2337
|
-
substitution patterns, and `$'` — "everything after the match" — is one character away from every
|
|
2338
|
-
observable name in the codebase. A replacement containing `reloadAndSeekTo$'`
|
|
2339
|
-
inserted the entire remainder of the file into itself and left an unterminated string; the only
|
|
2340
|
-
thing that caught it was ESLint's `Parsing error`. Pass a function, which is never interpreted:
|
|
569
|
+
<!-- agents-outline: agent-docs/checklist.md -->
|
|
2341
570
|
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
571
|
+
- If you were asked why a suite is slow
|
|
572
|
+
- Migrating a suite off `jest-auto-spies` — run the codemod, then verify it
|
|
573
|
+
- If you are writing a codemod over specs
|
|
2345
574
|
|
|
2346
|
-
|
|
2347
|
-
silently eats the `// eslint-disable-next-line` above the node. Use `node.getFullStart()`, or count
|
|
2348
|
-
the comments before and after and compare against `HEAD`.
|
|
575
|
+
<!-- /agents-outline -->
|
|
2349
576
|
|
|
2350
577
|
---
|
|
2351
578
|
|