vitest-auto-spy 5.24.0 → 5.25.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.
Files changed (73) hide show
  1. package/AGENTS.md +203 -22
  2. package/README.md +57 -33
  3. package/dist/angular-diagnostics.js +10 -10
  4. package/dist/angular-doubles.d.ts +1 -1
  5. package/dist/angular-doubles.js +7 -7
  6. package/dist/angular-http.js +4 -4
  7. package/dist/angular-router.d.ts +11 -1
  8. package/dist/angular-router.js +56 -34
  9. package/dist/angular.d.ts +10 -6
  10. package/dist/angular.js +115 -22
  11. package/dist/bun-angular.d.ts +6 -6
  12. package/dist/bun-angular.js +71 -25
  13. package/dist/bun.d.ts +46 -10
  14. package/dist/bun.js +9 -9
  15. package/dist/{chunk-V6TIK7XU.js → chunk-2ISMAFNN.js} +2 -2
  16. package/dist/{chunk-QO4BPR3V.js → chunk-2XXGISYD.js} +2 -1
  17. package/dist/{chunk-45W5LBPY.js → chunk-34S5ZMWU.js} +5 -5
  18. package/dist/{chunk-U5NJJ4ZN.js → chunk-3MRCVCRZ.js} +1 -1
  19. package/dist/{chunk-DQ3SPSCH.js → chunk-6FIKN2RJ.js} +27 -7
  20. package/dist/{chunk-YEQQIKQR.js → chunk-6RP37RWX.js} +1 -1
  21. package/dist/{chunk-MBSPAUUD.js → chunk-73FGLUVI.js} +3 -3
  22. package/dist/{chunk-BBPYNKGB.js → chunk-7TGT2436.js} +1 -1
  23. package/dist/{chunk-6UETTNQH.js → chunk-FZMWANZV.js} +1 -1
  24. package/dist/{chunk-ZCWAFGHX.js → chunk-IOBBADJP.js} +2 -2
  25. package/dist/{chunk-CPLUHRHD.js → chunk-L6VPMLRJ.js} +1 -1
  26. package/dist/{chunk-MUZZBRUP.js → chunk-R7DIFC2L.js} +2 -2
  27. package/dist/{chunk-RGRZDCM6.js → chunk-ROSQKG22.js} +1 -1
  28. package/dist/{chunk-LZVIH54N.js → chunk-V4XBUP4D.js} +24 -8
  29. package/dist/{chunk-JBV35SU3.js → chunk-V76LFJW4.js} +1 -1
  30. package/dist/{chunk-E5M7DBWB.js → chunk-YCWKPW3N.js} +55 -12
  31. package/dist/{chunk-MG7LKE4R.js → chunk-YZSBSAYZ.js} +1 -1
  32. package/dist/cli.js +438 -125
  33. package/dist/console.d.ts +1 -1
  34. package/dist/console.js +7 -7
  35. package/dist/dom-stubs.d.ts +86 -4
  36. package/dist/dom-stubs.js +167 -19
  37. package/dist/eslint-plugin.cjs +363 -118
  38. package/dist/eslint-plugin.d.ts +4 -2
  39. package/dist/{expect-emission-CD6thEE8.d.ts → expect-emission-DoGUvahc.d.ts} +15 -2
  40. package/dist/index.d.ts +5 -5
  41. package/dist/index.js +95 -16
  42. package/dist/jasmine-compat.d.ts +1 -1
  43. package/dist/jasmine-compat.js +2 -2
  44. package/dist/jasmine.d.ts +2 -2
  45. package/dist/jasmine.js +10 -10
  46. package/dist/nestjs.d.ts +2 -2
  47. package/dist/nestjs.js +10 -10
  48. package/dist/node.cjs +96 -15
  49. package/dist/node.d.cts +2 -0
  50. package/dist/node.d.ts +5 -5
  51. package/dist/node.js +95 -16
  52. package/dist/{package-identity-CqrrGVDm.d.ts → package-identity-BiGXcUoQ.d.ts} +1 -1
  53. package/dist/perf-reporter.d.ts +131 -0
  54. package/dist/perf-reporter.js +4 -1
  55. package/dist/{prop-mock-yHR11U7R.d.ts → prop-mock-C0VtO_pe.d.ts} +11 -8
  56. package/dist/react.d.ts +5 -5
  57. package/dist/react.js +95 -16
  58. package/dist/rstest.d.ts +5 -5
  59. package/dist/rstest.js +9 -9
  60. package/dist/rxjs.d.ts +2 -2
  61. package/dist/rxjs.js +2 -2
  62. package/dist/setup.d.ts +275 -5
  63. package/dist/setup.js +491 -75
  64. package/dist/signal-forms.js +1 -1
  65. package/dist/svelte.d.ts +5 -5
  66. package/dist/svelte.js +95 -16
  67. package/dist/{track-injections-L6IgAUL7.d.ts → track-injections-DLl6ra2D.d.ts} +1 -1
  68. package/dist/{track-signal-runs-DsBgqliM.d.ts → track-signal-runs-DS7_MCPC.d.ts} +18 -4
  69. package/dist/{types-BuQtoidb.d.ts → types-CUf4fME5.d.ts} +47 -7
  70. package/dist/vue.d.ts +6 -6
  71. package/dist/vue.js +95 -16
  72. package/package.json +7 -2
  73. package/skills/vitest-auto-spy/SKILL.md +7 -0
package/AGENTS.md CHANGED
@@ -15,7 +15,8 @@ Working on the library's own source instead? Read `CONTRIBUTING.md` in the repos
15
15
  Setting this up for a team? `npx vitest-auto-spy init` writes a pointer to this file into the
16
16
  instruction files this repository's agents actually read — `AGENTS.md` (Codex, Cursor, Copilot and
