vitest-auto-spy 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +456 -246
  2. package/README.md +488 -165
  3. package/dist/angular.d.ts +61 -5
  4. package/dist/angular.js +1746 -102
  5. package/dist/bun-angular.d.ts +4 -4
  6. package/dist/bun-angular.js +196 -12
  7. package/dist/bun.d.ts +122 -15
  8. package/dist/bun.js +6 -9
  9. package/dist/{chunk-ISSGEVLO.js → chunk-2U3J65A4.js} +17 -76
  10. package/dist/{chunk-WT75WGQZ.js → chunk-3AWKPZCE.js} +154 -18
  11. package/dist/{chunk-DNHLQG45.js → chunk-B7PDIHEG.js} +92 -25
  12. package/dist/chunk-J5O72UHX.js +6 -0
  13. package/dist/chunk-LVLSMH6T.js +49 -0
  14. package/dist/{chunk-OS2QFTIF.js → chunk-MG5EAMZR.js} +3 -2
  15. package/dist/{chunk-QGBNXDKU.js → chunk-R4EWAG7Y.js} +51 -16
  16. package/dist/{chunk-OWQR2YVQ.js → chunk-Y4YG7UOK.js} +10 -11
  17. package/dist/cli.js +1502 -207
  18. package/dist/console.d.ts +1 -1
  19. package/dist/console.js +4 -8
  20. package/dist/eslint-plugin.cjs +279 -90
  21. package/dist/eslint-plugin.d.cts +16 -0
  22. package/dist/eslint-plugin.d.ts +16 -0
  23. package/dist/eslint-plugin.js +279 -90
  24. package/dist/{expect-emission-Cpj2GGcD.d.ts → expect-emission-CuGjE0YR.d.ts} +2 -2
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +1922 -10
  27. package/dist/nestjs.d.ts +2 -1
  28. package/dist/nestjs.js +6 -6
  29. package/dist/node.cjs +304 -121
  30. package/dist/node.d.ts +3 -3
  31. package/dist/node.js +17 -19
  32. package/dist/{prop-mock-CeHqAeIG.d.ts → prop-mock-IgCwv_40.d.ts} +1 -1
  33. package/dist/react.d.ts +3 -3
  34. package/dist/react.js +7 -11
  35. package/dist/rxjs.d.ts +2 -2
  36. package/dist/rxjs.js +2 -4
  37. package/dist/setup.d.ts +66 -4
  38. package/dist/setup.js +112 -17
  39. package/dist/shared-state.js +447 -0
  40. package/dist/svelte.d.ts +3 -3
  41. package/dist/svelte.js +7 -11
  42. package/dist/track-injections-BAzV5ITq.d.ts +56 -0
  43. package/dist/{types-W3lPrwC7.d.ts → types-it6CnDkI.d.ts} +150 -18
  44. package/dist/vue.d.ts +4 -4
  45. package/dist/vue.js +8 -11
  46. package/dist/{zoneless-Ch2Ym_6z.d.ts → zoneless-BUNSBSIP.d.ts} +16 -10
  47. package/package.json +3 -2
  48. package/skills/vitest-auto-spy/SKILL.md +85 -50
  49. package/dist/chunk-2PFOBMTZ.js +0 -200
  50. package/dist/chunk-DA5E36HQ.js +0 -81
  51. package/dist/chunk-DMSELR3S.js +0 -9
  52. package/dist/chunk-TMO2UFLD.js +0 -200
  53. package/dist/chunk-TNB3Y3GI.js +0 -27
  54. package/dist/chunk-VR5GJRBS.js +0 -24
package/AGENTS.md CHANGED
@@ -12,11 +12,11 @@ node_modules/vitest-auto-spy/AGENTS.md
12
12
 
13
13
  Working on the library's own source instead? Read `CONTRIBUTING.md` in the repository.
14
14
 
15
- Setting this up for a team? The README section
16
- [Using this library with an AI agent](https://github.com/ASDAlexey/vitest-auto-spy#using-this-library-with-an-ai-agent)
17
- names the instruction file each agent reads `AGENTS.md` for OpenAI Codex, Cursor, Copilot and most
18
- of the field, `CLAUDE.md` for Claude Code and for GLM (z.ai) or Kimi running inside it, `GEMINI.md`
19
- for Gemini CLI — and gives the two commands that install this pointer for all of them.
15
+ Setting this up for a team? `npx vitest-auto-spy init` writes a pointer to this file into the
16
+ instruction files this repository's agents actually read — `AGENTS.md` (Codex, Cursor, Copilot and
17
+ most of the field), `CLAUDE.md` (Claude Code) and `GEMINI.md` (Gemini CLI), plus the glob-scoped
18
+ rule file of any tool whose own directory already exists. `--check` is the CI form. Full table:
19
+ <https://asdalexey.github.io/vitest-auto-spy/agents>.
20
20
 
21
21
  | Resource | Where |
22
22
  | ----------------------- | -------------------------------------------------------------------- |
@@ -49,13 +49,14 @@ adapter installed and spies fail at runtime.
49
49
  | Vue / Pinia | `vitest-auto-spy/vue` |
50
50
  | Svelte | `vitest-auto-spy/svelte` |
51
51
 
52
- Three add-ons, orthogonal to the runner:
52
+ Four add-ons, orthogonal to the runner:
53
53
 
54
54
  | Add-on | Import | Needed for |
55
55
  | ---------------- | ------------------------------- | --------------------------------------------------- |
56
56
  | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
57
57
  | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
58
58
  | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
59
+ | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
59
60
 
60
61
  The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
61
62
  CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
@@ -89,17 +90,16 @@ Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
89
90
  and the question that decides it is **how the double travels**. Through DI, it arrives as `Spy<T>`
90
91
  and is only ever asserted on — `createAutoMock`. Handed to the function under test as an argument
91
92
  (`detectVpnClient(url, logger)`, `applyPreferredTracks(target, …)`, `setLocalConfigEnabled(storage, …)`),
92
- it has to satisfy `T` at the call site *and* expose the spy helpers at the assertion, and
93
+ it has to satisfy `T` at the call site _and_ expose the spy helpers at the assertion, and
93
94
  `autoMocked<T>()` is that intersection — otherwise every call site needs an `asInstance()` and the
94
95
  noise scales with the number of them.
95
96
 
96
-
97
97
  `createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
98
98
  `no-type-assertion` lint rule without an `eslint-disable` on every fixture.
99
99
 
100
100
  **`mockDeep` builds depth on property access, not on calls** — the distinction the tree now spells
101
101
  out, and the one that costs an afternoon otherwise. `mock.repo.user.find()` chains because every hop
102
- but the last is a *read*. A node that is **called** returns what it was configured to return, and by
102
+ but the last is a _read_. A node that is **called** returns what it was configured to return, and by
103
103
  default that is `undefined`, so `mockDeep<AppLogger>().channel('app').info('x')` is a `TypeError` at
104
104
  the second call — while `DeepMockProxy<AppLogger>` types it perfectly, so nothing warns. Pass
105
105
  `{ selfReturning: true }` for a fluent API, or use `createAutoMock<T>()` with
@@ -113,7 +113,7 @@ expect(logger.channel('app').info).toHaveBeenCalledWith('started');
113
113
  ```
114
114
 
115
115
  Both bridges exist, and which one you need depends on the direction. What a self-returning **call**
116
- hands back is typed as the *declared* return type, not as a spy — `asSpy<T>(…)` when the helpers are
116
+ hands back is typed as the _declared_ return type, not as a spy — `asSpy<T>(…)` when the helpers are
117
117
  needed. The **whole mock** is a `DeepMockProxy<T>`, which is not assignable to `T` for the same
