vitest-auto-spy 5.65.0 → 5.67.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 (73) hide show
  1. package/AGENTS.md +12 -8
  2. package/CHANGELOG.md +135 -1
  3. package/README.md +33 -11
  4. package/agent-docs/adapters.md +9 -0
  5. package/agent-docs/angular.md +3 -0
  6. package/agent-docs/anti-patterns.md +2 -2
  7. package/agent-docs/checklist.md +10 -0
  8. package/agent-docs/configuration.md +4 -0
  9. package/agent-docs/doubles.md +21 -3
  10. package/agent-docs/errors.md +5 -2
  11. package/agent-docs/eslint.md +2 -2
  12. package/agent-docs/observables.md +6 -5
  13. package/agent-docs/return-helpers.md +38 -2
  14. package/dist/angular-diagnostics.js +9 -9
  15. package/dist/angular-doubles.js +14 -14
  16. package/dist/angular-http.js +4 -4
  17. package/dist/angular-matchers.js +1 -1
  18. package/dist/angular-router.js +6 -6
  19. package/dist/angular.d.ts +4 -3
  20. package/dist/angular.js +34 -8
  21. package/dist/bun-angular.d.ts +3 -2
  22. package/dist/bun-angular.js +201 -51
  23. package/dist/bun.d.ts +2 -1
  24. package/dist/bun.js +11 -11
  25. package/dist/{chunk-Y3MYFJ75.js → chunk-2KW2IWIH.js} +4 -4
  26. package/dist/{chunk-BRKMJMTO.js → chunk-2NAHRTOC.js} +1 -1
  27. package/dist/{chunk-2OYBXIQC.js → chunk-6DZMSVSY.js} +3 -3
  28. package/dist/{chunk-FWXGEM2E.js → chunk-6SB6DLGG.js} +1 -1
  29. package/dist/{chunk-LZ64J6OW.js → chunk-ARQ3C27A.js} +3 -3
  30. package/dist/{chunk-VJV6QF4Z.js → chunk-BOOQKIP2.js} +7 -7
  31. package/dist/{chunk-JOLWHOGU.js → chunk-CYVUUAEG.js} +1 -1
  32. package/dist/{chunk-VFPT6JP6.js → chunk-DKD6CTLO.js} +2 -2
  33. package/dist/{chunk-7A4QSHDK.js → chunk-DNKAWUL6.js} +4 -1
  34. package/dist/{chunk-TH2H3MSZ.js → chunk-DSRYDTZG.js} +4 -4
  35. package/dist/{chunk-NGVSSYI4.js → chunk-EVC6RR4C.js} +1 -1
  36. package/dist/{chunk-LGWVFIIJ.js → chunk-EZ5DTZGI.js} +1 -1
  37. package/dist/{chunk-C4OFWAK7.js → chunk-FBLX5XPI.js} +2 -2
  38. package/dist/{chunk-SSTOOTI2.js → chunk-FSBPGITE.js} +1 -1
  39. package/dist/{chunk-P6P7UE7C.js → chunk-JKRICBGX.js} +1 -1
  40. package/dist/{chunk-IR5AZKBS.js → chunk-JQDO2A5C.js} +1 -1
  41. package/dist/{chunk-5XYULG5M.js → chunk-O4WLGJOB.js} +2 -2
  42. package/dist/{chunk-UON4XYJW.js → chunk-QAEK57O6.js} +24 -9
  43. package/dist/{chunk-PHXYSUVL.js → chunk-RHTW4M6I.js} +1 -1
  44. package/dist/{chunk-VEYLI6J2.js → chunk-YR6YOBTZ.js} +2 -2
  45. package/dist/cli.js +494 -225
  46. package/dist/console.js +8 -8
  47. package/dist/core.d.ts +66 -62
  48. package/dist/dom-stubs.d.ts +22 -3
  49. package/dist/dom-stubs.js +108 -3
  50. package/dist/{emission-positive-sync-BrSc2lb-.d.ts → emission-positive-sync-CgpTvdAY.d.ts} +2 -1
  51. package/dist/eslint-plugin.cjs +96 -61
  52. package/dist/index.d.ts +2 -1
  53. package/dist/index.js +23 -8
  54. package/dist/internal-types-CjExK2uA.d.ts +16 -0
  55. package/dist/jasmine-compat.js +2 -2
  56. package/dist/jasmine.d.ts +3 -2
  57. package/dist/jasmine.js +12 -12
  58. package/dist/nestjs.d.ts +67 -2
  59. package/dist/nestjs.js +93 -12
  60. package/dist/node.cjs +23 -8
  61. package/dist/node.d.ts +2 -1
  62. package/dist/node.js +23 -8
  63. package/dist/react.d.ts +2 -1
  64. package/dist/rstest.d.ts +2 -1
  65. package/dist/rstest.js +10 -10
  66. package/dist/rxjs.js +50 -8
  67. package/dist/signal-forms.js +1 -1
  68. package/dist/svelte.d.ts +2 -1
  69. package/dist/{track-signal-runs-DLvVp9iI.d.ts → track-signal-runs-Bgm7l8At.d.ts} +6 -8
  70. package/dist/vue.d.ts +3 -2
  71. package/dist/zone.js +1 -1
  72. package/package.json +1 -1
  73. package/skills/vitest-auto-spy/SKILL.md +143 -139