17
17
  most of the field), `CLAUDE.md` (Claude Code) and `GEMINI.md` (Gemini CLI), plus the glob-scoped
18
- rule file of any tool whose own directory already exists. `--check` is the CI form. Full table:
18
+ rule file of any tool whose own directory already exists. `--check` is the CI form; `--only
19
+ CLAUDE.md,.claude` limits it to the files a repository keeps untracked. Full table:
19
20
  <https://asdalexey.github.io/vitest-auto-spy/agents>.
20
21
 
21
22
  | Resource | Where |
@@ -131,7 +132,7 @@ Do you have a real class at runtime?
131
132
 
132
133
  Object already built, other code already holds it?
133
134
  → createSpyFromInstance(obj, config?) → Spy<T>, patched IN PLACE
134
- One standalone function? → createFunctionSpy<Fn>('name')
135
+ One standalone function? → createFunctionSpy<Fn>('name') → FunctionSpy<Fn>
135
136
  Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
136
137
  → only a shape? mockConstructor<T>(() => instance)
137
138
  → on a global? stubConstructor(globalThis, 'Image', factory)
@@ -154,6 +155,20 @@ with an optional `overrides` parameter relies on that. A fixture that means "no
154
155
  `undefined` itself: `getters.profile.mockReturnValue(undefined)`, never
155
156
  `mockReturnValue(createMock<Profile>(undefined))`.
156
157
 
158
+ A fixture **outside** its type on purpose — the `null` or array a backend sends where the type says
159
+ object, a payload that has to reach a runtime guard — is `outOfType<T>(value)`: nothing is checked,
160
+ the call site says so, and `prefer-create-mock` / `no-ts-expect-error-on-double` accept it where they
161
+ would report `as unknown as T` or a `@ts-expect-error`. It takes its type from the slot, so
162
+ `mockValueProp(job, 'status', outOfType('UNKNOWN'))` stays on the key-checked overload. Everything else
163
+ stays `createMock<T>()`.
164
+ On a strict double, `spy.m.mockReturnValue(outOfType(undefined))` reaches a defensive
165
+ `?? of(null)` branch of a method typed to return an `Observable`, and counts as configured.
166
+
167
+ Under `strict`, `returns: { open: undefined }` marks a method configured whose answer nobody reads
168
+ (an ngrx `rxMethod` ref, a snack-bar ref) — accepted for any return type. It is also how a
169
+ type-only double under `strict` takes its void calls, one key each:
170
+ `createAutoMock<CanvasRenderingContext2D>(undefined, { strict: true, returns: { save: undefined, restore: undefined } })`.
171
+
157
172
  **`createSpyFromInstance(instance, config?)` is the one case where nothing is constructed.** Every
158
173
  factory above builds the double, which is no help once the object exists and other code already
159
174
  points at it — a service a factory built, a third-party client, a half-real `TestBed.inject(X)`. It
@@ -528,14 +543,17 @@ Four conventions that carry most of the value:
528
543
  Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementation`,
529
544
  `toHaveBeenCalledWith` and the rest all work as usual. On top of that:
530
545
 
531
- | Return type | Helpers added |
532
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
533
- | anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)` |
534
- | `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
535
- | `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
546
+ | Return type | Helpers added |
547
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
548
+ | anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)`. On a `Promise` or `Observable` method the handle carries that row's helpers instead — `calledWith(id).nextWith(v)`, `.resolveWith(v)`; `.mockReturnValue` there is a `TS2339` |
549
+ | `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
550
+ | `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
536
551
 
537
552
  `Observable` **properties** (not just methods) get the same helpers — list them in
538
- `observablePropsToSpyOn`.
553
+ `observablePropsToSpyOn`. The prop spy is a plain `Observable`: a `BehaviorSubject` member loses
554
+ `.value` / `getValue()`, and code that reads them gets `undefined`. Seed a real one instead —
555
+ `overrides: { isActive$: new BehaviorSubject(false) }` — and drive it with `.next()`; the same holds
556
+ for a stream the test pushes into itself.
539
557
 
540
558
  **What counts as an `Observable` is structural, and no declaration names rxjs (4.0.0).** A member
541
559
  earns the observable bundle when its type satisfies the exported `ObservableLike<T>` — `subscribe`
@@ -1154,13 +1172,14 @@ across realms.
1154
1172
  emits, the callback never runs and nothing is asserted. Invert it — **the assertion is the `await`**:
1155
1173
 
1156
1174
  ```ts
1157
- import { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission } from 'vitest-auto-spy';
1175
+ import { expectAllEmissions, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission } from 'vitest-auto-spy';
1158
1176
 
1159
1177
  await expect(expectEmission(component.visible$)).resolves.toBe(true); // the first VALUE, not a list
1160
1178
  await expect(expectEmission(tasks$)).resolves.toEqual({ id: 1 }); // the task itself, not `[task]`
1161
1179
  await expect(expectEmissions(source$, 3)).resolves.toEqual([1, 2, 3]); // the list is this one
1162
1180
  await expectNoEmission(source$, { timeout: 50 });
1163
1181
  await expectCompletion(service.purgeCache()); // "it finished" — the value is not the point
1182
+ await expect(expectAllEmissions(source$)).resolves.toEqual([1, 2]); // every value, and none after
1164
1183
  ```
1165
1184
 
1166
1185
  Options: `{ timeout, label }`. `timeout` defaults to `1000` ms (`0` for `expectNoEmission`, whose
@@ -1350,8 +1369,9 @@ real getters plus a `createFunctionSpy` per method — this is usually why.
1350
1369
 
1351
1370
  **The second overload is a normal tool, not a last resort.** Each helper has a checked overload
1352
1371
  (`K extends keyof T`) and a `(object, property: PropertyKey, value: unknown)` one behind it, and
1353
- the JSDoc calls the latter an escape hatch for `#private` fields. In practice it carries about half
1354
- of the real calls, all of them legitimate:
1372
+ the JSDoc calls the latter the one for members the public type does not describe. In practice it
1373
+ carries about half of the real calls, all of them legitimate. It cannot reach a JS `#private` field —
1374
+ no property key can — and `'x' as keyof T` over one patches a new, unrelated key that nothing reads:
1355
1375
 