118
118
  reason `Spy<T>` is not (a mapped type cannot see private members) — `asInstance(…)` when it has to
119
119
  go somewhere typed against the real thing:
@@ -147,7 +147,7 @@ by default would silence a real typo on every concrete class.
147
147
  **`overrides: { key: undefined }` is a seed, not an omission**, and the difference is load-bearing.
148
148
  `createAutoMock` reads its seed with `Reflect.ownKeys`, so a key written out with an explicit
149
149
  `undefined` **is** in the store: reading it answers `undefined`. Leave it out and the same read
150
- materialises a *function spy* — which is truthy, and sends `if (this.lastFocus)` down the branch the
150
+ materialises a _function spy_ — which is truthy, and sends `if (this.lastFocus)` down the branch the
151
151
  spec was trying to close:
152
152
 
153
153
  ```ts
@@ -162,15 +162,15 @@ redundant.
162
162
 
163
163
  `createAutoMock` and `mockDeep` build a Proxy, not an object, and there is one place where the
164
164
  difference shows: a Proxy answers only the operations its handler traps. Three of them used to be
165
- missing, and each produced a *silent* wrong answer rather than an error — the worst failure mode
165
+ missing, and each produced a _silent_ wrong answer rather than an error — the worst failure mode
166
166
  this library can have, because a checking test becomes a non-checking one and only the proxy's
167
167
  source says so. Two are fixed; the third cannot be:
168
168
 
169
- | Operation | Before 3.5.0 | Now |
170
- | ------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------ |
171
- | `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
172
- | `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
173
- | `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
169
+ | Operation | Before 3.5.0 | Now |
170
+ | --------------------------------- | ------------------------------------------------- | -------------------------------------------------------- |
171
+ | `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
172
+ | `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
173
+ | `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
174
174
 
175
175
  `ownKeys` cannot be completed: a type has no key list at runtime, which is the whole premise of
176
176
  these two factories. So a spec that installs a double by **copying it onto a real instance** —
@@ -179,7 +179,7 @@ every other call goes to the real implementation, silently. Use `createSpyFromCl
179
179
  returns an ordinary object whose method keys are enumerable (lazy accessors, but enumerable), so
180
180
  the copy is complete.
181
181
 
182
- ### It answers everything, so it must not answer *these*
182
+ ### It answers everything, so it must not answer _these_
183
183
 
184
184
  The same premise cuts the other way. A library that is handed an object and has to decide **what
185
185
  kind of thing it is** asks by probing a key — and a double that answers every property answers the
@@ -187,12 +187,12 @@ probe too, at which point it stops being a double of `T` and becomes whatever wa
187
187
  Four names are therefore answered with `undefined` unless the spec seeds them, alongside `then` and
188
188
  every symbol, which always were:
189
189
 
190
- | Key | Probed by | The double became |
191
- | -------------- | --------------------------------------------- | ------------------- |
192
- | `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
193
- | `lift` | `isObservable`, with `subscribe` | an Observable |
194
- | `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
195
- | `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
190
+ | Key | Probed by | The double became |
191
+ | -------------- | --------------------------------------------- | ----------------- |
192
+ | `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
193
+ | `lift` | `isObservable`, with `subscribe` | an Observable |
194
+ | `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
195
+ | `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
196
196
 
197
197
  The one that cost an afternoon reads like nothing at all:
198
198
 
@@ -209,8 +209,8 @@ was an assertion about an unrelated `emit()` three concerns away — nothing in
209
209
  **`subscribe` is deliberately not on that list.** It is an ordinary method name — a store, an
210
210
  Angular `OutputEmitterRef`, an event bus — and `expect(store.subscribe).toHaveBeenCalledWith(cb)` is
211
211
  a real assertion. Denying `lift` and `@@observable` already breaks the impersonation, so `subscribe`
212
- on its own fools nothing: `from(double)` now fails with rxjs's own *"You provided an invalid object
213
- where a stream was expected"*, loudly and in the right file.
212
+ on its own fools nothing: `from(double)` now fails with rxjs's own _"You provided an invalid object
213
+ where a stream was expected"_, loudly and in the right file.
214
214
 
215
215
  If your type genuinely has one of the four, say so once and it comes back — the list is consulted
216
216
  after the seed store:
@@ -225,7 +225,7 @@ file. A key is only added to that list with an observed mechanic behind it — n
225
225
  sounds protocol-ish — because every entry costs somebody the ability to mock a member of that name
226
226
  without seeding it.
227
227
 
228
- The tree asks whether the double is *called*, and there is a second question worth asking: whether
228
+ The tree asks whether the double is _called_, and there is a second question worth asking: whether
229
229
  the code under test **writes to it**. `createAutoMock` is a proxy with a `set` trap over the same
230
230
  cache its `get` trap answers from, so an assignment sticks and is read back — which makes it the
231
231
  double for a DOM-ish object a library drives by assigning handlers, where a hand-written fake is
@@ -242,15 +242,12 @@ xhr.send.mockImplementation(() => respond(asInstance(xhr)));
242
242
 
243
243
  ### Cost, so it stops being a question
244
244
 
245
- Building a spy is not a thing to optimise. Measured on a ten-method class:
246
- `provideAutoSpy` ~8 µs, `createSpyFromClass` ~29 µs, `createAutoMock` ~33 µs, a `calledWith`
247
- lookup ~0.7 µs. Five providers across two thousand tests is under a tenth of a second for the
248
- whole run. Call the factory in `beforeEach` and look at `TestBed` instead that is where a slow
249
- spec spends its time.
250
-
251
- `provideAutoSpy` is the fastest of the three because it defaults to `lazySpies: true`. The two
252
- settings that do cost: `{ lazySpies: false }` gives that up, and `autoSpyAccessors: true` walks
253
- the prototype chain uncached on every call — name the accessors instead.
245
+ Building a spy is not a thing to optimise. On a ten-method class: `provideAutoSpy` ~8 µs,
246
+ `createSpyFromClass` ~29 µs, `createAutoMock` ~33 µs, a `calledWith` lookup ~0.7 µs five providers
247
+ across two thousand tests is under a tenth of a second. Call the factory in `beforeEach` and look at
248
+ `TestBed` instead. The only two settings that cost: `{ lazySpies: false }` gives up the laziness
249
+ `provideAutoSpy` defaults to, and `autoSpyAccessors: true` walks the prototype chain uncached on
250
+ every call — name the accessors instead.
254
251
 
255
252
  ---
256
253
 
@@ -443,6 +440,8 @@ createSpyFromClass(MyService, {
443
440
  | `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
444
441
  | `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
445
442
  | `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. |
443
+ | `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
444
+ | `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
446
445
 
447
446
  **`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
448
447
  the reference suite). Method discovery walks the _prototype chain_; a callable assigned to an
@@ -471,7 +470,7 @@ There is no better message to be had at runtime, and it is worth saying why rath
471
470
  looking like an oversight. Instance fields do not exist until a constructor has run, and this
472
471
  library never constructs the class — that is what makes a spy safe to build from a service whose
473
472
  constructor talks to the network. The only alternative would be to answer an unknown member with
474
- *something*, and that something would be truthy: `if (service.optionalThing)` in the code under test
473
+ _something_, and that something would be truthy: `if (service.optionalThing)` in the code under test
475
474
  would then take the wrong branch, silently, which is the exact failure mode the protocol deny-list
476
475
  in §2 exists to remove. A loud `TypeError` on the spec's own line is the better of the two.
477
476
 
@@ -498,7 +497,7 @@ Also true, and worth not re-deriving:
498
497
  - **An overloaded method is not collapsed.** The worry that `Spy<T>` types every generated
499
498
  `api-mgw` client against its last signature does not hold: a four-overload
500
499
  `MgwContentsService.getMoviesBySlug` types as it should, and hand-written `{ m: vi.fn() }` doubles
501
- for those services convert with no changes to the assertions. When the *first* signature is the
500
+ for those services convert with no changes to the assertions. When the _first_ signature is the
502
501
  useful one, name it on the **declaration only** — the factory's result assigns to it, so the type
503
502
  argument is not written twice:
504
503
 
@@ -517,6 +516,38 @@ class prototype, so nothing is being shadowed), and
517
516
  `service.isKidMode.mockReturnValue(false)` reads like every other member. `mockSignalProp` is the
