vitest-auto-spy 5.49.0 → 5.51.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 (110) hide show
  1. package/AGENTS.md +192 -195
  2. package/CHANGELOG.md +527 -1
  3. package/README.md +2097 -2706
  4. package/agent-docs/adapters.md +72 -10
  5. package/agent-docs/angular.md +185 -155
  6. package/agent-docs/anti-patterns.md +1 -1
  7. package/agent-docs/checklist.md +49 -17
  8. package/agent-docs/configuration.md +23 -16
  9. package/agent-docs/doubles.md +48 -5
  10. package/agent-docs/errors.md +35 -6
  11. package/agent-docs/eslint.md +36 -50
  12. package/agent-docs/factories.md +48 -66
  13. package/agent-docs/migration-jasmine.md +9 -2
  14. package/agent-docs/observables.md +28 -8
  15. package/agent-docs/properties.md +2 -5
  16. package/agent-docs/return-helpers.md +35 -18
  17. package/agent-docs/setup.md +127 -48
  18. package/dist/angular-diagnostics.js +27 -17
  19. package/dist/angular-doubles.d.ts +214 -3
  20. package/dist/angular-doubles.js +276 -11
  21. package/dist/angular-http.d.ts +12 -5
  22. package/dist/angular-http.js +16 -15
  23. package/dist/angular-matchers.js +6 -4
  24. package/dist/angular-router.d.ts +3 -4
  25. package/dist/angular-router.js +7 -7
  26. package/dist/angular.d.ts +33 -8
  27. package/dist/angular.js +277 -34
  28. package/dist/bun-angular.d.ts +7 -7
  29. package/dist/bun-angular.js +83 -36
  30. package/dist/bun.d.ts +6 -1168
  31. package/dist/bun.js +13 -10
  32. package/dist/{chunk-GG7BQKEK.js → chunk-4JY7KIJG.js} +1 -1
  33. package/dist/{chunk-TXDBDMIL.js → chunk-75WPOZKE.js} +1 -1
  34. package/dist/{chunk-QGLUX55Q.js → chunk-7LZ6V5UO.js} +185 -43
  35. package/dist/{chunk-FOL5RH22.js → chunk-F7UPX6YV.js} +1 -1
  36. package/dist/{chunk-M5H32F4Q.js → chunk-GHKBJN2T.js} +80 -30
  37. package/dist/chunk-HRE4K6N3.js +30 -0
  38. package/dist/{chunk-5FQZGJLJ.js → chunk-IATDAQMO.js} +8 -2
  39. package/dist/{chunk-3JL2YWIG.js → chunk-JCTHGJTY.js} +20 -8
  40. package/dist/{chunk-MNY4ZHEQ.js → chunk-JMBAECZN.js} +38 -21
  41. package/dist/{chunk-XEIOGUGP.js → chunk-M4CUD2XH.js} +1 -1
  42. package/dist/{chunk-PUUSCWDD.js → chunk-MDWE3FCP.js} +90 -11
  43. package/dist/chunk-MXKNUVYR.js +38 -0
  44. package/dist/chunk-OXONPFSL.js +217 -0
  45. package/dist/{chunk-3ZBRBPOL.js → chunk-R43PMWFH.js} +1 -1
  46. package/dist/{chunk-UH3KWLVL.js → chunk-RHGDQQNC.js} +3 -2
  47. package/dist/{chunk-HVNKTPKG.js → chunk-SF4GKKN4.js} +1 -9
  48. package/dist/chunk-SOROJD3W.js +9 -0
  49. package/dist/chunk-T5MQSNRX.js +23 -0
  50. package/dist/chunk-TIXTHSW4.js +216 -0
  51. package/dist/{chunk-Q7L4AWI7.js → chunk-UOKFQQ5H.js} +1 -1
  52. package/dist/{chunk-I6CB4CNT.js → chunk-UTOPAD66.js} +160 -92
  53. package/dist/{chunk-QDBRZQJO.js → chunk-VLEU3CWT.js} +5 -2
  54. package/dist/chunk-ZVF3E6T7.js +18 -0
  55. package/dist/cli-FXW5AFQ5.js +2234 -0
  56. package/dist/cli-SELXGLDQ.js +819 -0
  57. package/dist/cli.js +2350 -3563
  58. package/dist/console.d.ts +26 -15
  59. package/dist/console.js +91 -31
  60. package/dist/core.d.ts +1146 -0
  61. package/dist/diagnostics.d.ts +2 -2
  62. package/dist/diagnostics.js +3 -0
  63. package/dist/dom-stubs.d.ts +130 -3
  64. package/dist/dom-stubs.js +578 -76
  65. package/dist/{expect-emission-DMQZF5QR.d.ts → emission-sync-xEda8ZCk.d.ts} +20 -2
  66. package/dist/eslint-plugin.cjs +963 -185
  67. package/dist/index.d.ts +6 -5
  68. package/dist/index.js +595 -224
  69. package/dist/jasmine-compat.d.ts +3 -4
  70. package/dist/jasmine-compat.js +2 -2
  71. package/dist/jasmine.d.ts +4 -3
  72. package/dist/jasmine.js +28 -10
  73. package/dist/nest-unit-Bg6JWPgu.d.ts +59 -0
  74. package/dist/nestjs.d.ts +4 -60
  75. package/dist/nestjs.js +13 -175
  76. package/dist/node.cjs +552 -156
  77. package/dist/node.d.cts +2 -0
  78. package/dist/node.d.ts +6 -6
  79. package/dist/node.js +552 -158
  80. package/dist/observer-spy.d.ts +39 -6
  81. package/dist/observer-spy.js +41 -8
  82. package/dist/{package-identity-5kYzh3ad.d.ts → package-identity-COBYU0WJ.d.ts} +4 -2
  83. package/dist/perf-profiler.js +26 -3
  84. package/dist/perf-reporter.d.ts +35 -1
  85. package/dist/perf-reporter.js +132 -17
  86. package/dist/{prop-mock-WOpRQPUc.d.ts → prop-mock-CE9gNMPY.d.ts} +18 -8
  87. package/dist/react.d.ts +6 -5
  88. package/dist/react.js +1 -1
  89. package/dist/rstest.d.ts +5 -6
  90. package/dist/rstest.js +14 -11
  91. package/dist/rxjs.d.ts +4 -5
  92. package/dist/rxjs.js +12 -6
  93. package/dist/setup.d.ts +50 -8
  94. package/dist/setup.js +480 -118
  95. package/dist/signal-forms.js +4 -3
  96. package/dist/svelte.d.ts +6 -5
  97. package/dist/svelte.js +1 -1
  98. package/dist/{track-injections-CYQKc3M8.d.ts → track-injections-C6zITWXE.d.ts} +1 -1
  99. package/dist/{track-signal-runs-CUdp5dJZ.d.ts → track-signal-runs-DDzbFmGc.d.ts} +32 -8
  100. package/dist/{types-Dxw-oWfr.d.ts → types-qYoxQi5C.d.ts} +127 -7
  101. package/dist/vitest-mock-types-B17-xaWt.d.ts +10 -0
  102. package/dist/vue.d.ts +7 -6
  103. package/dist/vue.js +1 -1
  104. package/dist/zone.d.ts +2 -1
  105. package/dist/zone.js +24 -12
  106. package/package.json +24 -9
  107. package/skills/vitest-auto-spy/SKILL.md +51 -39
  108. package/dist/chunk-FQ6NT2IN.js +0 -64
  109. package/dist/chunk-IB72SVKT.js +0 -23
  110. package/dist/chunk-NGZYYSEA.js +0 -54