1356
1376
  ```ts
1357
1377
  mockValueProp(router, 'routerState', { snapshot: { url: '/home' } }); // a partial fixture of a fat type
@@ -1447,6 +1467,12 @@ every one after it reads the real member. `propsOutsideHooks` grades that — `'
1447
1467
  `'throw'`, `'off'` — and `reportPropsOutsideHooks(reaction)` sets the same dial for a suite that
1448
1468
  wires its own hooks. Move the call into `beforeEach`.
1449
1469
 
1470
+ **Adopting `setupAutoSpy()` in a suite that restored its patches once per file** — one
1471
+ `restoreMockedProps()` in an `afterAll` — changes a patch from "lives for the file" to "lives for one
1472
+ test". On 176 files of one such suite, 3 broke, each a `mock*Prop` in a `describe` body or
1473
+ `beforeAll`, and the `'warn'` report is easy to miss in a full run. Run the first time with
1474
+ `setupAutoSpy({ propsOutsideHooks: 'throw' })`, fix what it names, then drop the option.
1475
+
1450
1476
  **The one that only bites at scale:** with `isolate: false`, a `setTimeout` or
1451
1477
  `requestAnimationFrame` a component schedules and never clears keeps running after its file is done,
1452
1478
  and fires while the **next** file is mid-test. It is reported against that innocent file, as
@@ -1566,6 +1592,72 @@ One entry is never pruned, and it is not one of yours: the Vitest adapter regist
1566
1592
  `vi.clearAllMocks()` into a silent no-op for every double in the run, so it carries a mark the
1567
1593
  pruner skips. Why the library needs a mock of its own for that is the next section.
1568
1594
 
1595
+ **The one that answers for every file that follows:** `vi.spyOn(localStorage, 'setItem')` puts the
1596
+ mock on the storage as an own property, and happy-dom's `Storage` hands each instance out wrapped
1597
+ in a Proxy whose `deleteProperty` trap only knows stored items. `mockRestore()` — and
1598
+ `vi.restoreAllMocks()` walking the same path — deletes through that trap, comes back green, and
1599
+ does nothing: the spy keeps recording, and a later spec's own `vi.spyOn` is handed the same mock
1600
+ with the previous file's calls already in it, so "not to have been called" fails every other run.
1601
+ Probing by running `vi.restoreAllMocks()` rules nothing out — it is the failing path itself. Writing
1602
+ `Storage.prototype`'s method back over the spy does go through the proxy; that asymmetry, define
1603
+ passes and delete does not, is the repair, and it is on by default:
1604
+
1605
+ ```ts
1606
+ setupAutoSpy(); // restoreStorageSpies: true — the sweep runs at the file boundary only
1607
+ ```
1608
+
1609
+ `restoreStorageSpies()` is the one-shot (it returns the storages it repaired); a suite that
1610
+ deliberately keeps a spy on a storage method for a whole worker turns the option off. jsdom breaks
1611
+ the other half of the same contract — its Storage proxy turns a `defineProperty` of a method into a
1612
+ stored item — which is why the gate is `vi.isMockFunction` rather than "differs from the
1613
+ prototype": a junk item is left alone, a mock is repaired.
1614
+
1615
+ **The one that outlives its component:** a listener on `window` or `document` is not the
1616
+ component's to take off. Overlays, portals and services register on the shared targets, and under
1617
+ `isolate: false` whatever a file leaves registered fires during the next one — the same wrong-file
1618
+ blame as a stray timer, with none of the visibility: nothing errors, the callback simply runs
1619
+ against mocks and a DOM it was never written for.
1620
+
1621
+ ```ts
1622
+ setupAutoSpy({ strayListeners: true }); // wrap addEventListener, sweep what the file added
1623
+ ```
1624
+
1625
+ The split is the one `pruneMockRegistry` uses: a `beforeAll` marks the listeners already on
1626
+ `window`/`document` — registered while the module graph was evaluated, a framework's one-time
1627
+ initialisation — and the `afterAll` takes off everything added since. The pieces are exported too:
1628
+ `trackStrayListeners()` (idempotent, returns the undo), `baselineStrayListeners()`,
1629
+ `removeStrayListeners()` (returns how many), `countStrayListeners()` (throws before
1630
+ `trackStrayListeners()` has run, like the timer counter) and `describeStrayListeners()` — each
1631
+ stray's target, type, spec file and up to five frames, the list `onStrayListeners({ removed, listeners })`
1632
+ receives when a file should fail instead: `onStrayListeners: ({ removed }) => expect(removed).toBe(0)`.
1633
+ Under jsdom a listener registered with `{ once: true }` that already fired stays counted until
1634
+ something removes it: the wrapper cannot observe the firing without breaking identity-based
1635
+ `removeEventListener` from the code under test, and removing an already-fired listener is a no-op
1636
+ anyway. happy-dom detaches it through the public `removeEventListener`, so there it leaves the count.
1637
+
1638
+ **The one no registry tracks:** `vi.stubGlobal` is undone by `unstubGlobals` and
1639
+ `vi.spyOn(globalThis, …)` by `restoreMocks`, but a plain assignment —
1640
+ `global.ResizeObserver = stub` — is written straight into the shared worker and read by every later
1641
+ file. `guardGlobals` names the _unrepairable_ case, a non-configurable redefine; this is the net
1642
+ for the repairable one:
1643
+
1644
+ ```ts
1645
+ setupAutoSpy({ restoreGlobals: true }); // one snapshot per worker, restored at every file boundary
1646
+ ```
1647
+
1648
+ `captureGlobalBaseline()` takes the snapshot — the first call is the worker's truth, a later one
1649
+ does nothing, so a re-capture can never launder a replacement into the baseline. `restoreGlobals()`
1650
+ is the sweep, and returns the keys it changed. Two traps it steps around, both load-bearing. A DOM
1651
+ environment installs window properties on `globalThis` as accessor pairs forwarding to an override
1652
+ map, so `global.ResizeObserver = stub` leaves the descriptor untouched and the restore has to write
1653
+ the captured value back through the same setter, not just re-define the descriptor. And the
1654
+ library's own wrappers — `setTimeout` under `strayTimers`, `addEventListener` under
1655
+ `strayListeners` — are marked as theirs, so the restore steps around them instead of uninstalling
1656
+ the tracking at the first boundary. Added keys are never deleted (a framework that installs a
1657
+ global at import time must not lose it), and the identity globals — `location`, `document`,
1658
+ `window` and their kin — are never written back at all. A leftover fake clock comes off first,
1659
+ because restoring timer descriptors under an installed fake breaks both.
1660
+
1569
1661
  ### What a method spy is, and the one thing that differs from `vi.fn()`