518
517
  other answer when the value has to change during the test.
519
518
 
519
+ ### `strict` — a method nobody configured throws instead of answering `undefined`
520
+
521
+ ```ts
522
+ const users = createSpyFromClass(UserService, { strict: true });
523
+
524
+ users.load.resolveWith([]);
525
+ users.currentTenant(); // throws here, not four frames later inside the component
526
+ ```
527
+
528
+ ```
529
+ [vitest-auto-spy] Nothing configured Cart.checkout, and strict mode is on.
530
+ Called as: Cart.checkout(1,'now')
531
+ ```
532
+
533
+ Also on `createAutoMock`, `provideAutoSpy`, and suite-wide as `setupAutoSpy({ strict: true })`.
534
+ Precedence, first one set wins: the double's `onUnstubbedCall` → the global `onUnstubbedCall` → the
535
+ double's `strict` (**including `strict: false`**, the only way to exempt one double from a suite-wide
536
+ default) → the global `strict`.
537
+
538
+ - **It is not argument-level.** A `calledWith(1, 'now')` chain says the method is stubbed, so
539
+ `checkout(9, 'later')` still answers `undefined`. Use `mustBeCalledWith` for that.
540
+ - **`mockReturnValue` / `mockImplementation` / the `returns:` option replace the dispatch**, so they
541
+ never reach the guard. Consequence: after the last `mockReturnValueOnce` the queue empties back
542
+ onto the library dispatch and the next call **is** reported as unstubbed — seed
543
+ `mockReturnValue(...)` too when a `Once` sequence is meant to run out.
544
+ - **It does not reach** accessor spies, observable-property spies, `mockDeep` nodes, `console-spy`,
545
+ `mockResourceProp`'s `reload` or a standalone `createFunctionSpy`. A strict double still answers
546
+ `undefined` for an unconfigured getter or `items$`. `fillMissing` members **are** covered.
547
+ - Use `onUnstubbedCall` to survey a suite before turning the throw on — it is handed
548
+ `{ className, method, args }` and whatever it returns is what the call answers, so a handler that
549
+ pushes to an array records the gap without failing anything.
550
+
520
551
  ### Getters and setters live in `accessorSpies`
521
552
 
522
553
  ```ts
@@ -536,7 +567,7 @@ Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs:
536
567
  `accessorSpies.setters.theme` was `undefined`, and the failure read
537
568
  `Cannot read properties of undefined` three steps from the configuration behind it.
538
569
 
539
- Only spy a getter when the spec asserts that it was **read**. To make one *answer* something, on a
570
+ Only spy a getter when the spec asserts that it was **read**. To make one _answer_ something, on a
540
571
  spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
541
572
  factory, which is the part that is otherwise found by trial:
542
573
 
@@ -608,6 +639,24 @@ resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mo
608
639
  Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
609
640
  proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
610
641
 
642
+ Every double also carries `[Symbol.dispose]()` — it runs `resetAutoSpy(this)` — so an `afterEach`
643
+ that exists only to reset one spy can be deleted:
644
+
645
+ ```ts
646
+ it('loads', () => {
647
+ using cart = createSpyFromClass(Cart); // reset when the block ends
648
+
649
+ cart.total.calledWith().mockReturnValue(42);
650
+ expect(cart.total()).toBe(42);
651
+ });
652
+ ```
653
+
654
+ `createAutoMock` proxies and **every `mockDeep` node** carry it (so `using` on a sub-tree resets that
655
+ sub-tree). The key is non-enumerable, so a spread does not copy it, and there is no
656
+ `[Symbol.asyncDispose]`. A standalone `createFunctionSpy` is **not** covered: Vitest's own
657
+ `[Symbol.dispose]` on a host mock restores the original implementation instead — call
658
+ `resetAutoSpy(spy)` there. If the project does not transpile `using`, call `spy[Symbol.dispose]()`.
659
+
611
660
  ---
612
661
 
613
662
  ## 8. Observable assertions (core entry — no rxjs needed)
@@ -638,7 +687,7 @@ with rxjs's `EmptyError`, and the workaround people arrive at,
638
687
  part. Emissions do not fail it: it asserts termination, nothing about what came before.
639
688
 
640
689
  **To assert that production code pushed into a stream, do not use `observablePropsToSpyOn`.** That
641
- option points the other way: it gives the spec `nextWith` so it can *feed* the double. When the
690
+ option points the other way: it gives the spec `nextWith` so it can _feed_ the double. When the
642
691
  question is whether the code under test called `next` on a property, the double needs a real
643
692
  `Subject` and a spy on its method:
644
693
 
@@ -656,9 +705,9 @@ expect(next).toHaveBeenCalledWith(1000);
656
705
  the type either way — which is exactly why this is worth saying: the code compiles against the spy
657
706
  surface and asserts nothing.
658
707
 
659
- **When the error *is* the assertion, use `expectError`.** The other helpers wrap a stream failure in
708
+ **When the error _is_ the assertion, use `expectError`.** The other helpers wrap a stream failure in
660
709
  a new `Error` whose message names the stream — right for reporting an unexpected failure, useless
661
- when the failure is the subject. `expectError` resolves *with* the error, exactly as it was thrown:
710
+ when the failure is the subject. `expectError` resolves _with_ the error, exactly as it was thrown:
662
711
 
663
712
  ```ts
664
713
  await expect(expectError(service.load())).resolves.toBe(originalError);
@@ -684,7 +733,7 @@ they keep the diagnosis: emissions that do not match are still counted, so a fai
684
733
  fired".
685
734
 
686
735
  **`advance` closes the window between subscribing and awaiting.** A stream driven by a
687
- `debounceTime`, a retry or a poll needs the clock moved *after* something is listening, and `await`
736
+ `debounceTime`, a retry or a poll needs the clock moved _after_ something is listening, and `await`
688
737
  gives control away before the next statement runs:
689
738
 
690
739
  ```ts
@@ -699,7 +748,7 @@ runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`.
699
748
  **The watchdog runs on real time, on purpose — even under fake timers.** A virtual one would race
700
749
  the timers the spec advances: `expectEmission(source$, { timeout: 200 })` followed by
701
750
  `vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the spec was
702
- about to advance into. The cost is that in a suite with global fake timers a *failing* assertion
751
+ about to advance into. The cost is that in a suite with global fake timers a _failing_ assertion
703
752
  spends a real second. Do **not** answer that with `{ timeout: 0 }` at every call site — that
704
753
  disables the watchdog, and the next silent stream hangs to the runner's own timeout with nothing
705
754
  useful in the message. Lower the default once instead:
@@ -715,7 +764,7 @@ setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
715
764
 
716
765
  **`expectEmission` subscribes when you call it, not when you await it**, and that is load-bearing
717
766
  rather than an implementation detail. It is what converts the test whose source has to be poked
718
- *after* somebody is listening — a router event, a `Subject` the spec pushes into, anything that
767
+ _after_ somebody is listening — a router event, a `Subject` the spec pushes into, anything that
719
768
  does not replay:
720
769
 
721
770
  ```ts
