vitest-auto-spy 5.48.0 → 5.50.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/CHANGELOG.md +39 -1
- package/README.md +15 -4
- package/agent-docs/angular.md +1 -1
- package/agent-docs/configuration.md +2 -0
- package/agent-docs/eslint.md +3 -2
- package/dist/cli.js +22 -12
- package/dist/eslint-plugin.cjs +118 -2
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,42 @@ The latest released version here must always match the one published on
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [5.50.0] - 2026-09-29
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **`doctor` and `perf` color their findings by severity** — the `error` label red, `warn` yellow,
|
|
18
|
+
`info` plain — and the `perf --gate` verdict line turns red when something fails the run. The
|
|
19
|
+
same switch as the `perf` tables decides it (`NO_COLOR`, `FORCE_COLOR`, `TERM=dumb`, a terminal);
|
|
20
|
+
`--format json`, `--format markdown` and `--code-quality` stay free of escapes.
|
|
21
|
+
- **Color in GitLab CI and GitHub Actions logs.** Both render ANSI color although the job's stdout
|
|
22
|
+
is a pipe, so the CLI now paints when `GITLAB_CI` or `GITHUB_ACTIONS` is set; `NO_COLOR`,
|
|
23
|
+
`FORCE_COLOR=0` and `TERM=dumb` still turn it off.
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
- **`returns` vs `overrides` has a page of its own**, with a landing card and a sidebar entry: one
|
|
28
|
+
table of what each seeds, whether the member stays a spy, how it behaves under `strict` and which
|
|
29
|
+
wins when a member is named in both. `agent-docs/configuration.md` gains the two rows its option
|
|
30
|
+
table was missing.
|
|
31
|
+
|
|
32
|
+
## [5.49.0] - 2026-09-28
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`no-real-component-provider`** in `vitest-auto-spy/eslint-plugin`, `error` in `recommended`:
|
|
37
|
+
`fixture.debugElement.injector.get(X)` / `fixture.componentRef.injector.get(X)` for a token nothing
|
|
38
|
+
in the file replaced with a double. That read is how a spec reaches a component-level provider, and
|
|
39
|
+
when nothing swapped it the fixture hands back the production class — the component spec then
|
|
40
|
+
drives the real store through HTTP and repeats the store's own spec. The message names the repair,
|
|
41
|
+
`overrideComponentProvider(Component, X)`. Tokens from `@angular/*`, classes the file renders
|
|
42
|
+
(`createComponent`, `imports`, `declarations`, `hostDirectives`), reads wrapped in `asSpy(…)` and
|
|
43
|
+
files that call `createWithAutoSpies` are left alone; `{ ignoreTokens }` keeps an intended
|
|
44
|
+
integration test real. Only the fixture's own injector is read, never a child's. Measured on three
|
|
45
|
+
consumers, 1078 spec files: 5 reports, all of them real. The plugin now ships fifty-one rules.
|
|
46
|
+
`vitest-auto-spy/eslint-plugin` is +0.78 kB min+gzip (+1.8 %) for the rule module and its
|
|
47
|
+
messages; no module is added to the entry's graph.
|
|
48
|
+
|
|
13
49
|
## [5.48.0] - 2026-09-28
|
|
14
50
|
|
|
15
51
|
### Added
|
|
@@ -6076,7 +6112,9 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
|
|
|
6076
6112
|
This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
|
|
6077
6113
|
[full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
|
|
6078
6114
|
|
|
6079
|
-
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.
|
|
6115
|
+
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.50.0...HEAD
|
|
6116
|
+
[5.50.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.49.0...v5.50.0
|
|
6117
|
+
[5.49.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.48.0...v5.49.0
|
|
6080
6118
|
[5.48.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.47.0...v5.48.0
|
|
6081
6119
|
[5.47.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.46.0...v5.47.0
|
|
6082
6120
|
[5.46.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...v5.46.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 — fifty rules in `vitest-auto-spy/eslint-plugin` (eight `--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
|
|
84
|
+
- 📏 Lint rules and one-line test-run hygiene — fifty-one rules in `vitest-auto-spy/eslint-plugin` (eight `--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-init-and-ng-test) — 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
|
|
@@ -4019,6 +4019,7 @@ export can never be.
|
|
|
4019
4019
|
| `no-dead-schemas` | `error` | — | `schemas` on a testing module that declares nothing — a charm protecting nobody, and one that starts working the day `declarations` arrive |
|
|
4020
4020
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
|
|
4021
4021
|
| `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 |
|
|
4022
|
+
| `no-real-component-provider` | `error` | — | `fixture.debugElement.injector.get(X)` for a component-level provider nothing in the file replaced → the real store under a component spec; `overrideComponentProvider(Cmp, X)` |
|
|
4022
4023
|
| `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 |
|
|
4023
4024
|
| `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(signal()).toBe(…)` → `expect(signal).toHaveSignalValue(…)`; the matcher names the signal in the failure and refuses a non-signal. `toStrictEqual` keeps `{ strict: true }`, `toBeNull()` / `toBeUndefined()` become `toHaveSignalValue(null)` / `(undefined)`. **Type-aware** — the callee must be callable and carry Angular's signal brand — and silent without `parserOptions.project`; safe to autofix since 5.46.0 fixed the `Set`/`Map` comparison. A `toBe` against an object-typed signal is an identity check and is left alone |
|
|
4024
4025
|
| `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` |
|
|
@@ -4054,7 +4055,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
|
|
|
4054
4055
|
"don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
|
|
4055
4056
|
are versioned together and stop being re-written in every project that installs the package.
|
|
4056
4057
|
|
|
4057
|
-
**Eight of the fifty fix on their own, nineteen offer suggestions**, and the split is not about how hard
|
|
4058
|
+
**Eight of the fifty-one fix on their own, nineteen offer suggestions**, and the split is not about how hard
|
|
4058
4059
|
the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
|
|
4059
4060
|
compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
|
|
4060
4061
|
`import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
|
|
@@ -4131,6 +4132,16 @@ a spread, an unknown provider factory, `createWithAutoSpies`, `renderShallow` or
|
|
|
4131
4132
|
`TestBed.overrideProvider` silences the file. A token handed a plain `useValue` is left to
|
|
4132
4133
|
`prefer-provide-auto-spy` — two rules on one line only teach people to disable both.
|
|
4133
4134
|
|
|
4135
|
+
`no-real-component-provider` catches `fixture.debugElement.injector.get(X)` — or
|
|
4136
|
+
`fixture.componentRef.injector.get(X)` — for a token nothing in the file replaced with a double.
|
|
4137
|
+
`injectSpy` cannot reach a provider the component declares itself, so a spec that wants that store
|
|
4138
|
+
reads it through the fixture, and when nothing swapped it the fixture hands back the production
|
|
4139
|
+
class: the component spec drives the real store through HTTP and repeats the store's own spec. The
|
|
4140
|
+
message names the repair, [`overrideComponentProvider(Cmp, X)`](#a-provider-the-component-declares-for-itself). Tokens
|
|
4141
|
+
from `@angular/*`, classes the file renders, reads wrapped in `asSpy(…)` and files that call
|
|
4142
|
+
`createWithAutoSpies` are left alone, and `{ ignoreTokens }` keeps an intended integration test real.
|
|
4143
|
+
On three consumers, 1078 spec files, it reports 5, every one of them real.
|
|
4144
|
+
|
|
4134
4145
|
`no-ts-expect-error-on-double` reports `@ts-expect-error` / `@ts-ignore` above a double's
|
|
4135
4146
|
configuration whatever reason follows the directive. On one 1759-file suite it reports 34, every one
|
|
4136
4147
|
with a reason: four on overloaded clients ([the recipe](#how-to-mock-an-overloaded-method)), twenty-six
|
|
@@ -4225,7 +4236,7 @@ package's own — it needs its ESLint integration switched on.
|
|
|
4225
4236
|
### WebStorm and the other JetBrains IDEs
|
|
4226
4237
|
|
|
4227
4238
|
No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
|
|
4228
|
-
all run ESLint natively, so the fifty rules appear inline, in the **Problems** tool window, and
|
|
4239
|
+
all run ESLint natively, so the fifty-one rules appear inline, in the **Problems** tool window, and
|
|
4229
4240
|
under **Code → Inspect Code** for the whole project.
|
|
4230
4241
|
|
|
4231
4242
|
```js
|
|
@@ -4242,7 +4253,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
|
|
|
4242
4253
|
the fixes and suggestions live.
|
|
4243
4254
|
|
|
4244
4255
|
A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
|
|
4245
|
-
and then keep a second copy of fifty rules, in Kotlin, in step with the TypeScript ones.
|
|
4256
|
+
and then keep a second copy of fifty-one rules, in Kotlin, in step with the TypeScript ones.
|
|
4246
4257
|
|
|
4247
4258
|
### VS Code, Cursor, Windsurf, VSCodium
|
|
4248
4259
|
|
package/agent-docs/angular.md
CHANGED
|
@@ -92,7 +92,7 @@ provideAutoSpyForToken(PRODUCTS, undefined, { returns: { getProducts: of([]), ge
|
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
A seeded `overrides` value is stored verbatim and is **no longer a spy**, so seed data there and
|
|
95
|
-
name methods in `returns` when they must stay assertable. On `provideAutoSpy` a plain function
|
|
95
|
+
name methods in `returns` when they must stay assertable (the two compared in §5, [`configuration.md`](./configuration.md)). On `provideAutoSpy` a plain function
|
|
96
96
|
seeded on a method is the exception: it becomes that method's spy, running the function until the
|
|
97
97
|
test configures it, so `toHaveBeenCalled` works on it; a `vi.fn()` is kept as it is
|
|
98
98
|
(`provideAutoSpyForToken` still stores every seed verbatim). The reason to prefer this over a second
|
|
@@ -31,6 +31,8 @@ createSpyFromClass(MyService, {
|
|
|
31
31
|
| `lazySpies` | Unset: `true` (accessor placeholders) below 8 methods, `'proxy'` (a `Proxy`, lighter once touched) from 8, counting the methods the double spies (after `onlyMethodsToSpyOn` / `methodsToSpyOn`). Explicit `true` / `false` / `'proxy'` apply at any width. Calls, `Object.keys`, spread and snapshots behave the same in every mode. |
|
|
32
32
|
| `strict` | Throw on a method nobody configured, instead of answering `undefined` (below). |
|
|
33
33
|
| `onUnstubbedCall` | The general form of `strict`; its return value becomes the call's return value. |
|
|
34
|
+
| `returns` | What a spied **method** answers. The member stays a spy (`toHaveBeenCalled` works); a default that `calledWith` / `mockReturnValue` / `resolveWith` replace. A key that is not a spied method is reported. |
|
|
35
|
+
| `overrides` | Replaces **any member** with the value as written — a field, a signal, an `Observable` property. The member is no longer a spy, except a plain function seeded on a method (class factories only), which becomes its spy. Named in both, `overrides` wins. |
|
|
34
36
|
| `selfReturning` | The named methods answer the double itself — a default like `returns`; a name in both answers `returns`. |
|
|
35
37
|
|
|
36
38
|
**`instanceMethodsToSpyOn` is not an edge case — it is a top-5 option** (103 of ~370 spec files in
|
package/agent-docs/eslint.md
CHANGED
|
@@ -73,6 +73,7 @@ export default [
|
|
|
73
73
|
| `no-dead-schemas` | `error` | — | `schemas` on a testing module with no `declarations` — the schema applies to nothing; the file decides, so a `declarations` in another `configureTestingModule` call silences it |
|
|
74
74
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError`, or a silently empty object, while the bundle loads |
|
|
75
75
|
| `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 |
|
|
76
|
+
| `no-real-component-provider` | `error` | — | `fixture.debugElement.injector.get(X)` for a component-level provider nothing in the file replaced → the real store under a component spec; `overrideComponentProvider(Cmp, X)`; `{ ignoreTokens }` for an intended integration test |
|
|
76
77
|
| `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 |
|
|
77
78
|
| `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` |
|
|
78
79
|
| `prefer-to-have-signal-value` | `warn` | `--fix` | `expect(component.total()).toBe(3)` / `toEqual(…)` → `expect(component.total).toHaveSignalValue(3)`, `toStrictEqual` → `{ strict: true }`, `toBeNull()` / `toBeUndefined()` → `toHaveSignalValue(null)` / `(undefined)`; the matcher names the signal in the failure and refuses a value that is not one. **Type-aware**: the signal is recognised by Angular's brand on its type, so methods, functions and getters are never reported, and without `parserOptions.project` it reports nothing. A `toBe` against an object-typed signal (`expect(list.items()).toBe(items)`) is an identity check the deep matcher would lose, so it is reported only when the expected value is a primitive literal or the signal's type is primitive |
|
|
@@ -104,9 +105,9 @@ export default [
|
|
|
104
105
|
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
105
106
|
| `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 |
|
|
106
107
|
|
|
107
|
-
Fifty rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
|
|
108
|
+
Fifty-one rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
|
|
108
109
|
`no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
|
|
109
|
-
`prefer-set-inputs`, `no-unasserted-argument`, `prefer-spy-on-own-method` and `prefer-to-have-signal-value`**; eight fix on their own, nineteen offer suggestions. Forty-
|
|
110
|
+
`prefer-set-inputs`, `no-unasserted-argument`, `prefer-spy-on-own-method` and `prefer-to-have-signal-value`**; eight fix on their own, nineteen offer suggestions. Forty-seven are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
|
|
110
111
|
`no-unknown-use-value-key` and `prefer-to-have-signal-value` read types, and all four report nothing at all without `parserOptions.project` / `projectService`
|
|
111
112
|
rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
|
|
112
113
|
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
|
package/dist/cli.js
CHANGED
|
@@ -607,11 +607,12 @@ var wrap = (open) => (text) => `${ESC}[${open}m${text}${ESC}[0m`;
|
|
|
607
607
|
var TERMINAL = { red: wrap("31"), yellow: wrap("33"), cyan: wrap("36"), bold: wrap("1"), dim: wrap("2") };
|
|
608
608
|
var same = (text) => text;
|
|
609
609
|
var MONOCHROME = { red: same, yellow: same, cyan: same, bold: same, dim: same };
|
|
610
|
+
var COLOR_LOGS = ["GITLAB_CI", "GITHUB_ACTIONS"];
|
|
610
611
|
function colorWanted(env, isTTY = process.stdout.isTTY === true) {
|
|
611
612
|
if (env["NO_COLOR"] !== void 0 || env["FORCE_COLOR"] === "0" || env["TERM"] === "dumb") {
|
|
612
613
|
return false;
|
|
613
614
|
}
|
|
614
|
-
return env["FORCE_COLOR"] !== void 0 || isTTY;
|
|
615
|
+
return env["FORCE_COLOR"] !== void 0 || COLOR_LOGS.some((name) => env[name] !== void 0) || isTTY;
|
|
615
616
|
}
|
|
616
617
|
function painterFor(colors) {
|
|
617
618
|
return colors ?? colorWanted(process.env) ? TERMINAL : MONOCHROME;
|
|
@@ -658,7 +659,15 @@ function wrapLine(text, width, indent, firstIndent) {
|
|
|
658
659
|
|
|
659
660
|
// src/cli/report.ts
|
|
660
661
|
var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
661
|
-
var SEVERITY_LABEL = { error: "error", warning: "warn
|
|
662
|
+
var SEVERITY_LABEL = { error: "error", warning: "warn", info: "info" };
|
|
663
|
+
var SEVERITY_PAINT = {
|
|
664
|
+
error: (paint, text) => paint.red(text),
|
|
665
|
+
warning: (paint, text) => paint.yellow(text),
|
|
666
|
+
info: (_paint, text) => text
|
|
667
|
+
};
|
|
668
|
+
function severityLabel(severity, paint) {
|
|
669
|
+
return padEnd(SEVERITY_PAINT[severity](paint, SEVERITY_LABEL[severity]), 5);
|
|
670
|
+
}
|
|
662
671
|
var BODY = " ";
|
|
663
672
|
var ITEM = `${BODY} `;
|
|
664
673
|
var ITEM_BODY = `${ITEM} `;
|
|
@@ -679,23 +688,23 @@ function fixLines(finding2, width) {
|
|
|
679
688
|
const docs = docsFor(finding2.check);
|
|
680
689
|
return [...wrapText(finding2.fix, width, `${BODY} `, `${BODY}\u2192 `), ...docs === void 0 ? [] : [`${BODY}Docs: ${docs}`]];
|
|
681
690
|
}
|
|
682
|
-
function formatOne(finding2, width) {
|
|
691
|
+
function formatOne(finding2, width, paint) {
|
|
683
692
|
const where2 = finding2.file === void 0 ? "" : ` ${finding2.file}`;
|
|
684
693
|
return [
|
|
685
|
-
`${
|
|
694
|
+
`${severityLabel(finding2.severity, paint)} ${finding2.check}${where2}`,
|
|
686
695
|
...wrapText(finding2.message, width, BODY),
|
|
687
696
|
...(finding2.details ?? []).map((line) => `${BODY}${line}`),
|
|
688
697
|
...fixLines(finding2, width)
|
|
689
698
|
].join("\n");
|
|
690
699
|
}
|
|
691
|
-
function formatGroup({ first, members: group3 }, width) {
|
|
700
|
+
function formatGroup({ first, members: group3 }, width, paint) {
|
|
692
701
|
const sameMessage = group3.every((finding2) => finding2.message === first.message);
|
|
693
702
|
const places = group3.flatMap((finding2) => [
|
|
694
703
|
`${ITEM}${String(finding2.file)}`,
|
|
695
704
|
...sameMessage ? [] : wrapText(finding2.message, width, ITEM_BODY)
|
|
696
705
|
]);
|
|
697
706
|
return [
|
|
698
|
-
`${
|
|
707
|
+
`${severityLabel(first.severity, paint)} ${first.check} \u2014 ${placesOf(group3)}`,
|
|
699
708
|
...sameMessage ? wrapText(first.message, width, BODY) : [],
|
|
700
709
|
...places,
|
|
701
710
|
...fixLines(first, width)
|
|
@@ -723,8 +732,8 @@ function groupFindings(findings) {
|
|
|
723
732
|
function filterBySeverity(findings, minSeverity = "info") {
|
|
724
733
|
return findings.filter((finding2) => SEVERITY_ORDER[finding2.severity] <= SEVERITY_ORDER[minSeverity]);
|
|
725
734
|
}
|
|
726
|
-
function formatFindings(findings, minSeverity = "info", width = outputWidth()) {
|
|
727
|
-
return groupFindings(filterBySeverity(findings, minSeverity)).map((group3) => group3.members.length === 1 ? formatOne(group3.first, width) : formatGroup(group3, width)).join("\n\n");
|
|
735
|
+
function formatFindings(findings, minSeverity = "info", width = outputWidth(), paint = MONOCHROME) {
|
|
736
|
+
return groupFindings(filterBySeverity(findings, minSeverity)).map((group3) => group3.members.length === 1 ? formatOne(group3.first, width, paint) : formatGroup(group3, width, paint)).join("\n\n");
|
|
728
737
|
}
|
|
729
738
|
function tallyOf(findings) {
|
|
730
739
|
return {
|
|
@@ -3865,7 +3874,7 @@ function checkForeignPragma(graph) {
|
|
|
3865
3874
|
}
|
|
3866
3875
|
|
|
3867
3876
|
// src/cli/checks/export-map.generated.ts
|
|
3868
|
-
var EXPORT_MAP_VERSION = "5.
|
|
3877
|
+
var EXPORT_MAP_VERSION = "5.49.0";
|
|
3869
3878
|
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";
|
|
3870
3879
|
var EXPORTED_BY = {
|
|
3871
3880
|
AccessorImplementations: "0 1 2 3 4 12 20 21 22",
|
|
@@ -8236,7 +8245,8 @@ function formatVerdict(rows, paint) {
|
|
|
8236
8245
|
],
|
|
8237
8246
|
paint
|
|
8238
8247
|
);
|
|
8239
|
-
|
|
8248
|
+
const title = `perf gate verdict \u2014 ${rows.length} judged, ${failing} ${failing === 1 ? "fails" : "fail"} the run`;
|
|
8249
|
+
return [failing > 0 ? paint.red(title) : title, ...table];
|
|
8240
8250
|
}
|
|
8241
8251
|
|
|
8242
8252
|
// src/cli/report-markdown.ts
|
|
@@ -8464,7 +8474,7 @@ ${nothingToDo(analysis)}`);
|
|
|
8464
8474
|
reportOnly(analysis.findings, io, minSeverity);
|
|
8465
8475
|
}
|
|
8466
8476
|
function reportOnly(findings, io, minSeverity) {
|
|
8467
|
-
const report2 = formatFindings(findings, minSeverity);
|
|
8477
|
+
const report2 = formatFindings(findings, minSeverity, outputWidth(), painterFor(void 0));
|
|
8468
8478
|
if (report2 !== "") {
|
|
8469
8479
|
io.out(`
|
|
8470
8480
|
${report2}`);
|
|
@@ -9085,7 +9095,7 @@ function doctorCommand(cwd, argv, io) {
|
|
|
9085
9095
|
io.out(`vitest-auto-spy doctor \u2014 ${cwd}`);
|
|
9086
9096
|
io.out(`${profile.files.length} files scanned, ${specFiles} of them spec files \u2014 runner: ${profile.runner}, entry: ${profile.entry}
|
|
9087
9097
|
`);
|
|
9088
|
-
const report2 = findings.length === 0 ? "No problems found." : formatFindings(findings, minSeverity);
|
|
9098
|
+
const report2 = findings.length === 0 ? "No problems found." : formatFindings(findings, minSeverity, outputWidth(), painterFor(void 0));
|
|
9089
9099
|
if (report2 !== "") {
|
|
9090
9100
|
io.out(`${report2}
|
|
9091
9101
|
`);
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -4629,6 +4629,120 @@ var preferProvideAutoSpy = defineRule({
|
|
|
4629
4629
|
}
|
|
4630
4630
|
});
|
|
4631
4631
|
|
|
4632
|
+
// src/lib/eslint/real-component-provider.ts
|
|
4633
|
+
var REPLACING_CALLS = /* @__PURE__ */ new Set([
|
|
4634
|
+
"createSpyFromClass",
|
|
4635
|
+
"overrideAutoSpy",
|
|
4636
|
+
"overrideComponent",
|
|
4637
|
+
"overrideComponentProvider",
|
|
4638
|
+
"overrideProvider",
|
|
4639
|
+
"provideAutoSpy"
|
|
4640
|
+
]);
|
|
4641
|
+
var OPAQUE_HELPERS2 = /* @__PURE__ */ new Set(["createWithAutoSpies"]);
|
|
4642
|
+
var OWN_INJECTOR_HOLDERS = /* @__PURE__ */ new Set(["componentRef", "debugElement"]);
|
|
4643
|
+
var FRAMEWORK_SCOPE = "@angular/";
|
|
4644
|
+
var REAL_COMPONENT_PROVIDER_SCHEMA = [
|
|
4645
|
+
{
|
|
4646
|
+
type: "object",
|
|
4647
|
+
properties: { ignoreTokens: { type: "array", items: { type: "string" }, uniqueItems: true } },
|
|
4648
|
+
additionalProperties: false
|
|
4649
|
+
}
|
|
4650
|
+
];
|
|
4651
|
+
function calleeName2(node) {
|
|
4652
|
+
return isIdentifier(node.callee) ? node.callee.name : memberName(node.callee);
|
|
4653
|
+
}
|
|
4654
|
+
var RENDERED_KEYS = /* @__PURE__ */ new Set(["declarations", "hostDirectives", "imports"]);
|
|
4655
|
+
function isRenderedList(node) {
|
|
4656
|
+
const name = propertyName(node.parent);
|
|
4657
|
+
return name !== void 0 && RENDERED_KEYS.has(name) && Reflect.get(node.parent, "value") === node;
|
|
4658
|
+
}
|
|
4659
|
+
function isReplacingArgument(node) {
|
|
4660
|
+
const call = node.parent;
|
|
4661
|
+
if (!isCallExpression(call) || !call.arguments.includes(node)) {
|
|
4662
|
+
return false;
|
|
4663
|
+
}
|
|
4664
|
+
const name = calleeName2(call);
|
|
4665
|
+
return name !== void 0 && REPLACING_CALLS.has(name);
|
|
4666
|
+
}
|
|
4667
|
+
function ownInjectorToken(node) {
|
|
4668
|
+
const [token] = node.arguments;
|
|
4669
|
+
if (!isMemberExpression(node.callee) || memberName(node.callee) !== "get" || !token || !isIdentifier(token)) {
|
|
4670
|
+
return void 0;
|
|
4671
|
+
}
|
|
4672
|
+
const injector = node.callee.object;
|
|
4673
|
+
if (!isMemberExpression(injector) || memberName(injector) !== "injector") {
|
|
4674
|
+
return void 0;
|
|
4675
|
+
}
|
|
4676
|
+
const holder = memberName(injector.object);
|
|
4677
|
+
return holder !== void 0 && OWN_INJECTOR_HOLDERS.has(holder) ? token : void 0;
|
|
4678
|
+
}
|
|
4679
|
+
function isClaimedSpy(node) {
|
|
4680
|
+
return isCallExpression(node.parent) && calleeName2(node.parent) === "asSpy";
|
|
4681
|
+
}
|
|
4682
|
+
function ignoredTokens(options) {
|
|
4683
|
+
const configured = Reflect.get(Object(options[0]), "ignoreTokens");
|
|
4684
|
+
return new Set(Array.isArray(configured) ? configured.filter((entry) => typeof entry === "string") : []);
|
|
4685
|
+
}
|
|
4686
|
+
function isFrameworkImport(node) {
|
|
4687
|
+
return node.type === "ImportDeclaration" && String(Reflect.get(Reflect.get(node, "source"), "value")).startsWith(FRAMEWORK_SCOPE);
|
|
4688
|
+
}
|
|
4689
|
+
function readCall2(node, scan) {
|
|
4690
|
+
const name = calleeName2(node);
|
|
4691
|
+
if (name !== void 0 && OPAQUE_HELPERS2.has(name)) {
|
|
4692
|
+
scan.opaque = true;
|
|
4693
|
+
}
|
|
4694
|
+
if (name === "createComponent") {
|
|
4695
|
+
node.arguments.filter(isIdentifier).forEach((argument) => scan.rendered.add(argument.name));
|
|
4696
|
+
}
|
|
4697
|
+
const token = ownInjectorToken(node);
|
|
4698
|
+
if (token && !isClaimedSpy(node)) {
|
|
4699
|
+
scan.reads.push(token);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
var noRealComponentProvider = defineRule({
|
|
4703
|
+
name: "no-real-component-provider",
|
|
4704
|
+
description: "Do not read a component's own provider from the fixture unless the file replaced it with a spy",
|
|
4705
|
+
schema: REAL_COMPONENT_PROVIDER_SCHEMA,
|
|
4706
|
+
messages: {
|
|
4707
|
+
noRealComponentProvider: "Nothing in this file replaces `{{token}}`, so the fixture hands back the real instance the component provides, and the spec tests it through the component. Install the spy where the component looks: `const {{variable}} = overrideComponentProvider(Component, {{token}});`, before the fixture is created. If the real one is meant, list `{{token}}` in `{ ignoreTokens }`."
|
|
4708
|
+
},
|
|
4709
|
+
create: (context) => {
|
|
4710
|
+
const scan = { reads: [], replaced: /* @__PURE__ */ new Set(), rendered: /* @__PURE__ */ new Set(), framework: /* @__PURE__ */ new Set(), opaque: false };
|
|
4711
|
+
const ignored = ignoredTokens(context.options);
|
|
4712
|
+
return {
|
|
4713
|
+
Property: (node) => {
|
|
4714
|
+
if (propertyName(node) === "provide" && isIdentifier(node.value)) {
|
|
4715
|
+
scan.replaced.add(node.value.name);
|
|
4716
|
+
}
|
|
4717
|
+
},
|
|
4718
|
+
Identifier: (node) => {
|
|
4719
|
+
if (hasAncestor(node, isReplacingArgument)) {
|
|
4720
|
+
scan.replaced.add(node.name);
|
|
4721
|
+
}
|
|
4722
|
+
if (hasAncestor(node, isRenderedList)) {
|
|
4723
|
+
scan.rendered.add(node.name);
|
|
4724
|
+
}
|
|
4725
|
+
if (hasAncestor(node, isFrameworkImport)) {
|
|
4726
|
+
scan.framework.add(node.name);
|
|
4727
|
+
}
|
|
4728
|
+
},
|
|
4729
|
+
CallExpression: (node) => {
|
|
4730
|
+
readCall2(node, scan);
|
|
4731
|
+
},
|
|
4732
|
+
"Program:exit": () => {
|
|
4733
|
+
if (scan.opaque) {
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
const skipped = [scan.replaced, scan.rendered, scan.framework, ignored];
|
|
4737
|
+
scan.reads.filter(({ name }) => !skipped.some((names) => names.has(name))).forEach((token) => {
|
|
4738
|
+
const variable = `${token.name.charAt(0).toLowerCase()}${token.name.slice(1)}`;
|
|
4739
|
+
context.report({ node: token, messageId: "noRealComponentProvider", data: { token: token.name, variable } });
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
|
|
4632
4746
|
// src/lib/eslint/reflect-access.ts
|
|
4633
4747
|
var HELPER3 = "mockValueProp";
|
|
4634
4748
|
var REFLECT = /* @__PURE__ */ new Set(["Reflect"]);
|
|
@@ -5552,7 +5666,7 @@ function readCast(context, node) {
|
|
|
5552
5666
|
context.report({ node, messageId: "castResponse", data: { cast: excerpt(context, node, 50) } });
|
|
5553
5667
|
}
|
|
5554
5668
|
}
|
|
5555
|
-
function
|
|
5669
|
+
function readCall3(context, node) {
|
|
5556
5670
|
const helper = isIdentifier(node.callee) ? node.callee.name : void 0;
|
|
5557
5671
|
const argument = node.typeArguments?.params[0];
|
|
5558
5672
|
if (helper === void 0 || !TYPE_ARGUMENT_DOUBLES.has(helper) || argument === void 0) {
|
|
@@ -5574,7 +5688,7 @@ var preferStubResponse = defineRule({
|
|
|
5574
5688
|
},
|
|
5575
5689
|
create: (context) => ({
|
|
5576
5690
|
CallExpression: (node) => {
|
|
5577
|
-
|
|
5691
|
+
readCall3(context, node);
|
|
5578
5692
|
},
|
|
5579
5693
|
[CAST_SELECTORS.join(", ")]: (node) => {
|
|
5580
5694
|
readCast(context, node);
|
|
@@ -5810,6 +5924,7 @@ var rules = {
|
|
|
5810
5924
|
"no-dead-schemas": noDeadSchemas,
|
|
5811
5925
|
"no-import-time-spread": noImportTimeSpread,
|
|
5812
5926
|
"no-unregistered-inject-spy": noUnregisteredInjectSpy,
|
|
5927
|
+
"no-real-component-provider": noRealComponentProvider,
|
|
5813
5928
|
"prefer-render-shallow": preferRenderShallow,
|
|
5814
5929
|
"prefer-to-have-signal-value": preferToHaveSignalValue,
|
|
5815
5930
|
"prefer-observer-stub": preferObserverStub,
|
|
@@ -5862,6 +5977,7 @@ var recommendedRules = {
|
|
|
5862
5977
|
[`${PLUGIN_NAME}/no-dead-schemas`]: "error",
|
|
5863
5978
|
[`${PLUGIN_NAME}/no-import-time-spread`]: "error",
|
|
5864
5979
|
[`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "error",
|
|
5980
|
+
[`${PLUGIN_NAME}/no-real-component-provider`]: "error",
|
|
5865
5981
|
[`${PLUGIN_NAME}/prefer-render-shallow`]: "warn",
|
|
5866
5982
|
[`${PLUGIN_NAME}/prefer-to-have-signal-value`]: "warn",
|
|
5867
5983
|
[`${PLUGIN_NAME}/no-stub-class-double`]: "warn",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.50.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",
|
|
@@ -410,7 +410,9 @@ it('loads', async () => {
|
|
|
410
410
|
To control an `effect()`, set the signals it reads and assert what it produced.
|
|
411
411
|
- **`injectSpy(X)` only reaches the global TestBed.** For a component-level provider use
|
|
412
412
|
`asSpy(fixture.debugElement.injector.get(X))` to read it, or `overrideComponentProvider(Cmp, X)` to
|
|
413
|
-
replace it — `provideAutoSpy` loses to a provider the component declares, silently.
|
|
413
|
+
replace it — `provideAutoSpy` loses to a provider the component declares, silently. Reading it
|
|
414
|
+
bare, with nothing in the file replacing it, hands back the real store and makes the component spec
|
|
415
|
+
re-test it; `no-real-component-provider` reports that.
|
|
414
416
|
- **Declare `Spy<T>`, never Vitest's `Mocked<T>`** — `Mocked<T>` keeps the private members, so the
|
|
415
417
|
assignment fails with a list of private field names that says nothing about the declaration being
|
|
416
418
|
the problem.
|
|
@@ -487,7 +489,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
|
|
|
487
489
|
Most of this library's guarantees are type-level, so a green run that does not type-check is not
|
|
488
490
|
done. Report failures with their output rather than describing them as passing.
|
|
489
491
|
|
|
490
|
-
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The fifty rules in
|
|
492
|
+
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The fifty-one rules in
|
|
491
493
|
`vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
|
|
492
494
|
to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
|
|
493
495
|
evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
|