vitest-auto-spy 4.4.0 → 4.5.1

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 CHANGED
@@ -2328,6 +2328,7 @@ export default [
2328
2328
  | `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2329
2329
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2330
2330
  | `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
2331
+ | `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2331
2332
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2332
2333
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2333
2334
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
package/README.md CHANGED
@@ -76,7 +76,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
76
76
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
77
77
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
78
78
  - 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
79
- - 📏 Lint rules and one-line test-run hygiene — nineteen rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, seven suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
79
+ - 📏 Lint rules and one-line test-run hygiene — twenty rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, eight suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
80
80
  - 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
81
81
  - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
82
82
  - ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
@@ -2961,6 +2961,7 @@ export can never be.
2961
2961
  | `no-inject-before-override` | `error` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2962
2962
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2963
2963
  | `no-unregistered-inject-spy` | `error` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
2964
+ | `prefer-render-shallow` | `error` | suggest | `TestBed.createComponent` in a file that never reads the template → `renderShallow(X)`; 0.24× the per-test cycle at 100 children |
2964
2965
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
2965
2966
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` — none of them exist under Vitest |
2966
2967
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()`, which is a no-op here → take the copy at call time |
@@ -2970,7 +2971,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
2970
2971
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
2971
2972
  are versioned together and stop being re-written in every project that installs the package.
2972
2973
 
2973
- **Three of the nineteen fix on their own, seven offer suggestions**, and the split is not about how hard
2974
+ **Three of the twenty fix on their own, eight offer suggestions**, and the split is not about how hard
2974
2975
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
2975
2976
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
2976
2977
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -3074,7 +3075,7 @@ package's own — it needs its ESLint integration switched on.
3074
3075
  ### WebStorm and the other JetBrains IDEs
3075
3076
 
3076
3077
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
3077
- all run ESLint natively, so the nineteen rules appear inline, in the **Problems** tool window, and
3078
+ all run ESLint natively, so the twenty rules appear inline, in the **Problems** tool window, and
3078
3079
  under **Code → Inspect Code** for the whole project.
3079
3080
 
3080
3081
  ```js
@@ -3091,7 +3092,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
3091
3092
  the fixes and suggestions live.
3092
3093
 
3093
3094
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
3094
- and then keep a second copy of nineteen rules, in Kotlin, in step with the TypeScript ones.
3095
+ and then keep a second copy of twenty rules, in Kotlin, in step with the TypeScript ones.
3095
3096
 
3096
3097
  ### VS Code, Cursor, Windsurf, VSCodium
3097
3098
 
package/dist/angular.js CHANGED
@@ -2526,7 +2526,10 @@ function renderShallow(component, options = {}) {
2526
2526
  imports: standalone ? [component, ...options.imports ?? []] : options.imports ?? [],
2527
2527
  declarations: standalone ? [] : [component],
2528
2528
  providers: options.providers ?? [],
2529
- schemas: [NO_ERRORS_SCHEMA]
2529
+ // Only where it can do something. A standalone component carries its own dependency scope, so a
2530
+ // module-level schema never reaches its template — and `enableAngularDiagnostics({ deadSchemas })`
2531
+ // is right to report one, which made these two features of the same package cancel each other out.
2532
+ ...standalone ? {} : { schemas: [NO_ERRORS_SCHEMA] }
2530
2533
  });
2531
2534
  TestBed.overrideComponent(component, { set: buildOverride(component, options) });
2532
2535
  options.beforeCreate?.();
@@ -314,7 +314,10 @@ function renderShallow(component, options = {}) {
314
314
  imports: standalone ? [component, ...options.imports ?? []] : options.imports ?? [],
315
315
  declarations: standalone ? [] : [component],
316
316
  providers: options.providers ?? [],
317
- schemas: [NO_ERRORS_SCHEMA]
317
+ // Only where it can do something. A standalone component carries its own dependency scope, so a
318
+ // module-level schema never reaches its template — and `enableAngularDiagnostics({ deadSchemas })`
319
+ // is right to report one, which made these two features of the same package cancel each other out.
320
+ ...standalone ? {} : { schemas: [NO_ERRORS_SCHEMA] }
318
321
  });