@@ -769,12 +818,12 @@ mockValueProp(spy, 'products$', new Subject()); // a member the double does not
769
818
  ```
770
819
 
771
820
  The last one is worth knowing on its own: patching a key the object never had **works and is undone
772
- correctly** — the journal records the *absence* of a descriptor and puts it back by deleting the
821
+ correctly** — the journal records the _absence_ of a descriptor and puts it back by deleting the
773
822
  property. That is how you add an Observable member that `provideAutoSpy` did not create because
774
823
  `observablePropsToSpyOn` was not passed.
775
824
 
776
825
  What the second overload costs is the property-name check, so a typo in the name compiles. Nothing
777
- checks the *value* on either overload; that is deliberate, and the partial fixture above is why.
826
+ checks the _value_ on either overload; that is deliberate, and the partial fixture above is why.
778
827
 
779
828
  ### Properties of DOM objects — the same helpers, and the reason to look for them
780
829
 
@@ -817,7 +866,7 @@ check that fails the run, and (opt-in) `vi.restoreAllMocks()`. Turn on `restoreM
817
866
  suite runs with `isolate: false`.
818
867
 
819
868
  **The restore also runs from an `onTestFinished` net**, because the `afterEach` is not guaranteed
820
- to. Vitest calls `afterEach` hooks in *reverse* registration order, so the setup file's is the last
869
+ to. Vitest calls `afterEach` hooks in _reverse_ registration order, so the setup file's is the last
821
870
  one, and a hook the spec file registered takes the chain down with it when it throws — the patches
822
871
  then travel into the next test and the failure surfaces somewhere that never touched them. One spec
823
872
  kept `afterEach(() => vi.restoreAllMocks())`; migrating it to `gettersToSpyOn` made the restored
@@ -878,15 +927,13 @@ component trees.
878
927
  setupAutoSpy({ pruneMockRegistry: true }); // keep only the mocks that outlive a file
879
928
  ```
880
929
 
881
- The part to understand before turning it on is what must **not** be pruned. Dropping a mock from that
882
- set means `clearMocks` can no longer see it, so its calls accumulate silently — harmless for a mock
883
- that dies with its file, a bug for the module-level `vi.fn()` in a shared `*.mock.ts` that six spec
884
- files import. The first file to import it creates it; drop it when that file ends and the file that
885
- happens to run **second** fails on calls its predecessor made, which reads as flakiness because which
886
- file is first is the runner's choice. So the split is drawn where it is observable: what is already in
887
- the registry when a file's hooks start was created while the module graph was being evaluated and is
888
- kept; everything added after that belongs to the file and goes when it ends. One case lands on the
889
- wrong side — a module first loaded by a dynamic `import()` inside a test — and says so explicitly:
930
+ The part to understand before turning it on is what must **not** be pruned. A pruned mock is one
931
+ `clearMocks` can no longer see, so its calls accumulate silently — harmless for a mock that dies with
932
+ its file, a bug for the module-level `vi.fn()` in a shared `*.mock.ts` that six spec files import.
933
+ The split is therefore drawn where it is observable: what is already in the registry when a file's
934
+ hooks start was created while the module graph was evaluated and is kept; everything added after that
935
+ belongs to the file and goes when it ends. One case lands on the wrong side a module first loaded
936
+ by a dynamic `import()` inside a test and says so explicitly:
890
937
 
891
938
  ```ts
892
939
  export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
@@ -901,25 +948,21 @@ Two more switches, both about the environment rather than the spies:
901
948
  setupAutoSpy({ blockNetwork: true }); // fetch rejects, XHR fails, sendBeacon answers false
902
949
  ```
903
950
 
904
- Under happy-dom, which unlike jsdom implements `fetch`: a component that pulls a remote asset
905
- really fetches it, nothing asserts on the response so the tests pass, and the aborts at teardown
906
- fail the run with **no test named**. If a green run exits 1 with `DOMException [AbortError]`, this
907
- is it.
908
-
909
- Under jsdom too, for the other half. jsdom implements `XMLHttpRequest` in full, and plenty of
910
- libraries never left it `rmp-vast` pings every VAST tracker through a hand-rolled one
911
- (`FW.ajax`) — so a suite with `blockNetwork: true` already on was still reaching the internet, one
912
- ping per quartile per ad per test, and printing jsdom's `AggregateError at Object.dispatchError`
913
- for every connection that failed. What a green run prints then depends on whether the machine has a
914
- route out.
951
+ Both DOMs leak, through different holes. happy-dom implements `fetch`, so a component pulling a
952
+ remote asset really fetches it, nothing asserts on the response, and the aborts at teardown fail the
953
+ run with **no test named** a green run exiting 1 with `DOMException [AbortError]`. jsdom
954
+ implements `XMLHttpRequest` in full, and the libraries that never left XHR (a VAST player pinging
955
+ every tracker through a hand-rolled one) reach the internet once per ping per test, printing jsdom's
956
+ `AggregateError at Object.dispatchError` for each connection that failed so what a green run
957
+ prints depends on whether the machine has a route out.
915
958
 
916
959
  Every channel is closed by default; the object is for narrowing it:
917
960
 
918
- | option | default | what it does |
919
- | -------- | ---------- | ---------------------------------------------------------------------------------- |
920
- | `fetch` | `true` | `fetch` rejects, naming what was requested |
961
+ | option | default | what it does |
962
+ | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
963
+ | `fetch` | `true` | `fetch` rejects, naming what was requested |
921
964
  | `xhr` | `'reject'` | `'reject'` fails the request (`status` 0, an `error` event); `'empty'` answers 200 with an empty body; `false` leaves XHR alone |
922
- | `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one |
965
+ | `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one |
923
966
 
924
967
  `'reject'` is the default because it is what `fetch` does: the code takes its failure branch, which
925
968
  is the branch a unit test should be asserting on. `'empty'` is for a request whose response nobody
@@ -933,10 +976,10 @@ setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setti
933
976
  A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
934
977
  serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
935
978
  and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
936
- resolves it against the document origin, so a spec that reaches `/config` and passes is resting on
937
- nothing listening on that port. `WebSocket` and `EventSource` are left alone: their failure is an
979
+ resolves it against the document origin, so a spec that reaches `/config`and passes is resting on
980
+ nothing listening on that port.`WebSocket`and`EventSource`are left alone: their failure is an
938
981
  event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
939
- a behaviour change — `stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
982
+ a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
940
983
 