1570
1662
 
1571
1663
  A method spy is **not** a `vi.fn()`. It is this library's own mock function: one shared prototype
@@ -1768,6 +1860,28 @@ allows `node:fs` in `dist/setup.js` for exactly that. Silent outside the builder
1768
1860
  window, and on a Node before `getBuiltinModule`. `setupAutoSpy({ angularBuildHint: false })` turns
1769
1861
  it off.
1770
1862
 
1863
+ **Strict for one run:** `VITEST_AUTO_SPY_STRICT=1 npx vitest run <slice>` makes `setupAutoSpy()` arm
1864
+ `strict` whatever the setup file passed (`0` turns it off, `survey` counts instead of throwing); a second `setupAutoSpy()` in an extra setup
1865
+ file also works now, without grading the first call's network stubs as patched outside a hook.
1866
+
1867
+ **Moving a suite to `strict`:** most of the churn is the same few methods configured file by file.
1868
+ Run the suite once in survey mode, which refuses nothing and ends each file with the list strict mode
1869
+ would have refused, most frequent first (`setupAutoSpy({ strict: 'survey' })` is the same in code):
1870
+
1871
+ ```bash
1872
+ VITEST_AUTO_SPY_STRICT=survey npx vitest run
1873
+ ```
1874
+
1875
+ The top of that list — `SvgIconService.getIcon`, `NotificationsService.open` — goes into the setup
1876
+ file once, `registerAutoSpyDefaults(NotificationsService, { returns: { open: undefined } })`, and
1877
+ every double of the class answers it; the tail is what each spec configures itself.
1878
+
1879
+ **One setup file for the builder and for plain Vitest:** the builder initialises `TestBed` before any
1880
+ setup file, so a second `initTestEnvironment()` throws "Cannot set base providers because it has
1881
+ already been called". Guard it with `isAngularUnitTestBuilder()` from `/setup` — the same marker the
1882
+ hint reads — and name the file in the target's `setupFiles`, which is the only way the builder runs
1883
+ it (`doctor` reports `builder-setup-unreached` otherwise).
1884
+
1771
1885
  One more field of the same family differs quietly and changes only the report:
1772
1886
  `slowTestThreshold` is `5` in Jest (**seconds**) and `300` in Vitest (**milliseconds**), so a
1773
1887
  migrated suite starts marking most of its files slow. That is a unit change, not a regression.
@@ -2006,7 +2120,8 @@ frame outside `node_modules`:
2006
2120
  before the guard armed are taken off.
2007
2121
  - `strayConsole: { allow: ['…', /…/] }` is the last resort, for environment noise no spec can reach.
2008
2122
  Output written to `process.stdout` / `process.stderr` directly, and jsdom's own virtual console, is
2009
- not seen.
2123
+ not seen. An Angular suite that resets modules under `isolate: false` allows `/NG0912/`, Angular's import-time
2124
+ component-ID collision warning, which the guard otherwise blames on whichever file is importing.
2010
2125
  - The wrapper forwards every call unchanged, so Vitest's attribution and `onConsoleLog` still work.
2011
2126
  `guardStrayConsole(reaction)` registers the same guard on its own.
2012
2127
 
@@ -2117,7 +2232,9 @@ to a bare `TestBed.tick()`; `src/zone-tests/stable.zone-test.ts` is the guard.
2117
2232
  `flushEventLoopUntil(isDone, { turns, label })` is the same thing with a condition and a budget —
2118
2233
  for a chunk becoming reachable, an SDK reporting itself ready, a queue draining. Use it instead of a
2119
2234
  hand-tuned turn count: the count depends on the dependency, not on the spec, and a condition that
2120
- never holds fails naming the `label` rather than hanging until the runner's timeout.
2235
+ never holds fails naming the `label` rather than hanging until the runner's timeout. For real I/O —
2236
+ a socket round-trip, a child process — pass `{ timeoutMs, label }` instead of `turns`: it polls the
2237
+ real clock every 10 ms, unaffected by fake timers, and replaces a hand-rolled `waitFor(predicate, ms)`.
2121
2238
 
2122
2239
  **Not for an Angular `resource()` / `httpResource()`.** Those need a change-detection _tick_, and
2123
2240
  this helper only takes event-loop turns — a resource awaited through it finishes the whole budget
