vitest-auto-spy 5.34.0 → 5.35.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 +274 -2838
- package/CHANGELOG.md +153 -1
- package/README.md +66 -31
- package/agent-docs/angular.md +1316 -0
- package/agent-docs/errors.md +194 -0
- package/agent-docs/eslint.md +273 -0
- package/agent-docs/migration-jasmine.md +159 -0
- package/agent-docs/setup.md +784 -0
- package/dist/angular.d.ts +2 -2
- package/dist/bun-angular.js +1 -1
- package/dist/bun.js +1 -1
- package/dist/{chunk-TVVDNN3U.js → chunk-4HLOO2VK.js} +9 -7
- package/dist/cli.js +1070 -279
- package/dist/eslint-plugin.cjs +35 -14
- package/dist/index.js +9 -7
- package/dist/node.cjs +9 -7
- package/dist/node.js +9 -7
- package/dist/perf-reporter.d.ts +87 -10
- package/dist/perf-reporter.js +113 -21
- package/dist/react.js +9 -7
- package/dist/rstest.js +1 -1
- package/dist/setup.js +1 -1
- package/dist/svelte.js +9 -7
- package/dist/vue.js +9 -7
- package/package.json +17 -21
- package/skills/vitest-auto-spy/SKILL.md +15 -5
package/AGENTS.md
CHANGED
|
@@ -21,19 +21,39 @@ package's skill in `.claude/skills/vitest-auto-spy/` is reported `stale` (and fa
|
|
|
21
21
|
it and re-run `init`. Full table:
|
|
22
22
|
<https://asdalexey.github.io/vitest-auto-spy/agents>.
|
|
23
23
|
|
|
24
|
-
| Resource
|
|
25
|
-
|
|
|
26
|
-
| Spec patterns at scale
|
|
27
|
-
| Task recipes
|
|
28
|
-
| Docs index for LLMs
|
|
29
|
-
| Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt>
|
|
30
|
-
| Human docs
|
|
31
|
-
| Source
|
|
32
|
-
| Types
|
|
24
|
+
| Resource | Where |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Spec patterns at scale | <https://asdalexey.github.io/vitest-auto-spy/recipes> |
|
|
27
|
+
| Task recipes | <https://asdalexey.github.io/vitest-auto-spy/guides/mocking-classes> · `/guides/mocking-local-storage` · `/guides/mocking-prisma` · `/guides/storybook-angular` |
|
|
28
|
+
| Docs index for LLMs | <https://asdalexey.github.io/vitest-auto-spy/llms.txt> |
|
|
29
|
+
| Entire docs as one file | <https://asdalexey.github.io/vitest-auto-spy/llms-full.txt> |
|
|
30
|
+
| Human docs | <https://asdalexey.github.io/vitest-auto-spy/> |
|
|
31
|
+
| Source | <https://github.com/ASDAlexey/vitest-auto-spy> |
|
|
32
|
+
| Types | `node_modules/vitest-auto-spy/dist/index.d.ts` (and one per subpath) |
|
|
33
33
|
|
|
34
34
|
**Read `dist/*.d.ts` before inventing a call.** Every export is typed and documented there, and the
|
|
35
35
|
type is the authority when this file and the code disagree.
|
|
36
36
|
|
|
37
|
+
## How to read this file
|
|
38
|
+
|
|
39
|
+
No task needs all of it. This file is the core; five long sections live next to it in `agent-docs/`,
|
|
40
|
+
each with a stub here that says when to read it. Read by section, not whole:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
D=node_modules/vitest-auto-spy
|
|
44
|
+
grep -n '^## ' "$D/AGENTS.md" # the map: every section with its line number
|
|
45
|
+
sed -n '<start>,<end>p' "$D/AGENTS.md" # one section, from its heading to the next one
|
|
46
|
+
grep -n '^### ' "$D/agent-docs/angular.md" # the map of a topic file
|
|
47
|
+
grep -n -F '<text of the error>' "$D/agent-docs/errors.md" # a failure: its row, with the fix
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Read | Sections |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| Always, before writing a spec | §1 entry point, §2 factory, §3 the 90% recipe, §18 do not write this, §19 before you report success |
|
|
53
|
+
| When the task touches it | §4 return-type helpers, §5 `createSpyFromClass` configuration, §6 `Spy<T>` vs `T`, §7 resetting, §8 observables, §9 patching properties, §10 setup file (`agent-docs/setup.md`), §11 waiting, §12 doubles for what the code builds, §15 other adapters |
|
|
54
|
+
| Only for that stack | §13 Angular (`agent-docs/angular.md`), §14 `fakeAsync`, §16 ESLint plugin (`agent-docs/eslint.md`), §20 migrating off `jasmine-auto-spies` (`agent-docs/migration-jasmine.md`) |
|
|
55
|
+
| Never front to back | §17 Error → fix (`agent-docs/errors.md`): grep the message instead |
|
|
56
|
+
|
|
37
57
|
---
|
|
38
58
|
|
|
39
59
|
## 1. Pick the entry point first
|
|
@@ -41,37 +61,37 @@ type is the authority when this file and the code disagree.
|
|
|
41
61
|
Each entry registers its mock adapter **on import**. Importing the wrong one leaves the wrong
|
|
42
62
|
adapter installed and spies fail at runtime.
|
|
43
63
|
|
|
44
|
-
| Runner / framework
|
|
45
|
-
|
|
|
46
|
-
| Vitest (default)
|
|
47
|
-
| `bun test`
|
|
64
|
+
| Runner / framework | Import from |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| Vitest (default) | `vitest-auto-spy` |
|
|
67
|
+
| `bun test` | `vitest-auto-spy/bun` |
|
|
48
68
|
| `bun test` + Angular | `vitest-auto-spy/bun-angular` |
|
|
49
|
-
| `node --test`
|
|
50
|
-
| `rstest run`
|
|
51
|
-
| Angular + Vitest
|
|
52
|
-
| NestJS
|
|
53
|
-
| React
|
|
54
|
-
| Vue / Pinia
|
|
55
|
-
| Svelte
|
|
69
|
+
| `node --test` | `vitest-auto-spy/node` |
|
|
70
|
+
| `rstest run` | `vitest-auto-spy/rstest` |
|
|
71
|
+
| Angular + Vitest | `vitest-auto-spy/angular` |
|
|
72
|
+
| NestJS | `vitest-auto-spy/nestjs` |
|
|
73
|
+
| React | `vitest-auto-spy/react` |
|
|
74
|
+
| Vue / Pinia | `vitest-auto-spy/vue` |
|
|
75
|
+
| Svelte | `vitest-auto-spy/svelte` |
|
|
56
76
|
|
|
57
77
|
Add-ons, orthogonal to the runner:
|
|
58
78
|
|
|
59
|
-
| Add-on
|
|
60
|
-
|
|
|
61
|
-
| Observable spies
|
|
62
|
-
| observer-spy shim
|
|
63
|
-
| Console spies
|
|
64
|
-
| DOM stubs
|
|
65
|
-
| Run diagnostics
|
|
66
|
-
| Angular HTTP
|
|
67
|
-
| Angular router
|
|
68
|
-
| Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular` like `/angular-http` — no core re-export; **moved off `/angular` in 5.21.0** so importing spies stops evaluating it
|
|
69
|
-
| Angular doubles
|
|
70
|
-
| Angular matchers
|
|
71
|
-
| Signal forms
|
|
72
|
-
| Setup helpers
|
|
73
|
-
| Zone patch
|
|
74
|
-
| jasmine compat
|
|
79
|
+
| Add-on | Import | Needed for |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, in a setup file, a spec, or a `.d.ts` the `tsconfig` includes (§4) |
|
|
82
|
+
| observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
|
|
83
|
+
| Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `installConsoleSpies()` per test, `restoreConsole()` after |
|
|
84
|
+
| DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
|
|
85
|
+
| Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
|
|
86
|
+
| Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
|
|
87
|
+
| Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
|
|
88
|
+
| Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular` like `/angular-http` — no core re-export; **moved off `/angular` in 5.21.0** so importing spies stops evaluating it |
|
|
89
|
+
| Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio and the `Window`/`Document` platform doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box; **moved off `/angular` in 5.21.0** |
|
|
90
|
+
| Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 5.21.0** |
|
|
91
|
+
| Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
|
|
92
|
+
| Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
|
|
93
|
+
| Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
|
|
94
|
+
| jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
|
|
75
95
|
|
|
76
96
|
`vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
|
|
77
97
|
`node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
|
|
@@ -232,9 +252,9 @@ node's prototype chain, and past the component's own class that chain is the eng
|
|
|
232
252
|
patched it the strict guard refuses that call — the node cannot be removed from `document.body` at
|
|
233
253
|
all, and the leak then fails later tests in the file far from the cause. Without `onlyMethodsToSpyOn`
|
|
234
254
|
on a live `Node`, the global `window` or another engine-provided event target (`XMLHttpRequest`,
|
|
235
|
-
`AbortSignal
|
|
236
|
-
|
|
237
|
-
|
|
255
|
+
`AbortSignal`; a class of your own that extends `EventTarget` does not count), the call now reports
|
|
256
|
+
this itself, before patching anything — `warn` by default, or a throw right there under
|
|
257
|
+
`setMisconfigurationReaction('throw')`.
|
|
238
258
|
The bare-array shorthand does not silence it: `createSpyFromInstance(el, ['addEventListener'])`
|
|
239
259
|
merges as `methodsToSpyOn`, the _additive_ list, so discovery still walks the whole node — only
|
|
240
260
|
`{ onlyMethodsToSpyOn: [...] }` skips it. For a still-attached element, spy one property with
|
|
@@ -370,11 +390,11 @@ missing, and each produced a _silent_ wrong answer rather than an error — the
|
|
|
370
390
|
this library can have, because a checking test becomes a non-checking one and only the proxy's
|
|
371
391
|
source says so. Two are fixed; the third cannot be:
|
|
372
392
|
|
|
373
|
-
| Operation
|
|
374
|
-
|
|
|
375
|
-
| `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it
|
|
376
|
-
| `delete mock.optionalMethod`
|
|
377
|
-
| `Object.assign(real, mock)`
|
|
393
|
+
| Operation | Before 3.5.0 | Now |
|
|
394
|
+
| --- | --- | --- |
|
|
395
|
+
| `mockValueProp` & the other three | patch landed on the target; the double ignored it | works, and `restoreMockedProps()` undoes it |
|
|
396
|
+
| `delete mock.optionalMethod` | deleted nothing; the next read remade the spy | the member is absent, until something writes to it again |
|
|
397
|
+
| `Object.assign(real, mock)` | copies only the keys already **read** | still does — see below |
|
|
378
398
|
|
|
379
399
|
`ownKeys` cannot be completed: a type has no key list at runtime, which is the whole premise of
|
|
380
400
|
these two factories. So a spec that installs a double by **copying it onto a real instance** —
|
|
@@ -391,12 +411,12 @@ probe too, at which point it stops being a double of `T` and becomes whatever wa
|
|
|
391
411
|
Four names are therefore answered with `undefined` unless the spec seeds them, alongside `then` and
|
|
392
412
|
every symbol, which always were:
|
|
393
413
|
|
|
394
|
-
| Key
|
|
395
|
-
|
|
|
396
|
-
| `schedule`
|
|
397
|
-
| `lift`
|
|
398
|
-
| `@@observable` | `isInteropObservable` in `innerFrom`
|
|
399
|
-
| `getReader`
|
|
414
|
+
| Key | Probed by | The double became |
|
|
415
|
+
| --- | --- | --- |
|
|
416
|
+
| `schedule` | `popScheduler` in `of` / `from` / `merge` / … | a scheduler |
|
|
417
|
+
| `lift` | `isObservable`, with `subscribe` | an Observable |
|
|
418
|
+
| `@@observable` | `isInteropObservable` in `innerFrom` | an interop stream |
|
|
419
|
+
| `getReader` | `isReadableStreamLike` in `innerFrom` | a ReadableStream |
|
|
400
420
|
|
|
401
421
|
The one that cost an afternoon reads like nothing at all:
|
|
402
422
|
|
|
@@ -576,11 +596,11 @@ Four conventions that carry most of the value:
|
|
|
576
596
|
Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementation`,
|
|
577
597
|
`toHaveBeenCalledWith` and the rest all work as usual. On top of that:
|
|
578
598
|
|
|
579
|
-
| Return type
|
|
580
|
-
|
|
|
581
|
-
| anything
|
|
582
|
-
| `Promise<T>`
|
|
583
|
-
| `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()`
|
|
599
|
+
| Return type | Helpers added |
|
|
600
|
+
| --- | --- |
|
|
601
|
+
| anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)`. On a `Promise` or `Observable` method the handle carries that row's helpers instead — `calledWith(id).nextWith(v)`, `.resolveWith(v)`; `.mockReturnValue` there is a `TS2339` |
|
|
602
|
+
| `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
|
|
603
|
+
| `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
|
|
584
604
|
|
|
585
605
|
`Observable` **properties** (not just methods) get the same helpers — list them in
|
|
586
606
|
`observablePropsToSpyOn`. The prop spy is a plain `Observable`: a `BehaviorSubject` member loses
|
|
@@ -603,10 +623,12 @@ augmentable interface, `AutoSpyRxjsTypes<T>`, which `/rxjs` fills in with `subje
|
|
|
603
623
|
augment it yourself only to plug in a different subject type. If
|
|
604
624
|
`const s: Subject<T> = spy.m.returnSubject()` fails to compile, the import is missing from the
|
|
605
625
|
program the specs are checked in — usually a Vitest `setupFiles` entry that no `tsconfig`
|
|
606
|
-
`include` covers
|
|
607
|
-
`
|
|
608
|
-
|
|
609
|
-
|
|
626
|
+
`include` covers, or, from `@angular/build:unit-test` 22.2.0, a plain `.ts` that only the spec
|
|
627
|
+
`tsconfig`'s `include` lists: that builder's program is the specs, the setup files and the `.d.ts`
|
|
628
|
+
files (angular-cli#34134), so put the import in one of those. That is the _only_ breaking change in
|
|
629
|
+
4.0.0; the reason for it is that `dist/types-*.d.ts` used to open with
|
|
630
|
+
`import { Observable, Subject } from 'rxjs'` and load 189 rxjs `.d.ts` files into every consumer's
|
|
631
|
+
program (303 files against 114 without it), `import type` included — TypeScript resolves a type-only import the same way.
|
|
610
632
|
|
|
611
633
|
```ts
|
|
612
634
|
// argument dispatch — other arguments return undefined
|
|
@@ -782,16 +804,16 @@ createSpyFromClass(MyService, {
|
|
|
782
804
|
});
|
|
783
805
|
```
|
|
784
806
|
|
|
785
|
-
| Key
|
|
786
|
-
|
|
|
787
|
-
| `methodsToSpyOn`
|
|
788
|
-
| `onlyMethodsToSpyOn`
|
|
789
|
-
| `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below).
|
|
790
|
-
| `autoSpyAccessors`
|
|
791
|
-
| `lazySpies`
|
|
792
|
-
| `strict`
|
|
793
|
-
| `onUnstubbedCall`
|
|
794
|
-
| `selfReturning`
|
|
807
|
+
| Key | Semantics |
|
|
808
|
+
| --- | --- |
|
|
809
|
+
| `methodsToSpyOn` | **Additive**, as in `jest-auto-spies`. Same behaviour as `instanceMethodsToSpyOn`. |
|
|
810
|
+
| `onlyMethodsToSpyOn` | **Exhaustive whitelist.** Skips discovery; anything not listed is absent. |
|
|
811
|
+
| `instanceMethodsToSpyOn` | **Additive.** The name to prefer in new code (see below). |
|
|
812
|
+
| `autoSpyAccessors` | Merged with the explicit getter/setter lists. |
|
|
813
|
+
| `lazySpies` | Behaviour-identical; only changes _when_ each spy is built. `'proxy'` also changes _what holds the name_. |
|
|
814
|
+
| `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
|
|
815
|
+
| `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
|
|
816
|
+
| `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
|
|
795
817
|
|
|
796
818
|
**`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
|
|
797
819
|
the reference suite). Method discovery walks the _prototype chain_; a callable assigned to an
|
|
@@ -1102,12 +1124,12 @@ Both are the same object at runtime. `injectSpy(X)` already returns `Spy<X>`.
|
|
|
1102
1124
|
The compiler reports this in four different ways, none of which contains both the words "spy" and
|
|
1103
1125
|
"instance", which is why the fix is hard to find from the message alone:
|
|
1104
1126
|
|
|
1105
|
-
| Message
|
|
1106
|
-
|
|
|
1107
|
-
| `TS2352: … 'accessorSpies' is missing in type 'Router'`
|
|
1108
|
-
| `TS2739` / `TS2740: Type 'Spy<X>' is missing the following properties from type 'X'` | spy → `T` | `asInstance(spy)`
|
|
1109
|
-
| `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'`
|
|
1110
|
-
| `is missing the following properties: _modalOpened, body, …` (private names)
|
|
1127
|
+
| Message | Direction | Fix |
|
|
1128
|
+
| --- | --- | --- |
|
|
1129
|
+
| `TS2352: … 'accessorSpies' is missing in type 'Router'` | `T` → spy | `asSpy(TestBed.inject(Router))` |
|
|
1130
|
+
| `TS2739` / `TS2740: Type 'Spy<X>' is missing the following properties from type 'X'` | spy → `T` | `asInstance(spy)` |
|
|
1131
|
+
| `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` | spy → `T` | `asInstance(spy)` |
|
|
1132
|
+
| `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not `Mocked<T>` |
|
|
1111
1133
|
|
|
1112
1134
|
That last row is its own trap: Vitest's `Mocked<T>` keeps `T`'s **private** members, so the error
|
|
1113
1135
|
lists private field names and reads as "the double is incomplete". It is not — the declaration is
|
|
@@ -1500,776 +1522,23 @@ The hand-written form fails in three ways that all surface in **someone else's f
|
|
|
1500
1522
|
|
|
1501
1523
|
## 10. Setup file
|
|
1502
1524
|
|
|
1503
|
-
|
|
1504
|
-
// vitest.setup.ts
|
|
1505
|
-
import 'vitest-auto-spy/rxjs';
|
|
1506
|
-
// once — enables observable spies everywhere
|
|
1507
|
-
import { setupAutoSpy } from 'vitest-auto-spy/setup';
|
|
1508
|
-
|
|
1509
|
-
setupAutoSpy(); // { duplicateCopies: 'throw', restoreProps: true, restoreMocks: false }
|
|
1510
|
-
```
|
|
1511
|
-
|
|
1512
|
-
`setupAutoSpy()` does three things: `restoreMockedProps()` in a global `afterEach`, a duplicate-install
|
|
1513
|
-
check that fails the run, and (opt-in) `vi.restoreAllMocks()`. Turn on `restoreMocks: true` when the
|
|
1514
|
-
suite runs with `isolate: false`.
|
|
1515
|
-
|
|
1516
|
-
**The restore also runs from an `onTestFinished` net**, because the `afterEach` is not guaranteed
|
|
1517
|
-
to. Vitest calls `afterEach` hooks in _reverse_ registration order, so the setup file's is the last
|
|
1518
|
-
one, and a hook the spec file registered takes the chain down with it when it throws — the patches
|
|
1519
|
-
then travel into the next test and the failure surfaces somewhere that never touched them. One spec
|
|
1520
|
-
kept `afterEach(() => vi.restoreAllMocks())`; migrating it to `gettersToSpyOn` made the restored
|
|
1521
|
-
getter return `undefined`, `ngOnDestroy` called it as a signal, the `TypeError` aborted the hook,
|
|
1522
|
-
and a template error about a null profile appeared in a different `describe`. The net puts the
|
|
1523
|
-
properties back and warns with the count and the cause. `countMockedProps()` is exported if you
|
|
1524
|
-
would rather assert it: `afterEach(() => expect(countMockedProps()).toBe(0))`.
|
|
1525
|
-
|
|
1526
|
-
**A `mock*Prop` patch written in a `describe` body or a `beforeAll` is reported**, because the sweep
|
|
1527
|
-
undoes it after the test it was applied during and nothing puts it back: the first test passes and
|
|
1528
|
-
every one after it reads the real member. `propsOutsideHooks` grades that — `'warn'` (default),
|
|
1529
|
-
`'throw'`, `'off'` — and `reportPropsOutsideHooks(reaction)` sets the same dial for a suite that
|
|
1530
|
-
wires its own hooks. Move the call into `beforeEach`.
|
|
1531
|
-
|
|
1532
|
-
**Adopting `setupAutoSpy()` in a suite that restored its patches once per file** — one
|
|
1533
|
-
`restoreMockedProps()` in an `afterAll` — changes a patch from "lives for the file" to "lives for one
|
|
1534
|
-
test". On 176 files of one such suite, 3 broke, each a `mock*Prop` in a `describe` body or
|
|
1535
|
-
`beforeAll`, and the `'warn'` report is easy to miss in a full run. Run the first time with
|
|
1536
|
-
`setupAutoSpy({ propsOutsideHooks: 'throw' })`, fix what it names, then drop the option.
|
|
1537
|
-
|
|
1538
|
-
**The one that only bites at scale:** with `isolate: false`, a `setTimeout` or
|
|
1539
|
-
`requestAnimationFrame` a component schedules and never clears keeps running after its file is done,
|
|
1540
|
-
and fires while the **next** file is mid-test. It is reported against that innocent file, as
|
|
1541
|
-
`Schedulers cannot synchronously execute watches while scheduling`, `signal read during notification
|
|
1542
|
-
phase`, or an unhandled rejection naming a component the failing file never imported. If you see any
|
|
1543
|
-
of those, suspect the previous file, not the one that failed:
|
|
1544
|
-
|
|
1545
|
-
```ts
|
|
1546
|
-
setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors in afterAll
|
|
1547
|
-
```
|
|
1548
|
-
|
|
1549
|
-
The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
|
|
1550
|
-
`cancelStrayTimers()` (returns how many it cancelled), `countStrayTimers()` and
|
|
1551
|
-
`describeStrayTimers()` (each pending timer's `kind`, the `delay` of a timeout or interval, the spec
|
|
1552
|
-
`file` that scheduled it, up to five `frames`, and `test` — the `suite > test` that scheduled it — or
|
|
1553
|
-
`outsideTest: 'import' | 'hook'` when no test was running), all from `vitest-auto-spy/setup`. Use
|
|
1554
|
-
`expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak fail rather than be tidied
|
|
1555
|
-
away, or take the per-file report from the sweep itself — `timers` carries the same origins, so the
|
|
1556
|
-
failure diff names the file and the scheduling call:
|
|
1557
|
-
|
|
1558
|
-
```ts
|
|
1559
|
-
setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).toEqual([]) });
|
|
1560
|
-
```
|
|
1561
|
-
|
|
1562
|
-
`frames` starts below the tracking wrapper and is up to forty frames deep, so a zone or an rxjs
|
|
1563
|
-
scheduler in between does not hide the caller: project frames come first; when the call came from
|
|
1564
|
-
dependencies only, their frames stand in. The package's own frames are never quoted, from source or
|
|
1565
|
-
from `dist`. `onStrayTimers: 'throw'` fails the file with a message that names the file and lists
|
|
1566
|
-
every stray as `setTimeout 300 ms, scheduled in "<test>" at <frames[0]>`, then the one fix; a handler
|
|
1567
|
-
that prints its own message should print the same `kind`, `delay`, `file`, `test` and `frames[0]` —
|
|
1568
|
-
they are what makes a report that only reproduces under a slow coverage run actionable the first
|
|
1569
|
-
time.
|
|
1570
|
-
|
|
1571
|
-
A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
|
|
1572
|
-
the previous file is the one to fix.
|
|
1573
|
-
|
|
1574
|
-
**`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
|
|
1575
|
-
away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
|
|
1576
|
-
the fake clock hands out bypasses the count entirely — which makes `expect(countStrayTimers()).toBe(0)`
|
|
1577
|
-
vacuous in any file running on a frozen clock, `setupAutoSpy({ strayTimers: true, globalFakeTimers: true })`
|
|
1578
|
-
included. Read `vi.getTimerCount()` for the fake clock's own backlog; the two answer different
|
|
1579
|
-
questions and neither covers the other. A handle cleared by the number it coerces to
|
|
1580
|
-
(`clearTimeout(+handle)`) or through its own `close()` is forgotten properly now, so those stopped
|
|
1581
|
-
being counted as strays.
|
|
1582
|
-
|
|
1583
|
-
**With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
|
|
1584
|
-
arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
|
|
1585
|
-
Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
|
|
1586
|
-
reports **no leaks** for a file that leaks. Cancelling is still the right default — a callback firing
|
|
1587
|
-
during a later file is the more expensive failure — so when both are on and no `onStrayTimers` is
|
|
1588
|
-
given, the sweep prints to stderr the report `'throw'` fails with — the file, the first three timers
|
|
1589
|
-
and the test that scheduled each — closed by one sentence saying they are missing from Vitest's
|
|
1590
|
-
"Async Leaks" report; `onStrayTimers` gets all of them. Vitest's own report, with `strayTimers` off, points
|
|
1591
|
-
its code frame at the `setTimeout` in the spec, because the library's own scheduler wrappers go
|
|
1592
|
-
through `vi.defineHelper` and are dropped from the stack.
|
|
1593
|
-
|
|
1594
|
-
**The one that keeps a suite green while it is wrong:** zone.js replaces the global `Promise`, and a
|
|
1595
|
-
rejection nobody handled is drained into `console.error` and no further — it never reaches
|
|
1596
|
-
`process.on('unhandledRejection')`, the channel Vitest watches, so the runner is never told and the
|
|
1597
|
-
file still exits 0. `compileComponents().then(() => expect(…))`, an `async` helper called without
|
|
1598
|
-
`await`, a `TypeError` thrown inside `import('…').then(…)` in production code: each of those is a
|
|
1599
|
-
passing test with a line of stderr behind it. One migrated suite — 1688 spec files, 11 587 tests,
|
|
1600
|
-
green — was hiding six of them, two being assertions that were simply false.
|
|
1601
|
-
|
|
1602
|
-
```ts
|
|
1603
|
-
setupAutoSpy({ strayRejections: true }); // fail the test the swallowed rejection surfaced in
|
|
1604
|
-
```
|
|
1605
|
-
|
|
1606
|
-
Off by default, and it needs zone.js already loaded — this package never imports it, so the setup
|
|
1607
|
-
file does (`import 'zone.js';`) or the Angular builder does. Without it the call **throws** rather
|
|
1608
|
-
than quietly watching nothing. Native, non-zone rejections already fail a Vitest run, and nothing
|
|
1609
|
-
here touches them. The pieces are exported too — `trackStrayRejections()` (idempotent, returns the
|
|
1610
|
-
undo), `flushStrayRejections()` (takes what was captured and starts again from empty) and
|
|
1611
|
-
`countStrayRejections()`. The count is complete; the **reasons** retained are capped at 100, so a
|
|
1612
|
-
file rejecting in a loop cannot hold its whole run's worth of stacks. A promise rejected with
|
|
1613
|
-
`undefined` is reported as that rather than as `[object Object]`. The `no-floating-assertion` lint
|
|
1614
|
-
rule catches the commonest shape before it ever runs (§16).
|
|
1615
|
-
|
|
1616
|
-
**`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
|
|
1617
|
-
about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
|
|
1618
|
-
into `console.error` before `process.on('unhandledRejection')` would fire, so there is nothing for
|
|
1619
|
-
that callback to filter. Use `onUnhandledError` to triage the native failures Vitest already reports;
|
|
1620
|
-
use `strayRejections` to find the ones it never sees.
|
|
1621
|
-
|
|
1622
|
-
A rejection the runner has **already** blamed the finished test for is not reported again. An
|
|
1623
|
-
`async` test that fails an assertion leaves its own `AssertionError` in both places, so a red run
|
|
1624
|
-
used to print two messages per failure and the second one sent the reader hunting for a defect that
|
|
1625
|
-
was not there. What is left is what the check is for: the rejections that fail no test at all.
|
|
1626
|
-
|
|
1627
|
-
**The one that gets slower the longer the run goes on:** every `vi.fn()` and `vi.spyOn()` is added
|
|
1628
|
-
to one `Set` inside `@vitest/spy`, because that is what `vi.clearAllMocks()` walks, and nothing takes
|
|
1629
|
-
anything out of it again. With `isolate: false` the set is created once per worker and only grows:
|
|
1630
|
-
`clearMocks: true` then walks every mock of every file already run **before every single test**, and
|
|
1631
|
-
the worker holds all of them at once — their recorded arguments included, and through those whole
|
|
1632
|
-
component trees.
|
|
1633
|
-
|
|
1634
|
-
```ts
|
|
1635
|
-
setupAutoSpy({ pruneMockRegistry: true }); // keep only the mocks that outlive a file
|
|
1636
|
-
```
|
|
1637
|
-
|
|
1638
|
-
The part to understand before turning it on is what must **not** be pruned. A pruned mock is one
|
|
1639
|
-
`clearMocks` can no longer see, so its calls accumulate silently — harmless for a mock that dies with
|
|
1640
|
-
its file, a bug for the module-level `vi.fn()` in a shared `*.mock.ts` that six spec files import.
|
|
1641
|
-
The split is therefore drawn where it is observable: what is already in the registry when a file's
|
|
1642
|
-
hooks start was created while the module graph was evaluated and is kept; everything added after that
|
|
1643
|
-
belongs to the file and goes when it ends. One case lands on the wrong side — a module first loaded
|
|
1644
|
-
by a dynamic `import()` inside a test — and says so explicitly:
|
|
1645
|
-
|
|
1646
|
-
```ts
|
|
1647
|
-
export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
|
|
1648
|
-
```
|
|
1649
|
-
|
|
1650
|
-
The mocks it keeps, it also guards. Staying registered means `vi.resetAllMocks()` reaches them too —
|
|
1651
|
-
it walks the same set and calls `mockReset()`, which puts an implementation back only when it was
|
|
1652
|
-
passed to `vi.fn(implementation)`; behaviour chained on with `.mockReturnValue(…)` or
|
|
1653
|
-
`.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a _different_ file later
|
|
1654
|
-
in the same worker, inside application code, as `Cannot read properties of undefined` against a
|
|
1655
|
-
shared double that spec never touched — and `vi.restoreAllMocks()` does not cause it (Vitest 4 walks
|
|
1656
|
-
`MOCK_RESTORE` there, which only `vi.spyOn` writes to), so probing with that one comes back green and
|
|
1657
|
-
sends the search the wrong way. The implementation a long-lived mock carried when it was classified
|
|
1658
|
-
is therefore remembered and put back, in `beforeEach`, but only when it has gone missing.
|
|
1659
|
-
|
|
1660
|
-
`trackMockRegistry()` installs the hooks on its own, `pruneMockRegistry()` is the one-shot sweep (it
|
|
1661
|
-
returns how many went), `restoreLongLivedImplementations()` is that repair (it returns how many it
|
|
1662
|
-
put back) and `getMockRegistrySize()` reports what is left.
|
|
1663
|
-
|
|
1664
|
-
One entry is never pruned, and it is not one of yours: the Vitest adapter registers a single
|
|
1665
|
-
`vi.fn()` whose `mockClear` sweeps this library's own spies. Dropping it would turn
|
|
1666
|
-
`vi.clearAllMocks()` into a silent no-op for every double in the run, so it carries a mark the
|
|
1667
|
-
pruner skips. Why the library needs a mock of its own for that is the next section.
|
|
1668
|
-
|
|
1669
|
-
**The one that answers for every file that follows:** `vi.spyOn(localStorage, 'setItem')` puts the
|
|
1670
|
-
mock on the storage as an own property, and happy-dom's `Storage` hands each instance out wrapped
|
|
1671
|
-
in a Proxy whose `deleteProperty` trap only knows stored items. `mockRestore()` — and
|
|
1672
|
-
`vi.restoreAllMocks()` walking the same path — deletes through that trap, comes back green, and
|
|
1673
|
-
does nothing: the spy keeps recording, and a later spec's own `vi.spyOn` is handed the same mock
|
|
1674
|
-
with the previous file's calls already in it, so "not to have been called" fails every other run.
|
|
1675
|
-
Probing by running `vi.restoreAllMocks()` rules nothing out — it is the failing path itself. Writing
|
|
1676
|
-
`Storage.prototype`'s method back over the spy does go through the proxy; that asymmetry, define
|
|
1677
|
-
passes and delete does not, is the repair, and it is on by default:
|
|
1678
|
-
|
|
1679
|
-
```ts
|
|
1680
|
-
setupAutoSpy(); // restoreStorageSpies: true — the sweep runs at the file boundary only
|
|
1681
|
-
```
|
|
1682
|
-
|
|
1683
|
-
`restoreStorageSpies()` is the one-shot (it returns the storages it repaired); a suite that
|
|
1684
|
-
deliberately keeps a spy on a storage method for a whole worker turns the option off. jsdom breaks
|
|
1685
|
-
the other half of the same contract — its Storage proxy turns a `defineProperty` of a method into a
|
|
1686
|
-
stored item — which is why the gate is `vi.isMockFunction` rather than "differs from the
|
|
1687
|
-
prototype": a junk item is left alone, a mock is repaired.
|
|
1688
|
-
|
|
1689
|
-
**The one that outlives its component:** a listener on `window` or `document` is not the
|
|
1690
|
-
component's to take off. Overlays, portals and services register on the shared targets, and under
|
|
1691
|
-
`isolate: false` whatever a file leaves registered fires during the next one — the same wrong-file
|
|
1692
|
-
blame as a stray timer, with none of the visibility: nothing errors, the callback simply runs
|
|
1693
|
-
against mocks and a DOM it was never written for.
|
|
1694
|
-
|
|
1695
|
-
```ts
|
|
1696
|
-
setupAutoSpy({ strayListeners: true }); // wrap addEventListener, sweep what the file added
|
|
1697
|
-
```
|
|
1698
|
-
|
|
1699
|
-
The split is the one `pruneMockRegistry` uses: a `beforeAll` marks the listeners already on
|
|
1700
|
-
`window`/`document` — registered while the module graph was evaluated, a framework's one-time
|
|
1701
|
-
initialisation — and the `afterAll` takes off everything added since. The pieces are exported too:
|
|
1702
|
-
`trackStrayListeners()` (idempotent, returns the undo), `baselineStrayListeners()`,
|
|
1703
|
-
`removeStrayListeners()` (returns how many), `countStrayListeners()` (throws before
|
|
1704
|
-
`trackStrayListeners()` has run, like the timer counter) and `describeStrayListeners()` — each
|
|
1705
|
-
stray's target, type, spec file, up to five frames and the same `test` / `outsideTest` as a timer,
|
|
1706
|
-
the list `onStrayListeners({ removed, listeners })` receives when a file should fail instead:
|
|
1707
|
-
`onStrayListeners: ({ removed }) => expect(removed).toBe(0)`, or `onStrayListeners: 'throw'`, whose
|
|
1708
|
-
message names the file and lists each as `keydown on document, added in "<test>" at <frames[0]>`.
|
|
1709
|
-
A listener has `type` and `target` where a timer has `kind` and `delay` — a handler that prints its own
|
|
1710
|
-
message prints those fields instead.
|
|
1711
|
-
With `strayTimers` on too, both reports run after both sweeps and both run even if the first throws —
|
|
1712
|
-
a handler or `'throw'` alike; two failures surface as one `AggregateError`
|
|
1713
|
-
(`2 file-end checks failed when a.spec.ts ended:`, each report a numbered block with its `Docs:`
|
|
1714
|
-
line), so a throwing report never leaves a timer uncancelled. The stray-console file report runs in
|
|
1715
|
-
the same sweep.
|
|
1716
|
-
Under jsdom a listener registered with `{ once: true }` that already fired stays counted until
|
|
1717
|
-
something removes it: the wrapper cannot observe the firing without breaking identity-based
|
|
1718
|
-
`removeEventListener` from the code under test, and removing an already-fired listener is a no-op
|
|
1719
|
-
anyway. happy-dom detaches it through the public `removeEventListener`, so there it leaves the count.
|
|
1720
|
-
|
|
1721
|
-
**The one no registry tracks:** `vi.stubGlobal` is undone by `unstubGlobals` and
|
|
1722
|
-
`vi.spyOn(globalThis, …)` by `restoreMocks`, but a plain assignment —
|
|
1723
|
-
`global.ResizeObserver = stub` — is written straight into the shared worker and read by every later
|
|
1724
|
-
file. `guardGlobals` names the _unrepairable_ case, a non-configurable redefine; this is the net
|
|
1725
|
-
for the repairable one:
|
|
1726
|
-
|
|
1727
|
-
```ts
|
|
1728
|
-
setupAutoSpy({ restoreGlobals: true }); // one snapshot per worker, restored at every file boundary
|
|
1729
|
-
```
|
|
1730
|
-
|
|
1731
|
-
`captureGlobalBaseline()` takes the snapshot — the first call is the worker's truth, a later one
|
|
1732
|
-
does nothing, so a re-capture can never launder a replacement into the baseline. `restoreGlobals()`
|
|
1733
|
-
is the sweep, and returns the keys it changed. Two traps it steps around, both load-bearing. A DOM
|
|
1734
|
-
environment installs window properties on `globalThis` as accessor pairs forwarding to an override
|
|
1735
|
-
map, so `global.ResizeObserver = stub` leaves the descriptor untouched and the restore has to write
|
|
1736
|
-
the captured value back through the same setter, not just re-define the descriptor. And the
|
|
1737
|
-
library's own wrappers — `setTimeout` under `strayTimers`, `addEventListener` under
|
|
1738
|
-
`strayListeners` — are marked as theirs, so the restore steps around them instead of uninstalling
|
|
1739
|
-
the tracking at the first boundary. Added keys are never deleted (a framework that installs a
|
|
1740
|
-
global at import time must not lose it), and the identity globals — `location`, `document`,
|
|
1741
|
-
`window` and their kin — are never written back at all. A leftover fake clock comes off first,
|
|
1742
|
-
because restoring timer descriptors under an installed fake breaks both.
|
|
1743
|
-
|
|
1744
|
-
### What a method spy is, and the one thing that differs from `vi.fn()`
|
|
1745
|
-
|
|
1746
|
-
A method spy is **not** a `vi.fn()`. It is this library's own mock function: one shared prototype
|
|
1747
|
-
carrying the whole `Mock` surface, call state allocated on the first call rather than at creation,
|
|
1748
|
-
and no entry in any global registry. `vi.fn()` assigns some twenty-five closures as own properties
|
|
1749
|
-
of every mock it makes and allocates six arrays up front — per method, on every double a spec
|
|
1750
|
-
builds — and that cost is most of what a wide service used to pay for methods no test touches.
|
|
1751
|
-
|
|
1752
|
-
Everything a spec can observe is the same, and the suite pins it by putting the two side by side:
|
|
1753
|
-
|
|
1754
|
-
- `vi.isMockFunction(spy.load)`, `expect(spy.load).toHaveBeenCalledWith(…)`, `toHaveReturned`,
|
|
1755
|
-
`toHaveResolved`, `toHaveBeenNthCalledWith` — every matcher reads `_isMockFunction`, `mock.*` and
|
|
1756
|
-
`getMockName()`, all of which the spy implements with the runner's semantics.
|
|
1757
|
-
- `spy.load.mock.calls` / `.results` / `.settledResults` / `.instances` / `.contexts` /
|
|
1758
|
-
`.invocationCallOrder` / `.lastCall`, `mockReturnValue`, `mockResolvedValue`,
|
|
1759
|
-
`mockImplementation(Once)`, `withImplementation`, `mockClear` / `mockReset` / `mockRestore`,
|
|
1760
|
-
`mockName`, `using`.
|
|
1761
|
-
- `vi.clearAllMocks()`, `vi.resetAllMocks()`, and the `clearMocks` / `mockReset` config keys, which
|
|
1762
|
-
Vitest applies through those two functions.
|
|
1763
|
-
|
|
1764
|
-
**The one difference.** `mock.invocationCallOrder` counts on this library's own scale, so
|
|
1765
|
-
`expect(a).toHaveBeenCalledBefore(b)` is exact between two auto-spies and **wrong** between an
|
|
1766
|
-
auto-spy and a hand-written `vi.fn()` — not an error, an answer, computed from two counters that
|
|
1767
|
-
never met. `@vitest/spy` keeps its own in a module-private variable it neither exports nor lets
|
|
1768
|
-
anything advance, so the scales cannot be reconciled after the fact; they drift apart by however many
|
|
1769
|
-
calls of each kind the run has already made. Measured in a converted suite: a double reporting
|
|
1770
|
-
`[164, 165, 167, 168, 169]` beside a `vi.fn()` reporting `[28]` for its single call in the same test,
|
|
1771
|
-
under an assertion that had been passing — because `jest-auto-spies` built both halves with
|
|
1772
|
-
`jest.fn()`, which did share one counter. Three earlier calls of one family are enough to invert the
|
|
1773
|
-
verdict. Nothing else in the library or in Vitest reads that field. If a suite needs the comparison,
|
|
1774
|
-
compare two doubles of the same family, or switch the whole run back to the runner's factory:
|
|
1775
|
-
|
|
1776
|
-
```ts
|
|
1777
|
-
// vitest.setup.ts
|
|
1778
|
-
import { setSpyEngine } from 'vitest-auto-spy/setup';
|
|
1779
|
-
|
|
1780
|
-
setSpyEngine('runner'); // every double built afterwards is `vi.fn()` per method, as before 4.1
|
|
1781
|
-
```
|
|
1782
|
-
|
|
1783
|
-
**On Vitest 5 the registry story changes, and this library follows it.** Vitest 5 keeps registered
|
|
1784
|
-
mocks behind `WeakRef`s and its `clearAllMocks()` visits only the ones _called_ since the last
|
|
1785
|
-
sweep — so a spy that is not a `vi.fn()` has to make itself reachable to that sweep deliberately. It
|
|
1786
|
-
does, which is why `vi.clearAllMocks()`, `vi.resetAllMocks()` and the `clearMocks: true` default
|
|
1787
|
-
(new in Vitest 5) clear these doubles exactly as they clear the runner's own. Nothing in a spec
|
|
1788
|
-
changes, and the peer range still starts at 2.1 — one install spans Vitest 2.1 through 5.x.
|
|
1789
|
-
|
|
1790
|
-
What each config flag does to an auto-spy, measured on both spy engines: `clearMocks` empties the
|
|
1791
|
-
calls and keeps the configuration; `mockReset` empties the calls and drops `mockReturnValue` /
|
|
1792
|
-
`mockImplementation` but **keeps `calledWith` rules**; `restoreMocks` never touches an auto-spy (it
|
|
1793
|
-
only undoes `vi.spyOn`). Do not claim the flags skip auto-spies — a sweep sentinel in the Vitest
|
|
1794
|
-
adapter routes the first two. `resetAutoSpy(spy)` is the call that drops everything, `calledWith`
|
|
1795
|
-
included. `mockReset()` on a `vi.spyOn(…)` runs the **real** method again (Vitest ≥ 3).
|
|
1796
|
-
|
|
1797
|
-
What Vitest 5 does break is the runner's own doing, not this library's: with `clearMocks` on by
|
|
1798
|
-
default, a test asserting on a call that an **earlier** test or a `beforeAll` recorded now reads
|
|
1799
|
-
zero. Count it in a plain variable rather than in the spy, or set `clearMocks: false`.
|
|
1800
|
-
|
|
1801
|
-
**The one exception to "nothing in a spec changes".** `setupAutoSpy({ pruneMockRegistry: true })`
|
|
1802
|
-
discovers long-lived mocks by walking the runner's registry, which Vitest 5 no longer exposes — the
|
|
1803
|
-
automatic half is a no-op there. `keepMockRegistered(mock)` works on every version and is the fix;
|
|
1804
|
-
recommend it whenever a suite on Vitest 5 loses a shared mock's implementation to
|
|
1805
|
-
`vi.resetAllMocks()`.
|
|
1806
|
-
|
|
1807
|
-
`getSpyEngine()` reports the current one, `'auto-spy'` is the default, and doubles already built keep
|
|
1808
|
-
the engine they were built with. The switch is Vitest-only: on Bun and `node:test` the runner's own
|
|
1809
|
-
matchers recognise only the runner's own mocks, so those adapters keep using them.
|
|
1810
|
-
|
|
1811
|
-
Two more switches, both about the environment rather than the spies:
|
|
1812
|
-
|
|
1813
|
-
```ts
|
|
1814
|
-
setupAutoSpy({ blockNetwork: true }); // fetch rejects, XHR fails, sendBeacon answers false
|
|
1815
|
-
```
|
|
1816
|
-
|
|
1817
|
-
Both DOMs leak, through different holes. happy-dom implements `fetch`, so a component pulling a
|
|
1818
|
-
remote asset really fetches it, nothing asserts on the response, and the aborts at teardown fail the
|
|
1819
|
-
run with **no test named** — a green run exiting 1 with `DOMException [AbortError]`. jsdom
|
|
1820
|
-
implements `XMLHttpRequest` in full, and the libraries that never left XHR (a VAST player pinging
|
|
1821
|
-
every tracker through a hand-rolled one) reach the internet once per ping per test, printing jsdom's
|
|
1822
|
-
`AggregateError at Object.dispatchError` for each connection that failed — so what a green run
|
|
1823
|
-
prints depends on whether the machine has a route out.
|
|
1824
|
-
|
|
1825
|
-
Every channel is closed by default; the object is for narrowing it:
|
|
1826
|
-
|
|
1827
|
-
| option | default | what it does |
|
|
1828
|
-
| -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
1829
|
-
| `fetch` | `true` | `fetch` rejects, naming the method, the URL and the test, and the `stubResponse` line that answers it |
|
|
1830
|
-
| `xhr` | `'reject'` | `'reject'` fails the request (`status` 0, an `error` event); `'empty'` answers 200 with an empty body; `false` leaves XHR alone |
|
|
1831
|
-
| `beacon` | `true` | `navigator.sendBeacon` answers `false` — only where the environment has one |
|
|
1832
|
-
|
|
1833
|
-
`'reject'` is the default because it is what `fetch` does: the code takes its failure branch, which
|
|
1834
|
-
is the branch a unit test should be asserting on. `'empty'` is for a request whose response nobody
|
|
1835
|
-
reads — a tracker ping, an analytics beacon — where failing it only trades one kind of noise for
|
|
1836
|
-
another:
|
|
1837
|
-
|
|
1838
|
-
```ts
|
|
1839
|
-
setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setting
|
|
1840
|
-
```
|
|
1841
|
-
|
|
1842
|
-
**Called twice, the last caller's mode wins.** The setup file installs from its own `beforeEach`,
|
|
1843
|
-
which runs before the spec's, so a spec calling `blockNetwork({ xhr: 'reject' })` because the failure
|
|
1844
|
-
branch is what it is testing used to be served the setup file's empty 200 and pass on the wrong
|
|
1845
|
-
branch. The stubs are also installed once rather than re-journalled per test, so `restoreProps: false`
|
|
1846
|
-
no longer grows the restore journal for the length of the run.
|
|
1847
|
-
|
|
1848
|
-
A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
|
|
1849
|
-
serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
|
|
1850
|
-
and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
|
|
1851
|
-
resolves it against the document origin, so a spec that reaches `/config`and passes is resting on
|
|
1852
|
-
nothing listening on that port.`WebSocket`and`EventSource`are left alone: their failure is an
|
|
1853
|
-
event on an object the code keeps and reconnects, so there is no blanket answer that is not itself
|
|
1854
|
-
a behaviour change —`stubConstructor(globalThis, 'WebSocket', …)` is the tool for a spec with one.
|
|
1855
|
-
|
|
1856
|
-
- `stubResponse(init?)` (`/setup`) builds a real `Response` — never write `{ ok, json } as Response`.
|
|
1857
|
-
Plain data in `body` goes out as JSON with `application/json`; an `ok` that disagrees with
|
|
1858
|
-
`status` throws. A body can be read once: for a stub answering several calls use
|
|
1859
|
-
`mockImplementation(async () => stubResponse(…))`, not `mockResolvedValue`.
|
|
1860
|
-
- **`body: null` is the JSON literal, and only `undefined` or an omitted `body` send no body.** It
|
|
1861
|
-
reverses what the field used to mean: `null` sent no body, so `.json()` threw
|
|
1862
|
-
`Unexpected end of JSON input` on it while `0`, `false`, `[]` and `{}` round-tripped — the one
|
|
1863
|
-
literal a reader had no reason to expect to be different, and the shape a backend answering
|
|
1864
|
-
"nothing here" (an empty login, an absent profile) really sends. A spec that meant "no body"
|
|
1865
|
-
drops the field. `{ body: null, status: 204 }` throws by name rather than letting the constructor
|
|
1866
|
-
raise "Response with null body status cannot have body" about a field whose old meaning was the
|
|
1867
|
-
opposite; 205 and 304 are the same.
|
|
1868
|
-
- `blockNetwork` leaves `fetch` alone while `Symbol.for('fetch-interceptor')` is on `globalThis`
|
|
1869
|
-
(MSW `setupServer`, nock 14), so MSW handlers keep answering; XHR stays blocked for what MSW does
|
|
1870
|
-
not handle. MSW's `onUnhandledRequest: 'error'` exempts asset-looking URLs (`.svg`, `.json`,
|
|
1871
|
-
fonts…); a last `http.all('*', () => HttpResponse.error())` is the hard floor. MSW's browser
|
|
1872
|
-
`setupWorker` has not been checked.
|
|
1873
|
-
|
|
1874
|
-
`restoreTimerGlobals` is on by default and needs no thought unless you turn it off: uninstalling
|
|
1875
|
-
fake timers under happy-dom **deletes** `Date` instead of restoring it (the global is inherited from
|
|
1876
|
-
the realm, not owned by `globalThis`), and with `isolate: false` the next file dies inside Vitest's
|
|
1877
|
-
own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
|
|
1878
|
-
the file in the stack is not the cause.
|
|
1879
|
-
|
|
1880
|
-
`restoreWebStorage` is the other repair that is on by default. Vitest copies a DOM environment's
|
|
1881
|
-
globals onto `globalThis` behind `if (k in global) return KEYS.includes(k)`, and neither
|
|
1882
|
-
`localStorage` nor `sessionStorage` is in `KEYS` — both used to arrive only because Node put neither
|
|
1883
|
-
on `globalThis`, so the first half of that condition was false. Node's own Web Storage made the key
|
|
1884
|
-
exist, and the environment's storage stopped arriving: `setItem is not a function` on Node 25,
|
|
1885
|
-
`undefined` on Node 26, under jsdom and happy-dom alike, because the filter runs before either. The
|
|
1886
|
-
suite stays green until a spec touches storage, so this lands as "CI moved to a new Node and eleven
|
|
1887
|
-
unrelated specs died". The repair writes a key, reads it back and removes it — a storage that
|
|
1888
|
-
survives that is left alone, whoever implemented it — and it installs nothing in a `node`
|
|
1889
|
-
environment, which is supposed to have no Web Storage at all.
|
|
1890
|
-
|
|
1891
|
-
`hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
|
|
1892
|
-
CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
|
|
1893
|
-
`test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
|
|
1894
|
-
own and defaults it to 10 000 ms. Carry a preset's `testTimeout: 30000` into the runner config and
|
|
1895
|
-
stop there, and every hook in the suite silently runs on a third of what its tests get. Worse, the
|
|
1896
|
-
failure is filed against the wrong thing: a `beforeEach` timeout is attributed to the **test**, with
|
|
1897
|
-
the test's duration pinned at the limit, so the report reads `× should create 10045ms` and sends the
|
|
1898
|
-
reader looking for ten seconds of work inside a body that never ran. The hint appends both numbers
|
|
1899
|
-
and the field to set. It says nothing when the budgets agree — then the hook really is slow — and
|
|
1900
|
-
nothing for `beforeEach(fn, 300)`, which chose its own limit. `beforeAll` is out of reach by
|
|
1901
|
-
construction: its timeout is reported as a failed _suite_, every test is skipped and no `afterEach`
|
|
1902
|
-
runs.
|
|
1903
|
-
|
|
1904
|
-
**When migrating a runner config off Jest, set both timeouts side by side**, and treat the single
|
|
1905
|
-
Jest number as belonging to both fields:
|
|
1906
|
-
|
|
1907
|
-
```ts
|
|
1908
|
-
test: {
|
|
1909
|
-
testTimeout: 30_000,
|
|
1910
|
-
// Jest had one budget for both; Vitest defaults this to 10_000 on its own.
|
|
1911
|
-
hookTimeout: 30_000,
|
|
1912
|
-
}
|
|
1913
|
-
```
|
|
1914
|
-
|
|
1915
|
-
`frozenClockHint` is the same seam aimed at the other half of a timeout. A frozen clock turns
|
|
1916
|
-
waiting into waiting forever — `await new Promise(r => setTimeout(r, 10))` never resolves unless
|
|
1917
|
-
something advances it — and the runner's own advice ("pass a timeout value as the last argument") is
|
|
1918
|
-
the one repair that cannot work: the callback is not late, it is never scheduled to run. Under
|
|
1919
|
-
`globalFakeTimers` nothing in the spec says the clock is fake at all, so the timeout lands in a file
|
|
1920
|
-
that never mentions a timer. The hint reports `vi.isFakeTimers()` and `vi.getTimerCount()` — facts,
|
|
1921
|
-
not a guess — and stays silent when the clock is real or its queue is empty.
|
|
1922
|
-
|
|
1923
|
-
The shape that reaches this with no timer in sight is an HTTP spec. `setImmediate` is among the
|
|
1924
|
-
globals `vi.useFakeTimers()` replaces by default, and Express ends a request that matched no route
|
|
1925
|
-
through `finalhandler`, which schedules on `setImmediate`. So the 404 is never written and the test
|
|
1926
|
-
dies on its timeout: in such a file, "the test hung" means _the route did not match_, not "the
|
|
1927
|
-
server is slow". One thing this cannot see through — a spec whose own `afterEach` calls
|
|
1928
|
-
`vi.useRealTimers()`, because hooks run in reverse registration order and the clock is real again by
|
|
1929
|
-
the time the hint reads it. Nothing is reported then, rather than something wrong.
|
|
1930
|
-
|
|
1931
|
-
`angularBuildHint` is the third read-only hint, and the only one that speaks before a test rather
|
|
1932
|
-
than after one. `@angular/build` in `[22.1.5, 22.1.7)` builds the unit-test bundle with esbuild code
|
|
1933
|
-
splitting off — every spec a self-contained bundle, `--coverage` growing by hundreds of megabytes
|
|
1934
|
-
with no plateau (791 chunks / 596 MB on a 784-spec suite) — and the builder says nothing. The
|
|
1935
|
-
`doctor` check `angular-build-splitting-off` reports it, but has to be run; this line is printed
|
|
1936
|
-
from inside the affected run, to stderr, once per worker. The builder is recognised by the marker
|
|
1937
|
-
its own `vitest-mock-patch` setup file sets (`Symbol.for('@angular/cli/vitest-mock-patch')` on
|
|
1938
|
-
`globalThis`, before any user setup file), so a plain Vitest run reads nothing; under the builder
|
|
1939
|
-
the version comes from the nearest `node_modules/@angular/build/package.json` above the working
|
|
1940
|
-
directory. That read is the one place the library touches the disk — one file, read-only, through
|
|
1941
|
-
`process.getBuiltinModule` so `/setup` still loads with no `process` — and `scripts/check-dist.mjs`
|
|
1942
|
-
allows `node:fs` in `dist/setup.js` for exactly that. Silent outside the builder, outside the
|
|
1943
|
-
window, and on a Node before `getBuiltinModule`. `setupAutoSpy({ angularBuildHint: false })` turns
|
|
1944
|
-
it off.
|
|
1945
|
-
|
|
1946
|
-
**Strict for one run:** `VITEST_AUTO_SPY_STRICT=1 npx vitest run <slice>` makes `setupAutoSpy()` arm
|
|
1947
|
-
`strict` whatever the setup file passed (`0` turns it off, `survey` counts instead of throwing); a second `setupAutoSpy()` in an extra setup
|
|
1948
|
-
file also works now, without grading the first call's network stubs as patched outside a hook.
|
|
1949
|
-
|
|
1950
|
-
**Moving a suite to `strict`:** most of the churn is the same few methods configured file by file.
|
|
1951
|
-
Run the suite once in survey mode, which refuses nothing and ends each file with the list strict mode
|
|
1952
|
-
would have refused, most frequent first (`setupAutoSpy({ strict: 'survey' })` is the same in code):
|
|
1953
|
-
|
|
1954
|
-
```bash
|
|
1955
|
-
VITEST_AUTO_SPY_STRICT=survey npx vitest run
|
|
1956
|
-
```
|
|
1957
|
-
|
|
1958
|
-
The top of that list — `SvgIconService.getIcon`, `NotificationsService.open` — goes into the setup
|
|
1959
|
-
file once, `registerAutoSpyDefaults(NotificationsService, { returns: { open: undefined } })`, and
|
|
1960
|
-
every double of the class answers it; the tail is what each spec configures itself.
|
|
1961
|
-
|
|
1962
|
-
**One setup file for the builder and for plain Vitest:** the builder initialises `TestBed` before any
|
|
1963
|
-
setup file, so a second `initTestEnvironment()` throws "Cannot set base providers because it has
|
|
1964
|
-
already been called". Guard it with `isAngularUnitTestBuilder()` from `/setup` — the same marker the
|
|
1965
|
-
hint reads — and name the file in the target's `setupFiles`, which is the only way the builder runs
|
|
1966
|
-
it (`doctor` reports `builder-setup-unreached` otherwise).
|
|
1967
|
-
|
|
1968
|
-
One more field of the same family differs quietly and changes only the report:
|
|
1969
|
-
`slowTestThreshold` is `5` in Jest (**seconds**) and `300` in Vitest (**milliseconds**), so a
|
|
1970
|
-
migrated suite starts marking most of its files slow. That is a unit change, not a regression.
|
|
1971
|
-
|
|
1972
|
-
Fake timers:
|
|
1973
|
-
|
|
1974
|
-
```ts
|
|
1975
|
-
import { advanceTimers, setupFakeTimers } from 'vitest-auto-spy/setup';
|
|
1976
|
-
|
|
1977
|
-
setupFakeTimers(); // install + restore, paired
|
|
1978
|
-
await advanceTimers(5_000); // advance AND drain the microtasks a bare advanceTimersByTime leaves
|
|
1979
|
-
```
|
|
1980
|
-
|
|
1981
|
-
Coming from a Jest project that had `fakeTimers: { enableGlobally: true }`, every one of its tests
|
|
1982
|
-
was written against a frozen clock. Turning that back on file by file is a thousand edits; turn it
|
|
1983
|
-
on once instead:
|
|
1984
|
-
|
|
1985
|
-
```ts
|
|
1986
|
-
setupAutoSpy({ globalFakeTimers: true }); // or a vi.useFakeTimers() config
|
|
1987
|
-
```
|
|
1988
|
-
|
|
1989
|
-
Both ends are guarded, which is the half a hand-written pair of hooks gets wrong: a spec that drives
|
|
1990
|
-
the clock itself would otherwise reach a second `vi.useRealTimers()`, and that one leaves the
|
|
1991
|
-
environment without `clearInterval` — which explodes during teardown of whichever file runs next.
|
|
1992
|
-
|
|
1993
|
-
`globalFakeTimers` also keeps the clock fake **between** tests, and that half is not decoration: a
|
|
1994
|
-
`beforeAll` inside a nested `describe` runs after the previous test's `afterEach`, so a
|
|
1995
|
-
`beforeEach`-only pair leaves it on real timers and the block fails with `the timers APIs are not
|
|
1996
|
-
mocked` without touching a timer itself. For one `describe` instead of the whole run:
|
|
1997
|
-
`setupFakeTimers(config, { betweenTests: true })`.
|
|
1998
|
-
|
|
1999
|
-
Whatever you turn on, the hooks belong to the spec file whose collection imported the setup module.
|
|
2000
|
-
If something keeps that module in the cache across files — `@angular/build:unit-test` under
|
|
2001
|
-
`--coverage` serves every test file as a wrapper around the built bundle, so the setup module is
|
|
2002
|
-
never re-evaluated — only the first file of each worker gets them, and the rest fail somewhere
|
|
2003
|
-
unrelated. Run coverage with `--isolate`, or call `setupAutoSpy()` from something evaluated per file.
|
|
2004
|
-
|
|
2005
|
-
### Freezing and counting the clock
|
|
2006
|
-
|
|
2007
|
-
```ts
|
|
2008
|
-
import { mockNow, mockSystemTime, useCountingClock, withSystemTime } from 'vitest-auto-spy/setup';
|
|
2009
|
-
|
|
2010
|
-
mockSystemTime('2025-04-30T00:00:00Z'); // works whether or not fakes are already installed
|
|
2011
|
-
await withSystemTime('2025-04-30T00:00:00Z', async () => { … }); // scoped, restores itself
|
|
2012
|
-
const clock = useCountingClock(); // Date.now() → 1, 2, 3 …, reset before every test
|
|
2013
|
-
mockNow(() => nextTimestamp()); // any Date.now source, re-applied before every test
|
|
2014
|
-
```
|
|
2015
|
-
|
|
2016
|
-
**An assertion that contains a date must set the clock.** Otherwise the expected string is computed
|
|
2017
|
-
from `new Date()` and the test starts failing on its own some days after it was written — which
|
|
2018
|
-
reads as a regression and is not one.
|
|
2019
|
-
|
|
2020
|
-
`vi.spyOn(globalThis, 'Date')` is not the way. Fake timers already own that global, so it throws
|
|
2021
|
-
`Date is not a constructor` with a stack in production code and no mention of timers.
|
|
2022
|
-
`mockSystemTime` does the right thing either way.
|
|
2023
|
-
|
|
2024
|
-
Two rules about which of these composes with which, both of them earned:
|
|
2025
|
-
|
|
2026
|
-
- **`withSystemTime` puts the previous time back**, including when fake timers were already running.
|
|
2027
|
-
Its undo used to be a no-op there, so the clock stayed where the callback left it for the rest of
|
|
2028
|
-
the test.
|
|
2029
|
-
- **`mockSystemTime()` installs `Date` and nothing else**, so `advanceTimers()` under it has nothing
|
|
2030
|
-
to advance. It used to pass having done nothing; it now refuses and names `setupFakeTimers()`.
|
|
2031
|
-
`setupFakeTimers(config)` installs the config it was given rather than deferring to whatever fakes
|
|
2032
|
-
were already up — a nested call, a global pair plus a local one, and the `Date`-only case all now
|
|
2033
|
-
end with the clock the call asked for, where the outer configuration used to win silently.
|
|
2034
|
-
|
|
2035
|
-
`useCountingClock` exists because under fake timers every call inside one test reports the _same_
|
|
2036
|
-
"now", so a spec that asserts on **order** or **duration** — analytics batches, tracing, a rate
|
|
2037
|
-
limiter, a TTL cache — cannot express its expectation at all. Patching `Date.now` by hand does not
|
|
2038
|
-
survive: `vi.useFakeTimers()` installs a fresh `Date` on every call, so a module-scope or `beforeAll`
|
|
2039
|
-
patch is left on an object nothing reads, and the naive undo re-attaches a dead clock's `now` to the
|
|
2040
|
-
live one.
|
|
2041
|
-
|
|
2042
|
-
### Asserting focus
|
|
2043
|
-
|
|
2044
|
-
```ts
|
|
2045
|
-
registerFocusMatchers(); // once, in the setup file
|
|
2046
|
-
|
|
2047
|
-
expect(fixture.nativeElement.querySelector('.play')).toHaveFocus();
|
|
2048
|
-
```
|
|
2049
|
-
|
|
2050
|
-
The two idioms it replaces both fail unhelpfully: `expect(document.activeElement).toBe(el)` prints
|
|
2051
|
-
two whole DOM subtrees, and `expect(el === document.activeElement).toBe(true)` prints
|
|
2052
|
-
`expected false to deeply equal true`. The matcher separates the causes instead — the query found
|
|
2053
|
-
nothing, the element is not in the document, focus is still on `<body>` (nothing claimed it), or
|
|
2054
|
-
focus is on another element, which it names as `button#save.primary` rather than as a subtree.
|
|
2055
|
-
|
|
2056
|
-
### Shared fixtures are functions, not constants
|
|
2057
|
-
|
|
2058
|
-
Under `isolate: false` a module is evaluated **once per worker**, so this is one set of spies shared
|
|
2059
|
-
by every file that imports it, registered against whichever file got there first — and the others'
|
|
2060
|
-
`clearMocks` never reaches them. The symptom is a 30-second timeout, in a different file each run.
|
|
2061
|
-
|
|
2062
|
-
```ts
|
|
2063
|
-
// ❌ __mocks__/context.ts
|
|
2064
|
-
export const mockActionContext = { actions: { navigateToSection: vi.fn() } };
|
|
2065
|
-
export const checkoutProvider = { provide: CheckoutState, useValue: { load: vi.fn() } };
|
|
2066
|
-
|
|
2067
|
-
// ✅
|
|
2068
|
-
export const createActionContext = () => ({ actions: { navigateToSection: vi.fn() } });
|
|
2069
|
-
export const createCheckoutProvider = () => ({ provide: CheckoutState, useValue: { load: vi.fn() } });
|
|
2070
|
-
```
|
|
2071
|
-
|
|
2072
|
-
A spec file must **export nothing**: under `isolate: false` an exported spec file is imported by its
|
|
2073
|
-
neighbours and loses its own suite. Put shared doubles in a `*.mock.ts` next to them, as factories.
|
|
2074
|
-
The `no-shared-module-level-mock` lint rule (§16) finds these mechanically.
|
|
2075
|
-
|
|
2076
|
-
### A stub must be re-installed for every test
|
|
2077
|
-
|
|
2078
|
-
Every stub this library installs is taken off again by `restoreMockedProps()` after each test — that
|
|
2079
|
-
is what keeps it out of the next file. So a stub installed once at `describe` level, or in a
|
|
2080
|
-
`beforeAll`, is gone from the second test on, and what fails is an assertion about the component
|
|
2081
|
-
with the stub sitting ten lines above it, apparently in force. The same ordering bites the other way:
|
|
2082
|
-
a project-wide setup file installs its defaults in a root `beforeEach`, and root hooks run **before**
|
|
2083
|
-
a file's own — so a `beforeAll` in a spec loses to them silently, while a `beforeEach` wins.
|
|
2084
|
-
|
|
2085
|
-
```ts
|
|
2086
|
-
import { installPerTest } from 'vitest-auto-spy/setup';
|
|
2087
|
-
|
|
2088
|
-
const observers = installPerTest(() => stubIntersectionObserver({ autoEmit: true }));
|
|
2089
|
-
|
|
2090
|
-
it('…', () => expect(observers().last.targets).toEqual([host]));
|
|
2091
|
-
```
|
|
2092
|
-
|
|
2093
|
-
It hands back a **reader**, not the handle: the handle is a different object each test.
|
|
2094
|
-
|
|
2095
|
-
### Naming the file that sealed a global
|
|
2096
|
-
|
|
2097
|
-
```ts
|
|
2098
|
-
setupAutoSpy({ guardGlobals: 'throw' }); // or 'warn' while a suite is being cleaned up
|
|
2099
|
-
```
|
|
1525
|
+
In [`agent-docs/setup.md`](./agent-docs/setup.md). Read it when the task touches `setupAutoSpy()` or the setup file, or when a guard failed a file: stray timers, console output, sealed globals, leftovers on `<body>`.
|
|
2100
1526
|
|
|
2101
|
-
|
|
2102
|
-
property can no longer be redefined _or_ deleted. Under `isolate: false` every later file in the
|
|
2103
|
-
worker inherits it, and what fails is some library, every other run, with nothing naming the file
|
|
2104
|
-
that did it. The guard compares `globalThis`, `document`, `navigator`, `location`, `screen` and the
|
|
2105
|
-
DOM prototypes a Jest-era stub reaches for — `Element`, `HTMLElement`, `HTMLCanvasElement`,
|
|
2106
|
-
`HTMLMediaElement`, `Node`, `EventTarget` — around every test, symbol keys included, and reports only
|
|
2107
|
-
what appeared and cannot be removed.
|
|
1527
|
+
<!-- agents-outline: agent-docs/setup.md -->
|
|
2108
1528
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
1529
|
+
- What a method spy is, and the one thing that differs from `vi.fn()`
|
|
1530
|
+
- Freezing and counting the clock
|
|
1531
|
+
- Asserting focus
|
|
1532
|
+
- Shared fixtures are functions, not constants
|
|
1533
|
+
- A stub must be re-installed for every test
|
|
1534
|
+
- Naming the file that sealed a global
|
|
1535
|
+
- Naming the file that polluted `Object.prototype`
|
|
1536
|
+
- Naming the test that left an attribute on `<body>`
|
|
1537
|
+
- Failing on console output nothing absorbed
|
|
1538
|
+
- One grade for everything — `preset: 'strict'`
|
|
1539
|
+
- Hook order differs from Jest
|
|
2116
1540
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
```ts
|
|
2120
|
-
setupAutoSpy(); // prototypePollution: 'throw' by default — 'warn' sweeps and only reports
|
|
2121
|
-
```
|
|
2122
|
-
|
|
2123
|
-
Vitest walks a file's hooks with `for…in` in `mergeHooks`, so one own enumerable key on
|
|
2124
|
-
`Object.prototype` is spread as if it were an array during **collect**:
|
|
2125
|
-
`TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`, with no stack —
|
|
2126
|
-
every frame of it is filtered out by `stackIgnorePatterns`. Under `isolate: false` the key outlives
|
|
2127
|
-
its file, so **every later spec file in that worker fails to collect** while the run still reports
|
|
2128
|
-
success: `145 failed | 1613 passed` over `11880 passed | 0 failed`, zero failing tests because those
|
|
2129
|
-
files never ran. The guard compares `Object.prototype` / `Array.prototype` / `Function.prototype`
|
|
2130
|
-
around every test, takes the key back off and names the file. The write is nearly always accidental:
|
|
2131
|
-
`Object.getPrototypeOf(instance)` **is** `Object.prototype` when `instance` is an object literal from
|
|
2132
|
-
a `useValue` provider or a test double — patch the prototype of the class the object came from.
|
|
2133
|
-
`guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
|
|
2134
|
-
that does not call `setupAutoSpy()`.
|
|
2135
|
-
|
|
2136
|
-
**A key an earlier file left behind is taken back before the next one is collected.** The per-test
|
|
2137
|
-
comparison cannot see a write made while a spec file was being imported or collected, or in its
|
|
2138
|
-
`afterAll` — all three happen outside any test, and the file that then fails to collect is the
|
|
2139
|
-
innocent one. `setupAutoSpy()` therefore also checks at the moment it is called, which is the one
|
|
2140
|
-
seam Vitest leaves between files: the key is deleted, a line goes to **stderr** naming it, and
|
|
2141
|
-
**nothing fails** — the file that would fail is not the file that wrote it. A key that refuses to be
|
|
2142
|
-
deleted is adopted into the baseline rather than reported once per file for the rest of the run. The
|
|
2143
|
-
in-file check still throws at its default grade; the file's own baseline is taken in `beforeAll` and
|
|
2144
|
-
re-checked after every `afterAll`, so a write made in either is covered as well.
|
|
2145
|
-
|
|
2146
|
-
### Naming the test that left an attribute on `<body>`
|
|
2147
|
-
|
|
2148
|
-
```ts
|
|
2149
|
-
setupAutoSpy({ documentPollution: 'throw' }); // default 'off'; 'throw' under preset: 'strict'
|
|
2150
|
-
setupAutoSpy({ documentPollution: { reaction: 'throw', nodes: true, ignoreAttributes: [/^data-cdk-/], ignoreNodes: 'style' } });
|
|
2151
|
-
```
|
|
2152
|
-
|
|
2153
|
-
Under `isolate: false` every spec file in a worker shares one document. An attribute a component set
|
|
2154
|
-
on `<body>` — `renderer.setAttribute(document.body, 'data-reset-focus', '')` in an `effect` — that
|
|
2155
|
-
nothing took off changes the branch a later file's code takes, so that file fails, only when the two
|
|
2156
|
-
share a worker and never alone. The guard records the attributes of `<html>`, `<head>` and `<body>`
|
|
2157
|
-
before each test and compares them from `onTestFinished` — **after** the TestBed destroyed the
|
|
2158
|
-
fixtures in its own `afterEach`, so what a component removes in `ngOnDestroy` / `DestroyRef.onDestroy`
|
|
2159
|
-
is never reported. Every attribute added, changed or removed is named with both values, put back,
|
|
2160
|
-
and fails that test. An empty `class` or `style` reads as the attribute being absent, in both
|
|
2161
|
-
directions — `classList.add` followed by `remove` leaves `class=""` where there was none and nothing
|
|
2162
|
-
observable changed; any other empty value, `data-reset-focus=""` included, is still reported.
|
|
2163
|
-
A change made in a `beforeAll` and never undone fails the file, checked from a
|
|
2164
|
-
`beforeAll` cleanup after every `afterAll`. `nodes: true` also watches the child elements of `<head>`
|
|
2165
|
-
and `<body>`; it is off by default because a module that injects a stylesheet on first import does
|
|
2166
|
-
so once per worker. Blind spots: a write made while the spec file is imported, and a fixture kept
|
|
2167
|
-
alive by `teardown: { destroyAfterEach: false }` (reported against the test that rendered it).
|
|
2168
|
-
`guardDocumentPollution(option)` from `/setup` registers the same check on its own. Vitest only, like
|
|
2169
|
-
every `/setup` guard — `bun:test`, `node:test` and Rstest have no setup entry to host it.
|
|
2170
|
-
|
|
2171
|
-
### Failing on console output nothing absorbed
|
|
2172
|
-
|
|
2173
|
-
```ts
|
|
2174
|
-
setupAutoSpy({ strayConsole: 'throw' }); // default 'off'; 'warn' prints the same report without failing
|
|
2175
|
-
```
|
|
2176
|
-
|
|
2177
|
-
Any call to a console method that writes (`log`, `info`, `warn`, `error`, `debug`, `trace`, `table`,
|
|
2178
|
-
`dir`, `dirxml`, `timeLog`, `timeEnd`, `count`; `group` with a label; a falsy `assert`) made during a
|
|
2179
|
-
test that nothing absorbed fails **that test**, quoting the method, the first three lines and the first
|
|
2180
|
-
frame outside `node_modules`, and naming the spy that absorbs it:
|
|
2181
|
-
|
|
2182
|
-
```
|
|
2183
|
-
[vitest-auto-spy] "CartService > reports a failed load" wrote to console.error 1 time and nothing absorbed it:
|
|
2184
|
-
- console.error: Error: load failed
|
|
2185
|
-
at CartService.load (src/app/cart.service.ts:41:15)
|
|
2186
|
-
Absorb what the test expects — installConsoleSpies() in a beforeEach, then assert consoleErrorSpy — or fix the code if the output is a defect.
|
|
2187
|
-
Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/setup#_16-console-output-nothing-absorbed
|
|
2188
|
-
```
|
|
2189
|
-
|
|
2190
|
-
- **Absorbed** means the call never reached the console: a `/console` spy installed for the test or
|
|
2191
|
-
the file, `vi.spyOn(console, m).mockImplementation(…)`, any replacement that does not call
|
|
2192
|
-
through. A bare `vi.spyOn(console, m)` calls through and **counts** — the report says so and names
|
|
2193
|
-
`.mockImplementation(() => undefined)`; a method with no `/console` spy (`table`, `dir`) gets a
|
|
2194
|
-
silent `vi.spyOn` instead.
|
|
2195
|
-
- **This library's own `'warn'`-grade reports are not stray console output.** `guardGlobals`,
|
|
2196
|
-
`prototypePollution`, `unconfiguredReads`, `swallowedStrictCalls`, `misconfiguration`, the
|
|
2197
|
-
duplicate-copy report, the skipped-teardown net and the `test.concurrent` notice write past the
|
|
2198
|
-
guard's wrapper, so `'warn'` stays a warning under `strayConsole: 'throw'` instead of failing the
|
|
2199
|
-
test it is advising about. A `vi.spyOn(console, 'warn')` a test installs still absorbs them — the
|
|
2200
|
-
channel checks that the guard's own wrapper is the installed method and steps aside when anything
|
|
2201
|
-
sits on top of it.
|
|
2202
|
-
- Output **outside any test** — collection/import, `beforeAll` / `afterAll`, a callback after its
|
|
2203
|
-
test ended — fails the **file** at its end, in the same sweep as the timer and listener reports. The
|
|
2204
|
-
report names the phase — `while the file was being imported`, `in a beforeAll`,
|
|
2205
|
-
`after a test had ended`, or `outside any test` when they mix — and gives that phase's advice: for
|
|
2206
|
-
import-time output, the module and the line to fix. A recognised line (`NG0912`, any `NGxxxx`)
|
|
2207
|
-
adds a `Likely cause:` block. It always names the file that wrote the output: when that file's end
|
|
2208
|
-
never ran (its own `afterAll` threw), the next file reports it and says
|
|
2209
|
-
`It is reported at the end of b.spec.ts: the file-end check of a.spec.ts did not run.`
|
|
2210
|
-
- Every console method a test replaced is put back after it; one a file replaced, after the file.
|
|
2211
|
-
- Under the guard, **importing `vitest-auto-spy/console` installs nothing** — call
|
|
2212
|
-
`installConsoleSpies()` in a `beforeEach` (or at the top of the file). Spies an import installed
|
|
2213
|
-
before the guard armed are taken off.
|
|
2214
|
-
- `strayConsole: { allow: ['…', /…/] }` is the last resort, for a line from a dependency no spec can
|
|
2215
|
-
reach — the import-time report offers it, with the pattern, only when the quoted frame is inside
|
|
2216
|
-
`node_modules`. Output written to `process.stdout` / `process.stderr` directly, and jsdom's own
|
|
2217
|
-
virtual console, is not seen. `NG0912` is not noise to allow: it means two copies of one component
|
|
2218
|
-
in the bundle, fixed by importing the component from one place.
|
|
2219
|
-
- The wrapper forwards every call unchanged, so Vitest's attribution and `onConsoleLog` still work.
|
|
2220
|
-
`guardStrayConsole(reaction)` registers the same guard on its own.
|
|
2221
|
-
|
|
2222
|
-
### One grade for everything — `preset: 'strict'`
|
|
2223
|
-
|
|
2224
|
-
```ts
|
|
2225
|
-
setupAutoSpy({ preset: 'strict' });
|
|
2226
|
-
```
|
|
2227
|
-
|
|
2228
|
-
Sets `duplicateCopies`, `propsOutsideHooks`, `guardGlobals`, `prototypePollution`, `documentPollution`,
|
|
2229
|
-
`strayConsole` and `misconfiguration` to `'throw'`, turns `strayTimers` on, and `strayRejections` on when zone.js is
|
|
2230
|
-
loaded. An option passed alongside still wins. **Not** included: `strict` (strict doubles change what
|
|
2231
|
-
an unconfigured call returns — a semantic switch, not a grade; the name was taken, hence `preset`) and
|
|
2232
|
-
`unconfiguredReads`, its read side (survey with `onUnstubbedRead` before turning it on),
|
|
2233
|
-
`blockNetwork` (changes the code under test), `restoreMocks` (drops `beforeAll` spies), failing on
|
|
2234
|
-
stray-timer counts (the sweep fails the file from `afterAll`, and a callback scheduled after the
|
|
2235
|
-
previous file's sweep is charged to the next — opt in with `onStrayTimers: 'throw'`, whose message
|
|
2236
|
-
names the file and the test that scheduled each one, or `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`), and
|
|
2237
|
-
`enableAngularDiagnostics()`, which lives in `/angular/diagnostics` — call it in the same setup file as the Angular half of strict.
|
|
2238
|
-
|
|
2239
|
-
`misconfiguration: 'throw'` on its own makes the library's misuse reports — an `onlyMethodsToSpyOn`
|
|
2240
|
-
typo, `gettersToSpyOn` naming a method, a `returns` key no spy answers to, `injectSpy` handed a real
|
|
2241
|
-
instance, a `jasmine.DEFAULT_TIMEOUT_INTERVAL` write, `providedMethodNames`, a `nextWithValues()` on an
|
|
2242
|
-
observable property something already subscribed to — throw at the call site,
|
|
2243
|
-
every occurrence. The grade is process-wide and released after the file. The printed grade of the
|
|
2244
|
-
`injectSpy` warning is de-duplicated per token **per spec file**, no longer per worker.
|
|
2245
|
-
|
|
2246
|
-
`withoutStrayTimerTracking(work)` runs setup work whose timers `strayTimers` neither counts nor
|
|
2247
|
-
cancels — jsdom answers every Web Storage write with a real `setTimeout(…, 0)`, and the library's own
|
|
2248
|
-
storage probe runs under it.
|
|
2249
|
-
|
|
2250
|
-
**`test.concurrent` earns one warning per worker, and it is worth reading rather than silencing.**
|
|
2251
|
-
The per-test guards assume one test at a time: the document snapshot, the console window and the
|
|
2252
|
-
unconfigured-read counter are opened and judged per test, so with two in flight a finding can be
|
|
2253
|
-
charged to the other one — or cleared before anything sees it. The restores still run for every
|
|
2254
|
-
test, and the skipped-teardown net now remembers per test rather than per file, which is the half
|
|
2255
|
-
that was simply wrong. Run the files that need a guard sequentially, or keep `test.concurrent` for
|
|
2256
|
-
files whose setup passes `strayConsole: 'off'`, `documentPollution: 'off'` and
|
|
2257
|
-
`unconfiguredReads: 'off'`.
|
|
2258
|
-
|
|
2259
|
-
**What `/setup` costs per test**, measured on happy-dom over 10 000 empty tests (2026-09-17, Node
|
|
2260
|
-
24): the default `setupAutoSpy()` **19 µs**, `guardGlobals` **59 µs** (it snapshots once per file
|
|
2261
|
-
now, not once per test), `documentPollution` **23 µs** — it shares the `onTestFinished` the teardown
|
|
2262
|
-
net registers anyway — and `preset: 'strict'` **67 µs**. `documentPollution: { nodes: true }` over a
|
|
2263
|
-
`<head>` of 1000 children is **0.19 ms** per test rather than 7.1 ms, because the children are walked
|
|
2264
|
-
as a sibling chain instead of through a live `HTMLCollection`; an `ignoreNodes` selector's answer for
|
|
2265
|
-
an element is decided at snapshot time and reused for that test.
|
|
2266
|
-
|
|
2267
|
-
### Hook order differs from Jest
|
|
2268
|
-
|
|
2269
|
-
Vitest runs `afterEach` hooks as a stack (innermost / last-registered first); Jest ran them in
|
|
2270
|
-
declaration order. A ported suite where a spec's `afterEach` depends on a patch the setup file
|
|
2271
|
-
installed needs `sequence: { hooks: 'list' }` in the Vitest config, or the setup file's teardown runs
|
|
2272
|
-
first and the spec's hook operates on an already-restored environment.
|
|
1541
|
+
<!-- /agents-outline -->
|
|
2273
1542
|
|
|
2274
1543
|
---
|
|
2275
1544
|
|
|
@@ -2278,13 +1547,13 @@ first and the spec's hook operates on an already-restored environment.
|
|
|
2278
1547
|
Under Jest these were hard to tell apart; under Vitest with a real bundler they are four separate
|
|
2279
1548
|
mechanisms, and a test that waits on the wrong one fails with a message that names none of them.
|
|
2280
1549
|
|
|
2281
|
-
| What is pending
|
|
2282
|
-
|
|
|
2283
|
-
| change detection
|
|
2284
|
-
| effects + `afterNextRender` + CD
|
|
2285
|
-
| timers, debounces, polling
|
|
2286
|
-
| a dynamic `import()`, native `async` in a dep
|
|
2287
|
-
| an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`)
|
|
1550
|
+
| What is pending | What drives it | What does **not** |
|
|
1551
|
+
| --- | --- | --- |
|
|
1552
|
+
| change detection | `fixture.detectChanges()` | anything `await`ed |
|
|
1553
|
+
| effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
|
|
1554
|
+
| timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
|
|
1555
|
+
| a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
|
|
1556
|
+
| an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
|
|
2288
1557
|
|
|
2289
1558
|
```ts
|
|
2290
1559
|
import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
|
|
@@ -2555,1316 +1824,38 @@ a fixture between the factory and the tests with `vi.hoisted()`.
|
|
|
2555
1824
|
|
|
2556
1825
|
## 13. Angular
|
|
2557
1826
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
`
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
cart: CartService,
|
|
2591
|
-
api: [ApiService, { returns: { get: of([]) } }],
|
|
2592
|
-
passcode: PASSCODE_TOKEN,
|
|
2593
|
-
});
|
|
2594
|
-
|
|
2595
|
-
test('checks out', async ({ cart }) => {
|
|
2596
|
-
cart.checkout.resolveWith(true);
|
|
2597
|
-
|
|
2598
|
-
await expect(cart.checkout(1)).resolves.toBe(true);
|
|
2599
|
-
});
|
|
2600
|
-
```
|
|
2601
|
-
|
|
2602
|
-
No `let cart: Spy<CartService>` that is `undefined` between tests, and a test that never destructures
|
|
2603
|
-
`api` never builds it. Entries are a class, `[Class, config]` with whatever `provideAutoSpy` takes,
|
|
2604
|
-
or an `InjectionToken` (built from the token's own type, like `provideAutoSpyForToken`). Extra
|
|
2605
|
-
providers — the component under test, a real service, `provideHttpClient()` — go in the third
|
|
2606
|
-
argument and are registered in the same call, ahead of the generated ones, so a token named there
|
|
2607
|
-
wins.
|
|
2608
|
-
|
|
2609
|
-
**It takes the whole map at once, and that is a `TestBed` constraint rather than a typing one.** The
|
|
2610
|
-
composing form — `base.extend('cart', …).extend('api', …)` — cannot work: fixtures resolve
|
|
2611
|
-
independently, so `cart` would configure the testing module _and_ inject, which instantiates it, and
|
|
2612
|
-
`api` would then reach `configureTestingModule` after instantiation and fail with Angular's own
|
|
2613
|
-
"Cannot configure the test module when the test module has already been instantiated". A `beforeEach`
|
|
2614
|
-
that configures the module further still composes, because it runs before any fixture resolves and
|
|
2615
|
-
repeated `configureTestingModule` calls merge right up until the first injection. A `beforeEach` that
|
|
2616
|
-
**injects** does not, and nothing can repair that from here.
|
|
2617
|
-
|
|
2618
|
-
The token may be an **abstract class** — `abstract class LocalStorage extends AbstractStorage {}`,
|
|
2619
|
-
the shape production provides with `useClass`. Its members are erased before they reach a prototype,
|
|
2620
|
-
so there is nothing to discover; the factory notices and returns the `createAutoMock` proxy, which
|
|
2621
|
-
answers every method of the declared type. `injectSpy(LocalStorage)` recognises it as an auto-spy
|
|
2622
|
-
and stays quiet.
|
|
2623
|
-
|
|
2624
|
-
**Seed the double in the provider, not in the `beforeEach` under it.** Both factories take both
|
|
2625
|
-
halves — `returns` for what a spied method answers, `overrides` for a member that is not a method
|
|
2626
|
-
result:
|
|
2627
|
-
|
|
2628
|
-
```ts
|
|
2629
|
-
provideAutoSpy(FavoritesService, {
|
|
2630
|
-
returns: { load: of([]) },
|
|
2631
|
-
overrides: { savedItemsChanged$: of(undefined), favoriteItems: [] },
|
|
2632
|
-
});
|
|
2633
|
-
|
|
2634
|
-
provideAutoSpyForToken(PRODUCTS, undefined, { returns: { getProducts: of([]), getById: of(null) } });
|
|
2635
|
-
```
|
|
2636
|
-
|
|
2637
|
-
A seeded `overrides` member is stored verbatim and is **no longer a spy**, so seed data there and
|
|
2638
|
-
name methods in `returns` when they must stay assertable. The reason to prefer this over a second
|
|
2639
|
-
statement is not brevity: the shortcut people take instead is an exported `const` provider carrying
|
|
2640
|
-
the values, and under `isolate: false` that is one set of spies shared by every file that imports
|
|
2641
|
-
it.
|
|
2642
|
-
|
|
2643
|
-
**A member named in both wins as the seed**, whatever the seed is — a value, a plain function, a
|
|
2644
|
-
`vi.fn()`. `returns` and `selfReturning` skip such a member rather than configuring it, which is the
|
|
2645
|
-
precedence the merge already applies between a registration and a call site: a spec seeding `channel`
|
|
2646
|
-
over `registerAutoSpyDefaults(LOGGER, { selfReturning: ['channel'] })` gets its own function back, not
|
|
2647
|
-
the registered link.
|
|
2648
|
-
|
|
2649
|
-
**`observablePropsToSpyOn` works on a token too**, and matters more there than on a class. A class
|
|
2650
|
-
tells the factory which members are methods; a type does not, so _every_ unnamed key of a
|
|
2651
|
-
token-driven double is a function spy — including an `Observable` property, which the code under
|
|
2652
|
-
test then subscribes to as if it were a function, failing far from the double:
|
|
2653
|
-
|
|
2654
|
-
```ts
|
|
2655
|
-
provideAutoSpyForToken(FAVORITES, undefined, { observablePropsToSpyOn: ['favorites$'] });
|
|
2656
|
-
// …
|
|
2657
|
-
injectSpy(FAVORITES).favorites$.nextWith([{ id: 1 }]);
|
|
2658
|
-
```
|
|
2659
|
-
|
|
2660
|
-
A member also named in `overrides` keeps its seed — hand the double a real `Subject` there when the
|
|
2661
|
-
spec drives the stream itself, and name it here when `nextWith` is what the spec wants. That is the
|
|
2662
|
-
same precedence the class-based factory uses. Before 3.5.0 this option existed only on the class
|
|
2663
|
-
path, and reaching a token with observable members meant going back to a hand-written double —
|
|
2664
|
-
which is exactly what `prefer-provide-auto-spy` and `prefer-create-spy-from-class` exist to prevent.
|
|
2665
|
-
|
|
2666
|
-
**Do not write a local `injectSpy`.** A wrapper of the shape
|
|
2667
|
-
`TestBed.inject(token as never) as Spy<T>` — a double assertion, typed
|
|
2668
|
-
`<T>(token: abstract new (...args: never[]) => T)` — is a common thing to find already in a
|
|
2669
|
-
repository, and the library's is strictly wider: it takes `ClassType<T>`, an `InjectionToken<T>` and
|
|
2670
|
-
an abstract constructor, warns when the injector hands back something that is not a spy, and has no
|
|
2671
|
-
assertion for the project's lint rules to argue with. Two functions with the same name and different
|
|
2672
|
-
signatures means the import order decides which one a file gets. Delete the local one, or re-export
|
|
2673
|
-
the library's under that name.
|
|
2674
|
-
|
|
2675
|
-
### Signals — which helper depends on whose signal it is
|
|
2676
|
-
|
|
2677
|
-
```ts
|
|
2678
|
-
// a DEPENDENCY's signal — name it, then configure the mock like any other
|
|
2679
|
-
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current'] });
|
|
2680
|
-
injectSpy(ProjectStore).current.mockReturnValue({ id: 1 });
|
|
2681
|
-
|
|
2682
|
-
// the CLASS UNDER TEST's own signal / computed / input — replace the field with a REAL signal
|
|
2683
|
-
mockReadonlyProp(component, 'selected', signal(true));
|
|
2684
|
-
mockReadonlyProp(component, 'items', signal([]));
|
|
2685
|
-
mockReadonlyProp(component, 'host', signal({ nativeElement: element }));
|
|
2686
|
-
|
|
2687
|
-
// a value that changes during the test — keep the signal, set it
|
|
2688
|
-
const selected = signal(false);
|
|
2689
|
-
|
|
2690
|
-
mockReadonlyProp(component, 'selected', selected);
|
|
2691
|
-
selected.set(true); // every computed reading it updates
|
|
2692
|
-
```
|
|
2693
|
-
|
|
2694
|
-
Pass a real `signal()`, not a `vi.fn()` returning a value — anything `computed()` downstream has to
|
|
2695
|
-
recompute, and only a real signal notifies it.
|
|
2696
|
-
|
|
2697
|
-
`mockSignalProp` is that pair in one call, and hands back the writable half:
|
|
2698
|
-
|
|
2699
|
-
```ts
|
|
2700
|
-
import { mockSignalProp } from 'vitest-auto-spy/angular';
|
|
2701
|
-
|
|
2702
|
-
const selected = mockSignalProp(component, 'selected', false);
|
|
2703
|
-
|
|
2704
|
-
selected.set(true); // every computed reading it updates
|
|
2705
|
-
```
|
|
2706
|
-
|
|
2707
|
-
Use it whenever the value has to change during the test. `mockReadonlyProp` stays right when the
|
|
2708
|
-
value is fixed for the whole test and you never need the handle.
|
|
2709
|
-
|
|
2710
|
-
A `protected` or TS-`private` signal is reached too: the key is checked against the public type
|
|
2711
|
-
first, and a key that type does not describe takes the value's own type instead —
|
|
2712
|
-
`mockSignalProp(component, 'saving', true)`. `mockResourceProp` works the same way. A public key never
|
|
2713
|
-
falls through to that form, so its value is still checked.
|
|
2714
|
-
|
|
2715
|
-
A `linkedSignal()` is a writable signal like any other, so `mockSignalProp` covers it and there is
|
|
2716
|
-
no separate helper to go looking for.
|
|
2717
|
-
|
|
2718
|
-
**An ngrx `signalStore()`'s state members go through `mockSignalProp` too**, on the store double —
|
|
2719
|
-
`mockSignalProp(store, 'currentAngle', null)`, then `.set(angle)` mid-test. A state slice is a
|
|
2720
|
-
signal the template reads during the first render; a method spy there answers `undefined`, and
|
|
2721
|
-
`mockImplementation(() => value())` is re-read by nothing downstream. A nullable object slice is
|
|
2722
|
-
typed `DeepSignal<A> | Signal<null>`: `overrides` accepts one `signal<A | null>()` for it, but only
|
|
2723
|
-
`mockSignalProp` hands back the handle.
|
|
2724
|
-
|
|
2725
|
-
**It patches as little as it can, and the order rule follows from that.** A member that is already
|
|
2726
|
-
writable — `signal()`, `model()`, `linkedSignal()` — is **written through**, not replaced: Angular
|
|
2727
|
-
links a consumer to the signal it read, not to the property it read it through, so replacing one
|
|
2728
|
-
that anything has already read strands every `computed()`, `effect()` and template binding on the
|
|
2729
|
-
old signal, silently and for the rest of the test. Writing through keeps all of them, keeps a
|
|
2730
|
-
`model()`'s output half alive, and leaves nothing for `restoreMockedProps()` to undo — the value
|
|
2731
|
-
stays where the spec left it.
|
|
2732
|
-
|
|
2733
|
-
**A `signal().asReadonly()` member counts as writable.** The view a class exposes is a different
|
|
2734
|
-
function from the signal behind it, but it is the same reactive node, so the value is set through the
|
|
2735
|
-
node and the member the class publishes keeps working — whenever it was first read. That is the
|
|
2736
|
-
common shape for a store's public state (`readonly items = this.#items.asReadonly()`), and it used to
|
|
2737
|
-
be swapped, which meant a `computed()` that had already read it never saw the new value.
|
|
2738
|
-
|
|
2739
|
-
One shape is still swapped: a `computed()` the class declares, and a member the double does not have
|
|
2740
|
-
yet. **Patch those before anything reads them** — before the first `detectChanges()` /
|
|
2741
|
-
`stable(fixture)`. A `computed()` a live consumer has already read is refused by name rather than
|
|
2742
|
-
replaced where nothing would notice it — the refusal now names `computed()` specifically, and says
|
|
2743
|
-
that driving the signal the computation reads is the way through. An `input()` is refused outright:
|
|
2744
|
-
Angular sets an input through the input node rather than the property, so a replaced one breaks the
|
|
2745
|
-
host's next write. Drive an input with `await setInputs(fixture, { … })`, or with
|
|
2746
|
-
`renderShallow(Component, { inputs: { … } })` for the value it starts at.
|
|
2747
|
-
|
|
2748
|
-
### Observers the component constructs itself
|
|
2749
|
-
|
|
2750
|
-
Do not assign `globalThis.IntersectionObserver` by hand: it stays assigned, and under
|
|
2751
|
-
`isolate: false` the next file inherits it.
|
|
2752
|
-
|
|
2753
|
-
```ts
|
|
2754
|
-
import { intersectionEntry, stubIntersectionObserver } from 'vitest-auto-spy/dom-stubs';
|
|
2755
|
-
|
|
2756
|
-
const observers = stubIntersectionObserver(); // also stubResizeObserver / stubMutationObserver
|
|
2757
|
-
|
|
2758
|
-
fixture.detectChanges(); // the component constructs it
|
|
2759
|
-
|
|
2760
|
-
observers.last.emit([intersectionEntry(element, true)]); // one batch, as the browser delivers it
|
|
2761
|
-
await fixture.whenStable();
|
|
2762
|
-
|
|
2763
|
-
expect(observers.last.disconnected).toBe(true); // after the component is destroyed
|
|
2764
|
-
```
|
|
2765
|
-
|
|
2766
|
-
`restoreMockedProps()` puts the real constructor back, so `setupAutoSpy()` covers the teardown.
|
|
2767
|
-
`observers.last` throws if the code under test constructed nothing — render first, and install the
|
|
2768
|
-
stub before the construction, not after.
|
|
2769
|
-
|
|
2770
|
-
Install it in `beforeEach`, **never in `beforeAll`**: a shared setup file's root `beforeEach` runs
|
|
2771
|
-
_after_ a file's `beforeAll`, so a stub installed there is overwritten by the setup file's default
|
|
2772
|
-
before the test starts. The symptom is `expected "vi.fn()" to be called 2 times, but got 0 times`,
|
|
2773
|
-
in a file where the mock class is ten lines above.
|
|
2774
|
-
|
|
2775
|
-
Three more knobs, each for a shape that otherwise gets hand-rolled:
|
|
2776
|
-
|
|
2777
|
-
```ts
|
|
2778
|
-
stubIntersectionObserver({ autoEmit: true }); // every observed target reports as visible, at once
|
|
2779
|
-
observers.last.options; // the init object: { rootMargin, threshold, … }
|
|
2780
|
-
observers.last.host; // the observer the code under test constructed — and its callback's 2nd argument
|
|
2781
|
-
observers.last.emit([mutationRecord(host, { addedNodes: [span] })]);
|
|
2782
|
-
observers.last.emit([resizeEntry(host, { width: 320 })]);
|
|
2783
|
-
```
|
|
2784
|
-
|
|
2785
|
-
**The callback's second argument is the observer the code under test holds**, not an internal
|
|
2786
|
-
record — so `(entries, observer) => observer.unobserve(entries[0].target)`, `observer.disconnect()`
|
|
2787
|
-
and `observer.takeRecords()` all reach the thing the spec drives, which is what production code
|
|
2788
|
-
written against the platform expects. `root`, `rootMargin` and `thresholds` read back off the init
|
|
2789
|
-
the way the platform normalises them (`null`, `'0px 0px 0px 0px'`, `[0]` by default) rather than as
|
|
2790
|
-
empty values.
|
|
2791
|
-
|
|
2792
|
-
`autoEmit` is the mode a suite ported from Jest needs: there the global mock fired its callback with
|
|
2793
|
-
`isIntersecting: true` immediately, so lazily-loading sections fetched their data during
|
|
2794
|
-
`detectChanges()`. Against the default inert observer those specs assert on an empty component and
|
|
2795
|
-
fail with something that has nothing to do with intersection.
|
|
2796
|
-
|
|
2797
|
-
`mutationRecord()` exists because a `MutationRecord` cannot be written as an object literal at all —
|
|
2798
|
-
`addedNodes` is a `NodeList`. Do not build one from a `DocumentFragment`: appending **moves** the
|
|
2799
|
-
nodes, so the helper silently rips the element out of the fixture it was just asserted on.
|
|
2800
|
-
|
|
2801
|
-
### A component's own `providers` win, and the symptom is nowhere near the cause
|
|
2802
|
-
|
|
2803
|
-
Worth reading before the rest of this section: it has now come up twice in one migration wave, and
|
|
2804
|
-
both times the failure landed in a different file from its cause.
|
|
2805
|
-
|
|
2806
|
-
`@Component({ providers: [DeleteAccountService] })` declares the provider on the **element**
|
|
2807
|
-
injector, and a module-level `provideAutoSpy(DeleteAccountService)` in `configureTestingModule`
|
|
2808
|
-
loses to it — so the component builds the **real** service. Nothing warns. What fails is whatever
|
|
2809
|
-
the real service touches first: in the observed case a logger, with
|
|
2810
|
-
`TypeError: Cannot read properties of undefined (reading 'pipe')`, which names neither the component
|
|
2811
|
-
nor the provider nor the spy.
|
|
2812
|
-
|
|
2813
|
-
Two things fix it, and which one depends on whether the double is wanted:
|
|
2814
|
-
|
|
2815
|
-
```ts
|
|
2816
|
-
// keep a double, but put it where the component will look
|
|
2817
|
-
const menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService);
|
|
2818
|
-
|
|
2819
|
-
// or take the component's own provider away, so the module-level one is reached again
|
|
2820
|
-
TestBed.overrideComponent(ProfileComponent, { remove: { providers: [DeleteAccountService] } });
|
|
2821
|
-
```
|
|
2822
|
-
|
|
2823
|
-
`overrideComponentProvider` is the one to reach for by default — it also queues the component with
|
|
2824
|
-
the TestBed compiler, which `overrideProvider` alone does not do. Reach for the `remove` form when
|
|
2825
|
-
the module already provides the spy and the component's declaration is simply in the way.
|
|
2826
|
-
|
|
2827
|
-
Under `renderShallow` the override goes in `beforeCreate`, which runs after the testing module is
|
|
2828
|
-
configured and before the component is created:
|
|
2829
|
-
|
|
2830
|
-
```ts
|
|
2831
|
-
let menu: Spy<NavigationBuilderService>;
|
|
2832
|
-
|
|
2833
|
-
renderShallow(CatalogPageComponent, {
|
|
2834
|
-
beforeCreate: () => {
|
|
2835
|
-
menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService);
|
|
2836
|
-
},
|
|
2837
|
-
});
|
|
2838
|
-
```
|
|
2839
|
-
|
|
2840
|
-
### `injectSpy` cannot reach a component-level provider
|
|
2841
|
-
|
|
2842
|
-
`injectSpy(X)` reads the **global** `TestBed` injector. A provider declared on the component
|
|
2843
|
-
(`@Component({ providers: [...] })`) lives in the element injector, which `TestBed.inject` never
|
|
2844
|
-
sees. Go through the fixture and re-view the result:
|
|
2845
|
-
|
|
2846
|
-
```ts
|
|
2847
|
-
const player = asSpy(fixture.debugElement.injector.get(PlayerService));
|
|
2848
|
-
|
|
2849
|
-
player.play.mockReturnValue(true);
|
|
2850
|
-
```
|
|
2851
|
-
|
|
2852
|
-
To _replace_ it rather than read it, the provider has to be overridden — and `provideAutoSpy` cannot
|
|
2853
|
-
do that, because a testing-module provider loses to one the component declares:
|
|
2854
|
-
|
|
2855
|
-
```ts
|
|
2856
|
-
import { overrideAutoSpy, overrideComponentProvider } from 'vitest-auto-spy/angular';
|
|
2857
|
-
|
|
2858
|
-
const menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService); // → Spy<NavigationBuilderService>
|
|
2859
|
-
|
|
2860
|
-
// or, when the component is already in the testing module:
|
|
2861
|
-
TestBed.configureTestingModule({ … }).overrideProvider(PaymentMethodService, overrideAutoSpy(PaymentMethodService));
|
|
2862
|
-
```
|
|
2863
|
-
|
|
2864
|
-
`overrideProvider(X, provideAutoSpy(X))` is **not** broken, contrary to what this section used to
|
|
2865
|
-
say: `provideAutoSpy` returns `{ provide, useValue }`, `overrideProvider` reads the `useValue` off
|
|
2866
|
-
it and ignores the extra `provide`, and the spy is installed. `overrideAutoSpy` is the right call
|
|
2867
|
-
because it says what it does and hands the spy back directly — not because the other form is a
|
|
2868
|
-
no-op.
|
|
2869
|
-
|
|
2870
|
-
The failure that is real: `overrideProvider` only reaches a component the TestBed compiler knows
|
|
2871
|
-
about, so a standalone component instantiated through a parent's template needs to be in `imports`
|
|
2872
|
-
first; `overrideComponentProvider` queues it.
|
|
2873
|
-
|
|
2874
|
-
**`overrideComponentProvider` verifies itself**, always — not behind a diagnostics flag. On the next
|
|
2875
|
-
`TestBed.createComponent` it asks the component's **own** injector for the token and throws when the
|
|
2876
|
-
answer is not the spy it created, naming the component, the token and what was resolved instead. It
|
|
2877
|
-
checks the **first** fixture only and stays silent when the component was not rendered (behind an
|
|
2878
|
-
`@if`, a different host), so a throwaway fixture created first means "not checked", not "wrong". A
|
|
2879
|
-
later `TestBed.overrideProvider(Token, …)` still wins; the check reports that but cannot prevent it.
|
|
2880
|
-
`overrideAutoSpy` carries no verification of its own.
|
|
2881
|
-
|
|
2882
|
-
Do **not** reach for `TestBed.overrideComponent` here — see the next subsection for why it is worse
|
|
2883
|
-
than the problem it solves.
|
|
2884
|
-
|
|
2885
|
-
### An NgModule that contributes nothing
|
|
2886
|
-
|
|
2887
|
-
Under an AOT test bundle (`@angular/build:unit-test`, and any builder that compiles specs the way it
|
|
2888
|
-
compiles production code) `ɵɵsetNgModuleScope` is stripped, because only the TestBed reads it. Every
|
|
2889
|
-
NgModule then has an empty `ɵmod.declarations` / `ɵmod.exports` at runtime. Nothing notices while
|
|
2890
|
-
AOT is in charge — the flat dependency list is already baked into each `ɵcmp` — but the moment the
|
|
2891
|
-
TestBed resolves a scope itself, through `imports: [SomeModule]` or through a JIT recompilation
|
|
2892
|
-
after `overrideComponent`, it resolves it from nothing:
|
|
2893
|
-
|
|
2894
|
-
```
|
|
2895
|
-
NG0303: Can't bind to 'appTruncate' since it isn't a known property of 'div'
|
|
2896
|
-
NG0301: Export of name 'focusable' not found!
|
|
2897
|
-
NG0304: 'ui-smart-row' is not a known element
|
|
2898
|
-
(nothing at all — an attribute directive simply never instantiates)
|
|
2899
|
-
```
|
|
2900
|
-
|
|
2901
|
-
None of the four names the module. Say so up front instead:
|
|
2902
|
-
|
|
2903
|
-
```ts
|
|
2904
|
-
import { assertNgModuleScopes } from 'vitest-auto-spy/angular';
|
|
2905
|
-
|
|
2906
|
-
assertNgModuleScopes(DirectivesModule, PipesModule); // throws, naming the module and the cause
|
|
2907
|
-
```
|
|
2908
|
-
|
|
2909
|
-
Then declare what the spec needs in the TestBed module directly. Pass only modules you expect to
|
|
2910
|
-
bring declarations — a providers-only module is legitimately empty.
|
|
2911
|
-
|
|
2912
|
-
### A component whose own definition has a hole in it
|
|
2913
|
-
|
|
2914
|
-
The same bundle, one level down. A component's `providers`, `viewProviders` and compiled scope are
|
|
2915
|
-
**baked into `ɵcmp` when its module executes**, not read at `createComponent` time — so a barrel
|
|
2916
|
-
split into a chunk that has not run yet produces a definition with `undefined` in those lists.
|
|
2917
|
-
Angular finds out much later, from inside itself, with a stack that names neither the barrel nor the
|
|
2918
|
-
component:
|
|
2919
|
-
|
|
2920
|
-
```
|
|
2921
|
-
TypeError: Cannot read properties of undefined (reading 'provide')
|
|
2922
|
-
❯ resolveProvider render3/di_setup.ts:95
|
|
2923
|
-
```
|
|
2924
|
-
|
|
2925
|
-
Both obvious cures fail: `await import('@scope/lib')` at the top of `beforeEach` is already too late,
|
|
2926
|
-
and a static import at the top of the spec does not fix the order this bundler emits. Worse, the spec
|
|
2927
|
-
that breaks is one nobody touched — chunk boundaries move with file _contents_, so editing a type
|
|
2928
|
-
next door is enough.
|
|
2929
|
-
|
|
2930
|
-
```ts
|
|
2931
|
-
import { assertComponentDefIntact } from 'vitest-auto-spy/angular';
|
|
2932
|
-
|
|
2933
|
-
assertComponentDefIntact(HoverMenuComponent); // HoverMenuComponent.ɵcmp.providers[0] is undefined
|
|
2934
|
-
const fixture = TestBed.createComponent(HoverMenuComponent);
|
|
2935
|
-
```
|
|
2936
|
-
|
|
2937
|
-
It walks the three lists, nested arrays and forward-reference thunks included, and answers the
|
|
2938
|
-
related `Cannot read properties of undefined (reading 'ɵcmp')` from `imports: [Cmp]` as well — there
|
|
2939
|
-
the class reference itself never arrived, and the message names the argument position. Directives
|
|
2940
|
-
(`ɵdir`) are checked the same way. Neither this nor `assertNgModuleScopes` fixes the build; both
|
|
2941
|
-
replace a stack inside `@angular/core` with a line naming what is missing.
|
|
2942
|
-
|
|
2943
|
-
### Four silent failures, as one setup line
|
|
2944
|
-
|
|
2945
|
-
```ts
|
|
2946
|
-
// vitest.setup.ts — AFTER getTestBed().initTestEnvironment(…), because Vitest runs
|
|
2947
|
-
// afterEach hooks in reverse registration order and this one must run before the teardown.
|
|
2948
|
-
import { enableAngularDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
|
|
2949
|
-
|
|
2950
|
-
enableAngularDiagnostics(); // { ngModuleScopes, deadSchemas, unspiedProviders, pendingRequests, shadowedProviders }
|
|
2951
|
-
```
|
|
2952
|
-
|
|
2953
|
-
| Member | Fails when |
|
|
2954
|
-
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2955
|
-
| `ngModuleScopes` | a testing module imports an NgModule that contributes nothing at runtime |
|
|
2956
|
-
| `deadSchemas` | `schemas` sit next to a standalone component (`declarations` empty) — a no-op |
|
|
2957
|
-
| `unspiedProviders` | `injectSpy` got a real instance; a `console.warn` alone, a throw under the group |
|
|
2958
|
-
| `pendingRequests` | the test ended with unflushed `HttpTestingController` requests, named one by one; `{ ignoreCancelled: true }` forgives the ones the code under test cancelled |
|
|
2959
|
-
| `shadowedProviders` | a double on the testing module loses to the component's own `providers` |
|
|
2960
|
-
|
|
2961
|
-
Every member defaults to `true`, takes `false` to opt out, and a second call **replaces** the
|
|
2962
|
-
selection (safe from anywhere, including inside a test). `disableAngularDiagnostics()` turns the
|
|
2963
|
-
group off and leaves the TestBed timing instrumentation alone. `assertNoShadowedProviders(Component, fixture)`
|
|
2964
|
-
and `assertNoPendingRequests({ ignoreCancelled }?)` are the same checks on their own, for a spec that renders through a
|
|
2965
|
-
helper of its own or wants the HTTP one mid-test — reading takes the requests, so the group will not
|
|
2966
|
-
re-report what you inspected.
|
|
2967
|
-
|
|
2968
|
-
Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
|
|
2969
|
-
`provideHttpClientTesting()` / `HttpClientTestingModule` — including one reached through a nested
|
|
2970
|
-
module, which is the usual `imports: [SharedTestingModule]` shape and used to make `pendingRequests`
|
|
2971
|
-
inert without saying so — and a project using neither is genuinely inert. `ngModuleScopes` only fires
|
|
2972
|
-
on a module that contributes **nothing at all** — a providers-only module is legitimately
|
|
2973
|
-
scope-empty, so a stripped scope that still has providers passes silently; hand-call
|
|
2974
|
-
`assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
|
|
2975
|
-
not fire when `declarations` is non-empty.
|
|
2976
|
-
|
|
2977
|
-
**`getTestBed()` is covered as well as `TestBed`.** The instrumentation sits on the TestBed
|
|
2978
|
-
_instance_ every static delegates to, so the configuration inspectors, `shadowedProviders` and the
|
|
2979
|
-
`overrideComponentProvider` verification all see `getTestBed().configureTestingModule(…)` and
|
|
2980
|
-
`getTestBed().createComponent(…)` — the shape a setup helper of your own usually writes — and
|
|
2981
|
-
`overrideTemplate` is counted with the rest of the overrides. A suite on `getTestBed()` used to get
|
|
2982
|
-
silence from every one of these.
|
|
2983
|
-
|
|
2984
|
-
Two more things now judge what Angular ends up with rather than one call: `deadSchemas` tallies
|
|
2985
|
-
`schemas` and `declarations` across **every** `configureTestingModule` of a test, because Angular
|
|
2986
|
-
accumulates them — a schema added in a second call next to a component declared in the first is no
|
|
2987
|
-
longer reported, and the reverse order is no longer silently passed — and a test that resets the
|
|
2988
|
-
testing module twice reports the requests of every module it built, not only the last.
|
|
2989
|
-
|
|
2990
|
-
`enableAngularDiagnostics()` and `mockSignalProp()` both check once per worker that the Angular
|
|
2991
|
-
internals they read are still there, and **throw** naming the installed `@angular/core` version when
|
|
2992
|
-
one has moved. Nothing about that is fixable from a spec: the throw tells you which shape went and
|
|
2993
|
-
what stopped working, and the repair is a release of this package.
|
|
2994
|
-
|
|
2995
|
-
### `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
|
|
2996
|
-
|
|
2997
|
-
```ts
|
|
2998
|
-
import { expectRequest, provideHttpTesting } from 'vitest-auto-spy/angular-http';
|
|
2999
|
-
|
|
3000
|
-
TestBed.configureTestingModule({ providers: [...provideHttpTesting()] });
|
|
3001
|
-
|
|
3002
|
-
const products = TestBed.runInInjectionContext(() => httpResource<Product[]>(() => '/api/products'));
|
|
3003
|
-
|
|
3004
|
-
await expectRequest('/api/products').flush([product]);
|
|
3005
|
-
|
|
3006
|
-
expect(products.value()).toEqual([product]); // no tick, no microtask, no detectChanges
|
|
3007
|
-
```
|
|
3008
|
-
|
|
3009
|
-
`expectRequest` does the six steps a zoneless HTTP spec otherwise writes by hand: tick (an
|
|
3010
|
-
`httpResource()` created in an injection context has issued **nothing** until something does), inject
|
|
3011
|
-
the `HttpTestingController`, find the one matching request, flush it, let one microtask run so the
|
|
3012
|
-
response reaches the resource, tick again so the view reading it is current. Miss the first and
|
|
3013
|
-
`expectOne` reports a request that was never sent; miss the fifth and the assertion reads the
|
|
3014
|
-
resource's **default** value and passes.
|
|
3015
|
-
|
|
3016
|
-
| Call | Does |
|
|
3017
|
-
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3018
|
-
| `provideHttpTesting(opts?)` | `provideHttpClient()` + `provideHttpClientTesting()`; `{ verifyOnTeardown }` defaults to `true`, and takes `{ ignoreCancelled }` instead of `false` |
|
|
3019
|
-
| `expectRequest(matcher, opts?)` | tick, then the one match — `.request`, `.flush(body, opts?)`, `.error(status, opts?)` |
|
|
3020
|
-
| `expectNoRequest(matcher?, opts?)` | tick, then assert nothing matched; no argument means "nothing at all was requested" |
|
|
3021
|
-
| `verifyNoPendingRequests(opts?)` | the teardown check by hand, for mid-test use or a suite with `verifyOnTeardown: false`; `{ ignoreCancelled: true }` forgives a request the code under test unsubscribed from |
|
|
3022
|
-
|
|
3023
|
-
`matcher` is a URL (matched against either `url` or `urlWithParams`), a `RegExp` over
|
|
3024
|
-
`urlWithParams`, or a predicate `(request) => boolean`. `{ method }` is case-insensitive.
|
|
3025
|
-
`flush()` and `error()` are `async` because settling requires letting a microtask run — `await` them,
|
|
3026
|
-
and the next line reads the settled value.
|
|
3027
|
-
|
|
3028
|
-
Three things to know before reaching for it:
|
|
3029
|
-
|
|
3030
|
-
- **It does not re-export the core** — nor does `/angular-router`; every other subpath does. It is a
|
|
3031
|
-
companion to `vitest-auto-spy/angular`, which stays the import for spies, `TestBed` helpers and
|
|
3032
|
-
`settleResource`.
|
|
3033
|
-
Staying narrow is what keeps `@angular/common` — an **optional** peer — out of every other entry.
|
|
3034
|
-
- **`settleResource` is unchanged and still the answer** whenever the wait is not tied to one
|
|
3035
|
-
request: a `resource()` with an async loader, an `rxResource`, a reload, anything not HTTP.
|
|
3036
|
-
- **`enableAngularDiagnostics({ pendingRequests })` is unchanged and cooperates.** Both take the open
|
|
3037
|
-
requests with the one-shot `match(() => true)`, so an unanswered request is reported once. For a
|
|
3038
|
-
suite using `provideHttpTesting()` everywhere the diagnostic is redundant; keep it on while any
|
|
3039
|
-
file still configures HTTP testing by hand.
|
|
3040
|
-
|
|
3041
|
-
`provideHttpTesting({ verifyOnTeardown })` reads which test is running off the runner's own state,
|
|
3042
|
-
and where that state is absent — `bun:test`, `node:test`, anything that is not Vitest — the
|
|
3043
|
-
end-of-test check cannot arm. It now says so once per worker instead of checking nothing quietly;
|
|
3044
|
-
call `verifyNoPendingRequests()` yourself there.
|
|
3045
|
-
|
|
3046
|
-
### `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
|
|
3047
|
-
|
|
3048
|
-
```ts
|
|
3049
|
-
import { injectActivatedRoute, provideActivatedRoute } from 'vitest-auto-spy/angular-router';
|
|
3050
|
-
|
|
3051
|
-
TestBed.configureTestingModule({
|
|
3052
|
-
providers: [provideActivatedRoute({ params: { id: '7' }, queryParams: { tab: 'reviews' } })],
|
|
3053
|
-
});
|
|
3054
|
-
|
|
3055
|
-
const fixture = TestBed.createComponent(ProductPage);
|
|
3056
|
-
|
|
3057
|
-
injectActivatedRoute().setParams({ id: '8' }); // params + paramMap emit; snapshot.params already agrees
|
|
3058
|
-
fixture.detectChanges();
|
|
3059
|
-
```
|
|
3060
|
-
|
|
3061
|
-
**Do not hand-write an `ActivatedRoute` double, and do not `provideAutoSpy(ActivatedRoute)`.** Its
|
|
3062
|
-
streams and its snapshot are instance fields: the spy has neither, and a `useValue` has whichever half
|
|
3063
|
-
its author thought of — `route.paramMap.pipe(…)` throws on the `{ snapshot }` one, `route.snapshot`
|
|
3064
|
-
is `undefined` on the `{ params: of(…) }` one, and a spec that updates one half by hand tests a route
|
|
3065
|
-
no navigation produces. `provideActivatedRoute(init)` provides **Angular's own `ActivatedRoute`**
|
|
3066
|
-
built over one record, so every stream, both `ParamMap`s and `snapshot` read the same values.
|
|
3067
|
-
|
|
3068
|
-
| Call | Does |
|
|
3069
|
-
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3070
|
-
| `provideActivatedRoute(init?)` | a `FactoryProvider` — a fresh route per injector; `init`: `params`, `queryParams`, `data`, `title`, `fragment`, `url`, `outlet`, `component`, `routeConfig`, `resolve` |
|
|
3071
|
-
| `injectActivatedRoute(injector?)` | the handle: `.route`, `setParams`, `setQueryParams`, `setData`, `setFragment`, `setUrl`, `set({ … })` |
|
|
3072
|
-
| `createActivatedRoute(init?)` | the same handle without a `TestBed` — for `new Page(route)`, or a guard given `route.snapshot` |
|
|
3073
|
-
|
|
3074
|
-
A setter behaves like the router after a navigation: it **replaces** (not merges) that part, builds
|
|
3075
|
-
a new snapshot first, then emits only the streams whose value changed (router equality: same keys,
|
|
3076
|
-
arrays as sorted sets), in the order `queryParams`, `fragment`, `params`, `url`, `data`. Setting
|
|
3077
|
-
an equal value emits nothing; a string `url` is split on `/`.
|
|
3078
|
-
|
|
3079
|
-
Four things to know:
|
|
3080
|
-
|
|
3081
|
-
- **List it after `provideRouter()`** — the later provider of `ActivatedRoute` wins, and
|
|
3082
|
-
`injectActivatedRoute()` throws naming the one that did. With the real router present, the double
|
|
3083
|
-
still works as `relativeTo` for `router.createUrlTree`.
|
|
3084
|
-
- **One node, no tree.** `root` is the route itself, `parent` / `firstChild` are `null`. For
|
|
3085
|
-
`route.parent.params` patch that member with `mockReadonlyProp`, or use `RouterTestingHarness`.
|
|
3086
|
-
- **`title` is given, not resolved; no navigation, no input binding.** `provideActivatedRoute({
|
|
3087
|
-
title: 'Product 7' })` answers `route.snapshot.title` — the double puts the string under the
|
|
3088
|
-
router's own `RouteTitleKey`, read off the installed router, and does not run a `title: () => …`
|
|
3089
|
-
resolver. `Router.navigate` does not move it (the setters do), and `withComponentInputBinding()`
|
|
3090
|
-
is the outlet's job (`fixture.componentRef.setInput`).
|
|
3091
|
-
- **Its own entry, like `/angular-http`.** It does not re-export the core, imports no runner, and is
|
|
3092
|
-
the only file that reaches `@angular/router` — an optional peer. The `Location` double below wraps
|
|
3093
|
-
`@angular/common/testing`, which `@angular/router` itself depends on, so it adds no peer.
|
|
3094
|
-
|
|
3095
|
-
### `Router` from one URL — `vitest-auto-spy/angular-router`
|
|
3096
|
-
|
|
3097
|
-
```ts
|
|
3098
|
-
import { injectRouterDouble, provideRouterDouble } from 'vitest-auto-spy/angular-router';
|
|
3099
|
-
|
|
3100
|
-
TestBed.configureTestingModule({ providers: [provideRouterDouble({ url: '/products/7' })] });
|
|
3101
|
-
|
|
3102
|
-
const router = injectRouterDouble();
|
|
3103
|
-
|
|
3104
|
-
router.emitNavigation('/products/8'); // events emits a NavigationEnd; router.url already reads it
|
|
3105
|
-
expect(router.navigate).toHaveBeenCalledWith(['/checkout']);
|
|
3106
|
-
```
|
|
3107
|
-
|
|
3108
|
-
**Do not hand-write `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate:
|
|
3109
|
-
Promise.resolve(true) } })`** — the line 48 hand-rolled providers across two private suites all
|
|
3110
|
-
converged on, and every part of it is a guess the component falls through: `of()` never emits again,
|
|
3111
|
-
`url` is a string nobody updates, `routerState` is absent, `serializeUrl` throws on the first
|
|
3112
|
-
redirect a guard builds. `provideRouterDouble(init)` keeps one URL and derives the rest.
|
|
3113
|
-
|
|
3114
|
-
| Call | Does |
|
|
3115
|
-
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
3116
|
-
| `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
|
|
3117
|
-
| `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)`, `setCurrentNavigation(navigation?)` |
|
|
3118
|
-
| `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
|
|
3119
|
-
| `collectRouterEvents(events)` | the recording of what `router.events` emits, `expect([[Class, url], …])` in one line — Angular's own integration-spec idiom |
|
|
3120
|
-
|
|
3121
|
-
Unlike the route, this is **not** an instance of Angular's class: a real `Router` drags the whole
|
|
3122
|
-
routing stack in. It is a structural double that answers `url` (serialized as the real router does),
|
|
3123
|
-
`events` (a `BehaviorSubject` starting at the `NavigationEnd` that put it there), `navigate` and
|
|
3124
|
-
`navigateByUrl` (spies resolving `true`), `serializeUrl` / `parseUrl` / `createUrlTree` (the
|
|
3125
|
-
router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`), `routerState`
|
|
3126
|
-
(Angular's own, its `snapshot.url` the URL) and `currentNavigation` / `getCurrentNavigation()` (the
|
|
3127
|
-
navigation in flight, `null` while the router stands still). **Every other member `Router` declares
|
|
3128
|
-
throws by name** rather than reading `undefined` — and that holds for the instance fields too, which
|
|
3129
|
-
is why a hand-rolled double needs `instanceMethodsToSpyOn: ['currentNavigation']` and this one does
|
|
3130
|
-
not.
|
|
3131
|
-
|
|
3132
|
-
Five things to know:
|
|
3133
|
-
|
|
3134
|
-
- **It does not navigate.** `navigate()` records the call and resolves `true`; it does not move
|
|
3135
|
-
`url`. `setUrl()` and `emitNavigation()` move it; `RouterTestingHarness` over a real
|
|
3136
|
-
`provideRouter()` is what tests a navigation.
|
|
3137
|
-
- **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
|
|
3138
|
-
`NavigationEnd` moves the URL with it; any other event does not. It resolves once the event is
|
|
3139
|
-
delivered and a navigation it ended is back to `null` — the moment `navigate()` resolves in an
|
|
3140
|
-
application; the work is synchronous, so ignoring the promise changes nothing.
|
|
3141
|
-
- **The navigation in flight follows the events, and a terminal event ends it _after_ delivering
|
|
3142
|
-
it.** `setCurrentNavigation({ extras: { state } })` puts one up and `setCurrentNavigation(null)`
|
|
3143
|
-
ends it; a `NavigationStart` pushed through `emitNavigation()` starts one with that event's id,
|
|
3144
|
-
URL and trigger, and a `NavigationEnd`, `NavigationCancel`, `NavigationError` or
|
|
3145
|
-
`NavigationSkipped` drops it back to `null` once its subscribers have run. Angular's doc comment
|
|
3146
|
-
— "the current navigation becomes to null after the NavigationEnd event is emitted" — means
|
|
3147
|
-
_after_ literally: the router emits the terminal event from a `tap` with the navigation still in
|
|
3148
|
-
flight and clears it in the `finalize` below, and `events` is a Subject, so a synchronous
|
|
3149
|
-
subscriber runs between the two. **A component that reads `currentNavigation()` while handling a
|
|
3150
|
-
`NavigationEnd` gets the navigation that just finished**, here and in production; it reads `null`
|
|
3151
|
-
only once `navigate()` has resolved. Probed against a real `provideRouter()` on Angular 22.
|
|
3152
|
-
- **Ordering does not matter here.** `Router` is `providedIn: 'root'` and `provideRouter()` does not
|
|
3153
|
-
re-provide the token, so the double wins in either order — and a `TestBed` without it hands out a
|
|
3154
|
-
real router, which is what `injectRouterDouble()` says by name.
|
|
3155
|
-
- **It builds spies**, so a runtime entry has to be imported in the suite (`vitest-auto-spy/angular`
|
|
3156
|
-
in the same file, or the setup file); the route double needs none.
|
|
3157
|
-
|
|
3158
|
-
### `Location` from Angular's own testing classes — `vitest-auto-spy/angular-router`
|
|
3159
|
-
|
|
3160
|
-
```ts
|
|
3161
|
-
import { injectLocationDouble, provideLocationDouble } from 'vitest-auto-spy/angular-router';
|
|
3162
|
-
|
|
3163
|
-
TestBed.configureTestingModule({ providers: [provideLocationDouble()] });
|
|
3164
|
-
|
|
3165
|
-
const location = injectLocationDouble();
|
|
3166
|
-
|
|
3167
|
-
location.go('/reports/7');
|
|
3168
|
-
location.simulateUrlPop('/'); // the popstate no method call can cause
|
|
3169
|
-
```
|
|
3170
|
-
|
|
3171
|
-
**A wrap, not a rival.** Angular ships the double — `SpyLocation` with a real history array, a
|
|
3172
|
-
`urlChanges` journal for assertions, and `simulateUrlPop()` / `simulateHashChange()` for the
|
|
3173
|
-
browser's half of the contract — and this entry puts it in the family shape of the route and the
|
|
3174
|
-
router: one provider call, an `injectLocationDouble()` whose errors name the instance that won
|
|
3175
|
-
(`Location` is `providedIn: 'root'`, so the quiet failure is the real `Location` nobody configured).
|
|
3176
|
-
`createLocationDouble()` is the same without a `TestBed`. One asymmetry worth knowing: `back()` and
|
|
3177
|
-
`forward()` wake the popstate subscribers but do not write `urlChanges` — the journal holds what the
|
|
3178
|
-
app asked for, the subscribers carry what the browser did. And one deliberate difference from
|
|
3179
|
-
Angular's `SpyLocation`: `path()` keeps the query of `go(path, query)` / `replaceState(path, query)`
|
|
3180
|
-
(`/reports?tab=7`), as the real `Location.path()` does — `SpyLocation`'s own drops it.
|
|
3181
|
-
|
|
3182
|
-
### Signal forms — `vitest-auto-spy/signal-forms`
|
|
3183
|
-
|
|
3184
|
-
```ts
|
|
3185
|
-
import { minLength, required } from '@angular/forms/signals';
|
|
3186
|
-
import { createForm, registerFormMatchers } from 'vitest-auto-spy/signal-forms';
|
|
3187
|
-
|
|
3188
|
-
registerFormMatchers(); // once, in the setup file
|
|
3189
|
-
|
|
3190
|
-
const user = createForm({ email: '', name: '' }, (path) => {
|
|
3191
|
-
required(path.email, { message: 'Email is required' });
|
|
3192
|
-
minLength(path.name, 2);
|
|
3193
|
-
});
|
|
3194
|
-
|
|
3195
|
-
expect(user.email).toHaveFieldErrors([{ kind: 'required', message: 'Email is required' }]);
|
|
3196
|
-
|
|
3197
|
-
user.email().value.set('ada@example.test');
|
|
3198
|
-
|
|
3199
|
-
expect(user.email).toHaveFieldErrors([]);
|
|
3200
|
-
```
|
|
3201
|
-
|
|
3202
|
-
Signal forms are stable from Angular 22, and a spec that touches one meets the same two things every
|
|
3203
|
-
time. **`form()` injects**, so a call in a `beforeEach` throws `NG0203: The Injector token injection
|
|
3204
|
-
failed` — a message about `inject()` that never says "form", whose repair is
|
|
3205
|
-
`{ injector: TestBed.inject(Injector) }` or a `TestBed.runInInjectionContext` around the call.
|
|
3206
|
-
**`errors()` answers `RequiredValidationError` instances**, each carrying a `fieldTree`
|
|
3207
|
-
back-reference, so `toEqual([{ kind: 'required' }])` fails on a property nobody wrote and every
|
|
3208
|
-
suite falls back to `errors().some((error) => error.kind === 'required')` — which passes just as
|
|
3209
|
-
happily when the field has three other errors nobody expected.
|
|
3210
|
-
|
|
3211
|
-
| Call | Does |
|
|
3212
|
-
| --------------------------------------------- | ------------------------------------------------------------------------- |
|
|
3213
|
-
| `createForm(model, schema?, options?)` | Angular's own `form()`, built in the `TestBed`'s injection context |
|
|
3214
|
-
| `createForm(initialValue, schema?, options?)` | the same, with the model signal made for you |
|
|
3215
|
-
| `registerFormMatchers()` | adds `expect(field).toHaveFieldErrors(kinds)` — the whole set, order-free |
|
|
3216
|
-
|
|
3217
|
-
Four things to know:
|
|
3218
|
-
|
|
3219
|
-
- **What comes back is Angular's `FieldTree`**, not a double: the states, the validators and the
|
|
3220
|
-
write-through to the model are the framework's. A spec that passes its own `signal()` asserts on it
|
|
3221
|
-
directly; a plain value gets a signal made here, and `user().value()` reads it back either way.
|
|
3222
|
-
- **A `computed()` is refused by name** — a form writes into its model, and without the check a
|
|
3223
|
-
derived signal would be wrapped as a value while every write vanished.
|
|
3224
|
-
- **`options.injector` is for the validator that injects**: pass `fixture.debugElement.injector`
|
|
3225
|
-
when the service a schema reaches for lives in a component's own `providers`.
|
|
3226
|
-
- **`toHaveFieldErrors` compares the whole set**, by `kind`, and by `message` only where the spec
|
|
3227
|
-
names one; `[]` is "no errors". A field tree and its state both read, so `expect(user.email)` and
|
|
3228
|
-
`expect(user.email())` are the same assertion.
|
|
3229
|
-
|
|
3230
|
-
**No `FieldTree` double is shipped, on purpose.** It would stand in for a form a component takes as
|
|
3231
|
-
an input, and across the measured suites no component takes one: forms are built by the component
|
|
3232
|
-
that owns them (assert through `component.form.tags()`), and custom controls — `FormValueControl<T>`,
|
|
3233
|
-
`FormCheckboxControl` — are plain components whose contract is `value = model<T>()`, which is
|
|
3234
|
-
`renderShallow` plus `setInputs`.
|
|
3235
|
-
|
|
3236
|
-
### `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
|
|
3237
|
-
|
|
3238
|
-
```ts
|
|
3239
|
-
import { provideDocumentDouble, provideWindowDouble } from 'vitest-auto-spy/angular/doubles';
|
|
3240
|
-
|
|
3241
|
-
TestBed.configureTestingModule({
|
|
3242
|
-
providers: [
|
|
3243
|
-
provideWindowDouble(WINDOW, { screen: { width: 1920, height: 1080 }, devicePixelRatio: 2 }),
|
|
3244
|
-
provideDocumentDouble({ visibilityState: 'hidden' }),
|
|
3245
|
-
],
|
|
3246
|
-
});
|
|
3247
|
-
```
|
|
3248
|
-
|
|
3249
|
-
**Do not write `useValue: window`, and do not build a `mockDocument` from scratch.** These are the
|
|
3250
|
-
two most hand-rolled providers in an Angular suite — 95 `window` ones and 70 `document` ones across
|
|
3251
|
-
two private suites — and both hand-written shapes fail the same way. `useValue: window` isolates
|
|
3252
|
-
nothing: what the test writes stays on the global for the rest of the worker. A double built from
|
|
3253
|
-
scratch, whether a slice (`{ screen: { width: 1280, height: 720 } }`) or a `mockDocument` with one
|
|
3254
|
-
hand-written `querySelector`, knows only the members its author thought of, so the component that
|
|
3255
|
-
reads `screen.colorDepth` or calls `document.createElement` gets `undefined` and the spec fails
|
|
3256
|
-
somewhere unrelated to what it was testing.
|
|
3257
|
-
|
|
3258
|
-
These two merge the overrides **over the real jsdom object** instead: everything the spec did not
|
|
3259
|
-
name — `location.href`, `getComputedStyle`, `addEventListener`, `document.createElement` — still
|
|
3260
|
-
answers the way jsdom answers it.
|
|
3261
|
-
|
|
3262
|
-
| Call | Does |
|
|
3263
|
-
| --------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
3264
|
-
| `provideWindowDouble(token, overrides?)` | a `FactoryProvider` under the application's own window token |
|
|
3265
|
-
| `provideDocumentDouble(overrides?, token?)` | the same under Angular's `DOCUMENT`, or under a token of your own |
|
|
3266
|
-
| `createWindowDouble(overrides?)` / `createDocumentDouble(overrides?)` | the same doubles without a `TestBed` |
|
|
3267
|
-
|
|
3268
|
-
Five things to know:
|
|
3269
|
-
|
|
3270
|
-
- **The window helper needs your token.** Angular ships `DOCUMENT` (from `@angular/core` itself
|
|
3271
|
-
since v20) and has never shipped a `WINDOW`: every application declares its own
|
|
3272
|
-
`InjectionToken<Window>`. The helper is generic over the token's type, so an
|
|
3273
|
-
`InjectionToken<AppWindow>` has that interface's members checked in the overrides too.
|
|
3274
|
-
- **A plain `{ … }` merges, anything else replaces.** `{ screen: { width: 1920 } }` keeps
|
|
3275
|
-
`screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole. The
|
|
3276
|
-
slice goes three levels down, the way the merge does — `{ document: { location: { href: '' } } }` is
|
|
3277
|
-
checked and merged member by member.
|
|
3278
|
-
- **Nothing to restore.** The globals are never patched — the double is a view over them, and every
|
|
3279
|
-
write and delete lands on the view, which is also how a spec moves a value mid-test:
|
|
3280
|
-
`Object.assign(TestBed.inject(WINDOW), { scrollY: 40 })` (`Object.assign` because lib.dom declares
|
|
3281
|
-
most of `Window` `readonly`). There is no handle and no `restoreMockedProps()` in this recipe.
|
|
3282
|
-
- **`provideDocumentDouble` hands the double to Angular as well** — the renderer injects `DOCUMENT`
|
|
3283
|
-
too, so replace `createElement` or `body` only where the spec means to.
|
|
3284
|
-
- **`location` takes overrides like everything else**, though the platform declares its members
|
|
3285
|
-
unforgeable: `{ location: { reload: vi.fn() } }` — the shape most hand-written window mocks are —
|
|
3286
|
-
records the reload, leaves `location.href` real, and `win.location.href = '/next'` moves the
|
|
3287
|
-
double rather than the address bar. `mockValueProp(TestBed.inject(WINDOW), 'innerWidth', 800)` and
|
|
3288
|
-
its restore work on the double too, and neither touches the global.
|
|
3289
|
-
- **Constructors come out unbound.** `win.Date`, `win.Promise`, `win.Object`, `win.Event` are handed
|
|
3290
|
-
back as themselves rather than through a binding that loses the statics, so `win.Date.now()`,
|
|
3291
|
-
`win.Promise.resolve()`, `win.Object.keys(x)` and `new win.Event('x')` work, and
|
|
3292
|
-
`win.Event === window.Event` holds — which is what `instanceof` in production code needs.
|
|
3293
|
-
|
|
3294
|
-
### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
|
|
3295
|
-
|
|
3296
|
-
```ts
|
|
3297
|
-
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|
3298
|
-
import { expectEmission } from 'vitest-auto-spy/angular';
|
|
3299
|
-
import { injectMatDialogRef, provideMatDialogData, provideMatDialogRef } from 'vitest-auto-spy/angular/doubles';
|
|
3300
|
-
|
|
3301
|
-
TestBed.configureTestingModule({
|
|
3302
|
-
providers: [provideMatDialogData<EditUserData>(MAT_DIALOG_DATA, { id: 7, name: 'Ada' }), provideMatDialogRef(MatDialogRef)],
|
|
3303
|
-
});
|
|
3304
|
-
|
|
3305
|
-
const dialog = injectMatDialogRef(MatDialogRef);
|
|
3306
|
-
|
|
3307
|
-
TestBed.createComponent(EditUserDialog).componentInstance.save();
|
|
3308
|
-
|
|
3309
|
-
expect(dialog.close).toHaveBeenCalledWith('saved');
|
|
3310
|
-
await expect(expectEmission(dialog.ref.afterClosed())).resolves.toBe('saved');
|
|
3311
|
-
```
|
|
3312
|
-
|
|
3313
|
-
**`@angular/material` is not a dependency of this package and must not become one** — it ships no
|
|
3314
|
-
runtime dependencies at all. That is exactly why the token and the ref class are **arguments**:
|
|
3315
|
-
`MatDialogRef` is the DI token, the shape the double is measured against and the type the result is
|
|
3316
|
-
read off, so the spec's own Material import stays the only place it is named.
|
|
3317
|
-
|
|
3318
|
-
**Do not hand-roll `{ close: vi.fn() }`.** 36 of these across two private suites are the same three
|
|
3319
|
-
shapes, and the ref one fails twice. `close` is the only member anybody writes, so a component that
|
|
3320
|
-
subscribes to `afterClosed()` dies on "is not a function"; the repair written next to it,
|
|
3321
|
-
`afterClosed: () => of('saved')`, answers before anything closed the dialog — the spec then passes
|
|
3322
|
-
whether or not `close()` was ever called.
|
|
3323
|
-
|
|
3324
|
-
| Call | Does |
|
|
3325
|
-
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
3326
|
-
| `provideMatDialogData(token, data)` | a typed `{ provide, useValue }` — name the type argument and the data is checked against it |
|
|
3327
|
-
| `provideMatDialogRef(RefClass, init?)` | a `FactoryProvider` — a fresh ref per injector; `init`: `closedWith`, `disableClose`, `componentInstance` |
|
|
3328
|
-
| `injectMatDialogRef(RefClass, injector?)` | the handle: `.ref`, `.close` (the spy), `emitClose(result?)` |
|
|
3329
|
-
| `createMatDialogRef(RefClass, init?)` | the same handle without a `TestBed` — and the ref a spied `MatDialog.open()` hands back |
|
|
3330
|
-
|
|
3331
|
-
`MatDialog` itself needs no helper of its own: `provideAutoSpy(MatDialog)` already spies it, and the
|
|
3332
|
-
ref double is the thing its `open()` was missing.
|
|
3333
|
-
|
|
3334
|
-
```ts
|
|
3335
|
-
TestBed.configureTestingModule({ providers: [provideAutoSpy(MatDialog)] });
|
|
3336
|
-
|
|
3337
|
-
injectSpy(MatDialog).open.mockReturnValue(createMatDialogRef(MatDialogRef, { closedWith: 'saved' }).ref);
|
|
3338
|
-
```
|
|
3339
|
-
|
|
3340
|
-
Six things to know:
|
|
3341
|
-
|
|
3342
|
-
- **`close` is the spy, and it is the same function the ref carries.** `expect(dialog.close)` and
|
|
3343
|
-
`expect(TestBed.inject(MatDialogRef).close)` are one assertion. `emitClose(result?)` is the other
|
|
3344
|
-
half — the user closing the dialog from outside, where the streams move and the spy records
|
|
3345
|
-
nothing, and the only way to close with `undefined` (a dismissal), which `closedWith` cannot say.
|
|
3346
|
-
- **`afterClosed()` is replayed, where Material's is a plain `Subject`.** The deliberate departure:
|
|
3347
|
-
in a spec the assertion usually subscribes _after_ the component has closed the dialog, and a
|
|
3348
|
-
`Subject` has nothing left to say by then. `beforeClosed()` is the same stream — the double closes
|
|
3349
|
-
instantly, so there is no window between them, and `afterOpened()` has already emitted.
|
|
3350
|
-
- **`MAT_DIALOG_DATA` is `InjectionToken<any>` in Material**, so a hand-written `useValue: null`
|
|
3351
|
-
compiles for a component that reads `data.name`. `provideMatDialogData<EditUserData>(…)` checks the
|
|
3352
|
-
object; a token of your own typed `InjectionToken<EditUserData>` checks it without naming anything.
|
|
3353
|
-
With the type argument named the token parameter is `InjectionToken<unknown>`, so the call passes
|
|
3354
|
-
`@typescript-eslint/no-unsafe-argument` — do not drop the type argument to silence that rule, it
|
|
3355
|
-
only turns the data back into `any`. The value is handed out as it is, so build it per test rather than hoisting it to a constant.
|
|
3356
|
-
- **Name the ref type as a type argument, not as an instantiation expression.**
|
|
3357
|
-
`injectMatDialogRef(MatDialogRef<NameInputDialog, string>)` reads `Ref` off the class's `prototype`,
|
|
3358
|
-
which Material types `MatDialogRef<any, any>`, so the handle comes back `any`-typed and assigning it
|
|
3359
|
-
to a typed variable is `no-unsafe-assignment`. Write
|
|
3360
|
-
`injectMatDialogRef<MatDialogRef<NameInputDialog, string>>(MatDialogRef)` — the same for
|
|
3361
|
-
`createMatDialogRef` and `provideMatDialogRef`.
|
|
3362
|
-
- **`componentInstance` is how the opener drives the dialog**, and it is the one member Material
|
|
3363
|
-
keeps off the prototype, so a double that did not have it would read `undefined` rather than
|
|
3364
|
-
fail. Hand the stand-in over and the opener works against it — checked, member by member, against
|
|
3365
|
-
the component the ref class names:
|
|
3366
|
-
|
|
3367
|
-
```ts
|
|
3368
|
-
const save = new EventEmitter<string>();
|
|
3369
|
-
const dialog = createMatDialogRef<MatDialogRef<NameInputDialog, string>>(MatDialogRef, {
|
|
3370
|
-
componentInstance: { save, isSaving: signal(false) },
|
|
3371
|
-
});
|
|
3372
|
-
|
|
3373
|
-
injectSpy(MatDialog).open.mockReturnValue(dialog.ref);
|
|
3374
|
-
save.emit('Grace');
|
|
3375
|
-
|
|
3376
|
-
expect(dialog.close).toHaveBeenCalledWith('Grace');
|
|
3377
|
-
```
|
|
3378
|
-
|
|
3379
|
-
- **What it does not answer throws by name.** `backdropClick`, `keydownEvents`, `updateSize`,
|
|
3380
|
-
`updatePosition`, `getState`, `componentRef` and `id` are the dialog doing its own work; the
|
|
3381
|
-
double names them in the failure instead of reading `undefined`, and the real `MatDialogModule` is
|
|
3382
|
-
what answers them. A `componentInstance` nobody handed over says so by name too, and names the
|
|
3383
|
-
init field that fixes it.
|
|
3384
|
-
|
|
3385
|
-
### Which collaborators the code asked for — `trackInjections`
|
|
3386
|
-
|
|
3387
|
-
Do **not** reach for `vi.mock('@app/services')` to answer "was this collaborator used". Register the
|
|
3388
|
-
collaborators as provider factories and read back which ones DI constructed; a factory runs exactly
|
|
3389
|
-
when something injects its token, and DI is a seam the bundler cannot remove.
|
|
3390
|
-
|
|
3391
|
-
```ts
|
|
3392
|
-
import { trackInjections } from 'vitest-auto-spy/angular';
|
|
3393
|
-
|
|
3394
|
-
// the same function on /nestjs
|
|
3395
|
-
|
|
3396
|
-
const collaborators = trackInjections([FeatureFlagService, ANALYTICS_TOKEN]);
|
|
3397
|
-
|
|
3398
|
-
TestBed.configureTestingModule({ providers: [CheckoutFacade, ...collaborators.providers] });
|
|
3399
|
-
collaborators.get(FeatureFlagService).isOn.mockReturnValue(true);
|
|
3400
|
-
|
|
3401
|
-
TestBed.inject(CheckoutFacade).start();
|
|
3402
|
-
|
|
3403
|
-
expect(collaborators.names()).toEqual(['FeatureFlagService']); // analytics was never asked for
|
|
3404
|
-
```
|
|
3405
|
-
|
|
3406
|
-
`providers`, `injectedTokens()` (in factory-run order), `names()`, `wasInjected(token)`,
|
|
3407
|
-
`get<D>(token)` → `Spy<D>`, `reset()` (the record only — the doubles survive). A class token gets a
|
|
3408
|
-
class spy, anything else a `createAutoMock()`; pass `{ double: () => … }` when a collaborator has to
|
|
3409
|
-
be a real object. Doubles are built eagerly, so stub before the entry point runs; a factory runs once
|
|
3410
|
-
per injector, so a token appears once per injector, not once per injection site.
|
|
3411
|
-
|
|
3412
|
-
### Never mock `@angular/core` to control an `effect()`
|
|
3413
|
-
|
|
3414
|
-
Under the Angular unit-test builder the specs are bundled and `@angular/core` sits in a shared
|
|
3415
|
-
chunk, so `vi.mock('@angular/core', …)` re-enters a chunk that is still initialising and fails with
|
|
3416
|
-
`Cannot access '__vi_import_N__' before initialization`. The same applies to any module those shared
|
|
3417
|
-
chunks depend on, and to `vi.mock()` with a relative path (`./`, `../`), which has no module
|
|
3418
|
-
boundary left to replace once bundled.
|
|
3419
|
-
|
|
3420
|
-
Assert the effect's **result** instead — set the signals it reads, let it run, check what it
|
|
3421
|
-
produced:
|
|
3422
|
-
|
|
3423
|
-
```ts
|
|
3424
|
-
mockReadonlyProp(component, 'state', signal(State.Selected));
|
|
3425
|
-
|
|
3426
|
-
await stable(fixture);
|
|
3427
|
-
|
|
3428
|
-
expect(component.icon()).toBe('favouritesFilled');
|
|
3429
|
-
```
|
|
3430
|
-
|
|
3431
|
-
When the effect will never become dirty on its own — because its trigger is now a static signal —
|
|
3432
|
-
run that one effect directly:
|
|
3433
|
-
|
|
3434
|
-
```ts
|
|
3435
|
-
import { runEffect } from 'vitest-auto-spy/angular';
|
|
3436
|
-
|
|
3437
|
-
runEffect(component.highlightEffect); // runs the body now, with the current signal values
|
|
3438
|
-
```
|
|
3439
|
-
|
|
3440
|
-
`flushEffects()` runs everything currently dirty; `runEffect(ref)` runs one specific effect
|
|
3441
|
-
regardless. It runs the previous run's cleanup first, the way Angular's own scheduler does, so an
|
|
3442
|
-
`onCleanup` callback is observable and the node does not collect one closure per call. An effect
|
|
3443
|
-
whose view has been torn down — or which was destroyed on its own — is refused rather than run:
|
|
3444
|
-
after `fixture.destroy()` there is nothing production could still produce. Prefer asserting the
|
|
3445
|
-
result where practical — `runEffect` reads Angular's reactive node, and throws with instructions if
|
|
3446
|
-
a future version moves it.
|
|
3447
|
-
|
|
3448
|
-
### Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
|
|
3449
|
-
|
|
3450
|
-
"The template read `total()` six times and the computation ran once" and "changing the filter did
|
|
3451
|
-
not re-run the sync effect" are not observable from the outside: a `computed()` returns the same
|
|
3452
|
-
value whether it was cached or recomputed. The usual workaround is a counter inside the computation,
|
|
3453
|
-
which means editing production code to make a test possible.
|
|
3454
|
-
|
|
3455
|
-
```ts
|
|
3456
|
-
import { trackEffectRuns, trackRecomputations } from 'vitest-auto-spy/angular';
|
|
3457
|
-
|
|
3458
|
-
const recomputed = trackRecomputations(component.total);
|
|
3459
|
-
const synced = trackEffectRuns(component.syncEffect);
|
|
3460
|
-
|
|
3461
|
-
component.unrelatedFilter.set('open');
|
|
3462
|
-
await stable(fixture);
|
|
3463
|
-
|
|
3464
|
-
expect(component.total()).toBe(42);
|
|
3465
|
-
expect(recomputed.count).toBe(0); // memoised: nothing it reads changed
|
|
3466
|
-
expect(synced.count).toBe(0);
|
|
3467
|
-
|
|
3468
|
-
recomputed.stop(); // and `restoreMockedProps()` — so `setupAutoSpy()` — takes both off anyway
|
|
3469
|
-
```
|
|
3470
|
-
|
|
3471
|
-
Both return `{ count, stop() }`. `trackRecomputations` counts the **computation**, not the reads,
|
|
3472
|
-
and takes a `computed()` or a `linkedSignal()` — a plain `signal()` has nothing to compute and is
|
|
3473
|
-
refused by name. `trackEffectRuns` counts every run whoever asked for it: a flush, a `stable()`, a
|
|
3474
|
-
`runEffect()` of the same effect. Both wrap one member of Angular's reactive node through
|
|
3475
|
-
`mockValueProp`, so the undo is the journal every other stub in this package uses.
|
|
3476
|
-
|
|
3477
|
-
### ngrx `rxMethod`
|
|
3478
|
-
|
|
3479
|
-
An `rxMethod` is a function with a `destroy` property. A bare mock has no `destroy`, so the
|
|
3480
|
-
component's cleanup throws:
|
|
3481
|
-
|
|
3482
|
-
```ts
|
|
3483
|
-
const load = Object.assign(vi.fn(), { destroy: vi.fn() });
|
|
3484
|
-
```
|
|
3485
|
-
|
|
3486
|
-
Under `strict`, a call whose ref nobody reads is configured with `returns: { load: undefined }`. When
|
|
3487
|
-
the code does read the ref (it calls `destroy`), seed one from the method's own type — ngrx does not
|
|
3488
|
-
export `RxMethodRef`: `returns: { load: createAutoMock<ReturnType<Store['load']>>() }`.
|
|
3489
|
-
|
|
3490
|
-
A `signalStore()` class keeps its methods and `rxMethod`s on the instance, not the prototype, so
|
|
3491
|
-
discovery finds none of them. `provideAutoSpy(Store, { fillMissing: true, returns: { … } })` answers
|
|
3492
|
-
every such member with a spy — `returns` and `strict` apply to them as to any method — instead of a
|
|
3493
|
-
long `instanceMethodsToSpyOn` list.
|
|
3494
|
-
|
|
3495
|
-
```ts
|
|
3496
|
-
// shallow rendering — configureTestingModule + NO_ERRORS_SCHEMA + overrideComponent, in one call
|
|
3497
|
-
const { fixture, component } = renderShallow(TaskListComponent, {
|
|
3498
|
-
providers: [provideAutoSpy(TaskService)],
|
|
3499
|
-
inputs: { projectId: 42 }, // signal inputs take the VALUE, not the signal
|
|
3500
|
-
});
|
|
3501
|
-
// other options: imports, keepTemplate, keepModules, keepChildren, keepHostDirectives, template, beforeCreate, detectChanges
|
|
3502
|
-
// COVERAGE: the override recompiles the component under JIT for the rest of the file (keepTemplate too),
|
|
3503
|
-
// so its AOT template/host branches leave coverage — keep one real TestBed render first where they matter
|
|
3504
|
-
|
|
3505
|
-
// change an input after the first render — one setInput per key, then the wait, in one await
|
|
3506
|
-
await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
|
|
3507
|
-
await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
|
|
3508
|
-
// a fixture whose componentType carries no ɵcmp is refused by name too, not as a bare TypeError
|
|
3509
|
-
// aliases resolve by CLASS FIELD name, and an input a hostDirectives entry exposes is accepted
|
|
3510
|
-
|
|
3511
|
-
// a typed element instead of Angular's `any` — never `fixture.nativeElement as HTMLElement`
|
|
3512
|
-
const host = hostElement(fixture); // HTMLElement, checked with instanceof
|
|
3513
|
-
queryElement(fixture, '.close').click(); // HTMLElement; throws naming the selector when nothing matches
|
|
3514
|
-
queryElement(fixture.debugElement, 'input[name=q]', HTMLInputElement).value; // typed; wrong type throws
|
|
3515
|
-
// also on /bun-angular; queryElement takes an element found earlier as its root too
|
|
3516
|
-
// a null source (debugElement.query() that matched nothing) throws saying so, not 'Cannot read properties of null'
|
|
3517
|
-
expect(queryElement(fixture, '.title').textContent.trim()).toBe('Orders'); // present: read through queryElement
|
|
3518
|
-
expect(host.querySelector('.empty-state')).toBeNull(); // absent: querySelector on the typed host, no cast
|
|
3519
|
-
// NOT host.querySelector('.title')?.textContent.trim() — a miss is undefined, which toBeFalsy() accepts
|
|
3520
|
-
|
|
3521
|
-
// build a class through DI, every unprovided token auto-spied
|
|
3522
|
-
const { instance, spies } = createWithAutoSpies(CartService, {
|
|
3523
|
-
providers: [{ provide: TaxService, useValue: realTax }], // explicit providers win
|
|
3524
|
-
});
|
|
3525
|
-
spies.get(PricingService).total.mockReturnValue(100);
|
|
3526
|
-
// NOTE: Injector.create() — it does NOT accept EnvironmentProviders (provideHttpClient() etc.)
|
|
3527
|
-
|
|
3528
|
-
// zoneless waiting
|
|
3529
|
-
await stable(fixture); // flush effects, then await the fixture; fails at 2000 ms naming the cause
|
|
3530
|
-
await stable(fixture, { timeout: 5000, label: 'the products fixture' });
|
|
3531
|
-
flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
|
|
3532
|
-
|
|
3533
|
-
// resources — one wait for httpResource(), resource() and rxResource()
|
|
3534
|
-
flushEffects(); // an httpResource issues NO request until something ticks
|
|
3535
|
-
httpTesting.expectOne('/api/products').flush([product]);
|
|
3536
|
-
await settleResource(products, { label: 'the product resource' });
|
|
3537
|
-
// a resource left 'idle' FAILS here — params() returned undefined and value() is still the default;
|
|
3538
|
-
// { allowIdle: true } when that is the state under assertion, { turns } is spent exactly as reported
|
|
3539
|
-
|
|
3540
|
-
// on a component the resource is a FIELD and renderShallow is the injection context — nothing else changes
|
|
3541
|
-
const { component } = renderShallow(ProductListComponent, { providers: [...provideHttpTesting()], keepTemplate: true });
|
|
3542
|
-
await expectRequest('/api/products?q=').flush([product]); // both from /angular-http; renderShallow already ticked
|
|
3543
|
-
expect(component.products).toHaveResourceValue([product]);
|
|
3544
|
-
// after changing a signal the params() computation reads: flushEffects() before the next expectOne/expectRequest
|
|
3545
|
-
|
|
3546
|
-
// ...or skip the request entirely when it is not what the spec is about
|
|
3547
|
-
const products = mockResourceProp(service, 'products', []); // { status: 'idle' } starts it unstarted
|
|
3548
|
-
products.set([product]); // 'resolved' products.loading() products.fail('offline') products.idle()
|
|
3549
|
-
service.products.set([edited]); // the component's own optimistic write — status → 'local'
|
|
3550
|
-
expect(products.reload).toHaveBeenCalled(); // reload is spied, answers true, and re-issues nothing
|
|
3551
|
-
|
|
3552
|
-
// signal assertions
|
|
3553
|
-
registerSignalMatchers(); // once, in the setup file — /angular/matchers
|
|
3554
|
-
expect(component.total).toHaveSignalValue(3);
|
|
3555
|
-
|
|
3556
|
-
// resource assertions — value AND status, which is the whole point
|
|
3557
|
-
registerResourceMatchers(); // once, in the setup file — /angular/matchers
|
|
3558
|
-
expect(component.products).toBeLoading();
|
|
3559
|
-
expect(component.products).toHaveResourceValue([product]);
|
|
3560
|
-
expect(component.products).toHaveResourceError(/503/);
|
|
3561
|
-
```
|
|
3562
|
-
|
|
3563
|
-
**`renderShallow({ inputs })` and `setInputs` resolve a name the same way**, which they did not
|
|
3564
|
-
before. Both read the compiled definition, so an input renamed with an alias
|
|
3565
|
-
(`@Input('chart-series') series` or `series = input(…, { alias: 'chartSeries' })`) is set by its
|
|
3566
|
-
**class-field** name as well as its public one; both accept an input a `hostDirectives` entry
|
|
3567
|
-
exposes, which `setInputs` used to refuse with "It declares no inputs at all"; and both refuse a name
|
|
3568
|
-
the component does not declare, at the call, listing the ones it does. Angular's own answer to an
|
|
3569
|
-
undeclared name is an `NG0303` on the console and no change at all, so the spec used to fail several
|
|
3570
|
-
assertions later, on state nothing moved.
|
|
3571
|
-
|
|
3572
|
-
Two zoneless traps:
|
|
3573
|
-
|
|
3574
|
-
- `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
|
|
3575
|
-
effects. Asserting right after it reads state that has not finished computing. Use `await stable(fixture)`.
|
|
3576
|
-
- `expect(someSignal).toBeTruthy()` passes for **every** signal ever created — a signal is a
|
|
3577
|
-
function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake — and a
|
|
3578
|
-
spy, which it refuses instead of calling.
|
|
3579
|
-
|
|
3580
|
-
And one resource trap, which is the same shape one level up: an `httpResource()` reports `loading`
|
|
3581
|
-
with its **default** value until a tick _and_ a microtask after its response is flushed, so a spec
|
|
3582
|
-
that asserts too early asserts the default and passes. `settleResource` fails instead of passing
|
|
3583
|
-
emptily. Note the order — `flushEffects()` first (the request is issued there, not on creation),
|
|
3584
|
-
then the flush, then the wait. The same trap wearing a different hat is a resource still `idle` when
|
|
3585
|
-
the wait ends: its `params()` computation returned `undefined`, the loader never ran, and `value()`
|
|
3586
|
-
is the default. `settleResource` refuses that too, naming the signal the computation reads;
|
|
3587
|
-
`{ allowIdle: true }` is the opt-out for a spec whose subject is the idle state. Each round is a tick
|
|
3588
|
-
plus a microtask, and from the third round an event-loop turn as well, so a loader on a real timer
|
|
3589
|
-
settles rather than burning the budget in microseconds.
|
|
3590
|
-
|
|
3591
|
-
`toHaveResourceValue` is the matcher form of that trap and the reason to prefer it over
|
|
3592
|
-
`expect(products.value()).toEqual(...)`: it **fails an unresolved resource even when the default
|
|
3593
|
-
value matches**, and names the status it was in. And when the request is not what the spec is about
|
|
3594
|
-
at all, do not arrange one — `mockResourceProp(service, 'products', [])` replaces the property with
|
|
3595
|
-
a double whose `set` / `fail` / `loading` / `idle` move it directly, built from real `signal()`s so a
|
|
3596
|
-
`computed()` downstream still recomputes. The double carries the whole of `ResourceRef`: `value` is
|
|
3597
|
-
writable and a write through it lands in `'local'`, `set` / `update` / `asReadonly` / `destroy` /
|
|
3598
|
-
`snapshot` behave as Angular's own do, and `hasValue()` is value-based rather than status-based —
|
|
3599
|
-
`true` while the resource is loading over a value that is defined, which is what Angular has answered
|
|
3600
|
-
since v20. Nothing is in flight, so there is nothing to await.
|
|
3601
|
-
|
|
3602
|
-
**The double is no more forgiving than the real thing, in two places that used to hide a defect.**
|
|
3603
|
-
`value()` read after `fail(reason)` **throws** — a `ResourceValueError` carrying the reason on
|
|
3604
|
-
`cause` and naming the property — exactly as a real `ResourceRef` does, so a spec that read the value
|
|
3605
|
-
after arranging the error branch fails now instead of reading the last good value. Branch on
|
|
3606
|
-
`hasValue()` / `status()` first, or assert with `toHaveResourceError()`. And `reload()` answers
|
|
3607
|
-
`false` while the resource is `'idle'` or `'loading'`, as Angular's does, rather than a constant
|
|
3608
|
-
`true` — `expect(products.reload).toHaveBeenCalled()` is still the assertion to write; the return
|
|
3609
|
-
value is only for code that branches on it.
|
|
3610
|
-
|
|
3611
|
-
**The resource, focus and directive matchers throw on an argument of the wrong type.** They used to
|
|
3612
|
-
report `{ pass: false }`, which `.not` turns into a pass — so
|
|
3613
|
-
`expect(products.value()).not.toBeLoading()` (the value, not the resource),
|
|
3614
|
-
`expect(missingEl).not.toHaveFocus()` (a query that found nothing) and
|
|
3615
|
-
`expect(undefined).not.toHaveDirectiveApplied(X)` were three green assertions about nothing. Each
|
|
3616
|
-
now throws, naming what it received.
|
|
3617
|
-
|
|
3618
|
-
Per-file timing, to find which specs actually pay for `TestBed`:
|
|
3619
|
-
|
|
3620
|
-
```ts
|
|
3621
|
-
import { enableTestBedDiagnostics } from 'vitest-auto-spy/angular/diagnostics';
|
|
3622
|
-
|
|
3623
|
-
if (process.env['SPEC_TIMING']) {
|
|
3624
|
-
enableTestBedDiagnostics();
|
|
3625
|
-
}
|
|
3626
|
-
```
|
|
3627
|
-
|
|
3628
|
-
### Angular under `bun test`
|
|
3629
|
-
|
|
3630
|
-
`vitest-auto-spy/bun-angular` is a **preload**, not a normal import — it installs a DOM, inlines
|
|
3631
|
-
`templateUrl` / `styleUrls` through a `Bun.plugin` hook and boots a zoneless TestBed:
|
|
3632
|
-
|
|
3633
|
-
```toml
|
|
3634
|
-
# bunfig.toml
|
|
3635
|
-
[test]
|
|
3636
|
-
preload = ["vitest-auto-spy/bun-angular"]
|
|
3637
|
-
```
|
|
3638
|
-
|
|
3639
|
-
The preload imports `provideZonelessChangeDetection` and `platformBrowserTesting` as values, so it
|
|
3640
|
-
wants **Angular >= 20** and a declared `@angular/platform-browser`; on 18 or 19 it throws while
|
|
3641
|
-
loading, because the zoneless provider was still called
|
|
3642
|
-
`provideExperimentalZonelessChangeDetection` there.
|
|
3643
|
-
|
|
3644
|
-
It exports the core, `provideAutoSpy` / `injectSpy`, `renderShallow`, `createWithAutoSpies`,
|
|
3645
|
-
`setInputs`, `runEffect`, `settleResource`, `trackEffectRuns` / `trackRecomputations` and
|
|
3646
|
-
`stable` / `flushEffects` — nothing else from this section. The matcher registrars
|
|
3647
|
-
(`registerSignalMatchers`, `registerDirectiveMatchers`, `registerResourceMatchers`) need the runner's
|
|
3648
|
-
`expect.extend` and the TestBed diagnostics its suite-level hooks; the overrides, `extendWithAutoSpies`,
|
|
3649
|
-
`provideAutoSpyForToken`, `trackInjections`, `setupAngularTestEnv`, the stub factories and the
|
|
3650
|
-
`mock*Prop`, platform and dialog doubles are simply not routed to Bun. Import them from `/angular`
|
|
3651
|
-
under Vitest, and the registrars, doubles and diagnostics from their companion entries
|
|
3652
|
-
(`/angular/matchers`, `/angular/doubles`, `/angular/diagnostics`).
|
|
3653
|
-
|
|
3654
|
-
---
|
|
3655
|
-
|
|
3656
|
-
### Zone and zoneless spec files in one worker
|
|
3657
|
-
|
|
3658
|
-
`TestBed.initTestEnvironment` may be called once per platform, and under `isolate: false` the
|
|
3659
|
-
platform lives for the whole worker — so a repository migrating to zoneless gradually cannot express
|
|
3660
|
-
itself in setup files: the second file the worker picks up in the other mode fails with `Cannot set
|
|
3661
|
-
base providers because it has already been called`, naming neither file. `test.projects` does not
|
|
3662
|
-
help; nothing promises a worker serves files of one project.
|
|
3663
|
-
|
|
3664
|
-
```ts
|
|
3665
|
-
setupAngularTestEnv({
|
|
3666
|
-
zoneless: (testPath) => testPath.includes('/libs/widgets/'),
|
|
3667
|
-
initZone: setupZoneTestEnv,
|
|
3668
|
-
initZoneless: setupZonelessTestEnv,
|
|
3669
|
-
});
|
|
3670
|
-
```
|
|
3671
|
-
|
|
3672
|
-
It resets the environment only when the mode actually changes, and the initialisers stay yours —
|
|
3673
|
-
which platform and which providers is not this library's decision. The mode is remembered **per
|
|
3674
|
-
worker**, so under `isolate: false` a run of files all in one mode initialises once: before, every
|
|
3675
|
-
spec file reset the environment and re-ran your initialiser.
|
|
3676
|
-
|
|
3677
|
-
---
|
|
3678
|
-
|
|
3679
|
-
### A dependency behind an `InjectionToken`
|
|
3680
|
-
|
|
3681
|
-
```ts
|
|
3682
|
-
providers: [provideAutoSpyForToken(PASSCODE_SERVICE_TOKEN)];
|
|
3683
|
-
const passcode = injectSpy(PASSCODE_SERVICE_TOKEN); // Spy<PasscodeService>
|
|
3684
|
-
```
|
|
3685
|
-
|
|
3686
|
-
A token typed with an interface has no class to read, so the habit is a `…Mock` class written in the
|
|
3687
|
-
spec — after which `Spy<Mock>` and `Spy<Interface>` disagree and somebody casts. Do not write
|
|
3688
|
-
`TestBed.inject<any>(TOKEN)`; both of these accept a token. And it is `provideAutoSpyForToken`, not
|
|
3689
|
-
`provideAutoSpy`: the latter reads a class prototype, which a token does not have.
|
|
3690
|
-
|
|
3691
|
-
**The second argument is not optional as often as it looks.** A spy answers `undefined` until it is
|
|
3692
|
-
told otherwise, and that is fatal the moment the code under test _chains_ off it — a constructor
|
|
3693
|
-
doing `inject(LOGGER).channel('auth').debug('…')` dies on the `.debug` of `undefined` before the
|
|
3694
|
-
spec's first line runs, because nothing in production wrote `?.` there. Name the link:
|
|
3695
|
-
|
|
3696
|
-
```ts
|
|
3697
|
-
provideAutoSpyForToken(LOGGER, undefined, { selfReturning: ['channel'] });
|
|
3698
|
-
```
|
|
3699
|
-
|
|
3700
|
-
`channel` stays a spy (assertable, and configured under `strict`) and answers the double itself.
|
|
3701
|
-
When every file wants it, register it once — `registerAutoSpyDefaults(LOGGER, { selfReturning:
|
|
3702
|
-
['channel'] })` from `vitest-auto-spy/angular` (§5) — and the provider is `provideAutoSpyForToken(LOGGER)`.
|
|
3703
|
-
|
|
3704
|
-
For a chain more than one link long, `mockDeep<T>()` is the double that answers every level (§2).
|
|
3705
|
-
|
|
3706
|
-
### A host for a directive under test
|
|
3707
|
-
|
|
3708
|
-
```ts
|
|
3709
|
-
const Host = createDirectiveHost({
|
|
3710
|
-
template: `<div [appTruncate]="enabled"></div>`,
|
|
3711
|
-
scope: [DirectivesModule], // the component's imports, NOT the TestBed's
|
|
3712
|
-
props: { enabled: false },
|
|
3713
|
-
});
|
|
3714
|
-
|
|
3715
|
-
TestBed.configureTestingModule({ imports: [Host] });
|
|
3716
|
-
```
|
|
3717
|
-
|
|
3718
|
-
The two halves of Angular disagree about where `imports` is resolved: on a `@Component` the AOT
|
|
3719
|
-
compiler resolves it at build time and bakes the flat list into `ɵcmp`, so an NgModule there works;
|
|
3720
|
-
on `TestBed.configureTestingModule` it is resolved at runtime from `ɵmod`, and `ɵɵsetNgModuleScope`
|
|
3721
|
-
is not emitted into a test bundle, so the same line contributes nothing. A host written
|
|
3722
|
-
`standalone: false` inside a spec is compiled outside any scope at all — no `NgClass`, no
|
|
3723
|
-
`AsyncPipe`, nothing.
|
|
3724
|
-
|
|
3725
|
-
`registerDirectiveMatchers()` adds `expect(fixture).toHaveDirectiveApplied(Directive, 'div')`, which
|
|
3726
|
-
asserts the fact Angular reports three wrong ways (`NG0303` points at the module where the directive
|
|
3727
|
-
_is_ declared; `NG0304` calls a missing directive a missing component; a bare attribute reports
|
|
3728
|
-
nothing at all). `schemas: [NO_ERRORS_SCHEMA]` next to a standalone component is a dead entry —
|
|
3729
|
-
schemas apply to a testing module's `declarations` only.
|
|
3730
|
-
|
|
3731
|
-
### A stand-in for a child — `createComponentStub`
|
|
3732
|
-
|
|
3733
|
-
Do not hand-write `class MockChartComponent` with a copied selector: nothing checks the copy, so a
|
|
3734
|
-
renamed input leaves the parent's binding pointing at nothing and the spec green. Build the stub
|
|
3735
|
-
from the real class's compiled definition instead:
|
|
3736
|
-
|
|
3737
|
-
```ts
|
|
3738
|
-
import { createComponentStub } from 'vitest-auto-spy/angular';
|
|
3739
|
-
|
|
3740
|
-
const ChartStub = createComponentStub(ChartComponent); // also a directive or a pipe
|
|
3741
|
-
|
|
3742
|
-
TestBed.configureTestingModule({ imports: [DashboardComponent] });
|
|
3743
|
-
TestBed.overrideComponent(DashboardComponent, { remove: { imports: [ChartComponent] }, add: { imports: [ChartStub] } });
|
|
3744
|
-
|
|
3745
|
-
const chart = fixture.debugElement.query(By.directive(ChartStub)).componentInstance;
|
|
3746
|
-
expect(chart.series()).toEqual([1, 2, 3]); // a signal input stays a signal input
|
|
3747
|
-
chart.pointSelected.emit(2); // outputs are EventEmitters; a model() stays a model
|
|
3748
|
-
```
|
|
3749
|
-
|
|
3750
|
-
Copied from `ɵcmp` / `ɵdir` / `ɵpipe`: the selector, every input under its public name (transforms
|
|
3751
|
-
included), every output, `exportAs`, a pipe's name and purity. Not copied: the template (the stub
|
|
3752
|
-
renders one `<ng-content>` per slot the real one projects — `{ template }` in the third argument
|
|
3753
|
-
replaces it), host bindings, providers, lifecycle hooks, queries, and **`hostDirectives`** — so an
|
|
3754
|
-
input the real component exposes through one is not on the stub, even though `setInputs` accepts that
|
|
3755
|
-
name on the real thing. The second argument seeds members per instance — a method the parent calls
|
|
3756
|
-
through `viewChild`, a pipe's `transform` (identity by default). With `renderShallow`:
|
|
3757
|
-
`renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })` — the real child is
|
|
3758
|
-
dropped as a component import and the stub is the one child left.
|
|
3759
|
-
|
|
3760
|
-
### Patching a property of a spy
|
|
3761
|
-
|
|
3762
|
-
`mockReadonlyProp` / `mockValueProp` / `mockSignalProp` accept the `Spy<T>` that `injectSpy` returns,
|
|
3763
|
-
and type the value against the member's **own** type. For a signal-valued member prefer
|
|
3764
|
-
`mockSignalProp(service, 'state', initial)` over `gettersToSpyOn`: a spied getter returns `undefined`
|
|
3765
|
-
until configured, a real signal keeps everything downstream reactive.
|
|
3766
|
-
|
|
3767
|
-
**A spec importing `@ngneat/spectator`** is on a package whose repository is a 404 and which does not
|
|
3768
|
-
resolve on Angular 22 (it imports `@angular/platform-browser-dynamic/testing` without declaring it).
|
|
3769
|
-
`createSpyObject` → `createSpyFromClass`, `mockProvider` → `provideAutoSpy`, `spectator.inject` →
|
|
3770
|
-
`injectSpy`, `SpyObject<T>` → `Spy<T>`, `createComponentFactory` → `renderShallow`. There is no
|
|
3771
|
-
replacement here for `spectator.query`, the event helpers or the DOM matchers — leave those on
|
|
3772
|
-
`@testing-library/angular` rather than inventing an equivalent. Full mapping:
|
|
3773
|
-
[Migrating from @ngneat/spectator](https://asdalexey.github.io/vitest-auto-spy/migrating-spectator).
|
|
3774
|
-
|
|
3775
|
-
A suite coming off [`@suites/unit`](https://github.com/suites-dev/suites) maps one to one:
|
|
3776
|
-
`TestBed.solitary(S).compile()` → `createNestUnit(S)`, `TestBed.sociable(S).expose(D).compile()` →
|
|
3777
|
-
`createNestUnit(S, { expose: [D] })`, `unitRef.get` → `spies.get`, `.final(v)` →
|
|
3778
|
-
`{ provide, useValue }`, and the `await` goes away because the graph is built synchronously. The one
|
|
3779
|
-
thing to check by hand after the swap: Suites' double answers every property name, so a spec that
|
|
3780
|
-
stubbed a since-renamed method was passing over nothing and will now fail. Full mapping:
|
|
3781
|
-
<https://asdalexey.github.io/vitest-auto-spy/migrating-suites>.
|
|
3782
|
-
|
|
3783
|
-
**A spec importing `@testing-library/angular` usually keeps it.** `render`, `screen` and the query
|
|
3784
|
-
set are a different discipline and have no twin here. What overlaps is the secondary
|
|
3785
|
-
`@testing-library/angular/vitest-utils` entry alone — 52 lines, four exports:
|
|
3786
|
-
`createMock` → `createSpyFromClass`, `createMockWithValues` → `createSpyFromClass(X, { overrides })`,
|
|
3787
|
-
`provideMock` → `provideAutoSpy`, `provideMockWithValues` → `provideAutoSpy(X, { overrides })` —
|
|
3788
|
-
`overrides` and not `returns`, because those values replace the member rather than configure it. Its
|
|
3789
|
-
walk assigns a mock only where `typeof descriptor?.value === 'function'`, so accessors are dropped
|
|
3790
|
-
silently while its `Mock<T>` still types them callable, and it recurses past `Object.prototype`, so
|
|
3791
|
-
`hasOwnProperty` and `toString` end up as `vi.fn()`. Full mapping, verified against the 19.4.2
|
|
3792
|
-
tarball: <https://asdalexey.github.io/vitest-auto-spy/migrating-testing-library-angular>.
|
|
3793
|
-
|
|
3794
|
-
- **`renderShallow` has no single speed number, and quoting one is a bug.** This repository commits
|
|
3795
|
-
no harness for the helper, so no surface publishes a ratio for it and none may be invented.
|
|
3796
|
-
Describe the shape instead: `renderShallow` is flat in the number of children because it never
|
|
3797
|
-
builds the subtree, while `TestBed.createComponent` scales linearly with it — so the win is
|
|
3798
|
-
however much markup the component under test happens to own, nothing on a leaf component and a
|
|
3799
|
-
great deal on a table or a dashboard. A per-render ratio would in any case be the upper bound on
|
|
3800
|
-
what a spec file can gain rather than a prediction of it, because a file also pays for imports,
|
|
3801
|
-
the `TestBed` module and the assertions. Link to
|
|
3802
|
-
`/core/performance#_2-rendering-the-child-subtree`. A speed figure for the helper also sits in
|
|
3803
|
-
`PRIORITIES.md`, backed by nothing in this repository — do not publish it until someone produces
|
|
3804
|
-
the measurement.
|
|
3805
|
-
- **Competitor defects are quoted from the published tarball, with a file, a line and a date.** The
|
|
3806
|
-
`@testing-library/angular` `createMock` claims in `docs-site/comparison.md` are pinned to
|
|
3807
|
-
`fesm2022/testing-library-angular-vitest-utils.mjs` lines 14 and 18 at 19.4.2. Re-`npm pack` and
|
|
3808
|
-
re-read before restating them against a new version; the "Where the numbers come from" box records
|
|
3809
|
-
each re-verification date rather than replacing the previous one.
|
|
3810
|
-
- **Head-to-head benchmark numbers are quoted from the measurement, not from memory.** `npm run
|
|
3811
|
-
bench:vs` and `npm run bench:suite` (see `CONTRIBUTING.md` for setup) back the claims in
|
|
3812
|
-
`core/performance.md`; point at that page rather than retyping a figure here, so it lives in one
|
|
3813
|
-
place. Micro-benchmark ratios do not transfer to suite scale: double construction is on the order
|
|
3814
|
-
of 1% of a test's cost, and an advantage that looks like several times faster per double can be
|
|
3815
|
-
gone, or reversed, once a whole suite runs. Suite-scale wall-clock is noisy between invocations;
|
|
3816
|
-
peak RSS reproduces far more tightly and is the safer claim. Never state a magnitude the source
|
|
3817
|
-
marks as not established.
|
|
3818
|
-
|
|
3819
|
-
`node:test` keeps every `mock.fn()` in one process-wide `MockTracker` for the life of the process.
|
|
3820
|
-
On a long suite that is real memory — 20 000 spies of a 10-method class retained 124.5 MB on
|
|
3821
|
-
Node v24.19.0. `trackNodeMocks()` puts this library's spies on a tracker it owns and replaces it per
|
|
3822
|
-
test: 5.9 MB for the same run, against a 5.4 MB baseline.
|
|
3823
|
-
|
|
3824
|
-
\```ts
|
|
3825
|
-
import { before } from 'node:test';
|
|
3826
|
-
import { trackNodeMocks } from 'vitest-auto-spy/node';
|
|
3827
|
-
|
|
3828
|
-
before(() => trackNodeMocks()); // opt-in, idempotent, returns the undo
|
|
3829
|
-
|
|
3830
|
-
// by hand, for a concurrent suite: pruneNodeMocks() → how many were dropped; countNodeMocks() → how many are held
|
|
3831
|
-
\```
|
|
3832
|
-
|
|
3833
|
-
It never calls `mock.reset()` — that would restore and forget the `mock.fn()` the spec made itself —
|
|
3834
|
-
and it never throws: the class is reached through the undocumented `mock.constructor`, so the
|
|
3835
|
-
constructed tracker is probed first and any failure leaves spies on `node:test`'s own tracker.
|
|
3836
|
-
Spies created **before** the call stay there too. `mock.reset()` in `afterEach` is the fallback.
|
|
3837
|
-
|
|
3838
|
-
**A `node:test` spy now prints under its method name.** `mock.fn()` takes no name and has no
|
|
3839
|
-
`mockName()`, so a spy inherited the library's internal dispatcher and read back as
|
|
3840
|
-
`[Function: dispatch]` everywhere. The adapter names the _implementation_ at creation and lets
|
|
3841
|
-
`mock.fn()` carry that name onto the mock, which is where a `node:test` mock takes its `name` from;
|
|
3842
|
-
`displayName` is set on the mock alongside it. The name survives `mock.reset()`, `mock.restore()`,
|
|
3843
|
-
`resetCalls()` and a `mockImplementation()` swap, and it is what `node:assert` diffs, what
|
|
3844
|
-
`util.inspect()` prints and what this package's own messages read. Do not "simplify" this into
|
|
3845
|
-
`Object.defineProperty(mock, 'name', …)`: redefining `name` drops the function out of V8's fast map
|
|
3846
|
-
and costs +206 B per mock against +65 B for naming at creation, measured over 200 000 mocks. `getMockName()` still does not exist there: read `spy.method.name`. Nothing labels a
|
|
3847
|
-
mock in `node:test`'s reporter output on its own. Full account:
|
|
3848
|
-
<https://asdalexey.github.io/vitest-auto-spy/runtimes/node#spy-names>.
|
|
3849
|
-
|
|
3850
|
-
### Rstest
|
|
3851
|
-
|
|
3852
|
-
`vitest-auto-spy/rstest` drives the same core through `rstest.fn()` / `rstest.spyOn()`. Rstest
|
|
3853
|
-
implements the Jest/Vitest mock surface, so **none of the `node:test` differences above apply**:
|
|
3854
|
-
`spy.method.mock.calls[0]` is a bare argument array, the `mockReturnValue` family is native, and
|
|
3855
|
-
`gettersToSpyOn` / `settersToSpyOn` go through `rstest.spyOn(obj, 'prop', 'get' | 'set')` rather than
|
|
3856
|
-
the redefinition fallback. `rstest.clearAllMocks()`, `rstest.resetAllMocks()` and the
|
|
3857
|
-
`clearMocks: true` / `resetMocks: true` config keys reach spies built by `createSpyFromClass` — the
|
|
3858
|
-
entry plants one sentinel mock for it, and there is nothing to enable.
|
|
3859
|
-
|
|
3860
|
-
Two things are not there. `vitest-auto-spy/setup` is wired to Vitest's hooks, so `setupAutoSpy()` and
|
|
3861
|
-
the fake-timer helpers are Vitest-only — on Rstest, import `vitest-auto-spy/rstest` once in the setup
|
|
3862
|
-
file, which is the part a setup file is for. `trackNodeMocks()` is `node:test`-only and is not needed:
|
|
3863
|
-
Rstest drops its mock registry between files, like Vitest and Bun.
|
|
3864
|
-
|
|
3865
|
-
Rstest is 0.x. Vitest stays the zero-config default; reach for this entry when the suite already runs
|
|
3866
|
-
on Rstest, which is typically an Rspack project reusing its bundler config for tests. Full account:
|
|
3867
|
-
<https://asdalexey.github.io/vitest-auto-spy/runtimes/rstest>.
|
|
1827
|
+
In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that uses `TestBed`, signals, Angular HTTP, the router or an Angular double.
|
|
1828
|
+
|
|
1829
|
+
<!-- agents-outline: agent-docs/angular.md -->
|
|
1830
|
+
|
|
1831
|
+
- The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
|
|
1832
|
+
- Signals — which helper depends on whose signal it is
|
|
1833
|
+
- Observers the component constructs itself
|
|
1834
|
+
- A component's own `providers` win, and the symptom is nowhere near the cause
|
|
1835
|
+
- `injectSpy` cannot reach a component-level provider
|
|
1836
|
+
- An NgModule that contributes nothing
|
|
1837
|
+
- A component whose own definition has a hole in it
|
|
1838
|
+
- Four silent failures, as one setup line
|
|
1839
|
+
- `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
|
|
1840
|
+
- `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
|
|
1841
|
+
- `Router` from one URL — `vitest-auto-spy/angular-router`
|
|
1842
|
+
- `Location` from Angular's own testing classes — `vitest-auto-spy/angular-router`
|
|
1843
|
+
- Signal forms — `vitest-auto-spy/signal-forms`
|
|
1844
|
+
- `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
|
|
1845
|
+
- The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
|
|
1846
|
+
- Which collaborators the code asked for — `trackInjections`
|
|
1847
|
+
- Never mock `@angular/core` to control an `effect()`
|
|
1848
|
+
- Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
|
|
1849
|
+
- ngrx `rxMethod`
|
|
1850
|
+
- Angular under `bun test`
|
|
1851
|
+
- Zone and zoneless spec files in one worker
|
|
1852
|
+
- A dependency behind an `InjectionToken`
|
|
1853
|
+
- A host for a directive under test
|
|
1854
|
+
- A stand-in for a child — `createComponentStub`
|
|
1855
|
+
- Patching a property of a spy
|
|
1856
|
+
- Rstest
|
|
1857
|
+
|
|
1858
|
+
<!-- /agents-outline -->
|
|
3868
1859
|
|
|
3869
1860
|
---
|
|
3870
1861
|
|
|
@@ -3958,533 +1949,87 @@ adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives
|
|
|
3958
1949
|
|
|
3959
1950
|
## 16. ESLint plugin (flat config only)
|
|
3960
1951
|
|
|
3961
|
-
|
|
3962
|
-
import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
3963
|
-
|
|
3964
|
-
export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
3965
|
-
```
|
|
3966
|
-
|
|
3967
|
-
The `files` glob has no default — too narrow and the plugin is silently inert
|
|
3968
|
-
(`npx eslint --print-config a.spec.ts` settles it), too wide and `no-object-define-property` starts
|
|
3969
|
-
reporting on application code that is entitled to it. To override a severity, spread the rule map
|
|
3970
|
-
too: a bare `rules` key beside the spread config **replaces** it rather than merging, and nothing
|
|
3971
|
-
reports that.
|
|
3972
|
-
|
|
3973
|
-
```js
|
|
3974
|
-
export default [
|
|
3975
|
-
{
|
|
3976
|
-
files: ['**/*.spec.ts'],
|
|
3977
|
-
...autoSpy.configs.recommended,
|
|
3978
|
-
rules: { ...autoSpy.configs.recommended.rules, 'vitest-auto-spy/prefer-native-spy-api': 'off' },
|
|
3979
|
-
},
|
|
3980
|
-
];
|
|
3981
|
-
```
|
|
3982
|
-
|
|
3983
|
-
`autoSpy.configs.typeErrors` is a second flat config holding the subset whose findings are compile
|
|
3984
|
-
errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
|
|
3985
|
-
blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
|
|
3986
|
-
`autoSpy.configs.strict` is `recommended` with every rule at `error`, instead of mapping
|
|
3987
|
-
`Object.keys(autoSpy.rules)` by hand.
|
|
3988
|
-
|
|
3989
|
-
| Rule | Level | Fix | Flags |
|
|
3990
|
-
| --------------------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3991
|
-
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
3992
|
-
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
3993
|
-
| `prefer-provide-auto-spy` | `error` | fix | a hand-rolled `useValue`, `useFactory`, `useClass` or `useExisting` (also `useValue: new StubMock()`), in a provider **or** a `TestBed.overrideProvider(X, { … })` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)`. A name in the slot is followed one step, through an initialiser or a single `beforeEach` assignment. `{ provide: X, useValue: createSpyFromClass(X, config) }` — the factory's own body, written out — is reported and **rewritten** to `provideAutoSpy(X, config)`; a double read from a _different_ class (`{ provide: LocalStorage, useValue: createSpyFromClass(BaseLocalStorage) }`) is not, since the abstract token has no prototype for `provideAutoSpy` to read. On the `ActivatedRoute` token — in a provider or in an `overrideProvider` — the advice is `provideActivatedRoute()` from `/angular-router` instead, with a message of its own, so the two route rules agree |
|
|
3994
|
-
| `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
|
|
3995
|
-
| `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy` and delete the stub; a decorated, `implements`-ing, `extends`-ing or unnamed class is exempt — option: `{ minRunnerFns }` |
|
|
3996
|
-
| `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`; a bare `let fn: Mock` is a callback and never reported — option: `{ minRunnerFns }` |
|
|
3997
|
-
| `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }` |
|
|
3998
|
-
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
3999
|
-
| `no-mocked-for-spy` | `error` | `--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 |
|
|
4000
|
-
| `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
|
|
4001
|
-
| `no-done-callback` | `error` | — | a first parameter of a test or hook that is **called**, **passed on** as an argument, or never used at all → `async` + an awaited assertion, and `done.fail(…)` at the call site. A parameter read only as `ctx.skip()` / `ctx.task` / `ctx.expect` is Vitest's `TestContext` and is left alone, destructured or not |
|
|
4002
|
-
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
4003
|
-
| `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
|
|
4004
|
-
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array, or one a `TestBed.overrideProvider` in the same hook replaces → the earlier one never runs; the exact duplicate can be deleted |
|
|
4005
|
-
| `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
|
|
4006
|
-
| `no-private-member-access` | `error` | — | `instance['privateMember']`, `(instance as any).privateMember` (and `as unknown as`, and a decoy type), `vi.spyOn(Object.getPrototypeOf(x), 'm')` → drive the member through the public API. **Type-aware**: silent without `parserOptions.project`, and silent on an index signature |
|
|
4007
|
-
| `no-reflect-member-access` | `error` | on a double | `Reflect.get(component, 'privateField')` / `Reflect.set(service, 'x', v)` on a subject the test holds — the second door out of `no-private-member-access`, and the one no compiler stands in: the key is an ordinary string argument, so a rename leaves the spec reading a property that is no longer there and `Reflect.set` writing a dead one nothing reads. `window` / `globalThis` (or a name declared as `Window` / `typeof globalThis`) and a computed key are never reported; a write onto a double this library built is reported apart and suggests `mockValueProp` |
|
|
4008
|
-
| `no-dead-schemas` | `error` | — | `schemas` on a testing module with no `declarations` — the schema applies to nothing; the file decides, so a `declarations` in another `configureTestingModule` call silences it |
|
|
4009
|
-
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
|
|
4010
|
-
| `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
|
|
4011
|
-
| `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
4012
|
-
| `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
|
|
4013
|
-
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
|
|
4014
|
-
| `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
|
|
4015
|
-
| `no-hand-assigned-global` | `error` | `--fix` | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages, `stubWorker({ respond })` for `Worker`; the three observers stay with `prefer-observer-stub`; any value written into an imported object (`environment.production = true`) → `mockValueProp(environment, 'production', true)`, fixed in a test or `beforeEach` |
|
|
4016
|
-
| `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or `createMock<Response>(…)` / `createAutoMock<Response>(…)` → `stubResponse({ body })` from `/setup`; the literal answers `undefined` for every member it does not list (`status`, `headers`, `text()`) and the cast is what makes that compile. `Response` must resolve to the **global**, so an Express handler's or a generated client's `Response` is never reported |
|
|
4017
|
-
| `prefer-provide-activated-route` | `error` | — | a hand-built `ActivatedRoute` — any `useValue` / `useClass` / `useFactory` / `useExisting`, and `provideAutoSpy(ActivatedRoute)` too → `provideActivatedRoute({ … })`; the double knows either the streams or the snapshot, never both, and `injectActivatedRoute().setParams(…)` moves them together mid-test |
|
|
4018
|
-
| `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
|
|
4019
|
-
| `no-console-in-spec` | `error` | — | a spec calling `console.x(…)` itself, or `console.x = …`, which nothing restores → absorb the code's output through `vitest-auto-spy/console` |
|
|
4020
|
-
| `no-import-time-console-spies` | `error` | — | an import of `vitest-auto-spy/console` in a file that never calls `installConsoleSpies()` — the import installs once per worker and silences every later file → `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` |
|
|
4021
|
-
| `no-mistyped-use-value` | `error` | — | `{ provide: TOKEN, useValue }` whose value is not assignable to the primitive `T` of `InjectionToken<T>` (string, number, boolean, bigint, enum, their literals, `null`, `undefined`) — `useValue` is `any`, so `{}` for a `boolean` token compiles and is truthy. **Type-aware**: silent without `parserOptions.project`; object-typed tokens are `no-unknown-use-value-key`'s |
|
|
4022
|
-
| `no-unknown-use-value-key` | `error` | — | a key of an object `useValue` literal the provided type does not have — `T` of `InjectionToken<T>`, or the instance type of a `provide:` class; `{ provide: ActivatedRoute, useValue: { queryParams$: … } }` compiles, since `useValue` is `any`. Keys only, never assignability (a partial fixture is fine). **Type-aware**; silent on `any` / `unknown` / `object`, an index signature, a spread's keys, `multi: true` |
|
|
4023
|
-
| `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(instance, 'ngOnInit')` (and `ngOnDestroy`, `ngDoCheck`, `ngAfterContent*`, `ngAfterView*`) — a view calls the hook read off the prototype, so the instance spy is never called and its stub never runs → `vi.spyOn(Cls.prototype, …)` before `createComponent`, or assert the effect. `ngOnChanges` is exempt: Angular calls it through the instance |
|
|
4024
|
-
| `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `returnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature → an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`; otherwise the fixture is the wrong shape, checked against `ReturnType<X['m']>`. A reason after the directive does not silence it; a deliberate out-of-type value keeps it under `eslint-disable-next-line … -- <why>` |
|
|
4025
|
-
| `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer it already fixes (`toBe` / `toEqual` / `toStrictEqual` against a literal; `toBeTruthy`, `toBeDefined`, `toBeNull`… for any literal) → assert on what the code produced, or `expect.fail(…)` for an unreachable branch |
|
|
4026
|
-
| `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — every statement of the body an `expect(x)` under `toBeTruthy` / `toBeDefined` / `toBeInstanceOf` (or their negated twins), weighed against the tests that run the same setup: the rest of the block, and everything the blocks nested in it declare → delete it; the suggestion removes the test and the blank line above it. Silent where that test is the block's only running one, and a skipped sibling does not count as proof |
|
|
4027
|
-
| `no-self-called-spy` | `error` | — | `vi.spyOn(obj, 'm')`, then `obj.m(…)` written by the test itself, then a positive `toHaveBeenCalled*` — the assertion is satisfied by the test's own line, so the test survives the deletion of the behaviour its title names. Order decides it: a call written **before** the spy is arrangement, and a `mockClear` between the call and the assertion, a negated matcher or arguments the call did not pass all silence it |
|
|
4028
|
-
| `no-vacuous-absence-assertion` | `error` | — | a test **all** of whose assertions are satisfied by the stream never emitting. The carrier is a `const` / `let` declared in the test whose every write outside the declaration sits in a `subscribe` callback of that test (the assignment and the `push` forms both count), or a `vi.fn()` the test hands to `subscribe` and never calls itself. Silence satisfies an equality matcher repeating the initialiser's source text, `toBeUndefined` / `not.toBeDefined` on a `let` holding `undefined`, `toBeNull` on one holding `null`, `toBeFalsy` / `not.toBeTruthy` on any falsy literal, `toHaveLength(0)` on `[]` or `''`, and `not.toHaveBeenCalled` / `not.toHaveBeenCalledWith` / `toHaveBeenCalledTimes(0)` on any subject — read literally, so `toBeNull()` on a `let` with no initialiser is **not** reported. One `expect()` a silent source could fail silences the rule, which is what exempts the "nothing yet, trigger, now the value" shape; a test asserting through a local helper is skipped entirely → `await expectNoEmission(source$)`, or `expect(await expectEmission(source$))` |
|
|
4029
|
-
| `prefer-settle-dynamic-import` | `error` | suggest | `await import('./thing')` and `import('./thing').then(…)` inside an `it` / `test` body or a `beforeEach` / `beforeAll` / `afterEach` / `afterAll` hook (the `.only`, `.skip` and `.each` spellings included) → `await settleDynamicImport(() => import('./thing'))`, which is the same load plus `flushEventLoop(1)` and returns the namespace, so the destructured form keeps reading the same way. The bare `await` waits for the **module** and not for the continuation of the code that was loading it, so the assertion runs a turn early and the test is green only while that continuation is short. Syntax only. Silent wherever a function of its own sits between the runner's callback and the import — a `vi.mock` factory, a lazy route's `loadComponent` / `loadChildren`, a callback handed to production code, `settleDynamicImport`'s own `() => import(…)` — and, by the same reading, silent on a spec-local `const load = async () => { await import('…') }`, which is written identically whether the spec calls it or the code under test does |
|
|
4030
|
-
| `prefer-create-mock` | `warn` | suggest | an object literal under `as SomeType` — `{ id: '1', isOffline: false } as Device`, `<Device>{ … }` too → `createMock<Device>({ … })`. A cast asks whether the two types overlap rather than whether the value is one of them, so it passes a key the type does not declare (the excess-property check is skipped) **and** a required field the fixture never sets; both type gates stay silent, and the same object is then spread into the expected payload of a call assertion. Where the literal already sits in a typed slot the first repair is to delete the cast. Silent on `as const`, `as unknown` / `as any` and the double cast built from them, on a cast of anything but a literal, and on a literal inside one of this library's own factories. `warn`, because accepting the suggestion makes a drifted fixture red on the next type-check — the finding, and a migration taken file by file |
|
|
4031
|
-
| `no-mock-cast` | `error` | suggest | a cast to Vitest's `Mock` / `MockInstance` over a **member access** — `TestBed.inject(S).m as Mock` → `injectSpy(S).m`, which the suggestion writes whenever the token is in view (directly, or through a `const` the file settled with a `TestBed.inject`). `Mock` with no parameters is `Mock<any>`: `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments. `(spy.m.mockReturnValue as Mock)(…)` gets its own message — the cast is on the member that installs the answer, so neither the value nor the method's return type is checked. The name must resolve to an import from `vitest` / `@rstest/core` / `bun:test` / `jest`, or to nothing at all, so a domain `Mock` is left alone |
|
|
4032
|
-
| `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset the runner already performs between tests — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — standing where nothing the file wrote ran since the runner's own: the first statement of a `beforeEach` no other `beforeEach` precedes, or a clear as the last statement of an `afterEach` → delete it, and the hook with it where that is all it held. A restore or reset in `afterEach` / `afterAll` is never reported: after the last test it is what takes a spy off before the `afterAll` hooks. **Silent** until the flags are known: options `{ clearMocks, restoreMocks, mockReset }` (or `{ configFile }` naming the runner config) first, then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text. Flag matches call, never family — `restoreMocks` walks only the spies `vi.spyOn` installed. `--fix` only in a file whose only before-hook it is; a suggestion otherwise; never inside a test body |
|
|
4033
|
-
| `no-unasserted-argument` | `warn` | — | a bare `expect(spy).toHaveBeenCalled()` on evidence from the file: the same subject asserted with `toHaveBeenCalledWith(…)` in another test, or a title containing `with` over a body whose every assertion is a bare call → `toHaveBeenCalledWith(…)` / `mustBeCalledWith(…)`. `not.toHaveBeenCalled()` and the counting matchers are never reported |
|
|
4034
|
-
| `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so list those classes in `{ ignoreComponents: ['CardComponent'] }` (a spec naming one is left alone) or keep a `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
|
|
4035
|
-
| `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`, through `TestBed`, `getTestBed()`, a chain of those, or a name holding one → drop the `await`, and the `async` of a hook that then awaits nothing else; the suggestion does both. Reads **no types**, so it reports without `parserOptions.project`, where `@typescript-eslint/await-thenable` cannot. `inject` and `runInInjectionContext` are never reported: each answers whatever the token or the callback holds, which can be a promise |
|
|
4036
|
-
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
|
|
4037
|
-
| `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
|
|
4038
|
-
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
4039
|
-
| `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
|
|
4040
|
-
|
|
4041
|
-
Forty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
|
|
4042
|
-
`no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
|
|
4043
|
-
`prefer-set-inputs`, `no-unasserted-argument` and `prefer-spy-on-own-method`**; six fix on their own, nineteen offer suggestions. Forty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
|
|
4044
|
-
`no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
|
|
4045
|
-
rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
|
|
4046
|
-
builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
|
|
4047
|
-
consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The
|
|
4048
|
-
eight `warn`s left are not judgements about how much a finding matters. `prefer-render-shallow` is
|
|
4049
|
-
about the _kind_ of finding: every other rule names something wrong or dead, while this one names a
|
|
4050
|
-
file that could render more cheaply, and `renderShallow` is a migration a suite either takes or does
|
|
4051
|
-
not. At `error` the plugin would gate that migration — 491 findings across 398 of one consumer's
|
|
4052
|
-
1759 spec files — so `recommended` would exist to be overridden. `no-stub-class-double` and
|
|
4053
|
-
`no-structural-double` (5.5.0) are about the _evidence_: both report the same drift
|
|
4054
|
-
`prefer-create-spy-from-class` reports at `error`, but neither has a `provide:` beside it to settle
|
|
4055
|
-
the question, so each decides on a heuristic — and a project that disagrees with the reading has to
|
|
4056
|
-
be able to switch it off without losing the rule that reads a count. The counts are not the argument
|
|
4057
|
-
and moved a long way inside the release: measured on the same 1759 files, the two started at 12
|
|
4058
|
-
reports in 8 files and 115 in 74, and are 10 in 7 and 5 in 4 once `prefer-provide-auto-spy` learnt to
|
|
4059
|
-
follow a name into a `useValue` — 112 of those doubles are handed to Angular DI one name away, where
|
|
4060
|
-
a `provide:` settles it. That rule reports **154 times across 87 files** on the same suite, all at
|
|
4061
|
-
`error`. `no-instance-lifecycle-spy` is on the evidence too: Angular never calls an instance spy on a
|
|
4062
|
-
hook it read off the prototype, but a spec that calls `component.ngOnInit()` itself does, and so does
|
|
4063
|
-
the injector for a service's `ngOnDestroy`. `prefer-create-mock` (5.23.0) is graded on what its
|
|
4064
|
-
repair _costs_: the evidence is exact — the literal and the type it claims are written on the same
|
|
4065
|
-
line — but accepting the suggestion hands that literal to the compiler, and on a 2 032-file consumer
|
|
4066
|
-
that is 1 200 findings in 327 files going red on one day. `no-unasserted-argument` (5.23.0) is graded on what its
|
|
4067
|
-
repair _needs_ rather than on its evidence: both of its readings are facts out of the file — a
|
|
4068
|
-
subject some other test in the same file pins with `toHaveBeenCalledWith`, a title saying `with` over
|
|
4069
|
-
a body whose every assertion is a bare call — but the repair is the argument list the test should
|
|
4070
|
-
have named, which is the one thing the rule cannot supply. Every `error` here names an edit or names
|
|
4071
|
-
a helper; this one names a question for the author. `prefer-spy-on-own-method` names a shorter spelling of a correct call — the
|
|
4072
|
-
`createSpyFromInstance` it reports does exactly what the helper does — and its exact shapes carry a fix. Set any of the eight `warn`s to `'error'` once the batch is done. Three of
|
|
4073
|
-
them can report on a _correct_ project, and only one has an option:
|
|
4074
|
-
`jasmine-namespace-without-entry` takes `['error', { setupModules: ['./test-setup'] }]`, naming the
|
|
4075
|
-
file where `enableJasmineCompat()` is called; `prefer-native-spy-api` goes `'off'` for as long as a
|
|
4076
|
-
suite is still running on the jasmine bridge it reports; and `no-unregistered-inject-spy` has no
|
|
4077
|
-
option and needs none — it stays silent unless the file calls `provideAutoSpy` and its whole
|
|
4078
|
-
`providers` array is readable (a spread, an unknown factory, `createWithAutoSpies`, `renderShallow`
|
|
4079
|
-
or `TestBed.overrideProvider` all silence it), so the residue is a scoped `'off'`. The last four are
|
|
4080
|
-
for a suite mid-migration off `jasmine-auto-spies` (§20). `no-mocked-for-spy` only ever touches a
|
|
4081
|
-
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
4082
|
-
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
4083
|
-
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
|
4084
|
-
helper's parameter, `as unknown as Mocked<T>`. It declines where it cannot prove the rename (a
|
|
4085
|
-
`Mocked` the file declares itself, a `Spy` that is already something else, `Mocked<{ a: Mock }>`
|
|
4086
|
-
rather than a named type) and reports without a fix.
|
|
4087
|
-
|
|
4088
|
-
**The autofix is narrower than that licence.** A declaration is decidable; what the name is
|
|
4089
|
-
_assigned_ two lines below is not — `--fix` once renamed a declaration to `Spy<T>` and left an object
|
|
4090
|
-
literal beneath it that the new type rejects, so `eslint --fix` reported clean and `tsc` failed
|
|
4091
|
-
afterwards. The plain fix now survives only where the value came out of `createSpyFromClass`,
|
|
4092
|
-
`createAutoMock`, `createMock`, `mockDeep`, `injectSpy`, `asSpy` and friends, or where the annotation
|
|
4093
|
-
belongs to no variable (a parameter, a return type, an `as` expression). Everywhere else the same
|
|
4094
|
-
edit is a **suggestion** — accept it together with the repair at the creation site, usually
|
|
4095
|
-
`createAutoMock<T>()` in place of the literal. **After any `--fix` run over specs, run
|
|
4096
|
-
`npx tsc --noEmit`**: a lint pass reporting clean is not evidence that the types still hold.
|
|
4097
|
-
|
|
4098
|
-
`no-overridden-provider` is the one that catches a defect rather than a habit. Angular keeps the
|
|
4099
|
-
**last** provider for a token, so `[provideAutoSpy(X), { provide: X, useValue: mockX }]` is not an
|
|
4100
|
-
auto-spy with configuration — the auto-spy is dead and the hand-rolled double is what DI hands out
|
|
4101
|
-
(found on eight tokens of one file). It reads both spellings in either order and compares tokens as
|
|
4102
|
-
source text. A **verbatim duplicate** was already being ignored by Angular, so it carries a
|
|
4103
|
-
suggestion to delete the dead copy (a suggestion, never `--fix`: unattended deletions inside a
|
|
4104
|
-
`providers` array are not something to find in a diff). When
|
|
4105
|
-
the survivor is the **barer** of the two — `provideAutoSpy(X, { gettersToSpyOn: […] })` above a bare
|
|
4106
|
-
`provideAutoSpy(X)` — there is no edit, because which one to keep is the question; move the
|
|
4107
|
-
configuration onto the survivor or delete it. Both messages name the token and the surviving
|
|
4108
|
-
provider's line. A `multi: true` registration is exempt on both sides:
|
|
4109
|
-
Angular accumulates multi providers rather than keeping the last, so two of them for one token is
|
|
4110
|
-
the feature — a spec asserting that two `BEFORE_INIT` hooks run in registration order needs both.
|
|
4111
|
-
Multi mixed with plain is still reported, because Angular refuses that pair at runtime
|
|
4112
|
-
(`Cannot mix multi providers and regular providers`). For the same reason `prefer-provide-auto-spy`
|
|
4113
|
-
says nothing about a multi provider: `provideAutoSpy` takes no registration mode, so the replacement
|
|
4114
|
-
it would ask for does not exist.
|
|
4115
|
-
|
|
4116
|
-
`no-import-time-spread` is the one that fails **while the bundle loads**, on a tree whose every test
|
|
4117
|
-
passes: `export const events = [...BaseEvents]` at module scope raises
|
|
4118
|
-
`Spread syntax requires ...iterable[Symbol.iterator] to be a function` when a shared chunk is
|
|
4119
|
-
evaluated while the binding it re-exports is still `undefined`. Safe under `tsc` and under a
|
|
4120
|
-
browser's ESM loader, which is why nothing else catches it. A function body and an instance field are
|
|
4121
|
-
not reported (they run later); a `static` field is. An **object** spread is reported through a second
|
|
4122
|
-
message, `noImportTimeSpreadObject`, because that half fails without failing: `[...undefined]` and
|
|
4123
|
-
`f(...undefined)` throw, `{ ...undefined }` is `{}`, so `{ ...SectionItemType, ...Local }` loads
|
|
4124
|
-
clean and every key it meant to copy reads `undefined` for the rest of the run. One message cannot
|
|
4125
|
-
carry both: a reader sent looking for `Spread syntax requires …` in an object spread finds no such
|
|
4126
|
-
error in the log and takes the report for a false positive.
|
|
4127
|
-
|
|
4128
|
-
`prefer-as-spy` is the one a migration meets in bulk: a `jest-auto-spies` suite writes
|
|
4129
|
-
`TestBed.inject(X) as Spy<X>` once per injected double, and that cast fails here with `TS2352`.
|
|
4130
|
-
`asSpy` is a typed identity function, so `--fix` keeps the assertion, carries the type arguments
|
|
4131
|
-
across (inference answers `Spy<Service<any>>` for a generic class) and repairs the imports. A cast
|
|
4132
|
-
that hops through `unknown` is left alone — the hop says the value is not a `T` — except after
|
|
4133
|
-
`TestBed.inject(X)`, where the container returns `X` by construction and the hop was only silencing
|
|
4134
|
-
`TS2352`. Neither rule is for the object under test: a service the spec exercises is not a double,
|
|
4135
|
-
and typing it as the class is the repair there.
|
|
4136
|
-
|
|
4137
|
-
`no-expect-in-subscribe` reports one shape and **three different edits**, and says which: the
|
|
4138
|
-
subscription is the last thing the test does (invert it into `await firstValueFrom`); something
|
|
4139
|
-
after it is what makes the stream emit (hold the promise — `const p = expectEmission(src$)`, fire
|
|
4140
|
-
the trigger, `await p` — because inverting deadlocks); or the assertion is in the `error` branch
|
|
4141
|
-
(`await expect(firstValueFrom(src$)).rejects.toMatchObject(…)`). It also counts assertions the
|
|
4142
|
-
callback reaches through a helper it calls, which used to make `subscribe((d) => assertShape(d))`
|
|
4143
|
-
invisible. `prefer-provide-auto-spy` reads `useFactory` as well as `useValue`, through the function
|
|
4144
|
-
in the first case and not in the second — a factory's body is what DI ends up holding, while a
|
|
4145
|
-
function inside a `useValue` is a lazily-built double, i.e. the fix. Three rules change behaviour
|
|
4146
|
-
when applied — whether `injectSpy(X)` finds a spy is decided by a `provideAutoSpy(X)` usually
|
|
4147
|
-
written in another file, `mockValueProp` leaves the property writable and configurable, and
|
|
4148
|
-
`no-expect-in-subscribe` rewrites a whole test — so `prefer-inject-spy`,
|
|
4149
|
-
`no-object-define-property` and `no-expect-in-subscribe` only ever suggest:
|
|
4150
|
-
|
|
4151
|
-
```ts
|
|
4152
|
-
it('maps the products', () =>
|
|
4153
|
-
// ❌ flagged, and a suggestion is offered
|
|
4154
|
-
new Promise<void>((done) => {
|
|
4155
|
-
service.getProducts(id).subscribe((products) => {
|
|
4156
|
-
expect(products).toEqual(expected);
|
|
4157
|
-
done();
|
|
4158
|
-
});
|
|
4159
|
-
}));
|
|
4160
|
-
|
|
4161
|
-
it('maps the products', async () => {
|
|
4162
|
-
// ✅ what accepting it produces
|
|
4163
|
-
const products = await firstValueFrom(service.getProducts(id));
|
|
1952
|
+
In [`agent-docs/eslint.md`](./agent-docs/eslint.md). Read it when configuring `vitest-auto-spy/eslint-plugin` or fixing one of its reports.
|
|
4164
1953
|
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
```
|
|
4168
|
-
|
|
4169
|
-
That template was 111 of 133 violations in one migration batch. The suggestion appears only for the
|
|
4170
|
-
exact frame above — one `subscribe` statement in the executor, one block-bodied callback, `done()`
|
|
4171
|
-
mentioned once and standing last — and the report itself now counts assertions per `subscribe`
|
|
4172
|
-
rather than one message per `expect`, which used to double the apparent size of the job.
|
|
4173
|
-
|
|
4174
|
-
`prefer-inject-spy` reads both spellings of the same mistake, which is the point of the second one:
|
|
4175
|
-
|
|
4176
|
-
```ts
|
|
4177
|
-
vi.spyOn(TestBed.inject(AppEventsService), 'announce'); // flagged, always was
|
|
4178
|
-
const appEvents = TestBed.inject(AppEventsService);
|
|
4179
|
-
const announceSpy = vi.spyOn(appEvents, 'announce'); // flagged now
|
|
4180
|
-
```
|
|
4181
|
-
|
|
4182
|
-
The variable is resolved through the scope manager, so it has to be a `const`/`let` initialised
|
|
4183
|
-
from `TestBed.inject(...)` and never assigned again — a name bound by an import, a parameter, or a
|
|
4184
|
-
`let` that is reassigned is left alone.
|
|
4185
|
-
|
|
4186
|
-
Five tokens are left alone whatever is spied on them: `ApplicationRef`, `DestroyRef`,
|
|
4187
|
-
`EnvironmentInjector`, `HttpClient`, `Injector`. Each is a case where this rule's advice is
|
|
4188
|
-
impossible or self-defeating — `{ provide: DestroyRef, useValue }` is silently ignored, because
|
|
4189
|
-
`R3Injector.get()` short-circuits on `__NG_ENV_ID__` before it reads its providers; a spied
|
|
4190
|
-
`ApplicationRef` has no `injector` to build a component with; `HttpClient` under
|
|
4191
|
-
`provideHttpClientTesting()` is supposed to be real. A project adds its own with
|
|
4192
|
-
`['error', { ignoreTokens: ['MapRendererService'] }]`, compared as source text, which extends the
|
|
4193
|
-
five rather than replacing them.
|
|
4194
|
-
|
|
4195
|
-
`no-inject-before-override` catches the trap this plugin's own advice sets. `TestBed.inject()` in a
|
|
4196
|
-
`beforeEach` — the line you write once `provideAutoSpy(X)` has taken away the literal you used to
|
|
4197
|
-
configure — instantiates the module, and every `TestBed.override*` afterwards throws, including one
|
|
4198
|
-
written above it inside a `createComponent` helper. Configure the double after the overrides
|
|
4199
|
-
(`injectSpy(X)` in the test), or keep the access lazy: `const api = () => injectSpy(Api)`. The check
|
|
4200
|
-
is order-free by design, since a helper declared above the hook still runs after it.
|
|
4201
|
-
|
|
4202
|
-
`no-ts-expect-error-on-double` reports the suppression, not the reason written after it. On one
|
|
4203
|
-
1759-file suite it reports 34 directives in 15 files and **every one carries a reason** — so an
|
|
4204
|
-
escape for "a directive with a reason" would have silenced all of them. Four sat on overloaded
|
|
4205
|
-
clients, which `overload` repairs (§5); seven blamed "the collapsed generic" for a fixture the real
|
|
4206
|
-
instantiation rejects as well; nineteen hid a fixture or a production type that disagrees with the
|
|
4207
|
-
declared one. Four were deliberate — a value outside the declared type, to reach a default branch —
|
|
4208
|
-
and that is the case a per-line disable is for:
|
|
4209
|
-
`// eslint-disable-next-line vitest-auto-spy/no-ts-expect-error-on-double -- <why>` above the
|
|
4210
|
-
directive. The report sits on the directive's line so that comment reaches it. `rejectWith`,
|
|
4211
|
-
`failWith` and `throwWith` are not read: their parameter is `unknown`, so a directive there
|
|
4212
|
-
suppresses something other than the stub.
|
|
4213
|
-
|
|
4214
|
-
**What the plugin costs is bounded by the file, not by what is in it.** Three rules used to re-read
|
|
4215
|
-
the file per finding-site rather than once: over this repository's 173 spec files the whole plugin
|
|
4216
|
-
takes **56 ms** where it took 93, and on a single 1.7 MB spec **68 ms** where it took 3 263. The
|
|
4217
|
-
rules answer the same way they did — the ordering rules (`no-inject-before-override`,
|
|
4218
|
-
`no-overridden-provider`) collect the `override*` and `resetTestingModule` positions in one pass and
|
|
4219
|
-
decide by range, `prefer-render-shallow` asks the template-read question once per file, and
|
|
4220
|
-
`no-redundant-smoke-test` indexes identifiers only where a smoke test exists to judge.
|
|
4221
|
-
|
|
4222
|
-
**A rule message names what it found and one repair, and ends in `Docs: <url>`** — the rule's own
|
|
4223
|
-
section of the rules page (`utilities/eslint-rules#<rule-name>`), which is also its `meta.docs.url`.
|
|
4224
|
-
The reasoning a message leaves out lives in that section; read it before overriding a severity.
|
|
4225
|
-
|
|
4226
|
-
The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
|
|
4227
|
-
packages, which a subpath export can never be.
|
|
1954
|
+
<!-- agents-outline: agent-docs/eslint.md -->
|
|
1955
|
+
<!-- /agents-outline -->
|
|
4228
1956
|
|
|
4229
1957
|
---
|
|
4230
1958
|
|
|
4231
1959
|
## 17. Error → fix
|
|
4232
1960
|
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
| `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
|
|
4238
|
-
| `… is not subscribable (…)` | the argument is not an observable — most often the value it emits, or a promise | pass the observable itself; `await` a promise directly (§8) |
|
|
4239
|
-
| `expectEmission(x): no value within N ms (0 received)` | nothing triggered the stream — or, when the message says timers are fake, the callbacks feeding it wait on a clock this watchdog never advances | check the call that should make it emit and the spy feeding it (`nextWith`); under fake timers pass `{ advance: () => vi.advanceTimersByTime(ms) }` (§8) |
|
|
4240
|
-
| ``… the `advance` callback threw: …`` | the `{ advance }` callback failed; the original is on `cause` and the subscription is torn down | fix the callback — a `vi.runAllTimers()` with no fake timers installed is the usual one (§8) |
|
|
4241
|
-
| `expectEmissions(source$, 0) can never succeed` | a count below 1 — usually `expectEmissions(s, expected.length)` with an empty expectation | `expectNoEmission(source$)`, which is the assertion that was meant (§8) |
|
|
4242
|
-
| `setEmissionTimeout(NaN) needs a non-negative number of milliseconds` | arithmetic on something unset reached the setter | pass a number, `0` or `Infinity` for no watchdog (§8) |
|
|
4243
|
-
| `Type 'SubjectLike<T>' is not assignable to type 'Subject<T>'` | 4.0.0: `returnSubject()` is rxjs-typed only where `vitest-auto-spy/rxjs` is in the TypeScript program, and it is not in this one | put `import 'vitest-auto-spy/rxjs';` in a file the spec `tsconfig` includes — a `setupFiles` entry outside `include` is the usual cause |
|
|
4244
|
-
| `Cannot read properties of undefined (reading 'returnValue')` on `spy.m.and.…` | the jasmine namespaces are not installed — the spy was built before them, or nothing installed them at all | import from `vitest-auto-spy/jasmine`; on Bun / `node:test`, `enableJasmineCompat()` in the setup file (§20) |
|
|
4245
|
-
| `spy.withArgs is not a function` | the same, seen from the argument-matching side | the same — or write `spy.m.calledWith(a).mockReturnValue(v)`, which needs no layer (§20) |
|
|
4246
|
-
| `withArgs(…).and.callFake() is not supported` | `callFake` / `callThrough` / `returnValues` install an implementation, which answers every call rather than one argument list | `.withArgs(…).and.returnValue(v)` / `.throwError(e)` / `.resolveTo(v)`, or take the whole spy with `spy.and.callFake(…)` (§20) |
|
|
4247
|
-
| `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
|
|
4248
|
-
| `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
|
|
4249
|
-
| a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
|
|
4250
|
-
| `onlyMethodsToSpyOn names 'x' (did you mean 'y'?), not a method of X` | a typo, or a method the constructor assigns (an arrow property, a `signal()` field, a `signalStore()` method) | take the name the message suggests; with no suggestion it names `instanceMethodsToSpyOn: ['x']`, which adds to the discovered methods (§5) |
|
|
4251
|
-
| `All declarations of 'Matchers' must have identical type parameters` | a custom matcher declares `interface Matchers<T = any>` (the Vitest 4 shape) while Vitest 5 declares `Matchers<R, T>` | declare it on `Chai.Assertion` instead — that interface has no type parameters in either major, so one declaration merges on both (§10) |
|
|
4252
|
-
| `toHaveBeenCalledTimes` reads `0` for a call an earlier test made, after moving to Vitest 5 | `clearMocks` defaults to `true` on Vitest 5, so `vi.clearAllMocks()` runs before every test | count the calls in a plain variable instead of reading the spy, or set `clearMocks: false` (§10) |
|
|
4253
|
-
| `X.method is set up with mustBeCalledWith, and this call matches none of its configs` | the code called the spy with other arguments; with one config set up the line ends with the first that differs (`argument 2: expected 'eu', got 'us'`). `X.` appears on a strict double only | that is the assertion firing — fix the value the code under test passes, or configure that call too; relax to `calledWith` when other arguments are fine |
|
|
4254
|
-
| `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
|
|
4255
|
-
| `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
|
|
4256
|
-
| `advanceTimers() found only the clock faked, not the timers` | `mockSystemTime()` installed `Date` alone, so there is nothing to advance and the call used to pass having done nothing | `setupFakeTimers()` (or `vi.useFakeTimers()`) in the test that drives timers (§11) |
|
|
4257
|
-
| `X.method(…) was called; this strict double has nothing configured for it` | a strict double was asked for a method no line configured | paste the call the `Configure it in the test:` line names (`calledWith(args).mockReturnValue(…)`, or `mockReturnValue(…)` for any); `Called from` is the calling line; or `{ strict: false }` on that double (§5) |
|
|
4258
|
-
| `X.member was read N times on a strict double and nothing configured it` (or `was subscribed to N times on a strict double and never emitted`) | `setupAutoSpy({ unconfiguredReads })`, after the test — the code under test read a strict double's spied getter and got `undefined`, or subscribed to its observable property and nothing ever emitted | the advice under each finding names the member: `accessorSpies.getters.url.mockReturnValue(…)` for a getter, `events.nextWith(…)` or `overrides: { events: new Subject() }` for a stream; `mockReturnValue(undefined)` when `undefined` is the answer meant (§5) |
|
|
4259
|
-
| `[vitest-auto-spy] "…" wrote to console.<method> N time(s) and nothing absorbed it` | `setupAutoSpy({ strayConsole })` — the test printed, and nothing that does not call through stood on `console`: no `/console` spy installed for it, no `mockImplementation`. A bare `vi.spyOn(console, m)` calls through, and a `console.warn` from this library counts too | the report names the spy to assert (`installConsoleSpies()` in a `beforeEach`, then `consoleErrorSpy`, …), or says the `vi.spyOn` calls through and needs `.mockImplementation(() => undefined)`; or fix the code that printed. Importing a spy installs nothing under the guard |
|
|
4260
|
-
| `… wrote to the console N time(s) outside any test` | output while the file was imported, in a `beforeAll` / `afterAll`, from a callback after its test ended, or from a test whose `afterEach` never ran — the frame names the line | move the work into a test and absorb it there; a callback firing late is a leak — clear it where it is scheduled |
|
|
4261
|
-
| `… wrote to console.<method> N time(s) while the file was being imported` | a module the file imports wrote while it was evaluated (a static block, a decorator, a top-level call), or a `describe` body did — before any hook, so no spy absorbs it; it lands on the first file of the worker to import that module | fix the line the report names, reading the `Likely cause:` block (`NG0912`: two copies of one component in the bundle — import it from one place); `strayConsole: { allow: [...] }` only for a line inside `node_modules`, which the report offers itself |
|
|
4262
|
-
| `It is reported at the end of b.spec.ts: the file-end check of a.spec.ts did not run.` | `a.spec.ts`'s own `afterAll` threw, so its file-end checks never ran and its console output was carried into the next file | fix that `afterAll` first — its error is in `a.spec.ts`'s output — then the output the report quotes, in the file it names |
|
|
4263
|
-
| `"…" made N call(s) a strict double had nothing configured for, and the throw never reached the test` | `setupAutoSpy({ strict: true })` — a strict double threw, and a `catch` in the code under test or an RxJS subscriber with no error callback swallowed it; each call is quoted with its arguments and the line that made it | configure that method (`.mockReturnValue`, `.calledWith` or `returns`), or drain it with `takeStrictViolations()` when the test provokes it on purpose (§5) |
|
|
4264
|
-
| `a.spec.ts left N timer(s) pending when it ended` | `onStrayTimers: 'throw'` — a timer outlived its file; each line names the test that scheduled it (or `while the file was imported`, `outside any test`), and it was cancelled | clear it in the test named — `clearTimeout`, unsubscribe, `fixture.destroy()` — or run it out with fake timers before the test ends (§10) |
|
|
4265
|
-
| `a.spec.ts left N window/document listener(s) attached when it ended` | `onStrayListeners: 'throw'` — a listener outlived its file; each line names the test that added it, and it was removed | remove it in that test — `removeEventListener` with the same capture flag, an `AbortSignal`, `fixture.destroy()` (§10) |
|
|
4266
|
-
| `N file-end checks failed when a.spec.ts ended` | several file-end reports failed together — stray timers, listeners, console output — and each numbered block is one of them | fix each block on its own; Vitest also lists every member error separately |
|
|
4267
|
-
| `fetch is stubbed in unit tests — GET <url>. The test "…" requested it` | `blockNetwork` refused a request the named test made (a blocked `XMLHttpRequest` carries the same marker on `statusText`) | answer it in that test — `vi.spyOn(globalThis, 'fetch').mockResolvedValue(stubResponse({ body: … }))` — or `blockNetwork({ xhr: 'empty' })` for an XHR nobody reads (§10) |
|
|
4268
|
-
| `X.member.nextWithValues() ran after something subscribed to X.member` | `nextWithValues` on an observable property publishes a new stream, which the subscriber already holding the old one never sees; `X.` appears on a strict double only | call it before the code under test subscribes, or push into the held stream with `nextWith()`; a throw under `setupAutoSpy({ misconfiguration: 'throw' })` |
|
|
4269
|
-
| `assertMocked(…): x is the real function — … did not apply` (or `no export is a mock`) | the module mock did not reach the code under test; the message names the cause that fits the run — `isolate: false` with the module already loaded, or the code importing it through another path | under `isolate: false` mock it in the earlier file too or run this file isolated; otherwise `vi.mock` the specifier the code imports, or inject the dependency |
|
|
4270
|
-
| `setupAutoSpy({ strict: true })` has no effect — no double throws, a suite with it on stays green | before this release the default lived in module scope, and `/setup` and the factory bundles each carried a copy; a 1759-file consumer ran 12 717 tests green with it on | upgrade — the default, `onUnstubbedCall` and `setSpyEngine()` now live on `globalThis` |
|
|
4271
|
-
| a seeded getter reads `undefined`: `provideAutoSpy(X, { overrides: { flagsConfig } })` next to `gettersToSpyOn` or a `registerAutoSpyDefaults` row | before this release the seed was assigned into the spied accessor's setter, and the getter kept answering `undefined` | upgrade — `overrides` now seeds the getter spy; a later `accessorSpies.getters.x.mockReturnValue(…)` still wins |
|
|
4272
|
-
| `Cannot read properties of undefined (reading 'name')` on `value.constructor.name`, with `createAutoMock<T>()` | `constructor` answered `undefined` on an auto-mock | upgrade — it answers `Object`, as every other double does; or seed it: `createAutoMock<T>({ constructor: … })` |
|
|
4273
|
-
| `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 |
|
|
4274
|
-
| `Cannot spy on this instance in place: it is not extensible` | `createSpyFromInstance` was handed a frozen or sealed object | spy it before it is frozen, or build a separate double with `createSpyFromClass` (§2) |
|
|
4275
|
-
| `createSpyFromInstance(X): no onlyMethodsToSpyOn was given for a live DOM/BOM object` | discovery is about to walk the whole prototype chain of a live `Node`, `window` or other engine-provided event target (not a user class extending `EventTarget`), engine internals included — the bare-array shorthand does not avoid this, since it merges as `methodsToSpyOn`, not `onlyMethodsToSpyOn` | list the methods the test needs: `{ onlyMethodsToSpyOn: [...] }`; for one method on a node that must keep living a DOM life, `mockValueProp` / `spyOnVoidMethod` instead (§2, live DOM node) |
|
|
4276
|
-
| `Cannot mock the property 'X': it is not configurable` | the member was defined with `configurable: false`, so it cannot be redefined | members replaced before it are still restorable — `restoreSpiedInstance(instance)` or `restoreMockedProps()` |
|
|
4277
|
-
| `createSpyFromInstance`: `Cannot spy on 'X' in place: it is a non-configurable, non-enumerable own property` | the value can be rewritten but not made enumerable, so `resetAutoSpy` would never find the spy | spy it alone with `mockValueProp(instance, 'X', vi.fn())`, or build a double with `createSpyFromClass` |
|
|
4278
|
-
| `overrideComponentProvider(…): the override did not apply — C resolved T to …` | the component injects a different token, or something configured the token again after the call; the message says whether the real service or a different double won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
|
|
4279
|
-
| `.value() was read while the resource is in the error state: …` | `mockResourceProp`'s double after `fail(reason)`, as a real `ResourceRef` behaves; the reason is on `cause` | branch on `hasValue()` / `status()` first, or assert with `toHaveResourceError()` (§13) |
|
|
4280
|
-
| `@angular/core <version> no longer carries …, which this package reads` | an Angular internal this package reads has moved; checked once per worker | nothing is fixable from a spec — report the version in the message and pin the previous Angular until a release reads the new shape (§13) |
|
|
4281
|
-
| `provideHttpTesting(): globalThis.__vitest_worker__ is not there` | the runner does not say which test is running, so the end-of-test check cannot arm — `bun:test`, `node:test` | call `verifyNoPendingRequests()` yourself (§13) |
|
|
4282
|
-
| `ngModuleScopes: X is imported into the testing module but contributes nothing` | `enableAngularDiagnostics({ ngModuleScopes })` — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the directives it exports are missing (`NG0303` / `NG0304`) | import the directives it exports directly, or declare them in the TestBed (§13) |
|
|
4283
|
-
| `assertNgModuleScopes(): X has an empty runtime scope` | the same stripped `ɵɵsetNgModuleScope`, found by the manual check; a module with providers of its own may be providers-only, and the message says so | declare what the spec needs in the TestBed module directly; leave a providers-only module out of the call (§13) |
|
|
4284
|
-
| `Cannot read properties of undefined`, stack inside the component's own constructor or a field initializer | a member the double must **hold** rather than spy on is read while the component is being constructed, and the `mockReadonlyProp` that seeds it is written _below_ `TestBed.createComponent` — so it has not run yet, and never will | seed it **before** construction: `provideAutoSpy(X, { overrides: { paymentParams: … } })`, or `provideAutoSpyForToken(TOKEN, { … })`. No later call can repair this — the throw happens inside `createComponent`, so the helper is not reached and cannot say so; the stack names the component (§9, §13) |
|
|
4285
|
-
| `arr.map(asInstance)` infers `unknown[]` | `asInstance` is overloaded, and passing it **by reference** resolves against the `DeepMockProxy<T>` overload, from which `T` cannot be inferred | `arr.map((spy) => asInstance(spy))` — the arrow gives each call its own inference. For a fixed set, `asInstances(a, b, c)` keeps the tuple types (§6) |
|
|
4286
|
-
| `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
|
|
4287
|
-
| `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
|
|
4288
|
-
| `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
|
|
4289
|
-
| `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) |
|
|
4290
|
-
| `injectSpy(X): got a real X` (or `got a plain value, not an auto-spy` for an `InjectionToken`) | nothing in the testing module provides a double; a `console.warn` once per token and spec file, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` or `setupAutoSpy({ misconfiguration: 'throw' })` | the fix the message names: `provideAutoSpy(X)` in `providers`, or `{ provide: TOKEN, useValue: createAutoMock<T>() }` for a token; `TestBed.inject(X)` if the real thing is what the spec wants |
|
|
4291
|
-
| `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) |
|
|
4292
|
-
| `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) |
|
|
4293
|
-
| `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 })` |
|
|
4294
|
-
| 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 |
|
|
4295
|
-
| `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
|
|
4296
|
-
| `cannot read "…" referenced by …: ENOENT at …` | a `templateUrl` / `styleUrl` path does not resolve; the errno (`ENOENT`, `EACCES`) and the resolved path say why | fix the path, relative to the component file |
|
|
4297
|
-
| duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade. A setup file that initialises the Angular test environment before the library loads splits Angular the same way — two copies, a second `TestBed`, and `No mock adapter registered` or overrides that never apply: `resolve.dedupe: ['@angular/core', '@angular/common', '@angular/platform-browser', '@angular/compiler', 'rxjs']` plus `test.server.deps.inline: ['vitest-auto-spy']` (Vitest 1 and up) leaves one copy of each |
|
|
4298
|
-
| `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) |
|
|
4299
|
-
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
4300
|
-
| `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 |
|
|
4301
|
-
| `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) |
|
|
4302
|
-
| `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
|
|
4303
|
-
| `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) |
|
|
4304
|
-
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
4305
|
-
| 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) |
|
|
4306
|
-
| 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) |
|
|
4307
|
-
| `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 |
|
|
4308
|
-
| a spy on a storage method still answering after `vi.restoreAllMocks()` probed green | happy-dom's Storage proxy deletes only stored items, so the restore behind `mockRestore()` never lands; a later spec's `vi.spyOn` gets the same mock with the old calls in it | on by default — `setupAutoSpy()` repairs it at the file boundary; `restoreStorageSpies()` on its own, or `restoreStorageSpies: false` for a worker-lifetime spy (§10) |
|
|
4309
|
-
| a listener callback running against mocks and a DOM it was never written for, in a file that never added it | a `window`/`document` listener a previous file left registered, under `isolate: false` | `setupAutoSpy({ strayListeners: true })` — module-graph registrations stay, per-file ones go at the boundary (§10) |
|
|
4310
|
-
| a global a previous file patched (`ResizeObserver` answering a stub) with no file named | a raw assignment no registry tracks — not `vi.stubGlobal`, not `vi.spyOn` | `setupAutoSpy({ restoreGlobals: true })` puts the worker's baseline back at every file boundary; `guardGlobals` for the non-configurable case (§10) |
|
|
4311
|
-
| `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
|
|
4312
|
-
| `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)` |
|
|
4313
|
-
| `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 |
|
|
4314
|
-
| `runEffect(): this effect has been destroyed` | the fixture was destroyed, or effectRef.destroy() ran | move the call above `fixture.destroy()`, or assert what the teardown left behind |
|
|
4315
|
-
| `trackRecomputations(): this signal keeps no computation` | a plain `signal()`, which holds a value rather than computing one | track the `computed()` that reads it, or `trackEffectRuns()` the effect that does |
|
|
4316
|
-
| `mockSignalProp: '…' is an input() signal` | an `input()` replaced instead of set | `fixture.componentRef.setInput(name, value)`, or `renderShallow(C, { inputs: { … } })` |
|
|
4317
|
-
| `mockSignalProp: '…' is a computed() something has already read` | a `computed()` member swapped after the first render — a `signal().asReadonly()` member is written through instead and is never refused | patch before the first `detectChanges()` / `stable(fixture)`, or expose the member as a writable `signal()` |
|
|
4318
|
-
| `expected a signal (a zero-argument getter), received a spy` | `toHaveSignalValue` was handed a spied method instead of a signal — usually a typo, or a signal member spied as a method | assert the spy itself with `toHaveBeenCalled`, or put a real signal on the property with `mockSignalProp`; the matcher refuses it rather than calling it |
|
|
4319
|
-
| `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
|
|
4320
|
-
| `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
|
|
4321
|
-
| `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
|
|
4322
|
-
| `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
|
|
4323
|
-
| `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
|
|
4324
|
-
| `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
|
|
4325
|
-
| `createComponentStub(): X carries no ɵcmp, ɵdir or ɵpipe to copy` | the class is not an Angular declaration, or the import resolved to `undefined` (a barrel split across chunks) | pass the `@Component` / `@Directive` / `@Pipe` class, imported from its own file (§13) |
|
|
4326
|
-
| `'isKeyEnabled' does not exist in type 'MethodReturns<{ flagsConfig: any; }>'` | generic class: `createSpyFromClass(Config, { gettersToSpyOn: ['flagsConfig'], returns: { … } })` reads `T` back from the list, because the generic class argument is checked after the configuration | generic class: spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` from `/angular` infers it (§6) |
|
|
4327
|
-
| `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
|
|
4328
|
-
| `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
|
|
4329
|
-
| 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 |
|
|
4330
|
-
| 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 |
|
|
4331
|
-
| `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({ … }))` |
|
|
4332
|
-
| `adoptMock() needs a mock that can report its implementation` | a `node:test` `mock.fn()` handed to `adoptMock` | build the double with `createFunctionSpy()` and pass that to the module mock |
|
|
4333
|
-
| `Cannot spy on export "x". Module namespace is not configurable in ESM.` | `vi.spyOn(namespace, 'x')` under Vitest browser mode or a native ESM loader — the namespace is sealed | spy the class / prototype, or `vi.mock(path, { spy: true })` / `moduleNamespace(await importOriginal(), { passthrough: true })` |
|
|
4334
|
-
| `[Function] is not a spy or a call to a spy!` in a Storybook `play` | `expect` from `storybook/test` wraps a function with no own enumerable keys, and an auto-spy keeps its `mock*` methods on a shared prototype | import `expect` from `vitest` in that story file, or `setSpyEngine('runner')` from `/setup` |
|
|
4335
|
-
| `object is not iterable` destructuring a mocked hook tuple | an auto-mock stands for an object, and a Proxy double is not iterable | write the tuple — `mockReturnValue([data, false, null])`; auto-mocks are for the object a hook returns |
|
|
4336
|
-
| `The property "x" is not defined on the object` from `vi.spyOn(Class.prototype, 'x')` | `x` is an instance field (arrow function, signal), not a prototype method | `createSpyFromClass(C, { instanceMethodsToSpyOn: ['x'] })` |
|
|
4337
|
-
| `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 |
|
|
4338
|
-
| `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
|
|
4339
|
-
| 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…())` |
|
|
4340
|
-
| 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 test; then the `mock*Prop` helper the message names |
|
|
4341
|
-
| a spec green alone that fails only in a full run, on something it reads off `<body>` / `<html>` (`querySelector('[data-…]')`, a class) | an earlier file in the worker left an attribute on the shared document | `setupAutoSpy({ documentPollution: 'throw' })` (in `preset: 'strict'`) names the test; take the attribute off in `ngOnDestroy` / `DestroyRef.onDestroy` or an `afterEach` |
|
|
4342
|
-
| `A metric with the name … has already been registered` as a failed suite with 0 failed tests, only in a full run | under `@angular/build:unit-test` with `isolate: false` a workspace module can run its module scope once **per spec file**, while an external package such as `prom-client` keeps one default registry per worker | not something a hook can catch — it throws at import. Look the metric up before creating it: `register.getSingleMetric(name) ?? new Histogram({ name, … })`, or give the module its own `new Registry()` |
|
|
4343
|
-
| `NotSupportedError: This name has already been registered in the registry` at import | the same per-spec-file evaluation reaching a module-scope `customElements.define` — the registry belongs to the worker's document | `if (!customElements.get(name)) customElements.define(name, Element)`; a definition cannot be undone, so nothing can sweep it |
|
|
4344
|
-
| a block of files reported as failed suites with no stack, and zero failing tests | a test left an own enumerable key on `Object.prototype`; `mergeHooks` spreads it and collection dies | `setupAutoSpy()` guards it by default; `prototypePollution` tunes the reaction |
|
|
4345
|
-
| `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()` — a `Date`, `Map`, `Set` or class instance is compared whole and reported as `the element`, where it used to have no string keys and so read as "no difference" |
|
|
4346
|
-
| 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 })` |
|
|
4347
|
-
| 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 |
|
|
4348
|
-
| `settleResource: … never started — its status is 'idle'` | the `params()` / `request()` computation returned `undefined`, so the loader never ran and `value()` is still the default | set the signal that computation reads, `flushEffects()`, then await again — or `{ allowIdle: true }` when idle is the state under assertion |
|
|
4349
|
-
| 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 |
|
|
4350
|
-
| `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 |
|
|
4351
|
-
| `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) |
|
|
4352
|
-
| `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` — never `@ts-expect-error`, which `no-ts-expect-error-on-double` reports (§16) |
|
|
4353
|
-
| `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) |
|
|
4354
|
-
| `setInputs: … declares no input named …` (with a `Did you mean` for a near typo) | the key is not an input of that component — a typo, an input renamed since, or one of its own fields | take the suggestion, or set a name the message lists; a signal the component owns is moved through the signal (§13) |
|
|
4355
|
-
| `setInputs: X carries no ɵcmp, so there are no inputs to set` (or `the component class is undefined`, `X is a @Pipe`) | the fixture's `componentType` is not a compiled `@Component` — a `@Pipe`, a class Angular never compiled, or `undefined` from a barrel split across chunks | pass the `@Component` class the fixture was created from; call a pipe's `transform()` directly; for `undefined` import the class from its own file (§13) |
|
|
4356
|
-
| `TypeError: dependencies is not a function`, from `renderShallow({ keepTemplate: true })` | the compiled `ɵcmp.dependencies` is the flat array AOT emits when no cycle forced a factory, and the trim called it | upgrade — all three shapes (array, factory, `null`) are read; nothing to change in the spec |
|
|
4357
|
-
| `renderShallow(X, { keepTemplate: true }): … is declared by Module, not standalone` (or `by an NgModule`) | an AOT dependency list: ngtsc flattened an imported `NgModule`'s exported declarations into it, and Angular takes only standalone declarations and `NgModule`s in `imports` | name that module in `keepModules: [Module]` and it is put back whole — the message names it when the spec imports it; or drop `keepTemplate` when the spec reads TypeScript state only |
|
|
4358
|
-
| `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` |
|
|
4359
|
-
| `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) |
|
|
4360
|
-
| `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` |
|
|
4361
|
-
| `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic and keeps the `readonly` of the source member — deliberately: on a member replaced by a **spied accessor** the assignment reaches the setter spy and the getter still answers `undefined`, so the type error is the loud version of a silent no-op | `mockValueProp(spy, 'X', value)` — it defines rather than assigns, so it works in both cases; `Mutable<Spy<T>>` is the opt-in for plain data members, and `Reflect.set` is **not** a way round it |
|
|
4362
|
-
| `TS2345` from `mockReturnValue` / `mockImplementation` / `mockResolvedValue` on a double that used to compile | the mock surface is `MockInstance<Method>`, so the stub is now checked against the method's own return type | stub with what the method returns; if the double is meant to answer something else, the method's type or the spec's expectation is wrong |
|
|
4363
|
-
| `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` on `mockReturnValue(double)` | 5.1.0: the stub is checked against the method, and a double of the return type is a `Spy<X>`, not an `X` | `mockReturnValue(asInstance(double))` (§6) |
|
|
4364
|
-
| `with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties` on a stub | the stub's type spells an optional member `?: T` where the method's spells `?: T \| undefined` (or the reverse), or a fixture writes `key: undefined`; a `Subject` is invariant, so its type argument has to match exactly | `method.returnSubject()` for a subject typed from the method; omit the key instead of writing `undefined` |
|
|
4365
|
-
| `Types of parameters 'x' and 'y' are incompatible` on `mockImplementation(…)` | 5.1.0: the implementation is checked against the method's parameters, and an annotation in it disagrees | drop the parameter annotations and let the method type them; narrow inside the body |
|
|
4366
|
-
| `TS4111` / `TS2375` / `TS2322` inside a `mockImplementation` body that used to compile | before 5.1.0 the implementation's parameters were `any`, so nothing in its body was checked | fix the body — the error is ordinary once its parameters are typed |
|
|
4367
|
-
| `TS2349: This expression is not callable` on a value read from `spy.mock.calls` | 5.1.0 types `mock.calls` from the method; a listener taken from `addEventListener`'s calls is `EventListenerOrEventListenerObject` | narrow before calling: `if (typeof listener === 'function') listener(event)` |
|
|
4368
|
-
| `is not assignable to parameter of type 'RxMethod<…>'` (or another member type) on `mockReadonlyProp(x, 'm', vi.fn(impl))` | `vi.fn` infers its type from the member the helper replaces, so `impl` must return what the member returns | return the member's type — `Object.assign(vi.fn(), { destroy: vi.fn() })` for an `rxMethod` |
|
|
4369
|
-
| `Argument of type '() => A \| null' is not assignable to parameter of type '(() => A) \| (() => null)'` | the member is a union of call signatures — `@ngrx/signals`' `DeepSignal<A> \| Signal<null>` for a nullable slice — and 5.1.0–5.24.0 typed its stub as the union of both halves | upgrade past 5.24.0, which types the stub as one signature returning the union |
|
|
4370
|
-
| `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` |
|
|
4371
|
-
| 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 |
|
|
4372
|
-
| `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` |
|
|
4373
|
-
| 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; `TestRunSummary.counts` carries the multiplicity, so a duplicate name that dropped from two to one reads `name (×2 → ×1)` instead of vanishing from both sides |
|
|
4374
|
-
| 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) |
|
|
4375
|
-
| a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
|
|
4376
|
-
| a green test whose only line about a call is `spy.m.calledWith(1);` | that is a **stub**, not an assertion — chai's `expect(fn).to.have.been.calledWith(x)` is the one that checks | continue the chain, or `expect(spy.m).toHaveBeenCalledWith(1)` — `no-bare-called-with` (§16) |
|
|
4377
|
-
| `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | the stderr report names the file, the first three timers and the test that scheduled each; `onStrayTimers` gets every one in `timers` (§10) |
|
|
4378
|
-
| `createNestUnit(X): parameter #n of Y is typed as \`Object\``(or`undefined — usually a circular import`) | the compiler emitted no runtime class for that parameter — interface, union, primitive, or a circular import | `@Inject(TOKEN)` plus a `providers` entry, or `@Optional()` to receive `undefined`; `@Inject(forwardRef(() => X))` for the import cycle |
|
|
4379
|
-
| `createNestUnit(X): Y declares n constructor parameter(s) but carries no design:paramtypes metadata` | no `emitDecoratorMetadata` (esbuild / Vite), or `reflect-metadata` loaded after the class | tsc or SWC with `decoratorMetadata`, `@Injectable()`, `reflect-metadata` first; or `@Inject(X)` on every parameter |
|
|
4380
|
-
| `createNestUnit(X): A -> B -> A is a cycle among the classes built for real` | two exposed / `useClass` classes depend on each other; the helper does not resolve `forwardRef` cycles | expose one side less, or provide it |
|
|
4381
|
-
| `createNestUnit(X).spies.get(Y): the unit never asked for that token` | same guard as `createWithAutoSpies` — the spy would not be the one the unit uses | stub the token the unit actually injects (the message lists the auto-spied ones) |
|
|
4382
|
-
| `createNestUnit(X).spies.get(Y): Y is in \`expose\`, so the unit got a real instance` | an exposed class is real, not a spy | read the spies of its own collaborators, or drop it from `expose` |
|
|
4383
|
-
| `expect(component.ngOnInit).toHaveBeenCalled()` fails after `fixture.detectChanges()`, or a hook's `.mockImplementation` never runs | the spy was put on the **instance**; a view calls the hook it read off the class prototype when the component was created | `vi.spyOn(Cls.prototype, 'ngOnInit')` before `TestBed.createComponent`, or assert what the hook does — `no-instance-lifecycle-spy` (§16) |
|
|
4384
|
-
| `TS2345: Argument of type 'InjectionToken<X>' is not assignable to parameter of type 'ClassType<unknown>'`, on `registerAutoSpyDefaults` | the core entry's `registerAutoSpyDefaults` takes a class only — it may not name Angular's token type | import `registerAutoSpyDefaults` (and `clearAutoSpyDefaults`) from `vitest-auto-spy/angular`, which also takes an `InjectionToken` (§5) |
|
|
4385
|
-
| `returns names 'x', not a method of X` (or `which onlyMethodsToSpyOn left out`) | the name is not a method of the double — a typo, or a restricting `onlyMethodsToSpyOn` left it out; on a type-driven double, `then` and `constructor` are never spies; a method the constructor assigns (an ngrx `rxMethod`) is not on the prototype | the message names the fix: the close method it meant, adding it to `onlyMethodsToSpyOn`, or `instanceMethodsToSpyOn` for a constructor-assigned method; the value is silently never returned otherwise (§5) |
|
|
4386
|
-
| a spec green over a `useValue` fixture whose key the code never reads — `queryParams$` on an `ActivatedRoute` | `useValue` is `any`, so a renamed or misspelled key of an object literal compiles | the member's real name — `no-unknown-use-value-key` (§16), with type information; or `provideAutoSpy(X, { overrides })`, which the compiler checks |
|
|
4387
|
-
| a spec runs down the "browser" (or any flag-on) branch whatever its `useValue` says | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` — `useValue` is `any`, and an object is truthy | a value of the token's type (`useValue: false`) — `no-mistyped-use-value` (§16), with type information |
|
|
4388
|
-
| `expectRequest: no request matched … Made instead: …` | the URL or predicate describes nothing the code under test sent | compare with the requests listed — they are the ones actually made; then fix the URL or the predicate (§13) |
|
|
4389
|
-
| `expectRequest: no POST … — but GET … was made` | only the verb differs | pass `{ method }` for the verb the code sends, or fix the verb it sends (§13) |
|
|
4390
|
-
| `expectRequest: no GET …?page=2 — but GET …?page=3 was made; the query differs` | the same path, another query string | name the query the code sends, or match the path alone: `expectRequest('/api/products')` (§13) |
|
|
4391
|
-
| `expectRequest: no request matched … — nothing was requested at all` | an `httpResource()` whose `request()` returned no URL yet, or an `HttpClient` call nothing subscribed to | set the signal the resource reads, or subscribe to the call (§13) |
|
|
4392
|
-
| `this TestBed has no HttpTestingController` | `expectRequest` / `expectNoRequest` on a module that never got the testing backend | `TestBed.configureTestingModule({ providers: [...provideHttpTesting()] })` from `vitest-auto-spy/angular-http` (§13) |
|
|
4393
|
-
| `X was never answered (end of "…")` / `N requests were never answered` | `provideHttpTesting({ verifyOnTeardown })` or `enableAngularDiagnostics({ pendingRequests })` — the test left a request open; the message names its URL and the test | answer it with the line the message prints (`await expectRequest(url).flush(body)`, `controller.expectOne(url).flush(body)` under diagnostics), or assert its absence (`expectNoRequest(url)`); a cancelled one is forgiven with the `ignoreCancelled` opt-in the message names (§13) |
|
|
4394
|
-
| `injectActivatedRoute(): nothing provides ActivatedRoute here` | no `ActivatedRoute` provider in the injector it read | `provideActivatedRoute({ … })` from `vitest-auto-spy/angular-router` in `providers`; `injectActivatedRoute(fixture.debugElement.injector)` for a component-level one (§13) |
|
|
4395
|
-
| `the ActivatedRoute here is … not one provideActivatedRoute() built` | a later provider of `ActivatedRoute` won — `provideRouter()`, `RouterModule`, a `useValue`, `provideAutoSpy(ActivatedRoute)` | list `provideActivatedRoute()` last, or drop the other provider (§13) |
|
|
4396
|
-
| `provideActivatedRoute: @angular/router <version> does not wire ActivatedRoute …` | a router major builds `ActivatedRoute` with different internal constructors | report it with the `@angular/router` version; provide the route by hand until then (§13) |
|
|
4397
|
-
| `provideActivatedRoute({ title }): @angular/router <version> keeps the route title under a key …` | that router version stores the resolved title under a key this helper could not find | leave `title` out of the init for now, and report it with the `@angular/router` version (§13) |
|
|
4398
|
-
| `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `isActive`, `resetConfig`, `lastSuccessfulNavigation` | the double covers `url`, `events`, `navigate`, `navigateByUrl`, `createUrlTree`, `serializeUrl`, `parseUrl`, `routerState` and `currentNavigation` / `getCurrentNavigation()`; past that, a real `provideRouter([])` with `RouterTestingHarness` (§13) |
|
|
4399
|
-
| `injectRouterDouble(): the Router here is Angular's own …` | `Router` is `providedIn: 'root'`, so a `TestBed` without the double hands out a real one | add `provideRouterDouble({ … })` from `vitest-auto-spy/angular-router` to `providers` (§13) |
|
|
4400
|
-
| `collectRouterEvents().expect(): the events differ at #N` (or `#N … never came`, `#N … was not expected`) | the recorded navigation events part from the expected ones there; the message prints both sequences under it | read the `Expected:` / `Recorded:` lines under it, then fix the expectation or the navigation the test drives (§13) |
|
|
4401
|
-
|
|
4402
|
-
**Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
|
|
4403
|
-
nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
|
|
4404
|
-
`gettersToSpyOn: ['X']`; a `mock*Prop` on a `createAutoMock` double changing nothing;
|
|
4405
|
-
`gettersToSpyOn` rejecting a `Signal` key; `mockReadonlyProp` rejecting a real `signal()`;
|
|
4406
|
-
`mockReturnValue` missing on a generic method; a `mock*Prop` patch surfacing in a `describe` that
|
|
4407
|
-
never patched anything.
|
|
4408
|
-
|
|
4409
|
-
**When the double is the suspect and no message names it, print it.**
|
|
4410
|
-
`import { explainSpy } from 'vitest-auto-spy/diagnostics'` — it sits on that entry, not the root,
|
|
4411
|
-
because ESM re-export is eager and a reader calls it at a breakpoint, not from a spec.
|
|
4412
|
-
`console.log(explainSpy(spy))`
|
|
4413
|
-
reports every spied member with its configured argument lists numbered and every recorded call
|
|
4414
|
-
attributed to the config it hit — or to the default it fell through to, which is the shape of "the
|
|
4415
|
-
method answered `undefined` and nothing said why". `explainSpy(spy, 'load')` narrows to one member,
|
|
4416
|
-
`explainSpy(spy.load)` takes a bare function spy, `calledWith` and `mustBeCalledWith` share one
|
|
4417
|
-
numbering, and it never throws — a foreign mock is reported as one in the text. The wording is a
|
|
4418
|
-
diagnostic and is free to change: print it, never assert on it.
|
|
4419
|
-
<https://asdalexey.github.io/vitest-auto-spy/utilities/explain-spy>.
|
|
1961
|
+
In [`agent-docs/errors.md`](./agent-docs/errors.md). A table from the text of a failure to its fix. Do not read it front to back — grep it by the message: `grep -n -F '<text of the error>' node_modules/vitest-auto-spy/agent-docs/errors.md`.
|
|
1962
|
+
|
|
1963
|
+
<!-- agents-outline: agent-docs/errors.md -->
|
|
1964
|
+
<!-- /agents-outline -->
|
|
4420
1965
|
|
|
4421
1966
|
---
|
|
4422
1967
|
|
|
4423
1968
|
## 18. Do not write this
|
|
4424
1969
|
|
|
4425
|
-
| ❌
|
|
4426
|
-
|
|
|
4427
|
-
| `import … from 'jest-auto-spies'`
|
|
4428
|
-
| `vitest-auto-spy` inside a `bun test` file
|
|
4429
|
-
| `let s: MyService = createSpyFromClass(MyService)`
|
|
4430
|
-
| `createSpyFromClass(X) as unknown as X`
|
|
4431
|
-
| `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }`
|
|
4432
|
-
| `{ provide: ActivatedRoute, useValue: { snapshot: { params } } }`
|
|
4433
|
-
| `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate } })` | `provideRouterDouble({ url })` — one URL behind `url`, `routerState` and `events`
|
|
4434
|
-
| `vi.spyOn(TestBed.inject(X), 'method')`
|
|
4435
|
-
| `vi.spyOn(component, 'ngOnInit')` after `createComponent`
|
|
4436
|
-
| `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>`
|
|
4437
|
-
| `Object.defineProperty(service, 'ready', { value: true })`
|
|
4438
|
-
| `source$.subscribe(v => expect(v).toBe(1))`
|
|
4439
|
-
| `await lastValueFrom(done$, { defaultValue: undefined })`
|
|
4440
|
-
| `{ timeout: 0 }` on every helper because timers are faked
|
|
4441
|
-
| `{ provide: AbstractToken, useValue: createAutoMock<T>() }`
|
|
4442
|
-
| `mockDeep<T>()` for a chain that goes through a **call**
|
|
4443
|
-
| `await expect(expectEmission(x$)).rejects.toBe(originalError)`
|
|
4444
|
-
| `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper
|
|
4445
|
-
| hold the promise, `vi.runAllTimers()`, then await
|
|
4446
|
-
| `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach`
|
|
4447
|
-
| a local `injectSpy` wrapper with `as never` + `as Spy<T>`
|
|
4448
|
-
| a hand-written double for a token with `Observable` members
|
|
4449
|
-
| `mockDeep<T>() as unknown as T` to satisfy an API typed against `T`
|
|
4450
|
-
| `from([double])` to stop `of(double)` swallowing the double
|
|
4451
|
-
| `spy.instanceField.mockReturnValue(…)` on a member Angular moved
|
|
4452
|
-
| `expect(component.total).toBeTruthy()` (a signal)
|
|
4453
|
-
| `fixture.detectChanges()` then assert signal state
|
|
4454
|
-
| `onlyMethodsToSpyOn: [...]` "to add a method"
|
|
4455
|
-
| a `vi.fn()` the code calls with `new`
|
|
4456
|
-
| an exported `const` fixture holding `vi.fn()`s
|
|
4457
|
-
| `let s: Mocked<MyService>` (Vitest's own type)
|
|
4458
|
-
| `it('x', (done) => …)` / `beforeEach((done) => …)`
|
|
4459
|
-
| `{ ...modelInstance, flag: true }` (drops every getter)
|
|
4460
|
-
| `if ('params' in link) … else throw` in every spec
|
|
4461
|
-
| `const x = a?.b; assert.exists(x); … x …` on every optional read
|
|
4462
|
-
| `expect(body).toBeInstanceOf(FormData); const form = body as FormData`
|
|
4463
|
-
| five `asInstance(...)` in one call
|
|
4464
|
-
| `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))`
|
|
4465
|
-
| `Object.defineProperty(document, 'cookie', { value })`
|
|
4466
|
-
| `vi.spyOn(globalThis, 'Date')`
|
|
4467
|
-
| ten `await Promise.resolve()` for a dynamic `import()`
|
|
4468
|
-
| a bare `await import('…')` in a test body
|
|
4469
|
-
| a capture whose only assertions are `toEqual([])` / `not.toHaveBeenCalled()`
|
|
4470
|
-
| `{ id: '1', isOffline: false } as Device` (a cast passes an excess key)
|
|
4471
|
-
| `TestBed.inject(S).m as Mock` (`Mock` is `Mock<any>`)
|
|
4472
|
-
| `await fixture.whenRenderingDone()`
|
|
4473
|
-
| an exported `const` provider with `vi.fn()` inside
|
|
4474
|
-
| `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing)
|
|
4475
|
-
| `TestBed.overrideComponent` to swap a provider
|
|
4476
|
-
| `{ target, isIntersecting } as unknown as IntersectionObserverEntry`
|
|
4477
|
-
| `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift
|
|
4478
|
-
| a `TestingStorage` class assigned to `globalThis.localStorage`
|
|
4479
|
-
| an assertion containing a date, with no clock set
|
|
4480
|
-
| `configureTestingModule` inside every `it()`
|
|
4481
|
-
| `vi.mock('@angular/core')` to neutralise `effect()`
|
|
4482
|
-
| a second `vi.spyOn(console, 'error')`
|
|
4483
|
-
| `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install
|
|
4484
|
-
| a bare `vi.spyOn(console, 'error')` to keep a spec quiet
|
|
4485
|
-
| `mockReadonlyProp(c, 'items', vi.fn(() => []))`
|
|
4486
|
-
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)`
|
|
4487
|
-
| `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call
|
|
1970
|
+
| ❌ | ✅ |
|
|
1971
|
+
| --- | --- |
|
|
1972
|
+
| `import … from 'jest-auto-spies'` | `import … from 'vitest-auto-spy'` |
|
|
1973
|
+
| `vitest-auto-spy` inside a `bun test` file | `vitest-auto-spy/bun` |
|
|
1974
|
+
| `let s: MyService = createSpyFromClass(MyService)` | `let s: Spy<MyService> = …` |
|
|
1975
|
+
| `createSpyFromClass(X) as unknown as X` | `asInstance(createSpyFromClass(X))` |
|
|
1976
|
+
| `{ provide: X, useValue: { a: vi.fn(), b: vi.fn() } }` | `provideAutoSpy(X)` |
|
|
1977
|
+
| `{ provide: ActivatedRoute, useValue: { snapshot: { params } } }` | `provideActivatedRoute({ params })` — streams and snapshot from one record |
|
|
1978
|
+
| `createAutoMock<Router>({ events: of(), url: '/' }, { returns: { navigate } })` | `provideRouterDouble({ url })` — one URL behind `url`, `routerState` and `events` |
|
|
1979
|
+
| `vi.spyOn(TestBed.inject(X), 'method')` | `injectSpy(X).method` |
|
|
1980
|
+
| `vi.spyOn(component, 'ngOnInit')` after `createComponent` | `vi.spyOn(Cls.prototype, 'ngOnInit')` before it, or assert the effect |
|
|
1981
|
+
| `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` | `useValue: false` — a value of the type the token declares |
|
|
1982
|
+
| `Object.defineProperty(service, 'ready', { value: true })` | `mockReadonlyProp(service, 'ready', true)` |
|
|
1983
|
+
| `source$.subscribe(v => expect(v).toBe(1))` | `await expect(expectEmission(source$)).resolves.toBe(1)` |
|
|
1984
|
+
| `await lastValueFrom(done$, { defaultValue: undefined })` | `await expectCompletion(done$)` |
|
|
1985
|
+
| `{ timeout: 0 }` on every helper because timers are faked | `setEmissionTimeout(100)` once, in the setup file |
|
|
1986
|
+
| `{ provide: AbstractToken, useValue: createAutoMock<T>() }` | `provideAutoSpy(AbstractToken)` |
|
|
1987
|
+
| `mockDeep<T>()` for a chain that goes through a **call** | `mockDeep<T>({}, { selfReturning: true })` |
|
|
1988
|
+
| `await expect(expectEmission(x$)).rejects.toBe(originalError)` | `await expect(expectError(x$)).resolves.toBe(originalError)` |
|
|
1989
|
+
| `source$.pipe(skip(1))` / `pipe(filter(p))` in front of the helper | `expectEmission(source$, { skip: 1 })` / `{ until: p }` |
|
|
1990
|
+
| hold the promise, `vi.runAllTimers()`, then await | `expectEmission(source$, { advance: () => vi.runAllTimers() })` |
|
|
1991
|
+
| `injectSpy(X)` then a `mockReturnValue` per method in `beforeEach` | `provideAutoSpy(X, { returns: { … }, overrides: { … } })` |
|
|
1992
|
+
| a local `injectSpy` wrapper with `as never` + `as Spy<T>` | the library's — it also takes an `InjectionToken` |
|
|
1993
|
+
| a hand-written double for a token with `Observable` members | `provideAutoSpyForToken(T, undefined, { observablePropsToSpyOn: […] })` |
|
|
1994
|
+
| `mockDeep<T>() as unknown as T` to satisfy an API typed against `T` | `asInstance(mockDeep<T>())` |
|
|
1995
|
+
| `from([double])` to stop `of(double)` swallowing the double | `of(double)` — `schedule` is no longer answered (§2) |
|
|
1996
|
+
| `spy.instanceField.mockReturnValue(…)` on a member Angular moved | `provideAutoSpy(X, { instanceMethodsToSpyOn: ['…'] })` (§5) |
|
|
1997
|
+
| `expect(component.total).toBeTruthy()` (a signal) | `expect(component.total).toHaveSignalValue(3)` |
|
|
1998
|
+
| `fixture.detectChanges()` then assert signal state | `await stable(fixture)` then assert |
|
|
1999
|
+
| `onlyMethodsToSpyOn: [...]` "to add a method" | omit it, or use `instanceMethodsToSpyOn` |
|
|
2000
|
+
| a `vi.fn()` the code calls with `new` | `createSpyClass(Foo)` / `mockConstructor(factory)` |
|
|
2001
|
+
| an exported `const` fixture holding `vi.fn()`s | an exported **factory** that returns it (§10) |
|
|
2002
|
+
| `let s: Mocked<MyService>` (Vitest's own type) | `let s: Spy<MyService>` |
|
|
2003
|
+
| `it('x', (done) => …)` / `beforeEach((done) => …)` | `async` + `await` — Vitest passes a `TestContext`, not `done` |
|
|
2004
|
+
| `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
|
|
2005
|
+
| `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
|
|
2006
|
+
| `const x = a?.b; assert.exists(x); … x …` on every optional read | `narrow.defined(a?.b)` — it returns the value, so the narrowing sits in the expression |
|
|
2007
|
+
| `expect(body).toBeInstanceOf(FormData); const form = body as FormData` | `narrow.instanceOf(body, FormData)` — the check and the type in one expression |
|
|
2008
|
+
| five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
|
|
2009
|
+
| `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
|
|
2010
|
+
| `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
|
|
2011
|
+
| `vi.spyOn(globalThis, 'Date')` | `mockSystemTime('2025-04-30T00:00:00Z')` |
|
|
2012
|
+
| ten `await Promise.resolve()` for a dynamic `import()` | `await settleDynamicImport(() => import('…'))` |
|
|
2013
|
+
| a bare `await import('…')` in a test body | `await settleDynamicImport(() => import('…'))` — `prefer-settle-dynamic-import` reports it |
|
|
2014
|
+
| a capture whose only assertions are `toEqual([])` / `not.toHaveBeenCalled()` | `await expectNoEmission(source$)` — `no-vacuous-absence-assertion` reports it |
|
|
2015
|
+
| `{ id: '1', isOffline: false } as Device` (a cast passes an excess key) | `createMock<Device>({ id: '1' })` — `prefer-create-mock` reports it |
|
|
2016
|
+
| `TestBed.inject(S).m as Mock` (`Mock` is `Mock<any>`) | `injectSpy(S).m` — `no-mock-cast` reports it |
|
|
2017
|
+
| `await fixture.whenRenderingDone()` | `await stable(fixture)` |
|
|
2018
|
+
| an exported `const` provider with `vi.fn()` inside | an exported **factory** returning it (§10) |
|
|
2019
|
+
| `.overrideProvider(X, provideAutoSpy(X))` (works, but says the wrong thing) | `.overrideProvider(X, overrideAutoSpy(X))` |
|
|
2020
|
+
| `TestBed.overrideComponent` to swap a provider | `overrideComponentProvider(Cmp, X)` |
|
|
2021
|
+
| `{ target, isIntersecting } as unknown as IntersectionObserverEntry` | `intersectionEntry(target, true)` |
|
|
2022
|
+
| `@Component({ selector: 'app-chart' }) class MockChartComponent { @Input() … }` | `createComponentStub(ChartComponent)` — the selector cannot drift |
|
|
2023
|
+
| a `TestingStorage` class assigned to `globalThis.localStorage` | `stubWebStorage('localStorage')`, restored with the other patches |
|
|
2024
|
+
| an assertion containing a date, with no clock set | `mockSystemTime(iso)` first |
|
|
2025
|
+
| `configureTestingModule` inside every `it()` | one per `describe` |
|
|
2026
|
+
| `vi.mock('@angular/core')` to neutralise `effect()` | set the signals, `await stable(fixture)`, assert the result |
|
|
2027
|
+
| a second `vi.spyOn(console, 'error')` | `consoleErrorSpy` from `vitest-auto-spy/console`, installed with `installConsoleSpies()` |
|
|
2028
|
+
| `import { consoleErrorSpy } from 'vitest-auto-spy/console'` as the install | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
|
|
2029
|
+
| a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
|
|
2030
|
+
| `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
|
|
2031
|
+
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
|
|
2032
|
+
| `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
|
|
4488
2033
|
|
|
4489
2034
|
**A hand-rolled double trips `rxjs-x/finnish`, and a double from this package does not.** The rule
|
|
4490
2035
|
reports any name whose type is an `Observable` **or whose call signature returns one**
|
|
@@ -4522,15 +2067,15 @@ anyone writing a codemod, because the rename looks like the safest edit in the f
|
|
|
4522
2067
|
Jest-era tutorials and cheat sheets repeat a handful of lines that fail on Vitest, or pass and leak.
|
|
4523
2068
|
Each was checked on Vitest 5.0.0:
|
|
4524
2069
|
|
|
4525
|
-
| Circulating advice
|
|
4526
|
-
|
|
|
4527
|
-
| `const load = vi.fn(); vi.mock('./api', () => ({ load }))` | `vi.mock` is hoisted above every import and `const`: `ReferenceError: Cannot access 'load' before initialization`, wrapped in `[vitest] There was an error when mocking a module` | `const mocks = vi.hoisted(() => ({ load: vi.fn() }))`, then `() => ({ load: mocks.load })`
|
|
4528
|
-
| "name it `mockLoad` and the factory may read it"
|
|
4529
|
-
| `vi.requireActual('./api')` / `jest.requireActual`
|
|
4530
|
-
| `import { jest } from 'vitest'`
|
|
4531
|
-
| `import { userEvent } from '@testing-library/user-event'`
|
|
4532
|
-
| `vi.restoreAllMocks()` "to undo fake timers"
|
|
4533
|
-
| `global.fetch = vi.fn()` in a test
|
|
2070
|
+
| Circulating advice | What happens | Write instead |
|
|
2071
|
+
| --- | --- | --- |
|
|
2072
|
+
| `const load = vi.fn(); vi.mock('./api', () => ({ load }))` | `vi.mock` is hoisted above every import and `const`: `ReferenceError: Cannot access 'load' before initialization`, wrapped in `[vitest] There was an error when mocking a module` | `const mocks = vi.hoisted(() => ({ load: vi.fn() }))`, then `() => ({ load: mocks.load })` |
|
|
2073
|
+
| "name it `mockLoad` and the factory may read it" | Jest's exemption for `mock`-prefixed names does not exist in Vitest — same `ReferenceError` | `vi.hoisted` |
|
|
2074
|
+
| `vi.requireActual('./api')` / `jest.requireActual` | `vi.requireActual` is `undefined` | `vi.mock('./api', async (importOriginal) => ({ ...(await importOriginal<typeof import('./api')>()), load: vi.fn() }))` |
|
|
2075
|
+
| `import { jest } from 'vitest'` | `vitest` exports no `jest`; the binding is `undefined` | `vi` — `vi.fn()`, `vi.spyOn()`, `vi.mock()` |
|
|
2076
|
+
| `import { userEvent } from '@testing-library/user-event'` | the named export exists only from 14.5.0; the default export works on every 14.x, and every 14.x method returns a promise | `import userEvent from '@testing-library/user-event'`; `const user = userEvent.setup(); await user.click(el)` |
|
|
2077
|
+
| `vi.restoreAllMocks()` "to undo fake timers" | `restoreAllMocks`, `resetAllMocks` and `clearAllMocks` leave fake timers installed — `vi.isFakeTimers()` is still `true` | `vi.useRealTimers()` in `afterEach` |
|
|
2078
|
+
| `global.fetch = vi.fn()` in a test | survives `vi.restoreAllMocks()` and `vi.unstubAllGlobals()`, and answers every later test of the file | `mockValueProp(globalThis, 'fetch', …)` or `vi.stubGlobal` + `unstubGlobals: true`; `blockNetwork()` to stay offline; the `no-hand-assigned-global` rule reports the bare form |
|
|
4534
2079
|
|
|
4535
2080
|
---
|
|
4536
2081
|
|
|
@@ -4563,8 +2108,25 @@ file an Angular or Nx unit-test target never runs because the target does not na
|
|
|
4563
2108
|
its checks are about coverage, where the run is green and the report is simply not the one the
|
|
4564
2109
|
config describes — `coverage.all` on a Vitest that stopped reading the key, a source-only
|
|
4565
2110
|
`coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
|
|
4566
|
-
recompiling it per file costs more than collecting the coverage does.
|
|
4567
|
-
|
|
2111
|
+
recompiling it per file costs more than collecting the coverage does. Two are about the Angular
|
|
2112
|
+
builder's version: `@angular/build` in `[22.1.5, 22.1.7)` (`angular-build-splitting-off`), and an
|
|
2113
|
+
`@analogjs/vite-plugin-angular` below 2.7.5 next to `@angular/build` 22.2 or newer
|
|
2114
|
+
(`analog-behind-angular-build`), which dies at startup with `TypeError: cache.has is not a function`.
|
|
2115
|
+
Five are about Vitest 5. On Vitest 5, `vitest-5-removed` reports what it took away and the run now
|
|
2116
|
+
trips over — an import from `vitest/reporters`, `vitest/coverage`, `vitest/environments`,
|
|
2117
|
+
`vitest/snapshot`, `vitest/runners`, `vitest/suite` or `vitest/mocker`, a `.sequential` chain,
|
|
2118
|
+
`--outputJson`/`--compare` in a script — as errors; on Vitest 4 the same lines are upgrade notes.
|
|
2119
|
+
`poolOptions` on Vitest 4 or newer is a warning, because Vitest runs without every option inside it.
|
|
2120
|
+
`vitest-5-deprecated` names the two renamed programmatic calls nothing warns about. `vitest-5-clear-mocks`
|
|
2121
|
+
says `clearMocks: true` restates the Vitest 5 default, and tells a Vitest 4 suite that never set it
|
|
2122
|
+
to price the change with `npx vitest run --clearMocks` first. `vitest-5-available` tells a Vitest 4
|
|
2123
|
+
repository what the upgrade buys — or names what holds it back: `@angular/build` below 22.2.0 beside
|
|
2124
|
+
a unit-test target, Analog below 2.7.5, `vite` below 6.4, or Node below 22.12 in `.nvmrc`,
|
|
2125
|
+
`.node-version`, CI or a private package's `engines`. `fs-module-cache-not-persisted` warns when
|
|
2126
|
+
`fsModuleCache` is on and no CI config caches its directory. Renamed config keys Vitest 5 still
|
|
2127
|
+
honours (`experimental.fsModuleCache`, `browser.isolate`, `cache.dir`, …) are left to Vitest's own
|
|
2128
|
+
deprecation line.
|
|
2129
|
+
It is worth one run after any large edit to a test suite — especially after a codemod, which is where
|
|
4568
2130
|
the eaten glob below came from.
|
|
4569
2131
|
|
|
4570
2132
|
Two of its checks are about this package's own names. `helper-from-wrong-entry` catches a named
|
|
@@ -4637,7 +2199,8 @@ npx vitest-auto-spy perf src/some/dir # a path is passed through to Vitest as a
|
|
|
4637
2199
|
|
|
4638
2200
|
It reads Vitest's own per-file phase timings through `TestModule.diagnostic()` — nothing here
|
|
4639
2201
|
parses terminal output — and reports six phases: `environment`, `import`, `tests`, `setup`,
|
|
4640
|
-
`prepare` (all measured per file) and `transform` (measured once for the whole run
|
|
2202
|
+
`prepare` (all measured per file) and `transform` (measured once for the whole run before Vitest 5,
|
|
2203
|
+
per file on 5, where it is the wait for Vite's transforms and `import`/`setup` are net of it). **The phase
|
|
4641
2204
|
totals are CPU time summed across every worker, not wall clock** — a report showing `20.29s of CPU
|
|
4642
2205
|
time` under `986ms wall clock` is not a bug, it is the work spread over workers.
|
|
4643
2206
|
|
|
@@ -4669,6 +2232,23 @@ about 2.8 % of wall clock. The finding itself counts this machine's cores
|
|
|
4669
2232
|
figures above are in the docs section, not in the message. Do not quote a worker count as
|
|
4670
2233
|
universally right — it is a property of the machine.
|
|
4671
2234
|
|
|
2235
|
+
**On Vitest 5, `perf` reads what Vitest 5 reports; an older Vitest prints exactly what it did before.**
|
|
2236
|
+
The advice reads the configuration Vitest resolved (`isolate`, `pool`, `maxWorkers`, `environment`,
|
|
2237
|
+
`fsModuleCache`) rather than the config text, and never advises against an option you set
|
|
2238
|
+
explicitly. `perf-transform` fires when the wait for Vite's transforms is 30 % or more of the CPU time
|
|
2239
|
+
with the module cache off, and advises `fsModuleCache: true` — kept between CI pipelines, or it only
|
|
2240
|
+
helps locally. `perf-long-pole` names the file still running alone after every other lane went idle,
|
|
2241
|
+
when that tail is at least 2 s and 30 % of the span. `perf-isolation` adds the workers spawned and
|
|
2242
|
+
their summed start-up, with an "at least" wall-clock saving; `perf-workers` counts the lanes the run
|
|
2243
|
+
used rather than the machine's cores; under `isolate: false`, `perf-heap` lists what each file added
|
|
2244
|
+
to its lane's heap. Environment time is counted once per lane (`concurrencyId`) and value — not per
|
|
2245
|
+
`workerId`, which Vitest 5 renews for every file even in a reused worker. A finding about a switch
|
|
2246
|
+
also prints `perf-vitest-doctor`: confirm it with `npx vitest doctor`, Vitest's own A/B runner, not
|
|
2247
|
+
this package's `doctor`. A bare run on Vitest 5 passes `--experimental.diagnostics=false` so Vitest's
|
|
2248
|
+
own hints do not repeat these. The reporter rewrites a `partial: true` report every ~2 s while the run
|
|
2249
|
+
goes, so a killed or timed-out run still leaves one: `perf` prints it under a warning and the gate
|
|
2250
|
+
refuses it like a red run.
|
|
2251
|
+
|
|
4672
2252
|
**A bare `vitest run` is not every repository's suite, and `perf` refuses to pretend otherwise.**
|
|
4673
2253
|
Where the suite is assembled by something else — an Angular builder, an Nx target, a script that
|
|
4674
2254
|
generates a config per project — there is no root `vite(st).config.*`, the Vitest defaults sweep up
|
|
@@ -4704,7 +2284,7 @@ budget and nothing else. And every candidate is re-measured on its own before it
|
|
|
4704
2284
|
file that is fast when it has the machine to itself is reported as _not reproduced_, an `info` rather
|
|
4705
2285
|
than a finding. Without a way to re-measure, findings are warnings that fail nothing unless
|
|
4706
2286
|
`--no-confirm` says one reading is enough. Exit `1` is "over budget", exit `2` is "there was nothing
|
|
4707
|
-
to judge" — including a red suite, which the gate will not judge at all, since a failed test is
|
|
2287
|
+
to judge" — including a red or unfinished (`partial`) suite, which the gate will not judge at all, since a failed test is
|
|
4708
2288
|
measured until its timeout and 30 s of timeout looks exactly like 30 s of slow code.
|
|
4709
2289
|
|
|
4710
2290
|
`--json <path>` re-analyses a report an earlier `--out <path>` run wrote, instead of running Vitest
|
|
@@ -4771,156 +2351,12 @@ the comments before and after and compare against `HEAD`.
|
|
|
4771
2351
|
|
|
4772
2352
|
## 20. Migrating a suite off `jasmine-auto-spies`
|
|
4773
2353
|
|
|
4774
|
-
`
|
|
4775
|
-
`@hirez_io/auto-spies-core`, with identical configuration keys (`methodsToSpyOn`,
|
|
4776
|
-
`observablePropsToSpyOn`, `gettersToSpyOn`, `settersToSpyOn`) and identical helper names.
|
|
4777
|
-
**Exactly one thing differs**: upstream parks its async helpers behind `.and`, because that is where
|
|
4778
|
-
jasmine keeps its own spy strategies.
|
|
4779
|
-
|
|
4780
|
-
```ts
|
|
4781
|
-
spy.load.and.nextWith(account); // jasmine-auto-spies
|
|
4782
|
-
spy.load.nextWith(account); // jest-auto-spies, and this library
|
|
4783
|
-
```
|
|
4784
|
-
|
|
4785
|
-
**Land it green first, rewrite second.** `vitest-auto-spy/jasmine` puts `.and`, `.calls` and
|
|
4786
|
-
`.withArgs` back on every spy, so the import specifier is the only edit needed to get the suite
|
|
4787
|
-
running — and the first red run then has one candidate cause instead of two:
|
|
4788
|
-
|
|
4789
|
-
```diff
|
|
4790
|
-
- import { createSpyFromClass, provideAutoSpy, type Spy } from 'jasmine-auto-spies';
|
|
4791
|
-
+ import { createSpyFromClass, provideAutoSpy, type Spy } from 'vitest-auto-spy/jasmine';
|
|
4792
|
-
```
|
|
4793
|
-
|
|
4794
|
-
```bash
|
|
4795
|
-
npx vitest-auto-spy codemod --from jasmine # dry run
|
|
4796
|
-
npx vitest-auto-spy codemod --from jasmine --write # apply, then drop the import
|
|
4797
|
-
npx vitest-auto-spy codemod --from jasmine --verify # match the result, not the diff
|
|
4798
|
-
```
|
|
2354
|
+
In [`agent-docs/migration-jasmine.md`](./agent-docs/migration-jasmine.md). Read it when moving a suite off `jasmine-auto-spies`.
|
|
4799
2355
|
|
|
4800
|
-
|
|
4801
|
-
(`objectContaining`, `any`, `createSpyObj`, `clock()`, `addMatchers`, and the eight asymmetric
|
|
4802
|
-
matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
|
|
4803
|
-
on `globalThis` — it is one explicit line per file, which the codemod later deletes.
|
|
4804
|
-
|
|
4805
|
-
Four places where that surface now matches jasmine's rather than approximating it:
|
|
4806
|
-
|
|
4807
|
-
- **`spy.withArgs(…).and` carries the strategies that describe one argument list** — `stub()`,
|
|
4808
|
-
`throwError(…)`, `resolveTo(…)` and `returnValue(…)`, plus this library's own `resolveWith` /
|
|
4809
|
-
`nextWith` / `returnSubject` family. The three that install an _implementation_ —
|
|
4810
|
-
`callFake`, `callThrough`, `returnValues` — are present and **throw** a message naming the
|
|
4811
|
-
alternative, rather than arriving as `… is not a function`: an implementation answers every call,
|
|
4812
|
-
which is the opposite of configuring one argument list. Take the whole spy with `spy.and.callFake(…)`
|
|
4813
|
-
if that is what the line meant.
|
|
4814
|
-
- **`jasmine.mapContaining` compares keys with the runner's equality**, as jasmine does, so an
|
|
4815
|
-
asymmetric matcher as a key and an object key compared deeply both work. `Map.has` alone answered
|
|
4816
|
-
on reference identity and missed both.
|
|
4817
|
-
- **`jasmine.clock().install()` leaves `Date` real**, exactly as jasmine's does — the timers are
|
|
4818
|
-
faked, the clock is not. `jasmine.clock().mockDate()` is what takes `Date` over, and because that
|
|
4819
|
-
re-installs the fake clock it reports when callbacks were already scheduled and have just been
|
|
4820
|
-
dropped. Call `mockDate()` right after `install()`, before anything schedules a timer.
|
|
4821
|
-
- **`jasmine.createSpyObj`'s third argument builds spied accessors**, reachable through
|
|
4822
|
-
`Object.getOwnPropertyDescriptor(obj, name).get`. Reading still answers the seed, so nothing about
|
|
4823
|
-
a migrated spec changes; what is gained is moving the value mid-test and asserting that the code
|
|
4824
|
-
under test wrote it.
|
|
4825
|
-
|
|
4826
|
-
### The renames, once the suite is green
|
|
4827
|
-
|
|
4828
|
-
| jasmine | here |
|
|
4829
|
-
| ------------------------------------------ | ---------------------------------------------------------- |
|
|
4830
|
-
| `spy.m.and.returnValue(v)` | `spy.m.mockReturnValue(v)` |
|
|
4831
|
-
| `spy.m.and.callFake(fn)` | `spy.m.mockImplementation(fn)` |
|
|
4832
|
-
| `spy.m.and.stub()` | `spy.m.mockImplementation(() => undefined)` |
|
|
4833
|
-
| `spy.m.and.returnValues(a, b)` | `.mockReturnValueOnce(a).mockReturnValueOnce(b)` |
|
|
4834
|
-
| `spy.m.and.throwError('boom')` | `.mockImplementation(() => { throw new Error('boom'); })` |
|
|
4835
|
-
| `spy.m.and.resolveTo(v)` | `spy.m.mockResolvedValue(v)` |
|
|
4836
|
-
| `spy.m.and.nextWith(v)` / `resolveWith(v)` | `spy.m.nextWith(v)` / `spy.m.resolveWith(v)` — drop `.and` |
|
|
4837
|
-
| `spy.m.withArgs(a).and.returnValue(v)` | `spy.m.calledWith(a).mockReturnValue(v)` |
|
|
4838
|
-
| `spy.m.calls.count()` / `argsFor(i)` | `spy.m.mock.calls.length` / `spy.m.mock.calls[i]` |
|
|
4839
|
-
| `spy.m.calls.reset()` | `spy.m.mockClear()` |
|
|
4840
|
-
| `jasmine.createSpy('n')` | `vi.fn()` — or `createFunctionSpy<F>('n')`, which is typed |
|
|
4841
|
-
| `jasmine.any(X)` / `objectContaining({…})` | `expect.any(X)` / `expect.objectContaining({…})` |
|
|
4842
|
-
| `jasmine.clock().tick(n)` | `vi.advanceTimersByTime(n)` — or `await advanceTimers(n)` |
|
|
4843
|
-
| `jasmine.SpyObj<T>` / `jasmine.Spy` | `Spy<T>` from this package / `Mock` from `vitest` |
|
|
4844
|
-
| `expect(x).toBeTrue()` / `toHaveSize(n)` | `.toBe(true)` / `.toHaveLength(n)` |
|
|
4845
|
-
| `expect(spy).toHaveBeenCalledOnceWith(a)` | `.toHaveBeenCalledExactlyOnceWith(a)` |
|
|
4846
|
-
| `fail(msg)` | `expect.fail(msg)` — there is no `vi.fail` |
|
|
4847
|
-
|
|
4848
|
-
### Four traps, all of them silent
|
|
4849
|
-
|
|
4850
|
-
1. **`spyOn(o, 'm')` inverts.** jasmine's `spyOn` installs a **stub**; `vi.spyOn` **calls through**.
|
|
4851
|
-
A bare rename compiles, passes, and starts running the real implementation inside every spec that
|
|
4852
|
-
installed the spy in order to stop it. Write
|
|
4853
|
-
`vi.spyOn(o, 'm').mockImplementation(() => undefined)` — the codemod appends exactly that, and a
|
|
4854
|
-
file whose only jasmine construct is `spyOn(` is deliberately **not** auto-detected: that suite
|
|
4855
|
-
has to say `--from jasmine` out loud.
|
|
4856
|
-
2. **`.withContext('msg')` does not throw — it loses the message.** Vitest's chai layer ships an
|
|
4857
|
-
`@internal` method of that name expecting a _flags object_; handed a string it walks the string's
|
|
4858
|
-
character indices, sets nonsense flags and returns the assertion. The chain runs, the failure
|
|
4859
|
-
reads `AssertionError: expected 2 to be 3`, and the label is gone. Write
|
|
4860
|
-
`expect(actual, 'msg').toBe(expected)`.
|
|
4861
|
-
3. **`.calls.saveArgumentsByValue()` is a no-op here.** No runner in this family copies call
|
|
4862
|
-
arguments. The call still runs, nothing fails, and a spec that relied on it silently starts
|
|
4863
|
-
asserting on post-mutation state. Take the copy at call time, inside a `mockImplementation`.
|
|
4864
|
-
4. **`.and.callThrough()` means something different.** Upstream had no original to call through to
|
|
4865
|
-
and silently answered `undefined`; here it restores **this library's own dispatch**, so a
|
|
4866
|
-
`calledWith` chain decides the value again. The codemod leaves it byte-for-byte and names the
|
|
4867
|
-
line, because there is no expression it could become.
|
|
4868
|
-
|
|
4869
|
-
`jasmine.DEFAULT_TIMEOUT_INTERVAL` is a config setting, not a statement: set **both**
|
|
4870
|
-
`testTimeout` (default 5 000 ms) and `hookTimeout` (default 10 000 ms), or
|
|
4871
|
-
`vi.setConfig({ testTimeout: n, hookTimeout: n })` per file. Assigning to it on the namespace warns
|
|
4872
|
-
once naming both rather than silently swallowing the write.
|
|
4873
|
-
|
|
4874
|
-
`it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
|
|
4875
|
-
shape, not a name. Use `async` + `await`; `no-done-callback` reports the parameter and any
|
|
4876
|
-
`done.fail(…)`. It reports the parameter that is _called_, handed on as an argument, or never used —
|
|
4877
|
-
not every named first parameter: `it('x', (ctx) => ctx.skip())` is Vitest's own `TestContext` read
|
|
4878
|
-
without destructuring, which is legal, and used to be reported as a `done` callback.
|
|
4879
|
-
|
|
4880
|
-
On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
|
|
4881
|
-
importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a
|
|
4882
|
-
setup file — spies built **before** the call do not get the namespaces. Observables still come from
|
|
4883
|
-
`vitest-auto-spy/rxjs`. A project that never imports the entry pays one `undefined` check per spy
|
|
4884
|
-
and ships none of the code.
|
|
4885
|
-
|
|
4886
|
-
### `@hirez_io/observer-spy` comes with it
|
|
4887
|
-
|
|
4888
|
-
That package sits beside `jasmine-auto-spies` in almost every suite that has one, and is the larger
|
|
4889
|
-
of the two by an order of magnitude. `vitest-auto-spy/rxjs` exports the same surface —
|
|
4890
|
-
`subscribeSpyTo(source$, config?)`, `SubscriberSpy<T>`, `ObserverSpy<T>`, `ObserverSpyConfig` — so a
|
|
4891
|
-
migration does not have to rewrite every stream assertion in the same commit:
|
|
2356
|
+
<!-- agents-outline: agent-docs/migration-jasmine.md -->
|
|
4892
2357
|
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
const spy = subscribeSpyTo(service.load());
|
|
4897
|
-
|
|
4898
|
-
expect(spy.getValues()).toEqual(['a', 'b']);
|
|
4899
|
-
expect(spy.receivedComplete()).toBe(true);
|
|
4900
|
-
```
|
|
2358
|
+
- The renames, once the suite is green
|
|
2359
|
+
- Four traps, all of them silent
|
|
2360
|
+
- `@hirez_io/observer-spy` comes with it
|
|
4901
2361
|
|
|
4902
|
-
|
|
4903
|
-
hands back its live array) and is typed `T[]` (upstream: `any[]`); `getFirstValue()` /
|
|
4904
|
-
`getValueAt(i)` **throw** on an empty spy rather than returning `undefined` from a `T` signature;
|
|
4905
|
-
and an **unexpected** error is thrown by the value readers, carrying the original as `cause`, rather
|
|
4906
|
-
than rethrown out of the observer — under rxjs 7 that rethrow goes through `reportUnhandledError`
|
|
4907
|
-
and never reaches the subscribing line. `{ expectErrors: true }` keeps the readers open, as upstream.
|
|
4908
|
-
|
|
4909
|
-
`autoUnsubscribe()`, `queueForAutoUnsubscribe()` and `fakeTime()` are **not implemented**. Use
|
|
4910
|
-
`using spy = subscribeSpyTo(source$)` — `SubscriberSpy` is disposable — and `setupFakeTimers()` with
|
|
4911
|
-
`await advanceTimers(ms)`, or rxjs's `TestScheduler` directly.
|
|
4912
|
-
|
|
4913
|
-
**It is a bridge, and the destination differs in kind.** observer-spy is synchronous inspection:
|
|
4914
|
-
subscribe, let things happen, then read. A stream that never emits leaves a spy with no values, so a
|
|
4915
|
-
spec reading `getValues()` gets `[]`, asserts about it, and passes having observed nothing. §8's
|
|
4916
|
-
`expectEmission` / `expectEmissions` invert that — the assertion _is_ the await, and silence is a
|
|
4917
|
-
failure with a watchdog. Land the suite green on `subscribeSpyTo`, then move the assertions over.
|
|
4918
|
-
|
|
4919
|
-
Full mapping, including what upstream cannot do at all:
|
|
4920
|
-
<https://asdalexey.github.io/vitest-auto-spy/migrating-jasmine>.
|
|
4921
|
-
|
|
4922
|
-
An Angular suite that came through `ng generate @schematics/angular:refactor-jasmine-vitest` rather
|
|
4923
|
-
than through `jasmine-auto-spies` is a different input — a `MockedObject<T>` literal per double and
|
|
4924
|
-
`// TODO: vitest-migration:` comments on the `createSpyObj` calls it could not expand. That diff is
|
|
4925
|
-
covered at <https://asdalexey.github.io/vitest-auto-spy/migrating-angular-schematic>; the answer to
|
|
4926
|
-
all three TODO categories is `createSpyFromClass(Api)`, which reads the prototype, not the call site.
|
|
2362
|
+
<!-- /agents-outline -->
|