941
984
  `restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
942
985
  fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
@@ -944,6 +987,50 @@ the realm, not owned by `globalThis`), and with `isolate: false` the next file d
944
987
  own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
945
988
  the file in the stack is not the cause.
946
989
 
990
+ `hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
991
+ CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
992
+ `test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
993
+ own and defaults it to 10 000 ms. Carry a preset's `testTimeout: 30000` into the runner config and
994
+ stop there, and every hook in the suite silently runs on a third of what its tests get. Worse, the
995
+ failure is filed against the wrong thing: a `beforeEach` timeout is attributed to the **test**, with
996
+ the test's duration pinned at the limit, so the report reads `× should create 10045ms` and sends the
997
+ reader looking for ten seconds of work inside a body that never ran. The hint appends both numbers
998
+ and the field to set. It says nothing when the budgets agree — then the hook really is slow — and
999
+ nothing for `beforeEach(fn, 300)`, which chose its own limit. `beforeAll` is out of reach by
1000
+ construction: its timeout is reported as a failed _suite_, every test is skipped and no `afterEach`
1001
+ runs.
1002
+
1003
+ **When migrating a runner config off Jest, set both timeouts side by side**, and treat the single
1004
+ Jest number as belonging to both fields:
1005
+
1006
+ ```ts
1007
+ test: {
1008
+ testTimeout: 30_000,
1009
+ // Jest had one budget for both; Vitest defaults this to 10_000 on its own.
1010
+ hookTimeout: 30_000,
1011
+ }
1012
+ ```
1013
+
1014
+ `frozenClockHint` is the same seam aimed at the other half of a timeout. A frozen clock turns
1015
+ waiting into waiting forever — `await new Promise(r => setTimeout(r, 10))` never resolves unless
1016
+ something advances it — and the runner's own advice ("pass a timeout value as the last argument") is
1017
+ the one repair that cannot work: the callback is not late, it is never scheduled to run. Under
1018
+ `globalFakeTimers` nothing in the spec says the clock is fake at all, so the timeout lands in a file
1019
+ that never mentions a timer. The hint reports `vi.isFakeTimers()` and `vi.getTimerCount()` — facts,
1020
+ not a guess — and stays silent when the clock is real or its queue is empty.
1021
+
1022
+ The shape that reaches this with no timer in sight is an HTTP spec. `setImmediate` is among the
1023
+ globals `vi.useFakeTimers()` replaces by default, and Express ends a request that matched no route
1024
+ through `finalhandler`, which schedules on `setImmediate`. So the 404 is never written and the test
1025
+ dies on its timeout: in such a file, "the test hung" means _the route did not match_, not "the
1026
+ server is slow". One thing this cannot see through — a spec whose own `afterEach` calls
1027
+ `vi.useRealTimers()`, because hooks run in reverse registration order and the clock is real again by
1028
+ the time the hint reads it. Nothing is reported then, rather than something wrong.
1029
+
1030
+ One more field of the same family differs quietly and changes only the report:
1031
+ `slowTestThreshold` is `5` in Jest (**seconds**) and `300` in Vitest (**milliseconds**), so a
1032
+ migrated suite starts marking most of its files slow. That is a unit change, not a regression.
1033
+
947
1034
  Fake timers:
948
1035
 
949
1036
  ```ts
@@ -1082,13 +1169,13 @@ first and the spec's hook operates on an already-restored environment.
1082
1169
  Under Jest these were hard to tell apart; under Vitest with a real bundler they are four separate
1083
1170
  mechanisms, and a test that waits on the wrong one fails with a message that names none of them.
1084
1171
 
1085
- | What is pending | What drives it | What does **not** |
1086
- | --------------------------------------------- | -------------------------------------------------- | ----------------------------------------- |
1087
- | change detection | `fixture.detectChanges()` | anything `await`ed |
1088
- | effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
1089
- | timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
1090
- | a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
1091
- | an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
1172
+ | What is pending | What drives it | What does **not** |
1173
+ | ------------------------------------------------- | -------------------------------------------------- | ----------------------------------------- |
1174
+ | change detection | `fixture.detectChanges()` | anything `await`ed |
1175
+ | effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
1176
+ | timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
1177
+ | a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
1178
+ | an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
1092
1179
 
1093
1180
  ```ts
1094
1181
  import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
@@ -1249,7 +1336,7 @@ the values, and under `isolate: false` that is one set of spies shared by every
1249
1336
  it.
1250
1337
 
1251
1338
  **`observablePropsToSpyOn` works on a token too**, and matters more there than on a class. A class
1252
- tells the factory which members are methods; a type does not, so *every* unnamed key of a
1339
+ tells the factory which members are methods; a type does not, so _every_ unnamed key of a
1253
1340
  token-driven double is a function spy — including an `Observable` property, which the code under
1254
1341
  test then subscribes to as if it were a function, failing far from the double:
1255
1342
 
@@ -1414,6 +1501,14 @@ The failure that is real: `overrideProvider` only reaches a component the TestBe
1414
1501
  about, so a standalone component instantiated through a parent's template needs to be in `imports`
1415
1502
  first; `overrideComponentProvider` queues it.
1416
1503
 
1504
+ **`overrideComponentProvider` verifies itself**, always — not behind a diagnostics flag. On the next
1505
+ `TestBed.createComponent` it asks the component's **own** injector for the token and throws when the
1506
+ answer is not the spy it created, naming the component, the token and what was resolved instead. It
1507
+ checks the **first** fixture only and stays silent when the component was not rendered (behind an
1508
+ `@if`, a different host), so a throwaway fixture created first means "not checked", not "wrong". A
1509
+ later `TestBed.overrideProvider(Token, …)` still wins; the check reports that but cannot prevent it.
1510
+ `overrideAutoSpy` carries no verification of its own.
1511
+
1417
1512
  Do **not** reach for `TestBed.overrideComponent` here — see the next subsection for why it is worse
1418
1513
  than the problem it solves.
1419
1514
 
@@ -1444,6 +1539,63 @@ assertNgModuleScopes(DirectivesModule, PipesModule); // throws, naming the modul
1444
1539
  Then declare what the spec needs in the TestBed module directly. Pass only modules you expect to
1445
1540
  bring declarations — a providers-only module is legitimately empty.
1446
1541
 
1542
+ ### Four silent failures, as one setup line
1543
+
1544
+ ```ts
1545
+ // vitest.setup.ts — AFTER getTestBed().initTestEnvironment(…), because Vitest runs
1546
+ // afterEach hooks in reverse registration order and this one must run before the teardown.
1547
+ import { enableAngularDiagnostics } from 'vitest-auto-spy/angular';
1548
+
1549
+ enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders, pendingRequests }
1550
+ ```
1551
+
1552
+ | Member | Fails when |
1553
+ | ------------------ | -------------------------------------------------------------------------------- |
1554
+ | `ngModuleScopes` | a testing module imports an NgModule that contributes nothing at runtime |
1555
+ | `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
1556
+ | `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
1557
+ | `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one |
1558
+
1559
+ Every member defaults to `true`, takes `false` to opt out, and a second call **replaces** the
1560
+ selection (safe from anywhere, including inside a test). `disableAngularDiagnostics()` turns the
1561
+ group off and leaves the TestBed timing instrumentation alone. `assertNoPendingRequests()` is the
1562
+ HTTP check on its own, for use mid-test — reading takes the requests, so the group will not
1563
+ re-report what you inspected.
1564
+
1565
+ Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
1566
+ `provideHttpClientTesting()` / `HttpClientTestingModule`, and a project using neither is inert.
1567
+ `ngModuleScopes` only fires on a module that contributes **nothing at all** — a providers-only module
1568
+ is legitimately scope-empty, so a stripped scope that still has providers passes silently; hand-call
1569
+ `assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
1570
+ not fire when `declarations` is non-empty.
1571
+
1572
+ ### Which collaborators the code asked for — `trackInjections`
1573
+
1574
+ Do **not** reach for `vi.mock('@app/services')` to answer "was this collaborator used". Register the
1575
+ collaborators as provider factories and read back which ones DI constructed; a factory runs exactly
1576
+ when something injects its token, and DI is a seam the bundler cannot remove.
1577
+
1578
+ ```ts
1579
+ import { trackInjections } from 'vitest-auto-spy/angular';
1580
+
1581
+ // the same function on /nestjs
1582
+
1583
+ const collaborators = trackInjections([FeatureFlagService, ANALYTICS_TOKEN]);
1584
+
1585
+ TestBed.configureTestingModule({ providers: [CheckoutFacade, ...collaborators.providers] });
1586
+ collaborators.get(FeatureFlagService).isOn.mockReturnValue(true);
1587
+
1588
+ TestBed.inject(CheckoutFacade).start();
1589
+
1590
+ expect(collaborators.names()).toEqual(['FeatureFlagService']); // analytics was never asked for
1591
+ ```
1592
+
1593
+ `providers`, `injectedTokens()` (in factory-run order), `names()`, `wasInjected(token)`,
1594
+ `get<D>(token)` → `Spy<D>`, `reset()` (the record only — the doubles survive). A class token gets a
1595
+ class spy, anything else a `createAutoMock()`; pass `{ double: () => … }` when a collaborator has to
1596
+ be a real object. Doubles are built eagerly, so stub before the entry point runs; a factory runs once
1597
+ per injector, so a token appears once per injector, not once per injection site.
1598
+
1447
1599
  ### Never mock `@angular/core` to control an `effect()`
1448
1600
 
1449
1601
  Under the Angular unit-test builder the specs are bundled and `@angular/core` sits in a shared
@@ -1608,7 +1760,7 @@ spec — after which `Spy<Mock>` and `Spy<Interface>` disagree and somebody cast
1608
1760
  `provideAutoSpy`: the latter reads a class prototype, which a token does not have.
1609
1761
 
1610
1762
  **The second argument is not optional as often as it looks.** A spy answers `undefined` until it is
1611
- told otherwise, and that is fatal the moment the code under test *chains* off it — a constructor
1763
+ told otherwise, and that is fatal the moment the code under test _chains_ off it — a constructor
1612
1764
  doing `inject(LOGGER).channel('auth').debug('…')` dies on the `.debug` of `undefined` before the
1613
1765
  spec's first line runs, because nothing in production wrote `?.` there. Seed the link:
1614
1766
 
@@ -1719,40 +1871,73 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
1719
1871
  export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
1720
1872
  ```