@@ -2180,7 +2297,10 @@ for that last reason: a class with a static named `calls` would otherwise shadow
2180
2297
  log. **The statics are not typed** — the return type is still `ConstructorSpy<T>`, so reading one
2181
2298
  needs a cast at the spec; type them and this note goes.
2182
2299
 
2183
- For the three observers, prefer the purpose-built stubs (§13). For `AbortController` — which breaks
2300
+ For the three observers, prefer the purpose-built stubs (§13). For a `Worker` the code builds with
2301
+ `new Worker(new URL(…, import.meta.url))`, prefer `stubWorker({ respond })` from `/dom-stubs`: an
2302
+ `EventTarget` whose listeners all receive the reply, answered on a microtask after `postMessage`
2303
+ returns, with `last.messages`, `last.emit(data)` and `last.fail(error)` on the handle. For `AbortController` — which breaks
2184
2304
  in a jsdom run for a reason involving none of the three parties in the stack trace — use
2185
2305
  `stubAbortController()`.
2186
2306
 
@@ -2241,6 +2361,9 @@ it('forgets the token on logout', () => {
2241
2361
  });
2242
2362
  ```
2243
2363
 
2364
+ Writes in the middle of a test go through `.storage`, the installed `Storage` itself:
2365
+ `local.storage.setItem('token', 'xyz')`. The stub is not a `Storage`; `local.setItem` is a `TS2339`.
2366
+
2244
2367
  It goes through `mockValueProp`, so `restoreMockedProps()` (and `setupAutoSpy()` between tests) puts
2245
2368
  the previous storage back; install it in `beforeEach`. It also lands on `document.defaultView` when
2246
2369
  that is a separate object. It is not `restoreWebStorage()`: that one repairs a broken environment
@@ -2453,9 +2576,21 @@ selected.set(true); // every computed reading it updates
2453
2576
  Use it whenever the value has to change during the test. `mockReadonlyProp` stays right when the
2454
2577
  value is fixed for the whole test and you never need the handle.
2455
2578
 
2579
+ A `protected` or TS-`private` signal is reached too: the key is checked against the public type
2580
+ first, and a key that type does not describe takes the value's own type instead —
2581
+ `mockSignalProp(component, 'saving', true)`. `mockResourceProp` works the same way. A public key never
2582
+ falls through to that form, so its value is still checked.
2583
+
2456
2584
  A `linkedSignal()` is a writable signal like any other, so `mockSignalProp` covers it and there is
2457
2585
  no separate helper to go looking for.
2458
2586
 
2587
+ **An ngrx `signalStore()`'s state members go through `mockSignalProp` too**, on the store double —
2588
+ `mockSignalProp(store, 'currentAngle', null)`, then `.set(angle)` mid-test. A state slice is a
2589
+ signal the template reads during the first render; a method spy there answers `undefined`, and
2590
+ `mockImplementation(() => value())` is re-read by nothing downstream. A nullable object slice is
2591
+ typed `DeepSignal<A> | Signal<null>`: `overrides` accepts one `signal<A | null>()` for it, but only
2592
+ `mockSignalProp` hands back the handle.
2593
+
2459
2594
  **It patches as little as it can, and the order rule follows from that.** A member that is already
2460
2595
  writable — `signal()`, `model()`, `linkedSignal()` — is **written through**, not replaced: Angular
2461
2596
  links a consumer to the signal it read, not to the property it read it through, so replacing one
@@ -2558,6 +2693,19 @@ TestBed.overrideComponent(ProfileComponent, { remove: { providers: [DeleteAccoun
2558
2693
  the TestBed compiler, which `overrideProvider` alone does not do. Reach for the `remove` form when
2559
2694
  the module already provides the spy and the component's declaration is simply in the way.
2560
2695
 
2696
+ Under `renderShallow` the override goes in `beforeCreate`, which runs after the testing module is
2697
+ configured and before the component is created:
2698
+
2699
+ ```ts
2700
+ let menu: Spy<NavigationBuilderService>;
2701
+
2702
+ renderShallow(CatalogPageComponent, {
2703
+ beforeCreate: () => {
2704
+ menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService);
2705
+ },
2706
+ });
2707
+ ```
2708
+
2561
2709
  ### `injectSpy` cannot reach a component-level provider
2562
2710
 
2563
2711
  `injectSpy(X)` reads the **global** `TestBed` injector. A provider declared on the component
@@ -3196,13 +3344,24 @@ component's cleanup throws:
3196
3344
  const load = Object.assign(vi.fn(), { destroy: vi.fn() });
3197
3345
  ```
3198
3346
 