package/AGENTS.md CHANGED
@@ -1,25 +1,39 @@
1
1
  # vitest-auto-spy — instructions for AI coding agents
2
2
 
3
- You are looking at the agent-facing reference for **`vitest-auto-spy`**: typed test spies generated
4
- from a class, a type, or nothing at all, on Vitest / `bun:test` / `node:test` / Rstest.
3
+ Reference for an agent **writing tests with `vitest-auto-spy`** in someone else's project: typed
4
+ test spies generated from a class, a type, or nothing at all, on Vitest, `bun:test`, `node:test` and
5
+ Rstest. It ships in the npm package, so it works offline: `node_modules/vitest-auto-spy/AGENTS.md`.
6
+ Working on the library's own source? Read `CONTRIBUTING.md` in the repository instead.
5
7
 
6
- This file is written for an agent **using** the library in someone's test suite. It is shipped
7
- inside the npm package, so it is readable with no network:
8
+ The spec you will write most often, an Angular service with a spied store and HTTP (the full recipe
9
+ is §3):
8
10
 
9
- ```
10
- node_modules/vitest-auto-spy/AGENTS.md
11
- ```
11
+ ```ts
12
+ import { TestBed } from '@angular/core/testing';
13
+ import { type Spy, injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
14
+ import { expectRequest, provideHttpTesting } from 'vitest-auto-spy/angular-http';
12
15
 
13
- Working on the library's own source instead? Read `CONTRIBUTING.md` in the repository.
16
+ let store: Spy<CartStore>;
14
17
 
15
- Setting this up for a team? `npx vitest-auto-spy init` writes a pointer to this file into the
16
- instruction files this repository's agents actually read — `AGENTS.md` (Codex, Cursor, Copilot and
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; `--only
19
- CLAUDE.md,.claude` limits it to the files a repository keeps untracked. A hand-made copy of the
20
- package's skill in `.claude/skills/vitest-auto-spy/` is reported `stale` (and fails `--check`): delete
21
- it and re-run `init`. Full table:
22
- <https://asdalexey.github.io/vitest-auto-spy/agents>.
18
+ beforeEach(() => {
19
+ TestBed.configureTestingModule({ providers: [...provideHttpTesting(), provideAutoSpy(CartStore)] });
20
+ store = injectSpy(CartStore);
21
+ });
22
+
23
+ it('posts the order', async () => {
24
+ const order = TestBed.inject(OrderService).checkout(); // Promise<Order>, sent through HttpClient
25
+
26
+ await expectRequest('/api/orders', { method: 'POST' }).flush({ id: 1 });
27
+ await expect(order).resolves.toEqual({ id: 1 });
28
+ expect(store.clear).toHaveBeenCalled();
29
+ });
30
+ ```
31
+
32
+ To point a team's agents at this file, run `npx vitest-auto-spy init`: it adds a pointer to
33
+ `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` and the rule files of tools already set up, all or nothing.
34
+ `--check` is the CI form and fails on a `stale` hand-made skill copy (delete it, re-run `init`) or an
35
+ `edited` managed block. `--only CLAUDE.md,.claude` limits it to untracked files. Full table:
36
+ <https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
23
37
 
24
38
  | Resource | Where |
25
39
  | --- | --- |
@@ -80,60 +94,47 @@ Add-ons, orthogonal to the runner:
80
94
  | Add-on | Import | Needed for |
81
95
  | --- | --- | --- |
82
96
  | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once**, in a setup file, a spec, or a `.d.ts` the `tsconfig` includes (§4) |