package/AGENTS.md CHANGED
@@ -229,7 +229,7 @@ it('loads', async () => {
229
229
  | --- | --- | --- |
230
230
  | method returning a value | nothing | `spy.m.mockReturnValue(v)` or `spy.m.calledWith(args).mockReturnValue(v)` |
231
231
  | method returning `Promise<T>` | nothing | `spy.m.resolveWith(v)` / `rejectWith(e)` (§4) |
232
- | method returning `Observable<T>` | nothing | `spy.m.nextWith(v)` / `throwWith(e)` (§4, needs `/rxjs`) |
232
+ | method returning `Observable<T>` | nothing | one-shot (HTTP, IPC): `spy.m.nextOneTimeWith(v)`; a stream that stays open: `nextWith(v)`; `throwWith(e)` (§4, needs `/rxjs`) |
233
233
  | `Observable` property (`items$`) | `observablePropsToSpyOn: ['items$']` | `spy.items$.nextWith(v)` |
234
234
  | signal / `computed` member of a dependency | `instanceMethodsToSpyOn: ['items']` | `spy.items.mockReturnValue(v)`; a value that changes mid-test: `mockSignalProp` (§13) |
235
235
  | plain data field | `overrides: { field: value }` | set in the provider (§5) |
@@ -241,7 +241,9 @@ Rules that carry most of the value:
241
241
  2. **Declare each spy as `Spy<T>`, never as `T`** (§6).
242
242
  3. **Seed defaults in `beforeEach`, override in the test.** An unconfigured method returns
243
243
  `undefined`, and the failure surfaces far from its cause. Seed before `TestBed.inject(ClassUnderTest)`
244
- when its constructor or field initialisers read the dependency.
244
+ when its constructor or field initialisers read the dependency. Seed a one-shot `Observable` with
245
+ `nextOneTimeWith`: a `nextWith` never completes (`forkJoin` / `lastValueFrom` hang) and its buffered
246
+ value reaches a later `throwWith`'s subscriber first.
245
247
  4. **`provideAutoSpy` is lazy by default**, so listing a wide service costs nothing for the methods a
246
248
  test never touches.
247
249
  5. **Two import lines.** `provideAutoSpy`, `injectSpy` and the `Spy<T>` type come from
@@ -319,12 +321,11 @@ const modal = injectSpy<ModalRef<PurchaseOptions>>(ModalRef); // a particular in
319
321
  constructor takes the type parameter (`constructor(public data: T)`), it reads the **constraint**:
320
322
  `ModalRef<T = unknown>` gives `Spy<ModalRef<unknown>>`. Spell the argument out when the spec means
321
323
  the default or one instantiation.
322
- - `createSpyFromClass` needs the argument in one combination: a generic class with an accessor list
323
- (or `overrides`) **and** `returns`. The symptom: `'isKeyEnabled' does not exist in type
324
- 'MethodReturns<{ flagsConfig: any; }>'`. Write `createSpyFromClass<FlagsConfigService>(…)`. The
325
- core `registerAutoSpyDefaults` needs it the same way.
326
- - `provideAutoSpy`, `overrideAutoSpy`, `overrideComponentProvider` and the `/angular` class overload
327
- of `registerAutoSpyDefaults` take `T` from the class alone. A token needs nothing: `T` comes from
324
+ - `createSpyFromClass`, `createSpyFromInstance`, every `provideAutoSpy`, `overrideAutoSpy`,
325
+ `overrideComponentProvider` and both class overloads of `registerAutoSpyDefaults` take `T` from the
326
+ class alone: an accessor list, `returns` or an `overrides` object held in a variable no longer
327
+ narrows it, so `createSpyFromClass(KeyboardEvent, { overrides: keys, returnsUndefined: [...] })`
328
+ needs no `<KeyboardEvent>`. A token needs nothing: `T` comes from
328
329
  `InjectionToken<T>`.
329
330
 
330
331
  **A generic method collapses to `unknown`.** `show<T, U>(component: Type<T>, data: U)` types on the
@@ -536,6 +537,9 @@ file, and `globals: true`.
536
537
 
537
538
  In [`agent-docs/adapters.md`](./agent-docs/adapters.md). Read it for NestJS, Vue, React, Svelte, `bun test`, `node:test`, Rstest and the console entry.
538
539
 
540
+ Nest HTTP tests: `createHttpExecutionContext` supplies the context; `invokeNestParamDecorator`
541
+ calls a custom parameter factory by index with its recorded data. Both come from `/nestjs` (§15).
542
+
539
543
  <!-- agents-outline: agent-docs/adapters.md -->
540
544
 
541
545
  - `trackNodeMocks` for `node:test` memory
package/CHANGELOG.md CHANGED
@@ -10,6 +10,138 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.67.0] - 2026-11-07
14
+
15
+ ### Added
16
+
17
+ - `/dom-stubs`: `fillMissingDomApis({ canvas: true })` gives every `<canvas>` an inert 2D context
18
+ and a `toDataURL()` of `'data:,'`, for a chart library that draws on mount where jsdom logs
19
+ "Not implemented". The context is one per canvas, accepts every call and setting, keeps what was
20
+ set and answers `measureText`, `getImageData` and the gradient factories with empty values; any id
21
+ but `'2d'` still gets `null`. Off by default, because it also replaces a real context.
22
+ - ESLint: `no-real-wait-in-test` suggests `await flushEventLoop()` for a sleep awaited on the spot,
23
+ adding the import, and its message names it for a wait that only lets mocked promises settle. A
24
+ suggestion rather than a fix: one real turn settles every resolved chain but fires no timer.
25
+ - `/dom-stubs`: `createDomEvent(type, { spy: true })` makes `preventDefault`, `stopPropagation` and
26
+ `stopImmediatePropagation` runner mocks that still act, so a direct handler call can assert both
27
+ `expect(event.preventDefault).toHaveBeenCalledOnce()` and `event.defaultPrevented`. The return type
28
+ narrows to the new `SpiedDomEvent` only under that option.
29
+ - `/rxjs`: `throwWith()` on a stream that still buffers a `nextWith()` value nobody has subscribed to
30
+ is reported — once per spied method or property as a warning, at every call under
31
+ `setupAutoSpy({ misconfiguration: 'throw' })`. The next subscriber would get that value before the
32
+ error and run the success branch on stale data: the shape of a success seeded in `beforeEach` and a
33
+ failure configured in one test. The message names `nextOneTimeWith()`, `resetAutoSpy()` and
34
+ `nextWithValues([{ value }, { errorValue }])`; a subscriber already on the stream still gets
35
+ "emit, then fail" with no report.
36
+ - A stream or promise helper written after `mockImplementation` / `mockReturnValue` on the same
37
+ method is reported: `throwWith() was configured on 'load' after mockImplementation() had replaced
38
+ its dispatch`. Those members replace the dispatch that reads `throwWith`, `nextWith`,
39
+ `nextOneTimeWith`, `nextWithValues`, `nextWithPerCall`, `complete`, `returnSubject`, `resolveWith`,
40
+ `rejectWith`, `resolveWithPerCall` and `failWith`, so the later helper answered nothing and the test
41
+ ran on the implementation. The report goes through `reportMisconfiguration` — a warning by default,
42
+ a throw under `setupAutoSpy({ misconfiguration: 'throw' })` — and names `mockReset()` as the way to
43
+ hand the answer back. **The behaviour is unchanged**; the other order stays last-writer-wins and is
44
+ not reported. Vitest and Rstest only, like the `calledWith` report.
45
+ - CLI: `perf --gate --changed [ref]` judges only the spec files the diff against the merge base of
46
+ `ref` reaches through the import graph — the graph `ng-test --changed` follows, where a config, a
47
+ lockfile or a change the setup files reach still judges everything. A finding elsewhere is printed
48
+ as a warning with the outcome `outside change` and is not re-measured, so a merge request is no
49
+ longer failed by a slow spec its author never touched. A bare `--changed` diffs against `HEAD`; an
50
+ empty diff fails nothing; when git cannot answer — a depth-1 CI clone without the base commit —
51
+ every file is judged and a warning says why. In GitLab pass
52
+ `--changed "$CI_MERGE_REQUEST_DIFF_BASE_SHA"` with that commit fetched, in GitHub
53
+ `--changed origin/$GITHUB_BASE_REF`.
54
+ - CLI: `perf --gate --debt warn`, with `--baseline` pointing at a `.json` baseline or a `.jsonl`
55
+ history, reports a slow file or test body the reference already recorded at 80 % of the current
56
+ budget or more as `known debt`: a warning, not re-measured, failing nothing. A history is read as
57
+ the median of its runs. A regression against the reference is never debt. The default,
58
+ `--debt fail`, keeps the gate as it was.
59
+ - CLI: baselines and histories also record each slow test body — every one over the reporter's
60
+ 100 ms floor — under a `tests` key, which is what body-level debt reads. A baseline recorded by an
61
+ older release knows only file debt until it is recorded again.
62
+ - CLI: `perf --gate --gate-margin <x>` (default 1) makes a confirmed reading reach budget × `x`
63
+ before it fails; between the budget and that it is a warning with the outcome `within margin`, so a
64
+ file sitting on its budget no longer flips with the runner. `--confirm-runs <n>` (default 1, at
65
+ most 5) re-measures the suspects `n` times and judges each file at its fastest reading, since noise
66
+ only ever adds time. The setup the docs recommend for a merge request is
67
+ `perf --gate --changed <base> --baseline perf-history.jsonl --debt warn --gate-margin 1.2 --confirm-runs 2`,
68
+ with the default branch recording that history through `--update-baseline` instead of gating.
69
+ - **Bundle size:** every entry that builds a spy (the root entry, `/angular`, `/bun`, `/node`,
70
+ `/jasmine`, `/console`, `/angular-router` and the rest) grows by 98–128 B min+gzip (0.3–1.1%) for
71
+ the helper-under-a-replaced-dispatch report and its message; `/rxjs` by 19 B for the hook call.
72
+ Against 5.66.0 as a whole, those entries grow by 0.17–0.28 kB (0.8–1.7%): the rest is the longer
73
+ strict "nothing configured" error and completion-timeout message below.
74
+ - **Bundle size:** `/dom-stubs` grows from 12.55 kB to 13.36 kB min+gzip (+0.82 kB, +6.5%): about
75
+ 0.73 kB for the inert canvas context and its default settings, which run only under
76
+ `{ canvas: true }`, and the rest for `createDomEvent(type, { spy: true })`.
77
+ - **Bundle size:** `/rxjs` grows from 2.26 kB to 2.64 kB min+gzip (+0.38 kB, +16.9%), and its import
78
+ graph from 31.9 kB to 33.7 kB, for the stale-value report on `throwWith()`: the message names the
79
+ three ways out, and every target carries the flag and the label it needs.
80
+
81
+ ### Changed
82
+
83
+ - The strict "nothing configured" error adds a line for a `void` method or an ignored result:
84
+ `returnsUndefined: ['method']` in the double's options.
85
+ - The completion timeout of `expectAllEmissions` / `expectCompletion` adds that a spied method
86
+ configured with `nextWith()` never completes, nor does a `forkJoin` over it, and names
87
+ `nextOneTimeWith()`.
88
+ - Docs: the RxJS page (EN, RU) has a new section, "Where a `throwWith` error goes": `toSignal` keeps
89
+ the error until the signal is read, `rxResource` keeps it in `error()`, and an `effect` reaches
90
+ `ErrorHandler` only once `TestBed.tick()` runs it — Angular semantics, not a lost error; the table
91
+ names the assertion for each. `agent-docs/return-helpers.md` and `agent-docs/errors.md` carry the
92
+ same, and `agent-docs/configuration.md` the new report.
93
+ - Docs: AGENTS.md, `agent-docs/return-helpers.md` and the RxJS page (new section "A request method
94
+ answers once") seed HTTP / IPC methods with `nextOneTimeWith`; the `void`-output example of
95
+ `expectEmissionSync` is a plain statement, which `no-confusing-void-expression` accepts; the
96
+ element-stub page shows a `<textarea>` target through `elementClass` and says the element a
97
+ `mockElementRefProp` handle installs is `handle.element`; the anti-patterns table names
98
+ `vitest-auto-spy/setup` as the entry of `mockSystemTime`.
99
+
100
+ ### Fixed
101
+
102
+ - `renderShallow(X, { keepTemplate: true })` no longer fails with `NG01914 … is an invalid [formField]
103
+ directive host` when the kept template binds a signal-forms `[formField]` to a child component. The
104
+ child was dropped while the directive stayed, and a bare tag has no `value` / `checked` model. A
105
+ dropped child that is a custom form control (a `value` input with a `valueChange` output, or
106
+ `checked` with `checkedChange`) now renders as `createComponentStub(child)`, so the form reads and
107
+ writes through it. Other children are still dropped; `keepChildren: [Child]`, or a stub with the
108
+ same selector there, takes its place.
109
+ - **Bundle size:** `/bun-angular` grows from 39.14 kB to 40.62 kB min+gzip (+1.48 kB, +3.8%), and
110
+ its import graph from 250.2 kB to 257.4 kB, because `renderShallow` now builds those stand-ins with
111
+ `createComponentStub`, which that entry did not load before. `/angular` grows by 0.49 kB (+1.3%).
112
+ - **`createSpyFromClass` takes `T` from the class, never from the configuration.**
113
+ `createSpyFromClass(KeyboardEvent, { overrides: keys, returnsUndefined: ['preventDefault'] })`, with
114
+ `keys` an `it.each` row or any other object held in a variable, typed the double as
115
+ `Spy<{ key: string; metaKey: boolean; ctrlKey: boolean }>`: the variable's type is a supertype of
116
+ the instance, so TypeScript picked it for `T`, rejected `'preventDefault'` with `TS2345` and lost
117
+ every other member with `TS2339`. The configuration no longer infers `T` (a deferred indexed
118
+ access, which works down to TypeScript 5.0, with a `T = any` default as `/angular` uses). The same
119
+ signature now accepts an accessor list next to `returns` on a generic class, the case AGENTS.md
120
+ said to spell out. `createSpyFromInstance`, `createSpyClass`, the core `registerAutoSpyDefaults`,
121
+ and `provideAutoSpy` on `/nestjs`, `/vue` and `/jasmine` (plus `/jasmine`'s `createSpyFromClass`)
122
+ take `T` the same way. An explicit `<T>` still compiles. Types only.
123
+ - ESLint: `prefer-render-shallow` matches DOM reads as whole camelCase words of a name, so
124
+ `getAttributesLayer()` no longer counts as `getAttribute`. Under `{ templates: 'never' }` that was a
125
+ false `domAccess` report; by default it silenced the file's `createComponent` findings.
126
+ `nativeElementOf()`, `debugElements` and `innerHTML` still count.
127
+ - ESLint: `prefer-create-spy-from-class` no longer recommends `createAutoMock<{ load: Mock }>()` or
128
+ `createAutoMock<Record<string, Mock>>()` for a double whose declared type describes the mocks
129
+ themselves; it names `createSpyFromClass(Class)` instead.
130
+
131
+ ## [5.66.0] - 2026-10-28
132
+
133
+ ### Added
134
+
135
+ - `/nestjs`: `createHttpExecutionContext` supplies an HTTP context for guards, interceptors,
136
+ exception filters and parameter decorators. Request, response and argument accessors share the
137
+ same values; missing handler/class metadata and non-HTTP transports produce explicit errors.
138
+ - `/nestjs`: `invokeNestParamDecorator` runs a custom parameter decorator's recorded factory by
139
+ parameter index and forwards its stored data. It refuses missing or ambiguous factories and
140
+ parameter pipes or schemas that need Nest's request pipeline.
141
+ - **Bundle size:** `/nestjs` grows from 20.06 kB to 20.85 kB min+gzip (+0.79 kB, +3.9%) for
142
+ the HTTP context and parameter metadata helpers, without importing NestJS or Vitest. The root
143
+ entry does not re-export these helpers.
144
+
13
145
  ## [5.65.0] - 2026-10-26
14
146
 
15
147
  ## [5.64.0] - 2026-10-23
@@ -6888,7 +7020,9 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
6888
7020
  This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
6889
7021
  [full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
6890
7022
 
6891
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.65.0...HEAD
7023
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.67.0...HEAD
7024
+ [5.67.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.66.0...v5.67.0
7025
+ [5.66.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.65.0...v5.66.0
6892
7026
  [5.65.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.64.0...v5.65.0
6893
7027
  [5.64.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.63.0...v5.64.0
6894
7028
  [5.63.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.62.0...v5.63.0
package/README.md CHANGED
@@ -15,7 +15,7 @@ replacement for [`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spie
15
15
  [![downloads per month](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2Flast-month%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2Fmonth)](https://www.npmjs.com/package/vitest-auto-spy)
16
16
  [![downloads over 18 months](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.npmjs.org%2Fdownloads%2Fpoint%2F2026-06-21%3A2030-01-01%2Fvitest-auto-spy&query=%24.downloads&color=brightgreen&logo=npm&label=downloads%2F18mo)](https://www.npmjs.com/package/vitest-auto-spy)
17
17
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
18
- [![minzipped size](https://img.shields.io/badge/minzip-30.7%20kB-brightgreen)](#install)
18
+ [![minzipped size](https://img.shields.io/badge/minzip-31.0%20kB-brightgreen)](#install)
19
19
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
20
20
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
21
21
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -28,7 +28,7 @@ replacement for [`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spie
28
28
 
29
29
  📚 [**Documentation**](https://asdalexey.github.io/vitest-auto-spy/) · 🧭 [**Spec patterns**](https://asdalexey.github.io/vitest-auto-spy/recipes) · 📦 [**npm**](https://www.npmjs.com/package/vitest-auto-spy) · 🐙 [**GitHub**](https://github.com/ASDAlexey/vitest-auto-spy) · 🔖 [**Changelog**](./CHANGELOG.md)
30
30
 
31
- 🤖 [**AGENTS.md**](./AGENTS.md) · 🔤 [**llms.txt**](https://asdalexey.github.io/vitest-auto-spy/llms.txt) · 📄 [**llms-full.txt**](https://asdalexey.github.io/vitest-auto-spy/llms-full.txt) — works with [Claude Code, OpenAI Codex, GLM, Cursor, Copilot, Gemini CLI and the rest](#which-file-your-agent-reads)
31
+ 📘 [**AGENTS.md**](./AGENTS.md) · 🔤 [**llms.txt**](https://asdalexey.github.io/vitest-auto-spy/llms.txt) · 📄 [**llms-full.txt**](https://asdalexey.github.io/vitest-auto-spy/llms-full.txt) — works with [Claude Code, OpenAI Codex, GLM, Cursor, Copilot, Gemini CLI and the rest](#which-file-your-agent-reads)
32
32
 
33
33
  <br/>
34
34
 
@@ -47,6 +47,8 @@ replacement for [`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spie
47
47
  - **A spy from a type alone**: `createAutoMock<T>()` and `mockDeep<T>()`.
48
48
  - **Angular `TestBed` in one line**: `provideAutoSpy(ApiService)`, then `injectSpy(ApiService)`.
49
49
  - **Short handler tests**: typed input/paste/drop events, element query stubs, and exact synchronous output assertions.
50
+ - **Nest HTTP tests without starting the app**: `createHttpExecutionContext` supplies the context;
51
+ `invokeNestParamDecorator` runs a custom parameter decorator with its recorded data.
50
52
  - **One API on every runner**: Vitest, Bun, `node:test` and Rstest; helpers for Angular, NestJS,
51
53
  React, Vue / Pinia and Svelte.
52
54
  - **Checks that make silent tests fail**: strict mode, Observable assertions, cleanup between tests,
@@ -227,11 +229,11 @@ Full page: [Control helpers](https://asdalexey.github.io/vitest-auto-spy/core/co
227
229
 
228
230
  Each spied method gets helpers for what it returns:
229
231
 
230
- | Method returns | Helpers |
231
- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
232
- | a value | `mockReturnValue`, `calledWith(...)`, `mustBeCalledWith(...)`, `failWith(error)` |
233
- | a `Promise` | `resolveWith(value)`, `rejectWith(error)`, `resolveWithPerCall([...])` |
234
- | an `Observable` | `nextWith(value)` emits and keeps the stream open; `nextOneTimeWith(value)` emits once, then completes (like an HTTP call); `nextWithValues([...])`, `throwWith(error)`, `complete()`, `returnSubject()` |
232
+ | Method returns | Helpers |
233
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
234
+ | a value | `mockReturnValue`, `calledWith(...)`, `mustBeCalledWith(...)`, `failWith(error)` |
235
+ | a `Promise` | `resolveWith(value)`, `rejectWith(error)`, `resolveWithPerCall([...])` |
236
+ | an `Observable` | `nextWith(value)` emits and keeps the stream open; `nextOneTimeWith(value)` emits once, then completes (like an HTTP call: the one to seed a request/response method with); `nextWithValues([...])`, `throwWith(error)`, `complete()`, `returnSubject()` |
235
237
 
236
238
  ```ts
237
239
  products.load.resolveWith([product]); // load(): Promise<Product[]>
@@ -2158,6 +2160,7 @@ users.currentTenant(); // throws here, on the line that called it
2158
2160
  [vitest-auto-spy] Cart.checkout(1, 'now') was called; this strict double has nothing configured for it.
2159
2161
  Called from src/app/cart.component.ts:41:12
2160
2162
  Configure it in the test: cart.checkout.calledWith(1, 'now').mockReturnValue(…) for these arguments, or .mockReturnValue(…) for any — .resolveWith(…) / .nextWith(…) when it returns a Promise / Observable.
2163
+ A void method, or a result the code ignores: returnsUndefined: ['checkout'] in the double's options.
2161
2164
  Docs: https://asdalexey.github.io/vitest-auto-spy/core/strict-mode#the-message
2162
2165
  ```
2163
2166
 
@@ -2933,6 +2936,25 @@ Pass `{ expose: [Dependency] }` to keep a collaborator real, and `providers` to
2933
2936
  `vitest`. On `node --test`, Bun or Rstest, import the runtime entry first. `createNestUnit` is also
2934
2937
  exported from `vitest-auto-spy/node` and `vitest-auto-spy/bun`.
2935
2938
 
2939
+ Test a custom parameter decorator without starting Nest or reading its metadata by hand.
2940
+ Here `ProfileController.profile` has `@CurrentUser('id')` on its first parameter:
2941
+
2942
+ ```ts
2943
+ import 'reflect-metadata';
2944
+
2945
+ import { expect } from 'vitest';
2946
+ import { createHttpExecutionContext, invokeNestParamDecorator } from 'vitest-auto-spy/nestjs';
2947
+
2948
+ import { ProfileController } from './profile.controller';
2949
+
2950
+ const context = createHttpExecutionContext({ request: { user: { id: 'user-7' } } });
2951
+ expect(invokeNestParamDecorator(ProfileController, 'profile', 0, context)).toBe('user-7');
2952
+ ```
2953
+
2954
+ The parameter index starts at zero. The helper runs the recorded factory with its decorator data;
2955
+ pipes and schemas require Nest's validation pipeline and are refused.
2956
+ Use the same context helper for guards, interceptors and exception filters.
2957
+
2936
2958
  Full page: [NestJS](https://asdalexey.github.io/vitest-auto-spy/adapters/nestjs).
2937
2959
 
2938
2960
  #### `httpResource()` and `HttpClient` — `vitest-auto-spy/angular-http`
@@ -3103,7 +3125,7 @@ only. Full contracts: [element and event stubs](https://asdalexey.github.io/vite
3103
3125
 
3104
3126
  | Utility | Entry point | What it's for |
3105
3127
  | ----------------------------------------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3106
- | `createDomEvent(type, init?)` / `createInputEvent(value, init?)` | `/dom-stubs` | Native events for direct handlers, with readonly targets or an input value |
3128
+ | `createDomEvent(type, init?)` / `createInputEvent(value, init?)` | `/dom-stubs` | Native events for direct handlers, with readonly targets or an input value; `{ spy: true }` spies `preventDefault` / `stopPropagation` |
3107
3129
  | `createPasteEvent(text, init?)` / `createDropEvent(text, init?)` | `/dom-stubs` | Text transfers with native `defaultPrevented`; `null` represents absent transfer data |
3108
3130
  | `mockElementRefProp(component, key, opts?)` | `/angular/doubles` | Inferred element spies, `ref` and writable `query`, installed together |
3109
3131
  | `injectSpy(token)` / `injectSpy(moduleRef, token)` | `/angular`, `/nestjs` | Pull a provided spy out of the DI container, already typed as `Spy<T>` — no casting |
@@ -3135,7 +3157,7 @@ only. Full contracts: [element and event stubs](https://asdalexey.github.io/vite
3135
3157
  | `stubAnimationFrame(opts?)` | `/dom-stubs` | `requestAnimationFrame` / `cancelAnimationFrame` run on the spot or on `flush(timestamp?)`; a frame requested inside a frame waits for the next flush; `onError` intercepts a throwing callback; `lastHandle` is the latest handle issued (they start above 2^30) |
3136
3158
  | `stubElementRect(el, rect?)` | `/dom-stubs` | `getBoundingClientRect()` answering a real `DOMRect` built from a partial `DOMRectInit`; returns the undo, plus the installed spy as `.getBoundingClientRect` |
3137
3159
  | `createElementStub(opts?)` | `/dom-stubs` | A typed element for `ElementRef`; `elementClass` enables `instanceof` checks and infers the type. Spies keep state; unconfigured DOM members require `overrides` |
3138
- | `fillMissingDomApis(opts?)` | `/dom-stubs` | For the setup file: `PointerEvent`, a no-op `ResizeObserver`, no-op scrolling and `document.doctype`, only where missing; `{ cheapComputedStyle: true }` |
3160
+ | `fillMissingDomApis(opts?)` | `/dom-stubs` | For the setup file: `PointerEvent`, a no-op `ResizeObserver`, no-op scrolling and `document.doctype`, only where missing; `{ cheapComputedStyle: true }`, `{ canvas: true }` |
3139
3161
  | `flushEventLoop(turns?)` / `settleDynamicImport(load, turns?)` | core | Real event-loop turns while the timers are faked — for a dynamic `import()` or native `async` in a dependency |
3140
3162
  | `flushEventLoopUntil(isDone, opts?)` | core | Real turns until a condition holds — a `resource()` leaving `loading` — with a budget instead of a hang |
3141
3163
  | `stubMediaElement(opts?)` | `/dom-stubs` | A `<video>` / `<audio>` that plays, reports a duration and fires the media events jsdom never does |
@@ -3604,7 +3626,7 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
3604
3626
  project takes file by file (`prefer-render-shallow`, `prefer-set-inputs`) or decide on a heuristic.
3605
3627
  - `autoSpy.configs.typeErrors` holds the rules whose findings are compile errors (`prefer-as-spy`,
3606
3628
  `no-mocked-for-spy`). Spread it after a blanket downgrade so they keep their severity.
3607
- - Eight rules fix with `--fix`; nineteen offer editor suggestions.
3629
+ - Nine rules fix with `--fix`; twenty offer editor suggestions.
3608
3630
 
3609
3631
  **Common mistake:** adding a bare `rules` key next to the spread config to change one severity. It
3610
3632
  replaces the whole rule map. Spread the rules too:
@@ -3658,7 +3680,7 @@ Rolling it out on a large suite, and the options each rule takes:
3658
3680
  | [`no-unasserted-console-spy`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-unasserted-console-spy) | `warn` | — | Assert on a console spy the spec installs, or let useConsoleSpies() do the silencing |
3659
3681
  | [`no-outer-binding-in-mock-factory`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-outer-binding-in-mock-factory) | `error` | — | Declare what a vi.mock factory reads through vi.hoisted — vi.mock runs before the file’s declarations |
3660
3682
  | [`no-relative-mock-under-builder`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-relative-mock-under-builder) | `error` | — | Do not vi.mock() a relative path in a spec the Angular unit-test builder runs — the builder throws on it |
3661
- | [`no-real-wait-in-test`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-real-wait-in-test) | `warn` | — | Do not sleep on the real clock in a spec — drive fake timers or wait for the outcome |
3683
+ | [`no-real-wait-in-test`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-real-wait-in-test) | `warn` | suggest | Do not sleep on the real clock in a spec — drive fake timers or wait for the outcome |
3662
3684
  | [`no-inline-test-data`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-inline-test-data) | `warn` | — | Keep long or repeated test data in a *.mock.ts file next to the spec |
3663
3685
  | [`no-disabled-testbed-teardown`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-disabled-testbed-teardown) | `error` | — | Keep TestBed teardown on — destroyAfterEach: false leaks every fixture into the tests after it |
3664
3686
  | [`no-import-time-console-spies`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-import-time-console-spies) | `error` | — | Install the console spies where the file needs them — the import installs them once per worker |
@@ -28,6 +28,15 @@ mount(Greeting, { global: { provide } });
28
28
  import { createSpyFromClass } from 'vitest-auto-spy/react';
29
29
  ```
30
30
 
31
+ **Nest HTTP tests.** `createHttpExecutionContext({ request, response, handler, class: Controller })`
32
+ from `/nestjs` supplies an HTTP `ExecutionContext` / `ArgumentsHost` without starting Nest. Omit
33
+ the options you do not need; `getClass()` / `getHandler()` throw if their option is missing.
34
+ `invokeNestParamDecorator(Controller, 'method', parameterIndex, context)` runs a custom parameter
35
+ factory by zero-based index and forwards its recorded `data`. Import `reflect-metadata` before
36
+ the controller. The result is `unknown`; async factories return their promise unchanged. Native
37
+ parameter decorators, pipes and schemas require Nest's pipeline and are refused. Examples:
38
+ [Nest HTTP helpers](https://asdalexey.github.io/vitest-auto-spy/adapters/nestjs#createhttpexecutioncontext).
39
+
31
40
  **Types without Vitest.** `/node`, `/bun`, `/bun-angular`, `/rstest`, `/jasmine-compat`,
32
41
  `/angular-router`, `/console`, `/nestjs`, `/observer-spy` and `/rxjs` type-check with no Vitest
33
42
  installed and `skipLibCheck: false`. `Spy<T>` is one type on every entry: once the program imports an
@@ -1126,6 +1126,9 @@ const { fixture, component } = renderShallow(TaskListComponent, {
1126
1126
  inputs: { projectId: 42 }, // signal inputs take the VALUE, not the signal
1127
1127
  });
1128
1128
  // other options: imports, keepTemplate, keepModules, keepChildren, keepHostDirectives, template, beforeCreate, detectChanges
1129
+ // keepTemplate: a dropped child that is a custom form control (value/valueChange or checked/checkedChange,
1130
+ // i.e. a model()) renders as createComponentStub(child), so a kept signal-forms [formField] on it has a host
1131
+ // instead of NG01914; keepChildren: [Child] keeps the real one
1129
1132
  // testBed: { deferBlockBehavior, errorOnUnknownElements, errorOnUnknownProperties, teardown, schemas } passes through;
1130
1133
  // schemas are ADDED to the NO_ERRORS_SCHEMA a standalone: false host gets. keepChildren works on such a host too
1131
1134
  // a TestBed.inject / injectSpy in beforeEach instantiates the module first: renderShallow then throws naming beforeCreate
@@ -45,7 +45,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
45
45
  | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
46
46
  | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
47
47
  | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
48
- | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
48
+ | `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` from `vitest-auto-spy/setup` (not the root entry) |
49
49
  | ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
50
50
  | a bare `await import('…')` in a test body | `await settleDynamicImport(() => import('…'))` — `prefer-settle-dynamic-import` reports it |
51
51
  | a capture whose only assertions are `toEqual([])` / `not.toHaveBeenCalled()` | `await expectNoEmission(source$)` — `no-vacuous-absence-assertion` reports it |
@@ -58,7 +58,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
58
58
  | `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
59
59
  | `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift |
60
60
  | a `TestingStorage` class assigned to `globalThis.localStorage` | `stubWebStorage('localStorage')`, restored with the other patches |
61
- | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
61
+ | an assertion containing a date, with no clock set | `mockSystemTime(iso)` first, imported from `vitest-auto-spy/setup` |
62
62
  | `configureTestingModule` inside every `it()` | one per `describe` |
63
63
  | `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
64
64
  | a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `useConsoleSpies()` |
@@ -266,6 +266,16 @@ unless `--no-confirm` says one reading is enough. Exit `1` is "over budget", exi
266
266
  to judge" — including a red or unfinished (`partial`) suite, which the gate will not judge at all, since a failed test is
267
267
  measured until its timeout and 30 s of timeout looks exactly like 30 s of slow code.
268
268
 
269
+ On a merge request the gate can also tell whose a finding is, and each of these only turns a failure
270
+ into a warning: `--changed <ref>` judges only the spec files the diff against the merge base of `ref`
271
+ reaches through the import graph (`outside change` elsewhere; every file when git fails, e.g. a
272
+ depth-1 clone without the base commit); `--debt warn` with `--baseline` excuses a slow file or body
273
+ the baseline or `.jsonl` history already had at 80 % of its budget (`known debt`; a regression never
274
+ is); `--gate-margin <x>` fails only at budget × `x` (`within margin` below it); `--confirm-runs <n>`
275
+ re-measures up to 5 times and keeps each file's fastest reading. Recommended on a merge request:
276
+ `perf --gate --changed "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --baseline perf-history.jsonl --debt warn --gate-margin 1.2 --confirm-runs 2`,
277
+ with the default branch recording into the same history with `--update-baseline` and not gating.
278
+
269
279
  `--json <path>` re-analyses a report an earlier `--out <path>` run wrote, instead of running Vitest
270
280
  again. Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
271
281
 
@@ -229,6 +229,7 @@ users.currentTenant(); // throws here, not four frames later inside the componen
229
229
  [vitest-auto-spy] Cart.checkout(1, 'now') was called; this strict double has nothing configured for it.
230
230
  Called from src/app/cart.component.ts:41:12
231
231
  Configure it in the test: cart.checkout.calledWith(1, 'now').mockReturnValue(…) for these arguments, or .mockReturnValue(…) for any — .resolveWith(…) / .nextWith(…) when it returns a Promise / Observable.
232
+ A void method, or a result the code ignores: returnsUndefined: ['checkout'] in the double's options.
232
233
  Docs: https://asdalexey.github.io/vitest-auto-spy/core/strict-mode#the-message
233
234
  ```
234
235
 
@@ -257,6 +258,9 @@ Precedence, first one set wins: the double's `onUnstubbedCall` → the double's
257
258
  for its callback, which is reported too, as is a chain configured inside that callback. When both a fallback and a
258
259
  per-argument value are wanted, the fallback goes in the spy's container — `returns:` where the
259
260
  double is built, or `resolveWith` / `nextWith` / `failWith` — which a `calledWith` still wins over.
261
+ The same report covers a container helper written after the replacement — `throwWith`, `nextWith`,
262
+ `resolveWith`, `failWith` and the rest answer nothing under a replaced dispatch; `mockReset()` the
263
+ spy first (it keeps the chains and the container, and puts the dispatch back).
260
264
  The report is this library's own spy engine's, so Vitest and Rstest have it; Bun, `node:test` and
261
265
  `setSpyEngine('runner')` do not. The `Once` family is never reported.
262
266
  `vi.when(spy)` (Vitest 5) is not reported either, nor any `mockImplementation` built from
@@ -223,6 +223,8 @@ expect(trigger.focus).toHaveBeenCalledExactlyOnceWith();
223
223
  This replaces a local element factory followed by `mockSignalProp(component, 'dateTrigger', new ElementRef(element))`.
224
224
  The query determines the element type. Options are those of `createElementStub` and cannot change that type.
225
225
  The handle also returns `ref` (the installed `ElementRef`) and `query` (a writable test signal).
226
+ The handle is not the element: the component receives `trigger.element`, so a call that was handed the
227
+ element is asserted with it (`toHaveBeenCalledWith(trigger.element, …)`), not with `trigger` or `trigger.ref.nativeElement`.
226
228
  Use `trigger.query.set(undefined)` to represent a missing optional query; required queries reject `undefined` in TypeScript.
227
229
  Queries returning component or directive instances, and `viewChildren`, need `mockSignalProp` instead.
228
230
 
@@ -267,6 +269,13 @@ Pass other native event flags directly in `init`.
267
269
  **Common mistake:** expecting `data`, `inputType`, or a browser input event constructor check to work.
268
270
  This helper returns `Event`, not `InputEvent`. Use a native `InputEvent` when testing those browser features.
269
271
 
272
+ `createDomEvent(type, { spy: true })` makes `preventDefault`, `stopPropagation` and
273
+ `stopImmediatePropagation` runner mocks that still act: assert `expect(event.preventDefault).toHaveBeenCalledOnce()`
274
+ and `event.defaultPrevented` alike, with no `createSpyFromClass(KeyboardEvent, …)`. For a `<textarea>` or
275
+ `<select>` target, `createInputEvent` does not fit (it builds an input): pass
276
+ `createElementStub({ elementClass: HTMLTextAreaElement, tagName: 'textarea', overrides: { value } }).element`
277
+ as `createDomEvent('input', { target })`. `elementClass` infers the element type, so no type argument.
278
+
270
279
  ### Text paste and drop events
271
280
 
272
281
  Replace local clipboard and drag event factories when a handler only reads plain text.
@@ -299,18 +308,27 @@ Once in the setup file, before `setupAutoSpy()`: it fills `PointerEvent`, a no-o
299
308
  no-op `scrollTo` / `scrollBy` / `scrollIntoView` on elements and `window` (jsdom's own only log
300
309
  "Not implemented") and `document.doctype` — only what is missing, safe to run twice, and it returns
301
310
  the names it filled. `{ cheapComputedStyle: true }` replaces `getComputedStyle` with one that answers
302
- the inline style only: fast, and silent for a pseudo-element. Filled before the snapshot, these
303
- globals are never blamed on a test by the global-patch guard.
311
+ the inline style only: fast, and silent for a pseudo-element. `{ canvas: true }` gives every
312
+ `<canvas>` an inert 2D context (one per canvas, every call accepted, settings kept, `measureText` /
313
+ `getImageData` / gradients answered empty, `null` for any id but `'2d'`) and a `toDataURL()` of
314
+ `'data:,'` — for a chart library that draws on mount, where jsdom logs "Not implemented"; leave it off
315
+ when the `canvas` package backs jsdom. Filled before the snapshot, these globals are never blamed on a
316
+ test by the global-patch guard. Do not hand-roll a `getContext` proxy in the setup file.
304
317
 
305
318
  ```ts
306
319
  // vitest.setup.ts
307
320
  import { fillMissingDomApis } from 'vitest-auto-spy/dom-stubs';
308
321
  import { setupAutoSpy } from 'vitest-auto-spy/setup';
309
322
 
310
- fillMissingDomApis();
323
+ fillMissingDomApis({ canvas: true });
311
324
  setupAutoSpy();
312
325
  ```
313
326
 
327
+ A desktop shell's bridge global (Tauri's `__TAURI_INTERNALS__`) is not a DOM member and is not
328
+ filled here. Use the shell's own mocks — `mockWindows()` / `mockIPC()` from `@tauri-apps/api/mocks`
329
+ — in the setup file, before `setupAutoSpy()`, so the guard counts the bridge as environment; a spec
330
+ that asserts on IPC mocks `@tauri-apps/api/core` or the service wrapping it.
331
+
314
332
  ### A module mock that did nothing
315
333
 
316
334
  `vi.mock()` is the one thing in a ported suite that fails **silently**. Under a bundler
@@ -43,7 +43,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
43
43
  | `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 |
44
44
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
45
45
  | `advanceTimers() found only the clock faked, not the timers` | `mockSystemTime()` installed `Date` alone, so there is nothing to advance | `setupFakeTimers()` (or `vi.useFakeTimers()`) in the test that drives timers (§11) |
46
- | `X.method(…) was called; this strict double has nothing configured for it` | a strict double was asked for a method no line configured | paste the call the `Configure it in the test:` line names (`calledWith(args).mockReturnValue(…)`, or `mockReturnValue(…)` for any); `Called from` is the calling line; or `{ strict: false }` on that double (§5) |
46
+ | `X.method(…) was called; this strict double has nothing configured for it` | a strict double was asked for a method no line configured | paste the call the `Configure it in the test:` line names (`calledWith(args).mockReturnValue(…)`, or `mockReturnValue(…)` for any); a `void` method or an ignored result: `returnsUndefined: ['method']` in the double's options, `returnsClass: { open: MatDialogRef }` when the code uses the returned object; `Called from` is the calling line; or `{ strict: false }` on that double (§5) |
47
47
  | `X.member was read N times on a strict double and nothing configured it` (or `was subscribed to N times on a strict double and never emitted`) | `setupAutoSpy({ unconfiguredReads })`, after the test — the code under test read a strict double's spied getter and got `undefined`, or subscribed to its observable property and nothing ever emitted | the advice under each finding names the member: `accessorSpies.getters.url.mockReturnValue(…)` for a getter, `events.nextWith(…)` or `overrides: { events: new Subject() }` for a stream; `mockReturnValue(undefined)` when `undefined` is the answer meant (§5) |
48
48
  | `[vitest-auto-spy] "…" wrote to console.<method> 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 | the report names the spy to assert (`installConsoleSpies()` in a `beforeEach`, then `consoleErrorSpy`, …), or says the `vi.spyOn` calls through and needs `.mockImplementation(() => undefined)`; or fix the code that printed. Importing a spy installs nothing under the guard |
49
49
  | `… 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 |
@@ -123,7 +123,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
123
123
  | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
124
124
  | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
125
125
  | `createComponentStub(): X carries no ɵcmp, ɵdir or ɵpipe to copy` | the class is not an Angular declaration, or the import resolved to `undefined` (a barrel split across chunks) | pass the `@Component` / `@Directive` / `@Pipe` class, imported from its own file (§13) |
126
- | `'isKeyEnabled' does not exist in type 'MethodReturns<{ flagsConfig: any; }>'` | generic class: `createSpyFromClass(Config, { gettersToSpyOn: ['flagsConfig'], returns: { … } })` reads `T` back from the list, because the generic class argument is checked after the configuration | generic class: spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` from `/angular` infers it (§6) |
126
+ | `'isKeyEnabled' does not exist in type 'MethodReturns<{ flagsConfig: any; }>'` | generic class: `createSpyFromClass(Config, { gettersToSpyOn: ['flagsConfig'], returns: { … } })` reads `T` back from the list, because the generic class argument is checked after the configuration | fixed: every factory takes `T` from the class alone; on an older release, spell out the type argument — `createSpyFromClass<Config>(Config, …)` (§6) |
127
127
  | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
128
128
  | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
129
129
  | `… was defined outside of the module's top level scope`, collecting the file on Vitest 5 | `vi.mock` / `vi.unmock` / `vi.hoisted` inside a `describe`, a hook or a helper — a warning on Vitest 4, a throw on 5 (`doctor`: `vitest-5-nested-hoist`) | move it to the top level; a per-test mock is `vi.doMock` plus a dynamic `import()` |
@@ -156,10 +156,13 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
156
156
  | `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] })`; on Angular ≥ 22.2, for a directive with no static host attribute, `TemplateRef` or sibling markup, `TestBed.createDirective(Dir, { tagName, bindings })` (§13) |
157
157
  | `setInputs: … declares no input named …` (with a `Did you mean` for a near typo) | the key is not an input of that component — a typo, an input renamed since, or one of its own fields | take the suggestion, or set a name the message lists; a signal the component owns is moved through the signal (§13) |
158
158
  | `setInputs: X carries no ɵcmp, so there are no inputs to set` (or `the component class is undefined`, `X is a @Pipe`) | the fixture's `componentType` is not a compiled `@Component` — a `@Pipe`, a class Angular never compiled, or `undefined` from a barrel split across chunks | pass the `@Component` class the fixture was created from; call a pipe's `transform()` directly; for `undefined` import the class from its own file (§13) |
159
+ | `NG01914: <x> is an invalid [formField] directive host`, from `renderShallow(X, { keepTemplate: true })` | the kept template binds a signal-forms `[formField]` to a child component, and the child was dropped — a bare tag has no `value` / `checked` model | upgrade — a dropped child that is a custom form control renders as `createComponentStub(child)`; `keepChildren: [Child]` keeps the real one (§13) |
159
160
  | `TypeError: dependencies is not a function`, from `renderShallow({ keepTemplate: true })` | the compiled `ɵcmp.dependencies` is the flat array AOT emits when no cycle forced a factory, and the trim called it | upgrade — all three shapes (array, factory, `null`) are read; nothing to change in the spec |
160
161
  | `renderShallow(X, { keepTemplate: true }): … is declared by Module, not standalone` (or `by an NgModule`) | an AOT dependency list: ngtsc flattened an imported `NgModule`'s exported declarations into it, and Angular takes only standalone declarations and `NgModule`s in `imports` | name that module in `keepModules: [Module]` and it is put back whole — the message names it when the spec imports it; or drop `keepTemplate` when the spec reads TypeScript state only |
161
162
  | `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` |
162
163
  | `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) |
164
+ | `throwWith() was configured on 'x' after mockImplementation() had replaced its dispatch` (also `nextWith`, `resolveWith`, `failWith`, … and `mockReturnValue()`) | a stream or promise helper written after a host implementation: the dispatch that reads it is gone, so every call still answers what `mockImplementation` installed | `spy.x.mockReset()` before the helper, or seed the earlier default with `nextOneTimeWith` / `returns:` instead of `mockImplementation` (§4) |
165
+ | `throwWith(error)` never fails the test, with no message — the code reads the stream through `toSignal`, `rxResource` or an `effect` | not the spy: it replays the error to every subscriber. `toSignal` keeps it until the signal is read, `rxResource` keeps it in `error()`, and an `effect` runs (and reaches `ErrorHandler`) only once something flushes it | read it where it is kept: `expect(() => service.x()).toThrow(…)`; `await settleResource(res)` then `res.error()`; `expect(() => TestBed.tick()).toThrow(…)` for an effect (§4) |
163
166
  | `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` |
164
167
  | `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 |
165
168
  | `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 |
@@ -100,7 +100,7 @@ export default [
100
100
  | `no-mock-cast` | `error` | suggest | a cast to Vitest's `Mock` / `MockInstance` over a **member access** — `TestBed.inject(S).m as Mock` → `injectSpy(S).m`, which the suggestion writes whenever the token is in view (directly, or through a `const` the file settled with a `TestBed.inject`). `Mock` with no parameters is `Mock<any>`: `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments. `(spy.m.mockReturnValue as Mock)(…)` gets its own message — the cast is on the member that installs the answer, so neither the value nor the method's return type is checked. The name must resolve to an import from `vitest` / `@rstest/core` / `bun:test` / `jest`, or to nothing at all, so a domain `Mock` is left alone |
101
101
  | `no-outer-binding-in-mock-factory` | `error` | — | a `vi.mock(path, factory)` factory reading a top-level `const` / `let` / `var` / `class` declared outside `vi.hoisted` — the factory is hoisted above it, so the read is `Cannot access 'x' before initialization` → `const { user } = vi.hoisted(() => ({ user: … }))`. Only reads made while the factory runs count; function declarations, imports, type positions and `vi.doMock` are not reported |
102
102
  | `no-relative-mock-under-builder` | `error` | — | `vi.mock` / `doMock` / `importMock` / `unmock` / `doUnmock` with a specifier starting `.` or `/` in a spec `@angular/build:unit-test` runs — the builder throws on it → `provideAutoSpy(X)`, or `overrideComponentProvider` for a component's own provider. **Silent until** an `@angular/build:unit-test` / `@nx/angular:unit-test` target serving the file is found (the `no-redundant-mock-reset` search) or `{ builder: 'unit-test' }` says so; a tsconfig path alias to a workspace file is reported too (the builder bundles it, so the mock silently does nothing) unless every run's `buildTarget` lists it in `externalDependencies` |
103
- | `no-real-wait-in-test` | `warn` | — | a sleep on the real clock — `new Promise((r) => setTimeout(r, N))` in any spelling where `resolve` reaches the timer, `setTimeout(N)` from `node:timers/promises`; a `sleep(ms)` helper once, at its definition; a missing or `0` delay is left alone → `setupFakeTimers()` + `await advanceTimers(N)`, or wait for the outcome (`vi.waitFor`, `fixture.whenStable()`) |
103
+ | `no-real-wait-in-test` | `warn` | suggest | a sleep on the real clock — `new Promise((r) => setTimeout(r, N))` in any spelling where `resolve` reaches the timer, `setTimeout(N)` from `node:timers/promises`; a `sleep(ms)` helper once, at its definition; a missing or `0` delay is left alone → `setupFakeTimers()` + `await advanceTimers(N)`, or wait for the outcome (`vi.waitFor`, `fixture.whenStable()`); a sleep awaited on the spot gets a suggestion of `await flushEventLoop()` — one real turn, enough when it only lets mocked promises settle, but it fires no timer |
104
104
  | `no-inline-test-data` | `warn` | — | test data inline in a spec — a data literal over `maxLines` (20) lines, or the same literal `repeats` (3) times in one file with at least `minValues` (2) values, one spelled out → export it once from `<name>.mock.ts` next to the spec (a `tsc`-built library must add `**/*.mock.ts` to its build tsconfig's `exclude`). A copy that reads a name the spec declares → a spec-local `const` instead. A long expected value (`toEqual`, `toMatchObject`, `toHaveBeenCalledWith`…) → keep its shape inline, move the part the message names (the smallest one that gets it under `maxLines`, a plain-value list or template string first). A literal repeated only as an expected value (`.resolves` / `.rejects` / `.not` included) → a spec-local `const`; a short expected object (≤ 3 keys, plain values) is not a repeat. Never reported: module wiring (`providers`, `imports`…), `it.each` tables, `vi.mock` factories, literals holding a function, short tuples (`[1, 5]`, `['0', '1']`) and flag bags (≤ 3 boolean keys) as repeats, and files named `*.mock.ts` / `*.fixture.ts` / under `__mocks__/` |
105
105
  | `no-disabled-testbed-teardown` | `error` | — | `destroyAfterEach: false`, in `initTestEnvironment` or one `configureTestingModule` — every fixture outlives its test, `ngOnDestroy` never runs → delete it (Angular's default since v13) and fix the test that then fails. A config scoped to `**/*.spec.ts` must add the setup file to `files` |
106
106
  | `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset the runner already performs between tests — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — standing where nothing the file wrote ran since the runner's own: the first statement of a `beforeEach` no other `beforeEach` precedes, or a clear as the last statement of an `afterEach` → delete it, and the hook with it where that is all it held. A restore or reset in `afterEach` / `afterAll` is never reported: after the last test it is what takes a spy off before the `afterAll` hooks. **Silent** until the flags are known: options `{ clearMocks, restoreMocks, mockReset }` (or `{ configFile }` naming the runner config) first, then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text; a config that leaves `clearMocks` out counts it on from Vitest 5 (its default). Where an `@angular/build:unit-test` / `@nx/angular:unit-test` target (found in `angular.json` / `project.json` above the file) also serves the spec, a config flag counts only if that target's `runnerConfig` applies it too — without `runnerConfig` the builder runs Vitest's defaults, so `restoreMocks` / `mockReset` from `vitest.config.ts` do not make a reset dead there, while a clear stays reported on Vitest 5; the rule's inline flags are not narrowed. A config built by a factory or `mergeConfig` from another module is not read through — name its flags as `{ configFile, configFlags: { clearMocks: true } }`, read as if the file set them, so the builder narrowing still applies (`configFlags` needs `configFile`; `doctor` notes the case as `mock-reset-config-unread`). `setupAutoSpy({ restoreMocks: true })` is not the runner's `restoreMocks` (it restores in an `afterEach`, after the test) — never pass the rule a flag only the setup file sets. Flag matches call, never family — `restoreMocks` walks only the spies `vi.spyOn` installed. `--fix` only in a file whose only before-hook it is; a suggestion otherwise; never inside a test body |
@@ -116,7 +116,7 @@ Fifty-eight rules, **every one an `error` except `prefer-render-shallow`,
116
116
  `no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
117
117
  `prefer-set-inputs`, `no-unasserted-argument`, `prefer-spy-on-own-method`, `prefer-to-have-signal-value`,
118
118
  `no-unasserted-console-spy`, `no-real-wait-in-test`, `no-inline-test-data` and
119
- `no-redundant-as-instance`**; nine fix on their own, nineteen offer suggestions. Fifty-four are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
119
+ `no-redundant-as-instance`**; nine fix on their own, twenty offer suggestions. Fifty-four are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
120
120
  `no-unknown-use-value-key` and `prefer-to-have-signal-value` read types, and all four report nothing at all without `parserOptions.project` / `projectService`
121
121
  rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
122
122
  builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it;
@@ -43,11 +43,8 @@ They return values immediately, so you need no listener spy, collection array, m
43
43
  ```ts
44
44
  import { expectEmissionSync, expectEmissionsSync } from 'vitest-auto-spy';
45
45
 
46
- expect(
47
- expectEmissionSync(component.closed, {
48
- advance: () => component.close(),
49
- }),
50
- ).toBeUndefined();
46
+ // `closed` is an `output<void>`: the call is the assertion, it throws unless exactly one value arrives
47
+ expectEmissionSync(component.closed, { advance: () => component.close() });
51
48
 
52
49
  expect(
53
50
  expectEmissionsSync(component.selected, 2, {
@@ -59,6 +56,10 @@ expect(
59
56
  ).toEqual([15, 20]);
60
57
  ```
61
58
 
59
+ On a `void` output, call the helper as a statement: wrapping it in `expect(…).toBeUndefined()` trips
60
+ `@typescript-eslint/no-confusing-void-expression`, and `vitest/expect-expect` already counts `expect*`
61
+ calls as assertions (`assertFunctionNames: ['expect*']`).
62
+
62
63
  The helper subscribes first, runs `advance` once, then unsubscribes before returning or throwing.
63
64
  Values emitted during subscription count too. Use `{ skip: 1 }` to ignore a replayed initial value.
64
65
  `until` filters accepted values; `label` names the source in failures. These are the only other options: there is no `timeout`.