3347
+ Under `strict`, a call whose ref nobody reads is configured with `returns: { load: undefined }`. When
3348
+ the code does read the ref (it calls `destroy`), seed one from the method's own type — ngrx does not
3349
+ export `RxMethodRef`: `returns: { load: createAutoMock<ReturnType<Store['load']>>() }`.
3350
+
3351
+ A `signalStore()` class keeps its methods and `rxMethod`s on the instance, not the prototype, so
3352
+ discovery finds none of them. `provideAutoSpy(Store, { fillMissing: true, returns: { … } })` answers
3353
+ every such member with a spy — `returns` and `strict` apply to them as to any method — instead of a
3354
+ long `instanceMethodsToSpyOn` list.
3355
+
3199
3356
  ```ts
3200
3357
  // shallow rendering — configureTestingModule + NO_ERRORS_SCHEMA + overrideComponent, in one call
3201
3358
  const { fixture, component } = renderShallow(TaskListComponent, {
3202
3359
  providers: [provideAutoSpy(TaskService)],
3203
3360
  inputs: { projectId: 42 }, // signal inputs take the VALUE, not the signal
3204
3361
  });
3205
- // other options: imports, keepTemplate, keepChildren, template, beforeCreate, detectChanges
3362
+ // other options: imports, keepTemplate, keepModules, keepChildren, keepHostDirectives, template, beforeCreate, detectChanges
3363
+ // COVERAGE: the override recompiles the component under JIT for the rest of the file (keepTemplate too),
3364
+ // so its AOT template/host branches leave coverage — keep one real TestBed render first where they matter
3206
3365
 
3207
3366
  // change an input after the first render — one setInput per key, then the wait, in one await
3208
3367
  await setInputs(fixture, { projectId: 7 }); // a name the component does not declare is refused here
@@ -3675,6 +3834,8 @@ export default [
3675
3834
  `autoSpy.configs.typeErrors` is a second flat config holding the subset whose findings are compile
3676
3835
  errors — `prefer-as-spy` (`TS2352`) and `no-mocked-for-spy` (`TS2322`). Spread its `rules` after a
3677
3836
  blanket downgrade so those keep their severity; do not copy the two names into a consumer's config.
3837
+ `autoSpy.configs.strict` is `recommended` with every rule at `error`, instead of mapping
3838
+ `Object.keys(autoSpy.rules)` by hand.
3678
3839
 
3679
3840
  | Rule | Level | Fix | Flags |
3680
3841
  | --------------------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -3720,7 +3881,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3720
3881
  | `no-mock-cast` | `error` | suggest | a cast to Vitest's `Mock` / `MockInstance` over a **member access** — `TestBed.inject(S).m as Mock` → `injectSpy(S).m`, which the suggestion writes whenever the token is in view (directly, or through a `const` the file settled with a `TestBed.inject`). `Mock` with no parameters is `Mock<any>`: `mockReturnValue` takes anything and `toHaveBeenCalledWith` stops comparing arguments. `(spy.m.mockReturnValue as Mock)(…)` gets its own message — the cast is on the member that installs the answer, so neither the value nor the method's return type is checked. The name must resolve to an import from `vitest` / `@rstest/core` / `bun:test` / `jest`, or to nothing at all, so a domain `Mock` is left alone |
3721
3882
  | `no-redundant-mock-reset` | `error` | `--fix` / suggest | a mock reset the runner already performs between tests — `vi.clearAllMocks()` / `resetAllMocks()` / `restoreAllMocks()`, `x.mockClear()` / `mockReset()` / `mockRestore()` — standing where nothing the file wrote ran since the runner's own: the first statement of a `beforeEach` no other `beforeEach` precedes, or a clear as the last statement of an `afterEach` → delete it, and the hook with it where that is all it held. A restore or reset in `afterEach` / `afterAll` is never reported: after the last test it is what takes a spy off before the `afterAll` hooks. **Silent** until the flags are known: options `{ clearMocks, restoreMocks, mockReset }` (or `{ configFile }` naming the runner config) first, then a `vitest.config.*` / `vite.config.*` searched upwards from the file and read as text. Flag matches call, never family — `restoreMocks` walks only the spies `vi.spyOn` installed. `--fix` only in a file whose only before-hook it is; a suggestion otherwise; never inside a test body |
3722
3883
  | `no-unasserted-argument` | `warn` | — | a bare `expect(spy).toHaveBeenCalled()` on evidence from the file: the same subject asserted with `toHaveBeenCalledWith(…)` in another test, or a title containing `with` over a body whose every assertion is a bare call → `toHaveBeenCalledWith(…)` / `mustBeCalledWith(…)`. `not.toHaveBeenCalled()` and the counting matchers are never reported |
3723
- | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
3884
+ | `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so list those classes in `{ ignoreComponents: ['CardComponent'] }` (a spec naming one is left alone) or keep a `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
3724
3885
  | `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`, through `TestBed`, `getTestBed()`, a chain of those, or a name holding one → drop the `await`, and the `async` of a hook that then awaits nothing else; the suggestion does both. Reads **no types**, so it reports without `parserOptions.project`, where `@typescript-eslint/await-thenable` cannot. `inject` and `runInInjectionContext` are never reported: each answers whatever the token or the callback holds, which can be a promise |
3725
3886
  | `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
3726
3887
  | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
@@ -3916,7 +4077,8 @@ packages, which a subpath export can never be.
3916
4077
 
3917
4078
  | Message contains | Cause | Fix |
3918
4079
  | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3919
- | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
4080
+ | `No mock adapter registered` | no runtime entry was imported, or the wrong one — or the setup file that imports it never ran: an `@angular/build:unit-test` / `@nx/angular:unit-test` target runs only the `setupFiles` it names (`doctor` reports `builder-setup-unreached`) | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies; under the unit-test builder, name the setup file in the target's `setupFiles` |
4081
+ | `No "X" export is defined on the "<m>" mock`, in a spec whose `vi.mock('<m>')` has no factory | another spec file mocked `<m>` with a factory, and under `isolate: false` Vitest hands that factory to a later automock or `{ spy: true }` mock of the same module (`doctor` reports `module-mock-leak`) | mock it the same way in both files, or `vi.resetModules()` in this file before importing it |
3920
4082
  | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
3921
4083
  | `… is not subscribable (…)` | the argument is not an observable — most often the value it emits, or a promise | pass the observable itself; `await` a promise directly (§8) |
3922
4084
  | ``… the `advance` callback threw: …`` | the `{ advance }` callback failed; the original is on `cause` and the subscription is torn down | fix the callback — a `vi.runAllTimers()` with no fake timers installed is the usual one (§8) |
@@ -3976,6 +4138,9 @@ packages, which a subpath export can never be.
3976
4138
  | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
3977
4139
  | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
3978
4140
  | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