319
322
  TestBed.overrideComponent(component, { set: buildOverride(component, options) });
320
323
  options.beforeCreate?.();
package/dist/cli.js CHANGED
@@ -2356,7 +2356,7 @@ function checkForeignPragma(graph) {
2356
2356
  }
2357
2357
 
2358
2358
  // src/cli/checks/export-map.generated.ts
2359
- var EXPORT_MAP_VERSION = "4.3.0";
2359
+ var EXPORT_MAP_VERSION = "4.5.0";
2360
2360
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular-http vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin";
2361
2361
  var EXPORTED_BY = {
2362
2362
  AccessorImplementations: "0 1 2 3 11 14 15 16",
@@ -316,6 +316,55 @@ function defineRule(options) {
316
316
  };
317
317
  }
318
318
 
319
+ // src/lib/eslint/dom-reads.ts
320
+ var TEMPLATE_READS = [
321
+ "nativeElement",
322
+ "debugElement",
323
+ "elementRef",
324
+ "querySelector",
325
+ "getComputedStyle",
326
+ "triggerEventHandler",
327
+ "innerHTML",
328
+ "innerText",
329
+ "textContent",
330
+ "getAttribute",
331
+ "classList",
332
+ "shadowRoot",
333
+ "By.css",
334
+ "By.directive"
335
+ ];
336
+ function templatePolicy(context) {
337
+ return Reflect.get(Object(context.options[0]), "templates") === "never" ? "never" : "as-needed";
338
+ }
339
+ var RENDER_MESSAGES = {
340
+ keepTemplate: '`keepTemplate: true` puts the real template back, and this project set `{ templates: "never" }`. Drop it \u2014 or, when the component genuinely reads its own template through `viewChild` or content projection, silence this line, because the alternative is a spec that cannot reach the component at all.',
341
+ preferRenderShallow: "`TestBed.createComponent` pays for compiling the template and instantiating the whole child subtree, and nothing in this file reads either \u2014 no `nativeElement`, no `debugElement`, no `By.css`, no `querySelector`. `renderShallow(X)` brings the same component up through the same `TestBed` with the children dropped and the template blank, and leaves inputs, signals, lifecycle hooks and DI exactly where they were; `fixture` is still a real `ComponentFixture`. What it buys is measured in `bench-angular/`: **0.24\xD7** the per-test cycle at 100 children and **0.05\xD7** at 400 \u2014 but at **zero** children the two are level (measurements straddle 1.0, and it is the noisiest row in the benchmark), so a leaf component gains nothing and this report is worth ignoring there. Reach for `{ keepTemplate: true }` when the component reads its own template through `viewChild` or content projection, which this rule cannot see from the spec."
342
+ };
343
+ function buildsDirectiveHarness(source) {
344
+ return source.includes("createDirectiveHost");
345
+ }
346
+ function readsRenderedTemplate(source) {
347
+ return TEMPLATE_READS.some((member) => source.includes(member));
348
+ }
349
+ function renderShallowSuggestion(context, node) {
350
+ const [component, ...extra] = node.arguments;
351
+ const state = bindingState(context.sourceCode.getScope(node), "renderShallow");
352
+ if (!component || extra.length > 0 || state === "taken") {
353
+ return void 0;
354
+ }
355
+ const replacement = `renderShallow(${context.sourceCode.getText(component)}).fixture`;
356
+ return {
357
+ desc: `Render without the children and the template: ${replacement}`,
358
+ fix: (fixer) => {
359
+ const edits = [fixer.replaceText(node, replacement)];
360
+ if (state === "free") {
361
+ edits.push(insertImport(fixer, `import { renderShallow } from '${PACKAGE}/angular';`));
362
+ }
363
+ return edits;
364
+ }
365
+ };
366
+ }
367
+
319
368
  // src/lib/eslint/floating-assertion.ts
320
369
  var PROMISE_CALLBACK_METHODS = /* @__PURE__ */ new Set(["catch", "finally", "then"]);
