vitest-auto-spy 5.16.0 → 5.17.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 +27 -2
- package/README.md +13 -2
- package/dist/cli.js +4 -1
- package/dist/setup.d.ts +46 -4
- package/dist/setup.js +145 -12
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +95 -92
package/AGENTS.md
CHANGED
|
@@ -1662,6 +1662,28 @@ a `useValue` provider or a test double — patch the prototype of the class the
|
|
|
1662
1662
|
`guardPrototypePollution(reaction)` from `/setup` registers the same check on its own, for a suite
|
|
1663
1663
|
that does not call `setupAutoSpy()`.
|
|
1664
1664
|
|
|
1665
|
+
### Naming the test that left an attribute on `<body>`
|
|
1666
|
+
|
|
1667
|
+
```ts
|
|
1668
|
+
setupAutoSpy({ documentPollution: 'throw' }); // default 'off'; 'throw' under preset: 'strict'
|
|
1669
|
+
setupAutoSpy({ documentPollution: { reaction: 'throw', nodes: true, ignoreAttributes: [/^data-cdk-/], ignoreNodes: 'style' } });
|
|
1670
|
+
```
|
|
1671
|
+
|
|
1672
|
+
Under `isolate: false` every spec file in a worker shares one document. An attribute a component set
|
|
1673
|
+
on `<body>` — `renderer.setAttribute(document.body, 'data-reset-focus', '')` in an `effect` — that
|
|
1674
|
+
nothing took off changes the branch a later file's code takes, so that file fails, only when the two
|
|
1675
|
+
share a worker and never alone. The guard records the attributes of `<html>`, `<head>` and `<body>`
|
|
1676
|
+
before each test and compares them from `onTestFinished` — **after** the TestBed destroyed the
|
|
1677
|
+
fixtures in its own `afterEach`, so what a component removes in `ngOnDestroy` / `DestroyRef.onDestroy`
|
|
1678
|
+
is never reported. Every attribute added, changed or removed is named with both values, put back,
|
|
1679
|
+
and fails that test. A change made in a `beforeAll` and never undone fails the file, checked from a
|
|
1680
|
+
`beforeAll` cleanup after every `afterAll`. `nodes: true` also watches the child elements of `<head>`
|
|
1681
|
+
and `<body>`; it is off by default because a module that injects a stylesheet on first import does
|
|
1682
|
+
so once per worker. Blind spots: a write made while the spec file is imported, and a fixture kept
|
|
1683
|
+
alive by `teardown: { destroyAfterEach: false }` (reported against the test that rendered it).
|
|
1684
|
+
`guardDocumentPollution(option)` from `/setup` registers the same check on its own. Vitest only, like
|
|
1685
|
+
every `/setup` guard — `bun:test`, `node:test` and Rstest have no setup entry to host it.
|
|
1686
|
+
|
|
1665
1687
|
### Failing on console output nothing absorbed
|
|
1666
1688
|
|
|
1667
1689
|
```ts
|
|
@@ -1701,8 +1723,8 @@ frame outside `node_modules`:
|
|
|
1701
1723
|
setupAutoSpy({ preset: 'strict' });
|
|
1702
1724
|
```
|
|
1703
1725
|
|
|
1704
|
-
Sets `duplicateCopies`, `propsOutsideHooks`, `guardGlobals`, `prototypePollution`, `
|
|
1705
|
-
`misconfiguration` to `'throw'`, turns `strayTimers` on, and `strayRejections` on when zone.js is
|
|
1726
|
+
Sets `duplicateCopies`, `propsOutsideHooks`, `guardGlobals`, `prototypePollution`, `documentPollution`,
|
|
1727
|
+
`strayConsole` and `misconfiguration` to `'throw'`, turns `strayTimers` on, and `strayRejections` on when zone.js is
|
|
1706
1728
|
loaded. An option passed alongside still wins. **Not** included: `strict` (strict doubles change what
|
|
1707
1729
|
an unconfigured call returns — a semantic switch, not a grade; the name was taken, hence `preset`) and
|
|
1708
1730
|
`unconfiguredReads`, its read side (survey with `onUnstubbedRead` before turning it on),
|
|
@@ -3459,6 +3481,9 @@ packages, which a subpath export can never be.
|
|
|
3459
3481
|
| `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
|
|
3460
3482
|
| a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
|
|
3461
3483
|
| a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
|
|
3484
|
+
| a spec green alone that fails only in a full run, on something it reads off `<body>` / `<html>` (`querySelector('[data-…]')`, a class) | an earlier file in the worker left an attribute on the shared document | `setupAutoSpy({ documentPollution: 'throw' })` (in `preset: 'strict'`) names the test; take the attribute off in `ngOnDestroy` / `DestroyRef.onDestroy` or an `afterEach` |
|
|
3485
|
+
| `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()` |
|
|
3486
|
+
| `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 |
|
|
3462
3487
|
| 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 |
|
|
3463
3488
|
| `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()` |
|
|
3464
3489
|
| 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 })` |
|
package/README.md
CHANGED
|
@@ -2923,6 +2923,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
2923
2923
|
| `withoutStrayTimerTracking(work)` | `/setup` | Run setup work whose timers the stray-timer tracker neither counts nor cancels — jsdom schedules one per Web Storage write |
|
|
2924
2924
|
| `describeStrayTimers()` | `/setup` | Every timer still pending, with its kind, the spec file that scheduled it and the scheduling frames — the list `onStrayTimers` gets |
|
|
2925
2925
|
| `guardPrototypePollution(reaction)` | `/setup` | Name the test that left a key on `Object.prototype` — it stops later files collecting ([details](#test-run-hygiene)) |
|
|
2926
|
+
| `guardDocumentPollution(option)` | `/setup` | Name the test that left an attribute on `<html>` / `<body>`, and put it back ([details](#test-run-hygiene)) |
|
|
2926
2927
|
| `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
|
|
2927
2928
|
| `setupAngularTestEnv(opts)` | `/angular` | Zone and zoneless spec files in one worker, switching platforms per file |
|
|
2928
2929
|
| `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
|
|
@@ -3202,8 +3203,16 @@ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom al
|
|
|
3202
3203
|
console method a test replaced is put back after it, so one file's silence cannot reach the next,
|
|
3203
3204
|
and the library's own warnings count like any other output. `allow: [...]` is the last resort,
|
|
3204
3205
|
for environment noise no spec can reach.
|
|
3205
|
-
13. **
|
|
3206
|
-
`
|
|
3206
|
+
13. **Attributes left on the shared document.** Opt-in, `documentPollution: 'throw'`, and on under
|
|
3207
|
+
`preset: 'strict'`. Under `isolate: false` a worker's spec files share one jsdom document, so a
|
|
3208
|
+
`data-*` attribute or a class a component set on `<body>` and never took off changes what a later
|
|
3209
|
+
file's code sees: `document.querySelector('[data-reset-focus]')` matches, a service returns early,
|
|
3210
|
+
and 34 of 209 tests fail only when the two files share a worker. The guard compares the attributes
|
|
3211
|
+
of `<html>`, `<head>` and `<body>` around every test, puts them back and fails the test that changed
|
|
3212
|
+
them; `{ nodes: true }` watches their child elements too. It looks after the TestBed's own teardown,
|
|
3213
|
+
so what a destroyed component cleans up is never reported.
|
|
3214
|
+
14. **Every guard at its strictest — `preset: 'strict'`.** `duplicateCopies`, `propsOutsideHooks`,
|
|
3215
|
+
`guardGlobals`, `prototypePollution`, `documentPollution`, `strayConsole` and `misconfiguration` to `'throw'`,
|
|
3207
3216
|
`strayTimers` on, `strayRejections` on where zone.js is loaded; an option passed alongside still
|
|
3208
3217
|
wins. Not in it: strict doubles (a semantic switch, not a grade) and `unconfiguredReads`, their read
|
|
3209
3218
|
side, `blockNetwork`, `restoreMocks`,
|
|
@@ -3224,6 +3233,7 @@ not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom al
|
|
|
3224
3233
|
| `blockNetwork` | `false` | Close every network channel the environment has — `true`, or a narrowing object |
|
|
3225
3234
|
| `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
|
|
3226
3235
|
| `prototypePollution` | `'throw'` | Sweep and report an enumerable key a test left on a built-in prototype |
|
|
3236
|
+
| `documentPollution` | `'off'` | Put back and report an attribute a test left on `<html>` / `<head>` / `<body>` — `'warn'`, `'throw'`, or `{ reaction, nodes, ignoreAttributes, ignoreNodes }` |
|
|
3227
3237
|
| `strayConsole` | `'off'` | Fail a test (or a file) whose console output nothing absorbed — `'warn'`, `'throw'`, or `{ reaction, allow }` |
|
|
3228
3238
|
| `misconfiguration` | `'warn'` | `'throw'` fails the library's own misuse reports at the call site, every occurrence |
|
|
3229
3239
|
| `preset` | — | `'strict'` starts every guard above at its strictest grade; explicit options still win |
|
|
@@ -3800,6 +3810,7 @@ another, and the next one is in that same file.
|
|
|
3800
3810
|
| `guardStrayConsole(reaction)` / `withoutStrayTimerTracking(work)` _(`/setup`)_ | Fail a test on console output nothing absorbed; keep setup work's timers out of the stray-timer count |
|
|
3801
3811
|
| `describeStrayTimers()` _(`/setup`)_ | Each pending timer with the spec file and frames that scheduled it — for a suite that sweeps by hand |
|
|
3802
3812
|
| `guardPrototypePollution(reaction)` _(`/setup`)_ | Name the test that left a key on `Object.prototype`, before the next file fails to collect |
|
|
3813
|
+
| `guardDocumentPollution(option)` _(`/setup`)_ | Name the test that left an attribute on `<html>` / `<body>`, before a later file takes another branch on it |
|
|
3803
3814
|
| `consoleDebugSpy` … `consoleWarnSpy` _(`/console`)_ | Silent typed spies replacing the global `console` methods on import |
|
|
3804
3815
|
| `installConsoleSpies()` / `resetConsoleSpies()` / `restoreConsole()` | Install / clear / undo the console spies |
|
|
3805
3816
|
| `explainSpy(spy, method?)` _(`/diagnostics`)_ | Every configured argument list next to every recorded call, attributed to the config it hit |
|
package/dist/cli.js
CHANGED
|
@@ -2455,7 +2455,7 @@ function checkForeignPragma(graph) {
|
|
|
2455
2455
|
}
|
|
2456
2456
|
|
|
2457
2457
|
// src/cli/checks/export-map.generated.ts
|
|
2458
|
-
var EXPORT_MAP_VERSION = "5.
|
|
2458
|
+
var EXPORT_MAP_VERSION = "5.17.0";
|
|
2459
2459
|
var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin";
|
|
2460
2460
|
var EXPORTED_BY = {
|
|
2461
2461
|
AccessorImplementations: "0 1 2 3 4 12 17 18 19",
|
|
@@ -2571,6 +2571,8 @@ var EXPORTED_BY = {
|
|
|
2571
2571
|
DirectiveHostOptions: "12",
|
|
2572
2572
|
disableAngularDiagnostics: "12",
|
|
2573
2573
|
disableTestBedDiagnostics: "12",
|
|
2574
|
+
DocumentPollutionOptions: "20",
|
|
2575
|
+
DocumentPollutionReaction: "20",
|
|
2574
2576
|
DomRegistrar: "2",
|
|
2575
2577
|
DuplicateCopiesReaction: "20",
|
|
2576
2578
|
EmissionObserver: "0 1 2 3 4 12 17 18 19",
|
|
@@ -2612,6 +2614,7 @@ var EXPORTED_BY = {
|
|
|
2612
2614
|
getWatchedTimerGlobals: "20",
|
|
2613
2615
|
GlobalPatchReaction: "20",
|
|
2614
2616
|
GlobalRegistratorOptions: "2",
|
|
2617
|
+
guardDocumentPollution: "20",
|
|
2615
2618
|
guardGlobalPatches: "20",
|
|
2616
2619
|
guardPrototypePollution: "20",
|
|
2617
2620
|
guardStrayConsole: "20",
|
package/dist/setup.d.ts
CHANGED
|
@@ -4,6 +4,39 @@ import { z as UnstubbedCallHandler, G as UnstubbedReadHandler } from './types-D3
|
|
|
4
4
|
export { d as describeDuplicateCopies, g as getPackageCopies, t as takeStrictViolations } from './package-identity-lm-gqZAR.js';
|
|
5
5
|
export { R as RestoreWebStorageOptions, r as restoreWebStorage } from './web-storage-dwQ9E7Lm.js';
|
|
6
6
|
|
|
7
|
+
/** How a per-test guard reacts to what it found: fail the test, only report it, or not run at all. */
|
|
8
|
+
type GuardReaction = 'off' | 'throw' | 'warn';
|
|
9
|
+
|
|
10
|
+
/** How {@link guardDocumentPollution} reacts to a document a test left changed. */
|
|
11
|
+
type DocumentPollutionReaction = GuardReaction;
|
|
12
|
+
/** The object form of `documentPollution`, for a reaction plus what a project cannot clean up. */
|
|
13
|
+
interface DocumentPollutionOptions {
|
|
14
|
+
/** Default `'throw'`. */
|
|
15
|
+
reaction?: DocumentPollutionReaction;
|
|
16
|
+
/**
|
|
17
|
+
* Also watch the element children of `<head>` and `<body>`. Default `false`: a module that injects a
|
|
18
|
+
* stylesheet when it is first imported does so once per worker, and a node check would charge that
|
|
19
|
+
* `<style>` to whichever test happened to import it first.
|
|
20
|
+
*/
|
|
21
|
+
nodes?: boolean;
|
|
22
|
+
/** Attribute names left alone, as an exact name or a tested RegExp. Matched on all three elements. */
|
|
23
|
+
ignoreAttributes?: readonly (RegExp | string)[];
|
|
24
|
+
/** A CSS selector for child elements left alone when `nodes` is on — `'style, link[rel=stylesheet]'`. */
|
|
25
|
+
ignoreNodes?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Watch `<html>`, `<head>` and `<body>` for attributes — and, when asked, children — a test leaves
|
|
29
|
+
* changed, put them back, and name the test that changed them.
|
|
30
|
+
*
|
|
31
|
+
* Registers the hooks itself; `setupAutoSpy({ documentPollution: … })` is how a project turns it on,
|
|
32
|
+
* and `preset: 'strict'` turns it on at `'throw'`. Off by default: a leftover attribute breaks a later
|
|
33
|
+
* file only when something reads it, and a suite that has lived with a few must not go red on upgrade.
|
|
34
|
+
*
|
|
35
|
+
* @param option `'throw'` fails the test (or, for a `beforeAll` leftover, the file), `'warn'` puts the
|
|
36
|
+
* document back and only reports it, `'off'` registers nothing — including the repair.
|
|
37
|
+
*/
|
|
38
|
+
declare function guardDocumentPollution(option: DocumentPollutionOptions | DocumentPollutionReaction): void;
|
|
39
|
+
|
|
7
40
|
/**
|
|
8
41
|
* Fake-timer helpers — the boilerplate every suite that tests a debounce, a poll or a retry ends up
|
|
9
42
|
* writing by hand, and the one mistake it makes while doing so.
|
|
@@ -86,9 +119,6 @@ interface SetupFakeTimersOptions {
|
|
|
86
119
|
*/
|
|
87
120
|
declare function advanceTimers(ms?: number): Promise<void>;
|
|
88
121
|
|
|
89
|
-
/** How a per-test guard reacts to what it found: fail the test, only report it, or not run at all. */
|
|
90
|
-
type GuardReaction = 'off' | 'throw' | 'warn';
|
|
91
|
-
|
|
92
122
|
/** How {@link guardGlobalPatches} reacts to a patch that cannot be undone. */
|
|
93
123
|
type GlobalPatchReaction = GuardReaction;
|
|
94
124
|
/**
|
|
@@ -587,6 +617,18 @@ interface SetupAutoSpyOptions {
|
|
|
587
617
|
* {@link guardPrototypePollution}.
|
|
588
618
|
*/
|
|
589
619
|
prototypePollution?: PrototypePollutionReaction;
|
|
620
|
+
/**
|
|
621
|
+
* Report — and put back — an attribute a test leaves added, changed or removed on `<html>`, `<head>`
|
|
622
|
+
* or `<body>`; with `{ nodes: true }`, a child element of `<head>` or `<body>` as well. Default
|
|
623
|
+
* `'off'`; `'throw'` under `preset: 'strict'`.
|
|
624
|
+
*
|
|
625
|
+
* Under `isolate: false` every file in a worker shares one document, so a `data-*` attribute or a
|
|
626
|
+
* class a component set on `<body>` and never took off changes the branch some later file's code
|
|
627
|
+
* takes — a failure in a file that never touched it, only when the two share a worker. Checked after
|
|
628
|
+
* the TestBed's own teardown, so what a destroyed component cleans up is never reported. See
|
|
629
|
+
* {@link guardDocumentPollution}.
|
|
630
|
+
*/
|
|
631
|
+
documentPollution?: DocumentPollutionOptions | DocumentPollutionReaction;
|
|
590
632
|
/**
|
|
591
633
|
* Put back timer globals that uninstalling the fakes removed rather than restored. Default `true`:
|
|
592
634
|
* it only ever replaces a global that has gone missing, so it cannot overwrite anything a spec
|
|
@@ -987,4 +1029,4 @@ declare function restoreTimerGlobals(): void;
|
|
|
987
1029
|
/** The names this module watches — exported for the diagnostics that report what went missing. */
|
|
988
1030
|
declare function getWatchedTimerGlobals(): string[];
|
|
989
1031
|
|
|
990
|
-
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, type BlockNetworkOptions, type CountingClock, type CountingClockOptions, type DuplicateCopiesReaction, type FakeTimersConfig, type GlobalPatchReaction, type MisconfigurationReaction, type PerTestHandle, type PrototypePollutionReaction, type RejectionHost, type SchedulerHost, type SetupAutoSpyOptions, type SetupAutoSpyPreset, type SpyEngine, type StopTrackingRejections, type StopTrackingTimers, type StrayConsoleOptions, type StrayConsoleReaction, type StrayRejection, type StrayTimer, type StrayTimerReport, type SwallowedStrictCallsReaction, type SystemTime, type UnconfiguredReadsReaction, type XhrBlockMode, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, describeStrayTimers, flushStrayRejections, getMockRegistrySize, getSpyEngine, getWatchedTimerGlobals, guardGlobalPatches, guardPrototypePollution, guardStrayConsole, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, setSpyEngine, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime, withoutStrayTimerTracking };
|
|
1032
|
+
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, type BlockNetworkOptions, type CountingClock, type CountingClockOptions, type DocumentPollutionOptions, type DocumentPollutionReaction, type DuplicateCopiesReaction, type FakeTimersConfig, type GlobalPatchReaction, type MisconfigurationReaction, type PerTestHandle, type PrototypePollutionReaction, type RejectionHost, type SchedulerHost, type SetupAutoSpyOptions, type SetupAutoSpyPreset, type SpyEngine, type StopTrackingRejections, type StopTrackingTimers, type StrayConsoleOptions, type StrayConsoleReaction, type StrayRejection, type StrayTimer, type StrayTimerReport, type SwallowedStrictCallsReaction, type SystemTime, type UnconfiguredReadsReaction, type XhrBlockMode, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, describeStrayTimers, flushStrayRejections, getMockRegistrySize, getSpyEngine, getWatchedTimerGlobals, guardDocumentPollution, guardGlobalPatches, guardPrototypePollution, guardStrayConsole, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, setSpyEngine, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime, withoutStrayTimerTracking };
|
package/dist/setup.js
CHANGED
|
@@ -10,7 +10,7 @@ import { defineHelper, setDefaultStrictMode, setMisconfigurationReaction, takeSt
|
|
|
10
10
|
export { takeStrictViolations } from './chunk-MUJTPXMJ.js';
|
|
11
11
|
import './chunk-OTOCFH6B.js';
|
|
12
12
|
import { withDocs, DOCS_LINKS } from './chunk-Q477VHHF.js';
|
|
13
|
-
import { beforeAll, beforeEach, afterEach, afterAll, vi,
|
|
13
|
+
import { beforeAll, beforeEach, onTestFinished, afterEach, afterAll, vi, expect } from 'vitest';
|
|
14
14
|
import { describeDuplicateCopies } from './shared-state.js';
|
|
15
15
|
export { describeDuplicateCopies, getPackageCopies } from './shared-state.js';
|
|
16
16
|
|
|
@@ -110,6 +110,137 @@ function noticeAngularBuildSplitting(write = writeWarning, readVersion = readIns
|
|
|
110
110
|
)
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
|
+
var QUOTED_VALUE_LENGTH = 80;
|
|
114
|
+
function resolveDocumentPollution(option) {
|
|
115
|
+
if (typeof option === "object") {
|
|
116
|
+
return {
|
|
117
|
+
reaction: option.reaction ?? "throw",
|
|
118
|
+
nodes: option.nodes ?? false,
|
|
119
|
+
ignoreAttributes: option.ignoreAttributes ?? [],
|
|
120
|
+
ignoreNodes: option.ignoreNodes
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { reaction: option ?? "off", nodes: false, ignoreAttributes: [], ignoreNodes: void 0 };
|
|
124
|
+
}
|
|
125
|
+
function isIgnoredAttribute(name, ignore) {
|
|
126
|
+
return ignore.some((pattern) => typeof pattern === "string" ? pattern === name : pattern.test(name));
|
|
127
|
+
}
|
|
128
|
+
function readAttributes(element, ignore) {
|
|
129
|
+
return new Map([...element.attributes].filter(({ name }) => !isIgnoredAttribute(name, ignore)).map(({ name, value }) => [name, value]));
|
|
130
|
+
}
|
|
131
|
+
function readChildren(element, options) {
|
|
132
|
+
if (!options.nodes) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const ignore = options.ignoreNodes;
|
|
136
|
+
return [...element.children].filter((child) => ignore === void 0 || !child.matches(ignore));
|
|
137
|
+
}
|
|
138
|
+
function snapshotDocument(options, doc = Reflect.get(globalThis, "document")) {
|
|
139
|
+
if (!doc) {
|
|
140
|
+
return { watched: [], options };
|
|
141
|
+
}
|
|
142
|
+
const candidates = [
|
|
143
|
+
["<html>", doc.documentElement],
|
|
144
|
+
["<head>", doc.head],
|
|
145
|
+
["<body>", doc.body]
|
|
146
|
+
];
|
|
147
|
+
const watched = candidates.flatMap(
|
|
148
|
+
([label, element]) => element ? [{ label, element, attributes: readAttributes(element, options.ignoreAttributes), children: readChildren(element, options) }] : []
|
|
149
|
+
);
|
|
150
|
+
return { watched, options };
|
|
151
|
+
}
|
|
152
|
+
function quote(value) {
|
|
153
|
+
const shown = value.length > QUOTED_VALUE_LENGTH ? `${value.slice(0, QUOTED_VALUE_LENGTH)}\u2026` : value;
|
|
154
|
+
return JSON.stringify(shown);
|
|
155
|
+
}
|
|
156
|
+
function restoreAttributes({ label, element, attributes }, ignore) {
|
|
157
|
+
const current = readAttributes(element, ignore);
|
|
158
|
+
const lines = [];
|
|
159
|
+
current.forEach((value, name) => {
|
|
160
|
+
const before = attributes.get(name);
|
|
161
|
+
if (before === void 0) {
|
|
162
|
+
lines.push(`${label} ${name}=${quote(value)} added`);
|
|
163
|
+
element.removeAttribute(name);
|
|
164
|
+
} else if (before !== value) {
|
|
165
|
+
lines.push(`${label} ${name} changed from ${quote(before)} to ${quote(value)}`);
|
|
166
|
+
element.setAttribute(name, before);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
attributes.forEach((before, name) => {
|
|
170
|
+
if (!current.has(name)) {
|
|
171
|
+
lines.push(`${label} ${name} removed (was ${quote(before)})`);
|
|
172
|
+
element.setAttribute(name, before);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
function describeElement(element) {
|
|
178
|
+
const id = element.id ? ` id="${element.id}"` : "";
|
|
179
|
+
const className = element.getAttribute("class");
|
|
180
|
+
return `<${element.localName}${id}${className ? ` class="${className}"` : ""}>`;
|
|
181
|
+
}
|
|
182
|
+
function restoreChildren({ label, element, children }, options) {
|
|
183
|
+
const before = children ?? [];
|
|
184
|
+
const now = readChildren(element, options) ?? [];
|
|
185
|
+
const added = now.filter((child) => !before.includes(child));
|
|
186
|
+
const removed = before.filter((child) => child.parentNode !== element);
|
|
187
|
+
added.forEach((child) => child.remove());
|
|
188
|
+
let anchor = null;
|
|
189
|
+
for (const child of [...before].reverse()) {
|
|
190
|
+
if (child.parentNode !== element) {
|
|
191
|
+
element.insertBefore(child, anchor);
|
|
192
|
+
}
|
|
193
|
+
anchor = child;
|
|
194
|
+
}
|
|
195
|
+
return [
|
|
196
|
+
...added.map((child) => `${label} child ${describeElement(child)} added`),
|
|
197
|
+
...removed.map((child) => `${label} child ${describeElement(child)} removed`)
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
function report(lines, scope) {
|
|
201
|
+
return withDocs(
|
|
202
|
+
`[vitest-auto-spy] ${scope} left the shared document changed:
|
|
203
|
+
${lines.map((line) => ` - ${line}`).join("\n")}
|
|
204
|
+
Under \`isolate: false\` every later spec file in this worker runs against that document, and code that reads it \u2014 \`document.querySelector('[data-reset-focus]')\`, a class on <body> \u2014 takes another branch there, so the failure lands in a file that never touched it, and only when the two share a worker. The document has been put back. Undo the change where it was made: in the \`ngOnDestroy\` / \`DestroyRef.onDestroy\` of the component that set it, in an \`afterEach\` of this spec, or by destroying the fixture that owns it.`,
|
|
205
|
+
DOCS_LINKS.setup
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
function checkDocumentPollution(snapshot, scope, write = writeWarning) {
|
|
209
|
+
const { options } = snapshot;
|
|
210
|
+
const lines = snapshot.watched.flatMap((watched) => [
|
|
211
|
+
...restoreAttributes(watched, options.ignoreAttributes),
|
|
212
|
+
...restoreChildren(watched, options)
|
|
213
|
+
]);
|
|
214
|
+
if (lines.length === 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const message = report(lines, scope);
|
|
218
|
+
if (options.reaction === "throw") {
|
|
219
|
+
throw new Error(message);
|
|
220
|
+
}
|
|
221
|
+
write(message);
|
|
222
|
+
}
|
|
223
|
+
function testScope() {
|
|
224
|
+
const { currentTestName, testPath } = expect.getState();
|
|
225
|
+
return `"${currentTestName ?? "this test"}" (${testPath ?? "this file"})`;
|
|
226
|
+
}
|
|
227
|
+
function fileScope() {
|
|
228
|
+
return `${expect.getState().testPath ?? "this file"}, outside any test (a beforeAll or afterAll),`;
|
|
229
|
+
}
|
|
230
|
+
function guardDocumentPollution(option) {
|
|
231
|
+
const options = resolveDocumentPollution(option);
|
|
232
|
+
if (options.reaction === "off") {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
beforeAll(() => {
|
|
236
|
+
const file = snapshotDocument(options);
|
|
237
|
+
return () => checkDocumentPollution(file, fileScope());
|
|
238
|
+
});
|
|
239
|
+
beforeEach(() => {
|
|
240
|
+
const test = snapshotDocument(options);
|
|
241
|
+
onTestFinished(() => checkDocumentPollution(test, testScope()));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
113
244
|
|
|
114
245
|
// src/lib/timer-globals.ts
|
|
115
246
|
var TIMER_GLOBALS = [
|
|
@@ -234,7 +365,7 @@ function sealedAdditions({ object, names }) {
|
|
|
234
365
|
}
|
|
235
366
|
return added.filter((name) => isSealed(object, name));
|
|
236
367
|
}
|
|
237
|
-
function
|
|
368
|
+
function report2({ name }, added) {
|
|
238
369
|
const testPath = expect.getState().testPath ?? "this file";
|
|
239
370
|
return withDocs(
|
|
240
371
|
`[vitest-auto-spy] ${testPath} redefined ${added.map((property) => `${name}.${property}`).join(", ")} as a non-configurable own property, so nothing can put it back \u2014 not \`restoreMockedProps()\`, not \`vi.unstubAllGlobals()\`, not the next file's own \`Object.defineProperty\`. \`Object.defineProperty\` defaults \`configurable\` to \`false\`; use \`mockValueProp(${name}, '${added[0]}', value)\`, which records the descriptor it replaced and registers the undo.`,
|
|
@@ -244,7 +375,7 @@ function report({ name }, added) {
|
|
|
244
375
|
function checkSealedAdditions(before, reaction) {
|
|
245
376
|
const found = before.flatMap((snapshot) => {
|
|
246
377
|
const added = sealedAdditions(snapshot);
|
|
247
|
-
return added.length > 0 ? [
|
|
378
|
+
return added.length > 0 ? [report2(snapshot, added)] : [];
|
|
248
379
|
});
|
|
249
380
|
reactToFindings(found, reaction);
|
|
250
381
|
}
|
|
@@ -514,7 +645,7 @@ function pollutingAdditions({ object, keys }) {
|
|
|
514
645
|
});
|
|
515
646
|
return added;
|
|
516
647
|
}
|
|
517
|
-
function
|
|
648
|
+
function report3({ name }, added) {
|
|
518
649
|
const testPath = expect.getState().testPath ?? "this file";
|
|
519
650
|
return withDocs(
|
|
520
651
|
`[vitest-auto-spy] ${testPath} left ${added.map((key) => `"${key}"`).join(", ")} on ${name} as an own enumerable property. Vitest walks a file's hooks with \`for\u2026in\`, so the extra key is spread as if it were an array and **every spec file after this one in the same worker fails to collect** \u2014 with no stack, because every frame of that error is filtered out of the report. The key has been taken back off so the rest of the run survives. Patch the prototype of the class the object came from, not the prototype of a plain object or of a test double: \`Object.getPrototypeOf(instance)\` is \`Object.prototype\` itself whenever \`instance\` is an object literal.`,
|
|
@@ -524,7 +655,7 @@ function report2({ name }, added) {
|
|
|
524
655
|
function checkPrototypePollution(before, reaction) {
|
|
525
656
|
const found = before.flatMap((snapshot) => {
|
|
526
657
|
const added = pollutingAdditions(snapshot);
|
|
527
|
-
return added.length > 0 ? [
|
|
658
|
+
return added.length > 0 ? [report3(snapshot, added)] : [];
|
|
528
659
|
});
|
|
529
660
|
reactToFindings(found, reaction);
|
|
530
661
|
}
|
|
@@ -686,7 +817,7 @@ function restoreConsoleMethods(guard) {
|
|
|
686
817
|
}
|
|
687
818
|
var ABSORB_ADVICE = 'Absorb what the test expects: `installConsoleSpies()` from `vitest-auto-spy/console` in a `beforeEach`, then assert on `consoleErrorSpy` and its siblings \u2014 or `vi.spyOn(console, "error").mockImplementation(() => undefined)`. A `vi.spyOn` with no implementation calls through and still prints. Output the code should not make is a defect to fix, not to silence; `strayConsole: { allow: [...] }` is the last resort, for environment noise no spec can reach.';
|
|
688
819
|
var IMPORTED_SPIES_ADVICE = "`vitest-auto-spy/console` is loaded in this worker, but under `strayConsole` importing a spy installs nothing: under `isolate: false` the import runs once per worker, so it cannot tell which file it belongs to. Call `installConsoleSpies()` in a `beforeEach`, or at the top of the file for all of its tests.";
|
|
689
|
-
function
|
|
820
|
+
function quote2(calls, total, withTest) {
|
|
690
821
|
const lines = calls.map((call) => {
|
|
691
822
|
const during = withTest && call.test !== void 0 ? ` (during "${call.test}")` : "";
|
|
692
823
|
const text = call.text.split("\n").join("\n ");
|
|
@@ -704,7 +835,7 @@ ${IMPORTED_SPIES_ADVICE}` : ABSORB_ADVICE;
|
|
|
704
835
|
const subject = file === void 0 ? `"${test ?? ""}" ${count}` : `${file} ${count} ${OUTSIDE_TEST}`;
|
|
705
836
|
return withDocs(
|
|
706
837
|
`[vitest-auto-spy] ${subject} and nothing absorbed it:
|
|
707
|
-
${
|
|
838
|
+
${quote2(bucket.calls, bucket.total, file !== void 0)}
|
|
708
839
|
${advice}`,
|
|
709
840
|
DOCS_LINKS.setup
|
|
710
841
|
);
|
|
@@ -1114,14 +1245,14 @@ function reportDuplicateCopies(reaction) {
|
|
|
1114
1245
|
if (reaction === "off") {
|
|
1115
1246
|
return;
|
|
1116
1247
|
}
|
|
1117
|
-
const
|
|
1118
|
-
if (!
|
|
1248
|
+
const report4 = describeDuplicateCopies();
|
|
1249
|
+
if (!report4) {
|
|
1119
1250
|
return;
|
|
1120
1251
|
}
|
|
1121
1252
|
if (reaction === "throw") {
|
|
1122
|
-
throw new Error(
|
|
1253
|
+
throw new Error(report4);
|
|
1123
1254
|
}
|
|
1124
|
-
console.warn(
|
|
1255
|
+
console.warn(report4);
|
|
1125
1256
|
}
|
|
1126
1257
|
var LATE_ASSERTION_ADVICE = "An assertion that settles after its test has finished cannot fail it: the test it belongs to was reported green without ever running it. The usual causes are `.then(() => expect(...))` and an `async` helper called without `await` \u2014 return or await the promise so the assertion lands inside the test.";
|
|
1127
1258
|
var UNHANDLED_ERROR_ADVICE = "A rejection nothing handled is a code path the suite never asserted on: under zone.js it fails no test, so the run stays green while the error scrolls past in stderr. Await the promise, or assert on it with `await expect(promise).rejects.toThrow(...)`.";
|
|
@@ -1288,6 +1419,7 @@ function applyPreset(options) {
|
|
|
1288
1419
|
propsOutsideHooks: options.propsOutsideHooks ?? "throw",
|
|
1289
1420
|
guardGlobals: options.guardGlobals ?? "throw",
|
|
1290
1421
|
prototypePollution: options.prototypePollution ?? "throw",
|
|
1422
|
+
documentPollution: options.documentPollution ?? "throw",
|
|
1291
1423
|
strayConsole: options.strayConsole ?? "throw",
|
|
1292
1424
|
misconfiguration: options.misconfiguration ?? "throw",
|
|
1293
1425
|
swallowedStrictCalls: options.swallowedStrictCalls ?? "throw",
|
|
@@ -1344,6 +1476,7 @@ function setupAutoSpy(input = {}) {
|
|
|
1344
1476
|
armStrictMode(options);
|
|
1345
1477
|
armUnconfiguredReads(options);
|
|
1346
1478
|
armMisconfiguration(options.misconfiguration);
|
|
1479
|
+
guardDocumentPollution(options.documentPollution ?? "off");
|
|
1347
1480
|
if (options.globalFakeTimers) {
|
|
1348
1481
|
setupFakeTimers(options.globalFakeTimers === true ? void 0 : options.globalFakeTimers, { betweenTests: true });
|
|
1349
1482
|
}
|
|
@@ -1505,4 +1638,4 @@ function registerFocusMatchers() {
|
|
|
1505
1638
|
}
|
|
1506
1639
|
useVitestAdapter();
|
|
1507
1640
|
|
|
1508
|
-
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, describeStrayTimers, flushStrayRejections, getMockRegistrySize, getWatchedTimerGlobals, guardGlobalPatches, guardPrototypePollution, guardStrayConsole, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, restoreWebStorage, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime, withoutStrayTimerTracking };
|
|
1641
|
+
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, describeStrayTimers, flushStrayRejections, getMockRegistrySize, getWatchedTimerGlobals, guardDocumentPollution, guardGlobalPatches, guardPrototypePollution, guardStrayConsole, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, restoreWebStorage, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime, withoutStrayTimerTracking };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auto-mock",
|
|
@@ -137,98 +137,101 @@ it('loads', async () => {
|
|
|
137
137
|
|
|
138
138
|
## Reach for these before hand-rolling
|
|
139
139
|
|
|
140
|
-
| Situation
|
|
141
|
-
|
|
|
142
|
-
| a `signal()` / `computed()` field on the class under test
|
|
143
|
-
| a resource field, when the HTTP round trip is not the point
|
|
144
|
-
| the HTTP round trip _is_ the point
|
|
145
|
-
| a component or service that reads `ActivatedRoute`
|
|
146
|
-
| a guard or a class built with `new` that takes the route
|
|
147
|
-
| a node:test suite whose heap grows all run
|
|
148
|
-
| a Nest provider whose constructor keeps changing
|
|
149
|
-
| asserting a resource's value _and_ status together
|
|
150
|
-
| a callback or config object the code under test built
|
|
151
|
-
| an object the test already holds — a real service, a client, `TestBed.inject(X)`
|
|
152
|
-
| a `calledWith` that is not matching and you cannot see why
|
|
153
|
-
| a method that has to throw — for all calls, or for some arguments
|
|
154
|
-
| a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach`
|
|
155
|
-
| an `effect()` whose trigger is now a static signal
|
|
156
|
-
| the component constructs its own `IntersectionObserver`
|
|
157
|
-
| a green run exiting 1 with `AbortError` under happy-dom
|
|
158
|
-
| a real request going out of a unit run (`fetch`, XHR, `sendBeacon`)
|
|
159
|
-
| timers or frames from a previous file failing this one
|
|
160
|
-
| an assertion error in stderr, every test green and the run at 0
|
|
161
|
-
| a run getting slower the longer it goes, on `isolate: false`
|
|
162
|
-
| `JavaScript heap out of memory` on a suite of wide generated clients
|
|
163
|
-
| `Cannot read properties of undefined (reading 'now')`
|
|
164
|
-
| `localStorage.setItem is not a function`, or it is `undefined`
|
|
165
|
-
| a spy handed to an API typed against the real class
|
|
166
|
-
| the code under test does `new X()` (a global, a vendor SDK)
|
|
167
|
-
| `X is not a constructor`, with a stack in production code
|
|
168
|
-
| waiting for a dynamic `import()` under fake timers
|
|
169
|
-
| `addEventListener(…, { signal })` throwing about `EventTarget`
|
|
170
|
-
| `codemod` reporting a truncated repository scan
|
|
171
|
-
| a suite ported from Jest's `fakeTimers.enableGlobally`
|
|
172
|
-
| `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()`
|
|
173
|
-
| a nested `describe`'s `beforeAll` landing on real timers
|
|
174
|
-
| setup hooks applying to the first spec file of a worker only
|
|
175
|
-
| `fakeAsync` inside `test.concurrent`
|
|
176
|
-
| an assertion containing a date
|
|
177
|
-
| a spec asserting on tick _order_ under a frozen clock
|
|
178
|
-
| a dependency declared in the component's own `providers`
|
|
179
|
-
| a double answering `undefined` for a method nobody configured
|
|
180
|
-
| a strict throw caught by a `try`/`catch` or an operator, the test still green
|
|
181
|
-
| a strict double's getter answering `undefined`, or its `items$` never emitting
|
|
182
|
-
| an `afterEach` that exists only to reset one spy
|
|
183
|
-
| a dead NgModule import, dead `schemas`, an unflushed HTTP request
|
|
184
|
-
| the spy is provided but the component resolves its own, so the test runs the real service
|
|
185
|
-
| "which collaborators did this actually inject?"
|
|
186
|
-
| `NG0303` / `NG0301` / `NG0304` from an imported NgModule
|
|
187
|
-
| `Cannot read properties of undefined (reading 'provide')` in `di_setup`
|
|
188
|
-
| a focus assertion failing as `expected false to deeply equal true`
|
|
189
|
-
| a collaborator passed as an argument, then asserted on
|
|
190
|
-
| a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN`
|
|
191
|
-
| a `vi.mock()` that silently did nothing under a bundler
|
|
192
|
-
| `No "default" export is defined on the mock`
|
|
193
|
-
| waiting for a `resource()` / an SDK to become ready
|
|
194
|
-
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …`
|
|
195
|
-
| a stub that works only in the first test of the file
|
|
196
|
-
| a library failing every other run after a `defineProperty` on DOM
|
|
197
|
-
| a block of files failing to collect, with no stack and zero failing tests
|
|
198
|
-
| the same check in a suite that does not call `setupAutoSpy()`
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
| `
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
|
|
|
210
|
-
| `
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
| `
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
217
|
-
| `'
|
|
218
|
-
|
|
|
219
|
-
|
|
|
220
|
-
|
|
|
221
|
-
|
|
|
222
|
-
| `
|
|
223
|
-
|
|
|
224
|
-
|
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
| `
|
|
231
|
-
|
|
|
140
|
+
| Situation | Helper |
|
|
141
|
+
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — writes through a writable one; an `input()` is refused |
|
|
143
|
+
| a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading` |
|
|
144
|
+
| the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
|
|
145
|
+
| a component or service that reads `ActivatedRoute` | `provideActivatedRoute({ params })`, then `injectActivatedRoute().setParams(…)` — `/angular-router` |
|
|
146
|
+
| a guard or a class built with `new` that takes the route | `createActivatedRoute({ params })` — the same handle, no `TestBed` |
|
|
147
|
+
| a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker |
|
|
148
|
+
| a Nest provider whose constructor keeps changing | `createNestUnit(Target, { expose })` — built from its DI metadata |
|
|
149
|
+
| asserting a resource's value _and_ status together | `registerResourceMatchers()` → `toHaveResourceValue` / `toBeLoading` |
|
|
150
|
+
| a callback or config object the code under test built | `captureArg<T>()` in the assertion, then read `.value` |
|
|
151
|
+
| an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it |
|
|
152
|
+
| a `calledWith` that is not matching and you cannot see why | `explainSpy(spy, 'method')` from `/diagnostics` — configs, calls, which hit which |
|
|
153
|
+
| a method that has to throw — for all calls, or for some arguments | `spy.m.failWith(err)` / `spy.m.calledWith(x).failWith(err)` — **not** `throwWith` |
|
|
154
|
+
| a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach` | `extendWithAutoSpies(test, { cart: CartService })` (`/angular`) |
|
|
155
|
+
| an `effect()` whose trigger is now a static signal | `runEffect(effectRef)` |
|
|
156
|
+
| the component constructs its own `IntersectionObserver` | `stubIntersectionObserver()` (+ `Resize` / `Mutation`) |
|
|
157
|
+
| a green run exiting 1 with `AbortError` under happy-dom | `setupAutoSpy({ blockNetwork: true })` |
|
|
158
|
+
| a real request going out of a unit run (`fetch`, XHR, `sendBeacon`) | `setupAutoSpy({ blockNetwork: true })` — `{ xhr: 'empty' }` for pings |
|
|
159
|
+
| timers or frames from a previous file failing this one | `setupAutoSpy({ strayTimers: true })` — mutes `--detect-async-leaks`, see below |
|
|
160
|
+
| an assertion error in stderr, every test green and the run at 0 | `setupAutoSpy({ strayRejections: true })` — zone.js swallowed it |
|
|
161
|
+
| a run getting slower the longer it goes, on `isolate: false` | `setupAutoSpy({ pruneMockRegistry: true })` — the mock registry |
|
|
162
|
+
| `JavaScript heap out of memory` on a suite of wide generated clients | `createSpyFromClass(X, { lazySpies: 'proxy' })` — one trap object, not a placeholder per method |
|
|
163
|
+
| `Cannot read properties of undefined (reading 'now')` | `restoreTimerGlobals` — on by default |
|
|
164
|
+
| `localStorage.setItem is not a function`, or it is `undefined` | `restoreWebStorage` — on by default; Vitest's global filter, Node 25+ |
|
|
165
|
+
| a spy handed to an API typed against the real class | `asInstance()` / `asSpy()` |
|
|
166
|
+
| the code under test does `new X()` (a global, a vendor SDK) | `mockConstructor(factory)` / `stubConstructor(obj, key, factory)` |
|
|
167
|
+
| `X is not a constructor`, with a stack in production code | same — a `vi.fn(() => …)` cannot serve `new` |
|
|
168
|
+
| waiting for a dynamic `import()` under fake timers | `settleDynamicImport(() => import('…'))` / `flushEventLoop()` |
|
|
169
|
+
| `addEventListener(…, { signal })` throwing about `EventTarget` | `stubAbortController()` |
|
|
170
|
+
| `codemod` reporting a truncated repository scan | `VITEST_AUTO_SPY_SCAN_CAP=<n>` — raises the 50 000-file cap |
|
|
171
|
+
| a suite ported from Jest's `fakeTimers.enableGlobally` | `setupAutoSpy({ globalFakeTimers: true })` |
|
|
172
|
+
| `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()` | `setSpyEngine('runner')` / `getSpyEngine()` — `/setup`, Vitest only |
|
|
173
|
+
| a nested `describe`'s `beforeAll` landing on real timers | `setupFakeTimers(cfg, { betweenTests: true })` |
|
|
174
|
+
| setup hooks applying to the first spec file of a worker only | the setup module is cached — run coverage with `--isolate` |
|
|
175
|
+
| `fakeAsync` inside `test.concurrent` | `installProxyZonePatch({ scope: 'callback' })` |
|
|
176
|
+
| an assertion containing a date | `mockSystemTime(iso)` — never `vi.spyOn(globalThis, 'Date')` |
|
|
177
|
+
| a spec asserting on tick _order_ under a frozen clock | `useCountingClock()` |
|
|
178
|
+
| a dependency declared in the component's own `providers` | `overrideComponentProvider(Cmp, Token)` — it verifies on the first fixture that the override applied |
|
|
179
|
+
| a double answering `undefined` for a method nobody configured | `{ strict: true }`, or `setupAutoSpy({ strict: true })` suite-wide |
|
|
180
|
+
| a strict throw caught by a `try`/`catch` or an operator, the test still green | `setupAutoSpy({ swallowedStrictCalls: 'throw' })`; provoked on purpose — `takeStrictViolations()` from `/setup` |
|
|
181
|
+
| a strict double's getter answering `undefined`, or its `items$` never emitting | `setupAutoSpy({ unconfiguredReads: 'throw' })` reports it after the test; survey first with `onUnstubbedRead` |
|
|
182
|
+
| an `afterEach` that exists only to reset one spy | `using spy = createSpyFromClass(X)` — `[Symbol.dispose]` resets it |
|
|
183
|
+
| a dead NgModule import, dead `schemas`, an unflushed HTTP request | `enableAngularDiagnostics()` in the setup file, after `initTestEnvironment` |
|
|
184
|
+
| the spy is provided but the component resolves its own, so the test runs the real service | `enableAngularDiagnostics({ shadowedProviders })`, or `assertNoShadowedProviders(Component, fixture)` |
|
|
185
|
+
| "which collaborators did this actually inject?" | `trackInjections([A, TOKEN])` — providers plus the record, not `vi.mock` |
|
|
186
|
+
| `NG0303` / `NG0301` / `NG0304` from an imported NgModule | `assertNgModuleScopes(Module)` — an AOT bundle stripped its scope |
|
|
187
|
+
| `Cannot read properties of undefined (reading 'provide')` in `di_setup` | `assertComponentDefIntact(Cmp)` — a barrel chunk left a hole in `ɵcmp` |
|
|
188
|
+
| a focus assertion failing as `expected false to deeply equal true` | `registerFocusMatchers()` + `expect(el).toHaveFocus()` |
|
|
189
|
+
| a collaborator passed as an argument, then asserted on | `autoMocked<T>()` — typed `T & Spy<T>` |
|
|
190
|
+
| a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN` | `stubMediaElement({ duration })`, then `media.set(el, …)` |
|
|
191
|
+
| a `vi.mock()` that silently did nothing under a bundler | `assertMocked(ns, { specifier, exports })` |
|
|
192
|
+
| `No "default" export is defined on the mock` | `vi.mock('x', () => moduleNamespace({ … }))` |
|
|
193
|
+
| waiting for a `resource()` / an SDK to become ready | `flushEventLoopUntil(() => …, { label })` — budgeted, not tuned |
|
|
194
|
+
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …` | `expect(diffByField(actual, expected)).toBeUndefined()` |
|
|
195
|
+
| a stub that works only in the first test of the file | `installPerTest(() => stub…())` — or install it in `beforeEach` |
|
|
196
|
+
| a library failing every other run after a `defineProperty` on DOM | `setupAutoSpy({ guardGlobals: 'throw' })` names the file |
|
|
197
|
+
| a block of files failing to collect, with no stack and zero failing tests | `setupAutoSpy()` names the file that left a key on `Object.prototype` (`prototypePollution`, on by default) |
|
|
198
|
+
| the same check in a suite that does not call `setupAutoSpy()` | `guardPrototypePollution('throw')` from `/setup` |
|
|
199
|
+
| a spec failing only in a full run on an attribute of `<body>` / `<html>` another file left | `setupAutoSpy({ documentPollution: 'throw' })` names the test and puts the document back (in `preset: 'strict'`) |
|
|
200
|
+
| the same document check without `setupAutoSpy()` | `guardDocumentPollution('throw')` from `/setup` |
|
|
201
|
+
| `A metric with the name … has already been registered` / `customElements.define` twice, at import | a module scope evaluated once per spec file under the Angular builder — guard the registration (`getSingleMetric`, `customElements.get`) |
|
|
202
|
+
| console output a test never asserted on, or a warning from this library | `setupAutoSpy({ strayConsole: 'throw' })` fails that test; absorb with `installConsoleSpies()` in `beforeEach` |
|
|
203
|
+
| every guard at its strictest grade in one line | `setupAutoSpy({ preset: 'strict' })` — plus `enableAngularDiagnostics()` for Angular |
|
|
204
|
+
| an `onlyMethodsToSpyOn` typo or `injectSpy` on a real instance that only warned | `setupAutoSpy({ misconfiguration: 'throw' })` throws at the call |
|
|
205
|
+
| which file and call scheduled a timer that outlived its file | `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`, or `describeStrayTimers()` from `/setup` |
|
|
206
|
+
| stray timers charged to a file that only writes `localStorage` (jsdom) | `withoutStrayTimerTracking(() => seed())` from `/setup` — its timers are neither counted nor cancelled |
|
|
207
|
+
| `import { consoleErrorSpy }` silencing other files under `isolate: false` | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
|
|
208
|
+
| `Cannot set base providers because it has already been called` | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` |
|
|
209
|
+
| a dependency behind an `InjectionToken`, with no class to spy | `provideAutoSpyForToken(TOKEN)` + `injectSpy(TOKEN)`; a chained call → `{ selfReturning: ['channel'] }` as the third argument |
|
|
210
|
+
| `Expected to be running in 'ProxyZone', but it was not found` | `import 'vitest-auto-spy/zone'` (needs `globals: true`) |
|
|
211
|
+
| `Property 'mockReturnValue' does not exist on type 'never'` | upgrade — the spy no longer collapses on an unreadable return type |
|
|
212
|
+
| `TS2345` inside `mockReturnValue` / `mockImplementation` / `mockResolvedValue` | the stub is checked against the method's return type now — fix the stub, not the spy |
|
|
213
|
+
| `TS2540: Cannot assign to 'x'` on a double whose runtime write works | `Spy<T>` keeps `readonly` on purpose — `mockValueProp(spy, 'x', v)`, which a spied **accessor** also needs |
|
|
214
|
+
| a signal-valued getter that `gettersToSpyOn` will not accept | it accepts any key now; for a signal prefer `mockSignalProp` |
|
|
215
|
+
| five `asInstance(…)` in one call, found one per `tsc` run | `...asInstances(a, b, c, d, e)` |
|
|
216
|
+
| `nextWith` demanding `HttpEvent<T>` on a generated client | `asSpy<Client, { overload: 'first' }>(…)` / `Overload<M, 0>` |
|
|
217
|
+
| `not assignable to parameter of type 'HttpEvent<…>'` in a stub of the real body | same thing — the helpers read the **last** overload; `{ overload: { m: 'first' } }`, not `@ts-expect-error` |
|
|
218
|
+
| a fixture that needs a nested object built by its own call | `createMock<T>({ a: { b: 1 } })` — deep partial, still exact |
|
|
219
|
+
| the same 100-line model literal copied into eight specs (`TS1117`) | one `createFixtureFactory<T>(defaults)`; specs call it with what they change |
|
|
220
|
+
| `'params' in link` ladders, or a cast, to pick a union branch | `narrow.byKey(link, 'params')` / `narrow.observable(x)` |
|
|
221
|
+
| `{ ...modelInstance, flag: true }` losing every getter | `withOverrides(modelInstance, { flag: true })` |
|
|
222
|
+
| `NG0303` / `NG0304` / silence from a directive spec | `createDirectiveHost({ template, scope: [Module] })` |
|
|
223
|
+
| a hand-written `class MockChartComponent` restating a child's selector | `createComponentStub(ChartComponent)` (`/angular`) — selector, inputs, outputs read from `ɵcmp` |
|
|
224
|
+
| a `TestingStorage` class for `localStorage` / `sessionStorage` | `stubWebStorage('localStorage', { items })` from `/dom-stubs` — `snapshot()` to assert |
|
|
225
|
+
| `'x' does not exist in type 'MethodReturns<{ y: any; }>'` on a generic class | spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` infers it |
|
|
226
|
+
| "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` |
|
|
227
|
+
| an input that has to change after the first render | `await setInputs(fixture, { … })` — one `setInput` per name, one wait |
|
|
228
|
+
| a component that navigates, or reads `router.url` | `provideRouterDouble({ url })` + `injectRouterDouble()` — `/angular-router` |
|
|
229
|
+
| a component that reads `router.currentNavigation()` for `extras.state` or `trigger` | `setCurrentNavigation({ extras: { state } })` on the same handle — no `instanceMethodsToSpyOn` |
|
|
230
|
+
| a `window` or `document` behind a DI token | `provideWindowDouble(WINDOW, { screen })` / `provideDocumentDouble({ … })` |
|
|
231
|
+
| `MAT_DIALOG_DATA` and `MatDialogRef` provided by hand | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(MatDialogRef)` + `injectMatDialogRef(Ref)` |
|
|
232
|
+
| asserting a `computed()` did **not** recompute | `trackRecomputations(sig)` / `trackEffectRuns(ref)` — `{ count, stop() }` |
|
|
233
|
+
| `form()` in a spec, or `errors()` read by hand | `createForm(model, schema)` + `registerFormMatchers()` — then `toHaveFieldErrors([…])` (`/signal-forms`) |
|
|
234
|
+
| any of those doubles without a `TestBed` | `createRouterDouble` / `createWindowDouble` / `createDocumentDouble` / `createMatDialogRef` |
|
|
232
235
|
|
|
233
236
|
## Rules that prevent most of the mistakes
|
|
234
237
|
|