4141
+ | a spy on a storage method still answering after `vi.restoreAllMocks()` probed green | happy-dom's Storage proxy deletes only stored items, so the restore behind `mockRestore()` never lands; a later spec's `vi.spyOn` gets the same mock with the old calls in it | on by default — `setupAutoSpy()` repairs it at the file boundary; `restoreStorageSpies()` on its own, or `restoreStorageSpies: false` for a worker-lifetime spy (§10) |
4142
+ | a listener callback running against mocks and a DOM it was never written for, in a file that never added it | a `window`/`document` listener a previous file left registered, under `isolate: false` | `setupAutoSpy({ strayListeners: true })` — module-graph registrations stay, per-file ones go at the boundary (§10) |
4143
+ | a global a previous file patched (`ResizeObserver` answering a stub) with no file named | a raw assignment no registry tracks — not `vi.stubGlobal`, not `vi.spyOn` | `setupAutoSpy({ restoreGlobals: true })` puts the worker's baseline back at every file boundary; `guardGlobals` for the non-configurable case (§10) |
3979
4144
  | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
3980
4145
  | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
3981
4146
  | `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
@@ -4028,6 +4193,13 @@ packages, which a subpath export can never be.
4028
4193
  | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
4029
4194
  | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic and keeps the `readonly` of the source member — deliberately: on a member replaced by a **spied accessor** the assignment reaches the setter spy and the getter still answers `undefined`, so the type error is the loud version of a silent no-op | `mockValueProp(spy, 'X', value)` — it defines rather than assigns, so it works in both cases; `Mutable<Spy<T>>` is the opt-in for plain data members, and `Reflect.set` is **not** a way round it |
4030
4195
  | `TS2345` from `mockReturnValue` / `mockImplementation` / `mockResolvedValue` on a double that used to compile | the mock surface is `MockInstance<Method>`, so the stub is now checked against the method's own return type | stub with what the method returns; if the double is meant to answer something else, the method's type or the spec's expectation is wrong |
4196
+ | `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` on `mockReturnValue(double)` | 5.1.0: the stub is checked against the method, and a double of the return type is a `Spy<X>`, not an `X` | `mockReturnValue(asInstance(double))` (§6) |
4197
+ | `with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties` on a stub | the stub's type spells an optional member `?: T` where the method's spells `?: T \| undefined` (or the reverse), or a fixture writes `key: undefined`; a `Subject` is invariant, so its type argument has to match exactly | `method.returnSubject()` for a subject typed from the method; omit the key instead of writing `undefined` |
4198
+ | `Types of parameters 'x' and 'y' are incompatible` on `mockImplementation(…)` | 5.1.0: the implementation is checked against the method's parameters, and an annotation in it disagrees | drop the parameter annotations and let the method type them; narrow inside the body |
4199
+ | `TS4111` / `TS2375` / `TS2322` inside a `mockImplementation` body that used to compile | before 5.1.0 the implementation's parameters were `any`, so nothing in its body was checked | fix the body — the error is ordinary once its parameters are typed |
4200
+ | `TS2349: This expression is not callable` on a value read from `spy.mock.calls` | 5.1.0 types `mock.calls` from the method; a listener taken from `addEventListener`'s calls is `EventListenerOrEventListenerObject` | narrow before calling: `if (typeof listener === 'function') listener(event)` |
4201
+ | `is not assignable to parameter of type 'RxMethod<…>'` (or another member type) on `mockReadonlyProp(x, 'm', vi.fn(impl))` | `vi.fn` infers its type from the member the helper replaces, so `impl` must return what the member returns | return the member's type — `Object.assign(vi.fn(), { destroy: vi.fn() })` for an `rxMethod` |
4202
+ | `Argument of type '() => A \| null' is not assignable to parameter of type '(() => A) \| (() => null)'` | the member is a union of call signatures — `@ngrx/signals`' `DeepSignal<A> \| Signal<null>` for a nullable slice — and 5.1.0–5.24.0 typed its stub as the union of both halves | upgrade past 5.24.0, which types the stub as one signature returning the union |
4031
4203
  | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
4032
4204
  | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
4033
4205
  | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
@@ -4043,7 +4215,7 @@ packages, which a subpath export can never be.
4043
4215
  | `createNestUnit(X).spies.get(Y): Y is in \`expose\`, so the unit got a real instance` | an exposed class is real, not a spy | read the spies of its own collaborators, or drop it from `expose` |
4044
4216
  | `expect(component.ngOnInit).toHaveBeenCalled()` fails after `fixture.detectChanges()`, or a hook's `.mockImplementation` never runs | the spy was put on the **instance**; a view calls the hook it read off the class prototype when the component was created | `vi.spyOn(Cls.prototype, 'ngOnInit')` before `TestBed.createComponent`, or assert what the hook does — `no-instance-lifecycle-spy` (§16) |
4045
4217
  | `TS2345: Argument of type 'InjectionToken<X>' is not assignable to parameter of type 'ClassType<unknown>'`, on `registerAutoSpyDefaults` | the core entry's `registerAutoSpyDefaults` takes a class only — it may not name Angular's token type | import `registerAutoSpyDefaults` (and `clearAutoSpyDefaults`) from `vitest-auto-spy/angular`, which also takes an `InjectionToken` (§5) |
4046
- | `selfReturning names 'x', which is not a spied method` | the name is not a method of the double — a typo, or a restricting `onlyMethodsToSpyOn` left it out; on a type-driven double, `then` and `constructor` are never spies | fix the name or the whitelist; the value is silently never returned otherwise (§5) |
4218
+ | `selfReturning names 'x', which is not a spied method` | the name is not a method of the double — a typo, or a restricting `onlyMethodsToSpyOn` left it out; on a type-driven double, `then` and `constructor` are never spies; a method the constructor assigns (an ngrx `rxMethod`) is not on the prototype | fix the name or the whitelist, or list a constructor-assigned method in `instanceMethodsToSpyOn`; the value is silently never returned otherwise (§5) |
4047
4219
  | a spec green over a `useValue` fixture whose key the code never reads — `queryParams$` on an `ActivatedRoute` | `useValue` is `any`, so a renamed or misspelled key of an object literal compiles | the member's real name — `no-unknown-use-value-key` (§16), with type information; or `provideAutoSpy(X, { overrides })`, which the compiler checks |
