vitest-auto-spy 5.66.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.
- package/AGENTS.md +9 -8
- package/CHANGELOG.md +120 -1
- package/README.md +12 -11
- package/agent-docs/angular.md +3 -0
- package/agent-docs/anti-patterns.md +2 -2
- package/agent-docs/checklist.md +10 -0
- package/agent-docs/configuration.md +4 -0
- package/agent-docs/doubles.md +21 -3
- package/agent-docs/errors.md +5 -2
- package/agent-docs/eslint.md +2 -2
- package/agent-docs/observables.md +6 -5
- package/agent-docs/return-helpers.md +38 -2
- package/dist/angular-diagnostics.js +9 -9
- package/dist/angular-doubles.js +14 -14
- package/dist/angular-http.js +4 -4
- package/dist/angular-matchers.js +1 -1
- package/dist/angular-router.js +6 -6
- package/dist/angular.d.ts +4 -3
- package/dist/angular.js +34 -8
- package/dist/bun-angular.d.ts +3 -2
- package/dist/bun-angular.js +201 -51
- package/dist/bun.d.ts +2 -1
- package/dist/bun.js +11 -11
- package/dist/{chunk-EZTP5VUF.js → chunk-2KW2IWIH.js} +4 -4
- package/dist/{chunk-XZS56XV5.js → chunk-2NAHRTOC.js} +1 -1
- package/dist/{chunk-XLKBJSYQ.js → chunk-6DZMSVSY.js} +3 -3
- package/dist/{chunk-PS6UQC4C.js → chunk-6SB6DLGG.js} +1 -1
- package/dist/{chunk-5GKIQX65.js → chunk-ARQ3C27A.js} +3 -3
- package/dist/{chunk-KCQOZSNA.js → chunk-BOOQKIP2.js} +7 -7
- package/dist/{chunk-EZKDQ4WO.js → chunk-CYVUUAEG.js} +1 -1
- package/dist/{chunk-A2X7O5EP.js → chunk-DKD6CTLO.js} +2 -2
- package/dist/{chunk-7WU53KRS.js → chunk-DNKAWUL6.js} +2 -1
- package/dist/{chunk-OXE6FCQX.js → chunk-DSRYDTZG.js} +4 -4
- package/dist/{chunk-QATP6Q3G.js → chunk-EVC6RR4C.js} +1 -1
- package/dist/{chunk-OIHOH2RY.js → chunk-EZ5DTZGI.js} +1 -1
- package/dist/{chunk-7GOVSGXB.js → chunk-FBLX5XPI.js} +2 -2
- package/dist/{chunk-DWMIQ7VO.js → chunk-FSBPGITE.js} +1 -1
- package/dist/{chunk-2CWSOIBW.js → chunk-JKRICBGX.js} +1 -1
- package/dist/{chunk-TRRI7TXE.js → chunk-JQDO2A5C.js} +1 -1
- package/dist/{chunk-NMXLXBXH.js → chunk-O4WLGJOB.js} +2 -2
- package/dist/{chunk-HIGHZ4BW.js → chunk-QAEK57O6.js} +24 -9
- package/dist/{chunk-DCCURN7I.js → chunk-RHTW4M6I.js} +1 -1
- package/dist/{chunk-IXARK3B6.js → chunk-YR6YOBTZ.js} +2 -2
- package/dist/cli.js +487 -225
- package/dist/console.js +8 -8
- package/dist/core.d.ts +66 -62
- package/dist/dom-stubs.d.ts +22 -3
- package/dist/dom-stubs.js +108 -3
- package/dist/{emission-positive-sync-BrSc2lb-.d.ts → emission-positive-sync-CgpTvdAY.d.ts} +2 -1
- package/dist/eslint-plugin.cjs +96 -61
- package/dist/index.d.ts +2 -1
- package/dist/index.js +23 -8
- package/dist/internal-types-CjExK2uA.d.ts +16 -0
- package/dist/jasmine-compat.js +2 -2
- package/dist/jasmine.d.ts +3 -2
- package/dist/jasmine.js +12 -12
- package/dist/nestjs.d.ts +2 -1
- package/dist/nestjs.js +10 -10
- package/dist/node.cjs +23 -8
- package/dist/node.d.ts +2 -1
- package/dist/node.js +23 -8
- package/dist/react.d.ts +2 -1
- package/dist/rstest.d.ts +2 -1
- package/dist/rstest.js +10 -10
- package/dist/rxjs.js +50 -8
- package/dist/signal-forms.js +1 -1
- package/dist/svelte.d.ts +2 -1
- package/dist/{track-signal-runs-DLvVp9iI.d.ts → track-signal-runs-Bgm7l8At.d.ts} +6 -8
- package/dist/vue.d.ts +3 -2
- package/dist/zone.js +1 -1
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +142 -140
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.
|
|
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`
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,124 @@ 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
|
+
|
|
13
131
|
## [5.66.0] - 2026-10-28
|
|
14
132
|
|
|
15
133
|
### Added
|
|
@@ -6902,7 +7020,8 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
|
|
|
6902
7020
|
This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
|
|
6903
7021
|
[full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
|
|
6904
7022
|
|
|
6905
|
-
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.
|
|
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
|
|
6906
7025
|
[5.66.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.65.0...v5.66.0
|
|
6907
7026
|
[5.65.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.64.0...v5.65.0
|
|
6908
7027
|
[5.64.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.63.0...v5.64.0
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ replacement for [`jest-auto-spies`](https://www.npmjs.com/package/jest-auto-spie
|
|
|
15
15
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
16
16
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
17
17
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
18
|
-
[](#install)
|
|
19
19
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
20
20
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
21
21
|
[](./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
|
-
|
|
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
|
|
|
@@ -229,11 +229,11 @@ Full page: [Control helpers](https://asdalexey.github.io/vitest-auto-spy/core/co
|
|
|
229
229
|
|
|
230
230
|
Each spied method gets helpers for what it returns:
|
|
231
231
|
|
|
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); `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()` |
|
|
237
237
|
|
|
238
238
|
```ts
|
|
239
239
|
products.load.resolveWith([product]); // load(): Promise<Product[]>
|
|
@@ -2160,6 +2160,7 @@ users.currentTenant(); // throws here, on the line that called it
|
|
|
2160
2160
|
[vitest-auto-spy] Cart.checkout(1, 'now') was called; this strict double has nothing configured for it.
|
|
2161
2161
|
Called from src/app/cart.component.ts:41:12
|
|
2162
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.
|
|
2163
2164
|
Docs: https://asdalexey.github.io/vitest-auto-spy/core/strict-mode#the-message
|
|
2164
2165
|
```
|
|
2165
2166
|
|
|
@@ -3124,7 +3125,7 @@ only. Full contracts: [element and event stubs](https://asdalexey.github.io/vite
|
|
|
3124
3125
|
|
|
3125
3126
|
| Utility | Entry point | What it's for |
|
|
3126
3127
|
| ----------------------------------------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3127
|
-
| `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` |
|
|
3128
3129
|
| `createPasteEvent(text, init?)` / `createDropEvent(text, init?)` | `/dom-stubs` | Text transfers with native `defaultPrevented`; `null` represents absent transfer data |
|
|
3129
3130
|
| `mockElementRefProp(component, key, opts?)` | `/angular/doubles` | Inferred element spies, `ref` and writable `query`, installed together |
|
|
3130
3131
|
| `injectSpy(token)` / `injectSpy(moduleRef, token)` | `/angular`, `/nestjs` | Pull a provided spy out of the DI container, already typed as `Spy<T>` — no casting |
|
|
@@ -3156,7 +3157,7 @@ only. Full contracts: [element and event stubs](https://asdalexey.github.io/vite
|
|
|
3156
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) |
|
|
3157
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` |
|
|
3158
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` |
|
|
3159
|
-
| `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 }` |
|
|
3160
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 |
|
|
3161
3162
|
| `flushEventLoopUntil(isDone, opts?)` | core | Real turns until a condition holds — a `resource()` leaving `loading` — with a budget instead of a hang |
|
|
3162
3163
|
| `stubMediaElement(opts?)` | `/dom-stubs` | A `<video>` / `<audio>` that plays, reports a duration and fires the media events jsdom never does |
|
|
@@ -3625,7 +3626,7 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
|
3625
3626
|
project takes file by file (`prefer-render-shallow`, `prefer-set-inputs`) or decide on a heuristic.
|
|
3626
3627
|
- `autoSpy.configs.typeErrors` holds the rules whose findings are compile errors (`prefer-as-spy`,
|
|
3627
3628
|
`no-mocked-for-spy`). Spread it after a blanket downgrade so they keep their severity.
|
|
3628
|
-
-
|
|
3629
|
+
- Nine rules fix with `--fix`; twenty offer editor suggestions.
|
|
3629
3630
|
|
|
3630
3631
|
**Common mistake:** adding a bare `rules` key next to the spread config to change one severity. It
|
|
3631
3632
|
replaces the whole rule map. Spread the rules too:
|
|
@@ -3679,7 +3680,7 @@ Rolling it out on a large suite, and the options each rule takes:
|
|
|
3679
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 |
|
|
3680
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 |
|
|
3681
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 |
|
|
3682
|
-
| [`no-real-wait-in-test`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-real-wait-in-test) | `warn` |
|
|
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 |
|
|
3683
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 |
|
|
3684
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 |
|
|
3685
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 |
|
package/agent-docs/angular.md
CHANGED
|
@@ -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()` |
|
package/agent-docs/checklist.md
CHANGED
|
@@ -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
|
package/agent-docs/doubles.md
CHANGED
|
@@ -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.
|
|
303
|
-
|
|
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
|
package/agent-docs/errors.md
CHANGED
|
@@ -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 |
|
|
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 |
|
package/agent-docs/eslint.md
CHANGED
|
@@ -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` |
|
|
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,
|
|
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
|
-
|
|
47
|
-
|
|
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`.
|
|
@@ -65,8 +65,8 @@ users.load.resolveWithPerCall([{ value: a }, { value: b }]);
|
|
|
65
65
|
expect(users.load.mock.settledResults).toEqual([{ type: 'fulfilled', value: { id: 1 } }]);
|
|
66
66
|
|
|
67
67
|
// observables — requires `import 'vitest-auto-spy/rxjs'` once
|
|
68
|
-
feed.items$.nextWith([item]); // emit, stream stays open
|
|
69
|
-
feed.items$.nextOneTimeWith([item]); // emit once, then complete
|
|
68
|
+
feed.items$.nextWith([item]); // emit, stream stays open — a store, a socket
|
|
69
|
+
feed.items$.nextOneTimeWith([item]); // emit once, then complete — HTTP, IPC, any request/response
|
|
70
70
|
feed.items$.nextWithValues([{ value: a }, { value: b, delay: 100 }, { complete: true }]);
|
|
71
71
|
const [first$, second$] = feed.watch$.nextWithPerCall([{ value: 'a' }, { value: 'b', doNotComplete: true }]);
|
|
72
72
|
feed.items$.throwWith('FAKE ERROR');
|
|
@@ -221,6 +221,42 @@ a spy hoisted to `describe` scope — put `resetAutoSpy(spy)` in `beforeEach`. I
|
|
|
221
221
|
sequence `nextWith(a)` then `throwWith(e)` still means "emit a, then fail"; only a reset or a
|
|
222
222
|
terminal call starts a new stream.
|
|
223
223
|
|
|
224
|
+
**Seed a request/response method with `nextOneTimeWith`, not `nextWith`.** An HTTP or IPC call emits
|
|
225
|
+
once and completes; a `nextWith` stream never completes, so `forkJoin`, `lastValueFrom`, `toArray()`
|
|
226
|
+
and `expectAllEmissions` over it wait for ever and the test reads the initial state. Seeded in
|
|
227
|
+
`beforeEach`, its buffered value also reaches the subscriber of a later `throwWith` first. That last
|
|
228
|
+
case is reported: `X.m.throwWith() ran on a stream that still holds a value from an earlier
|
|
229
|
+
nextWith() and has no subscriber` — a warning once per spied member, a throw under
|
|
230
|
+
`misconfiguration: 'throw'`. Fix: `nextOneTimeWith` for the seed, or `resetAutoSpy(spy.m)` before
|
|
231
|
+
`throwWith`, or `nextWithValues([{ value }, { errorValue }])` when "emit, then fail" is the point. A
|
|
232
|
+
subscriber already on the stream is not reported. To keep a call pending after such a seed,
|
|
233
|
+
`returnSubject()` is wrong (it hands back the subject holding the seed): `resetAutoSpy(spy.m)` then
|
|
234
|
+
`spy.m.mockReturnValue(NEVER)`.
|
|
235
|
+
|
|
236
|
+
**A `throwWith` error reaches every subscriber; whether it reaches the test is the consumer's call.**
|
|
237
|
+
The spy replays the error to a subscriber that arrives after `throwWith`, so nothing is lost on this
|
|
238
|
+
side. Angular's consumers keep it instead of handing it to `ErrorHandler`, and the test stays green
|
|
239
|
+
unless it reads the error where it was kept:
|
|
240
|
+
|
|
241
|
+
- `toSignal(source$)` stores it and throws it when the signal is **read** —
|
|
242
|
+
`expect(() => service.bonds()).toThrow('offline')`. A signal nothing reads never fails.
|
|
243
|
+
- `rxResource` / `resource` catch it: `await settleResource(service.bonds)`, then
|
|
244
|
+
`expect(service.bonds.error()).toEqual(error)` (`status()` is `'error'`; `value()` throws).
|
|
245
|
+
- An `effect` (or a template) that reads such a signal sends the throw to `ErrorHandler`, which
|
|
246
|
+
TestBed rethrows — but only once the effect runs: `expect(() => TestBed.tick()).toThrow('offline')`,
|
|
247
|
+
or `await fixture.whenStable()`. Injecting the service alone runs nothing.
|
|
248
|
+
- `.subscribe(next)` with no error callback: rxjs rethrows from a `setTimeout` after the test;
|
|
249
|
+
`strayTimers` turns that into a failure of the test (§10), `expectUnhandledObservableErrors()`
|
|
250
|
+
asserts it.
|
|
251
|
+
|
|
252
|
+
**A stream or promise helper written after `mockImplementation` / `mockReturnValue` answers nothing.**
|
|
253
|
+
Those replace the spy's dispatch, and the dispatch is what reads `throwWith`, `nextWith`,
|
|
254
|
+
`resolveWith`, `failWith` and the rest. The order is reported — `throwWith() was configured on 'load'
|
|
255
|
+
after mockImplementation() had replaced its dispatch` — a warning, a throw under
|
|
256
|
+
`misconfiguration: 'throw'`; the behaviour is unchanged. Fix: `spy.m.mockReset()` before the helper,
|
|
257
|
+
or seed the earlier default with `nextOneTimeWith` / `returns:` instead of `mockImplementation`. The
|
|
258
|
+
other order (helper, then `mockImplementation`) is ordinary last-writer-wins and is not reported.
|
|
259
|
+
|
|
224
260
|
### Keeping an awaited collaborator pending
|
|
225
261
|
|
|
226
262
|
`createDeferred<T>()` from the core entry returns `{ promise, resolve, reject }`. Return `promise`
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { pendingRequestsReport, instrumentTestBed, onTestingModuleConfigured, onComponentCreated, HTTP_TESTING_BRAND, beforeTestBedReset, verifyOnTeardown } from './chunk-C7K75NLS.js';
|
|
2
2
|
export { disableTestBedDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, reportSpecTiming } from './chunk-C7K75NLS.js';
|
|
3
|
-
import { assertAngularInternals } from './chunk-
|
|
4
|
-
import { failOnUnspiedProvider, onTestBedFileEnd, isLazyDoubleFactory } from './chunk-
|
|
3
|
+
import { assertAngularInternals } from './chunk-EZ5DTZGI.js';
|
|
4
|
+
import { failOnUnspiedProvider, onTestBedFileEnd, isLazyDoubleFactory } from './chunk-DSRYDTZG.js';
|
|
5
5
|
import './chunk-T5MQSNRX.js';
|
|
6
|
-
import './chunk-
|
|
7
|
-
import './chunk-
|
|
8
|
-
import './chunk-
|
|
9
|
-
import './chunk-
|
|
6
|
+
import './chunk-2NAHRTOC.js';
|
|
7
|
+
import './chunk-ARQ3C27A.js';
|
|
8
|
+
import './chunk-6DZMSVSY.js';
|
|
9
|
+
import './chunk-EVC6RR4C.js';
|
|
10
10
|
import './chunk-PC3FAZT3.js';
|
|
11
|
-
import './chunk-
|
|
12
|
-
import { isAutoSpyLike } from './chunk-
|
|
11
|
+
import './chunk-QAEK57O6.js';
|
|
12
|
+
import { isAutoSpyLike } from './chunk-JKRICBGX.js';
|
|
13
13
|
import { count, sourceClassName, taskName } from './chunk-DYAS4OK4.js';
|
|
14
|
-
import { angularPendingRequests, angularShadowedProviders, angularDeadSchemas, angularNgModuleScopesAuto } from './chunk-
|
|
14
|
+
import { angularPendingRequests, angularShadowedProviders, angularDeadSchemas, angularNgModuleScopesAuto } from './chunk-DNKAWUL6.js';
|
|
15
15
|
import { withDocs } from './chunk-ENCEEH23.js';
|
|
16
16
|
import { getTestBed } from '@angular/core/testing';
|
|
17
17
|
import { beforeEach, afterEach } from 'vitest';
|