vitest-auto-spy 1.12.0 → 2.0.2
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 +707 -0
- package/README.md +181 -36
- package/assets/one-api-three-runtimes.svg +9 -6
- package/dist/angular.d.ts +94 -136
- package/dist/angular.js +17 -118
- package/dist/bun-angular.d.ts +45 -530
- package/dist/bun-angular.js +25 -988
- package/dist/bun.d.ts +184 -9
- package/dist/bun.js +10 -34
- package/dist/{chunk-YUD7QJMP.js → chunk-6RGTS3E6.js} +1 -1
- package/dist/{chunk-E6UVGTHO.js → chunk-BUWKNMO3.js} +24 -12
- package/dist/{chunk-AT2AGHGX.js → chunk-DNKYISGG.js} +1 -1
- package/dist/chunk-JX5ROCS7.js +136 -0
- package/dist/chunk-KKXZ4JFP.js +19 -0
- package/dist/{chunk-7YSAUMPL.js → chunk-NCIGTWVZ.js} +83 -4
- package/dist/{chunk-QFTAM5KR.js → chunk-RZVTIR4R.js} +14 -2
- package/dist/chunk-WBQNVPQL.js +27 -0
- package/dist/{chunk-H2QNMFDC.js → chunk-XPUCQU4O.js} +1 -1
- package/dist/{chunk-CMSPQKEJ.js → chunk-ZJ7NH6LG.js} +6 -1
- package/dist/{chunk-5VURXYKY.js → chunk-ZJA7DTXR.js} +5 -4
- package/dist/console.d.ts +25 -3
- package/dist/console.js +4 -3
- package/dist/{expect-emission-DEFP9HqZ.d.ts → expect-emission-CyftZ6uw.d.ts} +73 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.js +9 -8
- package/dist/nestjs.d.ts +21 -3
- package/dist/nestjs.js +5 -4
- package/dist/node.cjs +135 -14
- package/dist/node.d.cts +648 -6
- package/dist/node.d.ts +4 -4
- package/dist/node.js +9 -8
- package/dist/package-identity-B1pqa-Sh.d.ts +33 -0
- package/dist/react.d.ts +4 -4
- package/dist/react.js +9 -8
- package/dist/{angular.d.cts → run-effect-xYIXi0O_.d.ts} +62 -63
- package/dist/rxjs.d.ts +10 -3
- package/dist/rxjs.js +2 -1
- package/dist/setup.d.ts +152 -2
- package/dist/setup.js +158 -9
- package/dist/svelte.d.ts +4 -4
- package/dist/svelte.js +9 -8
- package/dist/{types-hTDGBx8V.d.ts → types-DlmJE_eU.d.ts} +35 -4
- package/dist/vue.d.ts +5 -5
- package/dist/vue.js +10 -9
- package/package.json +24 -26
- package/skills/vitest-auto-spy/SKILL.md +137 -0
- package/dist/angular.cjs +0 -995
- package/dist/bun.cjs +0 -970
- package/dist/bun.d.cts +0 -115
- package/dist/console.cjs +0 -459
- package/dist/console.d.cts +0 -39
- package/dist/expect-emission-DEFP9HqZ.d.cts +0 -97
- package/dist/index.cjs +0 -939
- package/dist/index.d.cts +0 -6
- package/dist/nestjs.cjs +0 -569
- package/dist/nestjs.d.cts +0 -23
- package/dist/package-identity-BMWNY1fi.d.cts +0 -31
- package/dist/package-identity-BMWNY1fi.d.ts +0 -31
- package/dist/react.cjs +0 -939
- package/dist/react.d.cts +0 -6
- package/dist/rxjs.cjs +0 -63
- package/dist/rxjs.d.cts +0 -23
- package/dist/setup.cjs +0 -122
- package/dist/setup.d.cts +0 -90
- package/dist/svelte.cjs +0 -939
- package/dist/svelte.d.cts +0 -6
- package/dist/types-hTDGBx8V.d.cts +0 -140
- package/dist/vue.cjs +0 -945
- package/dist/vue.d.cts +0 -25
package/AGENTS.md
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
# vitest-auto-spy — instructions for AI coding agents
|
|
2
|
+
|
|
3
|
+
You are looking at the agent-facing reference for **`vitest-auto-spy`**: typed test spies generated
|
|
4
|
+
from a class, a type, or nothing at all, on Vitest / `bun:test` / `node:test`.
|
|
5
|
+
|
|
6
|
+
This file is written for an agent **using** the library in someone's test suite. It is shipped
|
|
7
|
+
inside the npm package, so it is readable with no network:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
node_modules/vitest-auto-spy/AGENTS.md
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Working on the library's own source instead? Read `CONTRIBUTING.md` in the repository.
|
|
14
|
+
|
|
15
|
+
| Resource | Where |
|
|
16
|
+
| ----------------------- | -------------------------------------------------------------------- |
|
|
17
|
+
| Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
|
|
18
|
+
| Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
|
|
19
|
+
| Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
|
|
20
|
+
| Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
|
|
21
|
+
| Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
|
|
22
|
+
| Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
|
|
23
|
+
|
|
24
|
+
**Read `dist/*.d.ts` before inventing a call.** Every export is typed and documented there, and the
|
|
25
|
+
type is the authority when this file and the code disagree.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Pick the entry point first
|
|
30
|
+
|
|
31
|
+
Each entry registers its mock adapter **on import**. Importing the wrong one leaves the wrong
|
|
32
|
+
adapter installed and spies fail at runtime.
|
|
33
|
+
|
|
34
|
+
| Runner / framework | Import from |
|
|
35
|
+
| --------------------- | ------------------------------- |
|
|
36
|
+
| Vitest (default) | `vitest-auto-spy` |
|
|
37
|
+
| `bun test` | `vitest-auto-spy/bun` |
|
|
38
|
+
| `bun test` + Angular | `vitest-auto-spy/bun-angular` |
|
|
39
|
+
| `node --test` | `vitest-auto-spy/node` |
|
|
40
|
+
| Angular + Vitest | `vitest-auto-spy/angular` |
|
|
41
|
+
| NestJS | `vitest-auto-spy/nestjs` |
|
|
42
|
+
| React | `vitest-auto-spy/react` |
|
|
43
|
+
| Vue / Pinia | `vitest-auto-spy/vue` |
|
|
44
|
+
| Svelte | `vitest-auto-spy/svelte` |
|
|
45
|
+
|
|
46
|
+
Three add-ons, orthogonal to the runner:
|
|
47
|
+
|
|
48
|
+
| Add-on | Import | Needed for |
|
|
49
|
+
| ------------------------------- | ------------------------------- | ------------------------------------------------------- |
|
|
50
|
+
| Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
|
|
51
|
+
| Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
|
|
52
|
+
| Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
|
|
53
|
+
|
|
54
|
+
The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
|
|
55
|
+
CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
|
|
56
|
+
Vitest refuses to be required).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. Pick the factory
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Do you have a real class at runtime?
|
|
64
|
+
├── yes → createSpyFromClass(Class, config?) → Spy<T>
|
|
65
|
+
└── no → Is the double CALLED by the code under test?
|
|
66
|
+
├── yes, and calls go one level deep → createAutoMock<T>(overrides?) → Spy<T>
|
|
67
|
+
├── yes, and calls chain (a.b.c()) → mockDeep<T>(overrides?) → DeepMockProxy<T>
|
|
68
|
+
└── no, it is only READ (DTO, config, route snapshot)
|
|
69
|
+
→ createMock<T>(partial?) → T (no spies)
|
|
70
|
+
|
|
71
|
+
One standalone function? → createFunctionSpy<Fn>('name')
|
|
72
|
+
Code under test does `new Foo()`? → createSpyClass(Foo) (a vi.fn() rejects `new`)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
|
|
76
|
+
`no-type-assertion` lint rule without an `eslint-disable` on every fixture.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Cost, so it stops being a question
|
|
81
|
+
|
|
82
|
+
Building a spy is not a thing to optimise. Measured on a ten-method class:
|
|
83
|
+
`provideAutoSpy` ~8 µs, `createSpyFromClass` ~29 µs, `createAutoMock` ~33 µs, a `calledWith`
|
|
84
|
+
lookup ~0.7 µs. Five providers across two thousand tests is under a tenth of a second for the
|
|
85
|
+
whole run. Call the factory in `beforeEach` and look at `TestBed` instead — that is where a slow
|
|
86
|
+
spec spends its time.
|
|
87
|
+
|
|
88
|
+
`provideAutoSpy` is the fastest of the three because it defaults to `lazySpies: true`. The two
|
|
89
|
+
settings that do cost: `{ lazySpies: false }` gives that up, and `autoSpyAccessors: true` walks
|
|
90
|
+
the prototype chain uncached on every call — name the accessors instead.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
## 3. The 90% recipe
|
|
94
|
+
|
|
95
|
+
Measured across a ~370-file Angular suite: `provideAutoSpy` appears in 371 files, `injectSpy` in
|
|
96
|
+
308, `mockReadonlyProp` in 127, `instanceMethodsToSpyOn` in 103, `observablePropsToSpyOn` in 79 —
|
|
97
|
+
and bare `createSpyFromClass` in only 41. **In an Angular app the spy almost always arrives through
|
|
98
|
+
DI.** Write that shape first.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { signal } from '@angular/core';
|
|
102
|
+
import { TestBed } from '@angular/core/testing';
|
|
103
|
+
import { of } from 'rxjs';
|
|
104
|
+
import { injectSpy, mockReadonlyProp, provideAutoSpy, type Spy } from 'vitest-auto-spy/angular';
|
|
105
|
+
|
|
106
|
+
describe('TaskService', () => {
|
|
107
|
+
let projects: Spy<ProjectStore>;
|
|
108
|
+
let feed: Spy<FeedService>;
|
|
109
|
+
let service: TaskService;
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
TestBed.configureTestingModule({
|
|
113
|
+
providers: [
|
|
114
|
+
provideAutoSpy(NotificationService), // plain service — nothing to configure
|
|
115
|
+
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current', 'isEmpty'] }), // signals
|
|
116
|
+
provideAutoSpy(FeedService, { observablePropsToSpyOn: ['connected$'] }), // Observable props
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
projects = injectSpy(ProjectStore);
|
|
121
|
+
feed = injectSpy(FeedService);
|
|
122
|
+
|
|
123
|
+
feed.connected$.nextWith(true); // seed the defaults every test needs, once
|
|
124
|
+
projects.save.mockReturnValue(of(true));
|
|
125
|
+
|
|
126
|
+
service = TestBed.inject(TaskService);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('saves through the store', () => {
|
|
130
|
+
service.save(task);
|
|
131
|
+
|
|
132
|
+
expect(projects.save).toHaveBeenCalledWith(task);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Outside Angular, or for a class you construct yourself:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { createSpyFromClass, type Spy } from 'vitest-auto-spy';
|
|
141
|
+
|
|
142
|
+
let users: Spy<UserService>;
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
users = createSpyFromClass(UserService);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('loads', async () => {
|
|
149
|
+
users.load.calledWith(1).resolveWith({ id: 1 });
|
|
150
|
+
|
|
151
|
+
await expect(subject.open(1)).resolves.toEqual({ id: 1 });
|
|
152
|
+
expect(users.load).toHaveBeenCalledWith(1);
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Four conventions that carry most of the value:
|
|
157
|
+
|
|
158
|
+
1. **One `configureTestingModule` per `describe`** — reconfiguring per `it()` pays for module
|
|
159
|
+
compilation on every test, the largest avoidable cost in an Angular suite.
|
|
160
|
+
2. **Declare each spy as `Spy<T>`, never as `T`** (§6).
|
|
161
|
+
3. **Seed defaults in `beforeEach`, override in the test.** An unconfigured method returns
|
|
162
|
+
`undefined`, and the failure surfaces far from its cause.
|
|
163
|
+
4. **`provideAutoSpy` is lazy by default** — listing a wide service costs nothing for the methods a
|
|
164
|
+
test never touches.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 4. Helpers a spied method earns from its return type
|
|
169
|
+
|
|
170
|
+
Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementation`,
|
|
171
|
+
`toHaveBeenCalledWith` and the rest all work as usual. On top of that:
|
|
172
|
+
|
|
173
|
+
| Return type | Helpers added |
|
|
174
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)`, `mustBeCalledWith(...args)` → same |
|
|
176
|
+
| `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
|
|
177
|
+
| `Observable<T>`| `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
|
|
178
|
+
|
|
179
|
+
`Observable` **properties** (not just methods) get the same helpers — list them in
|
|
180
|
+
`observablePropsToSpyOn`.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// argument dispatch — other arguments return undefined
|
|
184
|
+
users.getName.calledWith(1).mockReturnValue('Ada');
|
|
185
|
+
// argument enforcement — other arguments throw
|
|
186
|
+
users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
|
|
187
|
+
// asymmetric matchers work in both
|
|
188
|
+
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
|
|
189
|
+
|
|
190
|
+
// promises
|
|
191
|
+
users.load.resolveWith({ id: 1 });
|
|
192
|
+
users.load.rejectWith('FAKE ERROR');
|
|
193
|
+
users.load.resolveWithPerCall([{ value: a }, { value: b }]);
|
|
194
|
+
expect(users.load.mock.settledResults).toEqual([{ type: 'fulfilled', value: { id: 1 } }]);
|
|
195
|
+
|
|
196
|
+
// observables — requires `import 'vitest-auto-spy/rxjs'` once
|
|
197
|
+
feed.items$.nextWith([item]); // emit, stream stays open
|
|
198
|
+
feed.items$.nextOneTimeWith([item]); // emit once, then complete
|
|
199
|
+
feed.items$.nextWithValues([{ value: a }, { value: b, delay: 100 }, { complete: true }]);
|
|
200
|
+
const [first$, second$] = feed.watch$.nextWithPerCall([{ value: 'a' }, { value: 'b', doNotComplete: true }]);
|
|
201
|
+
feed.items$.throwWith('FAKE ERROR');
|
|
202
|
+
const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the helpers miss
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
|
|
206
|
+
all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 5. `createSpyFromClass` configuration
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
createSpyFromClass(MyService); // every method on the prototype chain
|
|
214
|
+
createSpyFromClass(MyService, ['reload', 'count']); // those two ADDED to the discovered ones
|
|
215
|
+
createSpyFromClass(MyService, {
|
|
216
|
+
methodsToSpyOn: ['reload'], // ADDS (jest-auto-spies semantics)
|
|
217
|
+
onlyMethodsToSpyOn: ['getName'], // RESTRICTS — skips prototype discovery
|
|
218
|
+
instanceMethodsToSpyOn: ['reload'], // ADDS; same behaviour, clearer name
|
|
219
|
+
observablePropsToSpyOn: ['products$'],
|
|
220
|
+
gettersToSpyOn: ['userName'],
|
|
221
|
+
settersToSpyOn: ['userName'],
|
|
222
|
+
autoSpyAccessors: true, // discover every accessor on the prototype chain
|
|
223
|
+
lazySpies: true, // build each method spy on first access
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Key | Semantics |
|
|
228
|
+
| ------------------------ | -------------------------------------------------------------------------- |
|
|
229
|
+
| `methodsToSpyOn` | **Additive**, as in `jest-auto-spies`. Same behaviour as `instanceMethodsToSpyOn`. |
|
|
230
|
+
| `onlyMethodsToSpyOn` | **Exhaustive whitelist.** Skips discovery; anything not listed is absent. |
|
|
231
|
+
| `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
|
|
232
|
+
| `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
|
|
233
|
+
| `lazySpies` | Behaviour-identical; only changes *when* each spy is built. |
|
|
234
|
+
|
|
235
|
+
**`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
|
|
236
|
+
the reference suite). Method discovery walks the *prototype chain*; a callable assigned to an
|
|
237
|
+
**instance field** is invisible to it:
|
|
238
|
+
|
|
239
|
+
- an Angular `signal()` / `computed()` field — the dominant case in a signals codebase
|
|
240
|
+
- an arrow-function property — `readonly reload = (): void => {}`
|
|
241
|
+
- anything on an ngrx `signalStore()`, which puts **everything** on the instance
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
createSpyFromClass(TaskStore, { instanceMethodsToSpyOn: ['count', 'reload'] });
|
|
245
|
+
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current', 'isEmpty'] });
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
For an ngrx `signalStore()`, prefer `createAutoMock<T>()` over listing every member: it mocks from
|
|
249
|
+
the type, needs no prototype, and the list cannot fall behind the store.
|
|
250
|
+
|
|
251
|
+
The symptom of getting this wrong is **a spy that is never called and no warning at all**: the
|
|
252
|
+
additive lists exist precisely to name things the prototype does not have, so a typo in one cannot
|
|
253
|
+
be told apart from an instance field and stays silent.
|
|
254
|
+
|
|
255
|
+
Only `onlyMethodsToSpyOn` warns, because only a restricting list can be silently destructive — a
|
|
256
|
+
misspelling there leaves the real method unspied, and the code under test then calls something that
|
|
257
|
+
is not there:
|
|
258
|
+
`[vitest-auto-spy] createSpyFromClass(X): onlyMethodsToSpyOn names method(s) that are not on the class prototype: …`.
|
|
259
|
+
|
|
260
|
+
Also true, and worth not re-deriving:
|
|
261
|
+
|
|
262
|
+
- **Inherited methods are spied** — discovery walks the whole chain (`Object.prototype` excluded).
|
|
263
|
+
- **Constructor bodies never run.** The spy is assembled from the prototype.
|
|
264
|
+
- **Abstract classes work at runtime** but TypeScript refuses them as `ClassType<T>`. Pass a
|
|
265
|
+
concrete subclass and keep the abstract class as the DI token.
|
|
266
|
+
|
|
267
|
+
### Getters and setters live in `accessorSpies`
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const settings = createSpyFromClass(SettingsService, { gettersToSpyOn: ['theme'], settersToSpyOn: ['theme'] });
|
|
271
|
+
|
|
272
|
+
settings.accessorSpies.getters.theme.mockReturnValue('dark');
|
|
273
|
+
expect(settings.theme).toBe('dark'); // the property itself stays typed as `string`
|
|
274
|
+
|
|
275
|
+
settings.theme = 'light';
|
|
276
|
+
expect(settings.accessorSpies.setters.theme).toHaveBeenCalledWith('light');
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 6. `Spy<T>` is not assignable to `T` — this is intentional
|
|
282
|
+
|
|
283
|
+
`Spy<T>` is a **mapped type**, so it drops `#private` and `private` members.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
let users: Spy<UserService> = createSpyFromClass(UserService); // ✅
|
|
287
|
+
let users: UserService = createSpyFromClass(UserService); // ❌ private members missing
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Do **not** patch this with `as any`, `as unknown as T`, or `@ts-expect-error`. Use the named views:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { asInstance, asSpy } from 'vitest-auto-spy';
|
|
294
|
+
|
|
295
|
+
asInstance(spy); // Spy<T> → T, for an API typed against the class
|
|
296
|
+
asSpy(TestBed.inject(CartService)); // T → Spy<T>, for the helpers
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Both are the same object at runtime. `injectSpy(X)` already returns `Spy<X>`.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 7. Resetting
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { clearAutoSpy, resetAutoSpy } from 'vitest-auto-spy';
|
|
307
|
+
|
|
308
|
+
clearAutoSpy(service); // recorded calls only — configured returns survive
|
|
309
|
+
resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mockReturnValue)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
|
|
313
|
+
proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 8. Observable assertions (core entry — no rxjs needed)
|
|
318
|
+
|
|
319
|
+
`expect()` inside a `subscribe()` callback is the classic green-but-empty test: if the stream never
|
|
320
|
+
emits, the callback never runs and nothing is asserted. Invert it — **the assertion is the `await`**:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { expectEmission, expectEmissions, expectNoEmission } from 'vitest-auto-spy';
|
|
324
|
+
|
|
325
|
+
await expect(expectEmission(component.visible$)).resolves.toEqual([task]);
|
|
326
|
+
await expect(expectEmissions(source$, 3)).resolves.toEqual([1, 2, 3]);
|
|
327
|
+
await expectNoEmission(source$, { timeout: 50 });
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Options: `{ timeout, label }`. `timeout` defaults to `1000` ms (`0` for `expectNoEmission`, and `0`
|
|
331
|
+
disables the watchdog — use it under fake timers). The source is duck-typed, so rxjs `Observable`s,
|
|
332
|
+
`Subject`s, Angular `toObservable()` results and hand-rolled subscribables all work.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 9. Patching properties (and putting them back)
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from 'vitest-auto-spy';
|
|
340
|
+
|
|
341
|
+
mockReadonlyProp(service, 'isReady', true); // static value, signals included
|
|
342
|
+
mockReadonlyPropGetter(service, 'label', () => 'A'); // dynamic getter
|
|
343
|
+
mockValueProp(service, 'retries', 3); // plain writable value
|
|
344
|
+
mockAccessorsProp(service, 'theme'); // spied get + set
|
|
345
|
+
|
|
346
|
+
restoreMockedProps(); // put every patch back; each helper also returns its own undo
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`vi.restoreAllMocks()` does **not** undo these — it knows about spies, not about redefined
|
|
350
|
+
properties. Never use bare `Object.defineProperty` in a spec: nothing restores the original
|
|
351
|
+
descriptor, and under `isolate: false` the patch leaks into the next file.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 10. Setup file
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// vitest.setup.ts
|
|
359
|
+
import 'vitest-auto-spy/rxjs'; // once — enables observable spies everywhere
|
|
360
|
+
import { setupAutoSpy } from 'vitest-auto-spy/setup';
|
|
361
|
+
|
|
362
|
+
setupAutoSpy(); // { duplicateCopies: 'throw', restoreProps: true, restoreMocks: false }
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`setupAutoSpy()` does three things: `restoreMockedProps()` in a global `afterEach`, a duplicate-install
|
|
366
|
+
check that fails the run, and (opt-in) `vi.restoreAllMocks()`. Turn on `restoreMocks: true` when the
|
|
367
|
+
suite runs with `isolate: false`.
|
|
368
|
+
|
|
369
|
+
**The one that only bites at scale:** with `isolate: false`, a `setTimeout` or
|
|
370
|
+
`requestAnimationFrame` a component schedules and never clears keeps running after its file is done,
|
|
371
|
+
and fires while the **next** file is mid-test. It is reported against that innocent file, as
|
|
372
|
+
`Schedulers cannot synchronously execute watches while scheduling`, `signal read during notification
|
|
373
|
+
phase`, or an unhandled rejection naming a component the failing file never imported. If you see any
|
|
374
|
+
of those, suspect the previous file, not the one that failed:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors in afterAll
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
|
|
381
|
+
`cancelStrayTimers()` (returns how many it cancelled) and `countStrayTimers()`, all from
|
|
382
|
+
`vitest-auto-spy/setup`. Use `expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak
|
|
383
|
+
fail rather than be tidied away.
|
|
384
|
+
|
|
385
|
+
Two more switches, both about the environment rather than the spies:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
setupAutoSpy({ blockNetwork: true }); // reject every fetch, naming what was requested
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Only relevant under happy-dom, which — unlike jsdom — implements `fetch`. A component that pulls a
|
|
392
|
+
remote asset then really fetches it; nothing asserts on the response, so the tests pass, and the
|
|
393
|
+
aborts at teardown fail the run with **no test named**. If a green run exits 1 with
|
|
394
|
+
`DOMException [AbortError]`, this is it.
|
|
395
|
+
|
|
396
|
+
`restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
|
|
397
|
+
fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
|
|
398
|
+
the realm, not owned by `globalThis`), and with `isolate: false` the next file dies inside Vitest's
|
|
399
|
+
own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
|
|
400
|
+
the file in the stack is not the cause.
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
Fake timers:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
import { advanceTimers, setupFakeTimers } from 'vitest-auto-spy/setup';
|
|
407
|
+
|
|
408
|
+
setupFakeTimers(); // install + restore, paired
|
|
409
|
+
await advanceTimers(5_000); // advance AND drain the microtasks a bare advanceTimersByTime leaves
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 11. Angular
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
|
|
418
|
+
|
|
419
|
+
TestBed.configureTestingModule({
|
|
420
|
+
providers: [provideAutoSpy(MyService), provideAutoSpy(ApiService, { methodsToSpyOn: ['get'] })],
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const myService = injectSpy(MyService); // Spy<MyService>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`provideAutoSpy` defaults to `lazySpies: true` (the plain `createSpyFromClass` does not). Pass
|
|
427
|
+
`{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
|
|
428
|
+
zone.js alike.
|
|
429
|
+
|
|
430
|
+
### Signals — which helper depends on whose signal it is
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
// a DEPENDENCY's signal — name it, then configure the mock like any other
|
|
434
|
+
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current'] });
|
|
435
|
+
injectSpy(ProjectStore).current.mockReturnValue({ id: 1 });
|
|
436
|
+
|
|
437
|
+
// the CLASS UNDER TEST's own signal / computed / input — replace the field with a REAL signal
|
|
438
|
+
mockReadonlyProp(component, 'selected', signal(true));
|
|
439
|
+
mockReadonlyProp(component, 'items', signal([]));
|
|
440
|
+
mockReadonlyProp(component, 'host', signal({ nativeElement: element }));
|
|
441
|
+
|
|
442
|
+
// a value that changes during the test — keep the signal, set it
|
|
443
|
+
const selected = signal(false);
|
|
444
|
+
|
|
445
|
+
mockReadonlyProp(component, 'selected', selected);
|
|
446
|
+
selected.set(true); // every computed reading it updates
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Pass a real `signal()`, not a `vi.fn()` returning a value — anything `computed()` downstream has to
|
|
450
|
+
recompute, and only a real signal notifies it.
|
|
451
|
+
|
|
452
|
+
`mockSignalProp` is that pair in one call, and hands back the writable half:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import { mockSignalProp } from 'vitest-auto-spy/angular';
|
|
456
|
+
|
|
457
|
+
const selected = mockSignalProp(component, 'selected', false);
|
|
458
|
+
|
|
459
|
+
selected.set(true); // every computed reading it updates
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Use it whenever the value has to change during the test. `mockReadonlyProp` stays right when the
|
|
463
|
+
value is fixed for the whole test and you never need the handle.
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
### Observers the component constructs itself
|
|
469
|
+
|
|
470
|
+
Do not assign `globalThis.IntersectionObserver` by hand: it stays assigned, and under
|
|
471
|
+
`isolate: false` the next file inherits it.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import { intersectionEntry, stubIntersectionObserver } from 'vitest-auto-spy';
|
|
475
|
+
|
|
476
|
+
const observers = stubIntersectionObserver(); // also stubResizeObserver / stubMutationObserver
|
|
477
|
+
|
|
478
|
+
fixture.detectChanges(); // the component constructs it
|
|
479
|
+
|
|
480
|
+
observers.last.emit([intersectionEntry(element, true)]); // one batch, as the browser delivers it
|
|
481
|
+
await fixture.whenStable();
|
|
482
|
+
|
|
483
|
+
expect(observers.last.disconnected).toBe(true); // after the component is destroyed
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
`restoreMockedProps()` puts the real constructor back, so `setupAutoSpy()` covers the teardown.
|
|
487
|
+
`observers.last` throws if the code under test constructed nothing — render first, and install the
|
|
488
|
+
stub before the construction, not after.
|
|
489
|
+
### `injectSpy` cannot reach a component-level provider
|
|
490
|
+
|
|
491
|
+
`injectSpy(X)` reads the **global** `TestBed` injector. A provider declared on the component
|
|
492
|
+
(`@Component({ providers: [...] })`) lives in the element injector, which `TestBed.inject` never
|
|
493
|
+
sees. Go through the fixture and re-view the result:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
const player = asSpy(fixture.debugElement.injector.get(PlayerService));
|
|
497
|
+
|
|
498
|
+
player.play.mockReturnValue(true);
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Never mock `@angular/core` to control an `effect()`
|
|
502
|
+
|
|
503
|
+
Under the Angular unit-test builder the specs are bundled and `@angular/core` sits in a shared
|
|
504
|
+
chunk, so `vi.mock('@angular/core', …)` re-enters a chunk that is still initialising and fails with
|
|
505
|
+
`Cannot access '__vi_import_N__' before initialization`. The same applies to any module those shared
|
|
506
|
+
chunks depend on, and to `vi.mock()` with a relative path (`./`, `../`), which has no module
|
|
507
|
+
boundary left to replace once bundled.
|
|
508
|
+
|
|
509
|
+
Assert the effect's **result** instead — set the signals it reads, let it run, check what it
|
|
510
|
+
produced:
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
mockReadonlyProp(component, 'state', signal(State.Selected));
|
|
514
|
+
|
|
515
|
+
await stable(fixture);
|
|
516
|
+
|
|
517
|
+
expect(component.icon()).toBe('favouritesFilled');
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
When the effect will never become dirty on its own — because its trigger is now a static signal —
|
|
521
|
+
run that one effect directly:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { runEffect } from 'vitest-auto-spy/angular';
|
|
525
|
+
|
|
526
|
+
runEffect(component.highlightEffect); // runs the body now, with the current signal values
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
`flushEffects()` runs everything currently dirty; `runEffect(ref)` runs one specific effect
|
|
530
|
+
regardless. Prefer asserting the result where practical — `runEffect` reads Angular's reactive node,
|
|
531
|
+
and throws with instructions if a future version moves it.
|
|
532
|
+
|
|
533
|
+
### ngrx `rxMethod`
|
|
534
|
+
|
|
535
|
+
An `rxMethod` is a function with a `destroy` property. A bare mock has no `destroy`, so the
|
|
536
|
+
component's cleanup throws:
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
const load = Object.assign(vi.fn(), { destroy: vi.fn() });
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
// shallow rendering — configureTestingModule + NO_ERRORS_SCHEMA + overrideComponent, in one call
|
|
544
|
+
const { fixture, component } = renderShallow(TaskListComponent, {
|
|
545
|
+
providers: [provideAutoSpy(TaskService)],
|
|
546
|
+
inputs: { projectId: 42 }, // signal inputs take the VALUE, not the signal
|
|
547
|
+
});
|
|
548
|
+
// other options: imports, keepTemplate, keepChildren, template, beforeCreate, detectChanges
|
|
549
|
+
|
|
550
|
+
// build a class through DI, every unprovided token auto-spied
|
|
551
|
+
const { instance, spies } = createWithAutoSpies(CartService, {
|
|
552
|
+
providers: [{ provide: TaxService, useValue: realTax }], // explicit providers win
|
|
553
|
+
});
|
|
554
|
+
spies.get(PricingService).total.mockReturnValue(100);
|
|
555
|
+
// NOTE: Injector.create() — it does NOT accept EnvironmentProviders (provideHttpClient() etc.)
|
|
556
|
+
|
|
557
|
+
// zoneless waiting
|
|
558
|
+
await stable(fixture); // flush effects, then await the fixture
|
|
559
|
+
flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
|
|
560
|
+
|
|
561
|
+
// signal assertions
|
|
562
|
+
registerSignalMatchers(); // once, in the setup file
|
|
563
|
+
expect(component.total).toHaveSignalValue(3);
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Two zoneless traps:
|
|
567
|
+
|
|
568
|
+
- `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
|
|
569
|
+
effects. Asserting right after it reads state that has not finished computing. Use `await stable(fixture)`.
|
|
570
|
+
- `expect(someSignal).toBeTruthy()` passes for **every** signal ever created — a signal is a
|
|
571
|
+
function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake.
|
|
572
|
+
|
|
573
|
+
Per-file timing, to find which specs actually pay for `TestBed`:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular';
|
|
577
|
+
|
|
578
|
+
if (process.env['SPEC_TIMING']) { enableTestBedDiagnostics(); }
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Angular under `bun test`
|
|
582
|
+
|
|
583
|
+
`vitest-auto-spy/bun-angular` is a **preload**, not a normal import — it installs a DOM, inlines
|
|
584
|
+
`templateUrl` / `styleUrls` through a `Bun.plugin` hook and boots a zoneless TestBed:
|
|
585
|
+
|
|
586
|
+
```toml
|
|
587
|
+
# bunfig.toml
|
|
588
|
+
[test]
|
|
589
|
+
preload = ["vitest-auto-spy/bun-angular"]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
It re-exports everything in this section except `registerSignalMatchers` and the TestBed
|
|
593
|
+
diagnostics, which need the runner's `expect.extend` and suite-level hooks.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 12. Other adapters
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
// NestJS
|
|
601
|
+
import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/nestjs';
|
|
602
|
+
const moduleRef = await Test.createTestingModule({ providers: [provideAutoSpy(MyService)] }).compile();
|
|
603
|
+
const spy = injectSpy(moduleRef, MyService);
|
|
604
|
+
|
|
605
|
+
// Vue / Pinia — provideAutoSpy(token, Class, methodsOrConfig?) returns a `global.provide` map
|
|
606
|
+
import { provideAutoSpy } from 'vitest-auto-spy/vue';
|
|
607
|
+
const provide = provideAutoSpy(UserServiceKey, UserService);
|
|
608
|
+
provide[UserServiceKey].getName.mockReturnValue('Ada');
|
|
609
|
+
mount(Greeting, { global: { provide } });
|
|
610
|
+
// a setup-store (`defineStore('x', () => …)`) is not a class — use createAutoMock<T>() there
|
|
611
|
+
|
|
612
|
+
// React / Svelte — the core API, re-exported with the right adapter registered
|
|
613
|
+
import { createSpyFromClass } from 'vitest-auto-spy/react';
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Console spies — importing the entry replaces `console.debug` / `error` / `info` / `log` / `time` /
|
|
617
|
+
`timeEnd` / `trace` / `warn` with silent typed spies, named `console<Method>Spy`:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
import { consoleInfoSpy, consoleWarnSpy, resetConsoleSpies, restoreConsole } from 'vitest-auto-spy/console';
|
|
621
|
+
|
|
622
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('done'); // the output is silenced, not printed
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Import your runtime entry (`…/bun`, `…/node`) **before** `…/console`, or it registers the Vitest
|
|
626
|
+
adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives a detached one.
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 13. ESLint plugin (flat config only)
|
|
631
|
+
|
|
632
|
+
```js
|
|
633
|
+
import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
634
|
+
|
|
635
|
+
export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
| Rule | Level | Flags |
|
|
639
|
+
| ------------------------------ | ------- | -------------------------------------------------------------------- |
|
|
640
|
+
| `no-expect-in-subscribe` | `error` | `expect()` inside `subscribe()` → `expectEmission` |
|
|
641
|
+
| `no-object-define-property` | `error` | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
642
|
+
| `prefer-provide-auto-spy` | `warn` | `{ provide: X, useValue: { a: vi.fn() } }` → `provideAutoSpy(X)` |
|
|
643
|
+
| `prefer-create-spy-from-class` | `warn` | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` |
|
|
644
|
+
| `prefer-inject-spy` | `warn` | `vi.spyOn(TestBed.inject(X), 'm')` → `injectSpy(X)` |
|
|
645
|
+
|
|
646
|
+
The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
|
|
647
|
+
packages, which a subpath export can never be.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## 14. Error → fix
|
|
652
|
+
|
|
653
|
+
| Message contains | Cause | Fix |
|
|
654
|
+
| ------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
655
|
+
| `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
|
|
656
|
+
| `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
|
|
657
|
+
| `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
|
|
658
|
+
| `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
|
|
659
|
+
| `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
|
|
660
|
+
| `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
|
|
661
|
+
| `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
|
|
662
|
+
| duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
|
|
663
|
+
| `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
|
|
664
|
+
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
665
|
+
| `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
|
|
666
|
+
| `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
|
|
667
|
+
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
668
|
+
| `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
|
|
669
|
+
| `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
|
|
670
|
+
| `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## 15. Do not write this
|
|
675
|
+
|
|
676
|
+
| ❌ | ✅ |
|
|
677
|
+
| -------------------------------------------------------- | --------------------------------------------------------- |
|
|
678
|
+
| `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
|
|
679
|
+
| `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
|
|
680
|
+
| `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
|
|
681
|
+
| `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
|
|
682
|
+
| `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
|
|
683
|
+
| `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
|
|
684
|
+
| `Object.defineProperty(service, 'ready', { value: true })`| `mockReadonlyProp(service, 'ready', true)` |
|
|
685
|
+
| `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
|
|
686
|
+
| `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
|
|
687
|
+
| `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
|
|
688
|
+
| `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
|
|
689
|
+
| a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` |
|
|
690
|
+
| `configureTestingModule` inside every `it()` | one per `describe` |
|
|
691
|
+
| `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
|
|
692
|
+
| a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console` |
|
|
693
|
+
| `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## 16. Before you report success
|
|
698
|
+
|
|
699
|
+
Run what the project actually has — check its `package.json` first.
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
npx vitest run path/to/file.spec.ts # or: bun test path/to/file.test.ts
|
|
703
|
+
npx tsc --noEmit # Spy<T> mistakes are compile errors, not runtime ones
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Type errors matter here more than usual: most of this library's guarantees are type-level, so a
|
|
707
|
+
suite that runs green but does not type-check is not done.
|