vitest-auto-spy 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -0
- package/README.md +17 -4
- package/dist/cli.js +3 -1
- package/dist/eslint-plugin.cjs +70 -0
- package/dist/setup.d.ts +69 -11
- package/dist/setup.js +87 -9
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +66 -65
package/AGENTS.md
CHANGED
|
@@ -1183,6 +1183,17 @@ the realm, not owned by `globalThis`), and with `isolate: false` the next file d
|
|
|
1183
1183
|
own `useFakeTimers` with `Cannot read properties of undefined (reading 'now')`. If you see that,
|
|
1184
1184
|
the file in the stack is not the cause.
|
|
1185
1185
|
|
|
1186
|
+
`restoreWebStorage` is the other repair that is on by default. Vitest copies a DOM environment's
|
|
1187
|
+
globals onto `globalThis` behind `if (k in global) return KEYS.includes(k)`, and neither
|
|
1188
|
+
`localStorage` nor `sessionStorage` is in `KEYS` — both used to arrive only because Node put neither
|
|
1189
|
+
on `globalThis`, so the first half of that condition was false. Node's own Web Storage made the key
|
|
1190
|
+
exist, and the environment's storage stopped arriving: `setItem is not a function` on Node 25,
|
|
1191
|
+
`undefined` on Node 26, under jsdom and happy-dom alike, because the filter runs before either. The
|
|
1192
|
+
suite stays green until a spec touches storage, so this lands as "CI moved to a new Node and eleven
|
|
1193
|
+
unrelated specs died". The repair writes a key, reads it back and removes it — a storage that
|
|
1194
|
+
survives that is left alone, whoever implemented it — and it installs nothing in a `node`
|
|
1195
|
+
environment, which is supposed to have no Web Storage at all.
|
|
1196
|
+
|
|
1186
1197
|
`hookTimeoutHint` is on by default too, and it is the one that pays off on the day a suite lands in
|
|
1187
1198
|
CI. Jest resolves **one** budget — `hook.timeout || getState().testTimeout` for a hook,
|
|
1188
1199
|
`test.timeout || getState().testTimeout` for a body — while Vitest resolves `hookTimeout` on its
|
|
@@ -2317,6 +2328,7 @@ export default [
|
|
|
2317
2328
|
| `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
2318
2329
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
2319
2330
|
| `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 |
|
|
2331
|
+
| `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
2320
2332
|
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
|
|
2321
2333
|
| `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
|
|
2322
2334
|
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
@@ -2484,6 +2496,7 @@ packages, which a subpath export can never be.
|
|
|
2484
2496
|
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
2485
2497
|
| `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
|
|
2486
2498
|
| `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
|
|
2499
|
+
| `localStorage.setItem is not a function`, or `localStorage` undefined | Node's own Web Storage made Vitest's global filter skip the environment's — Node 25 and up, jsdom and happy-dom alike | on by default in `setupAutoSpy()`; `restoreWebStorage()` on its own (§10) |
|
|
2487
2500
|
| `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
|
|
2488
2501
|
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
2489
2502
|
| an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
|
|
|
76
76
|
- 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
|
|
77
77
|
- 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
|
|
78
78
|
- 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
|
|
79
|
-
- 📏 Lint rules and one-line test-run hygiene —
|
|
79
|
+
- 📏 Lint rules and one-line test-run hygiene — twenty rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, eight suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
|
|
80
80
|
- 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
|
|
81
81
|
- 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
|
|
82
82
|
- ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
|
|
@@ -2492,6 +2492,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
2492
2492
|
| `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
|
|
2493
2493
|
| `setupAngularTestEnv(opts)` | `/angular` | Zone and zoneless spec files in one worker, switching platforms per file |
|
|
2494
2494
|
| `restoreTimerGlobals()` | `/setup` | Put back timer globals that uninstalling the fakes deleted rather than restored |
|
|
2495
|
+
| `restoreWebStorage(options?)` | `/setup` | Give `globalThis` a `localStorage` / `sessionStorage` that work when the runner's copy never arrived ([details](#test-run-hygiene)) |
|
|
2495
2496
|
| `trackMockRegistry()` / `keepMockRegistered(mock)` / `restoreLongLivedImplementations()` | `/setup` | Keep @vitest/spy's mock registry to the mocks that outlive a file; mark one the split would miss; put back an implementation a cross-file `vi.resetAllMocks()` dropped ([details](#test-run-hygiene)) |
|
|
2496
2497
|
| `trackNodeMocks()` / `pruneNodeMocks()` / `countNodeMocks()` | `/node` | Give this library its own `node:test` `MockTracker` so a dropped spy is freed — 21× less retained heap; sweep by hand, and read the count back |
|
|
2497
2498
|
| `setSpyEngine(engine)` / `getSpyEngine()` | `/setup` | Build method spies from this library's own mock (`'auto-spy'`, the default) or from `vi.fn()` (`'runner'`) ([details](#the-spy-engine)) |
|
|
@@ -2733,6 +2734,16 @@ expensive to diagnose when it is missing. The first three are on by default:
|
|
|
2733
2734
|
which is a fact rather than a guess, and names the `setImmediate` case that reaches this with no
|
|
2734
2735
|
timer in sight: a request matching no Express route is ended by `finalhandler` on `setImmediate`,
|
|
2735
2736
|
so the 404 is never written and a routing mistake is reported as a slow test.
|
|
2737
|
+
11. **Web Storage the runner never handed over.** On by default, because it only ever repairs.
|
|
2738
|
+
Vitest copies a DOM environment's globals onto `globalThis` behind `if (k in global) return
|
|
2739
|
+
KEYS.includes(k)`, and neither `localStorage` nor `sessionStorage` is in `KEYS` — they used to
|
|
2740
|
+
arrive only because Node put neither on `globalThis`, so the first half was false. Node's own
|
|
2741
|
+
Web Storage made the key exist, and now the environment's storage never arrives: `setItem is
|
|
2742
|
+
not a function` on Node 25, `undefined` on Node 26, under jsdom and happy-dom alike, since the
|
|
2743
|
+
filter runs before either. A suite stays green with this broken — only the specs that touch
|
|
2744
|
+
storage fail — so it arrives as "CI moved to a new Node and eleven unrelated specs died". The
|
|
2745
|
+
repair decides by using the storage rather than looking at it, replaces only one that cannot
|
|
2746
|
+
keep a value, and installs nothing at all in a `node` environment.
|
|
2736
2747
|
|
|
2737
2748
|
| Option | Default | Notes |
|
|
2738
2749
|
| --------------------- | --------- | ------------------------------------------------------------------------------------- |
|
|
@@ -2746,6 +2757,7 @@ expensive to diagnose when it is missing. The first three are on by default:
|
|
|
2746
2757
|
| `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
|
|
2747
2758
|
| `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
|
|
2748
2759
|
| `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
|
|
2760
|
+
| `restoreWebStorage` | `true` | Give the run a `localStorage` / `sessionStorage` that work |
|
|
2749
2761
|
| `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
|
|
2750
2762
|
| `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
|
|
2751
2763
|
| `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
|
|
@@ -2949,6 +2961,7 @@ export can never be.
|
|
|
2949
2961
|
| `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
2950
2962
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
2951
2963
|
| `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 |
|
|
2964
|
+
| `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
2952
2965
|
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
|
|
2953
2966
|
| `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
|
|
2954
2967
|
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
|
|
@@ -2958,7 +2971,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
|
|
|
2958
2971
|
"don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
|
|
2959
2972
|
are versioned together and stop being re-written in every project that installs the package.
|
|
2960
2973
|
|
|
2961
|
-
**Three of the
|
|
2974
|
+
**Three of the twenty fix on their own, eight offer suggestions**, and the split is not about how hard
|
|
2962
2975
|
the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
|
|
2963
2976
|
compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
|
|
2964
2977
|
`import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
|
|
@@ -3062,7 +3075,7 @@ package's own — it needs its ESLint integration switched on.
|
|
|
3062
3075
|
### WebStorm and the other JetBrains IDEs
|
|
3063
3076
|
|
|
3064
3077
|
No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
|
|
3065
|
-
all run ESLint natively, so the
|
|
3078
|
+
all run ESLint natively, so the twenty rules appear inline, in the **Problems** tool window, and
|
|
3066
3079
|
under **Code → Inspect Code** for the whole project.
|
|
3067
3080
|
|
|
3068
3081
|
```js
|
|
@@ -3079,7 +3092,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
|
|
|
3079
3092
|
the fixes and suggestions live.
|
|
3080
3093
|
|
|
3081
3094
|
A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
|
|
3082
|
-
and then keep a second copy of
|
|
3095
|
+
and then keep a second copy of twenty rules, in Kotlin, in step with the TypeScript ones.
|
|
3083
3096
|
|
|
3084
3097
|
### VS Code, Cursor, Windsurf, VSCodium
|
|
3085
3098
|
|
package/dist/cli.js
CHANGED
|
@@ -2356,7 +2356,7 @@ function checkForeignPragma(graph) {
|
|
|
2356
2356
|
}
|
|
2357
2357
|
|
|
2358
2358
|
// src/cli/checks/export-map.generated.ts
|
|
2359
|
-
var EXPORT_MAP_VERSION = "4.
|
|
2359
|
+
var EXPORT_MAP_VERSION = "4.4.0";
|
|
2360
2360
|
var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node 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/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";
|
|
2361
2361
|
var EXPORTED_BY = {
|
|
2362
2362
|
AccessorImplementations: "0 1 2 3 11 14 15 16",
|
|
@@ -2604,6 +2604,8 @@ var EXPORTED_BY = {
|
|
|
2604
2604
|
RestoreProp: "0 1 2 3 11 14 15 16",
|
|
2605
2605
|
restoreSpiedInstance: "0 1 2 3 14 15 16",
|
|
2606
2606
|
restoreTimerGlobals: "17",
|
|
2607
|
+
restoreWebStorage: "17",
|
|
2608
|
+
RestoreWebStorageOptions: "17",
|
|
2607
2609
|
RuleSeverity: "19",
|
|
2608
2610
|
runEffect: "2 11",
|
|
2609
2611
|
SchedulerHost: "17",
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -316,6 +316,48 @@ function defineRule(options) {
|
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// src/lib/eslint/dom-reads.ts
|
|
320
|
+
var TEMPLATE_READS = [
|
|
321
|
+
"nativeElement",
|
|
322
|
+
"debugElement",
|
|
323
|
+
"elementRef",
|
|
324
|
+
"querySelector",
|
|
325
|
+
"getComputedStyle",
|
|
326
|
+
"triggerEventHandler",
|
|
327
|
+
"innerHTML",
|
|
328
|
+
"innerText",
|
|
329
|
+
"textContent",
|
|
330
|
+
"getAttribute",
|
|
331
|
+
"classList",
|
|
332
|
+
"shadowRoot",
|
|
333
|
+
"By.css",
|
|
334
|
+
"By.directive"
|
|
335
|
+
];
|
|
336
|
+
function templatePolicy(context) {
|
|
337
|
+
return Reflect.get(Object(context.options[0]), "templates") === "never" ? "never" : "as-needed";
|
|
338
|
+
}
|
|
339
|
+
function readsRenderedTemplate(source) {
|
|
340
|
+
return TEMPLATE_READS.some((member) => source.includes(member));
|
|
341
|
+
}
|
|
342
|
+
function renderShallowSuggestion(context, node) {
|
|
343
|
+
const [component, ...extra] = node.arguments;
|
|
344
|
+
const state = bindingState(context.sourceCode.getScope(node), "renderShallow");
|
|
345
|
+
if (!component || extra.length > 0 || state === "taken") {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
const replacement = `renderShallow(${context.sourceCode.getText(component)}).fixture`;
|
|
349
|
+
return {
|
|
350
|
+
desc: `Render without the children and the template: ${replacement}`,
|
|
351
|
+
fix: (fixer) => {
|
|
352
|
+
const edits = [fixer.replaceText(node, replacement)];
|
|
353
|
+
if (state === "free") {
|
|
354
|
+
edits.push(insertImport(fixer, `import { renderShallow } from '${PACKAGE}/angular';`));
|
|
355
|
+
}
|
|
356
|
+
return edits;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
319
361
|
// src/lib/eslint/floating-assertion.ts
|
|
320
362
|
var PROMISE_CALLBACK_METHODS = /* @__PURE__ */ new Set(["catch", "finally", "then"]);
|
|
321
363
|
function isPromiseCallback(fn) {
|
|
@@ -1609,6 +1651,32 @@ var noUnregisteredInjectSpy = defineRule({
|
|
|
1609
1651
|
};
|
|
1610
1652
|
}
|
|
1611
1653
|
});
|
|
1654
|
+
var preferRenderShallow = defineRule({
|
|
1655
|
+
anchor: "-a-components-children",
|
|
1656
|
+
description: "Render through renderShallow() when the spec never reads the rendered template",
|
|
1657
|
+
hasSuggestions: true,
|
|
1658
|
+
schema: [{ type: "object", properties: { templates: { enum: ["as-needed", "never"] } }, additionalProperties: false }],
|
|
1659
|
+
messages: {
|
|
1660
|
+
keepTemplate: '`keepTemplate: true` puts the real template back, and this project set `{ templates: "never" }`. Drop it \u2014 or, when the component genuinely reads its own template through `viewChild` or content projection, silence this line, because the alternative is a spec that cannot reach the component at all.',
|
|
1661
|
+
preferRenderShallow: "`TestBed.createComponent` pays for compiling the template and instantiating the whole child subtree, and nothing in this file reads either \u2014 no `nativeElement`, no `debugElement`, no `By.css`, no `querySelector`. `renderShallow(X)` brings the same component up through the same `TestBed` with the children dropped and the template blank, and leaves inputs, signals, lifecycle hooks and DI exactly where they were; `fixture` is still a real `ComponentFixture`. What it buys is measured in `bench-angular/`: **0.24\xD7** the per-test cycle at 100 children and **0.05\xD7** at 400 \u2014 but at **zero** children the two are level (measurements straddle 1.0, and it is the noisiest row in the benchmark), so a leaf component gains nothing and this report is worth ignoring there. Reach for `{ keepTemplate: true }` when the component reads its own template through `viewChild` or content projection, which this rule cannot see from the spec."
|
|
1662
|
+
},
|
|
1663
|
+
create: (context) => ({
|
|
1664
|
+
'CallExpression[callee.object.name="TestBed"][callee.property.name="createComponent"]': (node) => {
|
|
1665
|
+
if (templatePolicy(context) === "as-needed" && readsRenderedTemplate(context.sourceCode.getText())) {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const suggestion = renderShallowSuggestion(context, node);
|
|
1669
|
+
context.report(
|
|
1670
|
+
suggestion ? { node, messageId: "preferRenderShallow", suggest: [suggestion] } : { node, messageId: "preferRenderShallow" }
|
|
1671
|
+
);
|
|
1672
|
+
},
|
|
1673
|
+
'Property[key.name="keepTemplate"][value.value=true]': (node) => {
|
|
1674
|
+
if (templatePolicy(context) === "never") {
|
|
1675
|
+
context.report({ node, messageId: "keepTemplate" });
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
})
|
|
1679
|
+
});
|
|
1612
1680
|
var rules = {
|
|
1613
1681
|
"prefer-provide-auto-spy": preferProvideAutoSpy,
|
|
1614
1682
|
"prefer-create-spy-from-class": preferCreateSpyFromClass,
|
|
@@ -1625,6 +1693,7 @@ var rules = {
|
|
|
1625
1693
|
"no-inject-before-override": noInjectBeforeOverride,
|
|
1626
1694
|
"no-import-time-spread": noImportTimeSpread,
|
|
1627
1695
|
"no-unregistered-inject-spy": noUnregisteredInjectSpy,
|
|
1696
|
+
"prefer-render-shallow": preferRenderShallow,
|
|
1628
1697
|
...jasmineRules
|
|
1629
1698
|
};
|
|
1630
1699
|
|
|
@@ -1646,6 +1715,7 @@ var recommendedRules = {
|
|
|
1646
1715
|
[`${PLUGIN_NAME}/no-inject-before-override`]: "error",
|
|
1647
1716
|
[`${PLUGIN_NAME}/no-import-time-spread`]: "error",
|
|
1648
1717
|
[`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "error",
|
|
1718
|
+
[`${PLUGIN_NAME}/prefer-render-shallow`]: "error",
|
|
1649
1719
|
[`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
|
|
1650
1720
|
[`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
|
|
1651
1721
|
[`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
|
package/dist/setup.d.ts
CHANGED
|
@@ -356,6 +356,18 @@ interface SetupAutoSpyOptions {
|
|
|
356
356
|
* installed on purpose. See {@link restoreTimerGlobals}.
|
|
357
357
|
*/
|
|
358
358
|
restoreTimerGlobals?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Give the run a `localStorage` and a `sessionStorage` that work. Default `true`, for the same
|
|
361
|
+
* reason as {@link restoreTimerGlobals}: a storage that survives a write and a read back is left
|
|
362
|
+
* untouched, and a `node` environment — which is supposed to have neither — gets nothing.
|
|
363
|
+
*
|
|
364
|
+
* Vitest's `populateGlobal` copies a DOM environment's globals behind `if (k in global) return
|
|
365
|
+
* KEYS.includes(k)`, and neither storage is in `KEYS`. Node's own Web Storage put the key on
|
|
366
|
+
* `globalThis`, so the filter now answers "no" and the environment's storage never arrives:
|
|
367
|
+
* `setItem is not a function` on Node 25, `localStorage` undefined on Node 26, under jsdom and
|
|
368
|
+
* happy-dom alike. See {@link restoreWebStorage}.
|
|
369
|
+
*/
|
|
370
|
+
restoreWebStorage?: boolean;
|
|
359
371
|
/**
|
|
360
372
|
* Keep `@vitest/spy`'s registry of every mock ever created down to the mocks that outlive a file.
|
|
361
373
|
* Default `false`, because it reaches into a set the runner does not expose.
|
|
@@ -458,16 +470,6 @@ interface SetupAutoSpyOptions {
|
|
|
458
470
|
*/
|
|
459
471
|
onUnstubbedCall?: UnstubbedCallHandler;
|
|
460
472
|
}
|
|
461
|
-
/**
|
|
462
|
-
* Install the library's test-run hygiene.
|
|
463
|
-
*
|
|
464
|
-
* ```ts
|
|
465
|
-
* // vitest.setup.ts
|
|
466
|
-
* import { setupAutoSpy } from 'vitest-auto-spy/setup';
|
|
467
|
-
*
|
|
468
|
-
* setupAutoSpy();
|
|
469
|
-
* ```
|
|
470
|
-
*/
|
|
471
473
|
declare function setupAutoSpy(options?: SetupAutoSpyOptions): void;
|
|
472
474
|
|
|
473
475
|
/**
|
|
@@ -817,4 +819,60 @@ declare function restoreTimerGlobals(): void;
|
|
|
817
819
|
/** The names this module watches — exported for the diagnostics that report what went missing. */
|
|
818
820
|
declare function getWatchedTimerGlobals(): string[];
|
|
819
821
|
|
|
820
|
-
|
|
822
|
+
/**
|
|
823
|
+
* Putting `localStorage` / `sessionStorage` back after the runner drops them.
|
|
824
|
+
*
|
|
825
|
+
* Vitest copies a DOM environment's globals onto `globalThis` behind one filter — `if (k in global)
|
|
826
|
+
* return KEYS.includes(k)` — and neither storage is in `KEYS`. While Node put neither on
|
|
827
|
+
* `globalThis` the first half was false and both were copied; Node's own Web Storage made the key
|
|
828
|
+
* exist, so the filter now answers "no" and the environment's storage never arrives. Measured with
|
|
829
|
+
* one spec, under jsdom and happy-dom alike, because the filter runs before either:
|
|
830
|
+
*
|
|
831
|
+
* ```text
|
|
832
|
+
* Node 24.19 localStorage works
|
|
833
|
+
* Node 25.9 localStorage.setItem is not a function
|
|
834
|
+
* Node 26.7 localStorage is undefined
|
|
835
|
+
* ```
|
|
836
|
+
*
|
|
837
|
+
* A suite stays green with this broken — only the specs that touch storage fail — so it usually
|
|
838
|
+
* arrives as "CI moved to a new Node and eleven unrelated specs died".
|
|
839
|
+
*/
|
|
840
|
+
/** The two globals this module repairs. */
|
|
841
|
+
declare const STORAGE_KEYS: readonly ["localStorage", "sessionStorage"];
|
|
842
|
+
/** One of the two. */
|
|
843
|
+
type StorageKey = (typeof STORAGE_KEYS)[number];
|
|
844
|
+
/** How {@link restoreWebStorage} finds the window whose storage it trusts. */
|
|
845
|
+
interface RestoreWebStorageOptions {
|
|
846
|
+
/**
|
|
847
|
+
* The window to take a working storage from, and to repair alongside `globalThis`.
|
|
848
|
+
*
|
|
849
|
+
* Defaults to `document.defaultView`, and to nothing where there is no document. Pass `null` to
|
|
850
|
+
* say there is no window: the repair then does nothing, which is what a DOM-less runtime wants.
|
|
851
|
+
*/
|
|
852
|
+
view?: object | null;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Give `globalThis` a `localStorage` and a `sessionStorage` that work.
|
|
856
|
+
*
|
|
857
|
+
* Safe to call at any point and as often as you like, and a no-op in the ordinary case: a storage
|
|
858
|
+
* that survives a write and a read back is left exactly as it is, so a spec's own stub survives
|
|
859
|
+
* this. It is also a no-op where there is no window — a `node` environment is *supposed* to have no
|
|
860
|
+
* Web Storage, and inventing one would hand the code under test an API the real runtime lacks.
|
|
861
|
+
*
|
|
862
|
+
* The window's own storage is preferred over the stand-in: the environment implements the real
|
|
863
|
+
* thing, and under a runner whose window is a separate object (a Bun preload, say) only the copy to
|
|
864
|
+
* `globalThis` failed. The `Map`-backed one is for when nothing else works — which, under Vitest,
|
|
865
|
+
* is every time, because there `document.defaultView` *is* `globalThis`.
|
|
866
|
+
*
|
|
867
|
+
* ```ts
|
|
868
|
+
* // vitest.setup.ts — or leave it to setupAutoSpy(), which calls this by default
|
|
869
|
+
* import { restoreWebStorage } from 'vitest-auto-spy/setup';
|
|
870
|
+
*
|
|
871
|
+
* restoreWebStorage();
|
|
872
|
+
* ```
|
|
873
|
+
*
|
|
874
|
+
* @returns The keys it had to repair — `[]` when the environment was already sound.
|
|
875
|
+
*/
|
|
876
|
+
declare function restoreWebStorage(options?: RestoreWebStorageOptions): StorageKey[];
|
|
877
|
+
|
|
878
|
+
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, type BlockNetworkOptions, type CountingClock, type CountingClockOptions, type DuplicateCopiesReaction, type FakeTimersConfig, type GlobalPatchReaction, type PerTestHandle, type RejectionHost, type RestoreWebStorageOptions, type SchedulerHost, type SetupAutoSpyOptions, type SpyEngine, type StopTrackingRejections, type StopTrackingTimers, type StrayRejection, type SystemTime, type XhrBlockMode, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getSpyEngine, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, restoreWebStorage, setSpyEngine, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
|
package/dist/setup.js
CHANGED
|
@@ -140,7 +140,7 @@ function getWatchedTimerGlobals() {
|
|
|
140
140
|
|
|
141
141
|
// src/lib/fake-timers.ts
|
|
142
142
|
function setupFakeTimers(config, { betweenTests = false } = {}) {
|
|
143
|
-
const
|
|
143
|
+
const install2 = () => {
|
|
144
144
|
if (!vi.isFakeTimers()) {
|
|
145
145
|
vi.useFakeTimers(config);
|
|
146
146
|
}
|
|
@@ -152,13 +152,13 @@ function setupFakeTimers(config, { betweenTests = false } = {}) {
|
|
|
152
152
|
restoreTimerGlobals();
|
|
153
153
|
};
|
|
154
154
|
if (betweenTests) {
|
|
155
|
-
beforeAll(
|
|
155
|
+
beforeAll(install2);
|
|
156
156
|
}
|
|
157
|
-
beforeEach(
|
|
157
|
+
beforeEach(install2);
|
|
158
158
|
afterEach(() => {
|
|
159
159
|
uninstall();
|
|
160
160
|
if (betweenTests) {
|
|
161
|
-
|
|
161
|
+
install2();
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
if (betweenTests) {
|
|
@@ -681,6 +681,78 @@ function detectsAsyncLeaks(host = globalThis) {
|
|
|
681
681
|
return Reflect.get(Object(Reflect.get(Object(Reflect.get(host, "__vitest_worker__")), "config")), "detectAsyncLeaks") === true;
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
+
// src/lib/web-storage.ts
|
|
685
|
+
var STORAGE_KEYS = ["localStorage", "sessionStorage"];
|
|
686
|
+
var PROBE_KEY = "__vitest_auto_spy_probe__";
|
|
687
|
+
var PROBE_METHODS = ["setItem", "getItem", "removeItem"];
|
|
688
|
+
var MapBackedStorage = class {
|
|
689
|
+
#items = /* @__PURE__ */ new Map();
|
|
690
|
+
get length() {
|
|
691
|
+
return this.#items.size;
|
|
692
|
+
}
|
|
693
|
+
clear() {
|
|
694
|
+
this.#items.clear();
|
|
695
|
+
}
|
|
696
|
+
getItem(key) {
|
|
697
|
+
return this.#items.get(key) ?? null;
|
|
698
|
+
}
|
|
699
|
+
key(index) {
|
|
700
|
+
return [...this.#items.keys()][index] ?? null;
|
|
701
|
+
}
|
|
702
|
+
removeItem(key) {
|
|
703
|
+
this.#items.delete(key);
|
|
704
|
+
}
|
|
705
|
+
setItem(key, value) {
|
|
706
|
+
this.#items.set(key, String(value));
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
function restoreWebStorage(options = {}) {
|
|
710
|
+
const view = options.view === void 0 ? currentView() : options.view;
|
|
711
|
+
if (!view) {
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
const repaired = [];
|
|
715
|
+
for (const key of STORAGE_KEYS) {
|
|
716
|
+
if (works(read(globalThis, key))) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const fromView = read(view, key);
|
|
720
|
+
install(view, key, works(fromView) ? fromView : new MapBackedStorage());
|
|
721
|
+
repaired.push(key);
|
|
722
|
+
}
|
|
723
|
+
return repaired;
|
|
724
|
+
}
|
|
725
|
+
function currentView() {
|
|
726
|
+
return typeof document === "undefined" ? null : document.defaultView;
|
|
727
|
+
}
|
|
728
|
+
function read(host, key) {
|
|
729
|
+
return Reflect.get(host, key);
|
|
730
|
+
}
|
|
731
|
+
function isStorageShaped(candidate) {
|
|
732
|
+
const host = Object(candidate);
|
|
733
|
+
return PROBE_METHODS.every((method) => typeof Reflect.get(host, method) === "function");
|
|
734
|
+
}
|
|
735
|
+
function works(candidate) {
|
|
736
|
+
if (!isStorageShaped(candidate)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
candidate.setItem(PROBE_KEY, PROBE_KEY);
|
|
741
|
+
const stored = candidate.getItem(PROBE_KEY);
|
|
742
|
+
candidate.removeItem(PROBE_KEY);
|
|
743
|
+
return stored === PROBE_KEY;
|
|
744
|
+
} catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function install(view, key, storage) {
|
|
749
|
+
const descriptor = { value: storage, writable: true, configurable: true };
|
|
750
|
+
Object.defineProperty(globalThis, key, descriptor);
|
|
751
|
+
if (view !== globalThis) {
|
|
752
|
+
Object.defineProperty(view, key, descriptor);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
684
756
|
// src/lib/setup-auto-spy.ts
|
|
685
757
|
function warnAboutSuppressedLeaks(cancelled, write = writeWarning) {
|
|
686
758
|
write(
|
|
@@ -811,11 +883,17 @@ function watchStrayRejections(enabled) {
|
|
|
811
883
|
afterAll(trackStrayRejections());
|
|
812
884
|
return [reportStrayRejections];
|
|
813
885
|
}
|
|
814
|
-
function
|
|
815
|
-
reportDuplicateCopies(options.duplicateCopies ?? "throw");
|
|
886
|
+
function prepareEnvironment(options) {
|
|
816
887
|
if (options.angularBuildHint ?? true) {
|
|
817
888
|
noticeAngularBuildSplitting();
|
|
818
889
|
}
|
|
890
|
+
if (options.restoreWebStorage ?? true) {
|
|
891
|
+
restoreWebStorage();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function setupAutoSpy(options = {}) {
|
|
895
|
+
reportDuplicateCopies(options.duplicateCopies ?? "throw");
|
|
896
|
+
prepareEnvironment(options);
|
|
819
897
|
if (options.strayTimers ?? false) {
|
|
820
898
|
trackStrayTimers();
|
|
821
899
|
afterAll(() => {
|
|
@@ -886,10 +964,10 @@ function describeSkippedTeardown(leaked) {
|
|
|
886
964
|
DOCS_LINKS.setup
|
|
887
965
|
);
|
|
888
966
|
}
|
|
889
|
-
function installPerTest(
|
|
967
|
+
function installPerTest(install2) {
|
|
890
968
|
let current;
|
|
891
969
|
beforeEach(() => {
|
|
892
|
-
current = { value:
|
|
970
|
+
current = { value: install2() };
|
|
893
971
|
});
|
|
894
972
|
afterEach(() => {
|
|
895
973
|
current = void 0;
|
|
@@ -1000,4 +1078,4 @@ function registerFocusMatchers() {
|
|
|
1000
1078
|
}
|
|
1001
1079
|
useVitestAdapter();
|
|
1002
1080
|
|
|
1003
|
-
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
|
|
1081
|
+
export { BLOCKED_FETCH_MESSAGE, BLOCKED_XHR_MESSAGE, advanceTimers, blockNetwork, cancelStrayTimers, captureMockRegistry, countStrayRejections, countStrayTimers, flushStrayRejections, getMockRegistrySize, getWatchedTimerGlobals, guardGlobalPatches, installPerTest, keepMockRegistered, keepRegisteredMocks, mockNow, mockSystemTime, pruneMockRegistry, registerFocusMatchers, resetMockRegistryTracking, restoreLongLivedImplementations, restoreTimerGlobals, restoreWebStorage, setupAutoSpy, setupFakeTimers, trackMockRegistry, trackStrayRejections, trackStrayTimers, useCountingClock, withSystemTime };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, 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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vitest-auto-spy
|
|
3
|
-
description: Write or fix tests that use vitest-auto-spy — typed spies from a class or a type on Vitest, bun:test and node:test. Use when a spec imports `vitest-auto-spy` or
|
|
3
|
+
description: Write or fix tests that use vitest-auto-spy — typed spies from a class or a type on Vitest, bun:test and node:test. Use when a spec imports `vitest-auto-spy` or a subpath (`/angular`, `/angular-http`, `/bun-angular`, `/bun`, `/node`, `/rxjs`, `/nestjs`, `/jasmine`, `/observer-spy`, `/setup`, `/zone`, `/diagnostics`, `/eslint-plugin`), the user mentions createSpyFromClass, createAutoMock, createMock, mockDeep, createFunctionSpy, createSpyObj, enableJasmineCompat, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, createDirectiveHost, overrideComponentProvider, enableAngularDiagnostics, trackInjections, runEffect, settleResource, httpResource, mockResourceProp, mockSignalProp, mockReadonlyProp, captureArg, createSpyFromInstance, explainSpy, expectEmission, expectError, setupAutoSpy, setSpyEngine, restoreWebStorage, assertMocked, Spy<T>, calledWith, mustBeCalledWith, onlyMethodsToSpyOn, strict, onUnstubbedCall, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator (createSpyObject, mockProvider, SpectatorService), or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not found on the class prototype", "strict mode is on", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController", "localStorage.setItem is not a function" or "Spy<T> is not assignable".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# vitest-auto-spy
|
|
@@ -126,69 +126,70 @@ it('loads', async () => {
|
|
|
126
126
|
|
|
127
127
|
## Reach for these before hand-rolling
|
|
128
128
|
|
|
129
|
-
| Situation
|
|
130
|
-
|
|
|
131
|
-
| a `signal()` / `computed()` field on the class under test
|
|
132
|
-
| a resource field, when the HTTP round trip is not the point
|
|
133
|
-
| the HTTP round trip _is_ the point
|
|
134
|
-
| a node:test suite whose heap grows all run
|
|
135
|
-
| a Nest provider whose constructor keeps changing
|
|
136
|
-
| asserting a resource's value _and_ status together
|
|
137
|
-
| a callback or config object the code under test built
|
|
138
|
-
| an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it
|
|
139
|
-
| a `calledWith` that is not matching and you cannot see why
|
|
140
|
-
| a method that has to throw — for all calls, or for some arguments
|
|
141
|
-
| a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach`
|
|
142
|
-
| an `effect()` whose trigger is now a static signal
|
|
143
|
-
| the component constructs its own `IntersectionObserver`
|
|
144
|
-
| a green run exiting 1 with `AbortError` under happy-dom
|
|
145
|
-
| a real request going out of a unit run (`fetch`, XHR, `sendBeacon`)
|
|
146
|
-
| timers or frames from a previous file failing this one
|
|
147
|
-
| an assertion error in stderr, every test green and the run at 0
|
|
148
|
-
| a run getting slower the longer it goes, on `isolate: false`
|
|
149
|
-
| `JavaScript heap out of memory` on a suite of wide generated clients
|
|
150
|
-
| `Cannot read properties of undefined (reading 'now')`
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
| a
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
| a
|
|
164
|
-
| a
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
| `
|
|
170
|
-
|
|
|
171
|
-
| a
|
|
172
|
-
| a
|
|
173
|
-
| a `
|
|
174
|
-
| `
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
| a
|
|
179
|
-
|
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
| `
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
| `
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
|
|
|
129
|
+
| Situation | Helper |
|
|
130
|
+
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — returns the writable |
|
|
132
|
+
| a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading` |
|
|
133
|
+
| the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
|
|
134
|
+
| a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker |
|
|
135
|
+
| a Nest provider whose constructor keeps changing | `createNestUnit(Target, { expose })` — built from its DI metadata |
|
|
136
|
+
| asserting a resource's value _and_ status together | `registerResourceMatchers()` → `toHaveResourceValue` / `toBeLoading` |
|
|
137
|
+
| a callback or config object the code under test built | `captureArg<T>()` in the assertion, then read `.value` |
|
|
138
|
+
| an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it |
|
|
139
|
+
| a `calledWith` that is not matching and you cannot see why | `explainSpy(spy, 'method')` from `/diagnostics` — configs, calls, which hit which |
|
|
140
|
+
| 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` |
|
|
141
|
+
| a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach` | `extendWithAutoSpies(test, { cart: CartService })` (`/angular`) |
|
|
142
|
+
| an `effect()` whose trigger is now a static signal | `runEffect(effectRef)` |
|
|
143
|
+
| the component constructs its own `IntersectionObserver` | `stubIntersectionObserver()` (+ `Resize` / `Mutation`) |
|
|
144
|
+
| a green run exiting 1 with `AbortError` under happy-dom | `setupAutoSpy({ blockNetwork: true })` |
|
|
145
|
+
| a real request going out of a unit run (`fetch`, XHR, `sendBeacon`) | `setupAutoSpy({ blockNetwork: true })` — `{ xhr: 'empty' }` for pings |
|
|
146
|
+
| timers or frames from a previous file failing this one | `setupAutoSpy({ strayTimers: true })` — mutes `--detect-async-leaks`, see below |
|
|
147
|
+
| an assertion error in stderr, every test green and the run at 0 | `setupAutoSpy({ strayRejections: true })` — zone.js swallowed it |
|
|
148
|
+
| a run getting slower the longer it goes, on `isolate: false` | `setupAutoSpy({ pruneMockRegistry: true })` — the mock registry |
|
|
149
|
+
| `JavaScript heap out of memory` on a suite of wide generated clients | `createSpyFromClass(X, { lazySpies: 'proxy' })` — one trap object, not a placeholder per method |
|
|
150
|
+
| `Cannot read properties of undefined (reading 'now')` | `restoreTimerGlobals` — on by default |
|
|
151
|
+
| `localStorage.setItem is not a function`, or it is `undefined` | `restoreWebStorage` — on by default; Vitest's global filter, Node 25+ |
|
|
152
|
+
| a spy handed to an API typed against the real class | `asInstance()` / `asSpy()` |
|
|
153
|
+
| the code under test does `new X()` (a global, a vendor SDK) | `mockConstructor(factory)` / `stubConstructor(obj, key, factory)` |
|
|
154
|
+
| `X is not a constructor`, with a stack in production code | same — a `vi.fn(() => …)` cannot serve `new` |
|
|
155
|
+
| waiting for a dynamic `import()` under fake timers | `settleDynamicImport(() => import('…'))` / `flushEventLoop()` |
|
|
156
|
+
| `addEventListener(…, { signal })` throwing about `EventTarget` | `stubAbortController()` |
|
|
157
|
+
| a suite ported from Jest's `fakeTimers.enableGlobally` | `setupAutoSpy({ globalFakeTimers: true })` |
|
|
158
|
+
| `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()` | `setSpyEngine('runner')` / `getSpyEngine()` — `/setup`, Vitest only |
|
|
159
|
+
| a nested `describe`'s `beforeAll` landing on real timers | `setupFakeTimers(cfg, { betweenTests: true })` |
|
|
160
|
+
| setup hooks applying to the first spec file of a worker only | the setup module is cached — run coverage with `--isolate` |
|
|
161
|
+
| `fakeAsync` inside `test.concurrent` | `installProxyZonePatch({ scope: 'callback' })` |
|
|
162
|
+
| an assertion containing a date | `mockSystemTime(iso)` — never `vi.spyOn(globalThis, 'Date')` |
|
|
163
|
+
| a spec asserting on tick _order_ under a frozen clock | `useCountingClock()` |
|
|
164
|
+
| a dependency declared in the component's own `providers` | `overrideComponentProvider(Cmp, Token)` — it verifies on the first fixture that the override applied |
|
|
165
|
+
| a double answering `undefined` for a method nobody configured | `{ strict: true }`, or `setupAutoSpy({ strict: true })` suite-wide |
|
|
166
|
+
| an `afterEach` that exists only to reset one spy | `using spy = createSpyFromClass(X)` — `[Symbol.dispose]` resets it |
|
|
167
|
+
| a dead NgModule import, dead `schemas`, an unflushed HTTP request | `enableAngularDiagnostics()` in the setup file, after `initTestEnvironment` |
|
|
168
|
+
| "which collaborators did this actually inject?" | `trackInjections([A, TOKEN])` — providers plus the record, not `vi.mock` |
|
|
169
|
+
| `NG0303` / `NG0301` / `NG0304` from an imported NgModule | `assertNgModuleScopes(Module)` — an AOT bundle stripped its scope |
|
|
170
|
+
| `Cannot read properties of undefined (reading 'provide')` in `di_setup` | `assertComponentDefIntact(Cmp)` — a barrel chunk left a hole in `ɵcmp` |
|
|
171
|
+
| a focus assertion failing as `expected false to deeply equal true` | `registerFocusMatchers()` + `expect(el).toHaveFocus()` |
|
|
172
|
+
| a collaborator passed as an argument, then asserted on | `autoMocked<T>()` — typed `T & Spy<T>` |
|
|
173
|
+
| a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN` | `stubMediaElement({ duration })`, then `media.set(el, …)` |
|
|
174
|
+
| a `vi.mock()` that silently did nothing under a bundler | `assertMocked(ns, { specifier, exports })` |
|
|
175
|
+
| `No "default" export is defined on the mock` | `vi.mock('x', () => moduleNamespace({ … }))` |
|
|
176
|
+
| waiting for a `resource()` / an SDK to become ready | `flushEventLoopUntil(() => …, { label })` — budgeted, not tuned |
|
|
177
|
+
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …` | `expect(diffByField(actual, expected)).toBeUndefined()` |
|
|
178
|
+
| a stub that works only in the first test of the file | `installPerTest(() => stub…())` — or install it in `beforeEach` |
|
|
179
|
+
| a library failing every other run after a `defineProperty` on DOM | `setupAutoSpy({ guardGlobals: 'throw' })` names the file |
|
|
180
|
+
| `Cannot set base providers because it has already been called` | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` |
|
|
181
|
+
| a dependency behind an `InjectionToken`, with no class to spy | `provideAutoSpyForToken(TOKEN)` + `injectSpy(TOKEN)` |
|
|
182
|
+
| `Expected to be running in 'ProxyZone', but it was not found` | `import 'vitest-auto-spy/zone'` (needs `globals: true`) |
|
|
183
|
+
| `Property 'mockReturnValue' does not exist on type 'never'` | upgrade — the spy no longer collapses on an unreadable return type |
|
|
184
|
+
| a signal-valued getter that `gettersToSpyOn` will not accept | it accepts any key now; for a signal prefer `mockSignalProp` |
|
|
185
|
+
| five `asInstance(…)` in one call, found one per `tsc` run | `...asInstances(a, b, c, d, e)` |
|
|
186
|
+
| `nextWith` demanding `HttpEvent<T>` on a generated client | `asSpy<Client, { overload: 'first' }>(…)` / `Overload<M, 0>` |
|
|
187
|
+
| a fixture that needs a nested object built by its own call | `createMock<T>({ a: { b: 1 } })` — deep partial, still exact |
|
|
188
|
+
| the same 100-line model literal copied into eight specs (`TS1117`) | one `createFixtureFactory<T>(defaults)`; specs call it with what they change |
|
|
189
|
+
| `'params' in link` ladders, or a cast, to pick a union branch | `narrow.byKey(link, 'params')` / `narrow.observable(x)` |
|
|
190
|
+
| `{ ...modelInstance, flag: true }` losing every getter | `withOverrides(modelInstance, { flag: true })` |
|
|
191
|
+
| `NG0303` / `NG0304` / silence from a directive spec | `createDirectiveHost({ template, scope: [Module] })` |
|
|
192
|
+
| "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` |
|
|
192
193
|
|
|
193
194
|
## Rules that prevent most of the mistakes
|
|
194
195
|
|
|
@@ -322,7 +323,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
|
|
|
322
323
|
Most of this library's guarantees are type-level, so a green run that does not type-check is not
|
|
323
324
|
done. Report failures with their output rather than describing them as passing.
|
|
324
325
|
|
|
325
|
-
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The
|
|
326
|
+
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The twenty rules in
|
|
326
327
|
`vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
|
|
327
328
|
to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
|
|
328
329
|
evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
|