vitest-auto-spy 5.49.0 → 5.51.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 +192 -195
- package/CHANGELOG.md +527 -1
- package/README.md +2097 -2706
- package/agent-docs/adapters.md +72 -10
- package/agent-docs/angular.md +185 -155
- package/agent-docs/anti-patterns.md +1 -1
- package/agent-docs/checklist.md +49 -17
- package/agent-docs/configuration.md +23 -16
- package/agent-docs/doubles.md +48 -5
- package/agent-docs/errors.md +35 -6
- package/agent-docs/eslint.md +36 -50
- package/agent-docs/factories.md +48 -66
- package/agent-docs/migration-jasmine.md +9 -2
- package/agent-docs/observables.md +28 -8
- package/agent-docs/properties.md +2 -5
- package/agent-docs/return-helpers.md +35 -18
- package/agent-docs/setup.md +127 -48
- package/dist/angular-diagnostics.js +27 -17
- package/dist/angular-doubles.d.ts +214 -3
- package/dist/angular-doubles.js +276 -11
- package/dist/angular-http.d.ts +12 -5
- package/dist/angular-http.js +16 -15
- package/dist/angular-matchers.js +6 -4
- package/dist/angular-router.d.ts +3 -4
- package/dist/angular-router.js +7 -7
- package/dist/angular.d.ts +33 -8
- package/dist/angular.js +277 -34
- package/dist/bun-angular.d.ts +7 -7
- package/dist/bun-angular.js +83 -36
- package/dist/bun.d.ts +6 -1168
- package/dist/bun.js +13 -10
- package/dist/{chunk-GG7BQKEK.js → chunk-4JY7KIJG.js} +1 -1
- package/dist/{chunk-TXDBDMIL.js → chunk-75WPOZKE.js} +1 -1
- package/dist/{chunk-QGLUX55Q.js → chunk-7LZ6V5UO.js} +185 -43
- package/dist/{chunk-FOL5RH22.js → chunk-F7UPX6YV.js} +1 -1
- package/dist/{chunk-M5H32F4Q.js → chunk-GHKBJN2T.js} +80 -30
- package/dist/chunk-HRE4K6N3.js +30 -0
- package/dist/{chunk-5FQZGJLJ.js → chunk-IATDAQMO.js} +8 -2
- package/dist/{chunk-3JL2YWIG.js → chunk-JCTHGJTY.js} +20 -8
- package/dist/{chunk-MNY4ZHEQ.js → chunk-JMBAECZN.js} +38 -21
- package/dist/{chunk-XEIOGUGP.js → chunk-M4CUD2XH.js} +1 -1
- package/dist/{chunk-PUUSCWDD.js → chunk-MDWE3FCP.js} +90 -11
- package/dist/chunk-MXKNUVYR.js +38 -0
- package/dist/chunk-OXONPFSL.js +217 -0
- package/dist/{chunk-3ZBRBPOL.js → chunk-R43PMWFH.js} +1 -1
- package/dist/{chunk-UH3KWLVL.js → chunk-RHGDQQNC.js} +3 -2
- package/dist/{chunk-HVNKTPKG.js → chunk-SF4GKKN4.js} +1 -9
- package/dist/chunk-SOROJD3W.js +9 -0
- package/dist/chunk-T5MQSNRX.js +23 -0
- package/dist/chunk-TIXTHSW4.js +216 -0
- package/dist/{chunk-Q7L4AWI7.js → chunk-UOKFQQ5H.js} +1 -1
- package/dist/{chunk-I6CB4CNT.js → chunk-UTOPAD66.js} +160 -92
- package/dist/{chunk-QDBRZQJO.js → chunk-VLEU3CWT.js} +5 -2
- package/dist/chunk-ZVF3E6T7.js +18 -0
- package/dist/cli-FXW5AFQ5.js +2234 -0
- package/dist/cli-SELXGLDQ.js +819 -0
- package/dist/cli.js +2350 -3563
- package/dist/console.d.ts +26 -15
- package/dist/console.js +91 -31
- package/dist/core.d.ts +1146 -0
- package/dist/diagnostics.d.ts +2 -2
- package/dist/diagnostics.js +3 -0
- package/dist/dom-stubs.d.ts +130 -3
- package/dist/dom-stubs.js +578 -76
- package/dist/{expect-emission-DMQZF5QR.d.ts → emission-sync-xEda8ZCk.d.ts} +20 -2
- package/dist/eslint-plugin.cjs +963 -185
- package/dist/index.d.ts +6 -5
- package/dist/index.js +595 -224
- package/dist/jasmine-compat.d.ts +3 -4
- package/dist/jasmine-compat.js +2 -2
- package/dist/jasmine.d.ts +4 -3
- package/dist/jasmine.js +28 -10
- package/dist/nest-unit-Bg6JWPgu.d.ts +59 -0
- package/dist/nestjs.d.ts +4 -60
- package/dist/nestjs.js +13 -175
- package/dist/node.cjs +552 -156
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +6 -6
- package/dist/node.js +552 -158
- package/dist/observer-spy.d.ts +39 -6
- package/dist/observer-spy.js +41 -8
- package/dist/{package-identity-5kYzh3ad.d.ts → package-identity-COBYU0WJ.d.ts} +4 -2
- package/dist/perf-profiler.js +26 -3
- package/dist/perf-reporter.d.ts +35 -1
- package/dist/perf-reporter.js +132 -17
- package/dist/{prop-mock-WOpRQPUc.d.ts → prop-mock-CE9gNMPY.d.ts} +18 -8
- package/dist/react.d.ts +6 -5
- package/dist/react.js +1 -1
- package/dist/rstest.d.ts +5 -6
- package/dist/rstest.js +14 -11
- package/dist/rxjs.d.ts +4 -5
- package/dist/rxjs.js +12 -6
- package/dist/setup.d.ts +50 -8
- package/dist/setup.js +480 -118
- package/dist/signal-forms.js +4 -3
- package/dist/svelte.d.ts +6 -5
- package/dist/svelte.js +1 -1
- package/dist/{track-injections-CYQKc3M8.d.ts → track-injections-C6zITWXE.d.ts} +1 -1
- package/dist/{track-signal-runs-CUdp5dJZ.d.ts → track-signal-runs-DDzbFmGc.d.ts} +32 -8
- package/dist/{types-Dxw-oWfr.d.ts → types-qYoxQi5C.d.ts} +127 -7
- package/dist/vitest-mock-types-B17-xaWt.d.ts +10 -0
- package/dist/vue.d.ts +7 -6
- package/dist/vue.js +1 -1
- package/dist/zone.d.ts +2 -1
- package/dist/zone.js +24 -12
- package/package.json +24 -9
- package/skills/vitest-auto-spy/SKILL.md +51 -39
- package/dist/chunk-FQ6NT2IN.js +0 -64
- package/dist/chunk-IB72SVKT.js +0 -23
- package/dist/chunk-NGZYYSEA.js +0 -54
package/AGENTS.md
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
# vitest-auto-spy — instructions for AI coding agents
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from a class, a type, or nothing at all, on Vitest
|
|
3
|
+
Reference for an agent **writing tests with `vitest-auto-spy`** in someone else's project: typed
|
|
4
|
+
test spies generated from a class, a type, or nothing at all, on Vitest, `bun:test`, `node:test` and
|
|
5
|
+
Rstest. It ships in the npm package, so it works offline: `node_modules/vitest-auto-spy/AGENTS.md`.
|
|
6
|
+
Working on the library's own source? Read `CONTRIBUTING.md` in the repository instead.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
The spec you will write most often, an Angular service with a spied store and HTTP (the full recipe
|
|
9
|
+
is §3):
|
|
8
10
|
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
```ts
|
|
12
|
+
import { TestBed } from '@angular/core/testing';
|
|
13
|
+
import { type Spy, injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
|
|
14
|
+
import { expectRequest, provideHttpTesting } from 'vitest-auto-spy/angular-http';
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
let store: Spy<CartStore>;
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
TestBed.configureTestingModule({ providers: [...provideHttpTesting(), provideAutoSpy(CartStore)] });
|
|
20
|
+
store = injectSpy(CartStore);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('posts the order', async () => {
|
|
24
|
+
const order = TestBed.inject(OrderService).checkout(); // Promise<Order>, sent through HttpClient
|
|
25
|
+
|
|
26
|
+
await expectRequest('/api/orders', { method: 'POST' }).flush({ id: 1 });
|
|
27
|
+
await expect(order).resolves.toEqual({ id: 1 });
|
|
28
|
+
expect(store.clear).toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To point a team's agents at this file, run `npx vitest-auto-spy init`: it adds a pointer to
|
|
33
|
+
`AGENTS.md`, `CLAUDE.md`, `GEMINI.md` and the rule files of tools already set up, all or nothing.
|
|
34
|
+
`--check` is the CI form and fails on a `stale` hand-made skill copy (delete it, re-run `init`) or an
|
|
35
|
+
`edited` managed block. `--only CLAUDE.md,.claude` limits it to untracked files. Full table:
|
|
36
|
+
<https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
23
37
|
|
|
24
38
|
| Resource | Where |
|
|
25
39
|
| --- | --- |
|
|
@@ -80,60 +94,47 @@ Add-ons, orthogonal to the runner:
|
|
|
80
94
|
| Add-on | Import | Needed for |
|
|
81
95
|
| --- | --- | --- |
|
|
82
96
|
| Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, in a setup file, a spec, or a `.d.ts` the `tsconfig` includes (§4) |
|
|
83
|
-
| observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20).
|
|
84
|
-
| Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `useConsoleSpies()` in the `describe` (or `installConsoleSpies()` per test, `restoreConsole()` after); `consoleOutput()` is everything logged, keyed by channel |
|
|
85
|
-
| DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord
|
|
86
|
-
| Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`.
|
|
97
|
+
| observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). |
|
|
98
|
+
| Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `useConsoleSpies()` in the `describe` (or `installConsoleSpies()` per test, `restoreConsole()` after); `consoleOutput()` is everything logged, keyed by channel, `consoleLines()` the same in call order. No `vitest` import: loads on every runner |
|
|
99
|
+
| DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord`, `createElementStub` (an `HTMLElement` for `ElementRef`), `fillMissingDomApis` (setup file). |
|
|
100
|
+
| Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. |
|
|
87
101
|
| Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
|
|
88
102
|
| Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
|
|
89
|
-
| Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular
|
|
90
|
-
| Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio
|
|
91
|
-
| Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular
|
|
103
|
+
| Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular`, no core re-export |
|
|
104
|
+
| Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio, the `Window`/`Document` doubles, `providePlatform`, and `DomSanitizer` / `ChangeDetectorRef` / CDK `Overlay` doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box |
|
|
105
|
+
| Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular`, no core re-export |
|
|
92
106
|
| Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
|
|
93
|
-
| Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
|
|
107
|
+
| Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `withFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
|
|
94
108
|
| Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
|
|
95
109
|
| jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
|
|
96
110
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`vitest-auto-spy/
|
|
101
|
-
`bun test` nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy
|
|
102
|
-
|
|
103
|
-
`mock.module()`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
The
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Vitest refuses to be required). The plugin's CommonJS declaration is an `export =`, which is what
|
|
125
|
-
`require('vitest-auto-spy/eslint-plugin')` actually returns — an `eslint.config.cts` / `.cjs` used to
|
|
126
|
-
type-check the call that throws and reject the one that works.
|
|
127
|
-
|
|
128
|
-
`vitest-auto-spy/package.json` resolves as well. Tools that read a dependency's manifest by specifier
|
|
129
|
-
— Storybook, Nx, a renovate helper — used to get `ERR_PACKAGE_PATH_NOT_EXPORTED` and had nowhere to
|
|
130
|
-
go from there.
|
|
131
|
-
|
|
132
|
-
**Every peer is optional now, `vitest` included.** The range is unchanged (`>=2.1.0`); what changed
|
|
133
|
-
is that a suite on `/bun` or `/node`, which never loads the Vitest runner, no longer installs it to
|
|
134
|
-
satisfy a peer. The one thing still to know is that `/bun`, `/bun-angular` and `/node` declarations
|
|
135
|
-
name Vitest's `Mock` type, so a project type-checking those entries without `vitest` installed will
|
|
136
|
-
want it as a devDependency anyway; freeing that is a type change and waits for a major.
|
|
111
|
+
**Runner limits of two add-ons.** `/jasmine` and `/setup` import Vitest, so they are Vitest-only.
|
|
112
|
+
|
|
113
|
+
- `bun test` / `node --test` and jasmine's API: call `enableJasmineCompat()` from
|
|
114
|
+
`vitest-auto-spy/jasmine-compat`.
|
|
115
|
+
- `bun test` and teardown: nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy
|
|
116
|
+
between tests. Put `afterEach(() => { restoreMockedProps(); mock.restore(); })` in a `--preload`
|
|
117
|
+
file, with any `mock.module()` (inside a test it swaps bindings after the module already ran).
|
|
118
|
+
|
|
119
|
+
**Requirements.**
|
|
120
|
+
|
|
121
|
+
- rxjs **>= 7.2** (not `>=7`); every Angular 16–22 project already has it.
|
|
122
|
+
- The Angular entries need **Angular >= 20**: `@angular/core`, `@angular/common`,
|
|
123
|
+
`@angular/platform-browser`, `@angular/router`, all optional peers on one range. Below 20 the
|
|
124
|
+
`/bun-angular` preload throws before the first spec.
|
|
125
|
+
- The package is **ESM**. Only `/node` and `/eslint-plugin` also ship CommonJS; every other entry
|
|
126
|
+
is ESM-only, and `require()` of a Vitest-backed one throws. `require('vitest-auto-spy/eslint-plugin')` is typed as `export =`.
|
|
127
|
+
- Every peer is optional, `vitest` included (range `>=2.1.0`). `/node`, `/bun`, `/bun-angular`,
|
|
128
|
+
`/rstest`, `/jasmine-compat`, `/angular-router`, `/console`, `/nestjs`, `/observer-spy` and `/rxjs`
|
|
129
|
+
type-check with no Vitest installed and `skipLibCheck: false`. A Vitest suite that imports only
|
|
130
|
+
runner-free entries gets spies without `mockThrow` until one file adds
|
|
131
|
+
`import type {} from 'vitest-auto-spy';`.
|
|
132
|
+
`@angular/compiler` (`/angular/matchers`, `/bun-angular`) and `@rstest/core` (`/rstest`) are
|
|
133
|
+
optional peers too.
|
|
134
|
+
- `vitest-auto-spy/package.json` resolves, for tools that read a dependency's manifest. No
|
|
135
|
+
declaration needs `@types/node` or `lib: esnext.disposable`.
|
|
136
|
+
|
|
137
|
+
Why these floors: <https://asdalexey.github.io/vitest-auto-spy/core/compatibility>.
|
|
137
138
|
|
|
138
139
|
---
|
|
139
140
|
|
|
@@ -153,16 +154,13 @@ In [`agent-docs/factories.md`](./agent-docs/factories.md). Read it before writin
|
|
|
153
154
|
|
|
154
155
|
## 3. The 90% recipe
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
and bare `createSpyFromClass` in only 41. **In an Angular app the spy almost always arrives through
|
|
159
|
-
DI.** Write that shape first.
|
|
157
|
+
In an Angular app the spy almost always arrives through DI: `provideAutoSpy` in the providers,
|
|
158
|
+
`injectSpy` to read it. Write that shape first.
|
|
160
159
|
|
|
161
160
|
```ts
|
|
162
|
-
import { signal } from '@angular/core';
|
|
163
161
|
import { TestBed } from '@angular/core/testing';
|
|
164
|
-
import {
|
|
165
|
-
import {
|
|
162
|
+
import { type Spy, injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
|
|
163
|
+
import { expectRequest, provideHttpTesting } from 'vitest-auto-spy/angular-http';
|
|
166
164
|
|
|
167
165
|
describe('TaskService', () => {
|
|
168
166
|
let projects: Spy<ProjectStore>;
|
|
@@ -172,25 +170,33 @@ describe('TaskService', () => {
|
|
|
172
170
|
beforeEach(() => {
|
|
173
171
|
TestBed.configureTestingModule({
|
|
174
172
|
providers: [
|
|
175
|
-
|
|
176
|
-
provideAutoSpy(
|
|
177
|
-
provideAutoSpy(
|
|
173
|
+
...provideHttpTesting(), // HttpClient + the testing backend
|
|
174
|
+
provideAutoSpy(NotificationService), // plain service: nothing to configure
|
|
175
|
+
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current'] }), // a signal member
|
|
176
|
+
provideAutoSpy(NewsFeedService, { observablePropsToSpyOn: ['connected$'] }), // an Observable property
|
|
178
177
|
],
|
|
179
178
|
});
|
|
180
179
|
|
|
181
180
|
projects = injectSpy(ProjectStore);
|
|
182
181
|
feed = injectSpy(NewsFeedService);
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
projects.
|
|
183
|
+
projects.current.mockReturnValue({ id: 1 }); // defaults every test needs, once
|
|
184
|
+
projects.fetchAll.resolveWith([]); // a Promise method
|
|
185
|
+
feed.connected$.nextWith(true); // needs `import 'vitest-auto-spy/rxjs'` in the setup file
|
|
186
186
|
|
|
187
187
|
service = TestBed.inject(TaskService);
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
it('saves through the store', () => {
|
|
191
|
-
|
|
190
|
+
it('saves through the API and tells the store', async () => {
|
|
191
|
+
let saved: Task | undefined;
|
|
192
|
+
service.save(task).subscribe((result) => (saved = result)); // HttpClient.post inside
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
const request = expectRequest('/api/tasks', { method: 'POST' });
|
|
195
|
+
expect(request.request.body).toEqual(task);
|
|
196
|
+
await request.flush({ ...task, id: 7 }); // await flush(), not subscribe()
|
|
197
|
+
|
|
198
|
+
expect(saved?.id).toBe(7);
|
|
199
|
+
expect(projects.refresh).toHaveBeenCalled(); // every method is a spy, configured or not
|
|
194
200
|
});
|
|
195
201
|
});
|
|
196
202
|
```
|
|
@@ -214,15 +220,28 @@ it('loads', async () => {
|
|
|
214
220
|
});
|
|
215
221
|
```
|
|
216
222
|
|
|
217
|
-
|
|
223
|
+
| Member of the dependency | How to declare it | How to seed it |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| method returning a value | nothing | `spy.m.mockReturnValue(v)` or `spy.m.calledWith(args).mockReturnValue(v)` |
|
|
226
|
+
| method returning `Promise<T>` | nothing | `spy.m.resolveWith(v)` / `rejectWith(e)` (§4) |
|
|
227
|
+
| method returning `Observable<T>` | nothing | `spy.m.nextWith(v)` / `throwWith(e)` (§4, needs `/rxjs`) |
|
|
228
|
+
| `Observable` property (`items$`) | `observablePropsToSpyOn: ['items$']` | `spy.items$.nextWith(v)` |
|
|
229
|
+
| signal / `computed` member of a dependency | `instanceMethodsToSpyOn: ['items']` | `spy.items.mockReturnValue(v)`; a value that changes mid-test: `mockSignalProp` (§13) |
|
|
230
|
+
| plain data field | `overrides: { field: value }` | set in the provider (§5) |
|
|
231
|
+
|
|
232
|
+
Rules that carry most of the value:
|
|
218
233
|
|
|
219
|
-
1. **One `configureTestingModule` per `describe
|
|
220
|
-
|
|
234
|
+
1. **One `configureTestingModule` per `describe`.** Reconfiguring per `it()` compiles the module for
|
|
235
|
+
every test, the largest avoidable cost in an Angular suite.
|
|
221
236
|
2. **Declare each spy as `Spy<T>`, never as `T`** (§6).
|
|
222
237
|
3. **Seed defaults in `beforeEach`, override in the test.** An unconfigured method returns
|
|
223
|
-
`undefined`, and the failure surfaces far from its cause.
|
|
224
|
-
|
|
238
|
+
`undefined`, and the failure surfaces far from its cause. Seed before `TestBed.inject(ClassUnderTest)`
|
|
239
|
+
when its constructor or field initialisers read the dependency.
|
|
240
|
+
4. **`provideAutoSpy` is lazy by default**, so listing a wide service costs nothing for the methods a
|
|
225
241
|
test never touches.
|
|
242
|
+
5. **Two import lines.** `provideAutoSpy`, `injectSpy` and the `Spy<T>` type come from
|
|
243
|
+
`vitest-auto-spy/angular`; `createSpyFromClass`, `createAutoMock`, `asSpy` and `asInstance` only
|
|
244
|
+
from `vitest-auto-spy`.
|
|
226
245
|
|
|
227
246
|
---
|
|
228
247
|
|
|
@@ -241,6 +260,7 @@ In [`agent-docs/configuration.md`](./agent-docs/configuration.md). Read it when
|
|
|
241
260
|
|
|
242
261
|
<!-- agents-outline: agent-docs/configuration.md -->
|
|
243
262
|
|
|
263
|
+
- `instanceMethodsToSpyOn` for members discovery cannot see
|
|
244
264
|
- The composition belongs to the class — `registerAutoSpyDefaults`
|
|
245
265
|
- `strict` — a method nobody configured throws instead of answering `undefined`
|
|
246
266
|
- Getters and setters live in `accessorSpies`
|
|
@@ -251,114 +271,89 @@ In [`agent-docs/configuration.md`](./agent-docs/configuration.md). Read it when
|
|
|
251
271
|
|
|
252
272
|
## 6. `Spy<T>` is not assignable to `T` — this is intentional
|
|
253
273
|
|
|
254
|
-
`Spy<T>` is a **mapped type**, so it drops `#private` and `private` members.
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
let users: Spy<UserService> = createSpyFromClass(UserService); // ✅
|
|
258
|
-
let users: UserService = createSpyFromClass(UserService); // ❌ private members missing
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Do **not** patch this with `as any`, `as unknown as T`, or `@ts-expect-error`. Use the named views:
|
|
274
|
+
`Spy<T>` is a **mapped type**, so it drops `#private` and `private` members. Declare doubles as
|
|
275
|
+
`Spy<T>`, and convert at the boundary with the two named views:
|
|
262
276
|
|
|
263
277
|
```ts
|
|
264
278
|
import { asInstance, asSpy } from 'vitest-auto-spy';
|
|
265
279
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
```
|
|
280
|
+
let users: Spy<UserService> = createSpyFromClass(UserService); // ✅
|
|
281
|
+
let users: UserService = createSpyFromClass(UserService); // ❌ private members missing
|
|
269
282
|
|
|
270
|
-
|
|
283
|
+
new ProfileFacade(asInstance(users)); // Spy<T> → T, for an API typed against the class
|
|
284
|
+
const cart = asSpy(TestBed.inject(CartService)); // T → Spy<T>, for the helpers
|
|
285
|
+
```
|
|
271
286
|
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
Both are the same object at runtime. `injectSpy(X)` already returns `Spy<X>`. Do **not** patch the
|
|
288
|
+
mismatch with `as any`, `as unknown as T` or `@ts-expect-error`.
|
|
274
289
|
|
|
275
|
-
|
|
|
290
|
+
| Compiler message | Direction | Fix |
|
|
276
291
|
| --- | --- | --- |
|
|
277
292
|
| `TS2352: … 'accessorSpies' is missing in type 'Router'` | `T` → spy | `asSpy(TestBed.inject(Router))` |
|
|
278
293
|
| `TS2739` / `TS2740: Type 'Spy<X>' is missing the following properties from type 'X'` | spy → `T` | `asInstance(spy)` |
|
|
279
294
|
| `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` | spy → `T` | `asInstance(spy)` |
|
|
280
|
-
| `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not `Mocked<T>` |
|
|
295
|
+
| `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not Vitest's `Mocked<T>` (it keeps private members, so the list reads like an incomplete double) |
|
|
281
296
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
**A generic class needs its type argument spelled out.** `TestBed.inject` infers from the
|
|
287
|
-
constructor and produces `Service<any>`, and the `any` surfaces much later as a mismatch between
|
|
288
|
-
`AddPromiseSpyMethods<unknown>` and `WithMockReturnValue<…>` — eight levels deep, and nothing about
|
|
289
|
-
it says "type parameter":
|
|
297
|
+
**A generic class: spell the type argument out.** `TestBed.inject` infers `Service<any>`, and the
|
|
298
|
+
`any` surfaces far away as a mismatch between `AddPromiseSpyMethods<unknown>` and
|
|
299
|
+
`WithMockReturnValue<…>`:
|
|
290
300
|
|
|
291
301
|
```ts
|
|
292
302
|
const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); // ✅
|
|
293
|
-
const config = injectSpy<FeatureFlagService>(FeatureFlagService);
|
|
303
|
+
const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
|
|
304
|
+
const modal = injectSpy<ModalRef<PurchaseOptions>>(ModalRef); // a particular instantiation
|
|
294
305
|
```
|
|
295
306
|
|
|
296
|
-
`injectSpy(X)` without the argument keeps a declared default
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
`
|
|
302
|
-
|
|
303
|
-
`
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
**A declared default now reaches the double, and used not to.** `class FlagsConfigService<T =
|
|
316
|
-
FlagsConfigDefaults>` handed to `createSpyFromClass` / `injectSpy` inferred `T` as `unknown`, so
|
|
317
|
-
every member typed against it read as `unknown` on a class that had said exactly what it should be.
|
|
318
|
-
Two shapes caused it, both fixed: the `& { [key: string]: any }` intersection `ClassType<T>` used to
|
|
319
|
-
carry (an index signature makes inference drop the default) and the union in `injectSpy`'s token
|
|
320
|
-
parameter (a union does too — a class now matches a bare construct-signature overload first).
|
|
321
|
-
Spelling the argument out still works and is still the answer when the class has **no** default.
|
|
322
|
-
|
|
323
|
-
**A generic _method_ is a different thing, and it does collapse.** `show<T, U>(component: Type<T>,
|
|
324
|
-
data: U)` on a modal or factory service reaches `Spy<T>` through `Parameters` / `ReturnType`, which
|
|
325
|
-
instantiate the method's own type parameters — so the double types as `show(component: Type<unknown>,
|
|
326
|
-
data: unknown)`. No mapped type in TypeScript can preserve a generic signature, so this is a limit
|
|
327
|
-
rather than a defect, and it is usually harmless: `unknown` accepts every argument and every seeded
|
|
328
|
-
return, so `spy.show.calledWith(MyModal, data).mockReturnValue(ref)` compiles. It bites only where
|
|
329
|
-
the instantiated type appears in a **contravariant** position; there, name the shape on a declaration
|
|
330
|
-
of your own and assign the factory's result to it.
|
|
307
|
+
- `injectSpy(X)` without the argument keeps a declared default (`class X<T = Defaults>`). When the
|
|
308
|
+
constructor takes the type parameter (`constructor(public data: T)`), it reads the **constraint**:
|
|
309
|
+
`ModalRef<T = unknown>` gives `Spy<ModalRef<unknown>>`. Spell the argument out when the spec means
|
|
310
|
+
the default or one instantiation.
|
|
311
|
+
- `createSpyFromClass` needs the argument in one combination: a generic class with an accessor list
|
|
312
|
+
(or `overrides`) **and** `returns`. The symptom: `'isKeyEnabled' does not exist in type
|
|
313
|
+
'MethodReturns<{ flagsConfig: any; }>'`. Write `createSpyFromClass<FlagsConfigService>(…)`. The
|
|
314
|
+
core `registerAutoSpyDefaults` needs it the same way.
|
|
315
|
+
- `provideAutoSpy`, `overrideAutoSpy`, `overrideComponentProvider` and the `/angular` class overload
|
|
316
|
+
of `registerAutoSpyDefaults` take `T` from the class alone. A token needs nothing: `T` comes from
|
|
317
|
+
`InjectionToken<T>`.
|
|
318
|
+
|
|
319
|
+
**A generic method collapses to `unknown`.** `show<T, U>(component: Type<T>, data: U)` types on the
|
|
320
|
+
double as `show(component: Type<unknown>, data: unknown)`; no mapped type can keep a generic
|
|
321
|
+
signature. Usually harmless: `spy.show.calledWith(MyModal, data).mockReturnValue(ref)` compiles.
|
|
322
|
+
Where the instantiated type sits in a contravariant position, name the shape on a declaration of
|
|
323
|
+
your own and assign the factory's result to it.
|
|
324
|
+
|
|
325
|
+
Details: <https://asdalexey.github.io/vitest-auto-spy/core/spy-typing>.
|
|
331
326
|
|
|
332
327
|
---
|
|
333
328
|
|
|
334
329
|
## 7. Resetting
|
|
335
330
|
|
|
331
|
+
Use these instead of looping over methods calling `mockClear` by hand:
|
|
332
|
+
|
|
336
333
|
```ts
|
|
337
334
|
import { clearAutoSpy, resetAutoSpy } from 'vitest-auto-spy';
|
|
338
335
|
|
|
339
|
-
clearAutoSpy(service); // recorded calls only
|
|
336
|
+
clearAutoSpy(service); // recorded calls only; configured returns survive
|
|
340
337
|
resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mockReturnValue)
|
|
341
338
|
```
|
|
342
339
|
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
| Call | Clears calls | Clears configuration | Also clears |
|
|
341
|
+
| --- | --- | --- | --- |
|
|
342
|
+
| `clearAutoSpy(x)` | yes | no | — |
|
|
343
|
+
| `resetAutoSpy(x)` | yes | yes | pending `mockReturnValueOnce` / `mockResolvedValueOnce` queues, accessor spies' configuration |
|
|
344
|
+
| `spy.m.mockReset()` | yes | runner state only | **not** the `calledWith` chains |
|
|
345
345
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
answer one value for every call after a `beforeEach` configured `calledWith(…)`, drop the chain with
|
|
350
|
-
`resetAutoSpy(spy.isFeatureOn)` first; the report names it.
|
|
346
|
+
Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
|
|
347
|
+
proxies alike. `resetAutoSpy` is `vi.resetAllMocks()` for one double; the double stays usable, and a
|
|
348
|
+
fresh `calledWith` configures it as normal.
|
|
351
349
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
`
|
|
356
|
-
test's value). A spec that relied on a queued `Once` value outliving the reset reads `undefined`
|
|
357
|
-
now. The double stays usable: the library's own dispatch is put back afterwards, so a fresh
|
|
358
|
-
`calledWith` configures it as normal.
|
|
350
|
+
**Common mistake:** `spy.isFeatureOn.mockReset(); spy.isFeatureOn.mockReturnValue(true)` after a
|
|
351
|
+
`beforeEach` that configured `calledWith(…)`. The chain survives `mockReset()`, so the library reports
|
|
352
|
+
that `mockReturnValue()` replaced a configured chain (and throws under `strict`). Fix: call
|
|
353
|
+
`resetAutoSpy(spy.isFeatureOn)` first.
|
|
359
354
|
|
|
360
|
-
Every double
|
|
361
|
-
that exists only to reset one spy can
|
|
355
|
+
**`using` resets a double at block end.** Every double carries `[Symbol.dispose]()`, which runs
|
|
356
|
+
`resetAutoSpy(this)`, so an `afterEach` that exists only to reset one spy can go:
|
|
362
357
|
|
|
363
358
|
```ts
|
|
364
359
|
it('loads', () => {
|
|
@@ -369,19 +364,14 @@ it('loads', () => {
|
|
|
369
364
|
});
|
|
370
365
|
```
|
|
371
366
|
|
|
372
|
-
`createAutoMock` proxies and
|
|
373
|
-
sub-tree). The key is non-enumerable, so a spread does not copy it
|
|
374
|
-
`[Symbol.asyncDispose]`.
|
|
375
|
-
`[Symbol.dispose]` on
|
|
376
|
-
`resetAutoSpy(spy)` there.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
management in V8: it patches the symbol in itself, as `Symbol.for('nodejs.dispose')`, onto the main
|
|
381
|
-
realm only, so under `jsdom` / `happy-dom` (a bare `vm` context) it is absent, the downlevelled
|
|
382
|
-
`using` throws out of `tslib.__addDisposableResource`, and `spy[Symbol.dispose]` degrades into a
|
|
383
|
-
property named `"undefined"`. The shim is the same registry symbol, so it is identical to Node's
|
|
384
|
-
across realms.
|
|
367
|
+
- `createAutoMock` proxies and every `mockDeep` node carry it (`using` on a sub-tree resets that
|
|
368
|
+
sub-tree). The key is non-enumerable, so a spread does not copy it. There is no
|
|
369
|
+
`[Symbol.asyncDispose]`.
|
|
370
|
+
- A standalone `createFunctionSpy` is **not** covered: Vitest's own `[Symbol.dispose]` on it restores
|
|
371
|
+
the original implementation instead. Call `resetAutoSpy(spy)` there.
|
|
372
|
+
- If the project does not transpile `using`, call `spy[Symbol.dispose]()`.
|
|
373
|
+
- Works under `jsdom` / `happy-dom` on Node 22, where the realm has no `Symbol.dispose`: the library
|
|
374
|
+
installs the same registry symbol Node uses.
|
|
385
375
|
|
|
386
376
|
---
|
|
387
377
|
|
|
@@ -418,12 +408,14 @@ In [`agent-docs/setup.md`](./agent-docs/setup.md). Read it when the task touches
|
|
|
418
408
|
- What a method spy is, and the one thing that differs from `vi.fn()`
|
|
419
409
|
- Freezing and counting the clock
|
|
420
410
|
- Asserting focus
|
|
411
|
+
- Running with `isolate: false`
|
|
421
412
|
- Shared fixtures are functions, not constants
|
|
422
413
|
- A stub must be re-installed for every test
|
|
423
414
|
- Naming the file that sealed a global
|
|
424
415
|
- Naming the file that polluted `Object.prototype`
|
|
425
416
|
- Naming the test that left an attribute on `<body>`
|
|
426
417
|
- Failing on console output nothing absorbed
|
|
418
|
+
- A `TestBed` left dirty at file end — `cleanTestBed`
|
|
427
419
|
- One grade for everything — `preset: 'strict'`
|
|
428
420
|
- Hook order differs from Jest
|
|
429
421
|
|
|
@@ -449,6 +441,8 @@ In [`agent-docs/doubles.md`](./agent-docs/doubles.md). Read it when production c
|
|
|
449
441
|
- `<video>` and `<audio>`
|
|
450
442
|
- `localStorage` and `sessionStorage`
|
|
451
443
|
- Animation frames and element boxes
|
|
444
|
+
- An element for `ElementRef` — `createElementStub`
|
|
445
|
+
- The members the DOM environment leaves out — `fillMissingDomApis`
|
|
452
446
|
- A module mock that did nothing
|
|
453
447
|
|
|
454
448
|
<!-- /agents-outline -->
|
|
@@ -476,6 +470,7 @@ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that
|
|
|
476
470
|
- Signal forms — `vitest-auto-spy/signal-forms`
|
|
477
471
|
- `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
|
|
478
472
|
- The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
|
|
473
|
+
- Platform, sanitizer, change detector and CDK overlay — `vitest-auto-spy/angular/doubles`
|
|
479
474
|
- Which collaborators the code asked for — `trackInjections`
|
|
480
475
|
- Never mock `@angular/core` to control an `effect()`
|
|
481
476
|
- Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
|
|
@@ -486,7 +481,7 @@ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that
|
|
|
486
481
|
- A host for a directive under test
|
|
487
482
|
- A stand-in for a child — `createComponentStub`
|
|
488
483
|
- Patching a property of a spy
|
|
489
|
-
-
|
|
484
|
+
- Coming from spectator, Suites or Testing Library
|
|
490
485
|
|
|
491
486
|
<!-- /agents-outline -->
|
|
492
487
|
|
|
@@ -494,32 +489,30 @@ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that
|
|
|
494
489
|
|
|
495
490
|
## 14. `fakeAsync` needs `vitest-auto-spy/zone`
|
|
496
491
|
|
|
492
|
+
Needed when a spec on Vitest uses `fakeAsync`, `tick` or `waitForAsync`. `zone.js/testing` patches
|
|
493
|
+
jasmine, mocha and jest, not Vitest.
|
|
494
|
+
|
|
497
495
|
```ts
|
|
498
|
-
// vitest.setup.ts
|
|
496
|
+
// vitest.setup.ts: zone.js first (or let the Angular builder load it), then the patch
|
|
499
497
|
import 'vitest-auto-spy/zone';
|
|
500
498
|
```
|
|
501
499
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
`
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
One proxy zone serves the whole run (`scope: 'shared'`, the default), because that is what Angular's
|
|
508
|
-
jasmine patch does and what the ecosystem expects: a component built in `beforeEach` schedules from
|
|
509
|
-
its constructor, and `tick()` in the `fakeAsync` test has to see those timers. Use
|
|
510
|
-
`installProxyZonePatch({ scope: 'callback' })` for `test.concurrent`, where two callbacks are in
|
|
511
|
-
flight at once and would otherwise swap the same `ProxyZoneSpec` delegate under one another.
|
|
500
|
+
| Option | Default | Meaning |
|
|
501
|
+
| --- | --- | --- |
|
|
502
|
+
| `installProxyZonePatch({ scope: 'shared' })` | `'shared'` | one proxy zone for the whole run, as Angular's jasmine patch does: `tick()` in the test sees timers a component scheduled in `beforeEach` |
|
|
503
|
+
| `installProxyZonePatch({ scope: 'callback' })` | — | one zone per test or hook; use it for `test.concurrent` specs that call `fakeAsync` |
|
|
512
504
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
505
|
+
- The import already installs `'shared'`. Call `installProxyZonePatch({ scope: 'callback' })` in the
|
|
506
|
+
setup file, after the import, only to switch; a later import of the entry does not switch it back.
|
|
507
|
+
- Requires `test: { globals: true }`. The patch replaces the runner globals; an imported `it` is a
|
|
508
|
+
module binding it cannot reach.
|
|
509
|
+
- A second call with the same scope is a no-op, and so is a second call of the undo it returns. The
|
|
510
|
+
undo puts back what the call replaced.
|
|
511
|
+
- The entry imports no zone.js of its own: it reads `globalThis.Zone`, which the project loaded.
|
|
517
512
|
|
|
518
|
-
**
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
no zone.js of its own (it reads `globalThis.Zone`, which the consumer loaded). Do not add a
|
|
522
|
-
convenient re-export from the root: it would quietly hand zone.js to every zoneless consumer.
|
|
513
|
+
**Common mistake:** no import, or `globals: false`. Symptom: every `fakeAsync` fails with
|
|
514
|
+
`Expected to be running in 'ProxyZone', but it was not found`. Fix: the import above in the setup
|
|
515
|
+
file, and `globals: true`.
|
|
523
516
|
|
|
524
517
|
---
|
|
525
518
|
|
|
@@ -528,6 +521,10 @@ convenient re-export from the root: it would quietly hand zone.js to every zonel
|
|
|
528
521
|
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.
|
|
529
522
|
|
|
530
523
|
<!-- agents-outline: agent-docs/adapters.md -->
|
|
524
|
+
|
|
525
|
+
- `trackNodeMocks` for `node:test` memory
|
|
526
|
+
- Rstest
|
|
527
|
+
|
|
531
528
|
<!-- /agents-outline -->
|
|
532
529
|
|
|
533
530
|
---
|