vitest-auto-spy 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1183,6 +1183,17 @@ the realm, not owned by `globalThis`), and with `isolate: false` the next file d
1183
1183
  own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
1184
1184
  the file in the stack is not the cause.
1185
1185
 
1186
+ `restoreWebStorage` is the other repair that is on by default. Vitest copies a DOM environment's
1187
+ globals onto `globalThis` behind `if (k in global) return KEYS.includes(k)`, and neither
1188
+ `localStorage` nor `sessionStorage` is in `KEYS` — both used to arrive only because Node put neither
1189
+ on `globalThis`, so the first half of that condition was false. Node's own Web Storage made the key
1190
+ exist, and the environment's storage stopped arriving: `setItem is not a function` on Node 25,
1191
+ `undefined` on Node 26, under jsdom and happy-dom alike, because the filter runs before either. The
1192
+ suite stays green until a spec touches storage, so this lands as "CI moved to a new Node and eleven
1193
+ unrelated specs died". The repair writes a key, reads it back and removes it — a storage that
1194
+ survives that is left alone, whoever implemented it — and it installs nothing in a `node`
1195
+ environment, which is supposed to have no Web Storage at all.
1196
+
1186
1197
  `hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
1187
1198
  CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
1188
1199
  `test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
@@ -2484,6 +2495,7 @@ packages, which a subpath export can never be.
2484
2495
  | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2485
2496
  | `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 |
2486
2497
  | `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) |
2498
+ | `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
2487
2499
  | `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) |
2488
2500
  | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2489
2501
  | 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) |
package/README.md CHANGED
@@ -2492,6 +2492,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
2492
2492
  | `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
2493
2493
  | `setupAngularTestEnv(opts)` | `/angular` | Zone and zoneless spec files in one worker, switching platforms per file |
2494
2494
  | `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
2495
+ | `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
2495
2496
  | `trackMockRegistry()` / `keepMockRegistered(mock)` / `restoreLongLivedImplementations()` | `/setup` | Keep @vitest/spy's mock registry to the mocks that outlive a file; mark one the split would miss; put back an implementation a cross-file `vi.resetAllMocks()` dropped ([details](#test-run-hygiene)) |
2496
2497
  | `trackNodeMocks()` / `pruneNodeMocks()` / `countNodeMocks()` | `/node` | Give this library its own `node:test` `MockTracker` so a dropped spy is freed — 21× less retained heap; sweep by hand, and read the count back |
2497
2498
  | `setSpyEngine(engine)` / `getSpyEngine()` | `/setup` | Build method spies from this library's own mock (`'auto-spy'`, the default) or from `vi.fn()` (`'runner'`) ([details](#the-spy-engine)) |
@@ -2733,6 +2734,16 @@ expensive to diagnose when it is missing. The first three are on by default:
2733
2734
  which is a fact rather than a guess, and names the `setImmediate` case that reaches this with no
2734
2735
  timer in sight: a request matching no Express route is ended by `finalhandler` on `setImmediate`,
2735
2736
  so the 404 is never written and a routing mistake is reported as a slow test.
2737
+ 11. **Web Storage the runner never handed over.** On by default, because it only ever repairs.
2738
+ Vitest copies a DOM environment's globals onto `globalThis` behind `if (k in global) return
2739
+ KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
2740
+ arrive only because Node put neither on `globalThis`, so the first half was false. Node's own
2741
+ Web Storage made the key exist, and now the environment's storage never arrives: `setItem is
2742
+ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
2743
+ filter runs before either. A suite stays green with this broken — only the specs that touch
2744
+ storage fail — so it arrives as "CI moved to a new Node and eleven unrelated specs died". The
2745
+ repair decides by using the storage rather than looking at it, replaces only one that cannot
2746
+ keep a value, and installs nothing at all in a `node` environment.
2736
2747
 
2737
2748
  | Option | Default | Notes |
2738
2749
  | --------------------- | --------- | ------------------------------------------------------------------------------------- |
@@ -2746,6 +2757,7 @@ expensive to diagnose when it is missing. The first three are on by default:
2746
2757
  | `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
2747
2758
  | `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
2748
2759
  | `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
2760
+ | `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
2749
2761
  | `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
2750
2762
  | `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
