vitest-auto-spy 5.19.0 → 5.20.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 (52) hide show
  1. package/AGENTS.md +135 -14
  2. package/README.md +67 -18
  3. package/dist/angular-router.d.ts +1 -1
  4. package/dist/angular-router.js +1 -1
  5. package/dist/angular.d.ts +6 -6
  6. package/dist/angular.js +43 -10
  7. package/dist/bun-angular.d.ts +6 -6
  8. package/dist/bun-angular.js +7 -7
  9. package/dist/bun.d.ts +88 -12
  10. package/dist/bun.js +3 -3
  11. package/dist/{chunk-3LU77DBV.js → chunk-2KBFEQOV.js} +6 -4
  12. package/dist/{chunk-HMECO5PK.js → chunk-4SJY7ZHM.js} +22 -2
  13. package/dist/{chunk-4JTRYOL2.js → chunk-IIZTARIO.js} +1 -1
  14. package/dist/{chunk-LBQWJZPS.js → chunk-LE6IB3DK.js} +1 -1
  15. package/dist/{chunk-F3OQDSKX.js → chunk-QB5VKPTI.js} +263 -20
  16. package/dist/{chunk-2ERHG4NE.js → chunk-SXR7EAUS.js} +42 -8
  17. package/dist/cli.js +881 -614
  18. package/dist/console.d.ts +1 -1
  19. package/dist/console.js +3 -3
  20. package/dist/dom-stubs.d.ts +1 -1
  21. package/dist/dom-stubs.js +5 -3
  22. package/dist/eslint-plugin.cjs +176 -107
  23. package/dist/{expect-emission-QuF55ViS.d.ts → expect-emission-CjwVMb2W.d.ts} +1 -1
  24. package/dist/index.d.ts +5 -5
  25. package/dist/index.js +326 -27
  26. package/dist/jasmine-compat.d.ts +1 -1
  27. package/dist/jasmine.d.ts +2 -2
  28. package/dist/jasmine.js +4 -4
  29. package/dist/nestjs.d.ts +2 -2
  30. package/dist/nestjs.js +6 -6
  31. package/dist/node.cjs +321 -23
  32. package/dist/node.d.cts +1 -0
  33. package/dist/node.d.ts +5 -5
  34. package/dist/node.js +321 -24
  35. package/dist/{package-identity-C2L4fM0k.d.ts → package-identity-Cn7yncmJ.d.ts} +1 -1
  36. package/dist/{prop-mock-DMqE-ldi.d.ts → prop-mock-DMEpvaAU.d.ts} +1 -1
  37. package/dist/react.d.ts +5 -5
  38. package/dist/react.js +326 -27
  39. package/dist/rstest.d.ts +5 -5
  40. package/dist/rstest.js +4 -4
  41. package/dist/rxjs.d.ts +2 -2
  42. package/dist/setup.d.ts +30 -4
  43. package/dist/setup.js +47 -5
  44. package/dist/svelte.d.ts +5 -5
  45. package/dist/svelte.js +326 -27
  46. package/dist/{track-injections-BfNI9fHy.d.ts → track-injections-Dig5t28T.d.ts} +1 -1
  47. package/dist/{track-signal-runs-C5nd4A4_.d.ts → track-signal-runs-CErdcgTA.d.ts} +13 -4
  48. package/dist/{types-B7Wjo00D.d.ts → types-BM3BcWj1.d.ts} +25 -1
  49. package/dist/vue.d.ts +6 -6
  50. package/dist/vue.js +326 -27
  51. package/package.json +1 -1
  52. package/skills/vitest-auto-spy/SKILL.md +127 -96
package/AGENTS.md CHANGED
@@ -18,14 +18,15 @@ most of the field), `CLAUDE.md` (Claude Code) and `GEMINI.md` (Gemini CLI), plus
18
18
  rule file of any tool whose own directory already exists. `--check` is the CI form. Full table:
19
19
  <https://asdalexey.github.io/vitest-auto-spy/agents>.
20
20
 
21
- | Resource | Where |
22
- | ----------------------- | -------------------------------------------------------------------- |
23
- | Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
24
- | Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
25
- | Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
26
- | Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
27
- | Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
28
- | Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
21
+ | Resource | Where |
22
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
24
+ | Task recipes | <https://asdalexey.github.io/vitest-auto-spy/guides/mocking-classes> · `/guides/mocking-local-storage` · `/guides/mocking-prisma` · `/guides/storybook-angular` |
25
+ | Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
26
+ | Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
27
+ | Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
28
+ | Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
29
+ | Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
29
30
 
30
31
  **Read `dist/*.d.ts` before inventing a call.** Every export is typed and documented there, and the
31
32
  type is the authority when this file and the code disagree.
@@ -62,13 +63,18 @@ Add-ons, orthogonal to the runner:
62
63
  | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
63
64
  | Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
64
65
  | Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
65
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
66
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
66
67
  | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
67
68
  | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
68
69
 
69
70
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
70
71
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
71
72
 
73
+ `vitest-auto-spy/setup` is Vitest-only as well — `setupAutoSpy()` registers Vitest hooks. On
74
+ `bun test` nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy between tests;
75
+ put `afterEach(() => { restoreMockedProps(); mock.restore(); })` in a `--preload` file, and any
76
+ `mock.module()` there too (inside a test it swaps bindings after the original module already ran).
77
+
72
78
  The rxjs peer is **>= 7.2**, not `>=7`. The observable layer used to pull `concatMap`, `delay`,
73
79
  `switchMap`, `take`, `takeUntil` and `takeWhile` from `rxjs/operators`; rxjs 8 removes that deep path,
74
80
  so the specifier moved to the root `rxjs` entry — which is where rxjs re-exported them in 7.2 — and
