vitest-auto-spy 3.3.0 → 3.5.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.
Potentially problematic release.
This version of vitest-auto-spy might be problematic. Click here for more details.
- package/AGENTS.md +699 -37
- package/README.md +86 -30
- package/dist/angular.d.ts +4 -4
- package/dist/angular.js +6 -6
- package/dist/bun-angular.d.ts +5 -5
- package/dist/bun-angular.js +6 -6
- package/dist/bun.d.ts +49 -47
- package/dist/bun.js +5 -5
- package/dist/{chunk-EYXIU67M.js → chunk-F3NMY5KU.js} +4 -5
- package/dist/{chunk-7JTTEZGJ.js → chunk-LSDPJMNS.js} +51 -30
- package/dist/{chunk-2R5SDL7C.js → chunk-OEQTH7RA.js} +2 -1
- package/dist/chunk-QGZRH5XG.js +200 -0
- package/dist/{chunk-IJOUTA65.js → chunk-RJJJTLQ3.js} +217 -15
- package/dist/console.d.ts +1 -1
- package/dist/console.js +1 -1
- package/dist/eslint-plugin.cjs +688 -36
- package/dist/eslint-plugin.d.cts +69 -1
- package/dist/eslint-plugin.d.ts +69 -1
- package/dist/eslint-plugin.js +688 -36
- package/dist/expect-emission-RtR1iYgI.d.ts +289 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -5
- package/dist/nestjs.d.ts +1 -1
- package/dist/nestjs.js +2 -2
- package/dist/node.cjs +385 -107
- package/dist/node.d.cts +376 -28
- package/dist/node.d.ts +4 -4
- package/dist/node.js +5 -5
- package/dist/{prop-mock-CSREZ983.d.ts → prop-mock-CKFksCvA.d.ts} +15 -10
- package/dist/react.d.ts +4 -4
- package/dist/react.js +5 -5
- package/dist/{run-effect-Ch8q5Uq6.d.ts → run-effect-C_7mFldc.d.ts} +9 -4
- package/dist/rxjs.d.ts +2 -2
- package/dist/rxjs.js +37 -20
- package/dist/setup.d.ts +151 -27
- package/dist/setup.js +213 -31
- package/dist/svelte.d.ts +4 -4
- package/dist/svelte.js +5 -5
- package/dist/{types-nH9zvKRV.d.ts → types-dZUFYsox.d.ts} +112 -9
- package/dist/vue.d.ts +5 -5
- package/dist/vue.js +6 -6
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +10 -1
- package/dist/chunk-SU5RBD7I.js +0 -148
- package/dist/expect-emission-CgAS7iiE.d.ts +0 -49
- package/dist/{chunk-T5VVDFYD.js → chunk-QGBNXDKU.js} +3 -3
package/AGENTS.md
CHANGED
|
@@ -62,24 +62,176 @@ Vitest refuses to be required).
|
|
|
62
62
|
```
|
|
63
63
|
Do you have a real class at runtime?
|
|
64
64
|
├── yes → createSpyFromClass(Class, config?) → Spy<T>
|
|
65
|
+
│ (an `abstract class` DI token counts — see below)
|
|
65
66
|
└── no → Is the double CALLED by the code under test?
|
|
66
|
-
├── yes, and
|
|
67
|
-
├── yes, and
|
|
67
|
+
├── yes, and it is INJECTED (DI, a field) → createAutoMock<T>(overrides?) → Spy<T>
|
|
68
|
+
├── yes, and it is an ARGUMENT of the function under test, asserted on
|
|
69
|
+
│ → autoMocked<T>(overrides?) → T & Spy<T>
|
|
70
|
+
├── yes, and reads chain (a.b.c()) → mockDeep<T>(overrides?) → DeepMockProxy<T>
|
|
71
|
+
├── yes, and CALLS chain (a.b().c()) → mockDeep<T>({}, { selfReturning: true })
|
|
68
72
|
└── no, it is only READ (DTO, config, route snapshot)
|
|
69
|
-
|
|
73
|
+
→ createMock<T>(partial?) → T (no spies)
|
|
70
74
|
|
|
71
75
|
One standalone function? → createFunctionSpy<Fn>('name')
|
|
72
76
|
Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
|
|
73
77
|
→ only a shape? mockConstructor<T>(() => instance)
|
|
74
78
|
→ on a global? stubConstructor(globalThis, 'Image', factory)
|
|
75
79
|
(a vi.fn() rejects `new` — see §12)
|
|
76
|
-
Passed as an argument, not injected, and asserted on?
|
|
77
|
-
→ autoMocked<T>() (typed `T & Spy<T>`, no asInstance/asSpy)
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
`createAutoMock` and `autoMocked` build the same object; they differ only in the type you get back,
|
|
83
|
+
and the question that decides it is **how the double travels**. Through DI, it arrives as `Spy<T>`
|
|
84
|
+
and is only ever asserted on — `createAutoMock`. Handed to the function under test as an argument
|
|
85
|
+
(`detectVpnClient(url, logger)`, `applyPreferredTracks(target, …)`, `setLocalConfigEnabled(storage, …)`),
|
|
86
|
+
it has to satisfy `T` at the call site *and* expose the spy helpers at the assertion, and
|
|
87
|
+
`autoMocked<T>()` is that intersection — otherwise every call site needs an `asInstance()` and the
|
|
88
|
+
noise scales with the number of them.
|
|
89
|
+
|
|
90
|
+
|
|
80
91
|
`createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
|
|
81
92
|
`no-type-assertion` lint rule without an `eslint-disable` on every fixture.
|
|
82
93
|
|
|
94
|
+
**`mockDeep` builds depth on property access, not on calls** — the distinction the tree now spells
|
|
95
|
+
out, and the one that costs an afternoon otherwise. `mock.repo.user.find()` chains because every hop
|
|
96
|
+
but the last is a *read*. A node that is **called** returns what it was configured to return, and by
|
|
97
|
+
default that is `undefined`, so `mockDeep<AppLogger>().channel('app').info('x')` is a `TypeError` at
|
|
98
|
+
the second call — while `DeepMockProxy<AppLogger>` types it perfectly, so nothing warns. Pass
|
|
99
|
+
`{ selfReturning: true }` for a fluent API, or use `createAutoMock<T>()` with
|
|
100
|
+
`channel.mockReturnThis()` when only one method chains:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const logger = mockDeep<AppLogger>({}, { selfReturning: true });
|
|
104
|
+
|
|
105
|
+
logger.channel('app').info('started');
|
|
106
|
+
expect(logger.channel('app').info).toHaveBeenCalledWith('started');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Both bridges exist, and which one you need depends on the direction. What a self-returning **call**
|
|
110
|
+
hands back is typed as the *declared* return type, not as a spy — `asSpy<T>(…)` when the helpers are
|
|
111
|
+
needed. The **whole mock** is a `DeepMockProxy<T>`, which is not assignable to `T` for the same
|
|
112
|
+
reason `Spy<T>` is not (a mapped type cannot see private members) — `asInstance(…)` when it has to
|
|
113
|
+
go somewhere typed against the real thing:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const logger = mockDeep<AppLogger>({}, { selfReturning: true });
|
|
117
|
+
|
|
118
|
+
boot(asInstance(logger)); // → AppLogger, for the API under test
|
|
119
|
+
asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // → the helpers
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
|
|
123
|
+
sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
|
|
124
|
+
|
|
125
|
+
**An `abstract class` is a class.** `abstract class LocalStorage extends AbstractStorage {}`,
|
|
126
|
+
provided in production as `{ provide: LocalStorage, useClass: BrowserLocalStorage }`, is the
|
|
127
|
+
standard Angular DI-token idiom, and `provideAutoSpy(LocalStorage)` / `createSpyFromClass(LocalStorage)`
|
|
128
|
+
take it — type and runtime both. Abstract members are erased before they reach a prototype, so there
|
|
129
|
+
is nothing to read there; when discovery comes back empty the factory hands back the `createAutoMock`
|
|
130
|
+
proxy instead of an empty object, and every method answers. Nothing to configure, and no reason to
|
|
131
|
+
reach for `{ provide: X, useValue: createAutoMock<X>() }` by hand any more.
|
|
132
|
+
|
|
133
|
+
That holds while the class is **fully** abstract. One concrete member — a helper, a getter — and
|
|
134
|
+
discovery is no longer empty, the fallback does not fire, and every `abstract` member is missing
|
|
135
|
+
while `Spy<T>` types it as present: the read is `undefined` and the call dies as
|
|
136
|
+
`… is not a function` in production code. Pass `{ fillMissing: true }` there
|
|
137
|
+
(`provideAutoSpy(LocalStorage, { fillMissing: true })`), which answers a name the prototype never
|
|
138
|
+
carried with a spy. It is opt-in because `abstract` is erased at runtime — filling every unknown key
|
|
139
|
+
by default would silence a real typo on every concrete class.
|
|
140
|
+
|
|
141
|
+
**`overrides: { key: undefined }` is a seed, not an omission**, and the difference is load-bearing.
|
|
142
|
+
`createAutoMock` reads its seed with `Reflect.ownKeys`, so a key written out with an explicit
|
|
143
|
+
`undefined` **is** in the store: reading it answers `undefined`. Leave it out and the same read
|
|
144
|
+
materialises a *function spy* — which is truthy, and sends `if (this.lastFocus)` down the branch the
|
|
145
|
+
spec was trying to close:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
createAutoMock<NavigationService>({ currentFocus: undefined, navRoot: undefined, selectors: 'button, a' });
|
|
149
|
+
// ^ "this member is data, and there is none" — not the same as omitting it
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This is the way to say "the member exists and is empty", and it is worth writing even when it looks
|
|
153
|
+
redundant.
|
|
154
|
+
|
|
155
|
+
### What a Proxy-backed double cannot do
|
|
156
|
+
|
|
157
|
+
`createAutoMock` and `mockDeep` build a Proxy, not an object, and there is one place where the
|
|
158
|
+
difference shows: a Proxy answers only the operations its handler traps. Three of them used to be
|
|
159
|
+
missing, and each produced a *silent* wrong answer rather than an error — the worst failure mode
|
|
160
|
+
this library can have, because a checking test becomes a non-checking one and only the proxy's
|
|
161
|
+
source says so. Two are fixed; the third cannot be:
|
|
162
|
+
|
|
163
|
+
| Operation | Before 3.5.0 | Now |
|
|
164
|
+
| ------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------ |
|
|
165
|
+
| `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
|
|
166
|
+
| `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
|
|
167
|
+
| `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
|
|
168
|
+
|
|
169
|
+
`ownKeys` cannot be completed: a type has no key list at runtime, which is the whole premise of
|
|
170
|
+
these two factories. So a spec that installs a double by **copying it onto a real instance** —
|
|
171
|
+
`Object.assign(player, engineDouble)` — gets whichever members happened to be touched first, and
|
|
172
|
+
every other call goes to the real implementation, silently. Use `createSpyFromClass` there: it
|
|
173
|
+
returns an ordinary object whose method keys are enumerable (lazy accessors, but enumerable), so
|
|
174
|
+
the copy is complete.
|
|
175
|
+
|
|
176
|
+
### It answers everything, so it must not answer *these*
|
|
177
|
+
|
|
178
|
+
The same premise cuts the other way. A library that is handed an object and has to decide **what
|
|
179
|
+
kind of thing it is** asks by probing a key — and a double that answers every property answers the
|
|
180
|
+
probe too, at which point it stops being a double of `T` and becomes whatever was being looked for.
|
|
181
|
+
Four names are therefore answered with `undefined` unless the spec seeds them, alongside `then` and
|
|
182
|
+
every symbol, which always were:
|
|
183
|
+
|
|
184
|
+
| Key | Probed by | The double became |
|
|
185
|
+
| -------------- | --------------------------------------------- | ------------------- |
|
|
186
|
+
| `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
|
|
187
|
+
| `lift` | `isObservable`, with `subscribe` | an Observable |
|
|
188
|
+
| `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
|
|
189
|
+
| `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
|
|
190
|
+
|
|
191
|
+
The one that cost an afternoon reads like nothing at all:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
of(autoMocked<AnimationItem>()); // an Observable that never emits
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`of(...)` takes its **last argument** for a scheduler when `typeof x.schedule === 'function'`, so
|
|
198
|
+
the whole double was eaten as one, `of()` was left with an empty argument list, and the emission was
|
|
199
|
+
scheduled onto a spy that does nothing. The component under test kept its `null`, and what failed
|
|
200
|
+
was an assertion about an unrelated `emit()` three concerns away — nothing in the failure mentions
|
|
201
|
+
`of`. The workaround people find is `from([double])`; it is not needed any more.
|
|
202
|
+
|
|
203
|
+
**`subscribe` is deliberately not on that list.** It is an ordinary method name — a store, an
|
|
204
|
+
Angular `OutputEmitterRef`, an event bus — and `expect(store.subscribe).toHaveBeenCalledWith(cb)` is
|
|
205
|
+
a real assertion. Denying `lift` and `@@observable` already breaks the impersonation, so `subscribe`
|
|
206
|
+
on its own fools nothing: `from(double)` now fails with rxjs's own *"You provided an invalid object
|
|
207
|
+
where a stream was expected"*, loudly and in the right file.
|
|
208
|
+
|
|
209
|
+
If your type genuinely has one of the four, say so once and it comes back — the list is consulted
|
|
210
|
+
after the seed store:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
createAutoMock<TaskScheduler>({ schedule: vi.fn() });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
That is the trade the deny-list makes: without a seed the member is absent and the failure is an
|
|
217
|
+
immediate `TypeError: … is not a function` at the call site, instead of a silent one in another
|
|
218
|
+
file. A key is only added to that list with an observed mechanic behind it — never because the name
|
|
219
|
+
sounds protocol-ish — because every entry costs somebody the ability to mock a member of that name
|
|
220
|
+
without seeding it.
|
|
221
|
+
|
|
222
|
+
The tree asks whether the double is *called*, and there is a second question worth asking: whether
|
|
223
|
+
the code under test **writes to it**. `createAutoMock` is a proxy with a `set` trap over the same
|
|
224
|
+
cache its `get` trap answers from, so an assignment sticks and is read back — which makes it the
|
|
225
|
+
double for a DOM-ish object a library drives by assigning handlers, where a hand-written fake is
|
|
226
|
+
otherwise the only option:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const xhr = createAutoMock<XhrLike>({ status: 0, timeout: 0, onload: null, onerror: null });
|
|
230
|
+
|
|
231
|
+
xhr.send.mockImplementation(() => respond(asInstance(xhr)));
|
|
232
|
+
// production does `xhr.onload = () => resolve(xhr.status !== 0)` — the proxy remembers it
|
|
233
|
+
```
|
|
234
|
+
|
|
83
235
|
---
|
|
84
236
|
|
|
85
237
|
### Cost, so it stops being a question
|
|
@@ -211,6 +363,31 @@ const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the
|
|
|
211
363
|
`mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
|
|
212
364
|
all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
|
|
213
365
|
|
|
366
|
+
**The observable helpers are backed by a `ReplaySubject(1)` that belongs to the spy, and it is
|
|
367
|
+
configuration — so it must be reset with the rest of it.** Two failures used to come out of that
|
|
368
|
+
buffer outliving the test that filled it, and both were silent:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// test 1
|
|
372
|
+
service.createSeamlessTransition.nextWith(uri); // buffered
|
|
373
|
+
|
|
374
|
+
// test 2 — the failure path is the point of this test
|
|
375
|
+
service.createSeamlessTransition.throwWith(error); // subscriber gets `uri` FIRST, then the error
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The code under test therefore ran the **success** branch on stale data, and the error branch arrived
|
|
379
|
+
one emission late. The second: `error()` and `complete()` close a Subject permanently, so a later
|
|
380
|
+
`nextWith` on that spy pushed into a dead subject and emitted nothing at all. Both are fixed —
|
|
381
|
+
`resetAutoSpy(spy)` now drops the subject, and a terminated one is replaced on the next
|
|
382
|
+
configuration.
|
|
383
|
+
|
|
384
|
+
What that does **not** change: `vi.clearAllMocks()` and `clearMocks: true` still cannot reach it,
|
|
385
|
+
for the same reason they cannot reach a `calledWith` chain — that state lives in this library's
|
|
386
|
+
closures, not on the runner's mock. So when a spy outlives a test — a TestBed built in `beforeAll`,
|
|
387
|
+
a spy hoisted to `describe` scope — put `resetAutoSpy(spy)` in `beforeEach`. Inside one test the
|
|
388
|
+
sequence `nextWith(a)` then `throwWith(e)` still means "emit a, then fail"; only a reset or a
|
|
389
|
+
terminal call starts a new stream.
|
|
390
|
+
|
|
214
391
|
---
|
|
215
392
|
|
|
216
393
|
## 5. `createSpyFromClass` configuration
|
|
@@ -227,6 +404,8 @@ createSpyFromClass(MyService, {
|
|
|
227
404
|
settersToSpyOn: ['userName'],
|
|
228
405
|
autoSpyAccessors: true, // discover every accessor on the prototype chain
|
|
229
406
|
lazySpies: true, // build each method spy on first access
|
|
407
|
+
returns: { getProducts: of([]) }, // what a spied METHOD answers
|
|
408
|
+
overrides: { products$: subject }, // a member that is not a method result
|
|
230
409
|
});
|
|
231
410
|
```
|
|
232
411
|
|
|
@@ -245,12 +424,30 @@ the reference suite). Method discovery walks the _prototype chain_; a callable a
|
|
|
245
424
|
- an Angular `signal()` / `computed()` field — the dominant case in a signals codebase
|
|
246
425
|
- an arrow-function property — `readonly reload = (): void => {}`
|
|
247
426
|
- anything on an ngrx `signalStore()`, which puts **everything** on the instance
|
|
427
|
+
- **members Angular's own classes moved onto the instance** — `Router.currentNavigation` in
|
|
428
|
+
Angular 20 is `currentNavigation = this.navigationTransitions.currentNavigation.asReadonly()`
|
|
248
429
|
|
|
249
430
|
```ts
|
|
250
431
|
createSpyFromClass(TaskStore, { instanceMethodsToSpyOn: ['count', 'reload'] });
|
|
251
432
|
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current', 'isEmpty'] });
|
|
433
|
+
provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] });
|
|
252
434
|
```
|
|
253
435
|
|
|
436
|
+
**The failure this produces says nothing about any of it.** The member is simply not on the spy, so
|
|
437
|
+
the next line reads `undefined` and configuring it throws:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
TypeError: Cannot read properties of undefined (reading 'mockReturnValue')
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
There is no better message to be had at runtime, and it is worth saying why rather than leaving it
|
|
444
|
+
looking like an oversight. Instance fields do not exist until a constructor has run, and this
|
|
445
|
+
library never constructs the class — that is what makes a spy safe to build from a service whose
|
|
446
|
+
constructor talks to the network. The only alternative would be to answer an unknown member with
|
|
447
|
+
*something*, and that something would be truthy: `if (service.optionalThing)` in the code under test
|
|
448
|
+
would then take the wrong branch, silently, which is the exact failure mode the protocol deny-list
|
|
449
|
+
in §2 exists to remove. A loud `TypeError` on the spec's own line is the better of the two.
|
|
450
|
+
|
|
254
451
|
For an ngrx `signalStore()`, prefer `createAutoMock<T>()` over listing every member: it mocks from
|
|
255
452
|
the type, needs no prototype, and the list cannot fall behind the store.
|
|
256
453
|
|
|
@@ -267,8 +464,31 @@ Also true, and worth not re-deriving:
|
|
|
267
464
|
|
|
268
465
|
- **Inherited methods are spied** — discovery walks the whole chain (`Object.prototype` excluded).
|
|
269
466
|
- **Constructor bodies never run.** The spy is assembled from the prototype.
|
|
270
|
-
- **Abstract classes
|
|
271
|
-
|
|
467
|
+
- **Abstract classes are accepted**, type and runtime both — `ClassType<T>` carries an abstract
|
|
468
|
+
construct signature, and when the prototype turns out to be empty (abstract members are erased
|
|
469
|
+
before emit) the factory hands back the `createAutoMock` proxy instead of an empty object. Do
|
|
470
|
+
**not** pass a concrete subclass instead; this file used to say so, and it was wrong twice over.
|
|
471
|
+
- **An overloaded method is not collapsed.** The worry that `Spy<T>` types every generated
|
|
472
|
+
`api-mgw` client against its last signature does not hold: a four-overload
|
|
473
|
+
`MgwContentsService.getMoviesBySlug` types as it should, and hand-written `{ m: vi.fn() }` doubles
|
|
474
|
+
for those services convert with no changes to the assertions. When the *first* signature is the
|
|
475
|
+
useful one, name it on the **declaration only** — the factory's result assigns to it, so the type
|
|
476
|
+
argument is not written twice:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
let mapping: Spy<MgwMappingService, { overload: 'first' }>;
|
|
480
|
+
|
|
481
|
+
mapping = createSpyFromClass(MgwMappingService); // no second type argument here
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**A getter that returns a `Signal<T>` goes in `instanceMethodsToSpyOn`**, not in `gettersToSpyOn`.
|
|
485
|
+
`get isKidMode(): Signal<boolean> { return this._isKidMode.asReadonly(); }` is read as a property
|
|
486
|
+
and called as a function, and the accessor route makes you write
|
|
487
|
+
`accessorSpies.getters.isKidMode.mockReturnValue(signal(false))` — two levels deeper than the value
|
|
488
|
+
in question. Naming it as an instance method puts a plain spy at that key (the spy object has no
|
|
489
|
+
class prototype, so nothing is being shadowed), and
|
|
490
|
+
`service.isKidMode.mockReturnValue(false)` reads like every other member. `mockSignalProp` is the
|
|
491
|
+
other answer when the value has to change during the test.
|
|
272
492
|
|
|
273
493
|
### Getters and setters live in `accessorSpies`
|
|
274
494
|
|
|
@@ -282,6 +502,25 @@ settings.theme = 'light';
|
|
|
282
502
|
expect(settings.accessorSpies.setters.theme).toHaveBeenCalledWith('light');
|
|
283
503
|
```
|
|
284
504
|
|
|
505
|
+
**Naming one half gets you the pair, when the class declares a pair.** `gettersToSpyOn: ['theme']`
|
|
506
|
+
on a class with both a getter and a setter installs both spies — mirroring reads the prototype
|
|
507
|
+
descriptor, so it only ever adds what the class already has, and a read-only member stays read-only.
|
|
508
|
+
Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs: the write vanished,
|
|
509
|
+
`accessorSpies.setters.theme` was `undefined`, and the failure read
|
|
510
|
+
`Cannot read properties of undefined` three steps from the configuration behind it.
|
|
511
|
+
|
|
512
|
+
Only spy a getter when the spec asserts that it was **read**. To make one *answer* something, on a
|
|
513
|
+
spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
|
|
514
|
+
factory, which is the part that is otherwise found by trial:
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
mockReadonlyProp(settings, 'theme', 'dark'); // no gettersToSpyOn, no accessorSpies
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
For a signal-valued property that is not merely convenience: a spied getter answers `undefined`
|
|
521
|
+
until it is configured, while `mockReadonlyProp(component, 'items', signal([]))` keeps every
|
|
522
|
+
`computed()` and `effect()` downstream of it reactive (§9).
|
|
523
|
+
|
|
285
524
|
---
|
|
286
525
|
|
|
287
526
|
## 6. `Spy<T>` is not assignable to `T` — this is intentional
|
|
@@ -350,16 +589,120 @@ proxies alike. Reach for these instead of looping over methods calling `mockClea
|
|
|
350
589
|
emits, the callback never runs and nothing is asserted. Invert it — **the assertion is the `await`**:
|
|
351
590
|
|
|
352
591
|
```ts
|
|
353
|
-
import { expectEmission, expectEmissions, expectNoEmission } from 'vitest-auto-spy';
|
|
592
|
+
import { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission } from 'vitest-auto-spy';
|
|
354
593
|
|
|
355
|
-
await expect(expectEmission(component.visible$)).resolves.
|
|
356
|
-
await expect(
|
|
594
|
+
await expect(expectEmission(component.visible$)).resolves.toBe(true); // the first VALUE, not a list
|
|
595
|
+
await expect(expectEmission(tasks$)).resolves.toEqual({ id: 1 }); // the task itself, not `[task]`
|
|
596
|
+
await expect(expectEmissions(source$, 3)).resolves.toEqual([1, 2, 3]); // the list is this one
|
|
357
597
|
await expectNoEmission(source$, { timeout: 50 });
|
|
598
|
+
await expectCompletion(service.purgeCache()); // "it finished" — the value is not the point
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Options: `{ timeout, label }`. `timeout` defaults to `1000` ms (`0` for `expectNoEmission`, whose
|
|
602
|
+
wait is a quiet window rather than a watchdog). The source is duck-typed, so rxjs `Observable`s,
|
|
603
|
+
`Subject`s, Angular `toObservable()` results, Angular `output()` (`OutputEmitterRef`, whose
|
|
604
|
+
`subscribe` takes a bare callback) and hand-rolled subscribables all work — and every helper infers
|
|
605
|
+
the emitted type, so `expectEmission(of(1))` is a `Promise<number>`.
|
|
606
|
+
|
|
607
|
+
`expectCompletion` is the one to reach for on a stream whose value is not the point — a save, a
|
|
608
|
+
purge, an `Observable<void>`, a `Subject` a teardown closes. `firstValueFrom` rejects such a stream
|
|
609
|
+
with rxjs's `EmptyError`, and the workaround people arrive at,
|
|
610
|
+
`lastValueFrom(x, { defaultValue: undefined })`, reads as though the default were the interesting
|
|
611
|
+
part. Emissions do not fail it: it asserts termination, nothing about what came before.
|
|
612
|
+
|
|
613
|
+
**To assert that production code pushed into a stream, do not use `observablePropsToSpyOn`.** That
|
|
614
|
+
option points the other way: it gives the spec `nextWith` so it can *feed* the double. When the
|
|
615
|
+
question is whether the code under test called `next` on a property, the double needs a real
|
|
616
|
+
`Subject` and a spy on its method:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
const forceRequery$ = new Subject<number>();
|
|
620
|
+
|
|
621
|
+
mockValueProp(state, 'forceRequeryAndStartPlaybackAt$', forceRequery$);
|
|
622
|
+
const next = vi.spyOn(forceRequery$, 'next');
|
|
623
|
+
|
|
624
|
+
service.seek(1000);
|
|
625
|
+
expect(next).toHaveBeenCalledWith(1000);
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
`Spy<T>` types an Observable property as `AddObservableSpyMethods<O> & T[K]`, so `next` is there on
|
|
629
|
+
the type either way — which is exactly why this is worth saying: the code compiles against the spy
|
|
630
|
+
surface and asserts nothing.
|
|
631
|
+
|
|
632
|
+
**When the error *is* the assertion, use `expectError`.** The other helpers wrap a stream failure in
|
|
633
|
+
a new `Error` whose message names the stream — right for reporting an unexpected failure, useless
|
|
634
|
+
when the failure is the subject. `expectError` resolves *with* the error, exactly as it was thrown:
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
await expect(expectError(service.load())).resolves.toBe(originalError);
|
|
638
|
+
expect(await expectError(process$)).toBeInstanceOf(UdmsStatusError);
|
|
639
|
+
expect((await expectError(account$)) as Error).toHaveProperty('message', 'websso fail');
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
It waits for the error however late it arrives, and fails — naming the stream — if the stream
|
|
643
|
+
completes or stays quiet instead. The wrapped failures of the other helpers now also carry the
|
|
644
|
+
original on `cause`, so `rejects.toMatchObject({ cause: original })` works; prefer `expectError`,
|
|
645
|
+
which needs no unwrapping. `firstValueFrom(source$).rejects` remains fine too.
|
|
646
|
+
|
|
647
|
+
**Which emission counts** — `skip` and `until`, for the stream whose first value is always stale:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
await expect(expectEmission(isXl$, { skip: 1 })).resolves.toBe(true); // a shareReplay / BehaviorSubject
|
|
651
|
+
await expect(expectEmission(currentParams$, { until: (p) => p.channelId === expected })).resolves.toEqual(…);
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Both say in the assertion what `source$.pipe(skip(1))` / `pipe(filter(…))` say in the source, and
|
|
655
|
+
they keep the diagnosis: emissions that do not match are still counted, so a failure reads
|
|
656
|
+
`4 emission(s) received` rather than `0` and tells "the wrong thing fired" apart from "nothing
|
|
657
|
+
fired".
|
|
658
|
+
|
|
659
|
+
**`advance` closes the window between subscribing and awaiting.** A stream driven by a
|
|
660
|
+
`debounceTime`, a retry or a poll needs the clock moved *after* something is listening, and `await`
|
|
661
|
+
gives control away before the next statement runs:
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
await expect(expectEmission(purchased$, { advance: () => vi.runAllTimers() })).resolves.toBe(false);
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
That replaces the fragile shape people arrive at — hold the promise, advance, then await — which
|
|
668
|
+
breaks silently the moment somebody adds an `await` one line above it. It is a callback rather than
|
|
669
|
+
an `advanceTimers: true` flag because these helpers are in the core entry, which contains no test
|
|
670
|
+
runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`.
|
|
671
|
+
|
|
672
|
+
**The watchdog runs on real time, on purpose — even under fake timers.** A virtual one would race
|
|
673
|
+
the timers the spec advances: `expectEmission(source$, { timeout: 200 })` followed by
|
|
674
|
+
`vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the spec was
|
|
675
|
+
about to advance into. The cost is that in a suite with global fake timers a *failing* assertion
|
|
676
|
+
spends a real second. Do **not** answer that with `{ timeout: 0 }` at every call site — that
|
|
677
|
+
disables the watchdog, and the next silent stream hangs to the runner's own timeout with nothing
|
|
678
|
+
useful in the message. Lower the default once instead:
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
// vitest.setup.ts
|
|
682
|
+
import { setEmissionTimeout } from 'vitest-auto-spy';
|
|
683
|
+
import { setupAutoSpy } from 'vitest-auto-spy/setup';
|
|
684
|
+
|
|
685
|
+
setupAutoSpy({ globalFakeTimers: true });
|
|
686
|
+
setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
|
|
358
687
|
```
|
|
359
688
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
689
|
+
**`expectEmission` subscribes when you call it, not when you await it**, and that is load-bearing
|
|
690
|
+
rather than an implementation detail. It is what converts the test whose source has to be poked
|
|
691
|
+
*after* somebody is listening — a router event, a `Subject` the spec pushes into, anything that
|
|
692
|
+
does not replay:
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
const breadcrumbs = expectEmission(service.buildDynamicBreadcrumbs({ root })); // subscribed already
|
|
696
|
+
|
|
697
|
+
router.events.nextWith(navigationEnd); // …so this emission is not missed
|
|
698
|
+
|
|
699
|
+
await expect(breadcrumbs).resolves.toEqual([…]);
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
`firstValueFrom` cannot do this half: it also subscribes eagerly, but there is nowhere to put the
|
|
703
|
+
line that triggers the source, because the `await` is the same statement as the subscription — so
|
|
704
|
+
the test deadlocks against a source that only emits once something pokes it. Hold the promise
|
|
705
|
+
first, poke, then await.
|
|
363
706
|
|
|
364
707
|
---
|
|
365
708
|
|
|
@@ -380,6 +723,32 @@ restoreMockedProps(); // put every patch back; each helper also returns its own
|
|
|
380
723
|
properties. Never use bare `Object.defineProperty` in a spec: nothing restores the original
|
|
381
724
|
descriptor, and under `isolate: false` the patch leaks into the next file.
|
|
382
725
|
|
|
726
|
+
**They work on `createAutoMock` and `mockDeep` doubles too** — which they did not until 3.5.0.
|
|
727
|
+
Both are Proxies, all four helpers are built on `Object.defineProperty`, and neither Proxy trapped
|
|
728
|
+
it: the patch landed on the Proxy's own target, the `get` trap never looked there, nothing threw,
|
|
729
|
+
and the test carried on reading the old value. If you have seen a spec build a double by hand —
|
|
730
|
+
real getters plus a `createFunctionSpy` per method — this is usually why.
|
|
731
|
+
|
|
732
|
+
**The second overload is a normal tool, not a last resort.** Each helper has a checked overload
|
|
733
|
+
(`K extends keyof T`) and a `(object, property: PropertyKey, value: unknown)` one behind it, and
|
|
734
|
+
the JSDoc calls the latter an escape hatch for `#private` fields. In practice it carries about half
|
|
735
|
+
of the real calls, all of them legitimate:
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
mockValueProp(router, 'routerState', { snapshot: { url: '/home' } }); // a partial fixture of a fat type
|
|
739
|
+
mockValueProp(window, 'AudioContext', undefined); // "this platform does not ship the API"
|
|
740
|
+
mockValueProp(transitionEvent, 'propertyName', 'opacity'); // a field a synthetic DOM event lacks
|
|
741
|
+
mockValueProp(spy, 'products$', new Subject()); // a member the double does not have at all
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
The last one is worth knowing on its own: patching a key the object never had **works and is undone
|
|
745
|
+
correctly** — the journal records the *absence* of a descriptor and puts it back by deleting the
|
|
746
|
+
property. That is how you add an Observable member that `provideAutoSpy` did not create because
|
|
747
|
+
`observablePropsToSpyOn` was not passed.
|
|
748
|
+
|
|
749
|
+
What the second overload costs is the property-name check, so a typo in the name compiles. Nothing
|
|
750
|
+
checks the *value* on either overload; that is deliberate, and the partial fixture above is why.
|
|
751
|
+
|
|
383
752
|
### Properties of DOM objects — the same helpers, and the reason to look for them
|
|
384
753
|
|
|
385
754
|
`document.fullscreenElement`, `document.visibilityState`, `document.cookie`, `navigator.userAgent`,
|
|
@@ -420,6 +789,16 @@ setupAutoSpy(); // { duplicateCopies: 'throw', restoreProps: true, restoreMocks:
|
|
|
420
789
|
check that fails the run, and (opt-in) `vi.restoreAllMocks()`. Turn on `restoreMocks: true` when the
|
|
421
790
|
suite runs with `isolate: false`.
|
|
422
791
|
|
|
792
|
+
**The restore also runs from an `onTestFinished` net**, because the `afterEach` is not guaranteed
|
|
793
|
+
to. Vitest calls `afterEach` hooks in *reverse* registration order, so the setup file's is the last
|
|
794
|
+
one, and a hook the spec file registered takes the chain down with it when it throws — the patches
|
|
795
|
+
then travel into the next test and the failure surfaces somewhere that never touched them. One spec
|
|
796
|
+
kept `afterEach(() => vi.restoreAllMocks())`; migrating it to `gettersToSpyOn` made the restored
|
|
797
|
+
getter return `undefined`, `ngOnDestroy` called it as a signal, the `TypeError` aborted the hook,
|
|
798
|
+
and a template error about a null profile appeared in a different `describe`. The net puts the
|
|
799
|
+
properties back and warns with the count and the cause. `countMockedProps()` is exported if you
|
|
800
|
+
would rather assert it: `afterEach(() => expect(countMockedProps()).toBe(0))`.
|
|
801
|
+
|
|
423
802
|
**The one that only bites at scale:** with `isolate: false`, a `setTimeout` or
|
|
424
803
|
`requestAnimationFrame` a component schedules and never clears keeps running after its file is done,
|
|
425
804
|
and fires while the **next** file is mid-test. It is reported against that innocent file, as
|
|
@@ -456,16 +835,81 @@ undo), `flushStrayRejections()` (takes what was captured and starts again from e
|
|
|
456
835
|
`countStrayRejections()`. The `no-floating-assertion` lint rule catches the commonest shape before
|
|
457
836
|
it ever runs (§16).
|
|
458
837
|
|
|
838
|
+
A rejection the runner has **already** blamed the finished test for is not reported again. An
|
|
839
|
+
`async` test that fails an assertion leaves its own `AssertionError` in both places, so a red run
|
|
840
|
+
used to print two messages per failure and the second one sent the reader hunting for a defect that
|
|
841
|
+
was not there. What is left is what the check is for: the rejections that fail no test at all.
|
|
842
|
+
|
|
843
|
+
**The one that gets slower the longer the run goes on:** every `vi.fn()` and `vi.spyOn()` is added
|
|
844
|
+
to one `Set` inside `@vitest/spy`, because that is what `vi.clearAllMocks()` walks, and nothing takes
|
|
845
|
+
anything out of it again. With `isolate: false` the set is created once per worker and only grows:
|
|
846
|
+
`clearMocks: true` then walks every mock of every file already run **before every single test**, and
|
|
847
|
+
the worker holds all of them at once — their recorded arguments included, and through those whole
|
|
848
|
+
component trees.
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
setupAutoSpy({ pruneMockRegistry: true }); // keep only the mocks that outlive a file
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
The part to understand before turning it on is what must **not** be pruned. Dropping a mock from that
|
|
855
|
+
set means `clearMocks` can no longer see it, so its calls accumulate silently — harmless for a mock
|
|
856
|
+
that dies with its file, a bug for the module-level `vi.fn()` in a shared `*.mock.ts` that six spec
|
|
857
|
+
files import. The first file to import it creates it; drop it when that file ends and the file that
|
|
858
|
+
happens to run **second** fails on calls its predecessor made, which reads as flakiness because which
|
|
859
|
+
file is first is the runner's choice. So the split is drawn where it is observable: what is already in
|
|
860
|
+
the registry when a file's hooks start was created while the module graph was being evaluated and is
|
|
861
|
+
kept; everything added after that belongs to the file and goes when it ends. One case lands on the
|
|
862
|
+
wrong side — a module first loaded by a dynamic `import()` inside a test — and says so explicitly:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
`trackMockRegistry()` installs the pair of hooks on its own, `pruneMockRegistry()` is the one-shot
|
|
869
|
+
sweep (it returns how many went) and `getMockRegistrySize()` reports what is left.
|
|
870
|
+
|
|
459
871
|
Two more switches, both about the environment rather than the spies:
|
|
460
872
|
|
|
461
873
|
```ts
|
|
462
|
-
setupAutoSpy({ blockNetwork: true }); //
|
|
874
|
+
setupAutoSpy({ blockNetwork: true }); // fetch rejects, XHR fails, sendBeacon answers false
|
|
463
875
|
```
|
|
464
876
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
877
|
+
Under happy-dom, which — unlike jsdom — implements `fetch`: a component that pulls a remote asset
|
|
878
|
+
really fetches it, nothing asserts on the response so the tests pass, and the aborts at teardown
|
|
879
|
+
fail the run with **no test named**. If a green run exits 1 with `DOMException [AbortError]`, this
|
|
880
|
+
is it.
|
|
881
|
+
|
|
882
|
+
Under jsdom too, for the other half. jsdom implements `XMLHttpRequest` in full, and plenty of
|
|
883
|
+
libraries never left it — `rmp-vast` pings every VAST tracker through a hand-rolled one
|
|
884
|
+
(`FW.ajax`) — so a suite with `blockNetwork: true` already on was still reaching the internet, one
|
|
885
|
+
ping per quartile per ad per test, and printing jsdom's `AggregateError at Object.dispatchError`
|
|
886
|
+
for every connection that failed. What a green run prints then depends on whether the machine has a
|
|
887
|
+
route out.
|
|
888
|
+
|
|
889
|
+
Every channel is closed by default; the object is for narrowing it:
|
|
890
|
+
|
|
891
|
+
| option | default | what it does |
|
|
892
|
+
| -------- | ---------- | ---------------------------------------------------------------------------------- |
|
|
893
|
+
| `fetch` | `true` | `fetch` rejects, naming what was requested |
|
|
894
|
+
| `xhr` | `'reject'` | `'reject'` fails the request (`status` 0, an `error` event); `'empty'` answers 200 with an empty body; `false` leaves XHR alone |
|
|
895
|
+
| `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one |
|
|
896
|
+
|
|
897
|
+
`'reject'` is the default because it is what `fetch` does: the code takes its failure branch, which
|
|
898
|
+
is the branch a unit test should be asserting on. `'empty'` is for a request whose response nobody
|
|
899
|
+
reads — a tracker ping, an analytics beacon — where failing it only trades one kind of noise for
|
|
900
|
+
another:
|
|
901
|
+
|
|
902
|
+
```ts
|
|
903
|
+
setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setting
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
|
|
907
|
+
serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
|
|
908
|
+
and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
|
|
909
|
+
resolves it against the document origin, so a spec that reaches `/config` and passes is resting on
|
|
910
|
+
nothing listening on that port. `WebSocket` and `EventSource` are left alone: their failure is an
|
|
911
|
+
event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
|
|
912
|
+
a behaviour change — `stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
|
|
469
913
|
|
|
470
914
|
`restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
|
|
471
915
|
fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
|
|
@@ -746,6 +1190,57 @@ const myService = injectSpy(MyService); // Spy<MyService>
|
|
|
746
1190
|
`{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
|
|
747
1191
|
zone.js alike.
|
|
748
1192
|
|
|
1193
|
+
The token may be an **abstract class** — `abstract class LocalStorage extends AbstractStorage {}`,
|
|
1194
|
+
the shape production provides with `useClass`. Its members are erased before they reach a prototype,
|
|
1195
|
+
so there is nothing to discover; the factory notices and returns the `createAutoMock` proxy, which
|
|
1196
|
+
answers every method of the declared type. `injectSpy(LocalStorage)` recognises it as an auto-spy
|
|
1197
|
+
and stays quiet.
|
|
1198
|
+
|
|
1199
|
+
**Seed the double in the provider, not in the `beforeEach` under it.** Both factories take both
|
|
1200
|
+
halves — `returns` for what a spied method answers, `overrides` for a member that is not a method
|
|
1201
|
+
result:
|
|
1202
|
+
|
|
1203
|
+
```ts
|
|
1204
|
+
provideAutoSpy(FavoritesService, {
|
|
1205
|
+
returns: { load: of([]) },
|
|
1206
|
+
overrides: { favoritesCacheUpdated$: of(undefined), favoriteVODs: [] },
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
provideAutoSpyForToken(PRODUCTS, undefined, { returns: { getProducts: of([]), getById: of(null) } });
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
A seeded `overrides` member is stored verbatim and is **no longer a spy**, so seed data there and
|
|
1213
|
+
name methods in `returns` when they must stay assertable. The reason to prefer this over a second
|
|
1214
|
+
statement is not brevity: the shortcut people take instead is an exported `const` provider carrying
|
|
1215
|
+
the values, and under `isolate: false` that is one set of spies shared by every file that imports
|
|
1216
|
+
it.
|
|
1217
|
+
|
|
1218
|
+
**`observablePropsToSpyOn` works on a token too**, and matters more there than on a class. A class
|
|
1219
|
+
tells the factory which members are methods; a type does not, so *every* unnamed key of a
|
|
1220
|
+
token-driven double is a function spy — including an `Observable` property, which the code under
|
|
1221
|
+
test then subscribes to as if it were a function, failing far from the double:
|
|
1222
|
+
|
|
1223
|
+
```ts
|
|
1224
|
+
provideAutoSpyForToken(FAVORITES, undefined, { observablePropsToSpyOn: ['favorites$'] });
|
|
1225
|
+
// …
|
|
1226
|
+
injectSpy(FAVORITES).favorites$.nextWith([{ id: 1 }]);
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
A member also named in `overrides` keeps its seed — hand the double a real `Subject` there when the
|
|
1230
|
+
spec drives the stream itself, and name it here when `nextWith` is what the spec wants. That is the
|
|
1231
|
+
same precedence the class-based factory uses. Before 3.5.0 this option existed only on the class
|
|
1232
|
+
path, and reaching a token with observable members meant going back to a hand-written double —
|
|
1233
|
+
which is exactly what `prefer-provide-auto-spy` and `prefer-create-spy-from-class` exist to prevent.
|
|
1234
|
+
|
|
1235
|
+
**Do not write a local `injectSpy`.** A wrapper of the shape
|
|
1236
|
+
`TestBed.inject(token as never) as Spy<T>` — a double assertion, typed
|
|
1237
|
+
`<T>(token: abstract new (...args: never[]) => T)` — is a common thing to find already in a
|
|
1238
|
+
repository, and the library's is strictly wider: it takes `ClassType<T>`, an `InjectionToken<T>` and
|
|
1239
|
+
an abstract constructor, warns when the injector hands back something that is not a spy, and has no
|
|
1240
|
+
assertion for the project's lint rules to argue with. Two functions with the same name and different
|
|
1241
|
+
signatures means the import order decides which one a file gets. Delete the local one, or re-export
|
|
1242
|
+
the library's under that name.
|
|
1243
|
+
|
|
749
1244
|
### Signals — which helper depends on whose signal it is
|
|
750
1245
|
|
|
751
1246
|
```ts
|
|
@@ -826,6 +1321,32 @@ fail with something that has nothing to do with intersection.
|
|
|
826
1321
|
`addedNodes` is a `NodeList`. Do not build one from a `DocumentFragment`: appending **moves** the
|
|
827
1322
|
nodes, so the helper silently rips the element out of the fixture it was just asserted on.
|
|
828
1323
|
|
|
1324
|
+
### A component's own `providers` win, and the symptom is nowhere near the cause
|
|
1325
|
+
|
|
1326
|
+
Worth reading before the rest of this section: it has now come up twice in one migration wave, and
|
|
1327
|
+
both times the failure landed in a different file from its cause.
|
|
1328
|
+
|
|
1329
|
+
`@Component({ providers: [RemoveProfileService] })` declares the provider on the **element**
|
|
1330
|
+
injector, and a module-level `provideAutoSpy(RemoveProfileService)` in `configureTestingModule`
|
|
1331
|
+
loses to it — so the component builds the **real** service. Nothing warns. What fails is whatever
|
|
1332
|
+
the real service touches first: in the observed case a logger, with
|
|
1333
|
+
`TypeError: Cannot read properties of undefined (reading 'pipe')`, which names neither the component
|
|
1334
|
+
nor the provider nor the spy.
|
|
1335
|
+
|
|
1336
|
+
Two things fix it, and which one depends on whether the double is wanted:
|
|
1337
|
+
|
|
1338
|
+
```ts
|
|
1339
|
+
// keep a double, but put it where the component will look
|
|
1340
|
+
const menu = overrideComponentProvider(SmartVodComponent, MenuBuilderService);
|
|
1341
|
+
|
|
1342
|
+
// or take the component's own provider away, so the module-level one is reached again
|
|
1343
|
+
TestBed.overrideComponent(ProfileComponent, { remove: { providers: [RemoveProfileService] } });
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
`overrideComponentProvider` is the one to reach for by default — it also queues the component with
|
|
1347
|
+
the TestBed compiler, which `overrideProvider` alone does not do. Reach for the `remove` form when
|
|
1348
|
+
the module already provides the spy and the component's declaration is simply in the way.
|
|
1349
|
+
|
|
829
1350
|
### `injectSpy` cannot reach a component-level provider
|
|
830
1351
|
|
|
831
1352
|
`injectSpy(X)` reads the **global** `TestBed` injector. A provider declared on the component
|
|
@@ -850,11 +1371,15 @@ const menu = overrideComponentProvider(SmartVodComponent, MenuBuilderService); /
|
|
|
850
1371
|
TestBed.configureTestingModule({ … }).overrideProvider(PaymentToolService, overrideAutoSpy(PaymentToolService));
|
|
851
1372
|
```
|
|
852
1373
|
|
|
853
|
-
|
|
854
|
-
`
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1374
|
+
`overrideProvider(X, provideAutoSpy(X))` is **not** broken, contrary to what this section used to
|
|
1375
|
+
say: `provideAutoSpy` returns `{ provide, useValue }`, `overrideProvider` reads the `useValue` off
|
|
1376
|
+
it and ignores the extra `provide`, and the spy is installed. `overrideAutoSpy` is the right call
|
|
1377
|
+
because it says what it does and hands the spy back directly — not because the other form is a
|
|
1378
|
+
no-op.
|
|
1379
|
+
|
|
1380
|
+
The failure that is real: `overrideProvider` only reaches a component the TestBed compiler knows
|
|
1381
|
+
about, so a standalone component instantiated through a parent's template needs to be in `imports`
|
|
1382
|
+
first; `overrideComponentProvider` queues it.
|
|
858
1383
|
|
|
859
1384
|
Do **not** reach for `TestBed.overrideComponent` here — see the next subsection for why it is worse
|
|
860
1385
|
than the problem it solves.
|
|
@@ -1014,7 +1539,19 @@ const pinCode = injectSpy(PIN_CODE_SERVICE_TOKEN); // Spy<PinCodeService>
|
|
|
1014
1539
|
|
|
1015
1540
|
A token typed with an interface has no class to read, so the habit is a `…Mock` class written in the
|
|
1016
1541
|
spec — after which `Spy<Mock>` and `Spy<Interface>` disagree and somebody casts. Do not write
|
|
1017
|
-
`TestBed.inject<any>(TOKEN)`; both of these accept a token.
|
|
1542
|
+
`TestBed.inject<any>(TOKEN)`; both of these accept a token. And it is `provideAutoSpyForToken`, not
|
|
1543
|
+
`provideAutoSpy`: the latter reads a class prototype, which a token does not have.
|
|
1544
|
+
|
|
1545
|
+
**The second argument is not optional as often as it looks.** A spy answers `undefined` until it is
|
|
1546
|
+
told otherwise, and that is fatal the moment the code under test *chains* off it — a constructor
|
|
1547
|
+
doing `inject(LOGGER).channel('auth').debug('…')` dies on the `.debug` of `undefined` before the
|
|
1548
|
+
spec's first line runs, because nothing in production wrote `?.` there. Seed the link:
|
|
1549
|
+
|
|
1550
|
+
```ts
|
|
1551
|
+
provideAutoSpyForToken(LOGGER, { channel: vi.fn().mockReturnThis() });
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
For a chain more than one link long, `mockDeep<T>()` is the double that answers every level (§2).
|
|
1018
1555
|
|
|
1019
1556
|
### A host for a directive under test
|
|
1020
1557
|
|
|
@@ -1117,17 +1654,88 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
|
1117
1654
|
export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
1118
1655
|
```
|
|
1119
1656
|
|
|
1120
|
-
| Rule | Level | Flags |
|
|
1121
|
-
| ------------------------------ | ------- | ------------------------------------------------------------------------ |
|
|
1122
|
-
| `no-expect-in-subscribe` | `error` | `expect()` inside `subscribe()` → `expectEmission`
|
|
1123
|
-
| `no-object-define-property` | `error` | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
1124
|
-
| `prefer-provide-auto-spy` | `warn` |
|
|
1125
|
-
| `prefer-create-spy-from-class` | `warn` | an object literal of 2+ `vi.fn()`s → `createSpyFromClass`
|
|
1126
|
-
| `prefer-inject-spy` | `warn` | `vi.spyOn(TestBed.inject(X), 'm')` → `injectSpy(X)`
|
|
1127
|
-
| `no-shared-module-level-mock` | `error` | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
1128
|
-
| `no-mocked-for-spy` | `warn` | `
|
|
1129
|
-
| `no-done-callback` | `error` | `it('x', (done) => …)` → `async` + an awaited assertion |
|
|
1130
|
-
| `no-floating-assertion` | `error` | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
1657
|
+
| Rule | Level | Fix | Flags |
|
|
1658
|
+
| ------------------------------ | ------- | --------- | ------------------------------------------------------------------------ |
|
|
1659
|
+
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
1660
|
+
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
1661
|
+
| `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
|
|
1662
|
+
| `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
|
|
1663
|
+
| `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
|
|
1664
|
+
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
1665
|
+
| `no-mocked-for-spy` | `warn` | `--fix` | `Mocked<T>` in any type position → `Spy<T>`, import and all |
|
|
1666
|
+
| `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion |
|
|
1667
|
+
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
1668
|
+
| `no-overridden-provider` | `error` | — | two providers for one token in one array → the earlier one never runs |
|
|
1669
|
+
| `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
1670
|
+
|
|
1671
|
+
Eleven rules; one fixes on its own, three offer suggestions. `no-mocked-for-spy` only ever touches a
|
|
1672
|
+
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
1673
|
+
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
1674
|
+
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
|
1675
|
+
helper's parameter, and `as unknown as Mocked<T>`, which in one batch stood next to the declaration
|
|
1676
|
+
in all eight reports — fix one and leave the other and the file says both. It declines where it
|
|
1677
|
+
cannot prove the rename (a `Mocked` the file declares itself, a `Spy` that is already something
|
|
1678
|
+
else, `Mocked<{ a: Mock }>` rather than a named type) and reports without a fix.
|
|
1679
|
+
|
|
1680
|
+
`no-expect-in-subscribe` reports one shape and **three different edits**, and says which: the
|
|
1681
|
+
subscription is the last thing the test does (invert it into `await firstValueFrom`); something
|
|
1682
|
+
after it is what makes the stream emit (hold the promise — `const p = expectEmission(src$)`, fire
|
|
1683
|
+
the trigger, `await p` — because inverting deadlocks); or the assertion is in the `error` branch
|
|
1684
|
+
(`await expect(firstValueFrom(src$)).rejects.toMatchObject(…)`). It also counts assertions the
|
|
1685
|
+
callback reaches through a helper it calls, which used to make `subscribe((d) => assertShape(d))`
|
|
1686
|
+
invisible. `prefer-provide-auto-spy` reads `useFactory` as well as `useValue`, through the function
|
|
1687
|
+
in the first case and not in the second — a factory's body is what DI ends up holding, while a
|
|
1688
|
+
function inside a `useValue` is a lazily-built double, i.e. the fix. The other three change behaviour — whether `injectSpy(X)`
|
|
1689
|
+
finds a spy is decided by a `provideAutoSpy(X)` usually written in another file, `mockValueProp`
|
|
1690
|
+
leaves the property writable and configurable, and `no-expect-in-subscribe` rewrites a whole test
|
|
1691
|
+
— so all three are suggestions an editor offers and a human accepts:
|
|
1692
|
+
|
|
1693
|
+
```ts
|
|
1694
|
+
it('maps the products', () => // ❌ flagged, and a suggestion is offered
|
|
1695
|
+
new Promise<void>((done) => {
|
|
1696
|
+
service.getProducts(id).subscribe((products) => {
|
|
1697
|
+
expect(products).toEqual(expected);
|
|
1698
|
+
done();
|
|
1699
|
+
});
|
|
1700
|
+
}));
|
|
1701
|
+
|
|
1702
|
+
it('maps the products', async () => { // ✅ what accepting it produces
|
|
1703
|
+
const products = await firstValueFrom(service.getProducts(id));
|
|
1704
|
+
|
|
1705
|
+
expect(products).toEqual(expected);
|
|
1706
|
+
});
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
That template was 111 of 133 violations in one migration batch. The suggestion appears only for the
|
|
1710
|
+
exact frame above — one `subscribe` statement in the executor, one block-bodied callback, `done()`
|
|
1711
|
+
mentioned once and standing last — and the report itself now counts assertions per `subscribe`
|
|
1712
|
+
rather than one message per `expect`, which used to double the apparent size of the job.
|
|
1713
|
+
|
|
1714
|
+
`prefer-inject-spy` reads both spellings of the same mistake, which is the point of the second one:
|
|
1715
|
+
|
|
1716
|
+
```ts
|
|
1717
|
+
vi.spyOn(TestBed.inject(DomainEventsService), 'announce'); // flagged, always was
|
|
1718
|
+
const domainEvents = TestBed.inject(DomainEventsService);
|
|
1719
|
+
const announceSpy = vi.spyOn(domainEvents, 'announce'); // flagged now
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
The variable is resolved through the scope manager, so it has to be a `const`/`let` initialised
|
|
1723
|
+
from `TestBed.inject(...)` and never assigned again — a name bound by an import, a parameter, or a
|
|
1724
|
+
`let` that is reassigned is left alone.
|
|
1725
|
+
|
|
1726
|
+
`no-inject-before-override` catches the trap this plugin's own advice sets. `TestBed.inject()` in a
|
|
1727
|
+
`beforeEach` — the line you write once `provideAutoSpy(X)` has taken away the literal you used to
|
|
1728
|
+
configure — instantiates the module, and every `TestBed.override*` afterwards throws, including one
|
|
1729
|
+
written above it inside a `createComponent` helper. Configure the double after the overrides
|
|
1730
|
+
(`injectSpy(X)` in the test), or keep the access lazy: `const api = () => injectSpy(Api)`. The check
|
|
1731
|
+
is order-free by design, since a helper declared above the hook still runs after it.
|
|
1732
|
+
|
|
1733
|
+
`no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
|
|
1734
|
+
last provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
|
|
1735
|
+
auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out.
|
|
1736
|
+
Found on eight tokens of one file. It reads both spellings in either order, compares tokens as
|
|
1737
|
+
source text, and offers no fix: deleting either line is a valid repair and they mean opposite
|
|
1738
|
+
things.
|
|
1131
1739
|
|
|
1132
1740
|
The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
|
|
1133
1741
|
packages, which a subpath export can never be.
|
|
@@ -1151,6 +1759,7 @@ packages, which a subpath export can never be.
|
|
|
1151
1759
|
| `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) |
|
|
1152
1760
|
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
1153
1761
|
| `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 |
|
|
1762
|
+
| `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
|
|
1154
1763
|
| `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) |
|
|
1155
1764
|
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
1156
1765
|
| an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
|
|
@@ -1176,6 +1785,8 @@ packages, which a subpath export can never be.
|
|
|
1176
1785
|
| a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
|
|
1177
1786
|
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
|
|
1178
1787
|
| a hand-tuned number of turns waiting for a `resource()` to load | the hand-off count depends on the dependency, not on the spec | `await flushEventLoopUntil(() => r.status() !== 'loading', { label })` |
|
|
1788
|
+
| `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
|
|
1789
|
+
| a template error in a `describe` that never patched anything | a spec `afterEach` threw and skipped `setupAutoSpy`'s, so a `mock*Prop` patch travelled | upgrade — an `onTestFinished` net restores it and names the cause (§10) |
|
|
1179
1790
|
| `Property 'mockReturnValue' does not exist on type 'never'` | a generic method with a conditional return type; the spy collapsed | upgrade — fixed in the types; the member now keeps its sync helper bundle |
|
|
1180
1791
|
| `Type 'string' is not assignable to type 'never'` on `gettersToSpyOn` | the list used to reject callable values, i.e. every `Signal<T>` | upgrade — any string key is nameable; prefer `mockSignalProp` for a signal |
|
|
1181
1792
|
| `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
|
|
@@ -1183,6 +1794,15 @@ packages, which a subpath export can never be.
|
|
|
1183
1794
|
| a spy that cannot take a real `signal()` in `mockReadonlyProp` | the value was typed against `Spy<T>[K]`, not against `T[K]` | upgrade — the `mock*Prop` helpers accept a `Spy<T>` and check against `T` |
|
|
1184
1795
|
| `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
|
|
1185
1796
|
| `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` |
|
|
1797
|
+
| `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) |
|
|
1798
|
+
| a stream built with `of(double)` that never emits | the double answered `schedule`, so rxjs ate it as the scheduler | upgrade — `schedule` / `lift` / `@@observable` / `getReader` are answered `undefined` (§2) |
|
|
1799
|
+
| `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` |
|
|
1800
|
+
| `nextWith` emitting nothing, on a spy an earlier test completed or errored | `error()`/`complete()` close a Subject for good | upgrade — the next configuration starts a new stream |
|
|
1801
|
+
| `accessorSpies.setters.X` is `undefined` after `gettersToSpyOn: ['X']` | only the named half was spied | upgrade — the declared other half is mirrored (§5) |
|
|
1802
|
+
| `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic, so it keeps the `readonly` of an abstract getter | `Mutable<Spy<T>>` for direct assignment, or `mockValueProp` for a patch that is undone |
|
|
1803
|
+
| a `mock*Prop` patch on a `createAutoMock` double that changes nothing | the Proxy had no `defineProperty` trap before 3.5.0 | upgrade — nothing to change in the spec |
|
|
1804
|
+
| `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
|
|
1805
|
+
| half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
|
|
1186
1806
|
| `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
|
|
1187
1807
|
| two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
|
|
1188
1808
|
| a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
|
|
@@ -1202,6 +1822,19 @@ packages, which a subpath export can never be.
|
|
|
1202
1822
|
| `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
|
|
1203
1823
|
| `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
|
|
1204
1824
|
| `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
|
|
1825
|
+
| `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
|
|
1826
|
+
| `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
|
|
1827
|
+
| `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
|
|
1828
|
+
| `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
|
|
1829
|
+
| `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
|
|
1830
|
+
| `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
|
|
1831
|
+
| hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
|
|
1832
|
+
| `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
|
|
1833
|
+
| a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
|
|
1834
|
+
| a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
|
|
1835
|
+
| `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
|
|
1836
|
+
| `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
|
|
1837
|
+
| `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
|
|
1205
1838
|
| `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
|
|
1206
1839
|
| `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
|
|
1207
1840
|
| `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
|
|
@@ -1218,7 +1851,7 @@ packages, which a subpath export can never be.
|
|
|
1218
1851
|
| ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
|
|
1219
1852
|
| `await fixture.whenRenderingDone()` | `await stable(fixture)` |
|
|
1220
1853
|
| an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
|
|
1221
|
-
| `.overrideProvider(X, provideAutoSpy(X))` (
|
|
1854
|
+
| `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
|
|
1222
1855
|
| `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
|
|
1223
1856
|
| `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
|
|
1224
1857
|
| an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
|
|
@@ -1226,7 +1859,18 @@ packages, which a subpath export can never be.
|
|
|
1226
1859
|
| `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
|
|
1227
1860
|
| a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console` |
|
|
1228
1861
|
| `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
|
|
1229
|
-
|
|
1862
|
+
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
**The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
|
|
1866
|
+
when the double is *called*; `mockReturnValue(x)` freezes the value `x` had when the double was
|
|
1867
|
+
*configured*. They are indistinguishable until the test reassigns `x` — and the commonest reason to
|
|
1868
|
+
do that is a fresh `Subject` after the previous one has been `error()`ed or completed, which is
|
|
1869
|
+
exactly the case a suite is testing when it reassigns. The double then keeps handing out the dead
|
|
1870
|
+
one: in one spec the service received a completed subject and silently skipped the modal it was
|
|
1871
|
+
meant to show, with the test still green. Carry `vi.fn(() => x)` over as
|
|
1872
|
+
`mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
|
|
1873
|
+
anyone writing a codemod, because the rename looks like the safest edit in the file.
|
|
1230
1874
|
---
|
|
1231
1875
|
|
|
1232
1876
|
## 19. Before you report success
|
|
@@ -1240,3 +1884,21 @@ npx tsc --noEmit # Spy<T> mistakes are compile errors, not
|
|
|
1240
1884
|
|
|
1241
1885
|
Type errors matter here more than usual: most of this library's guarantees are type-level, so a
|
|
1242
1886
|
suite that runs green but does not type-check is not done.
|
|
1887
|
+
|
|
1888
|
+
### If you are writing a codemod over specs
|
|
1889
|
+
|
|
1890
|
+
Two traps, both found the hard way on rxjs-heavy code.
|
|
1891
|
+
|
|
1892
|
+
**`String.prototype.replace` interprets `$` in the replacement.** `$&`, `` $` ``, `$'` and `$n` are
|
|
1893
|
+
substitution patterns, and `$'` — "everything after the match" — is one character away from every
|
|
1894
|
+
observable name in the codebase. A replacement containing `forceRequeryAndStartPlaybackAt$'`
|
|
1895
|
+
inserted the entire remainder of the file into itself and left an unterminated string; the only
|
|
1896
|
+
thing that caught it was ESLint's `Parsing error`. Pass a function, which is never interpreted:
|
|
1897
|
+
|
|
1898
|
+
```ts
|
|
1899
|
+
source.replace(from, () => to); // not source.replace(from, to)
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
**`node.getStart()` excludes leading comments.** A codemod that replaces a range starting there
|
|
1903
|
+
silently eats the `// eslint-disable-next-line` above the node. Use `node.getFullStart()`, or count
|
|
1904
|
+
the comments before and after and compare against `HEAD`.
|