vitest-auto-spy 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md 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/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.4.0";
2360
2360
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular-http vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin";
2361
2361
  var EXPORTED_BY = {
2362
2362
  AccessorImplementations: "0 1 2 3 11 14 15 16",
@@ -316,6 +316,48 @@ function defineRule(options) {
316
316
  };
317
317
  }
318
318
 
319
+ // src/lib/eslint/dom-reads.ts
320
+ var TEMPLATE_READS = [
321
+ "nativeElement",
322
+ "debugElement",
323
+ "elementRef",
324
+ "querySelector",
325
+ "getComputedStyle",
326
+ "triggerEventHandler",
327
+ "innerHTML",
328
+ "innerText",
329
+ "textContent",
330
+ "getAttribute",
331
+ "classList",
332
+ "shadowRoot",
333
+ "By.css",
334
+ "By.directive"
335
+ ];
336
+ function templatePolicy(context) {
337
+ return Reflect.get(Object(context.options[0]), "templates") === "never" ? "never" : "as-needed";
338
+ }
339
+ function readsRenderedTemplate(source) {
340
+ return TEMPLATE_READS.some((member) => source.includes(member));
341
+ }
342
+ function renderShallowSuggestion(context, node) {
343
+ const [component, ...extra] = node.arguments;
344
+ const state = bindingState(context.sourceCode.getScope(node), "renderShallow");
345
+ if (!component || extra.length > 0 || state === "taken") {
346
+ return void 0;
347
+ }
348
+ const replacement = `renderShallow(${context.sourceCode.getText(component)}).fixture`;
349
+ return {
350
+ desc: `Render without the children and the template: ${replacement}`,
351
+ fix: (fixer) => {
352
+ const edits = [fixer.replaceText(node, replacement)];
353
+ if (state === "free") {
354
+ edits.push(insertImport(fixer, `import { renderShallow } from '${PACKAGE}/angular';`));
355
+ }
356
+ return edits;
357
+ }
358
+ };
359
+ }
360
+
319
361
  // src/lib/eslint/floating-assertion.ts
320
362
  var PROMISE_CALLBACK_METHODS = /* @__PURE__ */ new Set(["catch", "finally", "then"]);
321
363
  function isPromiseCallback(fn) {
@@ -1609,6 +1651,32 @@ var noUnregisteredInjectSpy = defineRule({
1609
1651
  };
1610
1652
  }
1611
1653
  });
1654
+ var preferRenderShallow = defineRule({
1655
+ anchor: "-a-components-children",
1656
+ description: "Render through renderShallow() when the spec never reads the rendered template",
1657
+ hasSuggestions: true,
1658
+ schema: [{ type: "object", properties: { templates: { enum: ["as-needed", "never"] } }, additionalProperties: false }],
1659
+ messages: {
1660
+ keepTemplate: '`keepTemplate: true` puts the real template back, and this project set `{ templates: "never" }`. Drop it \u2014 or, when the component genuinely reads its own template through `viewChild` or content projection, silence this line, because the alternative is a spec that cannot reach the component at all.',
1661
+ preferRenderShallow: "`TestBed.createComponent` pays for compiling the template and instantiating the whole child subtree, and nothing in this file reads either \u2014 no `nativeElement`, no `debugElement`, no `By.css`, no `querySelector`. `renderShallow(X)` brings the same component up through the same `TestBed` with the children dropped and the template blank, and leaves inputs, signals, lifecycle hooks and DI exactly where they were; `fixture` is still a real `ComponentFixture`. What it buys is measured in `bench-angular/`: **0.24\xD7** the per-test cycle at 100 children and **0.05\xD7** at 400 \u2014 but at **zero** children the two are level (measurements straddle 1.0, and it is the noisiest row in the benchmark), so a leaf component gains nothing and this report is worth ignoring there. Reach for `{ keepTemplate: true }` when the component reads its own template through `viewChild` or content projection, which this rule cannot see from the spec."
1662
+ },
1663
+ create: (context) => ({
1664
+ 'CallExpression[callee.object.name="TestBed"][callee.property.name="createComponent"]': (node) => {
1665
+ if (templatePolicy(context) === "as-needed" && readsRenderedTemplate(context.sourceCode.getText())) {
1666
+ return;
1667
+ }
1668
+ const suggestion = renderShallowSuggestion(context, node);
1669
+ context.report(
1670
+ suggestion ? { node, messageId: "preferRenderShallow", suggest: [suggestion] } : { node, messageId: "preferRenderShallow" }
1671
+ );
1672
+ },
1673
+ 'Property[key.name="keepTemplate"][value.value=true]': (node) => {
1674
+ if (templatePolicy(context) === "never") {
1675
+ context.report({ node, messageId: "keepTemplate" });
1676
+ }
1677
+ }
1678
+ })
1679
+ });
1612
1680
  var rules = {
1613
1681
  "prefer-provide-auto-spy": preferProvideAutoSpy,
1614
1682
  "prefer-create-spy-from-class": preferCreateSpyFromClass,
@@ -1625,6 +1693,7 @@ var rules = {
1625
1693
  "no-inject-before-override": noInjectBeforeOverride,
1626
1694
  "no-import-time-spread": noImportTimeSpread,
1627
1695
  "no-unregistered-inject-spy": noUnregisteredInjectSpy,
1696
+ "prefer-render-shallow": preferRenderShallow,
1628
1697
  ...jasmineRules
1629
1698
  };
1630
1699
 
@@ -1646,6 +1715,7 @@ var recommendedRules = {
1646
1715
  [`${PLUGIN_NAME}/no-inject-before-override`]: "error",
1647
1716
  [`${PLUGIN_NAME}/no-import-time-spread`]: "error",
1648
1717
  [`${PLUGIN_NAME}/no-unregistered-inject-spy`]: "error",
1718
+ [`${PLUGIN_NAME}/prefer-render-shallow`]: "error",
1649
1719
  [`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
1650
1720
  [`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
1651
1721
  [`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun and node:test, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -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 —