2751
2763
  | `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
package/dist/cli.js CHANGED
@@ -2356,7 +2356,7 @@ function checkForeignPragma(graph) {
2356
2356
  }
2357
2357
 
2358
2358
  // src/cli/checks/export-map.generated.ts
2359
- var EXPORT_MAP_VERSION = "4.2.0";
2359
+ var EXPORT_MAP_VERSION = "4.3.0";
2360
2360
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular-http vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin";
2361
2361
  var EXPORTED_BY = {
2362
2362
  AccessorImplementations: "0 1 2 3 11 14 15 16",
@@ -2604,6 +2604,8 @@ var EXPORTED_BY = {
2604
2604
  RestoreProp: "0 1 2 3 11 14 15 16",
2605
2605
  restoreSpiedInstance: "0 1 2 3 14 15 16",
2606
2606
  restoreTimerGlobals: "17",
2607
+ restoreWebStorage: "17",
2608
+ RestoreWebStorageOptions: "17",
2607
2609
  RuleSeverity: "19",
2608
2610
  runEffect: "2 11",
2609
2611
  SchedulerHost: "17",
package/dist/setup.d.ts CHANGED
@@ -356,6 +356,18 @@ interface SetupAutoSpyOptions {
356
356
  * installed on purpose. See {@link restoreTimerGlobals}.
357
357
  */
358
358
  restoreTimerGlobals?: boolean;
359
+ /**
360
+ * Give the run a `localStorage` and a `sessionStorage` that work. Default `true`, for the same
361
+ * reason as {@link restoreTimerGlobals}: a storage that survives a write and a read back is left
362
+ * untouched, and a `node` environment — which is supposed to have neither — gets nothing.
363
+ *
364
+ * Vitest's `populateGlobal` copies a DOM environment's globals behind `if (k in global) return
365
+ * KEYS.includes(k)`, and neither storage is in `KEYS`. Node's own Web Storage put the key on
366
+ * `globalThis`, so the filter now answers "no" and the environment's storage never arrives:
367
+ * `setItem is not a function` on Node 25, `localStorage` undefined on Node 26, under jsdom and
368
+ * happy-dom alike. See {@link restoreWebStorage}.
369
+ */
370
+ restoreWebStorage?: boolean;
359
371
  /**
360
372
  * Keep `@vitest/spy`'s registry of every mock ever created down to the mocks that outlive a file.
361
373
  * Default `false`, because it reaches into a set the runner does not expose.
@@ -458,16 +470,6 @@ interface SetupAutoSpyOptions {
458
470
  */
459
471
  onUnstubbedCall?: UnstubbedCallHandler;
460
472
  }
461
- /**
462
- * Install the library's test-run hygiene.
463
- *
464
- * ```ts
465
- * // vitest.setup.ts
466
- * import { setupAutoSpy } from 'vitest-auto-spy/setup';
467
- *
468
- * setupAutoSpy();
469
- * ```
470
- */
471
473
  declare function setupAutoSpy(options?: SetupAutoSpyOptions): void;
472
474
 
473
475
  /**
@@ -817,4 +819,60 @@ declare function restoreTimerGlobals(): void;
817
819
  /** The names this module watches — exported for the diagnostics that report what went missing. */
818
820
  declare function getWatchedTimerGlobals(): string[];
819
821
 
820
- export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, type BlockNetworkOptions, type CountingClock, type CountingClockOptions, type DuplicateCopiesReaction, type FakeTimersConfig, type GlobalPatchReaction, type PerTestHandle, type RejectionHost, type SchedulerHost, type SetupAutoSpyOptions, type SpyEngine, type StopTrackingRejections, type StopTrackingTimers, type StrayRejection, type SystemTime, type XhrBlockMode, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getSpyEngine, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, setSpyEngine, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
822
+ /**
823
+ * Putting `localStorage` / `sessionStorage` back after the runner drops them.
824
+ *
825
+ * Vitest copies a DOM environment's globals onto `globalThis` behind one filter — `if (k in global)
826
+ * return KEYS.includes(k)` — and neither storage is in `KEYS`. While Node put neither on
827
+ * `globalThis` the first half was false and both were copied; Node's own Web Storage made the key
828
+ * exist, so the filter now answers "no" and the environment's storage never arrives. Measured with
829
+ * one spec, under jsdom and happy-dom alike, because the filter runs before either:
830
+ *
831
+ * ```text
832
+ * Node 24.19 localStorage works
833
+ * Node 25.9 localStorage.setItem is not a function
834
+ * Node 26.7 localStorage is undefined
835
+ * ```
836
+ *
837
+ * A suite stays green with this broken — only the specs that touch storage fail — so it usually
838
+ * arrives as "CI moved to a new Node and eleven unrelated specs died".
839
+ */
840
+ /** The two globals this module repairs. */
841
+ declare const STORAGE_KEYS: readonly ["localStorage", "sessionStorage"];
842
+ /** One of the two. */
843
+ type StorageKey = (typeof STORAGE_KEYS)[number];
844
+ /** How {@link restoreWebStorage} finds the window whose storage it trusts. */
845
+ interface RestoreWebStorageOptions {
846
+ /**
847
+ * The window to take a working storage from, and to repair alongside `globalThis`.
848
+ *
849
+ * Defaults to `document.defaultView`, and to nothing where there is no document. Pass `null` to
850
+ * say there is no window: the repair then does nothing, which is what a DOM-less runtime wants.
851
+ */
852
+ view?: object | null;
853
+ }
854
+ /**
855
+ * Give `globalThis` a `localStorage` and a `sessionStorage` that work.
856
+ *
857
+ * Safe to call at any point and as often as you like, and a no-op in the ordinary case: a storage
858
+ * that survives a write and a read back is left exactly as it is, so a spec's own stub survives
859
+ * this. It is also a no-op where there is no window — a `node` environment is *supposed* to have no
860
+ * Web Storage, and inventing one would hand the code under test an API the real runtime lacks.
861
+ *
862
+ * The window's own storage is preferred over the stand-in: the environment implements the real
863
+ * thing, and under a runner whose window is a separate object (a Bun preload, say) only the copy to
864
+ * `globalThis` failed. The `Map`-backed one is for when nothing else works — which, under Vitest,
865
+ * is every time, because there `document.defaultView` *is* `globalThis`.
866
+ *
867
+ * ```ts
868
+ * // vitest.setup.ts — or leave it to setupAutoSpy(), which calls this by default
869
+ * import { restoreWebStorage } from 'vitest-auto-spy/setup';
870
+ *
871
+ * restoreWebStorage();
872
+ * ```
873
+ *
874
+ * @returns The keys it had to repair — `[]` when the environment was already sound.
875
+ */
876
+ declare function restoreWebStorage(options?: RestoreWebStorageOptions): StorageKey[];
877
+
878
+ export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, type BlockNetworkOptions, type CountingClock, type CountingClockOptions, type DuplicateCopiesReaction, type FakeTimersConfig, type GlobalPatchReaction, type PerTestHandle, type RejectionHost, type RestoreWebStorageOptions, type SchedulerHost, type SetupAutoSpyOptions, type SpyEngine, type StopTrackingRejections, type StopTrackingTimers, type StrayRejection, type SystemTime, type XhrBlockMode, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getSpyEngine, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, restoreWebStorage, setSpyEngine, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
package/dist/setup.js CHANGED
@@ -140,7 +140,7 @@ function getWatchedTimerGlobals() {
140
140
 
141
141
  // src/lib/fake-timers.ts
142
142
  function setupFakeTimers(config, { betweenTests = false } = {}) {
143
- const install = () => {
143
+ const install2 = () => {
144
144
  if (!vi.isFakeTimers()) {
145
145
  vi.useFakeTimers(config);
146
146
  }
@@ -152,13 +152,13 @@ function setupFakeTimers(config, { betweenTests = false } = {}) {
152
152
  restoreTimerGlobals();
153
153
  };
154
154
  if (betweenTests) {
155
- beforeAll(install);
155
+ beforeAll(install2);
156
156
  }
157
- beforeEach(install);
157
+ beforeEach(install2);
158
158
  afterEach(() => {
159
159
  uninstall();
160
160
  if (betweenTests) {
161
- install();
161
+ install2();
162
162
  }
163
163
  });
164
164
  if (betweenTests) {
@@ -681,6 +681,78 @@ function detectsAsyncLeaks(host = globalThis) {
681
681
  return Reflect.get(Object(Reflect.get(Object(Reflect.get(host, "__vitest_worker__")), "config")), "detectAsyncLeaks") === true;
682
682
  }
683
683
 
684
+ // src/lib/web-storage.ts
685
+ var STORAGE_KEYS = ["localStorage", "sessionStorage"];
686
+ var PROBE_KEY = "__vitest_auto_spy_probe__";
687
+ var PROBE_METHODS = ["setItem", "getItem", "removeItem"];
688
+ var MapBackedStorage = class {
689
+ #items = /* @__PURE__ */ new Map();
690
+ get length() {
691
+ return this.#items.size;
692
+ }
693
+ clear() {
694
+ this.#items.clear();
695
+ }
696
+ getItem(key) {
697
+ return this.#items.get(key) ?? null;
698
+ }
699
+ key(index) {
700
+ return [...this.#items.keys()][index] ?? null;
701
+ }
702
+ removeItem(key) {
703
+ this.#items.delete(key);
704
+ }
705
+ setItem(key, value) {
706
+ this.#items.set(key, String(value));
707
+ }
708
+ };
709
+ function restoreWebStorage(options = {}) {
710
+ const view = options.view === void 0 ? currentView() : options.view;
711
+ if (!view) {
712
+ return [];
713
+ }
714
+ const repaired = [];
715
+ for (const key of STORAGE_KEYS) {
716
+ if (works(read(globalThis, key))) {
717
+ continue;
718
+ }
719
+ const fromView = read(view, key);
720
+ install(view, key, works(fromView) ? fromView : new MapBackedStorage());
721
+ repaired.push(key);
722
+ }
723
+ return repaired;
724
+ }
725
+ function currentView() {
726
+ return typeof document === "undefined" ? null : document.defaultView;
727
+ }
728
+ function read(host, key) {
729
+ return Reflect.get(host, key);
730
+ }
731
+ function isStorageShaped(candidate) {
732
+ const host = Object(candidate);
733
+ return PROBE_METHODS.every((method) => typeof Reflect.get(host, method) === "function");
734
+ }
735
+ function works(candidate) {
736
+ if (!isStorageShaped(candidate)) {
737
+ return false;
738
+ }
739
+ try {
740
+ candidate.setItem(PROBE_KEY, PROBE_KEY);
741
+ const stored = candidate.getItem(PROBE_KEY);
742
+ candidate.removeItem(PROBE_KEY);
743
+ return stored === PROBE_KEY;
744
+ } catch {
745
+ return false;
746
+ }
747
+ }
748
+ function install(view, key, storage) {
749
+ const descriptor = { value: storage, writable: true, configurable: true };
750
+ Object.defineProperty(globalThis, key, descriptor);
751
+ if (view !== globalThis) {
752
+ Object.defineProperty(view, key, descriptor);
753
+ }
754
+ }
755
+
684
756
  // src/lib/setup-auto-spy.ts
685
757
  function warnAboutSuppressedLeaks(cancelled, write = writeWarning) {
686
758
  write(
@@ -811,11 +883,17 @@ function watchStrayRejections(enabled) {
811
883
  afterAll(trackStrayRejections());
812
884
  return [reportStrayRejections];
813
885
  }
814
- function setupAutoSpy(options = {}) {
815
- reportDuplicateCopies(options.duplicateCopies ?? "throw");
886
+ function prepareEnvironment(options) {
816
887
  if (options.angularBuildHint ?? true) {
817
888
  noticeAngularBuildSplitting();
818
889
  }
890
+ if (options.restoreWebStorage ?? true) {
891
+ restoreWebStorage();
892
+ }
893
+ }
894
+ function setupAutoSpy(options = {}) {
895
+ reportDuplicateCopies(options.duplicateCopies ?? "throw");
896
+ prepareEnvironment(options);
819
897
  if (options.strayTimers ?? false) {
820
898
  trackStrayTimers();
821
899
  afterAll(() => {
@@ -886,10 +964,10 @@ function describeSkippedTeardown(leaked) {
886
964
  DOCS_LINKS.setup
887
965
  );
888
966
  }
889
- function installPerTest(install) {
967
+ function installPerTest(install2) {
890
968
  let current;
891
969
  beforeEach(() => {
892
- current = { value: install() };
970
+ current = { value: install2() };
893
971
  });
894
972
  afterEach(() => {
895
973
  current = void 0;
@@ -1000,4 +1078,4 @@ function registerFocusMatchers() {
1000
1078
  }
1001
1079
  useVitestAdapter();
1002
1080
 
1003
- export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
1081
+ export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, restoreWebStorage, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vitest-auto-spy
3
- description: Write or fix tests that use vitest-auto-spy — typed spies from a class or a type on Vitest, bun:test and node:test. Use when a spec imports `vitest-auto-spy` or any of its subpaths (`/angular`, `/angular-http`, `/bun-angular`, `/bun`, `/node`, `/rxjs`, `/nestjs`, `/jasmine`, `/jasmine-compat`, `/observer-spy`, `/setup`, `/zone`, `/diagnostics`, `/eslint-plugin`), when the user mentions createSpyFromClass, createAutoMock, createMock, mockDeep, createFunctionSpy, createSpyObj, enableJasmineCompat, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, createDirectiveHost, overrideComponentProvider, enableAngularDiagnostics, trackInjections, runEffect, settleResource, httpResource, mockResourceProp, mockSignalProp, mockReadonlyProp, captureArg, createSpyFromInstance, explainSpy, expectEmission, expectError, setupAutoSpy, setSpyEngine, assertMocked, Spy<T>, calledWith, mustBeCalledWith, onlyMethodsToSpyOn, strict, onUnstubbedCall, resolveWith or nextWith, when migrating a suite off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator (createSpyObject, mockProvider, SpectatorService), or when a test fails with "No mock adapter registered", "Observable spies require rxjs", "not found on the class prototype", "strict mode is on", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController" or "Spy<T> is not assignable".
3
+ description: Write or fix tests that use vitest-auto-spy — typed spies from a class or a type on Vitest, bun:test and node:test. Use when a spec imports `vitest-auto-spy` or a subpath (`/angular`, `/angular-http`, `/bun-angular`, `/bun`, `/node`, `/rxjs`, `/nestjs`, `/jasmine`, `/observer-spy`, `/setup`, `/zone`, `/diagnostics`, `/eslint-plugin`), the user mentions createSpyFromClass, createAutoMock, createMock, mockDeep, createFunctionSpy, createSpyObj, enableJasmineCompat, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, createDirectiveHost, overrideComponentProvider, enableAngularDiagnostics, trackInjections, runEffect, settleResource, httpResource, mockResourceProp, mockSignalProp, mockReadonlyProp, captureArg, createSpyFromInstance, explainSpy, expectEmission, expectError, setupAutoSpy, setSpyEngine, restoreWebStorage, assertMocked, Spy<T>, calledWith, mustBeCalledWith, onlyMethodsToSpyOn, strict, onUnstubbedCall, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator (createSpyObject, mockProvider, SpectatorService), or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not found on the class prototype", "strict mode is on", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController", "localStorage.setItem is not a function" or "Spy<T> is not assignable".
4
4
  ---
5
5
 
6
6
  # vitest-auto-spy
@@ -126,69 +126,70 @@ it('loads', async () => {
126
126
 
127
127
  ## Reach for these before hand-rolling
128
128
 
129
- | Situation | Helper |
130
- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
131
- | a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — returns the writable |
132
- | a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading` |
133
- | the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
134
- | a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker |
135
- | a Nest provider whose constructor keeps changing | `createNestUnit(Target, { expose })` — built from its DI metadata |
136
- | asserting a resource's value _and_ status together | `registerResourceMatchers()` → `toHaveResourceValue` / `toBeLoading` |
137
- | a callback or config object the code under test built | `captureArg<T>()` in the assertion, then read `.value` |
138
- | an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it |
139
- | a `calledWith` that is not matching and you cannot see why | `explainSpy(spy, 'method')` from `/diagnostics` — configs, calls, which hit which |
140
- | a method that has to throw — for all calls, or for some arguments | `spy.m.failWith(err)` / `spy.m.calledWith(x).failWith(err)` — **not** `throwWith` |
141
- | a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach` | `extendWithAutoSpies(test, { cart: CartService })` (`/angular`) |
142
- | an `effect()` whose trigger is now a static signal | `runEffect(effectRef)` |
143
- | the component constructs its own `IntersectionObserver` | `stubIntersectionObserver()` (+ `Resize` / `Mutation`) |
144
- | a green run exiting 1 with `AbortError` under happy-dom | `setupAutoSpy({ blockNetwork: true })` |
145
- | a real request going out of a unit run (`fetch`, XHR, `sendBeacon`) | `setupAutoSpy({ blockNetwork: true })` — `{ xhr: 'empty' }` for pings |
146
- | timers or frames from a previous file failing this one | `setupAutoSpy({ strayTimers: true })` — mutes `--detect-async-leaks`, see below |
147
- | an assertion error in stderr, every test green and the run at 0 | `setupAutoSpy({ strayRejections: true })` — zone.js swallowed it |
148
- | a run getting slower the longer it goes, on `isolate: false` | `setupAutoSpy({ pruneMockRegistry: true })` — the mock registry |
149
- | `JavaScript heap out of memory` on a suite of wide generated clients | `createSpyFromClass(X, { lazySpies: 'proxy' })` — one trap object, not a placeholder per method |
150
- | `Cannot read properties of undefined (reading 'now')` | `restoreTimerGlobals` — on by default |
151
- | a spy handed to an API typed against the real class | `asInstance()` / `asSpy()` |
152
- | the code under test does `new X()` (a global, a vendor SDK) | `mockConstructor(factory)` / `stubConstructor(obj, key, factory)` |
153
- | `X is not a constructor`, with a stack in production code | same — a `vi.fn(() => …)` cannot serve `new` |
154
- | waiting for a dynamic `import()` under fake timers | `settleDynamicImport(() => import('…'))` / `flushEventLoop()` |
155
- | `addEventListener(…, { signal })` throwing about `EventTarget` | `stubAbortController()` |
156
- | a suite ported from Jest's `fakeTimers.enableGlobally` | `setupAutoSpy({ globalFakeTimers: true })` |
157
- | `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()` | `setSpyEngine('runner')` / `getSpyEngine()` — `/setup`, Vitest only |
158
- | a nested `describe`'s `beforeAll` landing on real timers | `setupFakeTimers(cfg, { betweenTests: true })` |
159
- | setup hooks applying to the first spec file of a worker only | the setup module is cached — run coverage with `--isolate` |
160
- | `fakeAsync` inside `test.concurrent` | `installProxyZonePatch({ scope: 'callback' })` |
161
- | an assertion containing a date | `mockSystemTime(iso)` — never `vi.spyOn(globalThis, 'Date')` |
162
- | a spec asserting on tick _order_ under a frozen clock | `useCountingClock()` |
163
- | a dependency declared in the component's own `providers` | `overrideComponentProvider(Cmp, Token)` — it verifies on the first fixture that the override applied |
164
- | a double answering `undefined` for a method nobody configured | `{ strict: true }`, or `setupAutoSpy({ strict: true })` suite-wide |
165
- | an `afterEach` that exists only to reset one spy | `using spy = createSpyFromClass(X)` — `[Symbol.dispose]` resets it |
166
- | a dead NgModule import, dead `schemas`, an unflushed HTTP request | `enableAngularDiagnostics()` in the setup file, after `initTestEnvironment` |
167
- | "which collaborators did this actually inject?" | `trackInjections([A, TOKEN])` — providers plus the record, not `vi.mock` |
168
- | `NG0303` / `NG0301` / `NG0304` from an imported NgModule | `assertNgModuleScopes(Module)` — an AOT bundle stripped its scope |
169
- | `Cannot read properties of undefined (reading 'provide')` in `di_setup` | `assertComponentDefIntact(Cmp)` — a barrel chunk left a hole in `ɵcmp` |
170
- | a focus assertion failing as `expected false to deeply equal true` | `registerFocusMatchers()` + `expect(el).toHaveFocus()` |
171
- | a collaborator passed as an argument, then asserted on | `autoMocked<T>()` — typed `T & Spy<T>` |
172
- | a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN` | `stubMediaElement({ duration })`, then `media.set(el, …)` |
173
- | a `vi.mock()` that silently did nothing under a bundler | `assertMocked(ns, { specifier, exports })` |
174
- | `No "default" export is defined on the mock` | `vi.mock('x', () => moduleNamespace({ … }))` |
175
- | waiting for a `resource()` / an SDK to become ready | `flushEventLoopUntil(() => …, { label })` — budgeted, not tuned |
176
- | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …` | `expect(diffByField(actual, expected)).toBeUndefined()` |
177
- | a stub that works only in the first test of the file | `installPerTest(() => stub…())` — or install it in `beforeEach` |
178
- | a library failing every other run after a `defineProperty` on DOM | `setupAutoSpy({ guardGlobals: 'throw' })` names the file |
179
- | `Cannot set base providers because it has already been called` | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` |
180
- | a dependency behind an `InjectionToken`, with no class to spy | `provideAutoSpyForToken(TOKEN)` + `injectSpy(TOKEN)` |
181
- | `Expected to be running in 'ProxyZone', but it was not found` | `import 'vitest-auto-spy/zone'` (needs `globals: true`) |
182
- | `Property 'mockReturnValue' does not exist on type 'never'` | upgrade — the spy no longer collapses on an unreadable return type |
183
- | a signal-valued getter that `gettersToSpyOn` will not accept | it accepts any key now; for a signal prefer `mockSignalProp` |
184
- | five `asInstance(…)` in one call, found one per `tsc` run | `...asInstances(a, b, c, d, e)` |
185
- | `nextWith` demanding `HttpEvent<T>` on a generated client | `asSpy<Client, { overload: 'first' }>(…)` / `Overload<M, 0>` |
186
- | a fixture that needs a nested object built by its own call | `createMock<T>({ a: { b: 1 } })` — deep partial, still exact |
187
- | the same 100-line model literal copied into eight specs (`TS1117`) | one `createFixtureFactory<T>(defaults)`; specs call it with what they change |
188
- | `'params' in link` ladders, or a cast, to pick a union branch | `narrow.byKey(link, 'params')` / `narrow.observable(x)` |
189
- | `{ ...modelInstance, flag: true }` losing every getter | `withOverrides(modelInstance, { flag: true })` |
190
- | `NG0303` / `NG0304` / silence from a directive spec | `createDirectiveHost({ template, scope: [Module] })` |
191
- | "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` |
129
+ | Situation | Helper |
130
+ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
131
+ | a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — returns the writable |
132
+ | a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading` |
133
+ | the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
134
+ | a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker |
135
+ | a Nest provider whose constructor keeps changing | `createNestUnit(Target, { expose })` — built from its DI metadata |
136
+ | asserting a resource's value _and_ status together | `registerResourceMatchers()` → `toHaveResourceValue` / `toBeLoading` |
137
+ | a callback or config object the code under test built | `captureArg<T>()` in the assertion, then read `.value` |
138
+ | an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it |
139
+ | a `calledWith` that is not matching and you cannot see why | `explainSpy(spy, 'method')` from `/diagnostics` — configs, calls, which hit which |
140
+ | a method that has to throw — for all calls, or for some arguments | `spy.m.failWith(err)` / `spy.m.calledWith(x).failWith(err)` — **not** `throwWith` |
141
+ | a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach` | `extendWithAutoSpies(test, { cart: CartService })` (`/angular`) |
142
+ | an `effect()` whose trigger is now a static signal | `runEffect(effectRef)` |
143
+ | the component constructs its own `IntersectionObserver` | `stubIntersectionObserver()` (+ `Resize` / `Mutation`) |
144
+ | a green run exiting 1 with `AbortError` under happy-dom | `setupAutoSpy({ blockNetwork: true })` |
145
+ | a real request going out of a unit run (`fetch`, XHR, `sendBeacon`) | `setupAutoSpy({ blockNetwork: true })` — `{ xhr: 'empty' }` for pings |
146
+ | timers or frames from a previous file failing this one | `setupAutoSpy({ strayTimers: true })` — mutes `--detect-async-leaks`, see below |
147
+ | an assertion error in stderr, every test green and the run at 0 | `setupAutoSpy({ strayRejections: true })` — zone.js swallowed it |
148
+ | a run getting slower the longer it goes, on `isolate: false` | `setupAutoSpy({ pruneMockRegistry: true })` — the mock registry |
149
+ | `JavaScript heap out of memory` on a suite of wide generated clients | `createSpyFromClass(X, { lazySpies: 'proxy' })` — one trap object, not a placeholder per method |
150
+ | `Cannot read properties of undefined (reading 'now')` | `restoreTimerGlobals` — on by default |
151
+ | `localStorage.setItem is not a function`, or it is `undefined` | `restoreWebStorage` — on by default; Vitest's global filter, Node 25+ |
152
+ | a spy handed to an API typed against the real class | `asInstance()` / `asSpy()` |
153
+ | the code under test does `new X()` (a global, a vendor SDK) | `mockConstructor(factory)` / `stubConstructor(obj, key, factory)` |
154
+ | `X is not a constructor`, with a stack in production code | same — a `vi.fn(() => …)` cannot serve `new` |
155
+ | waiting for a dynamic `import()` under fake timers | `settleDynamicImport(() => import('…'))` / `flushEventLoop()` |
156
+ | `addEventListener(…, { signal })` throwing about `EventTarget` | `stubAbortController()` |
157
+ | a suite ported from Jest's `fakeTimers.enableGlobally` | `setupAutoSpy({ globalFakeTimers: true })` |
158
+ | `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()` | `setSpyEngine('runner')` / `getSpyEngine()` — `/setup`, Vitest only |
159
+ | a nested `describe`'s `beforeAll` landing on real timers | `setupFakeTimers(cfg, { betweenTests: true })` |
160
+ | setup hooks applying to the first spec file of a worker only | the setup module is cached — run coverage with `--isolate` |
161
+ | `fakeAsync` inside `test.concurrent` | `installProxyZonePatch({ scope: 'callback' })` |
162
+ | an assertion containing a date | `mockSystemTime(iso)` — never `vi.spyOn(globalThis, 'Date')` |
163
+ | a spec asserting on tick _order_ under a frozen clock | `useCountingClock()` |
164
+ | a dependency declared in the component's own `providers` | `overrideComponentProvider(Cmp, Token)` — it verifies on the first fixture that the override applied |
165
+ | a double answering `undefined` for a method nobody configured | `{ strict: true }`, or `setupAutoSpy({ strict: true })` suite-wide |
166
+ | an `afterEach` that exists only to reset one spy | `using spy = createSpyFromClass(X)` — `[Symbol.dispose]` resets it |
167
+ | a dead NgModule import, dead `schemas`, an unflushed HTTP request | `enableAngularDiagnostics()` in the setup file, after `initTestEnvironment` |
168
+ | "which collaborators did this actually inject?" | `trackInjections([A, TOKEN])` — providers plus the record, not `vi.mock` |
169
+ | `NG0303` / `NG0301` / `NG0304` from an imported NgModule | `assertNgModuleScopes(Module)` — an AOT bundle stripped its scope |
170
+ | `Cannot read properties of undefined (reading 'provide')` in `di_setup` | `assertComponentDefIntact(Cmp)` — a barrel chunk left a hole in `ɵcmp` |
171
+ | a focus assertion failing as `expected false to deeply equal true` | `registerFocusMatchers()` + `expect(el).toHaveFocus()` |
172
+ | a collaborator passed as an argument, then asserted on | `autoMocked<T>()` — typed `T & Spy<T>` |
173
+ | a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN` | `stubMediaElement({ duration })`, then `media.set(el, …)` |
174
+ | a `vi.mock()` that silently did nothing under a bundler | `assertMocked(ns, { specifier, exports })` |
175
+ | `No "default" export is defined on the mock` | `vi.mock('x', () => moduleNamespace({ … }))` |
176
+ | waiting for a `resource()` / an SDK to become ready | `flushEventLoopUntil(() => …, { label })` — budgeted, not tuned |
177
+ | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …` | `expect(diffByField(actual, expected)).toBeUndefined()` |
178
+ | a stub that works only in the first test of the file | `installPerTest(() => stub…())` — or install it in `beforeEach` |
179
+ | a library failing every other run after a `defineProperty` on DOM | `setupAutoSpy({ guardGlobals: 'throw' })` names the file |
180
+ | `Cannot set base providers because it has already been called` | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` |
181
+ | a dependency behind an `InjectionToken`, with no class to spy | `provideAutoSpyForToken(TOKEN)` + `injectSpy(TOKEN)` |
182
+ | `Expected to be running in 'ProxyZone', but it was not found` | `import 'vitest-auto-spy/zone'` (needs `globals: true`) |
183
+ | `Property 'mockReturnValue' does not exist on type 'never'` | upgrade — the spy no longer collapses on an unreadable return type |
184
+ | a signal-valued getter that `gettersToSpyOn` will not accept | it accepts any key now; for a signal prefer `mockSignalProp` |
185
+ | five `asInstance(…)` in one call, found one per `tsc` run | `...asInstances(a, b, c, d, e)` |
186
+ | `nextWith` demanding `HttpEvent<T>` on a generated client | `asSpy<Client, { overload: 'first' }>(…)` / `Overload<M, 0>` |
187
+ | a fixture that needs a nested object built by its own call | `createMock<T>({ a: { b: 1 } })` — deep partial, still exact |
188
+ | the same 100-line model literal copied into eight specs (`TS1117`) | one `createFixtureFactory<T>(defaults)`; specs call it with what they change |
189
+ | `'params' in link` ladders, or a cast, to pick a union branch | `narrow.byKey(link, 'params')` / `narrow.observable(x)` |
190
+ | `{ ...modelInstance, flag: true }` losing every getter | `withOverrides(modelInstance, { flag: true })` |
191
+ | `NG0303` / `NG0304` / silence from a directive spec | `createDirectiveHost({ template, scope: [Module] })` |
192
+ | "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` |
192
193
 
193
194
  ## Rules that prevent most of the mistakes
194
195