83
- | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
84
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `useConsoleSpies()` in the `describe` (or `installConsoleSpies()` per test, `restoreConsole()` after); `consoleOutput()` is everything logged, keyed by channel |
85
- | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord`. **Moved off the root in 4.0.0** |
86
- | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. **Moved off the root in 4.0.0** |
97
+ | observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). |
98
+ | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` — `useConsoleSpies()` in the `describe` (or `installConsoleSpies()` per test, `restoreConsole()` after); `consoleOutput()` is everything logged, keyed by channel, `consoleLines()` the same in call order. No `vitest` import: loads on every runner |
99
+ | DOM stubs | `vitest-auto-spy/dom-stubs` | `stubIntersectionObserver` / `stubResizeObserver` / `stubMutationObserver` / `stubObserver`, `stubMediaElement`, `stubAbortController`, `stubWebStorage` (§12), `stubAnimationFrame`, `stubElementRect`, `intersectionEntry` / `resizeEntry` / `mutationRecord`, `createElementStub` (an `HTMLElement` for `ElementRef`), `fillMissingDomApis` (setup file). |
100
+ | Run diagnostics | `vitest-auto-spy/diagnostics` | `compareTestRuns`, `summarizeTestRun`, `formatTestRunComparison`, `diffByField`. |
87
101
  | Angular HTTP | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest` — `httpResource()` / `HttpClient` (§13). Optional `@angular/common` peer, this entry only |
88
102
  | Angular router | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute` — an `ActivatedRoute` whose streams and snapshot share one record; `provideRouterDouble`, `injectRouterDouble` — a `Router` whose URL, `routerState` and `events` agree (§13). Optional `@angular/router` peer, this entry only |
89
- | Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular` like `/angular-http` — no core re-export; **moved off `/angular` in 5.21.0** so importing spies stops evaluating it |
90
- | Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio and the `Window`/`Document` platform doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box; **moved off `/angular` in 5.21.0** |
91
- | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular` — no core re-export; **moved off `/angular` in 5.21.0** |
103
+ | Angular diagnostics | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family (§13). Companion to `/angular`, no core re-export |
104
+ | Angular doubles | `vitest-auto-spy/angular/doubles` | The Material dialog trio, the `Window`/`Document` doubles, `providePlatform`, and `DomSanitizer` / `ChangeDetectorRef` / CDK `Overlay` doubles (§13). Companion to `/angular`; registers the Vitest adapter, so its doubles spy out of the box |
105
+ | Angular matchers | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` (§13). Companion to `/angular`, no core re-export |
92
106
  | Signal forms | `vitest-auto-spy/signal-forms` | `createForm`, `registerFormMatchers` — a signal form built where `form()` can inject, and `toHaveFieldErrors` over what it produced (§13). Optional `@angular/forms` peer, this entry only; Angular 22+ |
93
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
107
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()`, `withFakeTimers()`, `blockNetwork()`, `stubResponse()`; the entry imports Vitest, so it is not for `bun test` |
94
108
  | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
95
109
  | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
96
110
 
97
- `vitest-auto-spy/jasmine` is Vitest-only, because its types are Vitest's. On `bun test` and
98
- `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
99
-
100
- `vitest-auto-spy/setup` is Vitest-only as well — `setupAutoSpy()` registers Vitest hooks. On
101
- `bun test` nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy between tests;
102
- put `afterEach(() => { restoreMockedProps(); mock.restore(); })` in a `--preload` file, and any
103
- `mock.module()` there too (inside a test it swaps bindings after the original module already ran).
104
-
105
- The rxjs peer is **>= 7.2**, not `>=7`. The observable layer used to pull `concatMap`, `delay`,
106
- `switchMap`, `take`, `takeUntil` and `takeWhile` from `rxjs/operators`; rxjs 8 removes that deep path,
107
- so the specifier moved to the root `rxjs` entry — which is where rxjs re-exported them in 7.2 — and
108
- the floor moved with it. Any Angular project already satisfies it: Angular 16 through 22 all peer on
109
- `^6.5.3 || ^7.4.0`.
110
-
111
- The Angular entries need **Angular >= 20** — `@angular/core`, `@angular/common`,
112
- `@angular/platform-browser` and `@angular/router`, all optional peers on the same range. Below 20 the failure is a link
113
- error, not a missing helper: `/angular` imports `ɵSIGNAL` (Angular 18+) as a value on the first line
114
- of its bundle, so on 16 and 17 the whole entry fails to load, and `/bun-angular` imports
115
- `provideZonelessChangeDetection` (Angular 20+; called `provideExperimentalZonelessChangeDetection`
116
- in 18 and 19), so on anything older the preload throws before the first spec. Angular 19 and
117
- everything under it is out of Angular's own support window as well, so the floor cuts nothing that
118
- still gets fixes. `@angular/platform-browser` is a declared peer since this major — `By` in the
119
- directive matchers and `platformBrowserTesting()` in the Bun preload are both value imports of it,
120
- and under pnpm's isolated layout it never resolved before.
121
-
122
- The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
123
- CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
124
- Vitest refuses to be required). The plugin's CommonJS declaration is an `export =`, which is what
125
- `require('vitest-auto-spy/eslint-plugin')` actually returns — an `eslint.config.cts` / `.cjs` used to
126
- type-check the call that throws and reject the one that works.
127
-
128
- `vitest-auto-spy/package.json` resolves as well. Tools that read a dependency's manifest by specifier
129
- — Storybook, Nx, a renovate helper — used to get `ERR_PACKAGE_PATH_NOT_EXPORTED` and had nowhere to
130
- go from there.
131
-
132
- **Every peer is optional now, `vitest` included.** The range is unchanged (`>=2.1.0`); what changed
133
- is that a suite on `/bun` or `/node`, which never loads the Vitest runner, no longer installs it to
134
- satisfy a peer. The one thing still to know is that `/bun`, `/bun-angular` and `/node` declarations
135
- name Vitest's `Mock` type, so a project type-checking those entries without `vitest` installed will
136
- want it as a devDependency anyway; freeing that is a type change and waits for a major.
111
+ **Runner limits of two add-ons.** `/jasmine` and `/setup` import Vitest, so they are Vitest-only.
112
+
113
+ - `bun test` / `node --test` and jasmine's API: call `enableJasmineCompat()` from
114
+ `vitest-auto-spy/jasmine-compat`.
115
+ - `bun test` and teardown: nothing restores a `spyOn`, a `mock*Prop` patch or a file-scope auto-spy
116
+ between tests. Put `afterEach(() => { restoreMockedProps(); mock.restore(); })` in a `--preload`
117
+ file, with any `mock.module()` (inside a test it swaps bindings after the module already ran).
118
+
119
+ **Requirements.**
120
+
121
+ - rxjs **>= 7.2** (not `>=7`); every Angular 16–22 project already has it.
122
+ - The Angular entries need **Angular >= 20**: `@angular/core`, `@angular/common`,
123
+ `@angular/platform-browser`, `@angular/router`, all optional peers on one range. Below 20 the
124
+ `/bun-angular` preload throws before the first spec.
125
+ - The package is **ESM**. Only `/node` and `/eslint-plugin` also ship CommonJS; every other entry
126
+ is ESM-only, and `require()` of a Vitest-backed one throws. `require('vitest-auto-spy/eslint-plugin')` is typed as `export =`.
127
+ - Every peer is optional, `vitest` included (range `>=2.1.0`). `/node`, `/bun`, `/bun-angular`,
128
+ `/rstest`, `/jasmine-compat`, `/angular-router`, `/console`, `/nestjs`, `/observer-spy` and `/rxjs`
129
+ type-check with no Vitest installed and `skipLibCheck: false`. A Vitest suite that imports only
130
+ runner-free entries gets spies without `mockThrow` until one file adds
131
+ `import type {} from 'vitest-auto-spy';`.
132
+ `@angular/compiler` (`/angular/matchers`, `/bun-angular`) and `@rstest/core` (`/rstest`) are
133
+ optional peers too.
134
+ - `vitest-auto-spy/package.json` resolves, for tools that read a dependency's manifest. No
135
+ declaration needs `@types/node` or `lib: esnext.disposable`.
136
+
137
+ Why these floors: <https://asdalexey.github.io/vitest-auto-spy/core/compatibility>.
137
138
 
138
139
  ---
139
140
 
@@ -153,16 +154,13 @@ In [`agent-docs/factories.md`](./agent-docs/factories.md). Read it before writin
153
154
 
154
155
  ## 3. The 90% recipe
155
156
 
156
- Measured across a ~370-file Angular suite: `provideAutoSpy` appears in 371 files, `injectSpy` in
157
- 308, `mockReadonlyProp` in 127, `instanceMethodsToSpyOn` in 103, `observablePropsToSpyOn` in 79 —
158
- and bare `createSpyFromClass` in only 41. **In an Angular app the spy almost always arrives through
159
- DI.** Write that shape first.
157
+ In an Angular app the spy almost always arrives through DI: `provideAutoSpy` in the providers,
158
+ `injectSpy` to read it. Write that shape first.
160
159
 
161
160
  ```ts