@@ -139,7 +145,11 @@ noise scales with the number of them. Both take the same second argument (`retur
139
145
  `observablePropsToSpyOn`).
140
146
 
141
147
  `createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
142
- `no-type-assertion` lint rule without an `eslint-disable` on every fixture.
148
+ `no-type-assertion` lint rule without an `eslint-disable` on every fixture. `createMock<T>(undefined)`
149
+ is the same call as `createMock<T>()` and answers `{}`, **not** `undefined` — a forwarding helper
150
+ with an optional `overrides` parameter relies on that. A fixture that means "no value" passes
151
+ `undefined` itself: `getters.profile.mockReturnValue(undefined)`, never
152
+ `mockReturnValue(createMock<Profile>(undefined))`.
143
153
 
144
154
  **`createSpyFromInstance(instance, config?)` is the one case where nothing is constructed.** Every
145
155
  factory above builds the double, which is no help once the object exists and other code already
@@ -172,6 +182,17 @@ member declared `configurable: false`, come back as this package's diagnostic ra
172
182
  `sinon.createStubInstance` builds a new object from a constructor instead of patching the one you
173
183
  hold, and `bun:test` and `node:test` have nothing.
174
184
 
185
+ **`passthrough: true` keeps the patched object working.** Unconfigured methods run the real
186
+ implementation (with the instance as `this`) and are still recorded; any configuration takes the
187
+ whole method over and `resetAutoSpy` hands it back. The Angular shape is
188
+ `createSpyFromInstance(TestBed.inject(CartService), { passthrough: true })` — dependencies, `signal()`
189
+ fields, `ɵprov` and the real `ngOnDestroy` keep working. Lifecycle hooks, discovered callables with an
190
+ API of their own (signals) and discovered classes are left real rather than spied. Do **not** combine
191
+ it with `strict: true` or `onUnstubbedCall` on the same call — that throws; a suite-wide strict yields
192
+ to it. A `calledWith(1)` miss answers `undefined`, not the real method: configuring a method hands the
193
+ whole method over. Use it to assert an interaction on a real collaborator; use a plain double when
194
+ the test must not touch the real one.
195
+
175
196
  **`createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` are for the model
176
197
  that more than one spec builds.** The difference from `createMock` is the `defaults` argument: it is
177
198
  a **complete** `T`, checked in full, in one place — so a field the model dropped fails there instead
@@ -210,6 +231,16 @@ boot(asInstance(logger)); // → AppLogger, for the API under test
210
231
  asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // → the helpers
211
232
  ```
212
233
 
234
+ **Arrays and unmocked calls on `mockDeep`.** A member read by a numeric index (`api.items[0]`) is a
235
+ real `Array` of deep mocks from then on, and `map`, iteration and `toEqual` work on it. Read the
236
+ member again after the first index: a handle taken before the first index is still a node. To make a
237
+ call nobody configured fail, use `mockDeep<T>({}, { fallbackMockImplementation: () => { throw … } })`.
238
+ Options go in the **second** argument, not the first as in vitest-mock-extended. The precedence is
239
+ configuration > fallback > `selfReturning`. A `calledWith` miss answers `undefined`, not the
240
+ fallback; use `mustBeCalledWith` for "other arguments fail". For a callback API (`$transaction`), use
241
+ `method.mockImplementation((run) => run(asInstance(mock)))`; there is no option for it.
242
+ `vi.spyOn(mock.repo, 'find')` works on a member nobody has read and returns the node's own spy.
243
+
213
244
  `asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
214
245
  sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
215
246
 
@@ -394,6 +425,15 @@ from inside the placeholder's getter; the spy is kept beside the double instead,
394
425
  a stable mock and `mockReturnValue` on it works. On a merely sealed double an assignment still
395
426
  reaches the member.
396
427
 
428
+ `vi.spyOn(double, 'load')` on a method nobody has read yet works again. Vitest reads an accessor by
429
+ calling its getter with no receiver, and 5.19.0's shared getter answered that with
430
+ `TypeError: Invalid value used as weak map key`. The call now wraps a forwarder: a configured
431
+ `mockReturnValue` answers, an unconfigured call reaches the double's own spy (strict guard included),
432
+ and `mockRestore()` / `vi.restoreAllMocks()` hand back that same spy with the calls it recorded. It
433
+ is still redundant — the member already is a spy, so `double.load.mockReturnValue(…)` is the line to
434
+ write — and a wrapped method called off its double (`const { load } = double; load()`) throws a
435
+ message saying so.
436
+
397
437
  **Symbol-keyed methods are discovered and spied.** A method under a symbol the project owns —
398
438
  `[SERIALIZE]()`, `Symbol.for('app.render')` — is a method like any other, resets with the rest and
399
439
  shows up in `Reflect.ownKeys`. The runtime's own protocol symbols are deliberately left alone
@@ -816,7 +856,14 @@ walking the prototype chain would let one registration change doubles in files n
816
856
  A second registration for the same class replaces the first, because two of them in one suite is the
817
857
  drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
818
858
  `clearAutoSpyDefaults()` the lot. `createSpyFromInstance(obj)` reads the registration of the class
819
- `obj.constructor` names, merged the same way; an object literal resolves none.
859
+ `obj.constructor` names, merged the same way; an object literal resolves none. One exception, because
860
+ an instance is real: when the call site lists `onlyMethodsToSpyOn`, the rest of the object stays real,
861
+ so the registration contributes only `strict`, `onUnstubbedCall`, `onUnstubbedRead` and the
862
+ `returns` / `selfReturning` entries of the listed methods — not its accessor lists, its other method
863
+ lists or its `overrides`. `registerAutoSpyDefaults(Router, { gettersToSpyOn: ['url'] })` therefore
864
+ leaves `router.url` live under `createSpyFromInstance(router, { onlyMethodsToSpyOn: ['navigateByUrl'] })`.
865
+ A `returns` or `selfReturning` name the call site itself wrote for a method it left real is reported
866
+ as a misconfiguration and skipped.
820
867
 
821
868
  A setup file that registers more than a handful of classes can say them as one table instead of one
822
869
  call each. Rows apply in order, and each is checked against **its own** class — a key `Router` does
@@ -1012,6 +1059,15 @@ const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); //
1012
1059
  const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