1721
1873
 
1722
- | Rule | Level | Fix | Flags |
1723
- | ------------------------------ | ------- | --------- | ------------------------------------------------------------------------ |
1724
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
1725
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
1726
- | `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
1727
- | `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
1728
- | `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
1729
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
1730
- | `no-mocked-for-spy` | `warn` | `--fix` | `Mocked<T>` in any type position → `Spy<T>`, import and all |
1731
- | `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
1732
- | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion |
1733
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
1734
- | `no-overridden-provider` | `error` | | two providers for one token in one array → the earlier one never runs |
1735
- | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
1736
-
1737
- Twelve rules; two fix on their own, three offer suggestions. `no-mocked-for-spy` only ever touches a
1874
+ | Rule | Level | Fix | Flags |
1875
+ | ------------------------------ | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
1876
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
1877
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
1878
+ | `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
1879
+ | `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
1880
+ | `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
1881
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
1882
+ | `no-mocked-for-spy` | `warn` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
1883
+ | `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
1884
+ | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion |
1885
+ | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
1886
+ | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
1887
+ | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
1888
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
1889
+
1890
+ Thirteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
1738
1891
  **type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
1739
1892
  meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
1740
1893
  the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
1741
- helper's parameter, and `as unknown as Mocked<T>`, which in one batch stood next to the declaration
1742
- in all eight reports fix one and leave the other and the file says both. It declines where it
1743
- cannot prove the rename (a `Mocked` the file declares itself, a `Spy` that is already something
1744
- else, `Mocked<{ a: Mock }>` rather than a named type) and reports without a fix.
1745
-
1746
- `prefer-as-spy` is the same licence from the other end, and the one a migration meets in bulk: a
1747
- `jest-auto-spies` suite writes `TestBed.inject(X) as Spy<X>` once per injected double, and that cast
1748
- fails here with `TS2352` `Spy<T>` adds `accessorSpies` and the per-method helpers, so neither type
1749
- sufficiently overlaps the other. `asSpy` is a typed identity function, so `--fix` keeps the
1750
- assertion the developer already made, carries the type arguments across (inference answers
1751
- `Spy<Service<any>>` for a generic class), adds the `asSpy` import and removes a `Spy` import the
1752
- rewrite orphans. A cast that hops through `unknown` is left alone the hop says the value is not a
1753
- `T` except after `TestBed.inject(X)`, where the container returns `X` by construction and the hop
1754
- was only silencing `TS2352`. Neither rule is for the object under test: a service the spec
1755
- exercises is not a double, and typing it as the class is the repair there.
1894
+ helper's parameter, `as unknown as Mocked<T>`. It declines where it cannot prove the rename (a
1895
+ `Mocked` the file declares itself, a `Spy` that is already something else, `Mocked<{ a: Mock }>`
1896
+ rather than a named type) and reports without a fix.
1897
+
1898
+ **The autofix is narrower than that licence.** A declaration is decidable; what the name is
1899
+ _assigned_ two lines below is not `--fix` once renamed a declaration to `Spy<T>` and left an object
1900
+ literal beneath it that the new type rejects, so `eslint --fix` reported clean and `tsc` failed
1901
+ afterwards. The plain fix now survives only where the value came out of `createSpyFromClass`,
1902
+ `createAutoMock`, `createMock`, `mockDeep`, `injectSpy`, `asSpy` and friends, or where the annotation
1903
+ belongs to no variable (a parameter, a return type, an `as` expression). Everywhere else the same
1904
+ edit is a **suggestion** accept it together with the repair at the creation site, usually
1905
+ `createAutoMock<T>()` in place of the literal. **After any `--fix` run over specs, run
1906
+ `npx tsc --noEmit`**: a lint pass reporting clean is not evidence that the types still hold.
1907
+
1908
+ `no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
1909
+ **last** provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
1910
+ auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out
1911
+ (found on eight tokens of one file). It reads both spellings in either order and compares tokens as
1912
+ source text. A **verbatim duplicate** was already being ignored by Angular, so it carries a
1913
+ suggestion to delete the dead copy (a suggestion, never `--fix`: unattended deletions inside a
1914
+ `providers` array are not something to find in a diff). When
1915
+ the survivor is the **barer** of the two — `provideAutoSpy(X, { gettersToSpyOn: […] })` above a bare
1916
+ `provideAutoSpy(X)` — there is no edit, because which one to keep is the question; move the
1917
+ configuration onto the survivor or delete it. Both messages name the token and the surviving
1918
+ provider's line. A `multi: true` registration is exempt on both sides:
1919
+ Angular accumulates multi providers rather than keeping the last, so two of them for one token is
1920
+ the feature — a spec asserting that two `BEFORE_INIT` hooks run in registration order needs both.
1921
+ Multi mixed with plain is still reported, because Angular refuses that pair at runtime
1922
+ (`Cannot mix multi providers and regular providers`). For the same reason `prefer-provide-auto-spy`
1923
+ says nothing about a multi provider: `provideAutoSpy` takes no registration mode, so the replacement
1924
+ it would ask for does not exist.
1925
+
1926
+ `no-import-time-spread` is the one that fails **while the bundle loads**, on a tree whose every test
1927
+ passes: `export const events = [...BaseEvents]` at module scope raises
1928
+ `Spread syntax requires ...iterable[Symbol.iterator] to be a function` when a shared chunk is
1929
+ evaluated while the binding it re-exports is still `undefined`. Safe under `tsc` and under a
1930
+ browser's ESM loader, which is why nothing else catches it. A function body and an instance field are
1931
+ not reported (they run later); a `static` field is.
1932
+
1933
+ `prefer-as-spy` is the one a migration meets in bulk: a `jest-auto-spies` suite writes
1934
+ `TestBed.inject(X) as Spy<X>` once per injected double, and that cast fails here with `TS2352`.
1935
+ `asSpy` is a typed identity function, so `--fix` keeps the assertion, carries the type arguments
1936
+ across (inference answers `Spy<Service<any>>` for a generic class) and repairs the imports. A cast
1937
+ that hops through `unknown` is left alone — the hop says the value is not a `T` — except after
1938
+ `TestBed.inject(X)`, where the container returns `X` by construction and the hop was only silencing
1939
+ `TS2352`. Neither rule is for the object under test: a service the spec exercises is not a double,
1940
+ and typing it as the class is the repair there.
1756
1941
 
1757
1942
  `no-expect-in-subscribe` reports one shape and **three different edits**, and says which: the
1758
1943
  subscription is the last thing the test does (invert it into `await firstValueFrom`); something
@@ -1762,13 +1947,15 @@ the trigger, `await p` — because inverting deadlocks); or the assertion is in
1762
1947
  callback reaches through a helper it calls, which used to make `subscribe((d) => assertShape(d))`
1763
1948
  invisible. `prefer-provide-auto-spy` reads `useFactory` as well as `useValue`, through the function
1764
1949
  in the first case and not in the second — a factory's body is what DI ends up holding, while a
1765
- function inside a `useValue` is a lazily-built double, i.e. the fix. The other three change behaviour — whether `injectSpy(X)`
1766
- finds a spy is decided by a `provideAutoSpy(X)` usually written in another file, `mockValueProp`
1767
- leaves the property writable and configurable, and `no-expect-in-subscribe` rewrites a whole test
1768
- so all three are suggestions an editor offers and a human accepts:
1950
+ function inside a `useValue` is a lazily-built double, i.e. the fix. Three rules change behaviour
1951
+ when applied — whether `injectSpy(X)` finds a spy is decided by a `provideAutoSpy(X)` usually
1952
+ written in another file, `mockValueProp` leaves the property writable and configurable, and
1953
+ `no-expect-in-subscribe` rewrites a whole test so `prefer-inject-spy`,
1954
+ `no-object-define-property` and `no-expect-in-subscribe` only ever suggest:
1769
1955
 
1770
1956
  ```ts