162
- import { signal } from '@angular/core';
163
161
  import { TestBed } from '@angular/core/testing';
164
- import { of } from 'rxjs';
165
- import { type Spy, injectSpy, mockReadonlyProp, provideAutoSpy } from 'vitest-auto-spy/angular';
162
+ import { type Spy, injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
163
+ import { expectRequest, provideHttpTesting } from 'vitest-auto-spy/angular-http';
166
164
 
167
165
  describe('TaskService', () => {
168
166
  let projects: Spy<ProjectStore>;
@@ -172,25 +170,33 @@ describe('TaskService', () => {
172
170
  beforeEach(() => {
173
171
  TestBed.configureTestingModule({
174
172
  providers: [
175
- provideAutoSpy(NotificationService), // plain service — nothing to configure
176
- provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current', 'isEmpty'] }), // signals
177
- provideAutoSpy(NewsFeedService, { observablePropsToSpyOn: ['connected$'] }), // Observable props
173
+ ...provideHttpTesting(), // HttpClient + the testing backend
174
+ provideAutoSpy(NotificationService), // plain service: nothing to configure
175
+ provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current'] }), // a signal member
176
+ provideAutoSpy(NewsFeedService, { observablePropsToSpyOn: ['connected$'] }), // an Observable property
178
177
  ],
179
178
  });
180
179
 
181
180
  projects = injectSpy(ProjectStore);
182
181
  feed = injectSpy(NewsFeedService);
183
182
 
184
- feed.connected$.nextWith(true); // seed the defaults every test needs, once
185
- projects.save.mockReturnValue(of(true));
183
+ projects.current.mockReturnValue({ id: 1 }); // defaults every test needs, once
184
+ projects.fetchAll.resolveWith([]); // a Promise method
185
+ feed.connected$.nextWith(true); // needs `import 'vitest-auto-spy/rxjs'` in the setup file
186
186
 
187
187
  service = TestBed.inject(TaskService);
188
188
  });
189
189
 
190
- it('saves through the store', () => {
191
- service.save(task);
190
+ it('saves through the API and tells the store', async () => {
191
+ let saved: Task | undefined;
192
+ service.save(task).subscribe((result) => (saved = result)); // HttpClient.post inside
192
193
 
193
- expect(projects.save).toHaveBeenCalledWith(task);
194
+ const request = expectRequest('/api/tasks', { method: 'POST' });
195
+ expect(request.request.body).toEqual(task);
196
+ await request.flush({ ...task, id: 7 }); // await flush(), not subscribe()
197
+
198
+ expect(saved?.id).toBe(7);
199
+ expect(projects.refresh).toHaveBeenCalled(); // every method is a spy, configured or not
194
200
  });
195
201
  });
196
202
  ```
@@ -214,15 +220,28 @@ it('loads', async () => {
214
220
  });
215
221
  ```
216
222
 
217
- Four conventions that carry most of the value:
223
+ | Member of the dependency | How to declare it | How to seed it |
224
+ | --- | --- | --- |
225
+ | method returning a value | nothing | `spy.m.mockReturnValue(v)` or `spy.m.calledWith(args).mockReturnValue(v)` |
226
+ | method returning `Promise<T>` | nothing | `spy.m.resolveWith(v)` / `rejectWith(e)` (§4) |
227
+ | method returning `Observable<T>` | nothing | `spy.m.nextWith(v)` / `throwWith(e)` (§4, needs `/rxjs`) |
228
+ | `Observable` property (`items$`) | `observablePropsToSpyOn: ['items$']` | `spy.items$.nextWith(v)` |
229
+ | signal / `computed` member of a dependency | `instanceMethodsToSpyOn: ['items']` | `spy.items.mockReturnValue(v)`; a value that changes mid-test: `mockSignalProp` (§13) |
230
+ | plain data field | `overrides: { field: value }` | set in the provider (§5) |
231
+
232
+ Rules that carry most of the value:
218
233
 
219
- 1. **One `configureTestingModule` per `describe`** — reconfiguring per `it()` pays for module
220
- compilation on every test, the largest avoidable cost in an Angular suite.
234
+ 1. **One `configureTestingModule` per `describe`.** Reconfiguring per `it()` compiles the module for
235
+ every test, the largest avoidable cost in an Angular suite.
221
236
  2. **Declare each spy as `Spy<T>`, never as `T`** (§6).
222
237
  3. **Seed defaults in `beforeEach`, override in the test.** An unconfigured method returns
223
- `undefined`, and the failure surfaces far from its cause.
224
- 4. **`provideAutoSpy` is lazy by default** — listing a wide service costs nothing for the methods a
238
+ `undefined`, and the failure surfaces far from its cause. Seed before `TestBed.inject(ClassUnderTest)`
239
+ when its constructor or field initialisers read the dependency.
240
+ 4. **`provideAutoSpy` is lazy by default**, so listing a wide service costs nothing for the methods a
225
241
  test never touches.
242
+ 5. **Two import lines.** `provideAutoSpy`, `injectSpy` and the `Spy<T>` type come from
243
+ `vitest-auto-spy/angular`; `createSpyFromClass`, `createAutoMock`, `asSpy` and `asInstance` only
244
+ from `vitest-auto-spy`.
226
245
 
227
246
  ---
228
247
 
@@ -241,6 +260,7 @@ In [`agent-docs/configuration.md`](./agent-docs/configuration.md). Read it when
241
260
 
242
261
  <!-- agents-outline: agent-docs/configuration.md -->
243
262
 
263
+ - `instanceMethodsToSpyOn` for members discovery cannot see
244
264
  - The composition belongs to the class — `registerAutoSpyDefaults`
245
265
  - `strict` — a method nobody configured throws instead of answering `undefined`
246
266
  - Getters and setters live in `accessorSpies`
@@ -251,114 +271,89 @@ In [`agent-docs/configuration.md`](./agent-docs/configuration.md). Read it when
251
271
 
252
272
  ## 6. `Spy<T>` is not assignable to `T` — this is intentional
253
273
 
254
- `Spy<T>` is a **mapped type**, so it drops `#private` and `private` members.
255
-
256
- ```ts
257
- let users: Spy<UserService> = createSpyFromClass(UserService); // ✅
258
- let users: UserService = createSpyFromClass(UserService); // ❌ private members missing
259
- ```
260
-
261
- Do **not** patch this with `as any`, `as unknown as T`, or `@ts-expect-error`. Use the named views:
274
+ `Spy<T>` is a **mapped type**, so it drops `#private` and `private` members. Declare doubles as
275
+ `Spy<T>`, and convert at the boundary with the two named views:
262
276
 
263
277
  ```ts
264
278
  import { asInstance, asSpy } from 'vitest-auto-spy';
265
279
 
266
- asInstance(spy); // Spy<T> → T, for an API typed against the class
267
- asSpy(TestBed.inject(CartService)); // T → Spy<T>, for the helpers
268
- ```
280
+ let users: Spy<UserService> = createSpyFromClass(UserService); // ✅
281
+ let users: UserService = createSpyFromClass(UserService); // ❌ private members missing
269
282
 
270
- Both are the same object at runtime. `injectSpy(X)` already returns `Spy<X>`.
283
+ new ProfileFacade(asInstance(users)); // Spy<T> → T, for an API typed against the class
284
+ const cart = asSpy(TestBed.inject(CartService)); // T → Spy<T>, for the helpers
285
+ ```
271
286
 
272
- The compiler reports this in four different ways, none of which contains both the words "spy" and
273
- "instance", which is why the fix is hard to find from the message alone:
287
+ Both are the same object at runtime. `injectSpy(X)` already returns `Spy<X>`. Do **not** patch the
288
+ mismatch with `as any`, `as unknown as T` or `@ts-expect-error`.
274
289
 
275
- | Message | Direction | Fix |
290
+ | Compiler message | Direction | Fix |
276
291
  | --- | --- | --- |
277
292
  | `TS2352: … 'accessorSpies' is missing in type 'Router'` | `T` → spy | `asSpy(TestBed.inject(Router))` |
278
293
  | `TS2739` / `TS2740: Type 'Spy<X>' is missing the following properties from type 'X'` | spy → `T` | `asInstance(spy)` |
279
294
  | `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` | spy → `T` | `asInstance(spy)` |
280
- | `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not `Mocked<T>` |
295
+ | `is missing the following properties: _modalOpened, body, …` (private names) | — | declare `Spy<T>`, not Vitest's `Mocked<T>` (it keeps private members, so the list reads like an incomplete double) |
281
296
 
282
- That last row is its own trap: Vitest's `Mocked<T>` keeps `T`'s **private** members, so the error
283
- lists private field names and reads as "the double is incomplete". It is not — the declaration is
284
- wrong. `Spy<T>` covers the public surface on purpose.
285
-
286
- **A generic class needs its type argument spelled out.** `TestBed.inject` infers from the
287
- constructor and produces `Service<any>`, and the `any` surfaces much later as a mismatch between
288
- `AddPromiseSpyMethods<unknown>` and `WithMockReturnValue<…>` — eight levels deep, and nothing about
289
- it says "type parameter":
297
+ **A generic class: spell the type argument out.** `TestBed.inject` infers `Service<any>`, and the
298
+ `any` surfaces far away as a mismatch between `AddPromiseSpyMethods<unknown>` and
299
+ `WithMockReturnValue<…>`:
290
300
 
291
301
  ```ts
292
302
  const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); // ✅
293
- const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
303
+ const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
304
+ const modal = injectSpy<ModalRef<PurchaseOptions>>(ModalRef); // a particular instantiation
294
305
  ```
295
306
 
296
- `injectSpy(X)` without the argument keeps a declared default when the constructor does not take the
297
- type parameter. When it does — `constructor(public data: T, …)`, the shape of most modal refs — 5.19.0
298
- inferred `X<never>`, and with the typed `accessorSpies` bag `Spy<X<never>>` no longer assigns to
299
- `Spy<X<unknown>>`. Such a class is now read at its **constraint**: `ModalRef<T = unknown>` gives
300
- `Spy<ModalRef<unknown>>`, `ConfigService<T extends Config = Defaults>` gives
301
- `Spy<ConfigService<Config>>`. A constructor cannot hand TypeScript the default here, so spell the
302
- argument out whenever the default or a particular instantiation is what the spec means:
303
- `injectSpy<ConfigService>(ConfigService)`, `injectSpy<ModalRef<PurchaseOptions>>(ModalRef)`.
304
-
305
- It applies to `createSpyFromClass` with a configuration too, in one combination: an accessor list
306
- (or `overrides`) **and** `returns` on a generic class. TypeScript checks a generic class argument
307
- after the configuration, reads `T` back from `gettersToSpyOn: ['flagsConfig']` as
308
- `{ flagsConfig: any }`, and fails with `'isKeyEnabled' does not exist in type
309
- 'MethodReturns<{ flagsConfig: any; }>'`. Spell it out — `createSpyFromClass<FlagsConfigService>(…)`.
310
- `provideAutoSpy`, `overrideAutoSpy`, `overrideComponentProvider` and the class overload of
311
- `registerAutoSpyDefaults` (`/angular`) take `T` from the class alone, so there the inferred form
312
- compiles; the core `registerAutoSpyDefaults` needs the argument spelled out like
313
- `createSpyFromClass`. A token key needs nothing: `T` comes from the `InjectionToken<T>` itself.
314
-
315
- **A declared default now reaches the double, and used not to.** `class FlagsConfigService<T =
316
- FlagsConfigDefaults>` handed to `createSpyFromClass` / `injectSpy` inferred `T` as `unknown`, so
317
- every member typed against it read as `unknown` on a class that had said exactly what it should be.
318
- Two shapes caused it, both fixed: the `& { [key: string]: any }` intersection `ClassType<T>` used to
319
- carry (an index signature makes inference drop the default) and the union in `injectSpy`'s token
320
- parameter (a union does too — a class now matches a bare construct-signature overload first).
321
- Spelling the argument out still works and is still the answer when the class has **no** default.
322
-
323
- **A generic _method_ is a different thing, and it does collapse.** `show<T, U>(component: Type<T>,
324
- data: U)` on a modal or factory service reaches `Spy<T>` through `Parameters` / `ReturnType`, which
325
- instantiate the method's own type parameters — so the double types as `show(component: Type<unknown>,
326
- data: unknown)`. No mapped type in TypeScript can preserve a generic signature, so this is a limit
327
- rather than a defect, and it is usually harmless: `unknown` accepts every argument and every seeded
328
- return, so `spy.show.calledWith(MyModal, data).mockReturnValue(ref)` compiles. It bites only where
329
- the instantiated type appears in a **contravariant** position; there, name the shape on a declaration
330
- of your own and assign the factory's result to it.
307
+ - `injectSpy(X)` without the argument keeps a declared default (`class X<T = Defaults>`). When the
308
+ constructor takes the type parameter (`constructor(public data: T)`), it reads the **constraint**:
309
+ `ModalRef<T = unknown>` gives `Spy<ModalRef<unknown>>`. Spell the argument out when the spec means
310
+ the default or one instantiation.
311
+ - `createSpyFromClass` needs the argument in one combination: a generic class with an accessor list
312
+ (or `overrides`) **and** `returns`. The symptom: `'isKeyEnabled' does not exist in type
313
+ 'MethodReturns<{ flagsConfig: any; }>'`. Write `createSpyFromClass<FlagsConfigService>(…)`. The
314
+ core `registerAutoSpyDefaults` needs it the same way.
315
+ - `provideAutoSpy`, `overrideAutoSpy`, `overrideComponentProvider` and the `/angular` class overload
316
+ of `registerAutoSpyDefaults` take `T` from the class alone. A token needs nothing: `T` comes from
317
+ `InjectionToken<T>`.
318
+
319
+ **A generic method collapses to `unknown`.** `show<T, U>(component: Type<T>, data: U)` types on the
320
+ double as `show(component: Type<unknown>, data: unknown)`; no mapped type can keep a generic
321
+ signature. Usually harmless: `spy.show.calledWith(MyModal, data).mockReturnValue(ref)` compiles.
322
+ Where the instantiated type sits in a contravariant position, name the shape on a declaration of
323
+ your own and assign the factory's result to it.
324
+
325
+ Details: <https://asdalexey.github.io/vitest-auto-spy/core/spy-typing>.
331
326
 
332
327
  ---
333
328
 
334
329
  ## 7. Resetting
335
330
 
331
+ Use these instead of looping over methods calling `mockClear` by hand:
332
+
336
333
  ```ts
337
334
  import { clearAutoSpy, resetAutoSpy } from 'vitest-auto-spy';
338
335
 
339
- clearAutoSpy(service); // recorded calls only — configured returns survive
336
+ clearAutoSpy(service); // recorded calls only; configured returns survive
340
337
  resetAutoSpy(service); // calls AND configuration (calledWith / resolveWith / mockReturnValue)
341
338
  ```
342
339
 
343
- Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
344
- proxies alike. Reach for these instead of looping over methods calling `mockClear` by hand.
340
+ | Call | Clears calls | Clears configuration | Also clears |
341
+ | --- | --- | --- | --- |
342
+ | `clearAutoSpy(x)` | yes | no | — |
343
+ | `resetAutoSpy(x)` | yes | yes | pending `mockReturnValueOnce` / `mockResolvedValueOnce` queues, accessor spies' configuration |
344
+ | `spy.m.mockReset()` | yes | runner state only | **not** the `calledWith` chains |
345
345
 
346
- **`mockReset()` on one method keeps its `calledWith` chains** — they live in the library's state, where
347
- the runner's reset cannot reach — so `spy.isFeatureOn.mockReset(); spy.isFeatureOn.mockReturnValue(true)`
348
- still reports that `mockReturnValue()` replaced a configured chain (and throws under `strict`). To
349
- answer one value for every call after a `beforeEach` configured `calledWith(…)`, drop the chain with
350
- `resetAutoSpy(spy.isFeatureOn)` first; the report names it.
346
+ Both cover method spies **and** accessor spies, on `createSpyFromClass` spies and `createAutoMock`
347
+ proxies alike. `resetAutoSpy` is `vi.resetAllMocks()` for one double; the double stays usable, and a
348
+ fresh `calledWith` configures it as normal.
351
349
 
352
- **`resetAutoSpy` is `vi.resetAllMocks()` for one double**, and two things it used to leave behind now
353
- go with the rest: a pending `mockReturnValueOnce` / `mockResolvedValueOnce` queue (it used to answer
354
- the first call _after_ the reset, in a later test) and an accessor spy's configuration (a
355
- `accessorSpies.getters.x.mockReturnValue(…)` used to survive and hand the next test the previous
356
- test's value). A spec that relied on a queued `Once` value outliving the reset reads `undefined`
357
- now. The double stays usable: the library's own dispatch is put back afterwards, so a fresh
358
- `calledWith` configures it as normal.
350
+ **Common mistake:** `spy.isFeatureOn.mockReset(); spy.isFeatureOn.mockReturnValue(true)` after a
351
+ `beforeEach` that configured `calledWith(…)`. The chain survives `mockReset()`, so the library reports
352
+ that `mockReturnValue()` replaced a configured chain (and throws under `strict`). Fix: call
353
+ `resetAutoSpy(spy.isFeatureOn)` first.
359
354
 
360
- Every double also carries `[Symbol.dispose]()` — it runs `resetAutoSpy(this)` — so an `afterEach`
361
- that exists only to reset one spy can be deleted:
355
+ **`using` resets a double at block end.** Every double carries `[Symbol.dispose]()`, which runs
356
+ `resetAutoSpy(this)`, so an `afterEach` that exists only to reset one spy can go:
362
357
 
363
358
  ```ts
364
359
  it('loads', () => {
@@ -369,19 +364,14 @@ it('loads', () => {
369
364
  });
370
365
  ```
371
366
 
372
- `createAutoMock` proxies and **every `mockDeep` node** carry it (so `using` on a sub-tree resets that
373
- sub-tree). The key is non-enumerable, so a spread does not copy it, and there is no
374
- `[Symbol.asyncDispose]`. A standalone `createFunctionSpy` is **not** covered: Vitest's own
375
- `[Symbol.dispose]` on a host mock restores the original implementation instead — call
376
- `resetAutoSpy(spy)` there. If the project does not transpile `using`, call `spy[Symbol.dispose]()`.
377
-
378
- `src/lib/dispose-symbol.ts` installs `Symbol.dispose` when the realm has none, and `DISPOSE` — not
379
- `Symbol.dispose` — is what library code compares against. Node 22 has no explicit resource
380
- management in V8: it patches the symbol in itself, as `Symbol.for('nodejs.dispose')`, onto the main
381
- realm only, so under `jsdom` / `happy-dom` (a bare `vm` context) it is absent, the downlevelled
382
- `using` throws out of `tslib.__addDisposableResource`, and `spy[Symbol.dispose]` degrades into a
383
- property named `"undefined"`. The shim is the same registry symbol, so it is identical to Node's
384
- across realms.
367
+ - `createAutoMock` proxies and every `mockDeep` node carry it (`using` on a sub-tree resets that
368
+ sub-tree). The key is non-enumerable, so a spread does not copy it. There is no
369
+ `[Symbol.asyncDispose]`.
370
+ - A standalone `createFunctionSpy` is **not** covered: Vitest's own `[Symbol.dispose]` on it restores
371
+ the original implementation instead. Call `resetAutoSpy(spy)` there.
372
+ - If the project does not transpile `using`, call `spy[Symbol.dispose]()`.
373
+ - Works under `jsdom` / `happy-dom` on Node 22, where the realm has no `Symbol.dispose`: the library
374
+ installs the same registry symbol Node uses.
385
375
 
386
376
  ---
387
377
 
@@ -418,12 +408,14 @@ In [`agent-docs/setup.md`](./agent-docs/setup.md). Read it when the task touches
418
408
  - What a method spy is, and the one thing that differs from `vi.fn()`
419
409
  - Freezing and counting the clock
420
410
  - Asserting focus
411
+ - Running with `isolate: false`
421
412
  - Shared fixtures are functions, not constants
422
413
  - A stub must be re-installed for every test
423
414
  - Naming the file that sealed a global
424
415
  - Naming the file that polluted `Object.prototype`
425
416
  - Naming the test that left an attribute on `<body>`
426
417
  - Failing on console output nothing absorbed
418
+ - A `TestBed` left dirty at file end — `cleanTestBed`
427
419
  - One grade for everything — `preset: 'strict'`
428
420
  - Hook order differs from Jest
429
421
 
@@ -449,6 +441,8 @@ In [`agent-docs/doubles.md`](./agent-docs/doubles.md). Read it when production c
449
441
  - `<video>` and `<audio>`
450
442
  - `localStorage` and `sessionStorage`
451
443
  - Animation frames and element boxes
444
+ - An element for `ElementRef` — `createElementStub`
445
+ - The members the DOM environment leaves out — `fillMissingDomApis`
452
446
  - A module mock that did nothing
453
447
 
454
448
  <!-- /agents-outline -->
@@ -476,6 +470,7 @@ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that
476
470
  - Signal forms — `vitest-auto-spy/signal-forms`
477
471
  - `window` and `document` over the real ones — `provideWindowDouble` / `provideDocumentDouble`
478
472
  - The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
473
+ - Platform, sanitizer, change detector and CDK overlay — `vitest-auto-spy/angular/doubles`
479
474
  - Which collaborators the code asked for — `trackInjections`
480
475
  - Never mock `@angular/core` to control an `effect()`
481
476
  - Counting recomputations and effect runs — `trackRecomputations` / `trackEffectRuns`
@@ -486,7 +481,7 @@ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that
486
481
  - A host for a directive under test
487
482
  - A stand-in for a child — `createComponentStub`
488
483
  - Patching a property of a spy
489
- - Rstest
484
+ - Coming from spectator, Suites or Testing Library
490
485
 
491
486
  <!-- /agents-outline -->
492
487
 
@@ -494,32 +489,30 @@ In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that
494
489
 
495
490
  ## 14. `fakeAsync` needs `vitest-auto-spy/zone`
496
491
 
492
+ Needed when a spec on Vitest uses `fakeAsync`, `tick` or `waitForAsync`. `zone.js/testing` patches
493
+ jasmine, mocha and jest, not Vitest.
494
+
497
495
  ```ts
498
- // vitest.setup.ts — zone.js first (or the Angular builder loads it), then the patch
496
+ // vitest.setup.ts: zone.js first (or let the Angular builder load it), then the patch
499
497
  import 'vitest-auto-spy/zone';
500
498
  ```
501
499
 
502
- `zone.js/testing` patches jasmine, mocha and jest — not Vitest — so without this every `fakeAsync`
503
- fails with `Expected to be running in 'ProxyZone', but it was not found`. Needs
504
- `test: { globals: true }`: the patch replaces the runner globals, and an imported `it` is a module
505
- binding nothing can reach.
506
-
507
- One proxy zone serves the whole run (`scope: 'shared'`, the default), because that is what Angular's
508
- jasmine patch does and what the ecosystem expects: a component built in `beforeEach` schedules from
509
- its constructor, and `tick()` in the `fakeAsync` test has to see those timers. Use
510
- `installProxyZonePatch({ scope: 'callback' })` for `test.concurrent`, where two callbacks are in
511
- flight at once and would otherwise swap the same `ProxyZoneSpec` delegate under one another.
500
+ | Option | Default | Meaning |
501
+ | --- | --- | --- |
502
+ | `installProxyZonePatch({ scope: 'shared' })` | `'shared'` | one proxy zone for the whole run, as Angular's jasmine patch does: `tick()` in the test sees timers a component scheduled in `beforeEach` |
503
+ | `installProxyZonePatch({ scope: 'callback' })` | — | one zone per test or hook; use it for `test.concurrent` specs that call `fakeAsync` |
512
504
 
513
- Calling `installProxyZonePatch()` yourself is idempotent now: a second call is a no-op instead of
514
- wrapping the previous wrapper, and so is the undo it returns. Under `isolate: false` an explicit
515
- call in a setup file used to add a `Proxy` layer per spec file — two hundred layers at two hundred
516
- files, every `fakeAsync` callback paying for all of them.
505
+ - The import already installs `'shared'`. Call `installProxyZonePatch({ scope: 'callback' })` in the
506
+ setup file, after the import, only to switch; a later import of the entry does not switch it back.
507
+ - Requires `test: { globals: true }`. The patch replaces the runner globals; an imported `it` is a
508
+ module binding it cannot reach.
509
+ - A second call with the same scope is a no-op, and so is a second call of the undo it returns. The
510
+ undo puts back what the call replaced.
511
+ - The entry imports no zone.js of its own: it reads `globalThis.Zone`, which the project loaded.
517
512
 
518
- **Invariant of this package, not a detail of one release:** `zone.js` is a **devDependency and only a
519
- devDependency** — never a dependency, never a peer, not even an optional one. Everything about zones
520
- lives behind this one subpath; no other entry reaches it, even transitively, and the module imports
521
- no zone.js of its own (it reads `globalThis.Zone`, which the consumer loaded). Do not add a
522
- convenient re-export from the root: it would quietly hand zone.js to every zoneless consumer.
513
+ **Common mistake:** no import, or `globals: false`. Symptom: every `fakeAsync` fails with
514
+ `Expected to be running in 'ProxyZone', but it was not found`. Fix: the import above in the setup
515
+ file, and `globals: true`.
523
516
 
524
517
  ---
525
518
 
@@ -528,6 +521,10 @@ convenient re-export from the root: it would quietly hand zone.js to every zonel
528
521
  In [`agent-docs/adapters.md`](./agent-docs/adapters.md). Read it for NestJS, Vue, React, Svelte, `bun test`, `node:test`, Rstest and the console entry.
529
522
 
530
523
  <!-- agents-outline: agent-docs/adapters.md -->
524
+
525
+ - `trackNodeMocks` for `node:test` memory
526
+ - Rstest
527
+
531
528
  <!-- /agents-outline -->
532
529
 
533
530
  ---