4048
4220
  | a spec runs down the "browser" (or any flag-on) branch whatever its `useValue` says | `{ provide: IS_BROWSER, useValue: {} }` for an `InjectionToken<boolean>` — `useValue` is `any`, and an object is truthy | a value of the token's type (`useValue: false`) — `no-mistyped-use-value` (§16), with type information |
4049
4221
  | `no request matched …` (with the requests that were made listed) | the URL, verb or predicate does not describe anything the code under test sent, or nothing subscribed | read the list in the message — it is the requests that were actually made; then fix the URL, add `{ method }`, or subscribe (§13) |
@@ -4115,6 +4287,7 @@ diagnostic and is free to change: print it, never assert on it.
4115
4287
  | `{ ...modelInstance, flag: true }` (drops every getter) | `withOverrides(modelInstance, { flag: true })` |
4116
4288
  | `if ('params' in link) … else throw` in every spec | `narrow.byKey(link, 'params')` |
4117
4289
  | `const x = a?.b; assert.exists(x); … x …` on every optional read | `narrow.defined(a?.b)` — it returns the value, so the narrowing sits in the expression |
4290
+ | `expect(body).toBeInstanceOf(FormData); const form = body as FormData` | `narrow.instanceOf(body, FormData)` — the check and the type in one expression |
4118
4291
  | five `asInstance(...)` in one call | `...asInstances(a, b, c, d, e)` |
4119
4292
  | `vi.stubGlobal('Image', vi.fn(() => ({ src: '' })))` | `stubConstructor(globalThis, 'Image', () => ({ src: '' }))` |
4120
4293
  | `Object.defineProperty(document, 'cookie', { value })` | `mockValueProp(document, 'cookie', value)` |
@@ -4213,7 +4386,8 @@ suite that runs green but does not type-check is not done.
4213
4386
  `doctor` is read-only and finds what a green run cannot: a `tsconfig` `include` pattern that
4214
4387
  matches no file (so it type-checks nothing while `tsc --noEmit` still reports success), a
4215
4388
  production module importing a `*.spec.ts`, a spec importing another spec, a `@jest-environment`
4216
- pragma the runner never reads, and configuration left behind for a runner that is gone. Three of
4389
+ pragma the runner never reads, configuration left behind for a runner that is gone, and a setup
4390
+ file an Angular or Nx unit-test target never runs because the target does not name it. Three of
4217
4391
  its checks are about coverage, where the run is green and the report is simply not the one the
4218
4392
  config describes — `coverage.all` on a Vitest that stopped reading the key, a source-only
4219
4393
  `coverage.include` in a runner config over a bundle, and a scope so large that `picomatch`
@@ -4261,11 +4435,18 @@ command, an unknown flag, an unreadable `--only` / `--from` value, a path matchi
4261
4435
 
4262
4436
  **Reading the output from a script: `--format json`**, on `doctor` and on `perf`. One JSON document
4263
4437
  on stdout — `schema`, `exitCode`, `tally` (`errors`, `warnings`, `notes`), every finding with
4264
- `check`, `severity`, `file`, `message`, `fix`; `perf` adds `run`, `budgets` and `gate.verdicts` (one row
4438
+ `check`, `severity`, `file`, `message`, `fix`; `perf` adds `run` (with `slowestFiles`, the `--top` slowest files per phase), `budgets` and `gate.verdicts` (one row
4265
4439
  per candidate, `outcome` one of `confirmed`, `not reproduced`, `unconfirmed`, `single reading`,
4266
4440
  `over budget`). Parse that rather than the text: the text is wrapped to the terminal (80 columns in a
4267
4441
  pipe), groups one cause found in many files into one block, and ends in a tally line that starts
4268
- with `N errors, N warnings, N notes`.
4442
+ with `N errors, N warnings, N notes`. `--format markdown` renders the same document as tables — for a
4443
+ merge request note or a job summary, not for parsing.
4444
+
4445
+ **`doctor --ignore <check,…>`** leaves the named checks out of the report, the tally, the exit code
4446
+ and the `--code-quality` file. Use it only for a finding the repository has answered in a way
4447
+ `doctor` cannot see — `no-agent-instructions` in CI where the instruction files are kept out of git,
4448
+ `angular-build-splitting-off` under a patched builder. `--min-severity` hides findings from the text
4449
+ only; `--ignore` removes them.
4269
4450
 
4270
4451
  ### If you were asked why a suite is slow
4271
4452
 
@@ -4321,7 +4502,7 @@ npx vitest-auto-spy perf --command 'npm test -- {paths:--include=}' --gate
4321
4502
  `--command` puts `VITEST_AUTO_SPY_PERF_OUT` and `VITEST_AUTO_SPY_PERF_REPORTER` in that command's
4322
4503
  environment; the configuration it reaches attaches the reporter itself (`reporters: perf ===
4323
4504
  undefined ? ['default'] : ['default', perf]`), and the reporter writes nothing at all when the first
4324
- variable is unset, so it can be declared permanently. `{paths}` / `{paths:<prefix>}` is where the
4505
+ variable is unset, so it can be declared permanently — by name, `vitest-auto-spy/perf-reporter`. `{paths}` / `{paths:<prefix>}` is where the
4325
4506
  files of a confirmation pass go.
4326
4507
 
4327
4508
  **`--gate` is the only part of this command that fails anything**, and three rules keep it honest.