vitest-auto-spy 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +456 -246
- package/README.md +488 -165
- package/dist/angular.d.ts +61 -5
- package/dist/angular.js +1746 -102
- package/dist/bun-angular.d.ts +4 -4
- package/dist/bun-angular.js +196 -12
- package/dist/bun.d.ts +122 -15
- package/dist/bun.js +6 -9
- package/dist/{chunk-ISSGEVLO.js → chunk-2U3J65A4.js} +17 -76
- package/dist/{chunk-WT75WGQZ.js → chunk-3AWKPZCE.js} +154 -18
- package/dist/{chunk-DNHLQG45.js → chunk-B7PDIHEG.js} +92 -25
- package/dist/chunk-J5O72UHX.js +6 -0
- package/dist/chunk-LVLSMH6T.js +49 -0
- package/dist/{chunk-OS2QFTIF.js → chunk-MG5EAMZR.js} +3 -2
- package/dist/{chunk-QGBNXDKU.js → chunk-R4EWAG7Y.js} +51 -16
- package/dist/{chunk-OWQR2YVQ.js → chunk-Y4YG7UOK.js} +10 -11
- package/dist/cli.js +1502 -207
- package/dist/console.d.ts +1 -1
- package/dist/console.js +4 -8
- package/dist/eslint-plugin.cjs +279 -90
- package/dist/eslint-plugin.d.cts +16 -0
- package/dist/eslint-plugin.d.ts +16 -0
- package/dist/eslint-plugin.js +279 -90
- package/dist/{expect-emission-Cpj2GGcD.d.ts → expect-emission-CuGjE0YR.d.ts} +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1922 -10
- package/dist/nestjs.d.ts +2 -1
- package/dist/nestjs.js +6 -6
- package/dist/node.cjs +304 -121
- package/dist/node.d.ts +3 -3
- package/dist/node.js +17 -19
- package/dist/{prop-mock-CeHqAeIG.d.ts → prop-mock-IgCwv_40.d.ts} +1 -1
- package/dist/react.d.ts +3 -3
- package/dist/react.js +7 -11
- package/dist/rxjs.d.ts +2 -2
- package/dist/rxjs.js +2 -4
- package/dist/setup.d.ts +66 -4
- package/dist/setup.js +112 -17
- package/dist/shared-state.js +447 -0
- package/dist/svelte.d.ts +3 -3
- package/dist/svelte.js +7 -11
- package/dist/track-injections-BAzV5ITq.d.ts +56 -0
- package/dist/{types-W3lPrwC7.d.ts → types-it6CnDkI.d.ts} +150 -18
- package/dist/vue.d.ts +4 -4
- package/dist/vue.js +8 -11
- package/dist/{zoneless-Ch2Ym_6z.d.ts → zoneless-BUNSBSIP.d.ts} +16 -10
- package/package.json +3 -2
- package/skills/vitest-auto-spy/SKILL.md +85 -50
- package/dist/chunk-2PFOBMTZ.js +0 -200
- package/dist/chunk-DA5E36HQ.js +0 -81
- package/dist/chunk-DMSELR3S.js +0 -9
- package/dist/chunk-TMO2UFLD.js +0 -200
- package/dist/chunk-TNB3Y3GI.js +0 -27
- package/dist/chunk-VR5GJRBS.js +0 -24
package/AGENTS.md
CHANGED
|
@@ -12,11 +12,11 @@ node_modules/vitest-auto-spy/AGENTS.md
|
|
|
12
12
|
|
|
13
13
|
Working on the library's own source instead? Read `CONTRIBUTING.md` in the repository.
|
|
14
14
|
|
|
15
|
-
Setting this up for a team?
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
Setting this up for a team? `npx vitest-auto-spy init` writes a pointer to this file into the
|
|
16
|
+
instruction files this repository's agents actually read — `AGENTS.md` (Codex, Cursor, Copilot and
|
|
17
|
+
most of the field), `CLAUDE.md` (Claude Code) and `GEMINI.md` (Gemini CLI), plus the glob-scoped
|
|
18
|
+
rule file of any tool whose own directory already exists. `--check` is the CI form. Full table:
|
|
19
|
+
<https://asdalexey.github.io/vitest-auto-spy/agents>.
|
|
20
20
|
|
|
21
21
|
| Resource | Where |
|
|
22
22
|
| ----------------------- | -------------------------------------------------------------------- |
|
|
@@ -49,13 +49,14 @@ adapter installed and spies fail at runtime.
|
|
|
49
49
|
| Vue / Pinia | `vitest-auto-spy/vue` |
|
|
50
50
|
| Svelte | `vitest-auto-spy/svelte` |
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Four add-ons, orthogonal to the runner:
|
|
53
53
|
|
|
54
54
|
| Add-on | Import | Needed for |
|
|
55
55
|
| ---------------- | ------------------------------- | --------------------------------------------------- |
|
|
56
56
|
| Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
|
|
57
57
|
| Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
|
|
58
58
|
| Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
|
|
59
|
+
| Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
|
|
59
60
|
|
|
60
61
|
The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
|
|
61
62
|
CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
|
|
@@ -89,17 +90,16 @@ Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
|
|
|
89
90
|
and the question that decides it is **how the double travels**. Through DI, it arrives as `Spy<T>`
|
|
90
91
|
and is only ever asserted on — `createAutoMock`. Handed to the function under test as an argument
|
|
91
92
|
(`detectVpnClient(url, logger)`, `applyPreferredTracks(target, …)`, `setLocalConfigEnabled(storage, …)`),
|
|
92
|
-
it has to satisfy `T` at the call site
|
|
93
|
+
it has to satisfy `T` at the call site _and_ expose the spy helpers at the assertion, and
|
|
93
94
|
`autoMocked<T>()` is that intersection — otherwise every call site needs an `asInstance()` and the
|
|
94
95
|
noise scales with the number of them.
|
|
95
96
|
|
|
96
|
-
|
|
97
97
|
`createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
|
|
98
98
|
`no-type-assertion` lint rule without an `eslint-disable` on every fixture.
|
|
99
99
|
|
|
100
100
|
**`mockDeep` builds depth on property access, not on calls** — the distinction the tree now spells
|
|
101
101
|
out, and the one that costs an afternoon otherwise. `mock.repo.user.find()` chains because every hop
|
|
102
|
-
but the last is a
|
|
102
|
+
but the last is a _read_. A node that is **called** returns what it was configured to return, and by
|
|
103
103
|
default that is `undefined`, so `mockDeep<AppLogger>().channel('app').info('x')` is a `TypeError` at
|
|
104
104
|
the second call — while `DeepMockProxy<AppLogger>` types it perfectly, so nothing warns. Pass
|
|
105
105
|
`{ selfReturning: true }` for a fluent API, or use `createAutoMock<T>()` with
|
|
@@ -113,7 +113,7 @@ expect(logger.channel('app').info).toHaveBeenCalledWith('started');
|
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
Both bridges exist, and which one you need depends on the direction. What a self-returning **call**
|
|
116
|
-
hands back is typed as the
|
|
116
|
+
hands back is typed as the _declared_ return type, not as a spy — `asSpy<T>(…)` when the helpers are
|
|
117
117
|
needed. The **whole mock** is a `DeepMockProxy<T>`, which is not assignable to `T` for the same
|
|
118
118
|
reason `Spy<T>` is not (a mapped type cannot see private members) — `asInstance(…)` when it has to
|
|
119
119
|
go somewhere typed against the real thing:
|
|
@@ -147,7 +147,7 @@ by default would silence a real typo on every concrete class.
|
|
|
147
147
|
**`overrides: { key: undefined }` is a seed, not an omission**, and the difference is load-bearing.
|
|
148
148
|
`createAutoMock` reads its seed with `Reflect.ownKeys`, so a key written out with an explicit
|
|
149
149
|
`undefined` **is** in the store: reading it answers `undefined`. Leave it out and the same read
|
|
150
|
-
materialises a
|
|
150
|
+
materialises a _function spy_ — which is truthy, and sends `if (this.lastFocus)` down the branch the
|
|
151
151
|
spec was trying to close:
|
|
152
152
|
|
|
153
153
|
```ts
|
|
@@ -162,15 +162,15 @@ redundant.
|
|
|
162
162
|
|
|
163
163
|
`createAutoMock` and `mockDeep` build a Proxy, not an object, and there is one place where the
|
|
164
164
|
difference shows: a Proxy answers only the operations its handler traps. Three of them used to be
|
|
165
|
-
missing, and each produced a
|
|
165
|
+
missing, and each produced a _silent_ wrong answer rather than an error — the worst failure mode
|
|
166
166
|
this library can have, because a checking test becomes a non-checking one and only the proxy's
|
|
167
167
|
source says so. Two are fixed; the third cannot be:
|
|
168
168
|
|
|
169
|
-
| Operation
|
|
170
|
-
|
|
|
171
|
-
| `mockValueProp` & the other three
|
|
172
|
-
| `delete mock.optionalMethod`
|
|
173
|
-
| `Object.assign(real, mock)`
|
|
169
|
+
| Operation | Before 3.5.0 | Now |
|
|
170
|
+
| --------------------------------- | ------------------------------------------------- | -------------------------------------------------------- |
|
|
171
|
+
| `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
|
|
172
|
+
| `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
|
|
173
|
+
| `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
|
|
174
174
|
|
|
175
175
|
`ownKeys` cannot be completed: a type has no key list at runtime, which is the whole premise of
|
|
176
176
|
these two factories. So a spec that installs a double by **copying it onto a real instance** —
|
|
@@ -179,7 +179,7 @@ every other call goes to the real implementation, silently. Use `createSpyFromCl
|
|
|
179
179
|
returns an ordinary object whose method keys are enumerable (lazy accessors, but enumerable), so
|
|
180
180
|
the copy is complete.
|
|
181
181
|
|
|
182
|
-
### It answers everything, so it must not answer
|
|
182
|
+
### It answers everything, so it must not answer _these_
|
|
183
183
|
|
|
184
184
|
The same premise cuts the other way. A library that is handed an object and has to decide **what
|
|
185
185
|
kind of thing it is** asks by probing a key — and a double that answers every property answers the
|
|
@@ -187,12 +187,12 @@ probe too, at which point it stops being a double of `T` and becomes whatever wa
|
|
|
187
187
|
Four names are therefore answered with `undefined` unless the spec seeds them, alongside `then` and
|
|
188
188
|
every symbol, which always were:
|
|
189
189
|
|
|
190
|
-
| Key | Probed by | The double became
|
|
191
|
-
| -------------- | --------------------------------------------- |
|
|
192
|
-
| `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler
|
|
193
|
-
| `lift` | `isObservable`, with `subscribe` | an Observable
|
|
194
|
-
| `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream
|
|
195
|
-
| `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream
|
|
190
|
+
| Key | Probed by | The double became |
|
|
191
|
+
| -------------- | --------------------------------------------- | ----------------- |
|
|
192
|
+
| `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
|
|
193
|
+
| `lift` | `isObservable`, with `subscribe` | an Observable |
|
|
194
|
+
| `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
|
|
195
|
+
| `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
|
|
196
196
|
|
|
197
197
|
The one that cost an afternoon reads like nothing at all:
|
|
198
198
|
|
|
@@ -209,8 +209,8 @@ was an assertion about an unrelated `emit()` three concerns away — nothing in
|
|
|
209
209
|
**`subscribe` is deliberately not on that list.** It is an ordinary method name — a store, an
|
|
210
210
|
Angular `OutputEmitterRef`, an event bus — and `expect(store.subscribe).toHaveBeenCalledWith(cb)` is
|
|
211
211
|
a real assertion. Denying `lift` and `@@observable` already breaks the impersonation, so `subscribe`
|
|
212
|
-
on its own fools nothing: `from(double)` now fails with rxjs's own
|
|
213
|
-
where a stream was expected"
|
|
212
|
+
on its own fools nothing: `from(double)` now fails with rxjs's own _"You provided an invalid object
|
|
213
|
+
where a stream was expected"_, loudly and in the right file.
|
|
214
214
|
|
|
215
215
|
If your type genuinely has one of the four, say so once and it comes back — the list is consulted
|
|
216
216
|
after the seed store:
|
|
@@ -225,7 +225,7 @@ file. A key is only added to that list with an observed mechanic behind it — n
|
|
|
225
225
|
sounds protocol-ish — because every entry costs somebody the ability to mock a member of that name
|
|
226
226
|
without seeding it.
|
|
227
227
|
|
|
228
|
-
The tree asks whether the double is
|
|
228
|
+
The tree asks whether the double is _called_, and there is a second question worth asking: whether
|
|
229
229
|
the code under test **writes to it**. `createAutoMock` is a proxy with a `set` trap over the same
|
|
230
230
|
cache its `get` trap answers from, so an assignment sticks and is read back — which makes it the
|
|
231
231
|
double for a DOM-ish object a library drives by assigning handlers, where a hand-written fake is
|
|
@@ -242,15 +242,12 @@ xhr.send.mockImplementation(() => respond(asInstance(xhr)));
|
|
|
242
242
|
|
|
243
243
|
### Cost, so it stops being a question
|
|
244
244
|
|
|
245
|
-
Building a spy is not a thing to optimise.
|
|
246
|
-
`
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
`provideAutoSpy` is the fastest of the three because it defaults to `lazySpies: true`. The two
|
|
252
|
-
settings that do cost: `{ lazySpies: false }` gives that up, and `autoSpyAccessors: true` walks
|
|
253
|
-
the prototype chain uncached on every call — name the accessors instead.
|
|
245
|
+
Building a spy is not a thing to optimise. On a ten-method class: `provideAutoSpy` ~8 µs,
|
|
246
|
+
`createSpyFromClass` ~29 µs, `createAutoMock` ~33 µs, a `calledWith` lookup ~0.7 µs — five providers
|
|
247
|
+
across two thousand tests is under a tenth of a second. Call the factory in `beforeEach` and look at
|
|
248
|
+
`TestBed` instead. The only two settings that cost: `{ lazySpies: false }` gives up the laziness
|
|
249
|
+
`provideAutoSpy` defaults to, and `autoSpyAccessors: true` walks the prototype chain uncached on
|
|
250
|
+
every call — name the accessors instead.
|
|
254
251
|
|
|
255
252
|
---
|
|
256
253
|
|
|
@@ -443,6 +440,8 @@ createSpyFromClass(MyService, {
|
|
|
443
440
|
| `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
|
|
444
441
|
| `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
|
|
445
442
|
| `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. |
|
|
443
|
+
| `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
|
|
444
|
+
| `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
|
|
446
445
|
|
|
447
446
|
**`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
|
|
448
447
|
the reference suite). Method discovery walks the _prototype chain_; a callable assigned to an
|
|
@@ -471,7 +470,7 @@ There is no better message to be had at runtime, and it is worth saying why rath
|
|
|
471
470
|
looking like an oversight. Instance fields do not exist until a constructor has run, and this
|
|
472
471
|
library never constructs the class — that is what makes a spy safe to build from a service whose
|
|
473
472
|
constructor talks to the network. The only alternative would be to answer an unknown member with
|
|
474
|
-
|
|
473
|
+
_something_, and that something would be truthy: `if (service.optionalThing)` in the code under test
|
|
475
474
|
would then take the wrong branch, silently, which is the exact failure mode the protocol deny-list
|
|
476
475
|
in §2 exists to remove. A loud `TypeError` on the spec's own line is the better of the two.
|
|
477
476
|
|
|
@@ -498,7 +497,7 @@ Also true, and worth not re-deriving:
|
|
|
498
497
|
- **An overloaded method is not collapsed.** The worry that `Spy<T>` types every generated
|
|
499
498
|
`api-mgw` client against its last signature does not hold: a four-overload
|
|
500
499
|
`MgwContentsService.getMoviesBySlug` types as it should, and hand-written `{ m: vi.fn() }` doubles
|
|
501
|
-
for those services convert with no changes to the assertions. When the
|
|
500
|
+
for those services convert with no changes to the assertions. When the _first_ signature is the
|
|
502
501
|
useful one, name it on the **declaration only** — the factory's result assigns to it, so the type
|
|
503
502
|
argument is not written twice:
|
|
504
503
|
|
|
@@ -517,6 +516,38 @@ class prototype, so nothing is being shadowed), and
|
|
|
517
516
|
`service.isKidMode.mockReturnValue(false)` reads like every other member. `mockSignalProp` is the
|
|
518
517
|
other answer when the value has to change during the test.
|
|
519
518
|
|
|
519
|
+
### `strict` — a method nobody configured throws instead of answering `undefined`
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
const users = createSpyFromClass(UserService, { strict: true });
|
|
523
|
+
|
|
524
|
+
users.load.resolveWith([]);
|
|
525
|
+
users.currentTenant(); // throws here, not four frames later inside the component
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
[vitest-auto-spy] Nothing configured Cart.checkout, and strict mode is on.
|
|
530
|
+
Called as: Cart.checkout(1,'now')
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Also on `createAutoMock`, `provideAutoSpy`, and suite-wide as `setupAutoSpy({ strict: true })`.
|
|
534
|
+
Precedence, first one set wins: the double's `onUnstubbedCall` → the global `onUnstubbedCall` → the
|
|
535
|
+
double's `strict` (**including `strict: false`**, the only way to exempt one double from a suite-wide
|
|
536
|
+
default) → the global `strict`.
|
|
537
|
+
|
|
538
|
+
- **It is not argument-level.** A `calledWith(1, 'now')` chain says the method is stubbed, so
|
|
539
|
+
`checkout(9, 'later')` still answers `undefined`. Use `mustBeCalledWith` for that.
|
|
540
|
+
- **`mockReturnValue` / `mockImplementation` / the `returns:` option replace the dispatch**, so they
|
|
541
|
+
never reach the guard. Consequence: after the last `mockReturnValueOnce` the queue empties back
|
|
542
|
+
onto the library dispatch and the next call **is** reported as unstubbed — seed
|
|
543
|
+
`mockReturnValue(...)` too when a `Once` sequence is meant to run out.
|
|
544
|
+
- **It does not reach** accessor spies, observable-property spies, `mockDeep` nodes, `console-spy`,
|
|
545
|
+
`mockResourceProp`'s `reload` or a standalone `createFunctionSpy`. A strict double still answers
|
|
546
|
+
`undefined` for an unconfigured getter or `items$`. `fillMissing` members **are** covered.
|
|
547
|
+
- Use `onUnstubbedCall` to survey a suite before turning the throw on — it is handed
|
|
548
|
+
`{ className, method, args }` and whatever it returns is what the call answers, so a handler that
|
|
549
|
+
pushes to an array records the gap without failing anything.
|
|
550
|
+
|
|
520
551
|
### Getters and setters live in `accessorSpies`
|
|
521
552
|
|
|
522
553
|
```ts
|
|
@@ -536,7 +567,7 @@ Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs:
|
|
|
536
567
|
`accessorSpies.setters.theme` was `undefined`, and the failure read
|
|
537
568
|
`Cannot read properties of undefined` three steps from the configuration behind it.
|
|
538
569
|
|
|
539
|
-
Only spy a getter when the spec asserts that it was **read**. To make one
|
|
570
|
+
Only spy a getter when the spec asserts that it was **read**. To make one _answer_ something, on a
|
|
540
571
|
spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
|
|
541
572
|
factory, which is the part that is otherwise found by trial:
|
|
542
573
|
|
|
@@ -608,6 +639,24 @@ resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mo
|
|
|
608
639
|
Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
|
|
609
640
|
proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
|
|
610
641
|
|
|
642
|
+
Every double also carries `[Symbol.dispose]()` — it runs `resetAutoSpy(this)` — so an `afterEach`
|
|
643
|
+
that exists only to reset one spy can be deleted:
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
it('loads', () => {
|
|
647
|
+
using cart = createSpyFromClass(Cart); // reset when the block ends
|
|
648
|
+
|
|
649
|
+
cart.total.calledWith().mockReturnValue(42);
|
|
650
|
+
expect(cart.total()).toBe(42);
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
`createAutoMock` proxies and **every `mockDeep` node** carry it (so `using` on a sub-tree resets that
|
|
655
|
+
sub-tree). The key is non-enumerable, so a spread does not copy it, and there is no
|
|
656
|
+
`[Symbol.asyncDispose]`. A standalone `createFunctionSpy` is **not** covered: Vitest's own
|
|
657
|
+
`[Symbol.dispose]` on a host mock restores the original implementation instead — call
|
|
658
|
+
`resetAutoSpy(spy)` there. If the project does not transpile `using`, call `spy[Symbol.dispose]()`.
|
|
659
|
+
|
|
611
660
|
---
|
|
612
661
|
|
|
613
662
|
## 8. Observable assertions (core entry — no rxjs needed)
|
|
@@ -638,7 +687,7 @@ with rxjs's `EmptyError`, and the workaround people arrive at,
|
|
|
638
687
|
part. Emissions do not fail it: it asserts termination, nothing about what came before.
|
|
639
688
|
|
|
640
689
|
**To assert that production code pushed into a stream, do not use `observablePropsToSpyOn`.** That
|
|
641
|
-
option points the other way: it gives the spec `nextWith` so it can
|
|
690
|
+
option points the other way: it gives the spec `nextWith` so it can _feed_ the double. When the
|
|
642
691
|
question is whether the code under test called `next` on a property, the double needs a real
|
|
643
692
|
`Subject` and a spy on its method:
|
|
644
693
|
|
|
@@ -656,9 +705,9 @@ expect(next).toHaveBeenCalledWith(1000);
|
|
|
656
705
|
the type either way — which is exactly why this is worth saying: the code compiles against the spy
|
|
657
706
|
surface and asserts nothing.
|
|
658
707
|
|
|
659
|
-
**When the error
|
|
708
|
+
**When the error _is_ the assertion, use `expectError`.** The other helpers wrap a stream failure in
|
|
660
709
|
a new `Error` whose message names the stream — right for reporting an unexpected failure, useless
|
|
661
|
-
when the failure is the subject. `expectError` resolves
|
|
710
|
+
when the failure is the subject. `expectError` resolves _with_ the error, exactly as it was thrown:
|
|
662
711
|
|
|
663
712
|
```ts
|
|
664
713
|
await expect(expectError(service.load())).resolves.toBe(originalError);
|
|
@@ -684,7 +733,7 @@ they keep the diagnosis: emissions that do not match are still counted, so a fai
|
|
|
684
733
|
fired".
|
|
685
734
|
|
|
686
735
|
**`advance` closes the window between subscribing and awaiting.** A stream driven by a
|
|
687
|
-
`debounceTime`, a retry or a poll needs the clock moved
|
|
736
|
+
`debounceTime`, a retry or a poll needs the clock moved _after_ something is listening, and `await`
|
|
688
737
|
gives control away before the next statement runs:
|
|
689
738
|
|
|
690
739
|
```ts
|
|
@@ -699,7 +748,7 @@ runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`.
|
|
|
699
748
|
**The watchdog runs on real time, on purpose — even under fake timers.** A virtual one would race
|
|
700
749
|
the timers the spec advances: `expectEmission(source$, { timeout: 200 })` followed by
|
|
701
750
|
`vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the spec was
|
|
702
|
-
about to advance into. The cost is that in a suite with global fake timers a
|
|
751
|
+
about to advance into. The cost is that in a suite with global fake timers a _failing_ assertion
|
|
703
752
|
spends a real second. Do **not** answer that with `{ timeout: 0 }` at every call site — that
|
|
704
753
|
disables the watchdog, and the next silent stream hangs to the runner's own timeout with nothing
|
|
705
754
|
useful in the message. Lower the default once instead:
|
|
@@ -715,7 +764,7 @@ setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
|
|
|
715
764
|
|
|
716
765
|
**`expectEmission` subscribes when you call it, not when you await it**, and that is load-bearing
|
|
717
766
|
rather than an implementation detail. It is what converts the test whose source has to be poked
|
|
718
|
-
|
|
767
|
+
_after_ somebody is listening — a router event, a `Subject` the spec pushes into, anything that
|
|
719
768
|
does not replay:
|
|
720
769
|
|
|
721
770
|
```ts
|
|
@@ -769,12 +818,12 @@ mockValueProp(spy, 'products$', new Subject()); // a member the double does not
|
|
|
769
818
|
```
|
|
770
819
|
|
|
771
820
|
The last one is worth knowing on its own: patching a key the object never had **works and is undone
|
|
772
|
-
correctly** — the journal records the
|
|
821
|
+
correctly** — the journal records the _absence_ of a descriptor and puts it back by deleting the
|
|
773
822
|
property. That is how you add an Observable member that `provideAutoSpy` did not create because
|
|
774
823
|
`observablePropsToSpyOn` was not passed.
|
|
775
824
|
|
|
776
825
|
What the second overload costs is the property-name check, so a typo in the name compiles. Nothing
|
|
777
|
-
checks the
|
|
826
|
+
checks the _value_ on either overload; that is deliberate, and the partial fixture above is why.
|
|
778
827
|
|
|
779
828
|
### Properties of DOM objects — the same helpers, and the reason to look for them
|
|
780
829
|
|
|
@@ -817,7 +866,7 @@ check that fails the run, and (opt-in) `vi.restoreAllMocks()`. Turn on `restoreM
|
|
|
817
866
|
suite runs with `isolate: false`.
|
|
818
867
|
|
|
819
868
|
**The restore also runs from an `onTestFinished` net**, because the `afterEach` is not guaranteed
|
|
820
|
-
to. Vitest calls `afterEach` hooks in
|
|
869
|
+
to. Vitest calls `afterEach` hooks in _reverse_ registration order, so the setup file's is the last
|
|
821
870
|
one, and a hook the spec file registered takes the chain down with it when it throws — the patches
|
|
822
871
|
then travel into the next test and the failure surfaces somewhere that never touched them. One spec
|
|
823
872
|
kept `afterEach(() => vi.restoreAllMocks())`; migrating it to `gettersToSpyOn` made the restored
|
|
@@ -878,15 +927,13 @@ component trees.
|
|
|
878
927
|
setupAutoSpy({ pruneMockRegistry: true }); // keep only the mocks that outlive a file
|
|
879
928
|
```
|
|
880
929
|
|
|
881
|
-
The part to understand before turning it on is what must **not** be pruned.
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
file
|
|
887
|
-
|
|
888
|
-
kept; everything added after that belongs to the file and goes when it ends. One case lands on the
|
|
889
|
-
wrong side — a module first loaded by a dynamic `import()` inside a test — and says so explicitly:
|
|
930
|
+
The part to understand before turning it on is what must **not** be pruned. A pruned mock is one
|
|
931
|
+
`clearMocks` can no longer see, so its calls accumulate silently — harmless for a mock that dies with
|
|
932
|
+
its file, a bug for the module-level `vi.fn()` in a shared `*.mock.ts` that six spec files import.
|
|
933
|
+
The split is therefore drawn where it is observable: what is already in the registry when a file's
|
|
934
|
+
hooks start was created while the module graph was evaluated and is kept; everything added after that
|
|
935
|
+
belongs to the file and goes when it ends. One case lands on the wrong side — a module first loaded
|
|
936
|
+
by a dynamic `import()` inside a test — and says so explicitly:
|
|
890
937
|
|
|
891
938
|
```ts
|
|
892
939
|
export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
|
|
@@ -901,25 +948,21 @@ Two more switches, both about the environment rather than the spies:
|
|
|
901
948
|
setupAutoSpy({ blockNetwork: true }); // fetch rejects, XHR fails, sendBeacon answers false
|
|
902
949
|
```
|
|
903
950
|
|
|
904
|
-
|
|
905
|
-
really fetches it, nothing asserts on the response
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
(`FW.ajax`) — so a suite with `blockNetwork: true` already on was still reaching the internet, one
|
|
912
|
-
ping per quartile per ad per test, and printing jsdom's `AggregateError at Object.dispatchError`
|
|
913
|
-
for every connection that failed. What a green run prints then depends on whether the machine has a
|
|
914
|
-
route out.
|
|
951
|
+
Both DOMs leak, through different holes. happy-dom implements `fetch`, so a component pulling a
|
|
952
|
+
remote asset really fetches it, nothing asserts on the response, and the aborts at teardown fail the
|
|
953
|
+
run with **no test named** — a green run exiting 1 with `DOMException [AbortError]`. jsdom
|
|
954
|
+
implements `XMLHttpRequest` in full, and the libraries that never left XHR (a VAST player pinging
|
|
955
|
+
every tracker through a hand-rolled one) reach the internet once per ping per test, printing jsdom's
|
|
956
|
+
`AggregateError at Object.dispatchError` for each connection that failed — so what a green run
|
|
957
|
+
prints depends on whether the machine has a route out.
|
|
915
958
|
|
|
916
959
|
Every channel is closed by default; the object is for narrowing it:
|
|
917
960
|
|
|
918
|
-
| option | default | what it does
|
|
919
|
-
| -------- | ---------- |
|
|
920
|
-
| `fetch` | `true` | `fetch` rejects, naming what was requested
|
|
961
|
+
| option | default | what it does |
|
|
962
|
+
| -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
963
|
+
| `fetch` | `true` | `fetch` rejects, naming what was requested |
|
|
921
964
|
| `xhr` | `'reject'` | `'reject'` fails the request (`status` 0, an `error` event); `'empty'` answers 200 with an empty body; `false` leaves XHR alone |
|
|
922
|
-
| `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one
|
|
965
|
+
| `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one |
|
|
923
966
|
|
|
924
967
|
`'reject'` is the default because it is what `fetch` does: the code takes its failure branch, which
|
|
925
968
|
is the branch a unit test should be asserting on. `'empty'` is for a request whose response nobody
|
|
@@ -933,10 +976,10 @@ setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setti
|
|
|
933
976
|
A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
|
|
934
977
|
serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
|
|
935
978
|
and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
|
|
936
|
-
resolves it against the document origin, so a spec that reaches `/config`
|
|
937
|
-
nothing listening on that port
|
|
979
|
+
resolves it against the document origin, so a spec that reaches `/config`and passes is resting on
|
|
980
|
+
nothing listening on that port.`WebSocket`and`EventSource`are left alone: their failure is an
|
|
938
981
|
event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
|
|
939
|
-
a behaviour change
|
|
982
|
+
a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
|
|
940
983
|
|
|
941
984
|
`restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
|
|
942
985
|
fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
|
|
@@ -944,6 +987,50 @@ the realm, not owned by `globalThis`), and with `isolate: false` the next file d
|
|
|
944
987
|
own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
|
|
945
988
|
the file in the stack is not the cause.
|
|
946
989
|
|
|
990
|
+
`hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
|
|
991
|
+
CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
|
|
992
|
+
`test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
|
|
993
|
+
own and defaults it to 10 000 ms. Carry a preset's `testTimeout: 30000` into the runner config and
|
|
994
|
+
stop there, and every hook in the suite silently runs on a third of what its tests get. Worse, the
|
|
995
|
+
failure is filed against the wrong thing: a `beforeEach` timeout is attributed to the **test**, with
|
|
996
|
+
the test's duration pinned at the limit, so the report reads `× should create 10045ms` and sends the
|
|
997
|
+
reader looking for ten seconds of work inside a body that never ran. The hint appends both numbers
|
|
998
|
+
and the field to set. It says nothing when the budgets agree — then the hook really is slow — and
|
|
999
|
+
nothing for `beforeEach(fn, 300)`, which chose its own limit. `beforeAll` is out of reach by
|
|
1000
|
+
construction: its timeout is reported as a failed _suite_, every test is skipped and no `afterEach`
|
|
1001
|
+
runs.
|
|
1002
|
+
|
|
1003
|
+
**When migrating a runner config off Jest, set both timeouts side by side**, and treat the single
|
|
1004
|
+
Jest number as belonging to both fields:
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
test: {
|
|
1008
|
+
testTimeout: 30_000,
|
|
1009
|
+
// Jest had one budget for both; Vitest defaults this to 10_000 on its own.
|
|
1010
|
+
hookTimeout: 30_000,
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
`frozenClockHint` is the same seam aimed at the other half of a timeout. A frozen clock turns
|
|
1015
|
+
waiting into waiting forever — `await new Promise(r => setTimeout(r, 10))` never resolves unless
|
|
1016
|
+
something advances it — and the runner's own advice ("pass a timeout value as the last argument") is
|
|
1017
|
+
the one repair that cannot work: the callback is not late, it is never scheduled to run. Under
|
|
1018
|
+
`globalFakeTimers` nothing in the spec says the clock is fake at all, so the timeout lands in a file
|
|
1019
|
+
that never mentions a timer. The hint reports `vi.isFakeTimers()` and `vi.getTimerCount()` — facts,
|
|
1020
|
+
not a guess — and stays silent when the clock is real or its queue is empty.
|
|
1021
|
+
|
|
1022
|
+
The shape that reaches this with no timer in sight is an HTTP spec. `setImmediate` is among the
|
|
1023
|
+
globals `vi.useFakeTimers()` replaces by default, and Express ends a request that matched no route
|
|
1024
|
+
through `finalhandler`, which schedules on `setImmediate`. So the 404 is never written and the test
|
|
1025
|
+
dies on its timeout: in such a file, "the test hung" means _the route did not match_, not "the
|
|
1026
|
+
server is slow". One thing this cannot see through — a spec whose own `afterEach` calls
|
|
1027
|
+
`vi.useRealTimers()`, because hooks run in reverse registration order and the clock is real again by
|
|
1028
|
+
the time the hint reads it. Nothing is reported then, rather than something wrong.
|
|
1029
|
+
|
|
1030
|
+
One more field of the same family differs quietly and changes only the report:
|
|
1031
|
+
`slowTestThreshold` is `5` in Jest (**seconds**) and `300` in Vitest (**milliseconds**), so a
|
|
1032
|
+
migrated suite starts marking most of its files slow. That is a unit change, not a regression.
|
|
1033
|
+
|
|
947
1034
|
Fake timers:
|
|
948
1035
|
|
|
949
1036
|
```ts
|
|
@@ -1082,13 +1169,13 @@ first and the spec's hook operates on an already-restored environment.
|
|
|
1082
1169
|
Under Jest these were hard to tell apart; under Vitest with a real bundler they are four separate
|
|
1083
1170
|
mechanisms, and a test that waits on the wrong one fails with a message that names none of them.
|
|
1084
1171
|
|
|
1085
|
-
| What is pending
|
|
1086
|
-
|
|
|
1087
|
-
| change detection
|
|
1088
|
-
| effects + `afterNextRender` + CD
|
|
1089
|
-
| timers, debounces, polling
|
|
1090
|
-
| a dynamic `import()`, native `async` in a dep
|
|
1091
|
-
| an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`)
|
|
1172
|
+
| What is pending | What drives it | What does **not** |
|
|
1173
|
+
| ------------------------------------------------- | -------------------------------------------------- | ----------------------------------------- |
|
|
1174
|
+
| change detection | `fixture.detectChanges()` | anything `await`ed |
|
|
1175
|
+
| effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
|
|
1176
|
+
| timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
|
|
1177
|
+
| a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
|
|
1178
|
+
| an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
|
|
1092
1179
|
|
|
1093
1180
|
```ts
|
|
1094
1181
|
import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
|
|
@@ -1249,7 +1336,7 @@ the values, and under `isolate: false` that is one set of spies shared by every
|
|
|
1249
1336
|
it.
|
|
1250
1337
|
|
|
1251
1338
|
**`observablePropsToSpyOn` works on a token too**, and matters more there than on a class. A class
|
|
1252
|
-
tells the factory which members are methods; a type does not, so
|
|
1339
|
+
tells the factory which members are methods; a type does not, so _every_ unnamed key of a
|
|
1253
1340
|
token-driven double is a function spy — including an `Observable` property, which the code under
|
|
1254
1341
|
test then subscribes to as if it were a function, failing far from the double:
|
|
1255
1342
|
|
|
@@ -1414,6 +1501,14 @@ The failure that is real: `overrideProvider` only reaches a component the TestBe
|
|
|
1414
1501
|
about, so a standalone component instantiated through a parent's template needs to be in `imports`
|
|
1415
1502
|
first; `overrideComponentProvider` queues it.
|
|
1416
1503
|
|
|
1504
|
+
**`overrideComponentProvider` verifies itself**, always — not behind a diagnostics flag. On the next
|
|
1505
|
+
`TestBed.createComponent` it asks the component's **own** injector for the token and throws when the
|
|
1506
|
+
answer is not the spy it created, naming the component, the token and what was resolved instead. It
|
|
1507
|
+
checks the **first** fixture only and stays silent when the component was not rendered (behind an
|
|
1508
|
+
`@if`, a different host), so a throwaway fixture created first means "not checked", not "wrong". A
|
|
1509
|
+
later `TestBed.overrideProvider(Token, …)` still wins; the check reports that but cannot prevent it.
|
|
1510
|
+
`overrideAutoSpy` carries no verification of its own.
|
|
1511
|
+
|
|
1417
1512
|
Do **not** reach for `TestBed.overrideComponent` here — see the next subsection for why it is worse
|
|
1418
1513
|
than the problem it solves.
|
|
1419
1514
|
|
|
@@ -1444,6 +1539,63 @@ assertNgModuleScopes(DirectivesModule, PipesModule); // throws, naming the modul
|
|
|
1444
1539
|
Then declare what the spec needs in the TestBed module directly. Pass only modules you expect to
|
|
1445
1540
|
bring declarations — a providers-only module is legitimately empty.
|
|
1446
1541
|
|
|
1542
|
+
### Four silent failures, as one setup line
|
|
1543
|
+
|
|
1544
|
+
```ts
|
|
1545
|
+
// vitest.setup.ts — AFTER getTestBed().initTestEnvironment(…), because Vitest runs
|
|
1546
|
+
// afterEach hooks in reverse registration order and this one must run before the teardown.
|
|
1547
|
+
import { enableAngularDiagnostics } from 'vitest-auto-spy/angular';
|
|
1548
|
+
|
|
1549
|
+
enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders, pendingRequests }
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
| Member | Fails when |
|
|
1553
|
+
| ------------------ | -------------------------------------------------------------------------------- |
|
|
1554
|
+
| `ngModuleScopes` | a testing module imports an NgModule that contributes nothing at runtime |
|
|
1555
|
+
| `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
|
|
1556
|
+
| `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
|
|
1557
|
+
| `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one |
|
|
1558
|
+
|
|
1559
|
+
Every member defaults to `true`, takes `false` to opt out, and a second call **replaces** the
|
|
1560
|
+
selection (safe from anywhere, including inside a test). `disableAngularDiagnostics()` turns the
|
|
1561
|
+
group off and leaves the TestBed timing instrumentation alone. `assertNoPendingRequests()` is the
|
|
1562
|
+
HTTP check on its own, for use mid-test — reading takes the requests, so the group will not
|
|
1563
|
+
re-report what you inspected.
|
|
1564
|
+
|
|
1565
|
+
Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
|
|
1566
|
+
`provideHttpClientTesting()` / `HttpClientTestingModule`, and a project using neither is inert.
|
|
1567
|
+
`ngModuleScopes` only fires on a module that contributes **nothing at all** — a providers-only module
|
|
1568
|
+
is legitimately scope-empty, so a stripped scope that still has providers passes silently; hand-call
|
|
1569
|
+
`assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
|
|
1570
|
+
not fire when `declarations` is non-empty.
|
|
1571
|
+
|
|
1572
|
+
### Which collaborators the code asked for — `trackInjections`
|
|
1573
|
+
|
|
1574
|
+
Do **not** reach for `vi.mock('@app/services')` to answer "was this collaborator used". Register the
|
|
1575
|
+
collaborators as provider factories and read back which ones DI constructed; a factory runs exactly
|
|
1576
|
+
when something injects its token, and DI is a seam the bundler cannot remove.
|
|
1577
|
+
|
|
1578
|
+
```ts
|
|
1579
|
+
import { trackInjections } from 'vitest-auto-spy/angular';
|
|
1580
|
+
|
|
1581
|
+
// the same function on /nestjs
|
|
1582
|
+
|
|
1583
|
+
const collaborators = trackInjections([FeatureFlagService, ANALYTICS_TOKEN]);
|
|
1584
|
+
|
|
1585
|
+
TestBed.configureTestingModule({ providers: [CheckoutFacade, ...collaborators.providers] });
|
|
1586
|
+
collaborators.get(FeatureFlagService).isOn.mockReturnValue(true);
|
|
1587
|
+
|
|
1588
|
+
TestBed.inject(CheckoutFacade).start();
|
|
1589
|
+
|
|
1590
|
+
expect(collaborators.names()).toEqual(['FeatureFlagService']); // analytics was never asked for
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
`providers`, `injectedTokens()` (in factory-run order), `names()`, `wasInjected(token)`,
|
|
1594
|
+
`get<D>(token)` → `Spy<D>`, `reset()` (the record only — the doubles survive). A class token gets a
|
|
1595
|
+
class spy, anything else a `createAutoMock()`; pass `{ double: () => … }` when a collaborator has to
|
|
1596
|
+
be a real object. Doubles are built eagerly, so stub before the entry point runs; a factory runs once
|
|
1597
|
+
per injector, so a token appears once per injector, not once per injection site.
|
|
1598
|
+
|
|
1447
1599
|
### Never mock `@angular/core` to control an `effect()`
|
|
1448
1600
|
|
|
1449
1601
|
Under the Angular unit-test builder the specs are bundled and `@angular/core` sits in a shared
|
|
@@ -1608,7 +1760,7 @@ spec — after which `Spy<Mock>` and `Spy<Interface>` disagree and somebody cast
|
|
|
1608
1760
|
`provideAutoSpy`: the latter reads a class prototype, which a token does not have.
|
|
1609
1761
|
|
|
1610
1762
|
**The second argument is not optional as often as it looks.** A spy answers `undefined` until it is
|
|
1611
|
-
told otherwise, and that is fatal the moment the code under test
|
|
1763
|
+
told otherwise, and that is fatal the moment the code under test _chains_ off it — a constructor
|
|
1612
1764
|
doing `inject(LOGGER).channel('auth').debug('…')` dies on the `.debug` of `undefined` before the
|
|
1613
1765
|
spec's first line runs, because nothing in production wrote `?.` there. Seed the link:
|
|
1614
1766
|
|
|
@@ -1719,40 +1871,73 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
|
1719
1871
|
export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
1720
1872
|
```
|
|
1721
1873
|
|
|
1722
|
-
| Rule | Level | Fix
|
|
1723
|
-
| ------------------------------ | ------- |
|
|
1724
|
-
| `no-expect-in-subscribe` | `error` | suggest
|
|
1725
|
-
| `no-object-define-property` | `error` | suggest
|
|
1726
|
-
| `prefer-provide-auto-spy` | `warn` | —
|
|
1727
|
-
| `prefer-create-spy-from-class` | `warn` | —
|
|
1728
|
-
| `prefer-inject-spy` | `warn` | suggest
|
|
1729
|
-
| `no-shared-module-level-mock` | `error` | —
|
|
1730
|
-
| `no-mocked-for-spy` | `warn` | `--fix`
|
|
1731
|
-
| `prefer-as-spy` | `warn` | `--fix`
|
|
1732
|
-
| `no-done-callback` | `error` | —
|
|
1733
|
-
| `no-floating-assertion` | `error` | —
|
|
1734
|
-
| `no-overridden-provider` | `error` |
|
|
1735
|
-
| `no-inject-before-override` | `warn` | —
|
|
1736
|
-
|
|
1737
|
-
|
|
1874
|
+
| Rule | Level | Fix | Flags |
|
|
1875
|
+
| ------------------------------ | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1876
|
+
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
1877
|
+
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
1878
|
+
| `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
|
|
1879
|
+
| `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
|
|
1880
|
+
| `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
|
|
1881
|
+
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
1882
|
+
| `no-mocked-for-spy` | `warn` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
|
|
1883
|
+
| `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
|
|
1884
|
+
| `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion |
|
|
1885
|
+
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
1886
|
+
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
|
|
1887
|
+
| `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
1888
|
+
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
1889
|
+
|
|
1890
|
+
Thirteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
|
|
1738
1891
|
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
1739
1892
|
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
1740
1893
|
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
|
1741
|
-
helper's parameter,
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
`
|
|
1754
|
-
|
|
1755
|
-
|
|
1894
|
+
helper's parameter, `as unknown as Mocked<T>`. It declines where it cannot prove the rename (a
|
|
1895
|
+
`Mocked` the file declares itself, a `Spy` that is already something else, `Mocked<{ a: Mock }>`
|
|
1896
|
+
rather than a named type) and reports without a fix.
|
|
1897
|
+
|
|
1898
|
+
**The autofix is narrower than that licence.** A declaration is decidable; what the name is
|
|
1899
|
+
_assigned_ two lines below is not — `--fix` once renamed a declaration to `Spy<T>` and left an object
|
|
1900
|
+
literal beneath it that the new type rejects, so `eslint --fix` reported clean and `tsc` failed
|
|
1901
|
+
afterwards. The plain fix now survives only where the value came out of `createSpyFromClass`,
|
|
1902
|
+
`createAutoMock`, `createMock`, `mockDeep`, `injectSpy`, `asSpy` and friends, or where the annotation
|
|
1903
|
+
belongs to no variable (a parameter, a return type, an `as` expression). Everywhere else the same
|
|
1904
|
+
edit is a **suggestion** — accept it together with the repair at the creation site, usually
|
|
1905
|
+
`createAutoMock<T>()` in place of the literal. **After any `--fix` run over specs, run
|
|
1906
|
+
`npx tsc --noEmit`**: a lint pass reporting clean is not evidence that the types still hold.
|
|
1907
|
+
|
|
1908
|
+
`no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
|
|
1909
|
+
**last** provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
|
|
1910
|
+
auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out
|
|
1911
|
+
(found on eight tokens of one file). It reads both spellings in either order and compares tokens as
|
|
1912
|
+
source text. A **verbatim duplicate** was already being ignored by Angular, so it carries a
|
|
1913
|
+
suggestion to delete the dead copy (a suggestion, never `--fix`: unattended deletions inside a
|
|
1914
|
+
`providers` array are not something to find in a diff). When
|
|
1915
|
+
the survivor is the **barer** of the two — `provideAutoSpy(X, { gettersToSpyOn: […] })` above a bare
|
|
1916
|
+
`provideAutoSpy(X)` — there is no edit, because which one to keep is the question; move the
|
|
1917
|
+
configuration onto the survivor or delete it. Both messages name the token and the surviving
|
|
1918
|
+
provider's line. A `multi: true` registration is exempt on both sides:
|
|
1919
|
+
Angular accumulates multi providers rather than keeping the last, so two of them for one token is
|
|
1920
|
+
the feature — a spec asserting that two `BEFORE_INIT` hooks run in registration order needs both.
|
|
1921
|
+
Multi mixed with plain is still reported, because Angular refuses that pair at runtime
|
|
1922
|
+
(`Cannot mix multi providers and regular providers`). For the same reason `prefer-provide-auto-spy`
|
|
1923
|
+
says nothing about a multi provider: `provideAutoSpy` takes no registration mode, so the replacement
|
|
1924
|
+
it would ask for does not exist.
|
|
1925
|
+
|
|
1926
|
+
`no-import-time-spread` is the one that fails **while the bundle loads**, on a tree whose every test
|
|
1927
|
+
passes: `export const events = [...BaseEvents]` at module scope raises
|
|
1928
|
+
`Spread syntax requires ...iterable[Symbol.iterator] to be a function` when a shared chunk is
|
|
1929
|
+
evaluated while the binding it re-exports is still `undefined`. Safe under `tsc` and under a
|
|
1930
|
+
browser's ESM loader, which is why nothing else catches it. A function body and an instance field are
|
|
1931
|
+
not reported (they run later); a `static` field is.
|
|
1932
|
+
|
|
1933
|
+
`prefer-as-spy` is the one a migration meets in bulk: a `jest-auto-spies` suite writes
|
|
1934
|
+
`TestBed.inject(X) as Spy<X>` once per injected double, and that cast fails here with `TS2352`.
|
|
1935
|
+
`asSpy` is a typed identity function, so `--fix` keeps the assertion, carries the type arguments
|
|
1936
|
+
across (inference answers `Spy<Service<any>>` for a generic class) and repairs the imports. A cast
|
|
1937
|
+
that hops through `unknown` is left alone — the hop says the value is not a `T` — except after
|
|
1938
|
+
`TestBed.inject(X)`, where the container returns `X` by construction and the hop was only silencing
|
|
1939
|
+
`TS2352`. Neither rule is for the object under test: a service the spec exercises is not a double,
|
|
1940
|
+
and typing it as the class is the repair there.
|
|
1756
1941
|
|
|
1757
1942
|
`no-expect-in-subscribe` reports one shape and **three different edits**, and says which: the
|
|
1758
1943
|
subscription is the last thing the test does (invert it into `await firstValueFrom`); something
|
|
@@ -1762,13 +1947,15 @@ the trigger, `await p` — because inverting deadlocks); or the assertion is in
|
|
|
1762
1947
|
callback reaches through a helper it calls, which used to make `subscribe((d) => assertShape(d))`
|
|
1763
1948
|
invisible. `prefer-provide-auto-spy` reads `useFactory` as well as `useValue`, through the function
|
|
1764
1949
|
in the first case and not in the second — a factory's body is what DI ends up holding, while a
|
|
1765
|
-
function inside a `useValue` is a lazily-built double, i.e. the fix.
|
|
1766
|
-
finds a spy is decided by a `provideAutoSpy(X)` usually
|
|
1767
|
-
leaves the property writable and configurable, and
|
|
1768
|
-
|
|
1950
|
+
function inside a `useValue` is a lazily-built double, i.e. the fix. Three rules change behaviour
|
|
1951
|
+
when applied — whether `injectSpy(X)` finds a spy is decided by a `provideAutoSpy(X)` usually
|
|
1952
|
+
written in another file, `mockValueProp` leaves the property writable and configurable, and
|
|
1953
|
+
`no-expect-in-subscribe` rewrites a whole test — so `prefer-inject-spy`,
|
|
1954
|
+
`no-object-define-property` and `no-expect-in-subscribe` only ever suggest:
|
|
1769
1955
|
|
|
1770
1956
|
```ts
|
|
1771
|
-
it('maps the products', () =>
|
|
1957
|
+
it('maps the products', () =>
|
|
1958
|
+
// ❌ flagged, and a suggestion is offered
|
|
1772
1959
|
new Promise<void>((done) => {
|
|
1773
1960
|
service.getProducts(id).subscribe((products) => {
|
|
1774
1961
|
expect(products).toEqual(expected);
|
|
@@ -1776,7 +1963,8 @@ it('maps the products', () => // ❌ flagged, and a sugge
|
|
|
1776
1963
|
});
|
|
1777
1964
|
}));
|
|
1778
1965
|
|
|
1779
|
-
it('maps the products', async () => {
|
|
1966
|
+
it('maps the products', async () => {
|
|
1967
|
+
// ✅ what accepting it produces
|
|
1780
1968
|
const products = await firstValueFrom(service.getProducts(id));
|
|
1781
1969
|
|
|
1782
1970
|
expect(products).toEqual(expected);
|
|
@@ -1791,9 +1979,9 @@ rather than one message per `expect`, which used to double the apparent size of
|
|
|
1791
1979
|
`prefer-inject-spy` reads both spellings of the same mistake, which is the point of the second one:
|
|
1792
1980
|
|
|
1793
1981
|
```ts
|
|
1794
|
-
vi.spyOn(TestBed.inject(DomainEventsService), 'announce');
|
|
1982
|
+
vi.spyOn(TestBed.inject(DomainEventsService), 'announce'); // flagged, always was
|
|
1795
1983
|
const domainEvents = TestBed.inject(DomainEventsService);
|
|
1796
|
-
const announceSpy = vi.spyOn(domainEvents, 'announce');
|
|
1984
|
+
const announceSpy = vi.spyOn(domainEvents, 'announce'); // flagged now
|
|
1797
1985
|
```
|
|
1798
1986
|
|
|
1799
1987
|
The variable is resolved through the scope manager, so it has to be a `const`/`let` initialised
|
|
@@ -1807,13 +1995,6 @@ written above it inside a `createComponent` helper. Configure the double after t
|
|
|
1807
1995
|
(`injectSpy(X)` in the test), or keep the access lazy: `const api = () => injectSpy(Api)`. The check
|
|
1808
1996
|
is order-free by design, since a helper declared above the hook still runs after it.
|
|
1809
1997
|
|
|
1810
|
-
`no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
|
|
1811
|
-
last provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
|
|
1812
|
-
auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out.
|
|
1813
|
-
Found on eight tokens of one file. It reads both spellings in either order, compares tokens as
|
|
1814
|
-
source text, and offers no fix: deleting either line is a valid repair and they mean opposite
|
|
1815
|
-
things.
|
|
1816
|
-
|
|
1817
1998
|
The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
|
|
1818
1999
|
packages, which a subpath export can never be.
|
|
1819
2000
|
|
|
@@ -1821,135 +2002,143 @@ packages, which a subpath export can never be.
|
|
|
1821
2002
|
|
|
1822
2003
|
## 17. Error → fix
|
|
1823
2004
|
|
|
1824
|
-
| Message contains | Cause
|
|
1825
|
-
| ------------------------------------------------------------------------------------ |
|
|
1826
|
-
| `No mock adapter registered` | no runtime entry was imported, or the wrong one
|
|
1827
|
-
| `Observable spies require rxjs` | the rxjs layer was never loaded
|
|
1828
|
-
| `requested method(s) not found on the class prototype` | typo, or an instance-field callable
|
|
1829
|
-
| `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments
|
|
1830
|
-
| `advanceTimers() requires fake timers` | no fake timers installed
|
|
1831
|
-
| `
|
|
1832
|
-
|
|
|
1833
|
-
| `
|
|
1834
|
-
| `
|
|
1835
|
-
|
|
|
1836
|
-
| `
|
|
1837
|
-
|
|
|
1838
|
-
| `
|
|
1839
|
-
| `
|
|
1840
|
-
| `
|
|
1841
|
-
|
|
|
1842
|
-
|
|
|
1843
|
-
|
|
|
1844
|
-
| `
|
|
1845
|
-
|
|
|
1846
|
-
|
|
|
1847
|
-
| `
|
|
1848
|
-
| `
|
|
1849
|
-
| `
|
|
1850
|
-
| `
|
|
1851
|
-
|
|
|
1852
|
-
| `
|
|
1853
|
-
| `
|
|
1854
|
-
|
|
|
1855
|
-
| `
|
|
1856
|
-
|
|
|
1857
|
-
|
|
|
1858
|
-
| `
|
|
1859
|
-
| `
|
|
1860
|
-
| `
|
|
1861
|
-
|
|
|
1862
|
-
|
|
|
1863
|
-
| `
|
|
1864
|
-
|
|
|
1865
|
-
| a `
|
|
1866
|
-
| a
|
|
1867
|
-
| `
|
|
1868
|
-
|
|
|
1869
|
-
| `
|
|
1870
|
-
|
|
|
1871
|
-
|
|
|
1872
|
-
| `
|
|
1873
|
-
| a
|
|
1874
|
-
|
|
|
1875
|
-
|
|
|
1876
|
-
| `
|
|
1877
|
-
|
|
|
1878
|
-
| `
|
|
1879
|
-
| `
|
|
1880
|
-
| `
|
|
1881
|
-
| `
|
|
1882
|
-
|
|
|
1883
|
-
| `
|
|
1884
|
-
|
|
|
1885
|
-
|
|
|
1886
|
-
|
|
|
1887
|
-
|
|
|
1888
|
-
| a
|
|
2005
|
+
| Message contains | Cause | Fix |
|
|
2006
|
+
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
2007
|
+
| `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
|
|
2008
|
+
| `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
|
|
2009
|
+
| `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
|
|
2010
|
+
| `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
|
|
2011
|
+
| `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
|
|
2012
|
+
| `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
|
|
2013
|
+
| `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
|
|
2014
|
+
| `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
|
|
2015
|
+
| `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
|
|
2016
|
+
| `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
|
|
2017
|
+
| `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
|
|
2018
|
+
| `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
|
|
2019
|
+
| `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
|
|
2020
|
+
| `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
|
|
2021
|
+
| `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
|
|
2022
|
+
| setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
|
|
2023
|
+
| `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
|
|
2024
|
+
| `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
|
|
2025
|
+
| duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
|
|
2026
|
+
| `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) |
|
|
2027
|
+
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
2028
|
+
| `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 |
|
|
2029
|
+
| `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) |
|
|
2030
|
+
| `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) |
|
|
2031
|
+
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
2032
|
+
| 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) |
|
|
2033
|
+
| an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
|
|
2034
|
+
| `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
|
|
2035
|
+
| `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
|
|
2036
|
+
| `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)` |
|
|
2037
|
+
| `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 |
|
|
2038
|
+
| `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
|
|
2039
|
+
| `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
|
|
2040
|
+
| `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
|
|
2041
|
+
| `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
|
|
2042
|
+
| `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
|
|
2043
|
+
| `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
|
|
2044
|
+
| `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
|
|
2045
|
+
| `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
|
|
2046
|
+
| a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
|
|
2047
|
+
| a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
|
|
2048
|
+
| `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
|
|
2049
|
+
| `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
|
|
2050
|
+
| `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
|
|
2051
|
+
| a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
|
|
2052
|
+
| 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` |
|
|
2053
|
+
| `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()` |
|
|
2054
|
+
| a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
|
|
2055
|
+
| a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
|
|
2056
|
+
| a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
|
|
2057
|
+
| `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 |
|
|
2058
|
+
| `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) |
|
|
2059
|
+
| `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
|
|
2060
|
+
| `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) |
|
|
2061
|
+
| `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` |
|
|
2062
|
+
| `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) |
|
|
2063
|
+
| `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` |
|
|
2064
|
+
| `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 |
|
|
2065
|
+
| `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` |
|
|
2066
|
+
| 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 |
|
|
2067
|
+
| `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` |
|
|
2068
|
+
| 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 |
|
|
2069
|
+
| 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) |
|
|
2070
|
+
| a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
|
|
2071
|
+
|
|
2072
|
+
**Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
|
|
2073
|
+
nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
|
|
2074
|
+
`gettersToSpyOn: ['X']`; a `mock*Prop` on a `createAutoMock` double changing nothing;
|
|
2075
|
+
`gettersToSpyOn` rejecting a `Signal` key; `mockReadonlyProp` rejecting a real `signal()`;
|
|
2076
|
+
`mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
|
|
2077
|
+
never patched anything.
|
|
1889
2078
|
|
|
1890
2079
|
---
|
|
1891
2080
|
|
|
1892
2081
|
## 18. Do not write this
|
|
1893
2082
|
|
|
1894
|
-
| ❌
|
|
1895
|
-
|
|
|
1896
|
-
| `import … from 'jest-auto-spies'`
|
|
1897
|
-
| `vitest-auto-spy` inside a `bun test` file
|
|
1898
|
-
| `let s: MyService = createSpyFromClass(MyService)`
|
|
1899
|
-
| `createSpyFromClass(X) as unknown as X`
|
|
1900
|
-
| `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }`
|
|
1901
|
-
| `vi.spyOn(TestBed.inject(X), 'method')`
|
|
1902
|
-
| `Object.defineProperty(service, 'ready', { value: true })`
|
|
1903
|
-
| `source$.subscribe(v => expect(v).toBe(1))`
|
|
1904
|
-
| `await lastValueFrom(done$, { defaultValue: undefined })`
|
|
1905
|
-
| `{ timeout: 0 }` on every helper because timers are faked
|
|
1906
|
-
| `{ provide: AbstractToken, useValue: createAutoMock<T>() }`
|
|
1907
|
-
| `mockDeep<T>()` for a chain that goes through a **call**
|
|
1908
|
-
| `await expect(expectEmission(x$)).rejects.toBe(originalError)`
|
|
1909
|
-
| `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper
|
|
1910
|
-
| hold the promise, `vi.runAllTimers()`, then await
|
|
1911
|
-
| `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach`
|
|
1912
|
-
| a local `injectSpy` wrapper with `as never` + `as Spy<T>`
|
|
1913
|
-
| a hand-written double for a token with `Observable` members
|
|
1914
|
-
| `mockDeep<T>() as unknown as T` to satisfy an API typed against `T`
|
|
1915
|
-
| `from([double])` to stop `of(double)` swallowing the double
|
|
1916
|
-
| `spy.instanceField.mockReturnValue(…)` on a member Angular moved
|
|
1917
|
-
| `expect(component.total).toBeTruthy()` (a signal)
|
|
1918
|
-
| `fixture.detectChanges()` then assert signal state
|
|
1919
|
-
| `onlyMethodsToSpyOn: [...]` "to add a method"
|
|
1920
|
-
| a `vi.fn()` the code calls with `new`
|
|
1921
|
-
| an exported `const` fixture holding `vi.fn()`s
|
|
1922
|
-
| `let s: Mocked<MyService>` (Vitest's own type)
|
|
1923
|
-
| `it('x', (done) => …)` / `beforeEach((done) => …)`
|
|
1924
|
-
| `{ ...modelInstance, flag: true }` (drops every getter)
|
|
1925
|
-
| `if ('params' in link) … else throw` in every spec
|
|
1926
|
-
| five `asInstance(...)` in one call
|
|
1927
|
-
| `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))`
|
|
1928
|
-
| `Object.defineProperty(document, 'cookie', { value })`
|
|
1929
|
-
| `vi.spyOn(globalThis, 'Date')`
|
|
1930
|
-
| ten `await Promise.resolve()` for a dynamic `import()`
|
|
1931
|
-
| `await fixture.whenRenderingDone()`
|
|
1932
|
-
| an exported `const` provider with `vi.fn()` inside
|
|
1933
|
-
| `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))`
|
|
1934
|
-
| `TestBed.overrideComponent` to swap a provider
|
|
1935
|
-
| `{ target, isIntersecting } as unknown as IntersectionObserverEntry`
|
|
1936
|
-
| an assertion containing a date, with no clock set
|
|
1937
|
-
| `configureTestingModule` inside every `it()`
|
|
1938
|
-
| `vi.mock('@angular/core')` to neutralise `effect()`
|
|
1939
|
-
| a second `vi.spyOn(console, 'error')`
|
|
1940
|
-
| `mockReadonlyProp(c, 'items', vi.fn(() => []))`
|
|
1941
|
-
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)`
|
|
1942
|
-
|
|
2083
|
+
| ❌ | ✅ |
|
|
2084
|
+
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
2085
|
+
| `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
|
|
2086
|
+
| `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
|
|
2087
|
+
| `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
|
|
2088
|
+
| `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
|
|
2089
|
+
| `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
|
|
2090
|
+
| `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
|
|
2091
|
+
| `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
|
|
2092
|
+
| `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
|
|
2093
|
+
| `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
|
|
2094
|
+
| `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
|
|
2095
|
+
| `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
|
|
2096
|
+
| `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
|
|
2097
|
+
| `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
|
|
2098
|
+
| `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
|
|
2099
|
+
| hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
|
|
2100
|
+
| `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
|
|
2101
|
+
| a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
|
|
2102
|
+
| a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
|
|
2103
|
+
| `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
|
|
2104
|
+
| `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
|
|
2105
|
+
| `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
|
|
2106
|
+
| `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
|
|
2107
|
+
| `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
|
|
2108
|
+
| `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
|
|
2109
|
+
| a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
|
|
2110
|
+
| an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
|
|
2111
|
+
| `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
|
|
2112
|
+
| `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
|
|
2113
|
+
| `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
|
|
2114
|
+
| `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
|
|
2115
|
+
| five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
|
|
2116
|
+
| `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
|
|
2117
|
+
| `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
|
|
2118
|
+
| `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
|
|
2119
|
+
| ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
|
|
2120
|
+
| `await fixture.whenRenderingDone()` | `await stable(fixture)` |
|
|
2121
|
+
| an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
|
|
2122
|
+
| `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
|
|
2123
|
+
| `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
|
|
2124
|
+
| `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
|
|
2125
|
+
| an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
|
|
2126
|
+
| `configureTestingModule` inside every `it()` | one per `describe` |
|
|
2127
|
+
| `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
|
|
2128
|
+
| a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console` |
|
|
2129
|
+
| `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
|
|
2130
|
+
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
|
|
1943
2131
|
|
|
1944
2132
|
**The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
|
|
1945
|
-
when the double is
|
|
1946
|
-
|
|
2133
|
+
when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
|
|
2134
|
+
_configured_. They are indistinguishable until the test reassigns `x` — and the commonest reason to
|
|
1947
2135
|
do that is a fresh `Subject` after the previous one has been `error()`ed or completed, which is
|
|
1948
2136
|
exactly the case a suite is testing when it reassigns. The double then keeps handing out the dead
|
|
1949
2137
|
one: in one spec the service received a completed subject and silently skipped the modal it was
|
|
1950
2138
|
meant to show, with the test still green. Carry `vi.fn(() => x)` over as
|
|
1951
2139
|
`mockImplementation(() => x)`, and keep `mockReturnValue` for a literal. Worth saying out loud to
|
|
1952
2140
|
anyone writing a codemod, because the rename looks like the safest edit in the file.
|
|
2141
|
+
|
|
1953
2142
|
---
|
|
1954
2143
|
|
|
1955
2144
|
## 19. Before you report success
|
|
@@ -1973,6 +2162,27 @@ worth one run after any large edit to a test suite — especially after a codemo
|
|
|
1973
2162
|
the eaten glob below came from. Full reference:
|
|
1974
2163
|
<https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
1975
2164
|
|
|
2165
|
+
### Migrating a suite off `jest-auto-spies` — run the codemod, then verify it
|
|
2166
|
+
|
|
2167
|
+
```bash
|
|
2168
|
+
npx vitest-auto-spy codemod # dry run: prints the diff, writes nothing
|
|
2169
|
+
npx vitest-auto-spy codemod --write # apply
|
|
2170
|
+
npx vitest-auto-spy codemod --verify # exits 1 on anything the transforms should have removed
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
Do not hand-edit a suite of migrated imports; the codemod knows which entry point exports each name
|
|
2174
|
+
(it reads the installed package's export map) and it transposes `jest.Mock<R, [A]>` into the single
|
|
2175
|
+
call signature Vitest takes — a plain rename compiles into the **reverse** meaning and nothing fails
|
|
2176
|
+
until a call site disagrees. It leaves a `jest.*` member with no `vi` twin (`requireMock`,
|
|
2177
|
+
`replaceProperty`, `createMockFromModule`, `jest.setTimeout`, `requireActual`) exactly as it was and
|
|
2178
|
+
reports what to do instead, rather than guessing.
|
|
2179
|
+
|
|
2180
|
+
`--verify` matches the **result** against the patterns the codemod removes, so it also catches what
|
|
2181
|
+
the transforms declined to enter (a template literal, an unbalanced bracket) and a file somebody
|
|
2182
|
+
migrated by hand. Run it after `--write`, and again after any manual clean-up. `--only` / `--skip`
|
|
2183
|
+
select transforms by id, `--list` prints them. Full reference:
|
|
2184
|
+
<https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
|
|
2185
|
+
|
|
1976
2186
|
### If you are writing a codemod over specs
|
|
1977
2187
|
|
|
1978
2188
|
Two traps, both found the hard way on rxjs-heavy code.
|