1771
- it('maps the products', () => // ❌ flagged, and a suggestion is offered
1957
+ it('maps the products', () =>
1958
+ // ❌ flagged, and a suggestion is offered
1772
1959
  new Promise<void>((done) => {
1773
1960
  service.getProducts(id).subscribe((products) => {
1774
1961
  expect(products).toEqual(expected);
@@ -1776,7 +1963,8 @@ it('maps the products', () => // ❌ flagged, and a sugge
1776
1963
  });
1777
1964
  }));
1778
1965
 
1779
- it('maps the products', async () => { // ✅ what accepting it produces
1966
+ it('maps the products', async () => {
1967
+ // ✅ what accepting it produces
1780
1968
  const products = await firstValueFrom(service.getProducts(id));
1781
1969
 
1782
1970
  expect(products).toEqual(expected);
@@ -1791,9 +1979,9 @@ rather than one message per `expect`, which used to double the apparent size of
1791
1979
  `prefer-inject-spy` reads both spellings of the same mistake, which is the point of the second one:
1792
1980
 
1793
1981
  ```ts
1794
- vi.spyOn(TestBed.inject(DomainEventsService), 'announce'); // flagged, always was
1982
+ vi.spyOn(TestBed.inject(DomainEventsService), 'announce'); // flagged, always was
1795
1983
  const domainEvents = TestBed.inject(DomainEventsService);
1796
- const announceSpy = vi.spyOn(domainEvents, 'announce'); // flagged now
1984
+ const announceSpy = vi.spyOn(domainEvents, 'announce'); // flagged now
1797
1985
  ```
1798
1986
 
1799
1987
  The variable is resolved through the scope manager, so it has to be a `const`/`let` initialised
@@ -1807,13 +1995,6 @@ written above it inside a `createComponent` helper. Configure the double after t
1807
1995
  (`injectSpy(X)` in the test), or keep the access lazy: `const api = () => injectSpy(Api)`. The check
1808
1996
  is order-free by design, since a helper declared above the hook still runs after it.
1809
1997
 
1810
- `no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
1811
- last provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
1812
- auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out.
1813
- Found on eight tokens of one file. It reads both spellings in either order, compares tokens as
1814
- source text, and offers no fix: deleting either line is a valid repair and they mean opposite
1815
- things.
1816
-
1817
1998
  The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
1818
1999
  packages, which a subpath export can never be.
1819
2000
 
@@ -1821,135 +2002,143 @@ packages, which a subpath export can never be.
1821
2002
 
1822
2003
  ## 17. Error → fix
1823
2004
 
1824
- | Message contains | Cause | Fix |
1825
- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
1826
- | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
1827
- | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
1828
- | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
1829
- | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
1830
- | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
1831
- | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
1832
- | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
1833
- | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
1834
- | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
1835
- | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
1836
- | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
1837
- | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
1838
- | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it the specs are bundled. Assert the result instead |
1839
- | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings 10) |
1840
- | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
1841
- | `signal read during notification phase` | same a stray `requestAnimationFrame` callback | same |
1842
- | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
1843
- | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
1844
- | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
1845
- | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
1846
- | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
1847
- | `runEffect(): not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
1848
- | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass`12) |
1849
- | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
1850
- | `TS2352: 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` never a double assertion (§6) |
1851
- | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
1852
- | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>`6) |
1853
- | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
1854
- | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
1855
- | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent provide the double through the TestBed instead |
1856
- | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
1857
- | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
1858
- | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
1859
- | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
1860
- | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
1861
- | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
1862
- | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
1863
- | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
1864
- | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
1865
- | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` it fails loudly instead |
1866
- | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
1867
- | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import(''))` — await the module, do not count turns |
1868
- | a template error in a `describe` that never patched anything | a spec `afterEach` threw and skipped `setupAutoSpy`'s, so a `mock*Prop` patch travelled | upgrade — an `onTestFinished` net restores it and names the cause (§10) |
1869
- | `Property 'mockReturnValue' does not exist on type 'never'` | a generic method with a conditional return type; the spy collapsed | upgrade fixed in the types; the member now keeps its sync helper bundle |
1870
- | `Type 'string' is not assignable to type 'never'` on `gettersToSpyOn` | the list used to reject callable values, i.e. every `Signal<T>` | upgrade any string key is nameable; prefer `mockSignalProp` for a signal |
1871
- | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
1872
- | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>()`, or `Overload<M, 0>` |
1873
- | a spy that cannot take a real `signal()` in `mockReadonlyProp` | the value was typed against `Spy<T>[K]`, not against `T[K]` | upgrade the `mock*Prop` helpers accept a `Spy<T>` and check against `T` |
1874
- | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
1875
- | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` on a class **getter**, `mockReadonlyProp` |
1876
- | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
1877
- | a stream built with `of(double)` that never emits | the double answered `schedule`, so rxjs ate it as the scheduler | upgrade — `schedule` / `lift` / `@@observable` / `getReader` are answered `undefined` (§2) |
1878
- | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
1879
- | `nextWith` emitting nothing, on a spy an earlier test completed or errored | `error()`/`complete()` close a Subject for good | upgrade the next configuration starts a new stream |
1880
- | `accessorSpies.setters.X` is `undefined` after `gettersToSpyOn: ['X']` | only the named half was spied | upgrade the declared other half is mirrored (§5) |
1881
- | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic, so it keeps the `readonly` of an abstract getter | `Mutable<Spy<T>>` for direct assignment, or `mockValueProp` for a patch that is undone |
1882
- | a `mock*Prop` patch on a `createAutoMock` double that changes nothing | the Proxy had no `defineProperty` trap before 3.5.0 | upgrade nothing to change in the spec |
1883
- | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
1884
- | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` a real object, with enumerable method keys |
1885
- | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
1886
- | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
1887
- | a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
1888
- | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)`11) |
2005
+ | Message contains | Cause | Fix |
2006
+ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
2007
+ | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
2008
+ | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
2009
+ | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2010
+ | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2011
+ | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2012
+ | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
2013
+ | `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this give the code a real seam (inject it, pass it in) and spy on that |
2014
+ | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2015
+ | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2016
+ | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
2017
+ | `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })``schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost`13) |
2018
+ | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
2019
+ | `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
2020
+ | `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter `no-import-time-spread`16) |
2021
+ | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
2022
+ | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
2023
+ | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
2024
+ | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
2025
+ | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
2026
+ | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
2027
+ | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2028
+ | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it the specs are bundled. Assert the result instead |
2029
+ | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings 10) |
2030
+ | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
2031
+ | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2032
+ | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in 10) |
2033
+ | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion`16) |
2034
+ | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
2035
+ | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
2036
+ | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
2037
+ | `runEffect(): not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
2038
+ | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
2039
+ | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
2040
+ | `TS2352: 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` never a double assertion (§6) |
2041
+ | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
2042
+ | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
2043
+ | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>()` / `injectSpy<Service>()` (§6) |
2044
+ | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
2045
+ | `vi.requireMock is not a function` | a mechanical `jest.` `vi.` rename | there is no equivalent provide the double through the TestBed instead |
2046
+ | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
2047
+ | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
2048
+ | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ }))` |
2049
+ | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
2050
+ | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
2051
+ | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
2052
+ | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
2053
+ | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) },]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
2054
+ | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
2055
+ | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
2056
+ | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
2057
+ | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import(''))` — await the module, do not count turns |
2058
+ | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
2059
+ | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>()`, or `Overload<M, 0>` |
2060
+ | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
2061
+ | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` on a class **getter**, `mockReadonlyProp` |
2062
+ | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
2063
+ | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
2064
+ | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic, so it keeps the `readonly` of an abstract getter | `Mutable<Spy<T>>` for direct assignment, or `mockValueProp` for a patch that is undone |
2065
+ | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
2066
+ | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
2067
+ | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
2068
+ | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
2069
+ | a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory 10) |
2070
+ | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
2071
+
2072
+ **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
2073
+ nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
2074
+ `gettersToSpyOn: ['X']`; a `mock*Prop` on a `createAutoMock` double changing nothing;
2075
+ `gettersToSpyOn` rejecting a `Signal` key; `mockReadonlyProp` rejecting a real `signal()`;
2076
+ `mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
2077
+ never patched anything.
1889
2078
 
1890
2079
  ---
1891
2080
 
1892
2081
  ## 18. Do not write this
1893
2082
 
1894
- | ❌ | ✅ |
1895
- | -------------------------------------------------------------------- | ------------------------------------------------------------- |
1896
- | `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
1897
- | `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
1898
- | `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
1899
- | `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
1900
- | `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
1901
- | `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
1902
- | `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
1903
- | `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
1904
- | `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
1905
- | `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
1906
- | `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
1907
- | `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
1908
- | `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
1909
- | `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
1910
- | hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
1911
- | `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
1912
- | a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
1913
- | a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
1914
- | `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
1915
- | `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
1916
- | `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
1917
- | `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
1918
- | `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
1919
- | `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
1920
- | a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
1921
- | an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
1922
- | `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
1923
- | `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
1924
- | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
1925
- | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
1926
- | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
1927
- | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
1928
- | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
1929
- | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
1930
- | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
1931
- | `await fixture.whenRenderingDone()` | `await stable(fixture)` |
1932
- | an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
1933
- | `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
1934
- | `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
1935
- | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
1936
- | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
1937
- | `configureTestingModule` inside every `it()` | one per `describe` |
1938
- | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
1939
- | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console` |
1940
- | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
1941
- | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
1942
-
2083
+ | ❌ | ✅ |
2084
+ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
2085
+ | `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
2086
+ | `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
2087
+ | `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
2088
+ | `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
2089
+ | `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
2090
+ | `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
2091
+ | `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
2092
+ | `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
2093
+ | `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
2094
+ | `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
2095
+ | `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
2096
+ | `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
2097
+ | `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
2098
+ | `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
2099
+ | hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
2100
+ | `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
2101
+ | a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
2102
+ | a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
2103
+ | `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
2104
+ | `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
2105
+ | `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
2106
+ | `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
2107
+ | `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
2108
+ | `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
2109
+ | a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
2110
+ | an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
2111
+ | `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
2112
+ | `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
2113
+ | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
2114
+ | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
2115
+ | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
2116
+ | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
2117
+ | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
2118
+ | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
2119
+ | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
2120
+ | `await fixture.whenRenderingDone()` | `await stable(fixture)` |
2121
+ | an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
2122
+ | `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
2123
+ | `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
2124
+ | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
2125
+ | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
2126
+ | `configureTestingModule` inside every `it()` | one per `describe` |
2127
+ | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
2128
+ | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console` |
2129
+ | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
2130
+ | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
1943
2131
 
1944
2132
  **The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
1945
- when the double is *called*; `mockReturnValue(x)` freezes the value `x` had when the double was
1946
- *configured*. They are indistinguishable until the test reassigns `x` — and the commonest reason to
2133
+ when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
2134
+ _configured_. They are indistinguishable until the test reassigns `x` — and the commonest reason to
1947
2135
  do that is a fresh `Subject` after the previous one has been `error()`ed or completed, which is
1948
2136
  exactly the case a suite is testing when it reassigns. The double then keeps handing out the dead
1949
2137
  one: in one spec the service received a completed subject and silently skipped the modal it was
1950
2138
  meant to show, with the test still green. Carry `vi.fn(() => x)` over as
1951
2139
  `mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