1013
1060
  ```
1014
1061
 
1062
+ `injectSpy(X)` without the argument keeps a declared default when the constructor does not take the
1063
+ type parameter. When it does — `constructor(public data: T, …)`, the shape of most modal refs — 5.19.0
1064
+ inferred `X<never>`, and with the typed `accessorSpies` bag `Spy<X<never>>` no longer assigns to
1065
+ `Spy<X<unknown>>`. Such a class is now read at its **constraint**: `ModalRef<T = unknown>` gives
1066
+ `Spy<ModalRef<unknown>>`, `ConfigService<T extends Config = Defaults>` gives
1067
+ `Spy<ConfigService<Config>>`. A constructor cannot hand TypeScript the default here, so spell the
1068
+ argument out whenever the default or a particular instantiation is what the spec means:
1069
+ `injectSpy<ConfigService>(ConfigService)`, `injectSpy<ModalRef<PurchaseOptions>>(ModalRef)`.
1070
+
1015
1071
  It applies to `createSpyFromClass` with a configuration too, in one combination: an accessor list
1016
1072
  (or `overrides`) **and** `returns` on a generic class. TypeScript checks a generic class argument
1017
1073
  after the configuration, reads `T` back from `gettersToSpyOn: ['remoteConfig']` as
@@ -1530,6 +1586,13 @@ does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMock
1530
1586
  (new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
1531
1587
  changes, and the peer range still starts at 2.1 — one install spans Vitest 2.1 through 5.x.
1532
1588
 
1589
+ What each config flag does to an auto-spy, measured on both spy engines: `clearMocks` empties the
1590
+ calls and keeps the configuration; `mockReset` empties the calls and drops `mockReturnValue` /
1591
+ `mockImplementation` but **keeps `calledWith` rules**; `restoreMocks` never touches an auto-spy (it
1592
+ only undoes `vi.spyOn`). Do not claim the flags skip auto-spies — a sweep sentinel in the Vitest
1593
+ adapter routes the first two. `resetAutoSpy(spy)` is the call that drops everything, `calledWith`
1594
+ included. `mockReset()` on a `vi.spyOn(…)` runs the **real** method again (Vitest ≥ 3).
1595
+
1533
1596
  What Vitest 5 does break is the runner's own doing, not this library's: with `clearMocks` on by
1534
1597
  default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
1535
1598
  zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
@@ -1589,6 +1652,16 @@ nothing listening on that port.`WebSocket`and`EventSource`are left alone: their
1589
1652
  event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
1590
1653
  a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
1591
1654
 
1655
+ - `stubResponse(init?)` (`/setup`) builds a real `Response` — never write `{ ok, json } as Response`.
1656
+ Plain data in `body` goes out as JSON with `application/json`; an `ok` that disagrees with
1657
+ `status` throws. A body can be read once: for a stub answering several calls use
1658
+ `mockImplementation(async () => stubResponse(…))`, not `mockResolvedValue`.
1659
+ - `blockNetwork` leaves `fetch` alone while `Symbol.for('fetch-interceptor')` is on `globalThis`
1660
+ (MSW `setupServer`, nock 14), so MSW handlers keep answering; XHR stays blocked for what MSW does
1661
+ not handle. MSW's `onUnhandledRequest: 'error'` exempts asset-looking URLs (`.svg`, `.json`,
1662
+ fonts…); a last `http.all('*', () => HttpResponse.error())` is the hard floor. MSW's browser
1663
+ `setupWorker` has not been checked.
1664
+
1592
1665
  `restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
1593
1666
  fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
1594
1667
  the realm, not owned by `globalThis`), and with `isolate: false` the next file dies inside Vitest's
