vitest-auto-spy 4.3.0 → 4.5.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
@@ -2317,6 +2328,7 @@ export default [
2317
2328
  | `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2318
2329
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2319
2330
  | `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
2331
+ | `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2320
2332
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2321
2333
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2322
2334
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
@@ -2484,6 +2496,7 @@ packages, which a subpath export can never be.
2484
2496
  | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2485
2497
  | `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
2498
  | `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) |
2499
+ | `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
2500
  | `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
2501
  | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2489
2502
  | 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
@@ -76,7 +76,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
76
76
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
77
77
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
78
78
  - 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
79
- - 📏 Lint rules and one-line test-run hygiene — nineteen rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, seven suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
79
+ - 📏 Lint rules and one-line test-run hygiene — twenty rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, eight suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
80
80
  - 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
81
81
  - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
82
82
  - ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
@@ -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 |
@@ -2949,6 +2961,7 @@ export can never be.
2949
2961
  | `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2950
2962
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2951
2963
  | `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
2964
+ | `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2952
2965
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
2953
2966
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
2954
2967
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
@@ -2958,7 +2971,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
2958
2971
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
2959
2972
  are versioned together and stop being re-written in every project that installs the package.
2960
2973
 
2961
- **Three of the nineteen fix on their own, seven offer suggestions**, and the split is not about how hard
2974
+ **Three of the twenty fix on their own, eight offer suggestions**, and the split is not about how hard
2962
2975
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
2963
2976
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
2964
2977
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -3062,7 +3075,7 @@ package's own — it needs its ESLint integration switched on.
3062
3075
  ### WebStorm and the other JetBrains IDEs
3063
3076
 
3064
3077
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
3065
- all run ESLint natively, so the nineteen rules appear inline, in the **Problems** tool window, and
3078
+ all run ESLint natively, so the twenty rules appear inline, in the **Problems** tool window, and
3066
3079
  under **Code → Inspect Code** for the whole project.
3067
3080
 
3068
3081
  ```js
@@ -3079,7 +3092,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
3079
3092
  the fixes and suggestions live.
3080
3093
 
3081
3094
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
3082
- and then keep a second copy of nineteen rules, in Kotlin, in step with the TypeScript ones.
3095
+ and then keep a second copy of twenty rules, in Kotlin, in step with the TypeScript ones.
3083
3096
 
3084
3097
  ### VS Code, Cursor, Windsurf, VSCodium
3085
3098
 
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.4.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",
@@ -316,6 +316,48 @@ function defineRule(options) {
316
316
  };
317
317
  }
318
318
 
319
+ // src/lib/eslint/dom-reads.ts
320
+ var TEMPLATE_READS = [
321
+ "nativeElement",
322
+ "debugElement",
323
+ "elementRef",
324
+ "querySelector",
325
+ "getComputedStyle",
326
+ "triggerEventHandler",
327
+ "innerHTML",
328
+ "innerText",
329
+ "textContent",
330
+ "getAttribute",
331
+ "classList",
332
+ "shadowRoot",
333
+ "By.css",
334
+ "By.directive"
335
+ ];
336
+ function templatePolicy(context) {
337
+ return Reflect.get(Object(context.options[0]), "templates") === "never" ? "never" : "as-needed";
338
+ }
339
+ function readsRenderedTemplate(source) {
340
+ return TEMPLATE_READS.some((member) => source.includes(member));
341
+ }
342
+ function renderShallowSuggestion(context, node) {
343
+ const [component, ...extra] = node.arguments;
344
+ const state = bindingState(context.sourceCode.getScope(node), "renderShallow");
345
+ if (!component || extra.length > 0 || state === "taken") {
346
+ return void 0;
347
+ }
348
+ const replacement = `renderShallow(${context.sourceCode.getText(component)}).fixture`;
349
+ return {
350
+ desc: `Render without the children and the template: ${replacement}`,
351
+ fix: (fixer) => {
352
+ const edits = [fixer.replaceText(node, replacement)];
353
+ if (state === "free") {
354
+ edits.push(insertImport(fixer, `import { renderShallow } from '${PACKAGE}/angular';`));
355
+ }
356
+ return edits;
357
+ }
358
+ };
359
+ }
360
+
319
361
  // src/lib/eslint/floating-assertion.ts
320
362
  var PROMISE_CALLBACK_METHODS = /* @__PURE__ */ new Set(["catch", "finally", "then"]);
321
363
  function isPromiseCallback(fn) {
@@ -1609,6 +1651,32 @@ var noUnregisteredInjectSpy = defineRule({
1609
1651
  };
1610
1652
  }
1611
1653
  });
1654
+ var preferRenderShallow = defineRule({
1655
+ anchor: "-a-components-children",
1656
+ description: "Render through renderShallow() when the spec never reads the rendered template",
1657
+ hasSuggestions: true,
1658
+ schema: [{ type: "object", properties: { templates: { enum: ["as-needed", "never"] } }, additionalProperties: false }],
1659
+ messages: {
1660
+ keepTemplate: '`keepTemplate: true` puts the real template back, and this project set `{ templates: "never" }`. Drop it \u2014 or, when the component genuinely reads its own template through `viewChild` or content projection, silence this line, because the alternative is a spec that cannot reach the component at all.',
1661
+ preferRenderShallow: "`TestBed.createComponent` pays for compiling the template and instantiating the whole child subtree, and nothing in this file reads either \u2014 no `nativeElement`, no `debugElement`, no `By.css`, no `querySelector`. `renderShallow(X)` brings the same component up through the same `TestBed` with the children dropped and the template blank, and leaves inputs, signals, lifecycle hooks and DI exactly where they were; `fixture` is still a real `ComponentFixture`. What it buys is measured in `bench-angular/`: **0.24\xD7** the per-test cycle at 100 children and **0.05\xD7** at 400 \u2014 but at **zero** children the two are level (measurements straddle 1.0, and it is the noisiest row in the benchmark), so a leaf component gains nothing and this report is worth ignoring there. Reach for `{ keepTemplate: true }` when the component reads its own template through `viewChild` or content projection, which this rule cannot see from the spec."
1662
+ },
1663
+ create: (context) => ({
1664
+ 'CallExpression[callee.object.name="TestBed"][callee.property.name="createComponent"]': (node) => {
1665
+ if (templatePolicy(context) === "as-needed" && readsRenderedTemplate(context.sourceCode.getText())) {
1666
+ return;
1667
+ }
1668
+ const suggestion = renderShallowSuggestion(context, node);
1669
+ context.report(
1670
+ suggestion ? { node, messageId: "preferRenderShallow", suggest: [suggestion] } : { node, messageId: "preferRenderShallow" }
1671
+ );
1672
+ },
1673
+ 'Property[key.name="keepTemplate"][value.value=true]': (node) => {
1674
+ if (templatePolicy(context) === "never") {
1675
+ context.report({ node, messageId: "keepTemplate" });
1676
+ }
1677
+ }
1678
+ })
1679
+ });
1612
1680
  var rules = {
1613
1681
  "prefer-provide-auto-spy": preferProvideAutoSpy,
1614
1682
  "prefer-create-spy-from-class": preferCreateSpyFromClass,
@@ -1625,6 +1693,7 @@ var rules = {
1625
1693
  "no-inject-before-override": noInjectBeforeOverride,
1626
1694
  "no-import-time-spread": noImportTimeSpread,
1627
1695
  "no-unregistered-inject-spy": noUnregisteredInjectSpy,
1696
+ "prefer-render-shallow": preferRenderShallow,
1628
1697
  ...jasmineRules
1629
1698
  };
1630
1699
 
@@ -1646,6 +1715,7 @@ var recommendedRules = {
1646
1715
  [`${PLUGIN_NAME}/no-inject-before-override`]: "error",
1647
1716
  [`${PLUGIN_NAME}/no-import-time-spread`]: "error",
1648
1717
  [`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "error",
1718
+ [`${PLUGIN_NAME}/prefer-render-shallow`]: "error",
1649
1719
  [`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
1650
1720
  [`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
1651
1721
  [`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
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.5.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
 
@@ -322,7 +323,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
322
323
  Most of this library's guarantees are type-level, so a green run that does not type-check is not
323
324
  done. Report failures with their output rather than describing them as passing.
324
325
 
325
- **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The nineteen rules in
326
+ **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The twenty rules in
326
327
  `vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
327
328
  to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
328
329
  evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —