vitest-auto-spy 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +304 -153
  2. package/README.md +198 -92
  3. package/dist/angular-http.d.ts +4 -3
  4. package/dist/angular-http.js +46 -16
  5. package/dist/angular.d.ts +2 -2
  6. package/dist/angular.js +245 -95
  7. package/dist/bun-angular.d.ts +2 -2
  8. package/dist/bun-angular.js +26 -12
  9. package/dist/bun.d.ts +12 -2
  10. package/dist/bun.js +7 -6
  11. package/dist/{chunk-6YI3G3CH.js → chunk-4XY4DEM5.js} +2 -2
  12. package/dist/{chunk-EGU3YJJN.js → chunk-APSBHO3I.js} +13 -2
  13. package/dist/{chunk-QY4UO4BB.js → chunk-AVVUXEEI.js} +47 -9
  14. package/dist/{chunk-75GDWUIT.js → chunk-B4Z3CFDJ.js} +2 -19
  15. package/dist/{chunk-NLDPCJKC.js → chunk-C5EPUC3A.js} +4 -4
  16. package/dist/{chunk-JOODFKWS.js → chunk-GSWNTF5E.js} +20 -10
  17. package/dist/{chunk-DBDXQXS3.js → chunk-PBLYL3Q7.js} +62 -8
  18. package/dist/chunk-RZRKEUE6.js +15 -0
  19. package/dist/{chunk-OYQ2PHBU.js → chunk-SWXAPWFW.js} +1 -1
  20. package/dist/{chunk-5AOJI3BM.js → chunk-X57WQSOC.js} +1 -1
  21. package/dist/{chunk-WWO6CUEJ.js → chunk-YL7EQKZU.js} +1 -1
  22. package/dist/cli.js +53 -20
  23. package/dist/console.d.ts +8 -9
  24. package/dist/console.js +45 -20
  25. package/dist/dom-stubs.js +21 -2
  26. package/dist/eslint-plugin.cjs +610 -141
  27. package/dist/eslint-plugin.d.cts +7 -0
  28. package/dist/eslint-plugin.d.ts +7 -0
  29. package/dist/{expect-emission-DYtyyKM5.d.ts → expect-emission-C0xY_mYK.d.ts} +2 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +156 -37
  32. package/dist/jasmine.js +10 -9
  33. package/dist/nestjs.js +8 -7
  34. package/dist/node.cjs +85 -29
  35. package/dist/node.d.ts +1 -1
  36. package/dist/node.js +7 -6
  37. package/dist/react.d.ts +1 -1
  38. package/dist/react.js +8 -7
  39. package/dist/rstest.d.ts +1 -1
  40. package/dist/rstest.js +8 -7
  41. package/dist/rxjs.js +1 -1
  42. package/dist/setup.d.ts +231 -106
  43. package/dist/setup.js +457 -65
  44. package/dist/svelte.d.ts +1 -1
  45. package/dist/svelte.js +8 -7
  46. package/dist/vue.d.ts +1 -1
  47. package/dist/vue.js +9 -8
  48. package/dist/{zoneless-DM1AWQV2.d.ts → zoneless-DnjTTKlC.d.ts} +7 -3
  49. package/package.json +1 -1
  50. package/skills/vitest-auto-spy/SKILL.md +89 -77
package/AGENTS.md CHANGED
@@ -56,7 +56,7 @@ Add-ons, orthogonal to the runner:
56
56
  | ----------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
57
  | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, and in a file the `tsconfig` includes (§4) |
58
58
  | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
59
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
59
+ | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
60
60
  | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
61
61
  | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
62
62
  | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
@@ -643,9 +643,9 @@ Also true, and worth not re-deriving:
643
643
  for `@ts-expect-error`: one migration wrote sixty of those across twenty-five files before anyone
644
644
  found this option, and each one stops checking the response shape that line exists to describe.
645
645
 
646
- **Name the method rather than the whole double.** `'first'` on the type moves *every* overloaded
646
+ **Name the method rather than the whole double.** `'first'` on the type moves _every_ overloaded
647
647
  member, and on a wide type that breaks the ones nobody was fixing — `Spy<Response, { overload:
648
- 'first' }>` for one method collected five `TS2769`s on `download`. `overload` also takes a map:
648
+ 'first' }>` for one method collected five `TS2769`s on `download`. `overload` also takes a map:
649
649
 
650
650
  ```ts
651
651
  let perf: Spy<Performance, { overload: { getEntriesByType: 'first' } }>;
@@ -720,7 +720,9 @@ users.currentTenant(); // throws here, not four frames later inside the componen
720
720
  Called as: Cart.checkout(1,'now')
721
721
  ```
722
722
 
723
- Also on `createAutoMock`, `provideAutoSpy`, and suite-wide as `setupAutoSpy({ strict: true })`.
723
+ Also on `createAutoMock`, `provideAutoSpy`, and suite-wide as `setupAutoSpy({ strict: true })` — which
724
+ reaches a double whichever bundle of the package built it: the default lives on `globalThis`. Before
725
+ this release it lived in module scope, and a setup file's `strict: true` reached no double a spec built.
724
726
  Precedence, first one set wins: the double's `onUnstubbedCall` → the global `onUnstubbedCall` → the
725
727
  double's `strict` (**including `strict: false`**, the only way to exempt one double from a suite-wide
726
728
  default) → the global `strict`.
@@ -734,6 +736,12 @@ default) → the global `strict`.
734
736
  - **It does not reach** accessor spies, observable-property spies, `mockDeep` nodes, `console-spy`,
735
737
  `mockResourceProp`'s `reload` or a standalone `createFunctionSpy`. A strict double still answers
736
738
  `undefined` for an unconfigured getter or `items$`. `fillMissing` members **are** covered.
739
+ - **Angular lifecycle hooks are exempt.** `ngOnDestroy`, `ngOnInit`, `ngOnChanges`, `ngDoCheck` and the
740
+ `ngAfter…` hooks answer `undefined` on a strict double: Angular calls `ngOnDestroy` itself on every
741
+ provided value at teardown (a `createAutoMock` / `provideAutoSpyForToken` double always has one), and
742
+ a throw there broke the teardown for every test after it. The calls are still recorded.
743
+ - A class instance or DOM node in the `Called as:` line prints as `[ClassName]`, data up to 200
744
+ characters per argument; `provideAutoSpyForToken` names the token (`createAutoMock` takes `name`).
737
745
  - Use `onUnstubbedCall` to survey a suite before turning the throw on — it is handed
738
746
  `{ className, method, args }` and whatever it returns is what the call answers, so a handler that
739
747
  pushes to an array records the gap without failing anything.
@@ -823,7 +831,7 @@ carry (an index signature makes inference drop the default) and the union in `in
823
831
  parameter (a union does too — a class now matches a bare construct-signature overload first).
824
832
  Spelling the argument out still works and is still the answer when the class has **no** default.
825
833
 
826
- **A generic *method* is a different thing, and it does collapse.** `show<T, U>(component: Type<T>,
834
+ **A generic _method_ is a different thing, and it does collapse.** `show<T, U>(component: Type<T>,
827
835
  data: U)` on a modal or factory service reaches `Spy<T>` through `Parameters` / `ReturnType`, which
828
836
  instantiate the method's own type parameters — so the double types as `show(component: Type<unknown>,
829
837
  data: unknown)`. No mapped type in TypeScript can preserve a generic signature, so this is a limit
@@ -1046,6 +1054,29 @@ property. That is how you add an Observable member that `provideAutoSpy` did not
1046
1054
  What the second overload costs is the property-name check, so a typo in the name compiles. Nothing
1047
1055
  checks the _value_ on either overload; that is deliberate, and the partial fixture above is why.
1048
1056
 
1057
+ **A read the constructor does cannot be seeded by any of these.** They patch an object that already
1058
+ exists, so the earliest they can run is after the double is built — and a component that reads
1059
+ `service.paymentParams.offer` in a field initializer or in its constructor has already read it by the
1060
+ time `TestBed.createComponent` returns:
1061
+
1062
+ ```ts
1063
+ fixture = TestBed.createComponent(PaymentComponent); // ← TypeError thrown here, inside the component
1064
+ mockReadonlyProp(paymentService, 'paymentParams', params); // never reached
1065
+ ```
1066
+
1067
+ Nothing warns, because nothing of this library runs: the throw is a plain `TypeError` from the
1068
+ component's own line, and the stack names the component rather than the seeding that is missing. The
1069
+ only place early enough is the registration:
1070
+
1071
+ ```ts
1072
+ providers: [provideAutoSpy(PaymentService, { overrides: { paymentParams: params } })];
1073
+ providers: [provideAutoSpyForToken(STATE_TOKEN, { snapshot })]; // a token's second argument does the same
1074
+ ```
1075
+
1076
+ Three migration shards found this independently, each of them after the failure had pointed at the
1077
+ service. A `mock*Prop` after the render is right for everything a template or a method reads later —
1078
+ which is most of them — and wrong only for a constructor-time read.
1079
+
1049
1080
  ### Properties of DOM objects — the same helpers, and the reason to look for them
1050
1081
 
1051
1082
  `document.fullscreenElement`, `document.visibilityState`, `document.cookie`, `navigator.userAgent`,
@@ -1114,23 +1145,28 @@ setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors
1114
1145
  ```
1115
1146
 
1116
1147
  The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
1117
- `cancelStrayTimers()` (returns how many it cancelled) and `countStrayTimers()`, all from
1118
- `vitest-auto-spy/setup`. Use `expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak
1119
- fail rather than be tidied away, or take the per-file count from the sweep itself:
1148
+ `cancelStrayTimers()` (returns how many it cancelled), `countStrayTimers()` and
1149
+ `describeStrayTimers()` (each pending timer's kind, the spec file that scheduled it and up to five
1150
+ frames), all from `vitest-auto-spy/setup`. Use `expect(countStrayTimers()).toBe(0)` in an `afterEach`
1151
+ to make a leak fail rather than be tidied away, or take the per-file report from the sweep itself —
1152
+ `timers` carries the same origins, so the failure diff names the file and the scheduling call:
1120
1153
 
1121
1154
  ```ts