@@ -2168,6 +2241,24 @@ gives the probing dependency `dayjsStub`, where it used to be replaced by the na
2168
2241
  the default export silently became the wrong object. Only a factory without one gets
2169
2242
  `default: <the namespace>`, which is the interop shape it was there for; the return type follows.
2170
2243
 
2244
+ A factory's `vi.fn()` comes back typed as the real function, without `calledWith` or `resolveWith`.
2245
+ `adoptMock` takes it over in place — same object, history kept, typed from the export's signature —
2246
+ and an unconfigured call keeps answering what the mock answered before:
2247
+
2248
+ ```ts
2249
+ import { loadUser } from './api';
2250
+
2251
+ vi.mock('./api', () => ({ loadUser: vi.fn() }));
2252
+ adoptMock(loadUser).calledWith(7).resolveWith({ id: 7, name: 'Ada' });
2253
+ ```
2254
+
2255
+ To keep the real module and configure one case, spy it through:
2256
+ `vi.mock('./api', async (importOriginal) => moduleNamespace(await importOriginal(), { passthrough: true }))`.
2257
+ Every function export runs for real and is recorded until configured; classes and values stay real;
2258
+ calls one export makes to another inside the module are not recorded. A `vi.spyOn` / `{ spy: true }`
2259
+ mock calls an original it does not report, so adopting it makes an unconfigured call answer
2260
+ `undefined`; `node:test`'s `mock.fn()` is refused.
2261
+
2171
2262
  There is no `mockModule(…)` helper here, and there cannot be: Vitest hoists the literal `vi.mock`
2172
2263
  call, so a wrapper around it would be hoisted as a call to a function that does not exist yet. Share
2173
2264
  a fixture between the factory and the tests with `vi.hoisted()`.
@@ -3536,6 +3627,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3536
3627
  | `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
3537
3628
  | `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
3538
3629
  | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
3630
+ | `no-hand-assigned-global` | `error` | — | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages; the three observers stay with `prefer-observer-stub` |
3539
3631
  | `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory` / `useExisting`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
3540
3632
  | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
3541
3633
  | `no-console-in-spec` | `error` | — | a spec calling `console.x(…)` itself, or `console.x = …`, which nothing restores → absorb the code's output through `vitest-auto-spy/console` |
@@ -3553,8 +3645,8 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3553
3645
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
3554
3646
  | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
3555
3647
 
3556
- Thirty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3557
- `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-five are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3648
+ Thirty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
3649
+ `no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
3558
3650
  `no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
3559
3651
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
3560
3652
  builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
@@ -3814,6 +3906,11 @@ packages, which a subpath export can never be.
3814
3906
  | 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 |
3815
3907
  | 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 |
3816
3908
  | `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({ … }))` |
3909
+ | `adoptMock() needs a mock that can report its implementation` | a `node:test` `mock.fn()` handed to `adoptMock` | build the double with `createFunctionSpy()` and pass that to the module mock |
3910
+ | `Cannot spy on export "x". Module namespace is not configurable in ESM.` | `vi.spyOn(namespace, 'x')` under Vitest browser mode or a native ESM loader — the namespace is sealed | spy the class / prototype, or `vi.mock(path, { spy: true })` / `moduleNamespace(await importOriginal(), { passthrough: true })` |
3911
+ | `[Function] is not a spy or a call to a spy!` in a Storybook `play` | `expect` from `storybook/test` wraps a function with no own enumerable keys, and an auto-spy keeps its `mock*` methods on a shared prototype | import `expect` from `vitest` in that story file, or `setSpyEngine('runner')` from `/setup` |
3912
+ | `object is not iterable` destructuring a mocked hook tuple | an auto-mock stands for an object, and a Proxy double is not iterable | write the tuple — `mockReturnValue([data, false, null])`; auto-mocks are for the object a hook returns |
3913
+ | `The property "x" is not defined on the object` from `vi.spyOn(Class.prototype, 'x')` | `x` is an instance field (arrow function, signal), not a prototype method | `createSpyFromClass(C, { instanceMethodsToSpyOn: ['x'] })` |
3817
3914
  | `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 |