321
370
  function isPromiseCallback(fn) {
@@ -1609,6 +1658,30 @@ var noUnregisteredInjectSpy = defineRule({
1609
1658
  };
1610
1659
  }
1611
1660
  });
1661
+ var preferRenderShallow = defineRule({
1662
+ anchor: "-a-components-children",
1663
+ description: "Render through renderShallow() when the spec never reads the rendered template",
1664
+ hasSuggestions: true,
1665
+ schema: [{ type: "object", properties: { templates: { enum: ["as-needed", "never"] } }, additionalProperties: false }],
1666
+ messages: RENDER_MESSAGES,
1667
+ create: (context) => ({
1668
+ 'CallExpression[callee.object.name="TestBed"][callee.property.name="createComponent"]': (node) => {
1669
+ const source = context.sourceCode.getText();
1670
+ if (templatePolicy(context) === "as-needed" ? readsRenderedTemplate(source) : buildsDirectiveHarness(source)) {
1671
+ return;
1672
+ }
1673
+ const suggestion = renderShallowSuggestion(context, node);
1674
+ context.report(
1675
+ suggestion ? { node, messageId: "preferRenderShallow", suggest: [suggestion] } : { node, messageId: "preferRenderShallow" }
1676
+ );
1677
+ },
1678
+ 'Property[key.name="keepTemplate"][value.value=true]': (node) => {
1679
+ if (templatePolicy(context) === "never") {
1680
+ context.report({ node, messageId: "keepTemplate" });
1681
+ }
1682
+ }
1683
+ })
1684
+ });
1612
1685
  var rules = {
1613
1686
  "prefer-provide-auto-spy": preferProvideAutoSpy,
1614
1687
  "prefer-create-spy-from-class": preferCreateSpyFromClass,
@@ -1625,6 +1698,7 @@ var rules = {
1625
1698
  "no-inject-before-override": noInjectBeforeOverride,
1626
1699
  "no-import-time-spread": noImportTimeSpread,
1627
1700
  "no-unregistered-inject-spy": noUnregisteredInjectSpy,
1701
+ "prefer-render-shallow": preferRenderShallow,
1628
1702
  ...jasmineRules
1629
1703
  };
1630
1704
 
@@ -1646,6 +1720,7 @@ var recommendedRules = {
1646
1720
  [`${PLUGIN_NAME}/no-inject-before-override`]: "error",
1647
1721
  [`${PLUGIN_NAME}/no-import-time-spread`]: "error",
1648
1722
  [`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "error",
1723
+ [`${PLUGIN_NAME}/prefer-render-shallow`]: "error",
1649
1724
  [`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
1650
1725
  [`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
1651
1726
  [`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "4.4.0",
3
+ "version": "4.5.1",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -78,7 +78,7 @@ describe('TaskService', () => {
78
78
 
79
79
  One `configureTestingModule` per `describe` — reconfiguring per `it()` recompiles the module every
80
80
  test. Use `mockReadonlyProp(component, 'selected', signal(true))` for the signals of the class under
81
- test, `await stable(fixture)` before asserting zoneless state, `renderShallow` for components. For an
81
+ test, `await stable(fixture)` before asserting zoneless state, `renderShallow` for components — `prefer-render-shallow` reports the `TestBed.createComponent` in a file that reads no markup back, and the rewrite is a suggestion rather than a `--fix` because `renderShallow` configures the module itself. For an
82
82
  `httpResource()`, `await expectRequest(url).flush(body)` from `vitest-auto-spy/angular-http` is the
83
83
  whole dance — the request is issued by `flushEffects()`, not on creation, and the value is settled
84
84
  before the promise resolves; by hand it is six steps and asserting early reads the resource's
@@ -323,7 +323,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
323
323
  Most of this library's guarantees are type-level, so a green run that does not type-check is not
324
324
  done. Report failures with their output rather than describing them as passing.
325
325
 
326
- **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The nineteen rules in
326
+ **After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The twenty rules in
327
327
  `vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
328
328
  to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
329
329
  evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —