vitest-auto-spy 5.18.0 → 5.19.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 +510 -97
- package/README.md +402 -177
- package/dist/angular-http.js +27 -5
- package/dist/angular-router.d.ts +1 -1
- package/dist/angular-router.js +4 -3
- package/dist/angular.d.ts +40 -24
- package/dist/angular.js +946 -345
- package/dist/bun-angular.d.ts +6 -6
- package/dist/bun-angular.js +120 -63
- package/dist/bun.d.ts +75 -75
- package/dist/bun.js +9 -9
- package/dist/{chunk-WAMFS3UI.js → chunk-2ERHG4NE.js} +192 -72
- package/dist/{chunk-33NJHIS2.js → chunk-3LU77DBV.js} +12 -15
- package/dist/chunk-3VXZRDH3.js +12 -0
- package/dist/{chunk-DUFMKOKF.js → chunk-4JTRYOL2.js} +3 -2
- package/dist/{chunk-M6VMOOZ2.js → chunk-CEDA5PUY.js} +1 -1
- package/dist/{chunk-MAKQSO2R.js → chunk-DPTWTLIG.js} +27 -4
- package/dist/{chunk-4SJ7EGIY.js → chunk-F3OQDSKX.js} +385 -87
- package/dist/{chunk-MUJTPXMJ.js → chunk-HMECO5PK.js} +333 -87
- package/dist/{chunk-Q477VHHF.js → chunk-JJVTDL5R.js} +8 -7
- package/dist/{chunk-ZU55UY2M.js → chunk-LBQWJZPS.js} +12 -3
- package/dist/{chunk-OTOCFH6B.js → chunk-R3DF5ZEO.js} +2 -1
- package/dist/chunk-SOROJD3W.js +9 -0
- package/dist/{chunk-DTZCPCMA.js → chunk-TCGO3VCY.js} +2 -1
- package/dist/chunk-TZ7DRSRD.js +9 -0
- package/dist/chunk-X4BVCE4V.js +78 -0
- package/dist/cli.js +394 -89
- package/dist/console.d.ts +11 -1
- package/dist/console.js +28 -11
- package/dist/diagnostics.d.ts +15 -1
- package/dist/diagnostics.js +86 -20
- package/dist/dom-stubs.d.ts +11 -29
- package/dist/dom-stubs.js +50 -29
- package/dist/eslint-plugin.cjs +239 -146
- package/dist/eslint-plugin.d.cts +13 -244
- package/dist/eslint-plugin.d.ts +9 -0
- package/dist/{expect-emission-CmN9ADyN.d.ts → expect-emission-QuF55ViS.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +724 -228
- package/dist/jasmine-compat.d.ts +35 -22
- package/dist/jasmine-compat.js +3 -1
- package/dist/jasmine.d.ts +2 -2
- package/dist/jasmine.js +50 -15
- package/dist/nestjs.d.ts +2 -2
- package/dist/nestjs.js +11 -12
- package/dist/node.cjs +730 -226
- package/dist/node.d.ts +5 -5
- package/dist/node.js +3072 -11
- package/dist/observer-spy.d.ts +7 -2
- package/dist/observer-spy.js +44 -9
- package/dist/{package-identity-lm-gqZAR.d.ts → package-identity-C2L4fM0k.d.ts} +1 -1
- package/dist/{prop-mock-DnYNBNJL.d.ts → prop-mock-DMqE-ldi.d.ts} +1 -1
- package/dist/react.d.ts +5 -5
- package/dist/react.js +3574 -15
- package/dist/rstest.d.ts +5 -5
- package/dist/rstest.js +9 -9
- package/dist/rxjs.d.ts +2 -2
- package/dist/rxjs.js +43 -3
- package/dist/setup.d.ts +65 -11
- package/dist/setup.js +1195 -136
- package/dist/shared-state.js +40 -12
- package/dist/signal-forms.js +2 -1
- package/dist/svelte.d.ts +5 -5
- package/dist/svelte.js +3574 -15
- package/dist/{track-injections-CIHJWFpp.d.ts → track-injections-BfNI9fHy.d.ts} +1 -1
- package/dist/{track-signal-runs-BOfsdYsC.d.ts → track-signal-runs-C5nd4A4_.d.ts} +27 -7
- package/dist/{types-D3RkJjc_.d.ts → types-B7Wjo00D.d.ts} +95 -7
- package/dist/vue.d.ts +6 -6
- package/dist/vue.js +3571 -16
- package/dist/zone.js +12 -1
- package/package.json +11 -5
- package/skills/vitest-auto-spy/SKILL.md +132 -105
- package/dist/chunk-M5BULP7X.js +0 -41
- package/dist/chunk-N3T7KWWA.js +0 -157
- package/dist/chunk-OS4PTU6D.js +0 -99
- package/dist/chunk-PA4ZNLNZ.js +0 -8
package/AGENTS.md
CHANGED
|
@@ -88,7 +88,19 @@ and under pnpm's isolated layout it never resolved before.
|
|
|
88
88
|
|
|
89
89
|
The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
|
|
90
90
|
CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
|
|
91
|
-
Vitest refuses to be required).
|
|
91
|
+
Vitest refuses to be required). The plugin's CommonJS declaration is an `export =`, which is what
|
|
92
|
+
`require('vitest-auto-spy/eslint-plugin')` actually returns — an `eslint.config.cts` / `.cjs` used to
|
|
93
|
+
type-check the call that throws and reject the one that works.
|
|
94
|
+
|
|
95
|
+
`vitest-auto-spy/package.json` resolves as well. Tools that read a dependency's manifest by specifier
|
|
96
|
+
— Storybook, Nx, a renovate helper — used to get `ERR_PACKAGE_PATH_NOT_EXPORTED` and had nowhere to
|
|
97
|
+
go from there.
|
|
98
|
+
|
|
99
|
+
**Every peer is optional now, `vitest` included.** The range is unchanged (`>=2.1.0`); what changed
|
|
100
|
+
is that a suite on `/bun` or `/node`, which never loads the Vitest runner, no longer installs it to
|
|
101
|
+
satisfy a peer. The one thing still to know is that `/bun`, `/bun-angular` and `/node` declarations
|
|
102
|
+
name Vitest's `Mock` type, so a project type-checking those entries without `vitest` installed will
|
|
103
|
+
want it as a devDependency anyway; freeing that is a type change and waits for a major.
|
|
92
104
|
|
|
93
105
|
---
|
|
94
106
|
|
|
@@ -201,6 +213,13 @@ asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // →
|
|
|
201
213
|
`asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
|
|
202
214
|
sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
|
|
203
215
|
|
|
216
|
+
**A deep node asks what the spy surface is on every read**, rather than remembering the answer from
|
|
217
|
+
the first deep mock of the worker. That matters under `isolate: false`, where the surface grows
|
|
218
|
+
mid-run: `import 'vitest-auto-spy/rxjs'` in a later file adds `nextWith` and friends, and
|
|
219
|
+
`setSpyEngine` swaps the whole prototype. With the answer cached, `deep.feed.items.nextWith(1)` in
|
|
220
|
+
every double built after that resolved to a **child node** — callable, recorded, emitting nothing —
|
|
221
|
+
while the spec waited on a stream that was never fed.
|
|
222
|
+
|
|
204
223
|
**`selfReturning: true` chains a factory, not a `return this` builder**, and the difference decides
|
|
205
224
|
where the calls are recorded. A called node answers _itself_, not the object the method was read
|
|
206
225
|
off, so every hop moves one level deeper: `editor.chain().focus().insertContent('text')` records
|
|
@@ -354,15 +373,35 @@ five providers across two thousand tests is about two hundredths of a second. Ca
|
|
|
354
373
|
`autoSpyAccessors: true` walks the prototype chain uncached on every call — name the accessors
|
|
355
374
|
instead.
|
|
356
375
|
|
|
357
|
-
Memory
|
|
358
|
-
defines one
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
(`npm run bench:memory`, 2026-09-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
Memory used to be the exception, and it no longer is for the default. `lazySpies: true` still
|
|
377
|
+
defines one property per method, but the accessor pair behind it is now **shared by method name**
|
|
378
|
+
across every double in the process, so the placeholder is no longer what an untouched double
|
|
379
|
+
retains: an untouched 100-method double holds **215 B** where it held 25 593, and a 300-method one
|
|
380
|
+
284 B where it held 70 165 (`npm run bench:memory`, 2026-09-17, Node 24). The price is paid at
|
|
381
|
+
construction on very wide classes — building a 300-method double costs about 28 % more — and
|
|
382
|
+
materialising every method of one is about 26 % cheaper in exchange.
|
|
383
|
+
|
|
384
|
+
That inverts the case for `lazySpies: 'proxy'`, which answers every method from a single trap
|
|
385
|
+
object: it now retains **4 090 B** against the default's 215 B at 100 methods — 19× more — and still
|
|
386
|
+
pays a trap on every read (53 ns against 7 ns) for the life of the double, because a `Proxy` cannot
|
|
387
|
+
remove itself while the default leaves a plain data property behind once a method materialises. What
|
|
388
|
+
is left of its advantage is build time on a wide class. The option is unchanged and still supported;
|
|
389
|
+
there is no longer a memory reason to reach for it.
|
|
390
|
+
|
|
391
|
+
A frozen or sealed double is fine now. `Object.freeze(spy)` — a deep-freeze fixture helper, a
|
|
392
|
+
dev-mode state guard — used to make the first read of any method throw `Cannot redefine property`
|
|
393
|
+
from inside the placeholder's getter; the spy is kept beside the double instead, so the read answers
|
|
394
|
+
a stable mock and `mockReturnValue` on it works. On a merely sealed double an assignment still
|
|
395
|
+
reaches the member.
|
|
396
|
+
|
|
397
|
+
**Symbol-keyed methods are discovered and spied.** A method under a symbol the project owns —
|
|
398
|
+
`[SERIALIZE]()`, `Symbol.for('app.render')` — is a method like any other, resets with the rest and
|
|
399
|
+
shows up in `Reflect.ownKeys`. The runtime's own protocol symbols are deliberately left alone
|
|
400
|
+
(everything on `Symbol` itself — `Symbol.iterator`, `Symbol.dispose`, `Symbol.toPrimitive` and the
|
|
401
|
+
rest — plus `Symbol.for('nodejs.util.inspect.custom')`), because a double that answers those stops
|
|
402
|
+
being a double of `T` and starts impersonating an iterable or a disposable. Discovery covers methods
|
|
403
|
+
only: a symbol-keyed **getter or setter** is not found by `autoSpyAccessors`, so patch one with
|
|
404
|
+
`mockAccessorsProp` (§9).
|
|
366
405
|
|
|
367
406
|
---
|
|
368
407
|
|
|
@@ -480,8 +519,9 @@ included — TypeScript resolves a type-only import the same way.
|
|
|
480
519
|
users.getName.calledWith(1).mockReturnValue('Ada');
|
|
481
520
|
// argument enforcement — other arguments throw
|
|
482
521
|
users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
|
|
483
|
-
// asymmetric matchers work in both
|
|
522
|
+
// asymmetric matchers work in both, at any depth
|
|
484
523
|
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
|
|
524
|
+
users.save.calledWith({ id: expect.any(Number), tags: [expect.any(String)] }).mockReturnValue(true);
|
|
485
525
|
// re-registering the same arguments replaces the answer — matcher arguments included
|
|
486
526
|
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(false);
|
|
487
527
|
|
|
@@ -523,6 +563,31 @@ makes a second `calledWith(1, expect.anything())` an override rather than a seco
|
|
|
523
563
|
behind the first. A hand-rolled `{ asymmetricMatch }` object is compared by identity instead: its
|
|
524
564
|
verdict is a closure, so only re-registering that same instance overrides.
|
|
525
565
|
|
|
566
|
+
**A matcher counts wherever it sits, not only at the top level.** A config argument holding a
|
|
567
|
+
matcher — or a function — anywhere inside it is compared structurally rather than as data:
|
|
568
|
+
`calledWith({ id: expect.any(Number) })`, `calledWith([expect.any(String)])`, a matcher inside a
|
|
569
|
+
`Map` value or a `Set` member. The same comparison decides the rest of an argument's shape, so it is
|
|
570
|
+
worth knowing what it treats as equal: `Map` and `Set` are compared **without regard to insertion
|
|
571
|
+
order**, a `Date` by its time, a `RegExp` by its source and flags, an `Error` by its `name` and
|
|
572
|
+
`message` plus its own enumerable fields, a function by identity, and symbol-keyed properties
|
|
573
|
+
participate like string ones. Cycles are handled, so a component graph with a back-edge is a legal
|
|
574
|
+
argument. `mustBeCalledWith` uses the same map, and its `Wanted:` line renders a matcher as
|
|
575
|
+
`Any<Number>` rather than as the object it serialises to.
|
|
576
|
+
|
|
577
|
+
Each `calledWith(...)` / `mustBeCalledWith(...)` call hands back **its own** handle, so a chain kept
|
|
578
|
+
in a variable stays attached to the arguments it was written with:
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
const found = users.load.calledWith(1);
|
|
582
|
+
users.load.calledWith(2).mockReturnValue(undefined);
|
|
583
|
+
|
|
584
|
+
found.mockReturnValue({ id: 1 }); // configures 1, not 2
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
`new` on a method spy works: `new sdk.Client()` — the shape `createAutoMock<{ Client: typeof Client }>()`
|
|
588
|
+
and `mockDeep` produce — hands back the instance, or the object a `calledWith(...).mockReturnValue(...)`
|
|
589
|
+
configured for those arguments.
|
|
590
|
+
|
|
526
591
|
`mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
|
|
527
592
|
all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
|
|
528
593
|
|
|
@@ -541,11 +606,28 @@ onDone.value(); // and now exercise what was passed
|
|
|
541
606
|
expect(component.finished()).toBe(true);
|
|
542
607
|
```
|
|
543
608
|
|
|
544
|
-
`.values` holds every
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
609
|
+
`.values` holds every value the captor was **offered**, oldest first — candidates, not matches. A
|
|
610
|
+
captor in position 0 of `toHaveBeenCalledWith(captor, 3)` is asked about the first argument of every
|
|
611
|
+
call the runner tries, including the ones the `3` then rejects, so `.values` can be longer than the
|
|
612
|
+
set of calls the assertion accepted. `.captured` asks whether anything was recorded without
|
|
613
|
+
triggering the "nothing was captured" throw that reading `.value` raises; `.reset()` lets one captor
|
|
614
|
+
serve two phases.
|
|
615
|
+
|
|
616
|
+
`captureArg({ where })` narrows both halves at once — the filter decides what is recorded **and**
|
|
617
|
+
whether that position matches at all, so a rejected candidate leaves no entry in `.values` and the
|
|
618
|
+
whole expectation fails on it:
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
const config = captureArg<RequestInit>({ where: (value) => (value as RequestInit)?.method === 'POST' });
|
|
622
|
+
|
|
623
|
+
expect(fetchSpy).toHaveBeenCalledWith(url, config); // only the POST call satisfies this
|
|
624
|
+
expect(config.value.headers).toEqual({ 'x-trace': '1' });
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
The filter receives the raw argument as `unknown`, so narrow it yourself. **Assertions only** — a
|
|
628
|
+
captor without `where` matches every value, so putting one in `calledWith` would configure a return
|
|
629
|
+
for every call, which is `mockReturnValue` spelled less clearly, and `calledWith` is typed to the
|
|
630
|
+
method's own parameters so it will not compile anyway.
|
|
549
631
|
|
|
550
632
|
**The observable helpers are backed by a `ReplaySubject(1)` that belongs to the spy, and it is
|
|
551
633
|
configuration — so it must be reset with the rest of it.** Two failures used to come out of that
|
|
@@ -563,7 +645,14 @@ The code under test therefore ran the **success** branch on stale data, and the
|
|
|
563
645
|
one emission late. The second: `error()` and `complete()` close a Subject permanently, so a later
|
|
564
646
|
`nextWith` on that spy pushed into a dead subject and emitted nothing at all. Both are fixed —
|
|
565
647
|
`resetAutoSpy(spy)` now drops the subject, and a terminated one is replaced on the next
|
|
566
|
-
configuration.
|
|
648
|
+
configuration. That holds for a subject a spec closed **itself**, too:
|
|
649
|
+
`spy.items$.returnSubject().complete()` marks the stream closed, so the next `nextWith` opens a new
|
|
650
|
+
one rather than disappearing.
|
|
651
|
+
|
|
652
|
+
`nextWithValues` on an observable **property** builds a new stream, which a subscriber that already
|
|
653
|
+
holds the old one never sees — the spec's values go nowhere and the assertion below reads the
|
|
654
|
+
initial state. That case is reported now rather than passing silently; `nextWith` pushes into the
|
|
655
|
+
subject the current subscriber is on and is the one to reach for mid-test.
|
|
567
656
|
|
|
568
657
|
What that does **not** change: `vi.clearAllMocks()` and `clearMocks: true` still cannot reach it,
|
|
569
658
|
for the same reason they cannot reach a `calledWith` chain — that state lives in this library's
|
|
@@ -726,7 +815,8 @@ winning, scalars decided by the call site when it names the key. The bare-array
|
|
|
726
815
|
walking the prototype chain would let one registration change doubles in files nobody was looking at.
|
|
727
816
|
A second registration for the same class replaces the first, because two of them in one suite is the
|
|
728
817
|
drift this removes rather than a merge to perform. `clearAutoSpyDefaults(Class)` drops one,
|
|
729
|
-
`clearAutoSpyDefaults()` the lot.
|
|
818
|
+
`clearAutoSpyDefaults()` the lot. `createSpyFromInstance(obj)` reads the registration of the class
|
|
819
|
+
`obj.constructor` names, merged the same way; an object literal resolves none.
|
|
730
820
|
|
|
731
821
|
A setup file that registers more than a handful of classes can say them as one table instead of one
|
|
732
822
|
call each. Rows apply in order, and each is checked against **its own** class — a key `Router` does
|
|
@@ -858,6 +948,12 @@ Before 3.5.0 the assignment landed on the no-op setter the scaffolding installs:
|
|
|
858
948
|
`accessorSpies.setters.theme` was `undefined`, and the failure read
|
|
859
949
|
`Cannot read properties of undefined` three steps from the configuration behind it.
|
|
860
950
|
|
|
951
|
+
The bag is typed over every key of `T` unless the lists are repeated in the options type argument —
|
|
952
|
+
`createSpyFromClass<Settings, { gettersToSpyOn: ['theme'] }>(Settings, { gettersToSpyOn: ['theme'] })`
|
|
953
|
+
keys both halves by exactly the configured names, so `accessorSpies.setters.other` is a compile error
|
|
954
|
+
instead of an `undefined` at run time. Use it when a spec reaches into the bag by name; a
|
|
955
|
+
non-literal `string[]` falls back to the every-key bag.
|
|
956
|
+
|
|
861
957
|
Only spy a getter when the spec asserts that it was **read**. To make one _answer_ something, on a
|
|
862
958
|
spy that already exists, the pair above is one line — and it needs no `gettersToSpyOn` at the
|
|
863
959
|
factory, which is the part that is otherwise found by trial:
|
|
@@ -957,6 +1053,14 @@ resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mo
|
|
|
957
1053
|
Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
|
|
958
1054
|
proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
|
|
959
1055
|
|
|
1056
|
+
**`resetAutoSpy` is `vi.resetAllMocks()` for one double**, and two things it used to leave behind now
|
|
1057
|
+
go with the rest: a pending `mockReturnValueOnce` / `mockResolvedValueOnce` queue (it used to answer
|
|
1058
|
+
the first call _after_ the reset, in a later test) and an accessor spy's configuration (a
|
|
1059
|
+
`accessorSpies.getters.x.mockReturnValue(…)` used to survive and hand the next test the previous
|
|
1060
|
+
test's value). A spec that relied on a queued `Once` value outliving the reset reads `undefined`
|
|
1061
|
+
now. The double stays usable: the library's own dispatch is put back afterwards, so a fresh
|
|
1062
|
+
`calledWith` configures it as normal.
|
|
1063
|
+
|
|
960
1064
|
Every double also carries `[Symbol.dispose]()` — it runs `resetAutoSpy(this)` — so an `afterEach`
|
|
961
1065
|
that exists only to reset one spy can be deleted:
|
|
962
1066
|
|
|
@@ -1001,7 +1105,8 @@ await expectCompletion(service.purgeCache()); // "it finished" — the value is
|
|
|
1001
1105
|
```
|
|
1002
1106
|
|
|
1003
1107
|
Options: `{ timeout, label }`. `timeout` defaults to `1000` ms (`0` for `expectNoEmission`, whose
|
|
1004
|
-
wait is a quiet window rather than a watchdog).
|
|
1108
|
+
wait is a quiet window rather than a watchdog). `0` and `Infinity` both mean **no watchdog** — the
|
|
1109
|
+
wait then runs to the runner's own test timeout, which is the trade. The source is duck-typed, so rxjs `Observable`s,
|
|
1005
1110
|
`Subject`s, Angular `toObservable()` results, Angular `output()` (`OutputEmitterRef`, whose
|
|
1006
1111
|
`subscribe` takes a bare callback) and hand-rolled subscribables all work — and every helper infers
|
|
1007
1112
|
the emitted type, so `expectEmission(of(1))` is a `Promise<number>`.
|
|
@@ -1069,15 +1174,34 @@ await expect(expectEmission(purchased$, { advance: () => vi.runAllTimers() })).r
|
|
|
1069
1174
|
That replaces the fragile shape people arrive at — hold the promise, advance, then await — which
|
|
1070
1175
|
breaks silently the moment somebody adds an `await` one line above it. It is a callback rather than
|
|
1071
1176
|
an `advanceTimers: true` flag because these helpers are in the core entry, which contains no test
|
|
1072
|
-
runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`.
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1177
|
+
runner: only the spec knows whether it is on `vi`, `bun:test` or `node:test`. A throw out of
|
|
1178
|
+
`advance` — or out of an `until` predicate — is reported as itself and tears the subscription down,
|
|
1179
|
+
rather than being lost while the wait runs on to the timeout.
|
|
1180
|
+
|
|
1181
|
+
**These helpers subscribe as a subscriber, so a synchronous source stops at the value that settles
|
|
1182
|
+
the wait.** `expectEmission(from([1, 2, 3]).pipe(tap(spy)))` calls `tap` once, not three times, so
|
|
1183
|
+
`expect(spy).toHaveBeenCalledTimes(1)` is honest and a `finalize` runs at the stop; before, the
|
|
1184
|
+
producer ran to completion before anything could unsubscribe, and an endless synchronous source
|
|
1185
|
+
(`of(1).pipe(repeat())`) hung the worker instead of resolving. `expectEmissions(source$, 3)` stops
|
|
1186
|
+
at the third. A source that cannot be subscribed to at all is reported by name now, with a separate
|
|
1187
|
+
hint when what was passed is a promise.
|
|
1188
|
+
|
|
1189
|
+
`expectEmissions(source$, 0)` throws at the call — a count below 1 is not something a stream can
|
|
1190
|
+
satisfy, and the message names `expectNoEmission` instead. A suite that wrote
|
|
1191
|
+
`expectEmissions(s, expected.length)` with an empty expectation is the one this changes.
|
|
1192
|
+
|
|
1193
|
+
**The watchdog runs on real time, on purpose — even under fake timers, and even under zone.js.** A
|
|
1194
|
+
virtual one would race the timers the spec advances: `expectEmission(source$, { timeout: 200 })`
|
|
1195
|
+
followed by `vi.advanceTimersByTime(5_000)` would fire at 200 virtual ms and reject the stream the
|
|
1196
|
+
spec was about to advance into. Inside `fakeAsync` that used to happen anyway, because zone.js
|
|
1197
|
+
patches the global `setTimeout` and the watchdog was scheduled onto the virtual queue: a
|
|
1198
|
+
`tick(1_500)` towards a `debounceTime(2_000)` rejected the wait it was advancing. The timer is taken
|
|
1199
|
+
from `__zone_symbol__setTimeout` now, so it is outside the zone and `tick()` cannot reach it.
|
|
1200
|
+
|
|
1201
|
+
The cost is that in a suite with global fake timers a _failing_ assertion spends a real second. Do
|
|
1202
|
+
**not** answer that with `{ timeout: 0 }` at every call site — that disables the watchdog, and the
|
|
1203
|
+
next silent stream hangs to the runner's own timeout with nothing useful in the message. Lower the
|
|
1204
|
+
default once instead:
|
|
1081
1205
|
|
|
1082
1206
|
```ts
|
|
1083
1207
|
// vitest.setup.ts
|
|
@@ -1088,6 +1212,11 @@ setupAutoSpy({ globalFakeTimers: true });
|
|
|
1088
1212
|
setEmissionTimeout(100); // the clock is frozen; a real second buys nothing
|
|
1089
1213
|
```
|
|
1090
1214
|
|
|
1215
|
+
`setEmissionTimeout` takes `0` (no watchdog) and `Infinity` (the same), and **throws** on `NaN` and
|
|
1216
|
+
on a negative number rather than installing a default that fails every wait. The commonest way to
|
|
1217
|
+
reach it is arithmetic: `setEmissionTimeout(Number(process.env.EMISSION_TIMEOUT))` with the variable
|
|
1218
|
+
unset.
|
|
1219
|
+
|
|
1091
1220
|
**The code frame these failures open is the spec line, not this package.** The helpers build their
|
|
1092
1221
|
error inside a `subscribe` or timer callback, so the stack is captured at helper entry and pinned on
|
|
1093
1222
|
when the failure is finally built; only errors these helpers make themselves are re-anchored, and
|
|
@@ -1261,6 +1390,15 @@ setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).
|
|
|
1261
1390
|
A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
|
|
1262
1391
|
the previous file is the one to fix.
|
|
1263
1392
|
|
|
1393
|
+
**`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
|
|
1394
|
+
away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
|
|
1395
|
+
the fake clock hands out bypasses the count entirely — which makes `expect(countStrayTimers()).toBe(0)`
|
|
1396
|
+
vacuous in any file running on a frozen clock, `setupAutoSpy({ strayTimers: true, globalFakeTimers: true })`
|
|
1397
|
+
included. Read `vi.getTimerCount()` for the fake clock's own backlog; the two answer different
|
|
1398
|
+
questions and neither covers the other. A handle cleared by the number it coerces to
|
|
1399
|
+
(`clearTimeout(+handle)`) or through its own `close()` is forgotten properly now, so those stopped
|
|
1400
|
+
being counted as strays.
|
|
1401
|
+
|
|
1264
1402
|
**With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
|
|
1265
1403
|
arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
|
|
1266
1404
|
Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
|
|
@@ -1288,8 +1426,10 @@ file does (`import 'zone.js';`) or the Angular builder does. Without it the call
|
|
|
1288
1426
|
than quietly watching nothing. Native, non-zone rejections already fail a Vitest run, and nothing
|
|
1289
1427
|
here touches them. The pieces are exported too — `trackStrayRejections()` (idempotent, returns the
|
|
1290
1428
|
undo), `flushStrayRejections()` (takes what was captured and starts again from empty) and
|
|
1291
|
-
`countStrayRejections()`. The
|
|
1292
|
-
|
|
1429
|
+
`countStrayRejections()`. The count is complete; the **reasons** retained are capped at 100, so a
|
|
1430
|
+
file rejecting in a loop cannot hold its whole run's worth of stacks. A promise rejected with
|
|
1431
|
+
`undefined` is reported as that rather than as `[object Object]`. The `no-floating-assertion` lint
|
|
1432
|
+
rule catches the commonest shape before it ever runs (§16).
|
|
1293
1433
|
|
|
1294
1434
|
**`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
|
|
1295
1435
|
about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
|
|
@@ -1435,6 +1575,12 @@ another:
|
|
|
1435
1575
|
setupAutoSpy({ blockNetwork: { xhr: 'empty' } }); // the ad-player suite's setting
|
|
1436
1576
|
```
|
|
1437
1577
|
|
|
1578
|
+
**Called twice, the last caller's mode wins.** The setup file installs from its own `beforeEach`,
|
|
1579
|
+
which runs before the spec's, so a spec calling `blockNetwork({ xhr: 'reject' })` because the failure
|
|
1580
|
+
branch is what it is testing used to be served the setup file's empty 200 and pass on the wrong
|
|
1581
|
+
branch. The stubs are also installed once rather than re-journalled per test, so `restoreProps: false`
|
|
1582
|
+
no longer grows the restore journal for the length of the run.
|
|
1583
|
+
|
|
1438
1584
|
A `data:` URL is always let through, and it is the only thing that is: that is the scheme a spec
|
|
1439
1585
|
serves its own fixtures from (`xhr.open('GET', \`data:application/xml,\${encodeURIComponent(vast)}\`)`),
|
|
1440
1586
|
and the only one a DOM answers without a socket. A **relative** URL is not exempt either — the DOM
|
|
@@ -1571,6 +1717,17 @@ reads as a regression and is not one.
|
|
|
1571
1717
|
`Date is not a constructor` with a stack in production code and no mention of timers.
|
|
1572
1718
|
`mockSystemTime` does the right thing either way.
|
|
1573
1719
|
|
|
1720
|
+
Two rules about which of these composes with which, both of them earned:
|
|
1721
|
+
|
|
1722
|
+
- **`withSystemTime` puts the previous time back**, including when fake timers were already running.
|
|
1723
|
+
Its undo used to be a no-op there, so the clock stayed where the callback left it for the rest of
|
|
1724
|
+
the test.
|
|
1725
|
+
- **`mockSystemTime()` installs `Date` and nothing else**, so `advanceTimers()` under it has nothing
|
|
1726
|
+
to advance. It used to pass having done nothing; it now refuses and names `setupFakeTimers()`.
|
|
1727
|
+
`setupFakeTimers(config)` installs the config it was given rather than deferring to whatever fakes
|
|
1728
|
+
were already up — a nested call, a global pair plus a local one, and the `Date`-only case all now
|
|
1729
|
+
end with the clock the call asked for, where the outer configuration used to win silently.
|
|
1730
|
+
|
|
1574
1731
|
`useCountingClock` exists because under fake timers every call inside one test reports the _same_
|
|
1575
1732
|
"now", so a spec that asserts on **order** or **duration** — analytics batches, tracing, a rate
|
|
1576
1733
|
limiter, a TTL cache — cannot express its expectation at all. Patching `Date.now` by hand does not
|
|
@@ -1640,8 +1797,18 @@ setupAutoSpy({ guardGlobals: 'throw' }); // or 'warn' while a suite is being cle
|
|
|
1640
1797
|
`Object.defineProperty(document, 'cookie', { value })` defaults `configurable` to `false`, so the
|
|
1641
1798
|
property can no longer be redefined _or_ deleted. Under `isolate: false` every later file in the
|
|
1642
1799
|
worker inherits it, and what fails is some library, every other run, with nothing naming the file
|
|
1643
|
-
that did it. The guard compares `globalThis
|
|
1644
|
-
|
|
1800
|
+
that did it. The guard compares `globalThis`, `document`, `navigator`, `location`, `screen` and the
|
|
1801
|
+
DOM prototypes a Jest-era stub reaches for — `Element`, `HTMLElement`, `HTMLCanvasElement`,
|
|
1802
|
+
`HTMLMediaElement`, `Node`, `EventTarget` — around every test, symbol keys included, and reports only
|
|
1803
|
+
what appeared and cannot be removed.
|
|
1804
|
+
|
|
1805
|
+
**Its blind spot is an existing name redefined in place.** The guard reads a descriptor only for a
|
|
1806
|
+
key that was not there before, so `Object.defineProperty(document, 'cookie', { value, configurable: false })`
|
|
1807
|
+
over the `cookie` the environment already has is invisible to it: the name was in the snapshot, and
|
|
1808
|
+
only its configurability changed. What it catches is the addition — the shape that actually breaks a
|
|
1809
|
+
later file, because the next `defineProperty` of a name nothing expected is the one that throws.
|
|
1810
|
+
The snapshot is taken once per file, in `beforeAll`, rather than per test, and the comparison is also
|
|
1811
|
+
run after every `afterAll`, so a write made in a `beforeAll` or an `afterAll` is reported too.
|
|
1645
1812
|
|
|
1646
1813
|
### Naming the file that polluted `Object.prototype`
|
|
1647
1814
|
|
|
@@ -1662,6 +1829,16 @@ a `useValue` provider or a test double — patch the prototype of the class the
|
|
|
1662
1829
|
`guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
|
|
1663
1830
|
that does not call `setupAutoSpy()`.
|
|
1664
1831
|
|
|
1832
|
+
**A key an earlier file left behind is taken back before the next one is collected.** The per-test
|
|
1833
|
+
comparison cannot see a write made while a spec file was being imported or collected, or in its
|
|
1834
|
+
`afterAll` — all three happen outside any test, and the file that then fails to collect is the
|
|
1835
|
+
innocent one. `setupAutoSpy()` therefore also checks at the moment it is called, which is the one
|
|
1836
|
+
seam Vitest leaves between files: the key is deleted, a line goes to **stderr** naming it, and
|
|
1837
|
+
**nothing fails** — the file that would fail is not the file that wrote it. A key that refuses to be
|
|
1838
|
+
deleted is adopted into the baseline rather than reported once per file for the rest of the run. The
|
|
1839
|
+
in-file check still throws at its default grade; the file's own baseline is taken in `beforeAll` and
|
|
1840
|
+
re-checked after every `afterAll`, so a write made in either is covered as well.
|
|
1841
|
+
|
|
1665
1842
|
### Naming the test that left an attribute on `<body>`
|
|
1666
1843
|
|
|
1667
1844
|
```ts
|
|
@@ -1707,13 +1884,19 @@ frame outside `node_modules`:
|
|
|
1707
1884
|
- **Absorbed** means the call never reached the console: a `/console` spy installed for the test or
|
|
1708
1885
|
the file, `vi.spyOn(console, m).mockImplementation(…)`, any replacement that does not call
|
|
1709
1886
|
through. A bare `vi.spyOn(console, m)` calls through and **counts**.
|
|
1887
|
+
- **This library's own `'warn'`-grade reports are not stray console output.** `guardGlobals`,
|
|
1888
|
+
`prototypePollution`, `unconfiguredReads`, `swallowedStrictCalls`, `misconfiguration`, the
|
|
1889
|
+
duplicate-copy report, the skipped-teardown net and the `test.concurrent` notice write past the
|
|
1890
|
+
guard's wrapper, so `'warn'` stays a warning under `strayConsole: 'throw'` instead of failing the
|
|
1891
|
+
test it is advising about. A `vi.spyOn(console, 'warn')` a test installs still absorbs them — the
|
|
1892
|
+
channel checks that the guard's own wrapper is the installed method and steps aside when anything
|
|
1893
|
+
sits on top of it.
|
|
1710
1894
|
- Output **outside any test** — collection/import, `beforeAll` / `afterAll`, a callback after its
|
|
1711
1895
|
test ended — fails the **file** in `afterAll` (`… outside any test …`).
|
|
1712
1896
|
- Every console method a test replaced is put back after it; one a file replaced, after the file.
|
|
1713
1897
|
- Under the guard, **importing `vitest-auto-spy/console` installs nothing** — call
|
|
1714
1898
|
`installConsoleSpies()` in a `beforeEach` (or at the top of the file). Spies an import installed
|
|
1715
1899
|
before the guard armed are taken off.
|
|
1716
|
-
- The library's own warnings are console output: under the guard they fail the test that caused them.
|
|
1717
1900
|
- `strayConsole: { allow: ['…', /…/] }` is the last resort, for environment noise no spec can reach.
|
|
1718
1901
|
Output written to `process.stdout` / `process.stderr` directly, and jsdom's own virtual console, is
|
|
1719
1902
|
not seen.
|
|
@@ -1747,6 +1930,23 @@ every occurrence. The grade is process-wide and released after the file. The pri
|
|
|
1747
1930
|
cancels — jsdom answers every Web Storage write with a real `setTimeout(…, 0)`, and the library's own
|
|
1748
1931
|
storage probe runs under it.
|
|
1749
1932
|
|
|
1933
|
+
**`test.concurrent` earns one warning per worker, and it is worth reading rather than silencing.**
|
|
1934
|
+
The per-test guards assume one test at a time: the document snapshot, the console window and the
|
|
1935
|
+
unconfigured-read counter are opened and judged per test, so with two in flight a finding can be
|
|
1936
|
+
charged to the other one — or cleared before anything sees it. The restores still run for every
|
|
1937
|
+
test, and the skipped-teardown net now remembers per test rather than per file, which is the half
|
|
1938
|
+
that was simply wrong. Run the files that need a guard sequentially, or keep `test.concurrent` for
|
|
1939
|
+
files whose setup passes `strayConsole: 'off'`, `documentPollution: 'off'` and
|
|
1940
|
+
`unconfiguredReads: 'off'`.
|
|
1941
|
+
|
|
1942
|
+
**What `/setup` costs per test**, measured on happy-dom over 10 000 empty tests (2026-09-17, Node
|
|
1943
|
+
24): the default `setupAutoSpy()` **19 µs**, `guardGlobals` **59 µs** (it snapshots once per file
|
|
1944
|
+
now, not once per test), `documentPollution` **23 µs** — it shares the `onTestFinished` the teardown
|
|
1945
|
+
net registers anyway — and `preset: 'strict'` **67 µs**. `documentPollution: { nodes: true }` over a
|
|
1946
|
+
`<head>` of 1000 children is **0.19 ms** per test rather than 7.1 ms, because the children are walked
|
|
1947
|
+
as a sibling chain instead of through a live `HTMLCollection`; an `ignoreNodes` selector's answer for
|
|
1948
|
+
an element is decided at snapshot time and reused for that test.
|
|
1949
|
+
|
|
1750
1950
|
### Hook order differs from Jest
|
|
1751
1951
|
|
|
1752
1952
|
Vitest runs `afterEach` hooks as a stack (innermost / last-registered first); Jest ran them in
|
|
@@ -1853,6 +2053,26 @@ expect(Image.instances[0].src).toBe('https://tns.example/hit');
|
|
|
1853
2053
|
throws a named error if it is ever called **without** `new`. `stubConstructor` installs through
|
|
1854
2054
|
`mockValueProp`, so `restoreMockedProps()` puts the platform's constructor back.
|
|
1855
2055
|
|
|
2056
|
+
**Static members are opt-in — `createSpyClass(Class, config, { statics: true })`.** Production code
|
|
2057
|
+
that reaches the class rather than an instance (`BackgroundWorker.isSupported()`,
|
|
2058
|
+
`Client.fromToken(t)`, a `VERSION` constant) finds nothing on the double otherwise, and the failure
|
|
2059
|
+
is a `TypeError` on the class, not on the spy:
|
|
2060
|
+
|
|
2061
|
+
```ts
|
|
2062
|
+
const WorkerSpy = createSpyClass(BackgroundWorker, undefined, { statics: true });
|
|
2063
|
+
|
|
2064
|
+
WorkerSpy.isSupported.mockReturnValue(true); // needs a cast — see below
|
|
2065
|
+
```
|
|
2066
|
+
|
|
2067
|
+
What the option copies, walking the class's own prototype chain so inherited statics come too: a
|
|
2068
|
+
static **function** becomes a full spy of its own, a static **data member** is copied by value, and a
|
|
2069
|
+
static **accessor** is skipped and never evaluated — a getter that reads configuration or touches the
|
|
2070
|
+
network must not run because a double was built. `prototype`, `length`, `name`, `caller`,
|
|
2071
|
+
`arguments` and the double's own `calls` / `instances` are never overwritten. It is off by default
|
|
2072
|
+
for that last reason: a class with a static named `calls` would otherwise shadow the construction
|
|
2073
|
+
log. **The statics are not typed** — the return type is still `ConstructorSpy<T>`, so reading one
|
|
2074
|
+
needs a cast at the spec; type them and this note goes.
|
|
2075
|
+
|
|
1856
2076
|
For the three observers, prefer the purpose-built stubs (§13). For `AbortController` — which breaks
|
|
1857
2077
|
in a jsdom run for a reason involving none of the three parties in the stack trace — use
|
|
1858
2078
|
`stubAbortController()`.
|
|
@@ -1879,11 +2099,15 @@ const media = stubMediaElement({ duration: 120 });
|
|
|
1879
2099
|
|
|
1880
2100
|
media.set(video, { readyState: 1 }); // → loadedmetadata
|
|
1881
2101
|
media.set(video, { currentTime: 119 }); // → timeupdate
|
|
1882
|
-
media.set(video, { ended: true }); // → ended
|
|
2102
|
+
media.set(video, { ended: true }); // → pause, then ended — and the element is paused
|
|
1883
2103
|
expect(media.play).toHaveBeenCalledTimes(1);
|
|
1884
2104
|
```
|
|
1885
2105
|
|
|
1886
|
-
State is per element, so an ad and the content report different durations
|
|
2106
|
+
State is per element, so an ad and the content report different durations — and per **install**, so
|
|
2107
|
+
an element held in module scope or left in the document under `isolate: false` takes the new
|
|
2108
|
+
install's `duration` rather than the previous one's. `set({ ended: true })` pauses the element and
|
|
2109
|
+
fires `pause` before `ended`, as the platform does: an element that is both ended and playing is a
|
|
2110
|
+
state no browser produces, and a player listening for `pause` was never told.
|
|
1887
2111
|
|
|
1888
2112
|
`currentTime` is a get/set pair, so a player restarting itself with `video.currentTime = 0` reaches
|
|
1889
2113
|
the record and fires `timeupdate` too — `media.set()` is not the only way in, and the component's own
|
|
@@ -1939,6 +2163,11 @@ mock` from **inside that dependency**. `moduleNamespace` is the shape it expects
|
|
|
1939
2163
|
vi.mock('shaka-player', () => moduleNamespace({ Player: mockConstructor(() => playerStub) }));
|
|
1940
2164
|
```
|
|
1941
2165
|
|
|
2166
|
+
A factory that spells out its own `default` **keeps it** — `moduleNamespace({ default: dayjsStub, utc })`
|
|
2167
|
+
gives the probing dependency `dayjsStub`, where it used to be replaced by the namespace itself and
|
|
2168
|
+
the default export silently became the wrong object. Only a factory without one gets
|
|
2169
|
+
`default: <the namespace>`, which is the interop shape it was there for; the return type follows.
|
|
2170
|
+
|
|
1942
2171
|
There is no `mockModule(…)` helper here, and there cannot be: Vitest hoists the literal `vi.mock`
|
|
1943
2172
|
call, so a wrapper around it would be hoisted as a call to a function that does not exist yet. Share
|
|
1944
2173
|
a fixture between the factory and the tests with `vi.hoisted()`.
|
|
@@ -2103,12 +2332,19 @@ old signal, silently and for the rest of the test. Writing through keeps all of
|
|
|
2103
2332
|
`model()`'s output half alive, and leaves nothing for `restoreMockedProps()` to undo — the value
|
|
2104
2333
|
stays where the spec left it.
|
|
2105
2334
|
|
|
2106
|
-
|
|
2335
|
+
**A `signal().asReadonly()` member counts as writable.** The view a class exposes is a different
|
|
2336
|
+
function from the signal behind it, but it is the same reactive node, so the value is set through the
|
|
2337
|
+
node and the member the class publishes keeps working — whenever it was first read. That is the
|
|
2338
|
+
common shape for a store's public state (`readonly items = this.#items.asReadonly()`), and it used to
|
|
2339
|
+
be swapped, which meant a `computed()` that had already read it never saw the new value.
|
|
2340
|
+
|
|
2341
|
+
One shape is still swapped: a `computed()` the class declares, and a member the double does not have
|
|
2107
2342
|
yet. **Patch those before anything reads them** — before the first `detectChanges()` /
|
|
2108
|
-
`stable(fixture)`. A
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2343
|
+
`stable(fixture)`. A `computed()` a live consumer has already read is refused by name rather than
|
|
2344
|
+
replaced where nothing would notice it — the refusal now names `computed()` specifically, and says
|
|
2345
|
+
that driving the signal the computation reads is the way through. An `input()` is refused outright:
|
|
2346
|
+
Angular sets an input through the input node rather than the property, so a replaced one breaks the
|
|
2347
|
+
host's next write. Drive an input with `await setInputs(fixture, { … })`, or with
|
|
2112
2348
|
`renderShallow(Component, { inputs: { … } })` for the value it starts at.
|
|
2113
2349
|
|
|
2114
2350
|
### Observers the component constructs itself
|
|
@@ -2143,10 +2379,18 @@ Three more knobs, each for a shape that otherwise gets hand-rolled:
|
|
|
2143
2379
|
```ts
|
|
2144
2380
|
stubIntersectionObserver({ autoEmit: true }); // every observed target reports as visible, at once
|
|
2145
2381
|
observers.last.options; // the init object: { rootMargin, threshold, … }
|
|
2382
|
+
observers.last.host; // the observer the code under test constructed — and its callback's 2nd argument
|
|
2146
2383
|
observers.last.emit([mutationRecord(host, { addedNodes: [span] })]);
|
|
2147
2384
|
observers.last.emit([resizeEntry(host, { width: 320 })]);
|
|
2148
2385
|
```
|
|
2149
2386
|
|
|
2387
|
+
**The callback's second argument is the observer the code under test holds**, not an internal
|
|
2388
|
+
record — so `(entries, observer) => observer.unobserve(entries[0].target)`, `observer.disconnect()`
|
|
2389
|
+
and `observer.takeRecords()` all reach the thing the spec drives, which is what production code
|
|
2390
|
+
written against the platform expects. `root`, `rootMargin` and `thresholds` read back off the init
|
|
2391
|
+
the way the platform normalises them (`null`, `'0px 0px 0px 0px'`, `[0]` by default) rather than as
|
|
2392
|
+
empty values.
|
|
2393
|
+
|
|
2150
2394
|
`autoEmit` is the mode a suite ported from Jest needs: there the global mock fired its callback with
|
|
2151
2395
|
`isIntersecting: true` immediately, so lazily-loading sections fetched their data during
|
|
2152
2396
|
`detectChanges()`. Against the default inert observer those specs assert on an empty component and
|
|
@@ -2311,12 +2555,32 @@ helper of its own or wants the HTTP one mid-test — reading takes the requests,
|
|
|
2311
2555
|
re-report what you inspected.
|
|
2312
2556
|
|
|
2313
2557
|
Nothing here imports `@angular/common/http/testing`: the token is read out of the spec's own
|
|
2314
|
-
`provideHttpClientTesting()` / `HttpClientTestingModule
|
|
2315
|
-
|
|
2316
|
-
|
|
2558
|
+
`provideHttpClientTesting()` / `HttpClientTestingModule` — including one reached through a nested
|
|
2559
|
+
module, which is the usual `imports: [SharedTestingModule]` shape and used to make `pendingRequests`
|
|
2560
|
+
inert without saying so — and a project using neither is genuinely inert. `ngModuleScopes` only fires
|
|
2561
|
+
on a module that contributes **nothing at all** — a providers-only module is legitimately
|
|
2562
|
+
scope-empty, so a stripped scope that still has providers passes silently; hand-call
|
|
2317
2563
|
`assertNgModuleScopes(...)` where you know what the module was supposed to bring. `deadSchemas` does
|
|
2318
2564
|
not fire when `declarations` is non-empty.
|
|
2319
2565
|
|
|
2566
|
+
**`getTestBed()` is covered as well as `TestBed`.** The instrumentation sits on the TestBed
|
|
2567
|
+
_instance_ every static delegates to, so the configuration inspectors, `shadowedProviders` and the
|
|
2568
|
+
`overrideComponentProvider` verification all see `getTestBed().configureTestingModule(…)` and
|
|
2569
|
+
`getTestBed().createComponent(…)` — the shape a setup helper of your own usually writes — and
|
|
2570
|
+
`overrideTemplate` is counted with the rest of the overrides. A suite on `getTestBed()` used to get
|
|
2571
|
+
silence from every one of these.
|
|
2572
|
+
|
|
2573
|
+
Two more things now judge what Angular ends up with rather than one call: `deadSchemas` tallies
|
|
2574
|
+
`schemas` and `declarations` across **every** `configureTestingModule` of a test, because Angular
|
|
2575
|
+
accumulates them — a schema added in a second call next to a component declared in the first is no
|
|
2576
|
+
longer reported, and the reverse order is no longer silently passed — and a test that resets the
|
|
2577
|
+
testing module twice reports the requests of every module it built, not only the last.
|
|
2578
|
+
|
|
2579
|
+
`enableAngularDiagnostics()` and `mockSignalProp()` both check once per worker that the Angular
|
|
2580
|
+
internals they read are still there, and **throw** naming the installed `@angular/core` version when
|
|
2581
|
+
one has moved. Nothing about that is fixable from a spec: the throw tells you which shape went and
|
|
2582
|
+
what stopped working, and the repair is a release of this package.
|
|
2583
|
+
|
|
2320
2584
|
### `httpResource()` and `HttpClient` in two lines — `vitest-auto-spy/angular-http`
|
|
2321
2585
|
|
|
2322
2586
|
```ts
|
|
@@ -2363,6 +2627,11 @@ Three things to know before reaching for it:
|
|
|
2363
2627
|
suite using `provideHttpTesting()` everywhere the diagnostic is redundant; keep it on while any
|
|
2364
2628
|
file still configures HTTP testing by hand.
|
|
2365
2629
|
|
|
2630
|
+
`provideHttpTesting({ verifyOnTeardown })` reads which test is running off the runner's own state,
|
|
2631
|
+
and where that state is absent — `bun:test`, `node:test`, anything that is not Vitest — the
|
|
2632
|
+
end-of-test check cannot arm. It now says so once per worker instead of checking nothing quietly;
|
|
2633
|
+
call `verifyNoPendingRequests()` yourself there.
|
|
2634
|
+
|
|
2366
2635
|
### `ActivatedRoute` from one record — `vitest-auto-spy/angular-router`
|
|
2367
2636
|
|
|
2368
2637
|
```ts
|
|
@@ -2576,6 +2845,10 @@ Five things to know:
|
|
|
2576
2845
|
records the reload, leaves `location.href` real, and `win.location.href = '/next'` moves the
|
|
2577
2846
|
double rather than the address bar. `mockValueProp(TestBed.inject(WINDOW), 'innerWidth', 800)` and
|
|
2578
2847
|
its restore work on the double too, and neither touches the global.
|
|
2848
|
+
- **Constructors come out unbound.** `win.Date`, `win.Promise`, `win.Object`, `win.Event` are handed
|
|
2849
|
+
back as themselves rather than through a binding that loses the statics, so `win.Date.now()`,
|
|
2850
|
+
`win.Promise.resolve()`, `win.Object.keys(x)` and `new win.Event('x')` work, and
|
|
2851
|
+
`win.Event === window.Event` holds — which is what `instanceof` in production code needs.
|
|
2579
2852
|
|
|
2580
2853
|
### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
|
|
2581
2854
|
|
|
@@ -2772,6 +3045,7 @@ const { fixture, component } = renderShallow(TaskListComponent, {
|
|
|
2772
3045
|
await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
|
|
2773
3046
|
await setInputs(fixture, { projectId: 7 }, { label: 'the task list' }); // options go to stable()
|
|
2774
3047
|
// a fixture whose componentType carries no ɵcmp is refused by name too, not as a bare TypeError
|
|
3048
|
+
// aliases resolve by CLASS FIELD name, and an input a hostDirectives entry exposes is accepted
|
|
2775
3049
|
|
|
2776
3050
|
// build a class through DI, every unprovided token auto-spied
|
|
2777
3051
|
const { instance, spies } = createWithAutoSpies(CartService, {
|
|
@@ -2815,6 +3089,15 @@ expect(component.products).toHaveResourceValue([product]);
|
|
|
2815
3089
|
expect(component.products).toHaveResourceError(/503/);
|
|
2816
3090
|
```
|
|
2817
3091
|
|
|
3092
|
+
**`renderShallow({ inputs })` and `setInputs` resolve a name the same way**, which they did not
|
|
3093
|
+
before. Both read the compiled definition, so an input renamed with an alias
|
|
3094
|
+
(`@Input('chart-series') series` or `series = input(…, { alias: 'chartSeries' })`) is set by its
|
|
3095
|
+
**class-field** name as well as its public one; both accept an input a `hostDirectives` entry
|
|
3096
|
+
exposes, which `setInputs` used to refuse with "It declares no inputs at all"; and both refuse a name
|
|
3097
|
+
the component does not declare, at the call, listing the ones it does. Angular's own answer to an
|
|
3098
|
+
undeclared name is an `NG0303` on the console and no change at all, so the spec used to fail several
|
|
3099
|
+
assertions later, on state nothing moved.
|
|
3100
|
+
|
|
2818
3101
|
Two zoneless traps:
|
|
2819
3102
|
|
|
2820
3103
|
- `fixture.detectChanges()` runs **one** change-detection pass and does **not** flush pending
|
|
@@ -2845,6 +3128,22 @@ writable and a write through it lands in `'local'`, `set` / `update` / `asReadon
|
|
|
2845
3128
|
`true` while the resource is loading over a value that is defined, which is what Angular has answered
|
|
2846
3129
|
since v20. Nothing is in flight, so there is nothing to await.
|
|
2847
3130
|
|
|
3131
|
+
**The double is no more forgiving than the real thing, in two places that used to hide a defect.**
|
|
3132
|
+
`value()` read after `fail(reason)` **throws** — a `ResourceValueError` carrying the reason on
|
|
3133
|
+
`cause` and naming the property — exactly as a real `ResourceRef` does, so a spec that read the value
|
|
3134
|
+
after arranging the error branch fails now instead of reading the last good value. Branch on
|
|
3135
|
+
`hasValue()` / `status()` first, or assert with `toHaveResourceError()`. And `reload()` answers
|
|
3136
|
+
`false` while the resource is `'idle'` or `'loading'`, as Angular's does, rather than a constant
|
|
3137
|
+
`true` — `expect(products.reload).toHaveBeenCalled()` is still the assertion to write; the return
|
|
3138
|
+
value is only for code that branches on it.
|
|
3139
|
+
|
|
3140
|
+
**The resource, focus and directive matchers throw on an argument of the wrong type.** They used to
|
|
3141
|
+
report `{ pass: false }`, which `.not` turns into a pass — so
|
|
3142
|
+
`expect(products.value()).not.toBeLoading()` (the value, not the resource),
|
|
3143
|
+
`expect(missingEl).not.toHaveFocus()` (a query that found nothing) and
|
|
3144
|
+
`expect(undefined).not.toHaveDirectiveApplied(X)` were three green assertions about nothing. Each
|
|
3145
|
+
now throws, naming what it received.
|
|
3146
|
+
|
|
2848
3147
|
Per-file timing, to find which specs actually pay for `TestBed`:
|
|
2849
3148
|
|
|
2850
3149
|
```ts
|
|
@@ -2871,10 +3170,14 @@ wants **Angular >= 20** and a declared `@angular/platform-browser`; on 18 or 19
|
|
|
2871
3170
|
loading, because the zoneless provider was still called
|
|
2872
3171
|
`provideExperimentalZonelessChangeDetection` there.
|
|
2873
3172
|
|
|
2874
|
-
It
|
|
2875
|
-
`
|
|
2876
|
-
|
|
2877
|
-
|
|
3173
|
+
It exports the core, `provideAutoSpy` / `injectSpy`, `renderShallow`, `createWithAutoSpies`,
|
|
3174
|
+
`setInputs`, `runEffect`, `settleResource`, `trackEffectRuns` / `trackRecomputations` and
|
|
3175
|
+
`stable` / `flushEffects` — nothing else from this section. The matcher registrars
|
|
3176
|
+
(`registerSignalMatchers`, `registerDirectiveMatchers`, `registerResourceMatchers`) need the runner's
|
|
3177
|
+
`expect.extend` and the TestBed diagnostics its suite-level hooks; the overrides, `extendWithAutoSpies`,
|
|
3178
|
+
`provideAutoSpyForToken`, `trackInjections`, `setupAngularTestEnv`, the stub factories and the
|
|
3179
|
+
`mock*Prop`, platform and dialog doubles are simply not routed to Bun. Import them from `/angular`
|
|
3180
|
+
under Vitest.
|
|
2878
3181
|
|
|
2879
3182
|
---
|
|
2880
3183
|
|
|
@@ -2895,7 +3198,9 @@ setupAngularTestEnv({
|
|
|
2895
3198
|
```
|
|
2896
3199
|
|
|
2897
3200
|
It resets the environment only when the mode actually changes, and the initialisers stay yours —
|
|
2898
|
-
which platform and which providers is not this library's decision.
|
|
3201
|
+
which platform and which providers is not this library's decision. The mode is remembered **per
|
|
3202
|
+
worker**, so under `isolate: false` a run of files all in one mode initialises once: before, every
|
|
3203
|
+
spec file reset the environment and re-ran your initialiser.
|
|
2899
3204
|
|
|
2900
3205
|
---
|
|
2901
3206
|
|
|
@@ -2973,10 +3278,12 @@ chart.pointSelected.emit(2); // outputs are EventEmitters; a model() stays a mod
|
|
|
2973
3278
|
Copied from `ɵcmp` / `ɵdir` / `ɵpipe`: the selector, every input under its public name (transforms
|
|
2974
3279
|
included), every output, `exportAs`, a pipe's name and purity. Not copied: the template (the stub
|
|
2975
3280
|
renders one `<ng-content>` per slot the real one projects — `{ template }` in the third argument
|
|
2976
|
-
replaces it), host bindings, providers, lifecycle hooks, queries
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3281
|
+
replaces it), host bindings, providers, lifecycle hooks, queries, and **`hostDirectives`** — so an
|
|
3282
|
+
input the real component exposes through one is not on the stub, even though `setInputs` accepts that
|
|
3283
|
+
name on the real thing. The second argument seeds members per instance — a method the parent calls
|
|
3284
|
+
through `viewChild`, a pipe's `transform` (identity by default). With `renderShallow`:
|
|
3285
|
+
`renderShallow(Parent, { keepTemplate: true, keepChildren: [ChartStub] })` — the real child is
|
|
3286
|
+
dropped as a component import and the stub is the one child left.
|
|
2980
3287
|
|
|
2981
3288
|
### Patching a property of a spy
|
|
2982
3289
|
|
|
@@ -3107,6 +3414,11 @@ its constructor, and `tick()` in the `fakeAsync` test has to see those timers. U
|
|
|
3107
3414
|
`installProxyZonePatch({ scope: 'callback' })` for `test.concurrent`, where two callbacks are in
|
|
3108
3415
|
flight at once and would otherwise swap the same `ProxyZoneSpec` delegate under one another.
|
|
3109
3416
|
|
|
3417
|
+
Calling `installProxyZonePatch()` yourself is idempotent now: a second call is a no-op instead of
|
|
3418
|
+
wrapping the previous wrapper, and so is the undo it returns. Under `isolate: false` an explicit
|
|
3419
|
+
call in a setup file used to add a `Proxy` layer per spec file — two hundred layers at two hundred
|
|
3420
|
+
files, every `fakeAsync` callback paying for all of them.
|
|
3421
|
+
|
|
3110
3422
|
**Invariant of this package, not a detail of one release:** `zone.js` is a **devDependency and only a
|
|
3111
3423
|
devDependency** — never a dependency, never a peer, not even an optional one. Everything about zones
|
|
3112
3424
|
lives behind this one subpath; no other entry reaches it, even transitively, and the module imports
|
|
@@ -3162,6 +3474,11 @@ their calls. **Do not rely on the import to install them**: it does so once per
|
|
|
3162
3474
|
(`no-import-time-console-spies` reports it). Under `setupAutoSpy({ strayConsole })` the import installs
|
|
3163
3475
|
nothing at all.
|
|
3164
3476
|
|
|
3477
|
+
The spies survive `vi.resetModules()`. A fresh copy of the module used to record the previous copy's
|
|
3478
|
+
spy as "the real `console.warn`", after which `restoreConsole()` installed that dead spy for the rest
|
|
3479
|
+
of the worker and every log from then on went nowhere. The real methods are kept on one shared table
|
|
3480
|
+
and a spy of another copy is refused as an original.
|
|
3481
|
+
|
|
3165
3482
|
Import your runtime entry (`…/bun`, `…/node`) **before** `…/console`, or it registers the Vitest
|
|
3166
3483
|
adapter. Prefer not to touch the real global? `createAutoMock<Console>()` gives a detached one.
|
|
3167
3484
|
|
|
@@ -3195,46 +3512,46 @@ export default [
|
|
|
3195
3512
|
errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
|
|
3196
3513
|
blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
|
|
3197
3514
|
|
|
3198
|
-
| Rule | Level | Fix | Flags
|
|
3199
|
-
| --------------------------------- | ------- | ----------------- |
|
|
3200
|
-
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom`
|
|
3201
|
-
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp`
|
|
3202
|
-
| `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
|
|
3203
|
-
| `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt)
|
|
3204
|
-
| `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 }`
|
|
3205
|
-
| `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 }`
|
|
3206
|
-
| `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 }`
|
|
3207
|
-
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead
|
|
3208
|
-
| `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
|
|
3209
|
-
| `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all
|
|
3210
|
-
| `no-done-callback` | `error` | — |
|
|
3211
|
-
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)`
|
|
3212
|
-
| `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
|
|
3213
|
-
| `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
|
|
3214
|
-
| `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*`
|
|
3215
|
-
| `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
|
|
3216
|
-
| `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
|
|
3217
|
-
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads
|
|
3218
|
-
| `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
|
|
3219
|
-
| `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
|
|
3220
|
-
| `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`
|
|
3221
|
-
| `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo
|
|
3222
|
-
| `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
|
|
3223
|
-
| `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)`
|
|
3224
|
-
| `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`
|
|
3225
|
-
| `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`
|
|
3226
|
-
| `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
|
|
3227
|
-
| `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`
|
|
3228
|
-
| `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
|
|
3229
|
-
| `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>`
|
|
3230
|
-
| `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
|
|
3231
|
-
| `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
|
|
3232
|
-
| `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 such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata`
|
|
3233
|
-
| `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
|
|
3234
|
-
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }`
|
|
3235
|
-
| `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(`
|
|
3236
|
-
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state
|
|
3237
|
-
| `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
|
|
3515
|
+
| Rule | Level | Fix | Flags |
|
|
3516
|
+
| --------------------------------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3517
|
+
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
3518
|
+
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
3519
|
+
| `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 |
|
|
3520
|
+
| `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
|
|
3521
|
+
| `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 }` |
|
|
3522
|
+
| `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 }` |
|
|
3523
|
+
| `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 }` |
|
|
3524
|
+
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
3525
|
+
| `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 |
|
|
3526
|
+
| `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
|
|
3527
|
+
| `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 |
|
|
3528
|
+
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
3529
|
+
| `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 |
|
|
3530
|
+
| `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 |
|
|
3531
|
+
| `no-inject-before-override` | `error` | — | `TestBed.inject()` / `injectSpy()` / `renderShallow()` in a hook, in a suite that still calls `override*` |
|
|
3532
|
+
| `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 |
|
|
3533
|
+
| `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 |
|
|
3534
|
+
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
|
|
3535
|
+
| `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 |
|
|
3536
|
+
| `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 |
|
|
3537
|
+
| `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` |
|
|
3538
|
+
| `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
|
|
3539
|
+
| `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 |
|
|
3540
|
+
| `no-passthrough-console-spy` | `error` | suggest | `vi.spyOn(console, m)` nothing gives an implementation — it calls through and prints → `installConsoleSpies()` + `consoleXSpy`, or `.mockImplementation(() => undefined)` |
|
|
3541
|
+
| `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` |
|
|
3542
|
+
| `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` |
|
|
3543
|
+
| `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 |
|
|
3544
|
+
| `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` |
|
|
3545
|
+
| `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 |
|
|
3546
|
+
| `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>` |
|
|
3547
|
+
| `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 |
|
|
3548
|
+
| `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 |
|
|
3549
|
+
| `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 such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
|
|
3550
|
+
| `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 |
|
|
3551
|
+
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
|
|
3552
|
+
| `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
|
|
3553
|
+
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
3554
|
+
| `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 |
|
|
3238
3555
|
|
|
3239
3556
|
Thirty-eight rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
|
|
3240
3557
|
`no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy` and `prefer-set-inputs`**; four fix on their own, thirteen offer suggestions. Thirty-five are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
|
|
@@ -3399,6 +3716,14 @@ directive. The report sits on the directive's line so that comment reaches it. `
|
|
|
3399
3716
|
`failWith` and `throwWith` are not read: their parameter is `unknown`, so a directive there
|
|
3400
3717
|
suppresses something other than the stub.
|
|
3401
3718
|
|
|
3719
|
+
**What the plugin costs is bounded by the file, not by what is in it.** Three rules used to re-read
|
|
3720
|
+
the file per finding-site rather than once: over this repository's 173 spec files the whole plugin
|
|
3721
|
+
takes **56 ms** where it took 93, and on a single 1.7 MB spec **68 ms** where it took 3 263. The
|
|
3722
|
+
rules answer the same way they did — the ordering rules (`no-inject-before-override`,
|
|
3723
|
+
`no-overridden-provider`) collect the `override*` and `resetTestingModule` positions in one pass and
|
|
3724
|
+
decide by range, `prefer-render-shallow` asks the template-read question once per file, and
|
|
3725
|
+
`no-redundant-smoke-test` indexes identifiers only where a smoke test exists to judge.
|
|
3726
|
+
|
|
3402
3727
|
The legacy `.eslintrc` `plugins: []` form cannot work — it resolves names to `eslint-plugin-*`
|
|
3403
3728
|
packages, which a subpath export can never be.
|
|
3404
3729
|
|
|
@@ -3410,9 +3735,14 @@ packages, which a subpath export can never be.
|
|
|
3410
3735
|
| --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3411
3736
|
| `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
|
|
3412
3737
|
| `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
|
|
3738
|
+
| `… 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) |
|
|
3739
|
+
| ``… 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) |
|
|
3740
|
+
| `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) |
|
|
3741
|
+
| `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) |
|
|
3413
3742
|
| `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 |
|
|
3414
3743
|
| `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) |
|
|
3415
3744
|
| `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) |
|
|
3745
|
+
| `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) |
|
|
3416
3746
|
| `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) |
|
|
3417
3747
|
| `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)` |
|
|
3418
3748
|
| 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) |
|
|
@@ -3422,6 +3752,7 @@ packages, which a subpath export can never be.
|
|
|
3422
3752
|
| `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
|
|
3423
3753
|
| `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 |
|
|
3424
3754
|
| `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
|
|
3755
|
+
| `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) |
|
|
3425
3756
|
| `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
|
|
3426
3757
|
| `X.member was read N times and nothing configured it, and strict mode is on` (or `was subscribed to N times and nothing fed it`), after the test | `setupAutoSpy({ unconfiguredReads })` — the code under test read a strict double's spied getter and got `undefined`, or subscribed to its observable property and nothing ever emitted | configure it: `accessorSpies.getters.member.mockReturnValue(…)`, `overrides: { member }`, `mockReadonlyProp`; `nextWith` / `returnSubject` / `complete` for a stream; `mockReturnValue(undefined)` when `undefined` is the answer meant; `{ strict: false }` on that double (§5) |
|
|
3427
3758
|
| `[vitest-auto-spy] "…" wrote to the console 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 | absorb and assert: `installConsoleSpies()` in a `beforeEach`, then `expect(consoleErrorSpy)…`; or fix the code that printed. Importing a spy installs nothing under the guard. `strayConsole: { allow: [...] }` only for environment noise no spec can reach |
|
|
@@ -3434,6 +3765,9 @@ packages, which a subpath export can never be.
|
|
|
3434
3765
|
| `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()` |
|
|
3435
3766
|
| `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
|
|
3436
3767
|
| `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
|
|
3768
|
+
| `.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) |
|
|
3769
|
+
| `@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) |
|
|
3770
|
+
| `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) |
|
|
3437
3771
|
| `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
|
|
3438
3772
|
| `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) |
|
|
3439
3773
|
| `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) |
|
|
@@ -3465,7 +3799,7 @@ packages, which a subpath export can never be.
|
|
|
3465
3799
|
| `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 |
|
|
3466
3800
|
| `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 |
|
|
3467
3801
|
| `mockSignalProp: '…' is an input() signal` | an `input()` replaced instead of set | `fixture.componentRef.setInput(name, value)`, or `renderShallow(C, { inputs: { … } })` |
|
|
3468
|
-
| `mockSignalProp: '…' is a
|
|
3802
|
+
| `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()` |
|
|
3469
3803
|
| `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 |
|
|
3470
3804
|
| `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
|
|
3471
3805
|
| `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
|
|
@@ -3488,7 +3822,7 @@ packages, which a subpath export can never be.
|
|
|
3488
3822
|
| `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()` |
|
|
3489
3823
|
| `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 |
|
|
3490
3824
|
| 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 |
|
|
3491
|
-
| `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()`
|
|
3825
|
+
| `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" |
|
|
3492
3826
|
| 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 })` |
|
|
3493
3827
|
| 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 |
|
|
3494
3828
|
| `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 |
|
|
@@ -3509,7 +3843,7 @@ packages, which a subpath export can never be.
|
|
|
3509
3843
|
| `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` |
|
|
3510
3844
|
| 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 |
|
|
3511
3845
|
| `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` |
|
|
3512
|
-
| 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
|
|
3846
|
+
| 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 |
|
|
3513
3847
|
| 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) |
|
|
3514
3848
|
| a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
|
|
3515
3849
|
| 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) |
|
|
@@ -3613,6 +3947,28 @@ diagnostic and is free to change: print it, never assert on it.
|
|
|
3613
3947
|
| a bare `vi.spyOn(console, 'error')` to keep a spec quiet | `.mockImplementation(() => undefined)` — without it the line still prints |
|
|
3614
3948
|
| `mockReadonlyProp(c, 'items', vi.fn(() => []))` | `mockReadonlyProp(c, 'items', signal([]))` — a real signal |
|
|
3615
3949
|
| `spy.m.mockReturnValue(subject$)` for a `vi.fn(() => subject$)` | `spy.m.mockImplementation(() => subject$)` — the variable is re-read |
|
|
3950
|
+
| `const overrides = { m: vi.fn(() => of(x)) }` hoisted out of the call | `const overrides: DeepPartial<X> = { … }` — checked against `X` where it is written |
|
|
3951
|
+
|
|
3952
|
+
**A hand-rolled double trips `rxjs-x/finnish`, and a double from this package does not.** The rule
|
|
3953
|
+
reports any name whose type is an `Observable` **or whose call signature returns one**
|
|
3954
|
+
(`couldReturnObservable`). `vi.fn()` and `vi.spyOn()` hand back a callable mock carrying the
|
|
3955
|
+
signature of the method it stands for, so a double for a method returning an `Observable` reads to
|
|
3956
|
+
the rule as a stream and it asks for a `$` — on a spy, where the `$` would lie about what the
|
|
3957
|
+
variable holds. `functions: false` gives no relief: that option is about function _declarations_,
|
|
3958
|
+
not a variable holding one. A `Spy<T>` from `createSpyFromClass`, `provideAutoSpy` or
|
|
3959
|
+
`createAutoMock` is an ordinary object instead — the Observable sits on its members, not on the
|
|
3960
|
+
variable — so nothing is reported, and neither the name nor the assertion has to be bent. Measured
|
|
3961
|
+
on an Angular workspace of 1771 spec files: the rule named seven sites, every one a hand-rolled
|
|
3962
|
+
`vi.fn`/`vi.spyOn`, and none on a double built here.
|
|
3963
|
+
|
|
3964
|
+
The overrides bag is the one shape worth knowing in detail, because the repair is not a rename.
|
|
3965
|
+
Written inline, `provideAutoSpy(X, { overrides: { m: vi.fn(() => of(x)) } })` is invisible to the
|
|
3966
|
+
rule — an object literal inside a call is skipped. Hoisted into a `const` it is reported **per key**,
|
|
3967
|
+
and the key cannot be renamed: it is the method's name on `X`. What clears it is the annotation the
|
|
3968
|
+
bag should carry anyway, `const overrides: DeepPartial<X> = { … }` — the rule skips a literal whose
|
|
3969
|
+
variable is annotated, and the bag starts being checked against `X` where it is written rather than
|
|
3970
|
+
only at the call. For a spy whose name is genuinely free, `*Spy` plus a `names` exemption in the
|
|
3971
|
+
rule's own options says the same thing once for the whole repository.
|
|
3616
3972
|
|
|
3617
3973
|
**The one mechanical rename in a migration that is not equivalent.** `vi.fn(() => x)` reads `x`
|
|
3618
3974
|
when the double is _called_; `mockReturnValue(x)` freezes the value `x` had when the double was
|
|
@@ -3670,6 +4026,26 @@ with `as` still counts), the call must both begin and end a statement, and the p
|
|
|
3670
4026
|
the installed major differs from the table's. Neither has a fixer; `doctor` still never writes.
|
|
3671
4027
|
Full reference: <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
3672
4028
|
|
|
4029
|
+
**Three things about the CLI that decide whether its answer means anything:**
|
|
4030
|
+
|
|
4031
|
+
- **An unknown flag is refused, exit 2, nothing runs.** `init --dryrun` used to write the files and
|
|
4032
|
+
`perf --gat` used to pass with no gate at all — a typo in CI that read as a clean result. The two
|
|
4033
|
+
stderr lines name the flag and list what the command accepts; `--cwd`, `--help` and `--version`
|
|
4034
|
+
work everywhere.
|
|
4035
|
+
- **The scan does not descend into a nested repository or a git worktree.** A tree carrying
|
|
4036
|
+
worktrees under it listed every file twice, so `doctor` reported each import graph in duplicate and
|
|
4037
|
+
`codemod --write` would have rewritten specs on another branch. A `.git` entry is a stop, whether
|
|
4038
|
+
it is a directory (a nested clone) or a file (a worktree). Past 50 000 files the scan still
|
|
4039
|
+
truncates and says so; `VITEST_AUTO_SPY_SCAN_CAP` raises the cap.
|
|
4040
|
+
- **A path that matches no file is an error, exit 2.** _Nothing left to migrate_ off a path nobody
|
|
4041
|
+
read is not a clean result. Absolute paths and `./`-style ones resolve against `--cwd`.
|
|
4042
|
+
|
|
4043
|
+
Exit codes, as the commands implement them: **0** is "ran, nothing to report"; **1** is "ran, and
|
|
4044
|
+
here is the finding" — `doctor` with an error, `init --check` with a stale block, `codemod` with any
|
|
4045
|
+
note, `perf --gate` over budget; **2** is "there was nothing to judge" — no command, an unknown
|
|
4046
|
+
command, an unknown flag, an unreadable `--only` / `--from` value, a path matching no file, and a
|
|
4047
|
+
`perf` run that measured nothing (including a red suite, which the gate will not judge at all).
|
|
4048
|
+
|
|
3673
4049
|
### If you were asked why a suite is slow
|
|
3674
4050
|
|
|
3675
4051
|
```bash
|
|
@@ -3769,6 +4145,20 @@ migrated by hand. Run it after `--write`, and again after any manual clean-up. `
|
|
|
3769
4145
|
select transforms by id, `--list` prints them. Full reference:
|
|
3770
4146
|
<https://asdalexey.github.io/vitest-auto-spy/utilities/codemod>.
|
|
3771
4147
|
|
|
4148
|
+
**Every rewrite is parsed before it is written.** The result goes through the project's own
|
|
4149
|
+
`typescript` and its diagnostics are compared with the original's; a file the run would have broken
|
|
4150
|
+
is reported as `codemod-broke-syntax` and **left exactly as it was**, so the run exits 1 with one
|
|
4151
|
+
file named rather than a tree that no longer compiles. Where `typescript` is not installed the check
|
|
4152
|
+
is skipped silently — it is a safety net, not an install instruction.
|
|
4153
|
+
|
|
4154
|
+
It visits JavaScript specs too — `*.spec.js`, `*.test.jsx`, the `.cjs` / `.mjs` forms — because a
|
|
4155
|
+
Jest suite that was never TypeScript is the suite with the most `jest.` in it. Two things it now
|
|
4156
|
+
reports instead of rewriting into something wrong: `jest.fn<R, [A]>()` / `jest.spyOn<…>()` in a file
|
|
4157
|
+
that imports from `@jest/globals` (`jest-mock-type-arguments` — `jest-mock` 29 already takes the
|
|
4158
|
+
whole function type, so transposing a second time produces a return type of a return type), and
|
|
4159
|
+
`.withArgs(…)` on a `vi.spyOn` chain (`jasmine-with-args-on-spy-on` — `vi.spyOn` has no
|
|
4160
|
+
`calledWith`, and renaming it onto one produces a method that does not exist).
|
|
4161
|
+
|
|
3772
4162
|
Past 50 000 files the repository scan truncates and the run says so — _Nothing left to migrate_ off a
|
|
3773
4163
|
truncated list is a claim about a tree the tool never looked at. `VITEST_AUTO_SPY_SCAN_CAP` raises
|
|
3774
4164
|
the cap.
|
|
@@ -3826,6 +4216,27 @@ npx vitest-auto-spy codemod --from jasmine --verify # match the result, not th
|
|
|
3826
4216
|
matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
|
|
3827
4217
|
on `globalThis` — it is one explicit line per file, which the codemod later deletes.
|
|
3828
4218
|
|
|
4219
|
+
Four places where that surface now matches jasmine's rather than approximating it:
|
|
4220
|
+
|
|
4221
|
+
- **`spy.withArgs(…).and` carries the strategies that describe one argument list** — `stub()`,
|
|
4222
|
+
`throwError(…)`, `resolveTo(…)` and `returnValue(…)`, plus this library's own `resolveWith` /
|
|
4223
|
+
`nextWith` / `returnSubject` family. The three that install an _implementation_ —
|
|
4224
|
+
`callFake`, `callThrough`, `returnValues` — are present and **throw** a message naming the
|
|
4225
|
+
alternative, rather than arriving as `… is not a function`: an implementation answers every call,
|
|
4226
|
+
which is the opposite of configuring one argument list. Take the whole spy with `spy.and.callFake(…)`
|
|
4227
|
+
if that is what the line meant.
|
|
4228
|
+
- **`jasmine.mapContaining` compares keys with the runner's equality**, as jasmine does, so an
|
|
4229
|
+
asymmetric matcher as a key and an object key compared deeply both work. `Map.has` alone answered
|
|
4230
|
+
on reference identity and missed both.
|
|
4231
|
+
- **`jasmine.clock().install()` leaves `Date` real**, exactly as jasmine's does — the timers are
|
|
4232
|
+
faked, the clock is not. `jasmine.clock().mockDate()` is what takes `Date` over, and because that
|
|
4233
|
+
re-installs the fake clock it reports when callbacks were already scheduled and have just been
|
|
4234
|
+
dropped. Call `mockDate()` right after `install()`, before anything schedules a timer.
|
|
4235
|
+
- **`jasmine.createSpyObj`'s third argument builds spied accessors**, reachable through
|
|
4236
|
+
`Object.getOwnPropertyDescriptor(obj, name).get`. Reading still answers the seed, so nothing about
|
|
4237
|
+
a migrated spec changes; what is gained is moving the value mid-test and asserting that the code
|
|
4238
|
+
under test wrote it.
|
|
4239
|
+
|
|
3829
4240
|
### The renames, once the suite is green
|
|
3830
4241
|
|
|
3831
4242
|
| jasmine | here |
|
|
@@ -3875,8 +4286,10 @@ on `globalThis` — it is one explicit line per file, which the codemod later de
|
|
|
3875
4286
|
once naming both rather than silently swallowing the write.
|
|
3876
4287
|
|
|
3877
4288
|
`it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
|
|
3878
|
-
shape, not a name. Use `async` + `await`; `no-done-callback` reports
|
|
3879
|
-
`done.fail(…)`.
|
|
4289
|
+
shape, not a name. Use `async` + `await`; `no-done-callback` reports the parameter and any
|
|
4290
|
+
`done.fail(…)`. It reports the parameter that is _called_, handed on as an argument, or never used —
|
|
4291
|
+
not every named first parameter: `it('x', (ctx) => ctx.skip())` is Vitest's own `TestContext` read
|
|
4292
|
+
without destructuring, which is legal, and used to be reported as a `done` callback.
|
|
3880
4293
|
|
|
3881
4294
|
On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
|
|
3882
4295
|
importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a
|