3818
3915
  | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
3819
3916
  | 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…())` |
@@ -3980,6 +4077,21 @@ meant to show, with the test still green. Carry `vi.fn(() => x)` over as
3980
4077
  `mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
3981
4078
  anyone writing a codemod, because the rename looks like the safest edit in the file.
3982
4079
 
4080
+ ### Advice that circulates and is wrong on Vitest
4081
+
4082
+ Jest-era tutorials and cheat sheets repeat a handful of lines that fail on Vitest, or pass and leak.
4083
+ Each was checked on Vitest 5.0.0:
4084
+
4085
+ | Circulating advice | What happens | Write instead |
4086
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
4087
+ | `const load = vi.fn(); vi.mock('./api', () => ({ load }))` | `vi.mock` is hoisted above every import and `const`: `ReferenceError: Cannot access 'load' before initialization`, wrapped in `[vitest] There was an error when mocking a module` | `const mocks = vi.hoisted(() => ({ load: vi.fn() }))`, then `() => ({ load: mocks.load })` |
4088
+ | "name it `mockLoad` and the factory may read it" | Jest's exemption for `mock`-prefixed names does not exist in Vitest — same `ReferenceError` | `vi.hoisted` |
4089
+ | `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
4090
+ | `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
4091
+ | `import { userEvent } from '@testing-library/user-event'` | the named export exists only from 14.5.0; the default export works on every 14.x, and every 14.x method returns a promise | `import userEvent from '@testing-library/user-event'`; `const user = userEvent.setup(); await user.click(el)` |
4092
+ | `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
4093
+ | `global.fetch = vi.fn()` in a test | survives `vi.restoreAllMocks()` and `vi.unstubAllGlobals()`, and answers every later test of the file | `mockValueProp(globalThis, 'fetch', …)` or `vi.stubGlobal` + `unstubGlobals: true`; `blockNetwork()` to stay offline; the `no-hand-assigned-global` rule reports the bare form |
4094
+
3983
4095
  ---
3984
4096
 
3985
4097
  ## 19. Before you report success
@@ -4036,7 +4148,8 @@ Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
4036
4148
  worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
4037
4149
  `codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
4038
4150
  it is a directory (a nested clone) or a file (a worktree). Past 50 000 files the scan still
4039
- truncates and says so; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4151
+ truncates, and `doctor` reports that as a `scan-cap-reached` warning (exit 1) rather than a clean
4152
+ result; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
4040
4153
  - **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
4041
4154
  read is not a clean result. Absolute paths and `./`-style ones resolve against `--cwd`.
4042
4155
 
@@ -4046,6 +4159,14 @@ note, `perf --gate` over budget; **2** is "there was nothing to judge" — no co
4046
4159
  command, an unknown flag, an unreadable `--only` / `--from` value, a path matching no file, and a
4047
4160
  `perf` run that measured nothing (including a red suite, which the gate will not judge at all).
4048
4161
 
4162
+ **Reading the output from a script: `--format json`**, on `doctor` and on `perf`. One JSON document
4163
+ on stdout — `schema`, `exitCode`, `tally` (`errors`, `warnings`, `notes`), every finding with
4164
+ `check`, `severity`, `file`, `message`, `fix`; `perf` adds `run`, `budgets` and `gate.verdicts` (one row
4165
+ per candidate, `outcome` one of `confirmed`, `not reproduced`, `unconfirmed`, `single reading`,
4166
+ `over budget`). Parse that rather than the text: the text is wrapped to the terminal (80 columns in a
4167
+ pipe), groups one cause found in many files into one block, and ends in a tally line that starts
4168
+ with `N errors, N warnings, N notes`.
4169
+
4049
4170
  ### If you were asked why a suite is slow
4050
4171
 
4051
4172
  ```bash