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 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.48.0...HEAD
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
 
@@ -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
@@ -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-six are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
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 ", info: "info " };
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
- `${SEVERITY_LABEL[finding2.severity]} ${finding2.check}${where2}`,
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
- `${SEVERITY_LABEL[first.severity]} ${first.check} \u2014 ${placesOf(group3)}`,
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.47.0";
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
- return [`perf gate verdict \u2014 ${rows.length} judged, ${failing} ${failing === 1 ? "fails" : "fail"} the run`, ...table];
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
  `);
@@ -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 readCall2(context, node) {
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
- readCall2(context, node);
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.48.0",
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 —