1952
2140
  anyone writing a codemod, because the rename looks like the safest edit in the file.
2141
+
1953
2142
  ---
1954
2143
 
1955
2144
  ## 19. Before you report success
@@ -1973,6 +2162,27 @@ worth one run after any large edit to a test suite — especially after a codemo
1973
2162
  the eaten glob below came from. Full reference:
1974
2163
  <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
1975
2164
 
2165
+ ### Migrating a suite off `jest-auto-spies` — run the codemod, then verify it
2166
+
2167
+ ```bash
2168
+ npx vitest-auto-spy codemod # dry run: prints the diff, writes nothing
2169
+ npx vitest-auto-spy codemod --write # apply
2170
+ npx vitest-auto-spy codemod --verify # exits 1 on anything the transforms should have removed
2171
+ ```
2172
+
2173
+ Do not hand-edit a suite of migrated imports; the codemod knows which entry point exports each name
2174
+ (it reads the installed package's export map) and it transposes `jest.Mock<R, [A]>` into the single
2175
+ call signature Vitest takes — a plain rename compiles into the **reverse** meaning and nothing fails
2176
+ until a call site disagrees. It leaves a `jest.*` member with no `vi` twin (`requireMock`,
2177
+ `replaceProperty`, `createMockFromModule`, `jest.setTimeout`, `requireActual`) exactly as it was and
2178
+ reports what to do instead, rather than guessing.
2179
+
2180
+ `--verify` matches the **result** against the patterns the codemod removes, so it also catches what
2181
+ the transforms declined to enter (a template literal, an unbalanced bracket) and a file somebody
2182
+ migrated by hand. Run it after `--write`, and again after any manual clean-up. `--only` / `--skip`
2183
+ select transforms by id, `--list` prints them. Full reference:
2184
+ <https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
2185
+
1976
2186
  ### If you are writing a codemod over specs
1977
2187
 
1978
2188
  Two traps, both found the hard way on rxjs-heavy code.