vitest-auto-spy 5.29.0 → 5.30.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 +21 -9
- package/CHANGELOG.md +47 -1
- package/README.md +11 -6
- package/dist/cli.js +1 -1
- package/dist/eslint-plugin.cjs +259 -36
- package/dist/setup.d.ts +10 -2
- package/dist/setup.js +36 -14
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +121 -121
package/AGENTS.md
CHANGED
|
@@ -216,7 +216,10 @@ the test must not touch the real one.
|
|
|
216
216
|
verbose for the most common thing it says — _observe one method of the object under test and let it
|
|
217
217
|
run_ — so the helper says exactly that and hands back the single spy. It is also the drop-in for a
|
|
218
218
|
bare `vi.spyOn(component, 'method')` where a preset's `no-restricted-properties` bans `vi.spyOn`:
|
|
219
|
-
same default semantics, record and call through, and `restoreSpiedInstance` puts it back.
|
|
219
|
+
same default semantics, record and call through, and `restoreSpiedInstance` puts it back. It is a
|
|
220
|
+
core export — `import { spyOnOwnMethod } from 'vitest-auto-spy'`, or the runner entry on
|
|
221
|
+
`bun test` / `node --test` — while `vitest-auto-spy/angular` and `vitest-auto-spy/nestjs` have no
|
|
222
|
+
core re-export, so those suites import it from the root; `spyOnVoidMethod` below is the same.
|
|
220
223
|
|
|
221
224
|
**Do not `createSpyFromInstance` a DOM node that must keep living a DOM life.** Discovery walks the
|
|
222
225
|
node's prototype chain, and past the component's own class that chain is the engine's: happy-dom's
|
|
@@ -1508,15 +1511,22 @@ setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors
|
|
|
1508
1511
|
|
|
1509
1512
|
The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
|
|
1510
1513
|
`cancelStrayTimers()` (returns how many it cancelled), `countStrayTimers()` and
|
|
1511
|
-
`describeStrayTimers()` (each pending timer's kind
|
|
1512
|
-
frames), all from `vitest-auto-spy/setup`. Use
|
|
1513
|
-
to make a leak fail rather than be tidied
|
|
1514
|
-
|
|
1514
|
+
`describeStrayTimers()` (each pending timer's `kind`, the `delay` of a timeout or interval, the spec
|
|
1515
|
+
`file` that scheduled it and up to five `frames`), all from `vitest-auto-spy/setup`. Use
|
|
1516
|
+
`expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak fail rather than be tidied
|
|
1517
|
+
away, or take the per-file report from the sweep itself — `timers` carries the same origins, so the
|
|
1518
|
+
failure diff names the file and the scheduling call:
|
|
1515
1519
|
|
|
1516
1520
|
```ts
|
|
1517
1521
|
setupAutoSpy({ strayTimers: true, onStrayTimers: ({ timers }) => expect(timers).toEqual([]) });
|
|
1518
1522
|
```
|
|
1519
1523
|
|
|
1524
|
+
`frames` starts below the tracking wrapper and is up to forty frames deep, so a zone or an rxjs
|
|
1525
|
+
scheduler in between does not hide the caller: project frames come first; when the call came from
|
|
1526
|
+
dependencies only, their frames stand in. The package's own frames are never quoted, from source or
|
|
1527
|
+
from `dist`. A handler that prints its own message should print `kind`, `delay` and `frames[0]` —
|
|
1528
|
+
they are what makes a report that only reproduces under a slow coverage run actionable the first time.
|
|
1529
|
+
|
|
1520
1530
|
A stray whose `file` is not the file that failed was scheduled after the previous file's sweep —
|
|
1521
1531
|
the previous file is the one to fix.
|
|
1522
1532
|
|
|
@@ -3883,6 +3893,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
3883
3893
|
| `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 |
|
|
3884
3894
|
| `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
3885
3895
|
| `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })` — the name is resolved against the compiled definition before the first write (an undeclared one is an `NG0303` and no change) and the value is typed. The run collapses into one call and a `detectChanges()` under it goes; offered, not applied, because `stable()` ticks and a zone.js suite answers that with `NG0101` |
|
|
3896
|
+
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
|
|
3886
3897
|
| `prefer-observer-stub` | `error` | — | a hand-rolled observer global → `stubIntersectionObserver()` / `stubResizeObserver()` / `stubMutationObserver()`; the manual save-and-restore goes too, `restoreMockedProps()` runs the undo |
|
|
3887
3898
|
| `no-hand-assigned-global` | `error` | — | a double assigned to a global (`global.fetch = vi.fn()`, `window.matchMedia = vi.fn()`, `window.localStorage = { getItem: vi.fn() }`) with no restore in `afterEach` / `afterAll` / `onTestFinished` → `mockValueProp(globalThis, name, value)` or `vi.stubGlobal` + `unstubGlobals`; `blockNetwork()` for network globals, `stubWebStorage()` for the storages; the three observers stay with `prefer-observer-stub` |
|
|
3888
3899
|
| `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or `createMock<Response>(…)` / `createAutoMock<Response>(…)` → `stubResponse({ body })` from `/setup`; the literal answers `undefined` for every member it does not list (`status`, `headers`, `text()`) and the cast is what makes that compile. `Response` must resolve to the **global**, so an Express handler's or a generated client's `Response` is never reported |
|
|
@@ -3910,14 +3921,14 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
3910
3921
|
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
3911
3922
|
| `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
|
|
3912
3923
|
|
|
3913
|
-
Forty-
|
|
3924
|
+
Forty-nine rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
|
|
3914
3925
|
`no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
|
|
3915
|
-
`prefer-set-inputs` and `
|
|
3926
|
+
`prefer-set-inputs`, `no-unasserted-argument` and `prefer-spy-on-own-method`**; six fix on their own, nineteen offer suggestions. Forty-six are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
|
|
3916
3927
|
`no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
|
|
3917
3928
|
rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
|
|
3918
3929
|
builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
|
|
3919
3930
|
consumer how much each finding mattered — a `warn` nothing reads is `off` with extra output. The
|
|
3920
|
-
|
|
3931
|
+
eight `warn`s left are not judgements about how much a finding matters. `prefer-render-shallow` is
|
|
3921
3932
|
about the _kind_ of finding: every other rule names something wrong or dead, while this one names a
|
|
3922
3933
|
file that could render more cheaply, and `renderShallow` is a migration a suite either takes or does
|
|
3923
3934
|
not. At `error` the plugin would gate that migration — 491 findings across 398 of one consumer's
|
|
@@ -3940,7 +3951,8 @@ repair _needs_ rather than on its evidence: both of its readings are facts out o
|
|
|
3940
3951
|
subject some other test in the same file pins with `toHaveBeenCalledWith`, a title saying `with` over
|
|
3941
3952
|
a body whose every assertion is a bare call — but the repair is the argument list the test should
|
|
3942
3953
|
have named, which is the one thing the rule cannot supply. Every `error` here names an edit or names
|
|
3943
|
-
a helper; this one names a question for the author.
|
|
3954
|
+
a helper; this one names a question for the author. `prefer-spy-on-own-method` names a shorter spelling of a correct call — the
|
|
3955
|
+
`createSpyFromInstance` it reports does exactly what the helper does — and its exact shapes carry a fix. Set any of the eight `warn`s to `'error'` once the batch is done. Three of
|
|
3944
3956
|
them can report on a _correct_ project, and only one has an option:
|
|
3945
3957
|
`jasmine-namespace-without-entry` takes `['error', { setupModules: ['./test-setup'] }]`, naming the
|
|
3946
3958
|
file where `enableJasmineCompat()` is called; `prefer-native-spy-api` goes `'off'` for as long as a
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,51 @@ The latest released version here must always match the one published on
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [5.30.0] - 2026-09-25
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`prefer-spy-on-own-method`, in `recommended` at `warn`.** It finds the long spelling of the two
|
|
18
|
+
5.29 helpers, which a text search misses once the call spans several lines:
|
|
19
|
+
`createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone —
|
|
20
|
+
`.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` — is
|
|
21
|
+
`spyOnOwnMethod(x, 'm')`, and the same whitelist with `returns: { m: undefined }` is
|
|
22
|
+
`spyOnVoidMethod(x, 'm')`. Both are exact, so `--fix` rewrites the call and every `v.m` read, imports
|
|
23
|
+
the helper beside the factory and drops the factory's import once nothing else uses it. A
|
|
24
|
+
`Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed,
|
|
25
|
+
whose discovery also spies every other method, is offered as a suggestion only on a real event or
|
|
26
|
+
element (`new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement`, one name away
|
|
27
|
+
included), never on a double. `warn`, because the call it reports is correct. `/eslint-plugin`
|
|
28
|
+
grows 51.61 → 53.56 kB min+gzip (+1.96 kB, +3.8 %), all of it this rule: the fixer that follows
|
|
29
|
+
every `v.m` read, rewrites imports and the `Spy<X>` annotation. The plugin runs in the linter, never
|
|
30
|
+
in a test, and no runtime entry imports it.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **A stray timer's origin names the code that scheduled it, not the tracking wrapper.** Built from
|
|
35
|
+
`dist`, the report quoted `at captureOrigin (…/node_modules/vitest-auto-spy/dist/setup.js)`: the
|
|
36
|
+
filter for the wrapper's own frames matched the source file name, which the bundle does not have,
|
|
37
|
+
and twelve frames ran out inside a zone or an rxjs scheduler before the first line outside
|
|
38
|
+
dependencies, so the fallback quoted the wrapper. The stack is now cut at the installed
|
|
39
|
+
`setTimeout` / `setInterval` / `requestAnimationFrame` wrapper and taken forty frames deep below it;
|
|
40
|
+
the package's own frames are never quoted, and when the call came from dependencies only, their
|
|
41
|
+
frames stand in. The same `dist` filter now applies to stray listeners, console calls and
|
|
42
|
+
rejections. A flaky stray that shows up only under a slow coverage run is now actionable from the
|
|
43
|
+
first report.
|
|
44
|
+
- **`StrayTimer` carries the `delay`, and the `--detect-async-leaks` warning prints it.** Each
|
|
45
|
+
timeout and interval in `describeStrayTimers()` and in `onStrayTimers`' `timers` now has the delay
|
|
46
|
+
it was scheduled with (a frame has none), and the warning reads `timeout (300 ms) from …` instead
|
|
47
|
+
of `timeout from …`. An additive field; a handler that prints its own message should print `kind`,
|
|
48
|
+
`delay` and `frames[0]`.
|
|
49
|
+
|
|
50
|
+
### Docs
|
|
51
|
+
|
|
52
|
+
- **`AGENTS.md` says which entry `spyOnOwnMethod` and `spyOnVoidMethod` come from.** The §2
|
|
53
|
+
introduction sat in Angular-flavoured company, and `vitest-auto-spy/angular` has no core
|
|
54
|
+
re-export — an Angular-suite reader importing the helper there hit a missing export. The
|
|
55
|
+
paragraph now names the core entry (root, or the runner entry off Vitest), as the docs-site
|
|
56
|
+
page already does.
|
|
57
|
+
|
|
13
58
|
## [5.29.0] - 2026-09-24
|
|
14
59
|
|
|
15
60
|
### Added
|
|
@@ -7845,7 +7890,8 @@ by hand there, in more than one place, by more than one person.
|
|
|
7845
7890
|
`mockAccessorsProp`.
|
|
7846
7891
|
- Dual ESM + CJS build with type declarations; 100% test coverage.
|
|
7847
7892
|
|
|
7848
|
-
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.
|
|
7893
|
+
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.30.0...HEAD
|
|
7894
|
+
[5.30.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.29.0...v5.30.0
|
|
7849
7895
|
[5.29.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.28.0...v5.29.0
|
|
7850
7896
|
[5.28.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.27.0...v5.28.0
|
|
7851
7897
|
[5.27.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.26.0...v5.27.0
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
|
|
|
81
81
|
- 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored; `moduleNamespace(await importOriginal(), { passthrough: true })` is Vitest's `{ spy: true }` with `calledWith` on top, on any runner
|
|
82
82
|
- 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`, and `outOfType()` for the fixture that is outside its type on purpose
|
|
83
83
|
- 🚚 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
|
|
84
|
-
- 📏 Lint rules and one-line test-run hygiene — forty-
|
|
84
|
+
- 📏 Lint rules and one-line test-run hygiene — forty-nine rules in `vitest-auto-spy/eslint-plugin` (six `--fix`, nineteen suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade, and under `isolate: false` `strayListeners` / `restoreGlobals` for the listeners and raw global assignments a file leaves the next one, `restoreStorageSpies` (on by default) for the storage spy happy-dom will not let `mockRestore()` remove
|
|
85
85
|
- 🩺 [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
|
|
86
86
|
- 🔎 [`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
|
|
87
87
|
- ⏱️ [`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
|
|
@@ -3206,7 +3206,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
3206
3206
|
| `guardGlobalPatches(reaction)` | `/setup` | Name the test that redefined a property of `document` / `navigator` / `globalThis` as non-configurable |
|
|
3207
3207
|
| `guardStrayConsole(reaction)` | `/setup` | Fail a test that wrote to the console without absorbing it, and a file that wrote outside any test ([details](#test-run-hygiene)) |
|
|
3208
3208
|
| `withoutStrayTimerTracking(work)` | `/setup` | Run setup work whose timers the stray-timer tracker neither counts nor cancels — jsdom schedules one per Web Storage write |
|
|
3209
|
-
| `describeStrayTimers()` | `/setup` | Every timer still pending, with its kind, the spec file that scheduled it and the scheduling frames — the list `onStrayTimers` gets. Blind to anything scheduled under fake timers — `vi.useFakeTimers()` assigns over the wrapper
|
|
3209
|
+
| `describeStrayTimers()` | `/setup` | Every timer still pending, with its kind and delay, the spec file that scheduled it and the scheduling frames — the list `onStrayTimers` gets. Blind to anything scheduled under fake timers — `vi.useFakeTimers()` assigns over the wrapper |
|
|
3210
3210
|
| `guardPrototypePollution(reaction)` | `/setup` | Name the test that left a key on `Object.prototype` — it stops later files collecting ([details](#test-run-hygiene)) |
|
|
3211
3211
|
| `guardDocumentPollution(option)` | `/setup` | Name the test that left an attribute on `<html>` / `<body>`, and put it back ([details](#test-run-hygiene)) |
|
|
3212
3212
|
| `installPerTest(install)` | `/setup` | Re-install a stub before every test of the block — a `describe`-level stub is restored away after the first |
|
|
@@ -3790,7 +3790,7 @@ export default [
|
|
|
3790
3790
|
];
|
|
3791
3791
|
```
|
|
3792
3792
|
|
|
3793
|
-
One of the
|
|
3793
|
+
One of the exceptions is `prefer-render-shallow`, which ships as a **`warn`**, and its reason is the kind
|
|
3794
3794
|
of thing it says rather than how much it matters. Every other rule here names something wrong or dead
|
|
3795
3795
|
— a double that drifts from its class, an assertion that never runs, a provider the container already
|
|
3796
3796
|
dropped, a schema guarding nothing. That one names a file that could render more cheaply, which is a
|
|
@@ -3800,6 +3800,10 @@ it was measured against the rule reports 491 times across 398 files — a first
|
|
|
3800
3800
|
have to answer with a project-wide `warn` of its own. A project that has decided to make the move sets
|
|
3801
3801
|
it to `'error'` in the same one line that turns the others down.
|
|
3802
3802
|
|
|
3803
|
+
`prefer-spy-on-own-method` is graded on the same reading. The `createSpyFromInstance` call it reports is
|
|
3804
|
+
correct and does exactly what `spyOnOwnMethod` / `spyOnVoidMethod` does: the rule names a shorter
|
|
3805
|
+
spelling, not a defect, and its exact shapes carry a fix, so `'error'` is one `eslint --fix` away.
|
|
3806
|
+
|
|
3803
3807
|
The other two, `no-stub-class-double` and `no-structural-double` (5.5.0), are graded on the
|
|
3804
3808
|
_evidence_ rather than on the kind of finding. Both report the same drift
|
|
3805
3809
|
`prefer-create-spy-from-class` reports at `error` — a double whose shape was written by hand and is
|
|
@@ -3862,6 +3866,7 @@ export can never be.
|
|
|
3862
3866
|
| `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 |
|
|
3863
3867
|
| `prefer-render-shallow` | `warn` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
|
|
3864
3868
|
| `prefer-set-inputs` | `warn` | suggest | a run of `fixture.componentRef.setInput('title', v)` on one fixture → `await setInputs(fixture, { title: v })`, which resolves every name against the compiled definition before it writes one — an undeclared name is an `NG0303` on the console and no change at all — and types the value. The suggestion collapses the run, drops a `detectChanges()` under it and makes the callback `async`; it is offered rather than applied, because `stable()` ticks and a zone.js suite can answer that with `NG0101` |
|
|
3869
|
+
| `prefer-spy-on-own-method` | `warn` | `--fix` / suggest | `createSpyFromInstance(x, { onlyMethodsToSpyOn: ['m'], passthrough: true })` read for `m` alone — `.m` on the call, `const { m } = …`, a bare statement, or a name read only as `v.m` → `spyOnOwnMethod(x, 'm')`; the same whitelist with `returns: { m: undefined }` → `spyOnVoidMethod(x, 'm')`. Both fix, import and all; a `Spy<X>` annotation becomes `Spy<X>['m']` in a suggestion. A bare `returns: { m: undefined }` seed is a suggestion, offered only on a real event or element — `new MouseEvent(…)`, `document.createElement(…)`, `fixture.nativeElement` — never on a double |
|
|
3865
3870
|
| `prefer-observer-stub` | `error` | — | a hand-rolled `IntersectionObserver` / `ResizeObserver` / `MutationObserver` written into a global → `stubIntersectionObserver()` and friends, whose undo `restoreMockedProps()` already runs |
|
|
3866
3871
|
| `no-hand-assigned-global` | `error` | — | `global.fetch = vi.fn(…)` and any other double assigned to a global with no restore in a teardown hook → `mockValueProp(globalThis, …)` / `vi.stubGlobal` + `unstubGlobals`, `blockNetwork()` for a spec that only stays offline, `stubWebStorage()` for the storages |
|
|
3867
3872
|
| `prefer-stub-response` | `error` | — | an object literal cast to `Response` (`as Response`, `as unknown as Response`, `<Response>{ … }`) or a `createMock<Response>(…)` / `createAutoMock<Response>(…)` call → `stubResponse({ body })` from `/setup`, which builds the platform's own and answers every member; `Response` has to resolve to the global, so an Express handler's or a generated client's envelope of the same name is left alone |
|
|
@@ -3893,7 +3898,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
|
|
|
3893
3898
|
"don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
|
|
3894
3899
|
are versioned together and stop being re-written in every project that installs the package.
|
|
3895
3900
|
|
|
3896
|
-
**
|
|
3901
|
+
**Six of the forty-nine fix on their own, nineteen offer suggestions**, and the split is not about how hard
|
|
3897
3902
|
the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
|
|
3898
3903
|
compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
|
|
3899
3904
|
`import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
|
|
@@ -4064,7 +4069,7 @@ package's own — it needs its ESLint integration switched on.
|
|
|
4064
4069
|
### WebStorm and the other JetBrains IDEs
|
|
4065
4070
|
|
|
4066
4071
|
No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
|
|
4067
|
-
all run ESLint natively, so the forty-
|
|
4072
|
+
all run ESLint natively, so the forty-nine rules appear inline, in the **Problems** tool window, and
|
|
4068
4073
|
under **Code → Inspect Code** for the whole project.
|
|
4069
4074
|
|
|
4070
4075
|
```js
|
|
@@ -4081,7 +4086,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
|
|
|
4081
4086
|
the fixes and suggestions live.
|
|
4082
4087
|
|
|
4083
4088
|
A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
|
|
4084
|
-
and then keep a second copy of forty-
|
|
4089
|
+
and then keep a second copy of forty-nine rules, in Kotlin, in step with the TypeScript ones.
|
|
4085
4090
|
|
|
4086
4091
|
### VS Code, Cursor, Windsurf, VSCodium
|
|
4087
4092
|
|
package/dist/cli.js
CHANGED
|
@@ -3088,7 +3088,7 @@ function checkForeignPragma(graph) {
|
|
|
3088
3088
|
}
|
|
3089
3089
|
|
|
3090
3090
|
// src/cli/checks/export-map.generated.ts
|
|
3091
|
-
var EXPORT_MAP_VERSION = "5.
|
|
3091
|
+
var EXPORT_MAP_VERSION = "5.29.0";
|
|
3092
3092
|
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/diagnostics vitest-auto-spy/angular/doubles vitest-auto-spy/angular/matchers 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 vitest-auto-spy/perf-reporter";
|
|
3093
3093
|
var EXPORTED_BY = {
|
|
3094
3094
|
AccessorImplementations: "0 1 2 3 4 12 20 21 22",
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -478,8 +478,8 @@ var preferSettleDynamicImport = defineRule({
|
|
|
478
478
|
}
|
|
479
479
|
const messageId = consumption === "await" ? "awaitedDynamicImport" : "thenedDynamicImport";
|
|
480
480
|
const suggestion = wrap(context, node);
|
|
481
|
-
const
|
|
482
|
-
context.report(suggestion ? { ...
|
|
481
|
+
const report2 = { node, messageId };
|
|
482
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
483
483
|
}
|
|
484
484
|
})
|
|
485
485
|
});
|
|
@@ -966,8 +966,8 @@ var noCompileComponents = defineRule({
|
|
|
966
966
|
create: (context) => inlinesResources(context) && !namesIgnoredComponent(context) ? {
|
|
967
967
|
'CallExpression[callee.computed=false][callee.property.name="compileComponents"]': (node) => {
|
|
968
968
|
const suggestion = removal(context, node);
|
|
969
|
-
const
|
|
970
|
-
context.report(suggestion ? { ...
|
|
969
|
+
const report2 = { node, messageId: "noCompileComponents" };
|
|
970
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
971
971
|
}
|
|
972
972
|
} : {}
|
|
973
973
|
});
|
|
@@ -1792,8 +1792,8 @@ var preferCreateMock = defineRule({
|
|
|
1792
1792
|
return;
|
|
1793
1793
|
}
|
|
1794
1794
|
const suggestion = buildFixture(context, node);
|
|
1795
|
-
const
|
|
1796
|
-
context.report(suggestion ? { ...
|
|
1795
|
+
const report2 = { node, messageId: "castFixture", data: { type } };
|
|
1796
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
1797
1797
|
}
|
|
1798
1798
|
})
|
|
1799
1799
|
});
|
|
@@ -1876,12 +1876,12 @@ var noMockCast = defineRule({
|
|
|
1876
1876
|
const member = node.expression.property;
|
|
1877
1877
|
const configured = isIdentifier(member) && CONFIGURATION_MEMBERS.has(member.name);
|
|
1878
1878
|
const suggestion = readFromDi(context, node, node.expression);
|
|
1879
|
-
const
|
|
1879
|
+
const report2 = {
|
|
1880
1880
|
node,
|
|
1881
1881
|
messageId: configured ? "configurationCast" : "mockCast",
|
|
1882
1882
|
data: { member: context.sourceCode.getText(member), type }
|
|
1883
1883
|
};
|
|
1884
|
-
context.report(suggestion ? { ...
|
|
1884
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
1885
1885
|
}
|
|
1886
1886
|
})
|
|
1887
1887
|
});
|
|
@@ -1981,7 +1981,7 @@ var preferObserverStub = defineRule({
|
|
|
1981
1981
|
preferObserverStubOverRunner: "`{{observer}}` is being faked through the runner. `{{helper}}()` from `vitest-auto-spy/dom-stubs` replaces the whole of that and returns a handle rather than a static field: `observers.last.emit([{{entry}}(\u2026)])` drives the callback, `observers.last.targets` is what was observed, `observers.last.disconnected` is the teardown assertion, and `observers.instances.length` counts constructions \u2014 which is what a spy on the constructor was being counted for. Two things the runner form does not give you. It is not a constructor: `vi.fn().mockImplementation((callback) => ({ observe() {} }))` is an arrow, `new {{observer}}(callback)` inside the component cannot call it, and the `TypeError` lands in application code with the spec still looking correct. And it is not undone: `{{helper}}()` registers its undo with `restoreMockedProps()`, which `setupAutoSpy()` runs after every test, so nothing has to remember `vi.unstubAllGlobals()` or an `afterEach`."
|
|
1982
1982
|
},
|
|
1983
1983
|
create: (context) => {
|
|
1984
|
-
const
|
|
1984
|
+
const report2 = (node, target, messageId) => context.report({ node, messageId, data: target });
|
|
1985
1985
|
const spiedObserver = (node) => {
|
|
1986
1986
|
const target = node.arguments[0];
|
|
1987
1987
|
return target !== void 0 && isGlobalReceiver(target) ? observerNamed(node.arguments[1]) : void 0;
|
|
@@ -1990,7 +1990,7 @@ var preferObserverStub = defineRule({
|
|
|
1990
1990
|
AssignmentExpression: (node) => {
|
|
1991
1991
|
const target = observerOn(node.left);
|
|
1992
1992
|
if (target !== void 0 && isDouble(context, node.right)) {
|
|
1993
|
-
|
|
1993
|
+
report2(node, target, "preferObserverStub");
|
|
1994
1994
|
}
|
|
1995
1995
|
},
|
|
1996
1996
|
CallExpression: (node) => {
|
|
@@ -1998,13 +1998,13 @@ var preferObserverStub = defineRule({
|
|
|
1998
1998
|
const stubbed = observerNamed(node.arguments[0]);
|
|
1999
1999
|
const value = node.arguments[1];
|
|
2000
2000
|
if (stubbed !== void 0 && value !== void 0 && isDouble(context, value)) {
|
|
2001
|
-
|
|
2001
|
+
report2(node, stubbed, "preferObserverStubOverRunner");
|
|
2002
2002
|
}
|
|
2003
2003
|
return;
|
|
2004
2004
|
}
|
|
2005
2005
|
const spied = isRunnerCall(node, SPY_ON3) ? spiedObserver(node) : void 0;
|
|
2006
2006
|
if (spied !== void 0) {
|
|
2007
|
-
|
|
2007
|
+
report2(node, spied, "preferObserverStubOverRunner");
|
|
2008
2008
|
}
|
|
2009
2009
|
}
|
|
2010
2010
|
};
|
|
@@ -2387,7 +2387,7 @@ var preferNativeSpyApi = defineRule({
|
|
|
2387
2387
|
preferNativeSpyApi: "`{{from}}` is the compatibility layer speaking; `{{to}}` is what this library calls the same thing, and the only spelling the rest of its documentation uses. The layer is a bridge \u2014 it exists so a `jasmine-auto-spies` suite runs before it is rewritten \u2014 so this is worth doing once the suite is green and not before: rewrite, then delete the `vitest-auto-spy/jasmine` import. `npx vitest-auto-spy codemod --from jasmine` does the whole suite in one pass. The edit is applied only where the receiver came out of one of this library\u2019s factories; anywhere else the same rewrite is offered as a suggestion, because a `.calls` on somebody else\u2019s object is somebody else\u2019s method."
|
|
2388
2388
|
},
|
|
2389
2389
|
create: (context) => {
|
|
2390
|
-
const
|
|
2390
|
+
const report2 = (rewrite2, spy) => {
|
|
2391
2391
|
if (!rewrite2) {
|
|
2392
2392
|
return;
|
|
2393
2393
|
}
|
|
@@ -2398,8 +2398,8 @@ var preferNativeSpyApi = defineRule({
|
|
|
2398
2398
|
);
|
|
2399
2399
|
};
|
|
2400
2400
|
return {
|
|
2401
|
-
'MemberExpression[property.name="and"]': (node) =>
|
|
2402
|
-
'MemberExpression[property.name="calls"]': (node) =>
|
|
2401
|
+
'MemberExpression[property.name="and"]': (node) => report2(andRewrite(context, node), node.object),
|
|
2402
|
+
'MemberExpression[property.name="calls"]': (node) => report2(callsRewrite(context, node), node.object)
|
|
2403
2403
|
};
|
|
2404
2404
|
}
|
|
2405
2405
|
});
|
|
@@ -2416,15 +2416,15 @@ var noJasmineGlobals = defineRule({
|
|
|
2416
2416
|
},
|
|
2417
2417
|
create: (context) => ({
|
|
2418
2418
|
'MemberExpression[object.name="jasmine"]': (node) => {
|
|
2419
|
-
const
|
|
2420
|
-
if (
|
|
2421
|
-
context.report({ node, ...
|
|
2419
|
+
const report2 = jasmineMemberReport(context, node);
|
|
2420
|
+
if (report2) {
|
|
2421
|
+
context.report({ node, ...report2 });
|
|
2422
2422
|
}
|
|
2423
2423
|
},
|
|
2424
2424
|
[BARE_GLOBAL_SELECTOR]: (node) => {
|
|
2425
|
-
const
|
|
2426
|
-
if (
|
|
2427
|
-
context.report({ node, ...
|
|
2425
|
+
const report2 = bareGlobalReport(context, node);
|
|
2426
|
+
if (report2) {
|
|
2427
|
+
context.report({ node, ...report2 });
|
|
2428
2428
|
}
|
|
2429
2429
|
},
|
|
2430
2430
|
'MemberExpression[property.name="withContext"]': (node) => {
|
|
@@ -2736,12 +2736,12 @@ function describeReport(context, finding, dead) {
|
|
|
2736
2736
|
const { fn, hook, node, placement, reset } = finding;
|
|
2737
2737
|
const target = placement.statement ?? hookStatement(fn);
|
|
2738
2738
|
const data = { flag: FLAG[reset.kind], hook };
|
|
2739
|
-
const
|
|
2739
|
+
const report2 = { data, messageId: dead ? "deadReset" : "repeatedReset", node };
|
|
2740
2740
|
if (target === void 0) {
|
|
2741
|
-
return
|
|
2741
|
+
return report2;
|
|
2742
2742
|
}
|
|
2743
2743
|
const desc = placement.statement ? "Delete this reset" : `Delete this reset, and the ${hook} it is the whole of`;
|
|
2744
|
-
return dead ? { ...
|
|
2744
|
+
return dead ? { ...report2, fix: removal2(context, target) } : { ...report2, suggest: [{ desc, fix: removal2(context, target) }] };
|
|
2745
2745
|
}
|
|
2746
2746
|
var noRedundantMockReset = defineRule({
|
|
2747
2747
|
anchor: "-a-double-more-than-one-spec-uses",
|
|
@@ -3041,12 +3041,12 @@ var noOverriddenProvider = defineRule({
|
|
|
3041
3041
|
return {
|
|
3042
3042
|
ArrayExpression: (node) => {
|
|
3043
3043
|
overriddenProviders(context, node).forEach(({ element, token, kind, survivor }) => {
|
|
3044
|
-
const
|
|
3044
|
+
const report2 = {
|
|
3045
3045
|
node: element,
|
|
3046
3046
|
messageId: OVERRIDE_MESSAGES[kind],
|
|
3047
3047
|
data: { token, line: String(survivor.loc.start.line) }
|
|
3048
3048
|
};
|
|
3049
|
-
context.report(kind === "duplicate" ? { ...
|
|
3049
|
+
context.report(kind === "duplicate" ? { ...report2, suggest: [deleteProviderSuggestion(context, element, token)] } : report2);
|
|
3050
3050
|
});
|
|
3051
3051
|
if (isTestingModuleProviders(node)) {
|
|
3052
3052
|
registrations.push(...survivingRegistrations(context, node));
|
|
@@ -3745,8 +3745,8 @@ var noReflectMemberAccess = defineRule({
|
|
|
3745
3745
|
if (writes && isDouble2(context, target)) {
|
|
3746
3746
|
const data = { key, target: context.sourceCode.getText(target) };
|
|
3747
3747
|
const suggestion = restoreSuggestion(context, node);
|
|
3748
|
-
const
|
|
3749
|
-
context.report(suggestion ? { ...
|
|
3748
|
+
const report2 = { node, messageId: "reflectSetOnDouble", data };
|
|
3749
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
3750
3750
|
return;
|
|
3751
3751
|
}
|
|
3752
3752
|
const messageId = writes ? isFixtureLiteral(context, target) ? "reflectSetOnFixture" : "reflectSet" : "reflectGet";
|
|
@@ -4070,8 +4070,8 @@ var preferSetInputs = defineRule({
|
|
|
4070
4070
|
}
|
|
4071
4071
|
const callback = enclosingFunction(node);
|
|
4072
4072
|
const rewritable = callback && isHookCallback(callback) && bindingState(context.sourceCode.getScope(node), HELPER4) !== "taken" ? callback : void 0;
|
|
4073
|
-
const
|
|
4074
|
-
context.report(rewritable ? { ...
|
|
4073
|
+
const report2 = { node, messageId: "preferSetInputs" };
|
|
4074
|
+
context.report(rewritable ? { ...report2, suggest: [{ desc: EDIT, fix: rewrite(context, run, rewritable) }] } : report2);
|
|
4075
4075
|
}
|
|
4076
4076
|
};
|
|
4077
4077
|
}
|
|
@@ -4275,9 +4275,9 @@ function reportBlock(context, block, names) {
|
|
|
4275
4275
|
subject: context.sourceCode.getText(subject)
|
|
4276
4276
|
};
|
|
4277
4277
|
const statement = node.parent;
|
|
4278
|
-
const
|
|
4278
|
+
const report2 = { data, messageId: "noRedundantSmokeTest", node };
|
|
4279
4279
|
context.report(
|
|
4280
|
-
isExpressionStatement(statement) ? { ...
|
|
4280
|
+
isExpressionStatement(statement) ? { ...report2, suggest: [{ desc: "Remove this test", fix: removal3(context, statement) }] } : report2
|
|
4281
4281
|
);
|
|
4282
4282
|
});
|
|
4283
4283
|
}
|
|
@@ -4330,6 +4330,227 @@ var noRedundantSmokeTest = defineRule({
|
|
|
4330
4330
|
}
|
|
4331
4331
|
});
|
|
4332
4332
|
|
|
4333
|
+
// src/lib/eslint/spy-on-own-method.ts
|
|
4334
|
+
var FACTORY = "createSpyFromInstance";
|
|
4335
|
+
var OWN = "spyOnOwnMethod";
|
|
4336
|
+
var VOID = "spyOnVoidMethod";
|
|
4337
|
+
var CORE_ENTRIES = new Set(
|
|
4338
|
+
["", "/bun", "/bun-angular", "/node", "/rstest", "/react", "/vue", "/svelte"].map((entry) => `${PACKAGE}${entry}`)
|
|
4339
|
+
);
|
|
4340
|
+
var METHOD_NAME = /^[$A-Z_a-z][\w$]*$/;
|
|
4341
|
+
var EVENT_CONSTRUCTOR = /^[A-Za-z]*Event$/;
|
|
4342
|
+
var DOM_GLOBALS = /* @__PURE__ */ new Set(["document", "window"]);
|
|
4343
|
+
var NODE_FACTORIES = /* @__PURE__ */ new Set(["createElement", "createElementNS", "createEvent", "getElementById", "querySelector"]);
|
|
4344
|
+
var NODE_MEMBERS = /* @__PURE__ */ new Set(["body", "documentElement"]);
|
|
4345
|
+
var FIXTURE_NAME2 = /fixture$/i;
|
|
4346
|
+
var DECLARATION_KINDS = /* @__PURE__ */ new Set(["const", "let", "var"]);
|
|
4347
|
+
function isObjectPattern(node) {
|
|
4348
|
+
return node.type === "ObjectPattern";
|
|
4349
|
+
}
|
|
4350
|
+
function isUnwrapped(node) {
|
|
4351
|
+
return isCast(node) || node.type === "TSNonNullExpression";
|
|
4352
|
+
}
|
|
4353
|
+
function unwrap(node) {
|
|
4354
|
+
return isUnwrapped(node) ? node.expression : node;
|
|
4355
|
+
}
|
|
4356
|
+
function methodString(node) {
|
|
4357
|
+
const value = node.type === "Literal" ? Reflect.get(node, "value") : void 0;
|
|
4358
|
+
return typeof value === "string" && METHOD_NAME.test(value) ? value : void 0;
|
|
4359
|
+
}
|
|
4360
|
+
function listedMethod(value) {
|
|
4361
|
+
const list = unwrap(value);
|
|
4362
|
+
const elements = isArrayExpression(list) ? list.elements : [];
|
|
4363
|
+
const names = elements.filter((element) => element !== null).map(methodString);
|
|
4364
|
+
return elements.length === 1 ? names[0] : void 0;
|
|
4365
|
+
}
|
|
4366
|
+
function isUndefined(node) {
|
|
4367
|
+
return isIdentifier(node) && node.name === "undefined";
|
|
4368
|
+
}
|
|
4369
|
+
function seededVoid(value) {
|
|
4370
|
+
const entries2 = isObjectExpression(value) ? value.properties : [];
|
|
4371
|
+
const seeds = entries2.filter((entry) => propertyName(entry) !== void 0 && isUndefined(propertyValue(entry)));
|
|
4372
|
+
const names = seeds.map((entry) => String(propertyName(entry))).filter((name) => METHOD_NAME.test(name));
|
|
4373
|
+
return entries2.length === 1 ? names[0] : void 0;
|
|
4374
|
+
}
|
|
4375
|
+
function seedOf(options) {
|
|
4376
|
+
if (!isObjectExpression(options)) {
|
|
4377
|
+
return void 0;
|
|
4378
|
+
}
|
|
4379
|
+
const keys = options.properties.map(propertyName);
|
|
4380
|
+
const listed = findProperty(options, "onlyMethodsToSpyOn");
|
|
4381
|
+
const method = listed ? listedMethod(listed.value) : void 0;
|
|
4382
|
+
const returns = findProperty(options, "returns");
|
|
4383
|
+
const seeded = returns ? seededVoid(returns.value) : void 0;
|
|
4384
|
+
const passthrough = findProperty(options, "passthrough")?.value;
|
|
4385
|
+
if (keys.length === 1 && seeded !== void 0) {
|
|
4386
|
+
return { helper: VOID, method: seeded, exact: false };
|
|
4387
|
+
}
|
|
4388
|
+
if (keys.length !== 2 || method === void 0) {
|
|
4389
|
+
return void 0;
|
|
4390
|
+
}
|
|
4391
|
+
if (passthrough?.type === "Literal" && Reflect.get(passthrough, "value") === true) {
|
|
4392
|
+
return { helper: OWN, method, exact: true };
|
|
4393
|
+
}
|
|
4394
|
+
return seeded === method ? { helper: VOID, method, exact: true } : void 0;
|
|
4395
|
+
}
|
|
4396
|
+
function readsMethod(member, method) {
|
|
4397
|
+
const named = member.computed ? methodString(member.property) : memberName(member);
|
|
4398
|
+
const { parent } = member;
|
|
4399
|
+
return named === method && !(isAssignmentExpression(parent) && parent.left === member);
|
|
4400
|
+
}
|
|
4401
|
+
function isRealObject(scope, node, follow) {
|
|
4402
|
+
const value = unwrap(node);
|
|
4403
|
+
if (isIdentifier(value)) {
|
|
4404
|
+
const bound = follow ? boundValueOf(scope, value) : void 0;
|
|
4405
|
+
return bound ? isRealObject(scope, bound, false) : DOM_GLOBALS.has(value.name) && bindingState(scope, value.name) === "free";
|
|
4406
|
+
}
|
|
4407
|
+
if (isNewExpression(value)) {
|
|
4408
|
+
return isIdentifier(value.callee) && EVENT_CONSTRUCTOR.test(value.callee.name) && bindingState(scope, value.callee.name) === "free";
|
|
4409
|
+
}
|
|
4410
|
+
if (isCallExpression(value)) {
|
|
4411
|
+
return isMemberExpression(value.callee) && NODE_FACTORIES.has(String(memberName(value.callee))) && isRealObject(scope, value.callee.object, follow);
|
|
4412
|
+
}
|
|
4413
|
+
const name = memberName(value);
|
|
4414
|
+
if (isMemberExpression(value) && name === "nativeElement") {
|
|
4415
|
+
return isRenderedBy(value.object);
|
|
4416
|
+
}
|
|
4417
|
+
return isMemberExpression(value) && NODE_MEMBERS.has(String(name)) && isRealObject(scope, value.object, follow);
|
|
4418
|
+
}
|
|
4419
|
+
function isRenderedBy(node) {
|
|
4420
|
+
if (isIdentifier(node)) {
|
|
4421
|
+
return FIXTURE_NAME2.test(node.name);
|
|
4422
|
+
}
|
|
4423
|
+
return memberName(node) === "debugElement" || isCallExpression(node) && memberName(node.callee) === "query";
|
|
4424
|
+
}
|
|
4425
|
+
function annotationEdit(context, id, method) {
|
|
4426
|
+
const annotation = id.typeAnnotation?.typeAnnotation;
|
|
4427
|
+
if (!annotation) {
|
|
4428
|
+
return { edits: [], typed: false, blocked: false };
|
|
4429
|
+
}
|
|
4430
|
+
const { sourceCode } = context;
|
|
4431
|
+
const spy = isTypeReference(annotation) && sourceCode.getText(annotation.typeName) === "Spy" && annotation.typeArguments?.params.length === 1;
|
|
4432
|
+
const edits = spy ? [[annotation, `${sourceCode.getText(annotation)}['${method}']`]] : [];
|
|
4433
|
+
return { edits, typed: true, blocked: !spy };
|
|
4434
|
+
}
|
|
4435
|
+
function variableRewrite(context, call, id, method) {
|
|
4436
|
+
const variable = findBinding(context.sourceCode.getScope(call), id.name);
|
|
4437
|
+
const declarator = variable?.defs.map((definition) => definition.node).find(isVariableDeclarator);
|
|
4438
|
+
if (!variable || !declarator || !isIdentifier(declarator.id)) {
|
|
4439
|
+
return void 0;
|
|
4440
|
+
}
|
|
4441
|
+
const declaration = declarator.parent;
|
|
4442
|
+
const writes = variable.references.filter((reference) => reference.writeExpr);
|
|
4443
|
+
const members = variable.references.flatMap((reference) => reference.writeExpr ? [] : [reference.identifier.parent]);
|
|
4444
|
+
const reads = members.filter((member) => isMemberExpression(member) && readsMethod(member, method));
|
|
4445
|
+
if (writes.length !== 1 || !writes.every((write) => write.writeExpr === call) || reads.length !== members.length || !DECLARATION_KINDS.has(String(Reflect.get(declaration, "kind"))) || declaration.parent.type === "ExportNamedDeclaration") {
|
|
4446
|
+
return void 0;
|
|
4447
|
+
}
|
|
4448
|
+
const annotation = annotationEdit(context, declarator.id, method);
|
|
4449
|
+
return { ...annotation, target: call, edits: [...annotation.edits, ...reads.map((read) => [read, id.name])] };
|
|
4450
|
+
}
|
|
4451
|
+
function patternRewrite(call, pattern, method) {
|
|
4452
|
+
const [property] = pattern.properties;
|
|
4453
|
+
const local = property && propertyName(property) === method ? propertyValue(property) : void 0;
|
|
4454
|
+
if (pattern.properties.length !== 1 || !local || !isIdentifier(local)) {
|
|
4455
|
+
return void 0;
|
|
4456
|
+
}
|
|
4457
|
+
return { target: call, edits: [[pattern, local.name]], typed: false, blocked: pattern.typeAnnotation !== void 0 };
|
|
4458
|
+
}
|
|
4459
|
+
function rewriteOf(context, call, method) {
|
|
4460
|
+
const { parent } = call;
|
|
4461
|
+
if (isExpressionStatement(parent)) {
|
|
4462
|
+
return { target: call, edits: [], typed: false, blocked: false };
|
|
4463
|
+
}
|
|
4464
|
+
if (isMemberExpression(parent) && parent.object === call) {
|
|
4465
|
+
return readsMethod(parent, method) ? { target: parent, edits: [], typed: false, blocked: false } : void 0;
|
|
4466
|
+
}
|
|
4467
|
+
if (isVariableDeclarator(parent) && isObjectPattern(parent.id)) {
|
|
4468
|
+
return patternRewrite(call, parent.id, method);
|
|
4469
|
+
}
|
|
4470
|
+
if (isVariableDeclarator(parent) && isIdentifier(parent.id)) {
|
|
4471
|
+
return variableRewrite(context, call, parent.id, method);
|
|
4472
|
+
}
|
|
4473
|
+
return isAssignmentExpression(parent) && isIdentifier(parent.left) && isExpressionStatement(parent.parent) ? variableRewrite(context, call, parent.left, method) : void 0;
|
|
4474
|
+
}
|
|
4475
|
+
function swapSpecifier(context, fixer, specifier, helper) {
|
|
4476
|
+
const { specifiers } = specifier.parent;
|
|
4477
|
+
const others = specifiers.filter((candidate) => candidate !== specifier).map((candidate) => context.sourceCode.getText(candidate));
|
|
4478
|
+
const keys = others.map((text) => text.toLowerCase());
|
|
4479
|
+
const ordered = keys.every((key, index) => keys.slice(0, index).every((before2) => before2 <= key));
|
|
4480
|
+
if (!ordered || !specifiers.every(isNamedImportSpecifier)) {
|
|
4481
|
+
return fixer.replaceText(specifier, helper);
|
|
4482
|
+
}
|
|
4483
|
+
others.splice(keys.filter((key) => key < helper.toLowerCase()).length, 0, helper);
|
|
4484
|
+
return fixer.replaceTextRange(
|
|
4485
|
+
[Math.min(...specifiers.map((candidate) => candidate.range[0])), Math.max(...specifiers.map((candidate) => candidate.range[1]))],
|
|
4486
|
+
others.join(", ")
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
function importEdits(context, fixer, call, helper, factory) {
|
|
4490
|
+
const specifier = factory ? importSpecifierOf(factory) : void 0;
|
|
4491
|
+
const module = specifier ? String(specifier.parent.source.value) : PACKAGE;
|
|
4492
|
+
const home = CORE_ENTRIES.has(module) ? module : PACKAGE;
|
|
4493
|
+
const orphaned = factory?.references.length === 1;
|
|
4494
|
+
const drop = orphaned ? dropNamedImport(context.sourceCode, fixer, factory) : void 0;
|
|
4495
|
+
const dropped = drop ? [drop] : [];
|
|
4496
|
+
if (bindingState(context.sourceCode.getScope(call), helper) === "imported") {
|
|
4497
|
+
return dropped;
|
|
4498
|
+
}
|
|
4499
|
+
if (specifier && orphaned && home === module) {
|
|
4500
|
+
return [swapSpecifier(context, fixer, specifier, helper)];
|
|
4501
|
+
}
|
|
4502
|
+
return [importNamed(fixer, call, helper, home), ...dropped];
|
|
4503
|
+
}
|
|
4504
|
+
var MESSAGES = {
|
|
4505
|
+
ownMethod: "`createSpyFromInstance(\u2026, { onlyMethodsToSpyOn: ['{{method}}'], passthrough: true })`, read for `{{method}}` alone, says nothing `spyOnOwnMethod(target, '{{method}}')` does not: one method recorded and calling through, the rest of the object real, the spy handed back directly. Name the method once.",
|
|
4506
|
+
voidExact: "`createSpyFromInstance(\u2026, { onlyMethodsToSpyOn: ['{{method}}'], returns: { {{method}}: undefined } })` names `{{method}}` twice to say what `spyOnVoidMethod(target, '{{method}}')` says once: one void method recorded, answering `undefined`, the rest real.",
|
|
4507
|
+
voidOnRealObject: "This seeds `{{method}}` of a real event or element with `undefined`, and discovery spies every other method of it too \u2014 on a DOM node that reaches the engine's own internals. `spyOnVoidMethod(target, '{{method}}')` patches `{{method}}` alone. A suggestion, not a fix: check the test does not rely on another method being stubbed."
|
|
4508
|
+
};
|
|
4509
|
+
function report(context, call, target, seed, rewrite2) {
|
|
4510
|
+
const scope = context.sourceCode.getScope(call);
|
|
4511
|
+
const text = `${seed.helper}(${context.sourceCode.getText(target)}, '${seed.method}')`;
|
|
4512
|
+
const factory = findBinding(scope, FACTORY);
|
|
4513
|
+
const fix = (fixer) => [
|
|
4514
|
+
fixer.replaceText(rewrite2.target, text),
|
|
4515
|
+
...rewrite2.edits.map(([node, replacement]) => fixer.replaceText(node, replacement)),
|
|
4516
|
+
...importEdits(context, fixer, call, seed.helper, factory)
|
|
4517
|
+
];
|
|
4518
|
+
const fixable = !rewrite2.blocked && !call.typeArguments && bindingState(scope, seed.helper) !== "taken";
|
|
4519
|
+
const safe = seed.exact && !rewrite2.typed;
|
|
4520
|
+
const messageId = seed.helper === OWN ? "ownMethod" : seed.exact ? "voidExact" : "voidOnRealObject";
|
|
4521
|
+
context.report({
|
|
4522
|
+
node: call,
|
|
4523
|
+
messageId,
|
|
4524
|
+
data: { method: seed.method },
|
|
4525
|
+
...fixable && safe ? { fix } : {},
|
|
4526
|
+
...fixable && !safe ? { suggest: [{ desc: `Replace with ${text}`, fix }] } : {}
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
var preferSpyOnOwnMethod = defineRule({
|
|
4530
|
+
anchor: "-an-object-the-test-already-holds",
|
|
4531
|
+
description: "Spy one method of a real object with spyOnOwnMethod / spyOnVoidMethod instead of a one-method createSpyFromInstance",
|
|
4532
|
+
messages: MESSAGES,
|
|
4533
|
+
fixable: true,
|
|
4534
|
+
hasSuggestions: true,
|
|
4535
|
+
create: (context) => ({
|
|
4536
|
+
CallExpression: (call) => {
|
|
4537
|
+
const [target, options, ...rest] = call.arguments;
|
|
4538
|
+
if (!isIdentifier(call.callee) || call.callee.name !== FACTORY || !target || !options || rest.length > 0) {
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
const scope = context.sourceCode.getScope(call);
|
|
4542
|
+
const seed = seedOf(options);
|
|
4543
|
+
if (!seed || bindingState(scope, FACTORY) === "taken" || !seed.exact && !isRealObject(scope, target, true)) {
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4546
|
+
const rewrite2 = rewriteOf(context, call, seed.method);
|
|
4547
|
+
if (rewrite2) {
|
|
4548
|
+
report(context, call, target, seed, rewrite2);
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
})
|
|
4552
|
+
});
|
|
4553
|
+
|
|
4333
4554
|
// src/lib/eslint/stub-response.ts
|
|
4334
4555
|
var RESPONSE = "Response";
|
|
4335
4556
|
var TYPE_ARGUMENT_DOUBLES = /* @__PURE__ */ new Set(["createAutoMock", "createMock"]);
|
|
@@ -4781,8 +5002,8 @@ var noExpectInSubscribe = defineRule({
|
|
|
4781
5002
|
"Program:exit": () => {
|
|
4782
5003
|
assertions.forEach(({ count, repair }, subscribeCall) => {
|
|
4783
5004
|
const suggestion = rewrites.get(subscribeCall);
|
|
4784
|
-
const
|
|
4785
|
-
context.report(suggestion ? { ...
|
|
5005
|
+
const report2 = { node: subscribeCall, messageId: repair, data: { count: String(count) } };
|
|
5006
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
4786
5007
|
});
|
|
4787
5008
|
}
|
|
4788
5009
|
};
|
|
@@ -4958,8 +5179,8 @@ var noImportTimeSpread = defineRule({
|
|
|
4958
5179
|
}
|
|
4959
5180
|
const suggestion = lazyValueSuggestion(context, node);
|
|
4960
5181
|
const messageId = spreadFailureMode(node) === "object" ? "noImportTimeSpreadObject" : "noImportTimeSpread";
|
|
4961
|
-
const
|
|
4962
|
-
context.report(suggestion ? { ...
|
|
5182
|
+
const report2 = { node, messageId, data: { name: imported.name } };
|
|
5183
|
+
context.report(suggestion ? { ...report2, suggest: [suggestion] } : report2);
|
|
4963
5184
|
}
|
|
4964
5185
|
})
|
|
4965
5186
|
});
|
|
@@ -5031,6 +5252,7 @@ var rules = {
|
|
|
5031
5252
|
"no-redundant-smoke-test": noRedundantSmokeTest,
|
|
5032
5253
|
"no-self-called-spy": noSelfCalledSpy,
|
|
5033
5254
|
"prefer-set-inputs": preferSetInputs,
|
|
5255
|
+
"prefer-spy-on-own-method": preferSpyOnOwnMethod,
|
|
5034
5256
|
...jasmineRules
|
|
5035
5257
|
};
|
|
5036
5258
|
|
|
@@ -5081,6 +5303,7 @@ var recommendedRules = {
|
|
|
5081
5303
|
[`${PLUGIN_NAME}/no-redundant-mock-reset`]: "error",
|
|
5082
5304
|
[`${PLUGIN_NAME}/no-unasserted-argument`]: "warn",
|
|
5083
5305
|
[`${PLUGIN_NAME}/prefer-set-inputs`]: "warn",
|
|
5306
|
+
[`${PLUGIN_NAME}/prefer-spy-on-own-method`]: "warn",
|
|
5084
5307
|
[`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
|
|
5085
5308
|
[`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
|
|
5086
5309
|
[`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
|
package/dist/setup.d.ts
CHANGED
|
@@ -575,16 +575,24 @@ interface SchedulerHost {
|
|
|
575
575
|
/** One outstanding callback, and where it came from — what {@link describeStrayTimers} hands back. */
|
|
576
576
|
interface StrayTimer {
|
|
577
577
|
readonly kind: 'frame' | 'interval' | 'timeout';
|
|
578
|
+
/** The delay a timeout or interval was scheduled with, in milliseconds; `undefined` for a frame. */
|
|
579
|
+
readonly delay?: number;
|
|
578
580
|
/** The spec file that was running when it was scheduled; `undefined` outside a Vitest file. */
|
|
579
581
|
readonly file: string | undefined;
|
|
580
|
-
/**
|
|
582
|
+
/**
|
|
583
|
+
* Up to five frames of the scheduling call, starting below the tracking wrapper: those outside
|
|
584
|
+
* dependencies first, then — when the call came from dependencies only — theirs, never this package's.
|
|
585
|
+
*/
|
|
581
586
|
readonly frames: readonly string[];
|
|
582
587
|
}
|
|
583
588
|
/** The stack is taken now and formatted only if the callback turns out to be a stray. */
|
|
584
589
|
interface Origin {
|
|
585
590
|
readonly kind: StrayTimer['kind'];
|
|
591
|
+
readonly delay: number | undefined;
|
|
586
592
|
readonly file: unknown;
|
|
587
|
-
readonly trace:
|
|
593
|
+
readonly trace: {
|
|
594
|
+
readonly stack?: string;
|
|
595
|
+
};
|
|
588
596
|
}
|
|
589
597
|
/** Undo the wrapping installed by {@link trackStrayTimers}, cancelling anything still outstanding. */
|
|
590
598
|
type StopTrackingTimers = () => void;
|
package/dist/setup.js
CHANGED
|
@@ -1102,12 +1102,15 @@ function currentSpecFile() {
|
|
|
1102
1102
|
|
|
1103
1103
|
// src/lib/stack-frames.ts
|
|
1104
1104
|
var FOREIGN_FRAME = /node_modules|node:internal|\(node:/;
|
|
1105
|
+
var LIBRARY_FRAME = /[/\\]vitest-auto-spy[/\\]dist[/\\]|[/\\]\.vite[/\\]deps[^/\\]*[/\\]vitest-auto-spy[^/\\]*\.js/;
|
|
1105
1106
|
function stackFrames(stack) {
|
|
1106
1107
|
return String(stack).split("\n").filter((line) => /^\s*at\s/.test(line)).map((line) => line.trim());
|
|
1107
1108
|
}
|
|
1108
1109
|
function ownFrames(frames, limit) {
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1110
|
+
const outside = frames.filter((frame) => !LIBRARY_FRAME.test(frame));
|
|
1111
|
+
const own = outside.filter((frame) => !FOREIGN_FRAME.test(frame));
|
|
1112
|
+
const shown = [own, outside].find((candidates) => candidates.length > 0) ?? frames;
|
|
1113
|
+
return shown.slice(0, limit);
|
|
1111
1114
|
}
|
|
1112
1115
|
|
|
1113
1116
|
// src/lib/stray-listeners.ts
|
|
@@ -2357,7 +2360,8 @@ function defaultHost2() {
|
|
|
2357
2360
|
function defineScheduler(host, name, value) {
|
|
2358
2361
|
Object.defineProperty(host, name, { configurable: true, writable: true, value });
|
|
2359
2362
|
}
|
|
2360
|
-
function scheduleTracked(schedule, callback, handles,
|
|
2363
|
+
function scheduleTracked(schedule, callback, handles, origin, numeric) {
|
|
2364
|
+
const { kind } = origin;
|
|
2361
2365
|
const oneShot = kind !== "interval";
|
|
2362
2366
|
let handle;
|
|
2363
2367
|
const forgetting = (...args) => {
|
|
@@ -2369,19 +2373,32 @@ function scheduleTracked(schedule, callback, handles, kind, numeric) {
|
|
|
2369
2373
|
callback(...args);
|
|
2370
2374
|
};
|
|
2371
2375
|
handle = schedule(oneShot && typeof callback === "function" ? forgetting : callback);
|
|
2372
|
-
handles.set(handle, captureOrigin2(
|
|
2376
|
+
handles.set(handle, captureOrigin2(origin));
|
|
2373
2377
|
const id = numeric && numericIdOf(handle);
|
|
2374
2378
|
if (numeric && id !== void 0) {
|
|
2375
2379
|
numeric.set(id, handle);
|
|
2376
2380
|
}
|
|
2377
2381
|
return handle;
|
|
2378
2382
|
}
|
|
2379
|
-
|
|
2383
|
+
var ORIGIN_DEPTH = 40;
|
|
2384
|
+
function captureOrigin2({ kind, delay, boundary }) {
|
|
2380
2385
|
const limit = Error.stackTraceLimit;
|
|
2381
|
-
Error.stackTraceLimit =
|
|
2382
|
-
const trace =
|
|
2386
|
+
Error.stackTraceLimit = ORIGIN_DEPTH;
|
|
2387
|
+
const trace = captureTrace(boundary);
|
|
2383
2388
|
Error.stackTraceLimit = limit;
|
|
2384
|
-
return { kind, file: currentSpecFile(), trace };
|
|
2389
|
+
return { kind, delay, file: currentSpecFile(), trace };
|
|
2390
|
+
}
|
|
2391
|
+
function captureTrace(boundary) {
|
|
2392
|
+
if (typeof Error.captureStackTrace !== "function") {
|
|
2393
|
+
return new Error();
|
|
2394
|
+
}
|
|
2395
|
+
const holder = {};
|
|
2396
|
+
Error.captureStackTrace(holder, boundary);
|
|
2397
|
+
return holder;
|
|
2398
|
+
}
|
|
2399
|
+
function delayOf(value) {
|
|
2400
|
+
const delay = Number(value);
|
|
2401
|
+
return Number.isFinite(delay) && delay > 0 ? delay : 0;
|
|
2385
2402
|
}
|
|
2386
2403
|
function numericIdOf(handle) {
|
|
2387
2404
|
return typeof handle === "object" && handle !== null && Symbol.toPrimitive in handle ? Number(handle) : void 0;
|
|
@@ -2415,7 +2432,8 @@ function wrapTimerScheduler(host, name, sets) {
|
|
|
2415
2432
|
sets.opaque.add(handle);
|
|
2416
2433
|
return handle;
|
|
2417
2434
|
}
|
|
2418
|
-
|
|
2435
|
+
const origin = { kind, delay: delayOf(rest[0]), boundary: wrapper };
|
|
2436
|
+
return scheduleTracked((tracked) => original(tracked, ...rest), callback, sets.handles, origin, sets.numeric);
|
|
2419
2437
|
});
|
|
2420
2438
|
if (promisified !== void 0) {
|
|
2421
2439
|
Object.defineProperty(wrapper, PROMISIFY_CUSTOM, { configurable: true, value: promisified });
|
|
@@ -2441,7 +2459,7 @@ function wrapFrameScheduler(host, frames, pause) {
|
|
|
2441
2459
|
return () => void 0;
|
|
2442
2460
|
}
|
|
2443
2461
|
const request = defineHelper(
|
|
2444
|
-
(callback) => pause.paused ? original(callback) : scheduleTracked((tracked) => original(tracked), callback, frames, "frame")
|
|
2462
|
+
(callback) => pause.paused ? original(callback) : scheduleTracked((tracked) => original(tracked), callback, frames, { kind: "frame", delay: void 0, boundary: request })
|
|
2445
2463
|
);
|
|
2446
2464
|
markOwnedPatch(request);
|
|
2447
2465
|
host.requestAnimationFrame = request;
|
|
@@ -2535,9 +2553,10 @@ function pendingHandles(tracked) {
|
|
|
2535
2553
|
return pending;
|
|
2536
2554
|
}
|
|
2537
2555
|
var OWN_MODULE_FRAME2 = /stray-timers\.[jt]s/;
|
|
2538
|
-
function describeOrigin({ kind, file, trace }) {
|
|
2556
|
+
function describeOrigin({ kind, delay, file, trace }) {
|
|
2539
2557
|
const frames = stackFrames(trace.stack).filter((frame) => !OWN_MODULE_FRAME2.test(frame));
|
|
2540
|
-
|
|
2558
|
+
const described = { kind, file: typeof file === "string" ? file : void 0, frames: ownFrames(frames, 5) };
|
|
2559
|
+
return delay === void 0 ? described : { ...described, delay };
|
|
2541
2560
|
}
|
|
2542
2561
|
function describeStrayTimers(host = defaultHost2()) {
|
|
2543
2562
|
const tracked = registry5().get(host);
|
|
@@ -2729,8 +2748,11 @@ function warnAboutSuppressedLeaks(cancelled, write = writeWarning, timers = [])
|
|
|
2729
2748
|
);
|
|
2730
2749
|
}
|
|
2731
2750
|
function describeTimerOrigins(timers) {
|
|
2732
|
-
const lines = timers.slice(0, 3).map(({ kind, file, frames }) =>
|
|
2733
|
-
|
|
2751
|
+
const lines = timers.slice(0, 3).map(({ kind, delay, file, frames }) => {
|
|
2752
|
+
const scheduled = delay === void 0 ? kind : `${kind} (${delay} ms)`;
|
|
2753
|
+
return `
|
|
2754
|
+
- ${scheduled} from ${file ?? "no spec file"} ${frames[0] ?? ""}`.trimEnd();
|
|
2755
|
+
});
|
|
2734
2756
|
return lines.length > 0 ? `
|
|
2735
2757
|
Scheduled at:${lines.join("")}` : "";
|
|
2736
2758
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.30.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",
|
|
@@ -167,126 +167,126 @@ it('loads', async () => {
|
|
|
167
167
|
|
|
168
168
|
## Reach for these before hand-rolling
|
|
169
169
|
|
|
170
|
-
| Situation | Helper
|
|
171
|
-
| ------------------------------------------------------------------------------------------------- |
|
|
172
|
-
| a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — writes through a writable one, `asReadonly()` included; an `input()` is refused
|
|
173
|
-
| a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading`; `value()` after `fail()` throws
|
|
174
|
-
| the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included
|
|
175
|
-
| a component or service that reads `ActivatedRoute` | `provideActivatedRoute({ params })`, then `injectActivatedRoute().setParams(…)` — `/angular-router`
|
|
176
|
-
| a guard or a class built with `new` that takes the route | `createActivatedRoute({ params })` — the same handle, no `TestBed`
|
|
177
|
-
| a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker
|
|
178
|
-
| a Nest provider whose constructor keeps changing | `createNestUnit(Target, { expose })` — built from its DI metadata
|
|
179
|
-
| asserting a resource's value _and_ status together | `registerResourceMatchers()` (`/angular/matchers`) → `toHaveResourceValue` / `toBeLoading`
|
|
180
|
-
| a callback or config object the code under test built | `captureArg<T>()` in the assertion, then read `.value`; `{ where }` records only what it accepts
|
|
181
|
-
| an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it
|
|
182
|
-
| assert calls on a real object or service but keep it working | `createSpyFromInstance(obj, { passthrough: true })` — real method runs until configured; not with `strict: true` on the same call
|
|
183
|
-
| one method of the object under test, recorded and still running | `spyOnOwnMethod(sut, 'method')` — the `onlyMethodsToSpyOn` + `passthrough` packed; the drop-in for a bare banned `vi.spyOn`
|
|
184
|
-
| a native void method a handler is meant to call (`preventDefault`, `focus`) | `spyOnVoidMethod(event, 'preventDefault')` — records the call and seeds `undefined`, so `strict` does not throw on it
|
|
185
|
-
| a `vi.mock` factory's `vi.fn()` needs `calledWith` / `resolveWith` | `adoptMock(api.load).calledWith(7).resolveWith(…)` — same object, history kept
|
|
186
|
-
| real module, one case configured | `vi.mock('x', async (orig) => moduleNamespace(await orig(), { passthrough: true }))`
|
|
187
|
-
| feeding a stubbed `fetch` | `vi.fn(async () => stubResponse({ body: data }))` from `/setup` — a real `Response`, no `as Response`; a body reads once
|
|
188
|
-
| a backend that answers the JSON literal `null` | `stubResponse({ body: null })` — the literal, not "no body"; `undefined` or an omitted `body` is the way to send none
|
|
189
|
-
| the code under test lazy-loads on a click and the spec has no promise | `await settleDynamicImport(() => import('./thing'))` — a bare `await import(…)` waits for the module, not for the handler's continuation
|
|
190
|
-
| MSW handlers not answering under `setupAutoSpy({ blockNetwork: true })` | upgrade — `blockNetwork` now leaves `fetch` to MSW's interceptor; end the handlers with `http.all('*', () => HttpResponse.error())`
|
|
191
|
-
| `mockDeep` member read by index, or an unmocked call that should fail | `mock.items[0].x = 1` makes a real array (re-read `mock.items` after); `mockDeep<T>({}, { fallbackMockImplementation: () => { throw … } })` — options are the **second** argument
|
|
192
|
-
| mocking `PrismaClient` | `mockDeep<PrismaClient>()` + `resolveWith` / `rejectWith` / `resolveWithPerCall`; `$transaction.mockImplementation((run) => run(asInstance(prisma)))`
|
|
193
|
-
| `[Function] is not a spy or a call to a spy!` inside a Storybook `play` | import `expect` from `vitest` in that story file, or `setSpyEngine('runner')` from `/setup`
|
|
194
|
-
| `vi.spyOn(Class.prototype, 'x')` → `The property "x" is not defined on the object.` | `x` is an instance field (arrow, signal) — `createSpyFromClass(C, { instanceMethodsToSpyOn: ['x'] })`
|
|
195
|
-
| a spy or a `mock*Prop` patch leaking into the next `bun test` test | `/setup` is Vitest-only — preload `afterEach(() => { restoreMockedProps(); mock.restore(); })`
|
|
196
|
-
| a `calledWith` that is not matching and you cannot see why | `explainSpy(spy, 'method')` from `/diagnostics` — configs, calls, which hit which
|
|
197
|
-
| 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`
|
|
198
|
-
| a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach` | `extendWithAutoSpies(test, { cart: CartService })` (`/angular`)
|
|
199
|
-
| an `effect()` whose trigger is now a static signal | `runEffect(effectRef)`
|
|
200
|
-
| the component constructs its own `IntersectionObserver` | `stubIntersectionObserver()` (+ `Resize` / `Mutation`) — `last.host` is the callback’s 2nd argument
|
|
201
|
-
| a green run exiting 1 with `AbortError` under happy-dom | `setupAutoSpy({ blockNetwork: true })`
|
|
202
|
-
| a real request going out of a unit run (`fetch`, XHR, `sendBeacon`) | `setupAutoSpy({ blockNetwork: true })` — `{ xhr: 'empty' }` for pings
|
|
203
|
-
| timers or frames from a previous file failing this one | `setupAutoSpy({ strayTimers: true })` — mutes `--detect-async-leaks`; blind under fake timers
|
|
204
|
-
| an assertion error in stderr, every test green and the run at 0 | `setupAutoSpy({ strayRejections: true })` — zone.js swallowed it
|
|
205
|
-
| a run getting slower the longer it goes, on `isolate: false` | `setupAutoSpy({ pruneMockRegistry: true })` — the mock registry
|
|
206
|
-
| a listener or a replaced global from a previous file answering in this one, on `isolate: false` | `setupAutoSpy({ strayListeners: true, restoreGlobals: true })`
|
|
207
|
-
| the same, piece by piece | `trackStrayListeners` / `baselineStrayListeners` / `removeStrayListeners` / `countStrayListeners` / `describeStrayListeners`; `captureGlobalBaseline` / `restoreGlobals`
|
|
208
|
-
| a storage spy still recording after `vi.restoreAllMocks()` under happy-dom | `restoreStorageSpies` — on by default; `restoreStorageSpies()` alone
|
|
209
|
-
| a stream that must emit exactly these values and complete | `await expectAllEmissions(source$)`
|
|
210
|
-
| a fixture outside its type on purpose (the `null` a backend sends) | `outOfType<T>(value)` — no cast
|
|
211
|
-
| one setup file for the Angular unit-test builder and plain Vitest | `if (!isAngularUnitTestBuilder()) TestBed.initTestEnvironment(…)`
|
|
212
|
-
| `JavaScript heap out of memory` on a suite of wide generated clients | nothing to configure — the default shares one placeholder per method name (215 B per untouched 100-method double); `lazySpies: 'proxy'` retains 19× more now
|
|
213
|
-
| `Cannot read properties of undefined (reading 'now')` | `restoreTimerGlobals` — on by default
|
|
214
|
-
| `localStorage.setItem is not a function`, or it is `undefined` | `restoreWebStorage` — on by default; Vitest's global filter, Node 25+
|
|
215
|
-
| a spy handed to an API typed against the real class | `asInstance()` / `asSpy()`
|
|
216
|
-
| the code under test does `new X()` (a global, a vendor SDK) | `mockConstructor(factory)` / `stubConstructor(obj, key, factory)`
|
|
217
|
-
| `X is not a constructor`, with a stack in production code | same — a `vi.fn(() => …)` cannot serve `new`
|
|
218
|
-
| waiting for a dynamic `import()` under fake timers | `settleDynamicImport(() => import('…'))` / `flushEventLoop()`
|
|
219
|
-
| `addEventListener(…, { signal })` throwing about `EventTarget` | `stubAbortController()`
|
|
220
|
-
| `codemod` reporting a truncated repository scan | `VITEST_AUTO_SPY_SCAN_CAP=<n>` — raises the 50 000-file cap
|
|
221
|
-
| a suite ported from Jest's `fakeTimers.enableGlobally` | `setupAutoSpy({ globalFakeTimers: true })`
|
|
222
|
-
| `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()` | `setSpyEngine('runner')` / `getSpyEngine()` — `/setup`, Vitest only
|
|
223
|
-
| a nested `describe`'s `beforeAll` landing on real timers | `setupFakeTimers(cfg, { betweenTests: true })`
|
|
224
|
-
| setup hooks applying to the first spec file of a worker only | the setup module is cached — run coverage with `--isolate`
|
|
225
|
-
| `fakeAsync` inside `test.concurrent` | `installProxyZonePatch({ scope: 'callback' })`
|
|
226
|
-
| an assertion containing a date | `mockSystemTime(iso)` — never `vi.spyOn(globalThis, 'Date')`
|
|
227
|
-
| a spec asserting on tick _order_ under a frozen clock | `useCountingClock()`
|
|
228
|
-
| a dependency declared in the component's own `providers` | `overrideComponentProvider(Cmp, Token)` — it verifies on the first fixture that the override applied
|
|
229
|
-
| a double answering `undefined` for a method nobody configured | `{ strict: true }`, or `setupAutoSpy({ strict: true })` suite-wide
|
|
230
|
-
| a strict throw caught by a `try`/`catch` or an operator, the test still green | `setupAutoSpy({ swallowedStrictCalls: 'throw' })`; provoked on purpose — `takeStrictViolations()` from `/setup`
|
|
231
|
-
| a strict double's getter answering `undefined`, or its `items$` never emitting | `setupAutoSpy({ unconfiguredReads: 'throw' })` reports it after the test; survey first with `onUnstubbedRead`
|
|
232
|
-
| an `afterEach` that exists only to reset one spy | `using spy = createSpyFromClass(X)` — `[Symbol.dispose]` runs `resetAutoSpy`, which is `vi.resetAllMocks()` for one double
|
|
233
|
-
| a dead NgModule import, dead `schemas`, an unflushed HTTP request | `enableAngularDiagnostics()` (`/angular/diagnostics`) in the setup file, after `initTestEnvironment`
|
|
234
|
-
| the spy is provided but the component resolves its own, so the test runs the real service | `enableAngularDiagnostics({ shadowedProviders })` or `assertNoShadowedProviders(Component, fixture)` — `/angular/diagnostics`
|
|
235
|
-
| "which collaborators did this actually inject?" | `trackInjections([A, TOKEN])` — providers plus the record, not `vi.mock`
|
|
236
|
-
| `NG0303` / `NG0301` / `NG0304` from an imported NgModule | `assertNgModuleScopes(Module)` — an AOT bundle stripped its scope
|
|
237
|
-
| `Cannot read properties of undefined (reading 'provide')` in `di_setup` | `assertComponentDefIntact(Cmp)` — a barrel chunk left a hole in `ɵcmp`
|
|
238
|
-
| a focus assertion failing as `expected false to deeply equal true` | `registerFocusMatchers()` + `expect(el).toHaveFocus()`
|
|
239
|
-
| a collaborator passed as an argument, then asserted on | `autoMocked<T>()` — typed `T & Spy<T>`
|
|
240
|
-
| a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN` | `stubMediaElement({ duration })`, then `media.set(el, …)`
|
|
241
|
-
| a `vi.mock()` that silently did nothing under a bundler | `assertMocked(ns, { specifier, exports })`
|
|
242
|
-
| `No "default" export is defined on the mock` | `vi.mock('x', () => moduleNamespace({ … }))` — a `default` you spell out is kept
|
|
243
|
-
| waiting for a `resource()` / an SDK to become ready | `flushEventLoopUntil(() => …, { label })` — budgeted, not tuned
|
|
244
|
-
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …` | `expect(diffByField(actual, expected)).toBeUndefined()`
|
|
245
|
-
| a stub that works only in the first test of the file | `installPerTest(() => stub…())` — or install it in `beforeEach`
|
|
246
|
-
| a library failing every other run after a `defineProperty` on DOM | `setupAutoSpy({ guardGlobals: 'throw' })` names the file — it sees an added name, not an existing one redefined
|
|
247
|
-
| 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)
|
|
248
|
-
| the same check in a suite that does not call `setupAutoSpy()` | `guardPrototypePollution('throw')` from `/setup`
|
|
249
|
-
| 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'`)
|
|
250
|
-
| the same document check without `setupAutoSpy()` | `guardDocumentPollution('throw')` from `/setup`
|
|
251
|
-
| `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`)
|
|
252
|
-
| console output a test never asserted on (a `'warn'` from this library does not count) | `setupAutoSpy({ strayConsole: 'throw' })` fails that test; absorb with `installConsoleSpies()` in `beforeEach`
|
|
253
|
-
| every guard at its strictest grade in one line | `setupAutoSpy({ preset: 'strict' })` — plus `enableAngularDiagnostics()` from `/angular/diagnostics` for Angular
|
|
254
|
-
| an `onlyMethodsToSpyOn` typo or `injectSpy` on a real instance that only warned | `setupAutoSpy({ misconfiguration: 'throw' })` throws at the call
|
|
255
|
-
| which file and call scheduled a timer that outlived its file | `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`, or `describeStrayTimers()` from `/setup`
|
|
256
|
-
| stray timers charged to a file that only writes `localStorage` (jsdom) | `withoutStrayTimerTracking(() => seed())` from `/setup` — its timers are neither counted nor cancelled
|
|
257
|
-
| `import { consoleErrorSpy }` silencing other files under `isolate: false` | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker
|
|
258
|
-
| `Cannot set base providers because it has already been called` | `setupAngularTestEnv({ zoneless, initZone, initZoneless })`
|
|
259
|
-
| a dependency behind an `InjectionToken`, with no class to spy | `provideAutoSpyForToken(TOKEN)` + `injectSpy(TOKEN)`; a chained call → `{ selfReturning: ['channel'] }` as the third argument
|
|
260
|
-
| `Expected to be running in 'ProxyZone', but it was not found` | `import 'vitest-auto-spy/zone'` (needs `globals: true`)
|
|
261
|
-
| `Property 'mockReturnValue' does not exist on type 'never'` | upgrade — the spy no longer collapses on an unreadable return type
|
|
262
|
-
| `TS2345` inside `mockReturnValue` / `mockImplementation` / `mockResolvedValue` | the stub is checked against the method's return type now — fix the stub, not the spy
|
|
263
|
-
| `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
|
|
264
|
-
| a signal-valued getter that `gettersToSpyOn` will not accept | it accepts any key now; for a signal prefer `mockSignalProp`
|
|
265
|
-
| five `asInstance(…)` in one call, found one per `tsc` run | `...asInstances(a, b, c, d, e)`
|
|
266
|
-
| `nextWith` demanding `HttpEvent<T>` on a generated client | `asSpy<Client, { overload: 'first' }>(…)` / `Overload<M, 0>`
|
|
267
|
-
| `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`
|
|
268
|
-
| a fixture that needs a nested object built by its own call | `createMock<T>({ a: { b: 1 } })` — deep partial, still exact
|
|
269
|
-
| the same 100-line model literal copied into eight specs (`TS1117`) | one `createFixtureFactory<T>(defaults)`; specs call it with what they change
|
|
270
|
-
| `'params' in link` ladders, or a cast, to pick a union branch | `narrow.byKey(link, 'params')` / `narrow.observable(x)`
|
|
271
|
-
| `{ ...modelInstance, flag: true }` losing every getter | `withOverrides(modelInstance, { flag: true })`
|
|
272
|
-
| `NG0303` / `NG0304` / silence from a directive spec | `createDirectiveHost({ template, scope: [Module] })`
|
|
273
|
-
| a hand-written `class MockChartComponent` restating a child's selector | `createComponentStub(ChartComponent)` (`/angular`) — selector, inputs, outputs read from `ɵcmp`
|
|
274
|
-
| a `TestingStorage` class for `localStorage` / `sessionStorage` | `stubWebStorage('localStorage', { items })` from `/dom-stubs` — `snapshot()` to assert
|
|
275
|
-
| a hand-written `MockWorker` whose `addEventListener` sets `onmessage` | `stubWorker({ respond })` from `/dom-stubs` — `last.emit(data)`, `last.messages`
|
|
276
|
-
| `'x' does not exist in type 'MethodReturns<{ y: any; }>'` on a generic class | spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` infers it
|
|
277
|
-
| `injectSpy(ModalRef)` reads a generic class at its constraint, not its default | `injectSpy<ModalRef<Data>>(ModalRef)` — a constructor taking `T` hides the default
|
|
278
|
-
| "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` — `counts` reports `name (×2 → ×1)`
|
|
279
|
-
| an input that has to change after the first render | `await setInputs(fixture, { … })` — aliases and `hostDirectives` inputs resolve, unknown names refused
|
|
280
|
-
| a component that navigates, or reads `router.url` | `provideRouterDouble({ url })` + `injectRouterDouble()` — `/angular-router`
|
|
281
|
-
| a component that reads `router.currentNavigation()` for `extras.state` or `trigger` | `setCurrentNavigation({ extras: { state } })` on the same handle — no `instanceMethodsToSpyOn`
|
|
282
|
-
| the sequence of router events, not just the final URL | `collectRouterEvents(router.events)` — `expect([[NavigationStart, '/checkout'], [NavigationEnd, '/checkout']])`
|
|
283
|
-
| a component that reads `location.back()`, or asserts where a redirect landed | `provideLocationDouble()` + `injectLocationDouble()` — Angular's own `SpyLocation`; without the provider, `Location` is `providedIn: 'root'` and the real one answers
|
|
284
|
-
| "did A run before B?" across three collaborators | `createLog<'drop-cache' \| 'flush'>()` — hand `log.fn('drop-cache')` to each, assert `log.result()` once
|
|
285
|
-
| a `window` or `document` behind a DI token | `provideWindowDouble(WINDOW, { screen })` / `provideDocumentDouble({ … })` — `/angular/doubles`
|
|
286
|
-
| `MAT_DIALOG_DATA` and `MatDialogRef` provided by hand | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(MatDialogRef)` + `injectMatDialogRef(Ref)` — `/angular/doubles`
|
|
287
|
-
| asserting a `computed()` did **not** recompute | `trackRecomputations(sig)` / `trackEffectRuns(ref)` — `{ count, stop() }`
|
|
288
|
-
| `form()` in a spec, or `errors()` read by hand | `createForm(model, schema)` + `registerFormMatchers()` — then `toHaveFieldErrors([…])` (`/signal-forms`)
|
|
289
|
-
| any of those doubles without a `TestBed` | `createRouterDouble` (`/angular-router`) / `createWindowDouble` / `createDocumentDouble` / `createMatDialogRef` (`/angular/doubles`)
|
|
170
|
+
| Situation | Helper |
|
|
171
|
+
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
172
|
+
| a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — writes through a writable one, `asReadonly()` included; an `input()` is refused |
|
|
173
|
+
| a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading`; `value()` after `fail()` throws |
|
|
174
|
+
| the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
|
|
175
|
+
| a component or service that reads `ActivatedRoute` | `provideActivatedRoute({ params })`, then `injectActivatedRoute().setParams(…)` — `/angular-router` |
|
|
176
|
+
| a guard or a class built with `new` that takes the route | `createActivatedRoute({ params })` — the same handle, no `TestBed` |
|
|
177
|
+
| a node:test suite whose heap grows all run | `trackNodeMocks()` — `/node`, a private MockTracker |
|
|
178
|
+
| a Nest provider whose constructor keeps changing | `createNestUnit(Target, { expose })` — built from its DI metadata |
|
|
179
|
+
| asserting a resource's value _and_ status together | `registerResourceMatchers()` (`/angular/matchers`) → `toHaveResourceValue` / `toBeLoading` |
|
|
180
|
+
| a callback or config object the code under test built | `captureArg<T>()` in the assertion, then read `.value`; `{ where }` records only what it accepts |
|
|
181
|
+
| an object the test already holds — a real service, a client, `TestBed.inject(X)` | `createSpyFromInstance(obj, config?)` — patches it in place, `restoreSpiedInstance` undoes it |
|
|
182
|
+
| assert calls on a real object or service but keep it working | `createSpyFromInstance(obj, { passthrough: true })` — real method runs until configured; not with `strict: true` on the same call |
|
|
183
|
+
| one method of the object under test, recorded and still running | `spyOnOwnMethod(sut, 'method')` — the `onlyMethodsToSpyOn` + `passthrough` packed; the drop-in for a bare banned `vi.spyOn`; `prefer-spy-on-own-method` (`warn`, `--fix`) finds the long spelling |
|
|
184
|
+
| a native void method a handler is meant to call (`preventDefault`, `focus`) | `spyOnVoidMethod(event, 'preventDefault')` — records the call and seeds `undefined`, so `strict` does not throw on it |
|
|
185
|
+
| a `vi.mock` factory's `vi.fn()` needs `calledWith` / `resolveWith` | `adoptMock(api.load).calledWith(7).resolveWith(…)` — same object, history kept |
|
|
186
|
+
| real module, one case configured | `vi.mock('x', async (orig) => moduleNamespace(await orig(), { passthrough: true }))` |
|
|
187
|
+
| feeding a stubbed `fetch` | `vi.fn(async () => stubResponse({ body: data }))` from `/setup` — a real `Response`, no `as Response`; a body reads once |
|
|
188
|
+
| a backend that answers the JSON literal `null` | `stubResponse({ body: null })` — the literal, not "no body"; `undefined` or an omitted `body` is the way to send none |
|
|
189
|
+
| the code under test lazy-loads on a click and the spec has no promise | `await settleDynamicImport(() => import('./thing'))` — a bare `await import(…)` waits for the module, not for the handler's continuation |
|
|
190
|
+
| MSW handlers not answering under `setupAutoSpy({ blockNetwork: true })` | upgrade — `blockNetwork` now leaves `fetch` to MSW's interceptor; end the handlers with `http.all('*', () => HttpResponse.error())` |
|
|
191
|
+
| `mockDeep` member read by index, or an unmocked call that should fail | `mock.items[0].x = 1` makes a real array (re-read `mock.items` after); `mockDeep<T>({}, { fallbackMockImplementation: () => { throw … } })` — options are the **second** argument |
|
|
192
|
+
| mocking `PrismaClient` | `mockDeep<PrismaClient>()` + `resolveWith` / `rejectWith` / `resolveWithPerCall`; `$transaction.mockImplementation((run) => run(asInstance(prisma)))` |
|
|
193
|
+
| `[Function] is not a spy or a call to a spy!` inside a Storybook `play` | import `expect` from `vitest` in that story file, or `setSpyEngine('runner')` from `/setup` |
|
|
194
|
+
| `vi.spyOn(Class.prototype, 'x')` → `The property "x" is not defined on the object.` | `x` is an instance field (arrow, signal) — `createSpyFromClass(C, { instanceMethodsToSpyOn: ['x'] })` |
|
|
195
|
+
| a spy or a `mock*Prop` patch leaking into the next `bun test` test | `/setup` is Vitest-only — preload `afterEach(() => { restoreMockedProps(); mock.restore(); })` |
|
|
196
|
+
| a `calledWith` that is not matching and you cannot see why | `explainSpy(spy, 'method')` from `/diagnostics` — configs, calls, which hit which |
|
|
197
|
+
| 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` |
|
|
198
|
+
| a `TestBed` spec on Vitest 4.1+, to drop `let` + `beforeEach` | `extendWithAutoSpies(test, { cart: CartService })` (`/angular`) |
|
|
199
|
+
| an `effect()` whose trigger is now a static signal | `runEffect(effectRef)` |
|
|
200
|
+
| the component constructs its own `IntersectionObserver` | `stubIntersectionObserver()` (+ `Resize` / `Mutation`) — `last.host` is the callback’s 2nd argument |
|
|
201
|
+
| a green run exiting 1 with `AbortError` under happy-dom | `setupAutoSpy({ blockNetwork: true })` |
|
|
202
|
+
| a real request going out of a unit run (`fetch`, XHR, `sendBeacon`) | `setupAutoSpy({ blockNetwork: true })` — `{ xhr: 'empty' }` for pings |
|
|
203
|
+
| timers or frames from a previous file failing this one | `setupAutoSpy({ strayTimers: true })` — mutes `--detect-async-leaks`; blind under fake timers |
|
|
204
|
+
| an assertion error in stderr, every test green and the run at 0 | `setupAutoSpy({ strayRejections: true })` — zone.js swallowed it |
|
|
205
|
+
| a run getting slower the longer it goes, on `isolate: false` | `setupAutoSpy({ pruneMockRegistry: true })` — the mock registry |
|
|
206
|
+
| a listener or a replaced global from a previous file answering in this one, on `isolate: false` | `setupAutoSpy({ strayListeners: true, restoreGlobals: true })` |
|
|
207
|
+
| the same, piece by piece | `trackStrayListeners` / `baselineStrayListeners` / `removeStrayListeners` / `countStrayListeners` / `describeStrayListeners`; `captureGlobalBaseline` / `restoreGlobals` |
|
|
208
|
+
| a storage spy still recording after `vi.restoreAllMocks()` under happy-dom | `restoreStorageSpies` — on by default; `restoreStorageSpies()` alone |
|
|
209
|
+
| a stream that must emit exactly these values and complete | `await expectAllEmissions(source$)` |
|
|
210
|
+
| a fixture outside its type on purpose (the `null` a backend sends) | `outOfType<T>(value)` — no cast |
|
|
211
|
+
| one setup file for the Angular unit-test builder and plain Vitest | `if (!isAngularUnitTestBuilder()) TestBed.initTestEnvironment(…)` |
|
|
212
|
+
| `JavaScript heap out of memory` on a suite of wide generated clients | nothing to configure — the default shares one placeholder per method name (215 B per untouched 100-method double); `lazySpies: 'proxy'` retains 19× more now |
|
|
213
|
+
| `Cannot read properties of undefined (reading 'now')` | `restoreTimerGlobals` — on by default |
|
|
214
|
+
| `localStorage.setItem is not a function`, or it is `undefined` | `restoreWebStorage` — on by default; Vitest's global filter, Node 25+ |
|
|
215
|
+
| a spy handed to an API typed against the real class | `asInstance()` / `asSpy()` |
|
|
216
|
+
| the code under test does `new X()` (a global, a vendor SDK) | `mockConstructor(factory)` / `stubConstructor(obj, key, factory)` |
|
|
217
|
+
| `X is not a constructor`, with a stack in production code | same — a `vi.fn(() => …)` cannot serve `new` |
|
|
218
|
+
| waiting for a dynamic `import()` under fake timers | `settleDynamicImport(() => import('…'))` / `flushEventLoop()` |
|
|
219
|
+
| `addEventListener(…, { signal })` throwing about `EventTarget` | `stubAbortController()` |
|
|
220
|
+
| `codemod` reporting a truncated repository scan | `VITEST_AUTO_SPY_SCAN_CAP=<n>` — raises the 50 000-file cap |
|
|
221
|
+
| a suite ported from Jest's `fakeTimers.enableGlobally` | `setupAutoSpy({ globalFakeTimers: true })` |
|
|
222
|
+
| `toHaveBeenCalledBefore` across an auto-spy and a hand-written `vi.fn()` | `setSpyEngine('runner')` / `getSpyEngine()` — `/setup`, Vitest only |
|
|
223
|
+
| a nested `describe`'s `beforeAll` landing on real timers | `setupFakeTimers(cfg, { betweenTests: true })` |
|
|
224
|
+
| setup hooks applying to the first spec file of a worker only | the setup module is cached — run coverage with `--isolate` |
|
|
225
|
+
| `fakeAsync` inside `test.concurrent` | `installProxyZonePatch({ scope: 'callback' })` |
|
|
226
|
+
| an assertion containing a date | `mockSystemTime(iso)` — never `vi.spyOn(globalThis, 'Date')` |
|
|
227
|
+
| a spec asserting on tick _order_ under a frozen clock | `useCountingClock()` |
|
|
228
|
+
| a dependency declared in the component's own `providers` | `overrideComponentProvider(Cmp, Token)` — it verifies on the first fixture that the override applied |
|
|
229
|
+
| a double answering `undefined` for a method nobody configured | `{ strict: true }`, or `setupAutoSpy({ strict: true })` suite-wide |
|
|
230
|
+
| a strict throw caught by a `try`/`catch` or an operator, the test still green | `setupAutoSpy({ swallowedStrictCalls: 'throw' })`; provoked on purpose — `takeStrictViolations()` from `/setup` |
|
|
231
|
+
| a strict double's getter answering `undefined`, or its `items$` never emitting | `setupAutoSpy({ unconfiguredReads: 'throw' })` reports it after the test; survey first with `onUnstubbedRead` |
|
|
232
|
+
| an `afterEach` that exists only to reset one spy | `using spy = createSpyFromClass(X)` — `[Symbol.dispose]` runs `resetAutoSpy`, which is `vi.resetAllMocks()` for one double |
|
|
233
|
+
| a dead NgModule import, dead `schemas`, an unflushed HTTP request | `enableAngularDiagnostics()` (`/angular/diagnostics`) in the setup file, after `initTestEnvironment` |
|
|
234
|
+
| the spy is provided but the component resolves its own, so the test runs the real service | `enableAngularDiagnostics({ shadowedProviders })` or `assertNoShadowedProviders(Component, fixture)` — `/angular/diagnostics` |
|
|
235
|
+
| "which collaborators did this actually inject?" | `trackInjections([A, TOKEN])` — providers plus the record, not `vi.mock` |
|
|
236
|
+
| `NG0303` / `NG0301` / `NG0304` from an imported NgModule | `assertNgModuleScopes(Module)` — an AOT bundle stripped its scope |
|
|
237
|
+
| `Cannot read properties of undefined (reading 'provide')` in `di_setup` | `assertComponentDefIntact(Cmp)` — a barrel chunk left a hole in `ɵcmp` |
|
|
238
|
+
| a focus assertion failing as `expected false to deeply equal true` | `registerFocusMatchers()` + `expect(el).toHaveFocus()` |
|
|
239
|
+
| a collaborator passed as an argument, then asserted on | `autoMocked<T>()` — typed `T & Spy<T>` |
|
|
240
|
+
| a `<video>` / `<audio>`: `play()` throws, `duration` is `NaN` | `stubMediaElement({ duration })`, then `media.set(el, …)` |
|
|
241
|
+
| a `vi.mock()` that silently did nothing under a bundler | `assertMocked(ns, { specifier, exports })` |
|
|
242
|
+
| `No "default" export is defined on the mock` | `vi.mock('x', () => moduleNamespace({ … }))` — a `default` you spell out is kept |
|
|
243
|
+
| waiting for a `resource()` / an SDK to become ready | `flushEventLoopUntil(() => …, { label })` — budgeted, not tuned |
|
|
244
|
+
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal …` | `expect(diffByField(actual, expected)).toBeUndefined()` |
|
|
245
|
+
| a stub that works only in the first test of the file | `installPerTest(() => stub…())` — or install it in `beforeEach` |
|
|
246
|
+
| a library failing every other run after a `defineProperty` on DOM | `setupAutoSpy({ guardGlobals: 'throw' })` names the file — it sees an added name, not an existing one redefined |
|
|
247
|
+
| 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) |
|
|
248
|
+
| the same check in a suite that does not call `setupAutoSpy()` | `guardPrototypePollution('throw')` from `/setup` |
|
|
249
|
+
| 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'`) |
|
|
250
|
+
| the same document check without `setupAutoSpy()` | `guardDocumentPollution('throw')` from `/setup` |
|
|
251
|
+
| `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`) |
|
|
252
|
+
| console output a test never asserted on (a `'warn'` from this library does not count) | `setupAutoSpy({ strayConsole: 'throw' })` fails that test; absorb with `installConsoleSpies()` in `beforeEach` |
|
|
253
|
+
| every guard at its strictest grade in one line | `setupAutoSpy({ preset: 'strict' })` — plus `enableAngularDiagnostics()` from `/angular/diagnostics` for Angular |
|
|
254
|
+
| an `onlyMethodsToSpyOn` typo or `injectSpy` on a real instance that only warned | `setupAutoSpy({ misconfiguration: 'throw' })` throws at the call |
|
|
255
|
+
| which file and call scheduled a timer that outlived its file | `onStrayTimers: ({ timers }) => expect(timers).toEqual([])`, or `describeStrayTimers()` from `/setup` |
|
|
256
|
+
| stray timers charged to a file that only writes `localStorage` (jsdom) | `withoutStrayTimerTracking(() => seed())` from `/setup` — its timers are neither counted nor cancelled |
|
|
257
|
+
| `import { consoleErrorSpy }` silencing other files under `isolate: false` | `installConsoleSpies()` in `beforeEach`, `restoreConsole()` in `afterEach` — the import installs once per worker |
|
|
258
|
+
| `Cannot set base providers because it has already been called` | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` |
|
|
259
|
+
| a dependency behind an `InjectionToken`, with no class to spy | `provideAutoSpyForToken(TOKEN)` + `injectSpy(TOKEN)`; a chained call → `{ selfReturning: ['channel'] }` as the third argument |
|
|
260
|
+
| `Expected to be running in 'ProxyZone', but it was not found` | `import 'vitest-auto-spy/zone'` (needs `globals: true`) |
|
|
261
|
+
| `Property 'mockReturnValue' does not exist on type 'never'` | upgrade — the spy no longer collapses on an unreadable return type |
|
|
262
|
+
| `TS2345` inside `mockReturnValue` / `mockImplementation` / `mockResolvedValue` | the stub is checked against the method's return type now — fix the stub, not the spy |
|
|
263
|
+
| `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 |
|
|
264
|
+
| a signal-valued getter that `gettersToSpyOn` will not accept | it accepts any key now; for a signal prefer `mockSignalProp` |
|
|
265
|
+
| five `asInstance(…)` in one call, found one per `tsc` run | `...asInstances(a, b, c, d, e)` |
|
|
266
|
+
| `nextWith` demanding `HttpEvent<T>` on a generated client | `asSpy<Client, { overload: 'first' }>(…)` / `Overload<M, 0>` |
|
|
267
|
+
| `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` |
|
|
268
|
+
| a fixture that needs a nested object built by its own call | `createMock<T>({ a: { b: 1 } })` — deep partial, still exact |
|
|
269
|
+
| the same 100-line model literal copied into eight specs (`TS1117`) | one `createFixtureFactory<T>(defaults)`; specs call it with what they change |
|
|
270
|
+
| `'params' in link` ladders, or a cast, to pick a union branch | `narrow.byKey(link, 'params')` / `narrow.observable(x)` |
|
|
271
|
+
| `{ ...modelInstance, flag: true }` losing every getter | `withOverrides(modelInstance, { flag: true })` |
|
|
272
|
+
| `NG0303` / `NG0304` / silence from a directive spec | `createDirectiveHost({ template, scope: [Module] })` |
|
|
273
|
+
| a hand-written `class MockChartComponent` restating a child's selector | `createComponentStub(ChartComponent)` (`/angular`) — selector, inputs, outputs read from `ɵcmp` |
|
|
274
|
+
| a `TestingStorage` class for `localStorage` / `sessionStorage` | `stubWebStorage('localStorage', { items })` from `/dom-stubs` — `snapshot()` to assert |
|
|
275
|
+
| a hand-written `MockWorker` whose `addEventListener` sets `onmessage` | `stubWorker({ respond })` from `/dom-stubs` — `last.emit(data)`, `last.messages` |
|
|
276
|
+
| `'x' does not exist in type 'MethodReturns<{ y: any; }>'` on a generic class | spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` infers it |
|
|
277
|
+
| `injectSpy(ModalRef)` reads a generic class at its constraint, not its default | `injectSpy<ModalRef<Data>>(ModalRef)` — a constructor taking `T` hides the default |
|
|
278
|
+
| "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` — `counts` reports `name (×2 → ×1)` |
|
|
279
|
+
| an input that has to change after the first render | `await setInputs(fixture, { … })` — aliases and `hostDirectives` inputs resolve, unknown names refused |
|
|
280
|
+
| a component that navigates, or reads `router.url` | `provideRouterDouble({ url })` + `injectRouterDouble()` — `/angular-router` |
|
|
281
|
+
| a component that reads `router.currentNavigation()` for `extras.state` or `trigger` | `setCurrentNavigation({ extras: { state } })` on the same handle — no `instanceMethodsToSpyOn` |
|
|
282
|
+
| the sequence of router events, not just the final URL | `collectRouterEvents(router.events)` — `expect([[NavigationStart, '/checkout'], [NavigationEnd, '/checkout']])` |
|
|
283
|
+
| a component that reads `location.back()`, or asserts where a redirect landed | `provideLocationDouble()` + `injectLocationDouble()` — Angular's own `SpyLocation`; without the provider, `Location` is `providedIn: 'root'` and the real one answers |
|
|
284
|
+
| "did A run before B?" across three collaborators | `createLog<'drop-cache' \| 'flush'>()` — hand `log.fn('drop-cache')` to each, assert `log.result()` once |
|
|
285
|
+
| a `window` or `document` behind a DI token | `provideWindowDouble(WINDOW, { screen })` / `provideDocumentDouble({ … })` — `/angular/doubles` |
|
|
286
|
+
| `MAT_DIALOG_DATA` and `MatDialogRef` provided by hand | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(MatDialogRef)` + `injectMatDialogRef(Ref)` — `/angular/doubles` |
|
|
287
|
+
| asserting a `computed()` did **not** recompute | `trackRecomputations(sig)` / `trackEffectRuns(ref)` — `{ count, stop() }` |
|
|
288
|
+
| `form()` in a spec, or `errors()` read by hand | `createForm(model, schema)` + `registerFormMatchers()` — then `toHaveFieldErrors([…])` (`/signal-forms`) |
|
|
289
|
+
| any of those doubles without a `TestBed` | `createRouterDouble` (`/angular-router`) / `createWindowDouble` / `createDocumentDouble` / `createMatDialogRef` (`/angular/doubles`) |
|
|
290
290
|
|
|
291
291
|
## Rules that prevent most of the mistakes
|
|
292
292
|
|
|
@@ -444,7 +444,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
|
|
|
444
444
|
Most of this library's guarantees are type-level, so a green run that does not type-check is not
|
|
445
445
|
done. Report failures with their output rather than describing them as passing.
|
|
446
446
|
|
|
447
|
-
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The forty-
|
|
447
|
+
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The forty-nine rules in
|
|
448
448
|
`vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
|
|
449
449
|
to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
|
|
450
450
|
evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
|