1122
- setupAutoSpy({ strayTimers: true, onStrayTimers: ({ cancelled }) => expect(cancelled).toBe(0) });
1155
+ setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).toEqual([]) });
1123
1156
  ```
1124
1157
 
1158
+ A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
1159
+ the previous file is the one to fix.
1160
+
1125
1161
  **With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
1126
1162
  arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
1127
1163
  Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
1128
1164
  reports **no leaks** for a file that leaks. Cancelling is still the right default — a callback firing
1129
1165
  during a later file is the more expensive failure — so when both are on and no `onStrayTimers` is
1130
- given, the sweep prints one line to stderr saying how many it took away. To see _where_ each one was
1131
- scheduled, re-run that file with `strayTimers` off and read Vitest's report; the code frame points
1132
- at the `setTimeout` in the spec, because the library's own scheduler wrappers go through
1133
- `vi.defineHelper` and are dropped from the stack.
1166
+ given, the sweep prints one line to stderr saying how many it took away and where the first three
1167
+ were scheduled; `onStrayTimers` gets all of them. Vitest's own report, with `strayTimers` off, points
1168
+ its code frame at the `setTimeout` in the spec, because the library's own scheduler wrappers go
1169
+ through `vi.defineHelper` and are dropped from the stack.
1134
1170
 
1135
1171
  **The one that keeps a suite green while it is wrong:** zone.js replaces the global `Promise`, and a
1136
1172
  rejection nobody handled is drained into `console.error` and no further — it never reaches
@@ -1504,6 +1540,84 @@ worker inherits it, and what fails is some library, every other run, with nothin
1504
1540
  that did it. The guard compares `globalThis` / `document` / `navigator` around every test and reports
1505
1541
  only what appeared and cannot be removed.
1506
1542
 
1543
+ ### Naming the file that polluted `Object.prototype`
1544
+
1545
+ ```ts
1546
+ setupAutoSpy(); // prototypePollution: 'throw' by default — 'warn' sweeps and only reports
1547
+ ```
1548
+
1549
+ Vitest walks a file's hooks with `for…in` in `mergeHooks`, so one own enumerable key on
1550
+ `Object.prototype` is spread as if it were an array during **collect**:
1551
+ `TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`, with no stack —
1552
+ every frame of it is filtered out by `stackIgnorePatterns`. Under `isolate: false` the key outlives
1553
+ its file, so **every later spec file in that worker fails to collect** while the run still reports
1554
+ success: `145 failed | 1613 passed` over `11880 passed | 0 failed`, zero failing tests because those
1555
+ files never ran. The guard compares `Object.prototype` / `Array.prototype` / `Function.prototype`
1556
+ around every test, takes the key back off and names the file. The write is nearly always accidental:
1557
+ `Object.getPrototypeOf(instance)` **is** `Object.prototype` when `instance` is an object literal from
1558
+ a `useValue` provider or a test double — patch the prototype of the class the object came from.
1559
+ `guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
1560
+ that does not call `setupAutoSpy()`.
1561
+
1562
+ ### Failing on console output nothing absorbed
1563
+
1564
+ ```ts
1565
+ setupAutoSpy({ strayConsole: 'throw' }); // default 'off'; 'warn' prints the same report without failing
1566
+ ```
1567
+
1568
+ Any call to a console method that writes (`log`, `info`, `warn`, `error`, `debug`, `trace`, `table`,
1569
+ `dir`, `dirxml`, `timeLog`, `timeEnd`, `count`; `group` with a label; a falsy `assert`) made during a
1570
+ test that nothing absorbed fails **that test**, quoting the method, the first three lines and the first
1571
+ frame outside `node_modules`:
1572
+
1573
+ ```
1574
+ [vitest-auto-spy] "CartService > reports a failed load" wrote to the console 1 time(s) and nothing absorbed it:
1575
+ - console.error: Error: load failed
1576
+ at CartService.load (src/app/cart.service.ts:41:15)
1577
+ ```
1578
+
1579
+ - **Absorbed** means the call never reached the console: a `/console` spy installed for the test or
1580
+ the file, `vi.spyOn(console, m).mockImplementation(…)`, any replacement that does not call
1581
+ through. A bare `vi.spyOn(console, m)` calls through and **counts**.
1582
+ - Output **outside any test** — collection/import, `beforeAll` / `afterAll`, a callback after its
1583
+ test ended — fails the **file** in `afterAll` (`… outside any test …`).
1584
+ - Every console method a test replaced is put back after it; one a file replaced, after the file.
1585
+ - Under the guard, **importing `vitest-auto-spy/console` installs nothing** — call
1586
+ `installConsoleSpies()` in a `beforeEach` (or at the top of the file). Spies an import installed
1587
+ before the guard armed are taken off.
1588
+ - The library's own warnings are console output: under the guard they fail the test that caused them.
1589
+ - `strayConsole: { allow: ['…', /…/] }` is the last resort, for environment noise no spec can reach.
1590
+ Output written to `process.stdout` / `process.stderr` directly, and jsdom's own virtual console, is
1591
+ not seen.
1592
+ - The wrapper forwards every call unchanged, so Vitest's attribution and `onConsoleLog` still work.
1593
+ `guardStrayConsole(reaction)` registers the same guard on its own.
1594
+
1595
+ ### One grade for everything — `preset: 'strict'`
1596
+
1597
+ ```ts
1598
+ setupAutoSpy({ preset: 'strict' });
1599
+ ```
1600
+
1601
+ Sets `duplicateCopies`, `propsOutsideHooks`, `guardGlobals`, `prototypePollution`, `strayConsole` and
1602
+ `misconfiguration` to `'throw'`, turns `strayTimers` on, and `strayRejections` on when zone.js is
1603
+ loaded. An option passed alongside still wins. **Not** included: `strict` (strict doubles change what
1604
+ an unconfigured call returns — a semantic switch, not a grade; the name was taken, hence `preset`),
1605
+ `blockNetwork` (changes the code under test), `restoreMocks` (drops `beforeAll` spies), failing on
1606
+ stray-timer counts (the sweep fails the file from `afterAll`, and a callback scheduled after the
1607
+ previous file's sweep is charged to the next — opt in with
1608
+ `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`, whose diff names each one's file), and
1609
+ `enableAngularDiagnostics()`, which lives in `/angular` — call it in the same setup file as the Angular half of strict.
1610
+
1611
+ `misconfiguration: 'throw'` on its own makes the library's misuse reports — an `onlyMethodsToSpyOn`
1612
+ typo, `gettersToSpyOn` naming a method, a `returns` key no spy answers to, `injectSpy` handed a real
1613
+ instance, a `jasmine.DEFAULT_TIMEOUT_INTERVAL` write, `providedMethodNames` — throw at the call site,
1614
+ every occurrence. The grade is process-wide and released after the file. The printed grade of the
1615
+ `injectSpy` warning is de-duplicated per token **per spec file**, no longer per worker.
1616
+
1617
+ `withoutStrayTimerTracking(work)` runs setup work whose timers `strayTimers` neither counts nor
1618
+ cancels — jsdom answers every Web Storage write with a real `setTimeout(…, 0)`, and the library's own
1619
+ storage probe runs under it.
1620
+
1507
1621
  ### Hook order differs from Jest
1508
1622
 
1509
1623
  Vitest runs `afterEach` hooks as a stack (innermost / last-registered first); Jest ran them in
@@ -1980,13 +2094,13 @@ import { enableAngularDiagnostics } from 'vitest-auto-spy/angular';
1980
2094
  enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders, pendingRequests }
1981
2095
  ```
1982
2096
 
1983
- | Member | Fails when |
1984
- | ------------------ | -------------------------------------------------------------------------------- |
1985
- | `ngModuleScopes` | a testing module imports an NgModule that contributes nothing at runtime |
1986
- | `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
1987
- | `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
1988
- | `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one |
1989
- | `shadowedProviders` | a double on the testing module loses to the component's own `providers` |
2097
+ | Member | Fails when |
2098
+ | ------------------- | -------------------------------------------------------------------------------- |
2099
+ | `ngModuleScopes` | a testing module imports an NgModule that contributes nothing at runtime |
2100
+ | `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
2101
+ | `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
2102
+ | `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one |
2103
+ | `shadowedProviders` | a double on the testing module loses to the component's own `providers` |
1990
2104
 
1991
2105
  Every member defaults to `true`, takes `false` to opt out, and a second call **replaces** the
1992
2106
  selection (safe from anywhere, including inside a test). `disableAngularDiagnostics()` turns the
@@ -2439,15 +2553,29 @@ mount(Greeting, { global: { provide } });
2439
2553
  import { createSpyFromClass } from 'vitest-auto-spy/react';
2440
2554
  ```
2441
2555
 
2442
- Console spies — importing the entry replaces `console.debug` / `error` / `info` / `log` / `time` /
2443
- `timeEnd` / `trace` / `warn` with silent typed spies, named `console<Method>Spy`:
2556
+ Console spies — `installConsoleSpies()` replaces `console.debug` / `error` / `info` / `log` / `time` /
2557
+ `timeEnd` / `trace` / `warn` with silent typed spies, named `console<Method>Spy`. Install them per test
2558
+ and take them off after it:
2444
2559
 
2445
2560
  ```ts
2446
- import { consoleInfoSpy, consoleWarnSpy, resetConsoleSpies, restoreConsole } from 'vitest-auto-spy/console';
2561
+ import { type ConsoleSpies, installConsoleSpies, restoreConsole } from 'vitest-auto-spy/console';
2562
+
2563
+ let consoleSpies: ConsoleSpies;
2447
2564
 
2448
- expect(consoleInfoSpy).toHaveBeenCalledWith('done'); // the output is silenced, not printed
2565
+ beforeEach(() => {
2566
+ consoleSpies = installConsoleSpies();
2567
+ });
2568
+ afterEach(() => restoreConsole());
2569
+
2570
+ expect(consoleSpies.consoleInfoSpy).toHaveBeenCalledWith('done'); // the output is silenced, not printed
2449
2571
  ```
2450
2572
 
2573
+ The exported `consoleInfoSpy` & co. are the same objects; `restoreConsole()` keeps them and clears
2574
+ their calls. **Do not rely on the import to install them**: it does so once per worker, so under
2575
+ `isolate: false` the spies go on in whichever file imported them first and silence every later file
2576
+ (`no-import-time-console-spies` reports it). Under `setupAutoSpy({ strayConsole })` the import installs
2577
+ nothing at all.
2578
+
2451
2579
  Import your runtime entry (`…/bun`, `…/node`) **before** `…/console`, or it registers the Vitest
2452
2580
  adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives a detached one.
2453
2581
 
@@ -2481,43 +2609,56 @@ export default [
2481
2609
  errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
2482
2610
  blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
2483
2611
 
2484
- | Rule | Level | Fix | Flags |
2485
- | --------------------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
2486
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
2487
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
2488
- | `prefer-provide-auto-spy` | `error` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
2489
- | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
2490
- | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
2491
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
2492
- | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
2493
- | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
2494
- | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
2495
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
2496
- | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
2497
- | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
2498
- | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
2499
- | `no-private-member-access` | `error` | — | `instance['privateMember']`, `(instance as any).privateMember` (and `as unknown as`, and a decoy type), `vi.spyOn(Object.getPrototypeOf(x), 'm')` → drive the member through the public API. **Type-aware**: silent without `parserOptions.project`, and silent on an index signature |
2500
- | `no-dead-schemas` | `error` | — | `schemas` on a testing module with no `declarations` — the schema applies to nothing; the file decides, so a `declarations` in another `configureTestingModule` call silences it |
2501
- | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2502
- | `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 |
2503
- | `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2504
- | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
2505
- | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2506
- | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2507
- | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
2508
- | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
2509
-
2510
- Twenty-three rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`**; three fix on
2511
- their own, eight offer suggestions. Twenty-two are syntactic; `no-private-member-access` is the one
2612
+ | Rule | Level | Fix | Flags |
2613
+ | --------------------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2614
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
2615
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
2616
+ | `prefer-provide-auto-spy` | `error` | — | a hand-rolled `useValue`, `useFactory`, `useClass` or `useExisting` (also `useValue: new StubMock()`), in a provider **or** a `TestBed.overrideProvider(X, { … })` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`. A name in the slot is followed one step, through an initialiser or a single `beforeEach` assignment |
2617
+ | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
2618
+ | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy` and delete the stub; a decorated, `implements`-ing, `extends`-ing or unnamed class is exempt — option: `{ minRunnerFns }` |
2619
+ | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`; a bare `let fn: Mock` is a callback and never reported — option: `{ minRunnerFns }` |
2620
+ | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
2621
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
2622
+ | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
2623
+ | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
2624
+ | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
2625
+ | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
2626
+ | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
2627
+ | `no-overridden-provider` | `error` | suggest | two providers for one token in one array, or one a `TestBed.overrideProvider` in the same hook replaces → the earlier one never runs; the exact duplicate can be deleted |
2628
+ | `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
2629
+ | `no-private-member-access` | `error` | — | `instance['privateMember']`, `(instance as any).privateMember` (and `as unknown as`, and a decoy type), `vi.spyOn(Object.getPrototypeOf(x), 'm')` → drive the member through the public API. **Type-aware**: silent without `parserOptions.project`, and silent on an index signature |
2630
+ | `no-dead-schemas` | `error` | — | `schemas` on a testing module with no `declarations` — the schema applies to nothing; the file decides, so a `declarations` in another `configureTestingModule` call silences it |
2631
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2632
+ | `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 |
2633
+ | `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2634
+ | `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
2635
+ | `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
2636
+ | `no-console-in-spec` | `error` | — | a spec calling `console.x(…)` itself, or `console.x = …`, which nothing restores → absorb the code's output through `vitest-auto-spy/console` |
2637
+ | `no-import-time-console-spies` | `error` | — | an import of `vitest-auto-spy/console` in a file that never calls `installConsoleSpies()` — the import installs once per worker and silences every later file → `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` |
2638
+ | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2639
+ | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2640
+ | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
2641
+ | `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
2642
+
2643
+ Twenty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
2644
+ `no-stub-class-double` and `no-structural-double`**; three fix on their own, nine offer suggestions. Twenty-seven are syntactic; `no-private-member-access` is the one
2512
2645
  that reads types, and it reports nothing at all without `parserOptions.project` / `projectService`
2513
2646
  rather than guessing. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
2514
- consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The one
2515
- `warn` left is not a judgement about how much that finding matters but about what kind of finding it
2516
- is: every other rule names something wrong or dead, while `prefer-render-shallow` names a file that
2517
- could render more cheaply, and `renderShallow` is a migration a suite either takes or does not. At
2518
- `error` the plugin would gate that migration — 491 findings across 398 of one consumer's 1759 spec
2519
- files — so `recommended` would exist to be overridden. Set it to `'error'` once the project has decided
2520
- to make the move. Three of
2647
+ consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The
2648
+ three `warn`s left are not judgements about how much a finding matters. `prefer-render-shallow` is
2649
+ about the _kind_ of finding: every other rule names something wrong or dead, while this one names a
2650
+ file that could render more cheaply, and `renderShallow` is a migration a suite either takes or does
2651
+ not. At `error` the plugin would gate that migration — 491 findings across 398 of one consumer's
2652
+ 1759 spec files — so `recommended` would exist to be overridden. `no-stub-class-double` and
2653
+ `no-structural-double` (5.5.0) are about the _evidence_: both report the same drift
2654
+ `prefer-create-spy-from-class` reports at `error`, but neither has a `provide:` beside it to settle
2655
+ the question, so each decides on a heuristic — and a project that disagrees with the reading has to
2656
+ be able to switch it off without losing the rule that reads a count. The counts are not the argument
2657
+ and moved a long way inside the release: measured on the same 1759 files, the two started at 12
2658
+ reports in 8 files and 115 in 74, and are 10 in 7 and 5 in 4 once `prefer-provide-auto-spy` learnt to
2659
+ follow a name into a `useValue` — 112 of those doubles are handed to Angular DI one name away, where
2660
+ a `provide:` settles it. That rule reports **154 times across 87 files** on the same suite, all at
2661
+ `error`. Set any of the three `warn`s to `'error'` once the batch is done. Three of
2521
2662
  them can report on a _correct_ project, and only one has an option:
2522
2663
  `jasmine-namespace-without-entry` takes `['error', { setupModules: ['./test-setup'] }]`, naming the
2523
2664
  file where `enableJasmineCompat()` is called; `prefer-native-spy-api` goes `'off'` for as long as a
@@ -2640,98 +2781,106 @@ packages, which a subpath export can never be.
2640
2781
 
2641
2782
  ## 17. Error → fix
2642
2783
 
2643
- | Message contains | Cause | Fix |
2644
- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
2645
- | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
2646
- | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
2647
- | `Type 'SubjectLike<T>' is not assignable to type 'Subject<T>'` | 4.0.0: `returnSubject()` is rxjs-typed only where `vitest-auto-spy/rxjs` is in the TypeScript program, and it is not in this one | put `import 'vitest-auto-spy/rxjs';` in a file the spec `tsconfig` includes — a `setupFiles` entry outside `include` is the usual cause |
2648
- | `Cannot read properties of undefined (reading 'returnValue')` on `spy.m.and.…` | the jasmine namespaces are not installed — the spy was built before them, or nothing installed them at all | import from `vitest-auto-spy/jasmine`; on Bun / `node:test`, `enableJasmineCompat()` in the setup file (§20) |
2649
- | `spy.withArgs is not a function` | the same, seen from the argument-matching side | the same — or write `spy.m.calledWith(a).mockReturnValue(v)`, which needs no layer (§20) |
2650
- | `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
2651
- | `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
2652
- | a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
2653
- | `names method(s) that are not on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2654
- | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
2655
- | `toHaveBeenCalledTimes` reads `0` for a call an earlier test made, after moving to Vitest 5 | `clearMocks` defaults to `true` on Vitest 5, so `vi.clearAllMocks()` runs before every test | count the calls in a plain variable instead of reading the spy, or set `clearMocks: false` (§10) |
2656
- | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2657
- | `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
2658
- | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2659
- | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
2660
- | `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
2661
- | `Cannot spy on this instance in place: it is not extensible` | `createSpyFromInstance` was handed a frozen or sealed object | spy it before it is frozen, or build a separate double with `createSpyFromClass` (§2) |
2662
- | `Cannot mock the property 'X': it is not configurable` | the member was defined with `configurable: false`, so it cannot be redefined | members replaced before it are still restorable — `restoreSpiedInstance(instance)` or `restoreMockedProps()` |
2663
- | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2664
- | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2665
- | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
2666
- | `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
2667
- | `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
2668
- | `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
2669
- | `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
2670
- | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
2671
- | `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
2672
- | `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
2673
- | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
2674
- | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
2675
- | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
2676
- | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
2677
- | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
2678
- | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
2679
- | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2680
- | `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 |
2681
- | `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) |
2682
- | `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) |
2683
- | `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) |
2684
- | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2685
- | 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) |
2686
- | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
2687
- | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
2688
- | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
2689
- | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
2690
- | `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
2691
- | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
2692
- | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
2693
- | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
2694
- | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
2695
- | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
2696
- | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
2697
- | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
2698
- | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
2699
- | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
2700
- | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
2701
- | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
2702
- | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
2703
- | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
2704
- | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
2705
- | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
2706
- | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
2707
- | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
2708
- | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
2709
- | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
2710
- | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
2711
- | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
2712
- | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
2713
- | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
2714
- | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
2715
- | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
2716
- | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
2717
- | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic and keeps the `readonly` of the source member — deliberately: on a member replaced by a **spied accessor** the assignment reaches the setter spy and the getter still answers `undefined`, so the type error is the loud version of a silent no-op | `mockValueProp(spy, 'X', value)` — it defines rather than assigns, so it works in both cases; `Mutable<Spy<T>>` is the opt-in for plain data members, and `Reflect.set` is **not** a way round it |
2718
- | `TS2345` from `mockReturnValue` / `mockImplementation` / `mockResolvedValue` on a double that used to compile | the mock surface is `MockInstance<Method>`, so the stub is now checked against the method's own return type | stub with what the method returns; if the double is meant to answer something else, the method's type or the spec's expectation is wrong |
2719
- | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
2720
- | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
2721
- | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
2722
- | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
2723
- | a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
2724
- | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
2725
- | a green test whose only line about a call is `spy.m.calledWith(1);` | that is a **stub**, not an assertion — chai's `expect(fn).to.have.been.calledWith(x)` is the one that checks | continue the chain, or `expect(spy.m).toHaveBeenCalledWith(1)` — `no-bare-called-with` (§16) |
2726
- | `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | re-run that file with `strayTimers` off; the warning on stderr says how many were taken (§10) |
2727
- | `createNestUnit(X): parameter #n of Y is typed as \`Object\``(or`undefined — usually a circular import`) | the compiler emitted no runtime class for that parameter — interface, union, primitive, or a circular import | `@Inject(TOKEN)` plus a `providers` entry, or `@Optional()` to receive `undefined`; `@Inject(forwardRef(() => X))` for the import cycle |
2728
- | `createNestUnit(X): Y declares n constructor parameter(s) but carries no design:paramtypes metadata` | no `emitDecoratorMetadata` (esbuild / Vite), or `reflect-metadata` loaded after the class | tsc or SWC with `decoratorMetadata`, `@Injectable()`, `reflect-metadata` first; or `@Inject(X)` on every parameter |
2729
- | `createNestUnit(X): A -> B -> A is a cycle among the classes built for real` | two exposed / `useClass` classes depend on each other; the helper does not resolve `forwardRef` cycles | expose one side less, or provide it |
2730
- | `createNestUnit(X).spies.get(Y): the unit never asked for that token` | same guard as `createWithAutoSpies` — the spy would not be the one the unit uses | stub the token the unit actually injects (the message lists the auto-spied ones) |
2731
- | `createNestUnit(X).spies.get(Y): Y is in \`expose\`, so the unit got a real instance` | an exposed class is real, not a spy | read the spies of its own collaborators, or drop it from `expose` |
2732
- | `no request matched …` (with the requests that were made listed) | the URL, verb or predicate does not describe anything the code under test sent, or nothing subscribed | read the list in the message — it is the requests that were actually made; then fix the URL, add `{ method }`, or subscribe (§13) |
2733
- | `this TestBed has no HttpTestingController` | `expectRequest` / `expectNoRequest` on a module that never got the testing backend | `TestBed.configureTestingModule({ providers: [...provideHttpTesting()] })` from `vitest-auto-spy/angular-http` (§13) |
2734
- | `the test ended with N unanswered request(s)` | `provideHttpTesting({ verifyOnTeardown })` found a request the spec never flushed | answer it (`await expectRequest(url).flush(body)`), assert its absence (`expectNoRequest(url)`), or `{ verifyOnTeardown: false }` (§13) |
2784
+ | Message contains | Cause | Fix |
2785
+ | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2786
+ | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
2787
+ | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
2788
+ | `Type 'SubjectLike<T>' is not assignable to type 'Subject<T>'` | 4.0.0: `returnSubject()` is rxjs-typed only where `vitest-auto-spy/rxjs` is in the TypeScript program, and it is not in this one | put `import 'vitest-auto-spy/rxjs';` in a file the spec `tsconfig` includes — a `setupFiles` entry outside `include` is the usual cause |
2789
+ | `Cannot read properties of undefined (reading 'returnValue')` on `spy.m.and.…` | the jasmine namespaces are not installed — the spy was built before them, or nothing installed them at all | import from `vitest-auto-spy/jasmine`; on Bun / `node:test`, `enableJasmineCompat()` in the setup file (§20) |
2790
+ | `spy.withArgs is not a function` | the same, seen from the argument-matching side | the same — or write `spy.m.calledWith(a).mockReturnValue(v)`, which needs no layer (§20) |
2791
+ | `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
2792
+ | `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
2793
+ | a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
2794
+ | `names method(s) that are not on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2795
+ | `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
2796
+ | `toHaveBeenCalledTimes` reads `0` for a call an earlier test made, after moving to Vitest 5 | `clearMocks` defaults to `true` on Vitest 5, so `vi.clearAllMocks()` runs before every test | count the calls in a plain variable instead of reading the spy, or set `clearMocks: false` (§10) |
2797
+ | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2798
+ | `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
2799
+ | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2800
+ | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
2801
+ | `[vitest-auto-spy] "…" wrote to the console N time(s) and nothing absorbed it` | `setupAutoSpy({ strayConsole })` — the test printed, and nothing that does not call through stood on `console`: no `/console` spy installed for it, no `mockImplementation`. A bare `vi.spyOn(console, m)` calls through, and a `console.warn` from this library counts too| absorb and assert: `installConsoleSpies()` in a `beforeEach`, then `expect(consoleErrorSpy)…`; or fix the code that printed. Importing a spy installs nothing under the guard. `strayConsole: { allow: [...] }` only for environment noise no spec can reach |
2802
+ | `… wrote to the console N time(s) outside any test` | output while the file was imported, in a `beforeAll` / `afterAll`, from a callback after its test ended, or from a test whose `afterEach` never ran — the frame names the line | move the work into a test and absorb it there; a callback firing late is a leak — clear it where it is scheduled |
2803
+ | `setupAutoSpy({ strict: true })` has no effect — no double throws, a suite with it on stays green | before this release the default lived in module scope, and `/setup` and the factory bundles each carried a copy; a 1759-file consumer ran 12 717 tests green with it on | upgrade — the default, `onUnstubbedCall` and `setSpyEngine()` now live on `globalThis` |
2804
+ | a seeded getter reads `undefined`: `provideAutoSpy(X, { overrides: { remoteConfig } })` next to `gettersToSpyOn` or a `registerAutoSpyDefaults` row| before this release the seed was assigned into the spied accessor's setter, and the getter kept answering `undefined` | upgrade — `overrides` now seeds the getter spy; a later `accessorSpies.getters.x.mockReturnValue(…)` still wins |
2805
+ | `Cannot read properties of undefined (reading 'name')` on `value.constructor.name`, with `createAutoMock<T>()`| `constructor` answered `undefined` on an auto-mock | upgrade — it answers `Object`, as every other double does; or seed it: `createAutoMock<T>({ constructor: … })` |
2806
+ | `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
2807
+ | `Cannot spy on this instance in place: it is not extensible` | `createSpyFromInstance` was handed a frozen or sealed object | spy it before it is frozen, or build a separate double with `createSpyFromClass` (§2) |
2808
+ | `Cannot mock the property 'X': it is not configurable` | the member was defined with `configurable: false`, so it cannot be redefined | members replaced before it are still restorable — `restoreSpiedInstance(instance)` or `restoreMockedProps()` |
2809
+ | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2810
+ | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2811
+ | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
2812
+ | `Cannot read properties of undefined`, stack inside the component's own constructor or a field initializer | a member the double must **hold** rather than spy on is read while the component is being constructed, and the `mockReadonlyProp` that seeds it is written _below_ `TestBed.createComponent` — so it has not run yet, and never will | seed it **before** construction: `provideAutoSpy(X, { overrides: { paymentParams: … } })`, or `provideAutoSpyForToken(TOKEN, { … })`. No later call can repair this — the throw happens inside `createComponent`, so the helper is not reached and cannot say so; the stack names the component (§9, §13) |
2813
+ | `arr.map(asInstance)` infers `unknown[]` | `asInstance` is overloaded, and passing it **by reference** resolves against the `DeepMockProxy<T>` overload, from which `T` cannot be inferred | `arr.map((spy) => asInstance(spy))` — the arrow gives each call its own inference. For a fixed set, `asInstances(a, b, c)` keeps the tuple types (§6) |
2814
+ | `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
2815
+ | `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
2816
+ | `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
2817
+ | `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
2818
+ | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` once per token and spec file, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` or `setupAutoSpy({ misconfiguration: 'throw' })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
2819
+ | `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
2820
+ | `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
2821
+ | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
2822
+ | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
2823
+ | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
2824
+ | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
2825
+ | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
2826
+ | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
2827
+ | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2828
+ | `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 |
2829
+ | `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) |
2830
+ | `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) |
2831
+ | `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) |
2832
+ | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2833
+ | 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) |
2834
+ | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
2835
+ | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
2836
+ | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
2837
+ | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
2838
+ | `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
2839
+ | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
2840
+ | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
2841
+ | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
2842
+ | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
2843
+ | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
2844
+ | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
2845
+ | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
2846
+ | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
2847
+ | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
2848
+ | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
2849
+ | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
2850
+ | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
2851
+ | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
2852
+ | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
2853
+ | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
2854
+ | a block of files reported as failed suites with no stack, and zero failing tests | a test left an own enumerable key on `Object.prototype`; `mergeHooks` spreads it and collection dies | `setupAutoSpy()` guards it by default; `prototypePollution` tunes the reaction |
2855
+ | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
2856
+ | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
2857
+ | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
2858
+ | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
2859
+ | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
2860
+ | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
2861
+ | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
2862
+ | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
2863
+ | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
2864
+ | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
2865
+ | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
2866
+ | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic and keeps the `readonly` of the source member — deliberately: on a member replaced by a **spied accessor** the assignment reaches the setter spy and the getter still answers `undefined`, so the type error is the loud version of a silent no-op | `mockValueProp(spy, 'X', value)` — it defines rather than assigns, so it works in both cases; `Mutable<Spy<T>>` is the opt-in for plain data members, and `Reflect.set` is **not** a way round it |
2867
+ | `TS2345` from `mockReturnValue` / `mockImplementation` / `mockResolvedValue` on a double that used to compile | the mock surface is `MockInstance<Method>`, so the stub is now checked against the method's own return type | stub with what the method returns; if the double is meant to answer something else, the method's type or the spec's expectation is wrong |
2868
+ | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
2869
+ | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
2870
+ | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
2871
+ | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
2872
+ | a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
2873
+ | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
2874
+ | a green test whose only line about a call is `spy.m.calledWith(1);` | that is a **stub**, not an assertion — chai's `expect(fn).to.have.been.calledWith(x)` is the one that checks | continue the chain, or `expect(spy.m).toHaveBeenCalledWith(1)` — `no-bare-called-with` (§16) |
2875
+ | `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | the warning on stderr says how many were taken and where the first three were scheduled; `onStrayTimers` gets every one in `timers` (§10) |
2876
+ | `createNestUnit(X): parameter #n of Y is typed as \`Object\``(or`undefined — usually a circular import`) | the compiler emitted no runtime class for that parameter — interface, union, primitive, or a circular import | `@Inject(TOKEN)` plus a `providers` entry, or `@Optional()` to receive `undefined`; `@Inject(forwardRef(() => X))` for the import cycle |
2877
+ | `createNestUnit(X): Y declares n constructor parameter(s) but carries no design:paramtypes metadata` | no `emitDecoratorMetadata` (esbuild / Vite), or `reflect-metadata` loaded after the class | tsc or SWC with `decoratorMetadata`, `@Injectable()`, `reflect-metadata` first; or `@Inject(X)` on every parameter |
2878
+ | `createNestUnit(X): A -> B -> A is a cycle among the classes built for real` | two exposed / `useClass` classes depend on each other; the helper does not resolve `forwardRef` cycles | expose one side less, or provide it |
2879
+ | `createNestUnit(X).spies.get(Y): the unit never asked for that token` | same guard as `createWithAutoSpies` — the spy would not be the one the unit uses | stub the token the unit actually injects (the message lists the auto-spied ones) |
2880
+ | `createNestUnit(X).spies.get(Y): Y is in \`expose\`, so the unit got a real instance` | an exposed class is real, not a spy | read the spies of its own collaborators, or drop it from `expose` |
2881
+ | `no request matched …` (with the requests that were made listed) | the URL, verb or predicate does not describe anything the code under test sent, or nothing subscribed | read the list in the message — it is the requests that were actually made; then fix the URL, add `{ method }`, or subscribe (§13) |
2882
+ | `this TestBed has no HttpTestingController` | `expectRequest` / `expectNoRequest` on a module that never got the testing backend | `TestBed.configureTestingModule({ providers: [...provideHttpTesting()] })` from `vitest-auto-spy/angular-http` (§13) |
2883
+ | `the test ended with N unanswered request(s)` | `provideHttpTesting({ verifyOnTeardown })` found a request the spec never flushed | answer it (`await expectRequest(url).flush(body)`), assert its absence (`expectNoRequest(url)`), or `{ verifyOnTeardown: false }` (§13) |
2735
2884
 
2736
2885
  **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
2737
2886
  nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
@@ -2801,7 +2950,9 @@ diagnostic and is free to change: print it, never assert on it.
2801
2950
  | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
2802
2951
  | `configureTestingModule` inside every `it()` | one per `describe` |
2803
2952
  | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
2804
- | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console` |
2953
+ | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
2954
+ | `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
2955
+ | a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
2805
2956
  | `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
2806
2957
  | `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
2807
2958
 
@@ -2922,7 +3073,7 @@ migrated by hand. Run it after `--write`, and again after any manual clean-up. `
2922
3073
  select transforms by id, `--list` prints them. Full reference:
2923
3074
  <https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
2924
3075
 
2925
- Past 50 000 files the repository scan truncates and the run says so — *Nothing left to migrate* off a
3076
+ Past 50 000 files the repository scan truncates and the run says so — _Nothing left to migrate_ off a
2926
3077
  truncated list is a claim about a tree the tool never looked at. `VITEST_AUTO_SPY_SCAN_CAP` raises
2927
3078
  the cap.
2928
3079