vitest-auto-spy 1.9.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
16
16
  [![npm version](https://img.shields.io/npm/v/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
17
17
  [![npm downloads](https://img.shields.io/npm/dm/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
18
18
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
19
- [![minzipped size](https://img.shields.io/bundlephobia/minzip/vitest-auto-spy?label=minzip)](https://bundlephobia.com/package/vitest-auto-spy)
19
+ [![minzipped size](https://img.shields.io/badge/minzip-3.5%20kB-brightgreen)](#install)
20
20
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
21
21
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
22
22
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -85,10 +85,10 @@ npm i -D vitest-auto-spy
85
85
 
86
86
  ### Requirements
87
87
 
88
- | Tool | Minimum |
89
- | --- | --- |
90
- | Node.js | ≥ 18 |
91
- | Vitest | ≥ 1.0 (required peer) |
88
+ | Tool | Minimum |
89
+ | ---------- | -------------------------------------------------------------- |
90
+ | Node.js | ≥ 18 |
91
+ | Vitest | ≥ 1.0 (required peer) |
92
92
  | TypeScript | ≥ 4.7 for the typed helpers (plain JS works too, just untyped) |
93
93
 
94
94
  Ships **dual ESM + CommonJS** with bundled `.d.ts` types, so it drops into both `import`- and
@@ -99,11 +99,11 @@ Ships **dual ESM + CommonJS** with bundled `.d.ts` types, so it drops into both
99
99
  All peers are **provided by your project**; `rxjs` and `@angular/core` are **optional** — install
100
100
  them only for the matching entry point. The package itself has **zero runtime dependencies**.
101
101
 
102
- | Peer | Needed for | Optional? |
103
- | --- | --- | --- |
104
- | `vitest` | the default runner | no |
105
- | `rxjs` | `vitest-auto-spy/rxjs` observable spies (and `Spy<T>` type-checking) — `>=7`, **no upper bound** (the rxjs 8 line included) | yes |
106
- | `@angular/core` | `vitest-auto-spy/angular` helpers | yes |
102
+ | Peer | Needed for | Optional? |
103
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------- | --------- |
104
+ | `vitest` | the default runner | no |
105
+ | `rxjs` | `vitest-auto-spy/rxjs` observable spies (and `Spy<T>` type-checking) — `>=7`, **no upper bound** (the rxjs 8 line included) | yes |
106
+ | `@angular/core` | `vitest-auto-spy/angular` helpers | yes |
107
107
 
108
108
  ## Availability
109
109
 
@@ -111,11 +111,11 @@ them only for the matching entry point. The package itself has **zero runtime de
111
111
  > and the **Angular / NestJS / React / Vue·Pinia / Svelte** recipes all ship as importable entry points —
112
112
  > one identical API across every runner and framework.
113
113
 
114
- | Entry point | Status |
115
- | --- | --- |
114
+ | Entry point | Status |
115
+ | ---------------------------------------------------------------------- | ---------------- |
116
116
  | `vitest-auto-spy` · `vitest-auto-spy/rxjs` · `vitest-auto-spy/angular` | ✅ **Published** |
117
- | `vitest-auto-spy/bun` · `vitest-auto-spy/node` | ✅ **Published** |
118
- | `vitest-auto-spy/nestjs` · `/react` · `/vue` · `/svelte` · `/console` | ✅ **Published** |
117
+ | `vitest-auto-spy/bun` · `vitest-auto-spy/node` | ✅ **Published** |
118
+ | `vitest-auto-spy/nestjs` · `/react` · `/vue` · `/svelte` · `/console` | ✅ **Published** |
119
119
 
120
120
  ## Quick start
121
121
 
@@ -124,7 +124,7 @@ effects). The helper you get on each method matches its return type:
124
124
 
125
125
  ```ts
126
126
  import { beforeEach, expect, it } from 'vitest';
127
- import { createSpyFromClass, type Spy } from 'vitest-auto-spy';
127
+ import { type Spy, createSpyFromClass } from 'vitest-auto-spy';
128
128
 
129
129
  class UserService {
130
130
  getName(id: number): string {
@@ -200,7 +200,7 @@ What it **won't** auto-discover — by design, because these aren't prototype me
200
200
  mock them by hand. (Same constraint as `jest-auto-spies`.)
201
201
  - ⚠️ **Getters / setters** are skipped unless named in `gettersToSpyOn` / `settersToSpyOn` — see
202
202
  [Getters & setters](#getters--setters).
203
- - ⚠️ **Plain data properties** carry no value until you set one; auto-spy mocks *behaviour*
203
+ - ⚠️ **Plain data properties** carry no value until you set one; auto-spy mocks _behaviour_
204
204
  (methods), not state. To mock by type including properties, use
205
205
  [`createAutoMock`](#auto-mock-by-type-no-class-needed).
206
206
 
@@ -209,18 +209,18 @@ What it **won't** auto-discover — by design, because these aren't prototype me
209
209
  The library ships a framework-agnostic core plus runtime and framework layers, so a plain
210
210
  Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runtime bundle**:
211
211
 
212
- | Import | Provides | Pulls in | Status |
213
- | --- | --- | --- | :---: |
214
- | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
215
- | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) + `createObservableWithValues` | `rxjs` | ✅ |
216
- | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `mockReadonlyProp*`, `mockAccessorsProp` | `@angular/core` | ✅ |
217
- | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
218
- | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()` | `node:test` | ✅ |
219
- | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
220
- | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
221
- | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
222
- | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
223
- | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
212
+ | Import | Provides | Pulls in | Status |
213
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------- | :----: |
214
+ | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
215
+ | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) + `createObservableWithValues` | `rxjs` | ✅ |
216
+ | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `mockReadonlyProp*`, `mockValueProp`, `mockAccessorsProp`, `restoreMockedProps` | `@angular/core` | ✅ |
217
+ | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
218
+ | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()` | `node:test` | ✅ |
219
+ | `vitest-auto-spy/nestjs` | `provideAutoSpy`, `injectSpy` for `Test.createTestingModule` | — (your `@nestjs/*`) | ✅ |
220
+ | `vitest-auto-spy/react` | the core, with a natural import for React Testing Library suites | — (your `react`) | ✅ |
221
+ | `vitest-auto-spy/vue` | `provideAutoSpy` for `global.provide` + Pinia store spying | — (your `vue`/`pinia`) | ✅ |
222
+ | `vitest-auto-spy/svelte` | the core, with a natural import for Svelte suites | — (your `svelte`) | ✅ |
223
+ | `vitest-auto-spy/console` | `consoleInfoSpy` & friends — silent typed spies over the global `console`, installed on import | `vitest` | ✅ |
224
224
 
225
225
  ✅ all entry points published (see [Availability](#availability)).
226
226
 
@@ -230,8 +230,9 @@ Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runti
230
230
 
231
231
  ```ts
232
232
  import { createSpyFromClass } from 'vitest-auto-spy';
233
- import 'vitest-auto-spy/rxjs'; // once (e.g. in your test setup) — enables observable spies
234
- import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';
233
+ // once (e.g. in your test setup) — enables observable spies
234
+ import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
235
+ import 'vitest-auto-spy/rxjs';
235
236
  ```
236
237
 
237
238
  ### Runtimes
@@ -264,14 +265,14 @@ import { createSpyFromClass } from 'vitest-auto-spy/node'; // node:test
264
265
 
265
266
  ## Comparison
266
267
 
267
- | Library | Reads a class? | Return-type-aware helpers? | Runtimes | We win on |
268
- | --- | :---: | :---: | --- | --- |
269
- | **vitest-auto-spy** | ✅ | ✅ | Vitest · Bun · node:test | — |
270
- | [jest-auto-spies](https://www.npmjs.com/package/jest-auto-spies) | ✅ | ✅ | Jest only | Vitest/Bun/Node successor, **same API** — direct migration path |
271
- | [@bugsplat/vitest-auto-spies](https://www.npmjs.com/package/@bugsplat/vitest-auto-spies) | ✅ | ✅ | Vitest only | Same class-based API **plus** Bun & `node:test`, [type-only `createAutoMock`](#auto-mock-by-type-no-class-needed), framework recipes (Angular/NestJS/React/Vue/Svelte), console spies, and **zero runtime deps** (it depends on `@hirez_io/auto-spies-core`) |
272
- | [vitest-mock-extended](https://www.npmjs.com/package/vitest-mock-extended) | ❌ (Proxy) | ❌ | Vitest | Return-type ergonomics **and** reading a real class (we also ship a Proxy mode: [`createAutoMock`](#auto-mock-by-type-no-class-needed)) |
273
- | [@golevelup/ts-vitest](https://www.npmjs.com/package/@golevelup/ts-vitest) | partial | ❌ | Vitest | Typed `Promise`/`Observable` helpers + explicit class→spy + `mustBeCalledWith` |
274
- | [sinon](https://www.npmjs.com/package/sinon) | ❌ (manual) | ❌ | Any | Auto-generated + fully typed vs. manual + loosely typed |
268
+ | Library | Reads a class? | Return-type-aware helpers? | Runtimes | We win on |
269
+ | ---------------------------------------------------------------------------------------- | :------------: | :------------------------: | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
270
+ | **vitest-auto-spy** | ✅ | ✅ | Vitest · Bun · node:test | — |
271
+ | [jest-auto-spies](https://www.npmjs.com/package/jest-auto-spies) | ✅ | ✅ | Jest only | Vitest/Bun/Node successor, **same API** — direct migration path |
272
+ | [@bugsplat/vitest-auto-spies](https://www.npmjs.com/package/@bugsplat/vitest-auto-spies) | ✅ | ✅ | Vitest only | Same class-based API **plus** Bun & `node:test`, [type-only `createAutoMock`](#auto-mock-by-type-no-class-needed), framework recipes (Angular/NestJS/React/Vue/Svelte), console spies, and **zero runtime deps** (it depends on `@hirez_io/auto-spies-core`) |
273
+ | [vitest-mock-extended](https://www.npmjs.com/package/vitest-mock-extended) | ❌ (Proxy) | ❌ | Vitest | Return-type ergonomics **and** reading a real class (we also ship a Proxy mode: [`createAutoMock`](#auto-mock-by-type-no-class-needed)) |
274
+ | [@golevelup/ts-vitest](https://www.npmjs.com/package/@golevelup/ts-vitest) | partial | ❌ | Vitest | Typed `Promise`/`Observable` helpers + explicit class→spy + `mustBeCalledWith` |
275
+ | [sinon](https://www.npmjs.com/package/sinon) | ❌ (manual) | ❌ | Any | Auto-generated + fully typed vs. manual + loosely typed |
275
276
 
276
277
  **The pitch:** the only auto-spy library that reads a **class** and gives a **fully-typed** spy of
277
278
  every method with **return-type-aware** control helpers (`resolveWith` / `nextWith` / `calledWith`) —
@@ -296,18 +297,18 @@ This also covers migrating from [`@bugsplat/vitest-auto-spies`](https://www.npmj
296
297
  which re-exports the same `jest-auto-spies` API — the swap is identical, and you gain Bun /
297
298
  `node:test`, `createAutoMock`, framework recipes and console spies on top.
298
299
 
299
- | jest-auto-spies | vitest-auto-spy | Status |
300
- | --- | --- | --- |
301
- | `createSpyFromClass` | `createSpyFromClass` | ✅ identical |
302
- | `provideAutoSpy` | `provideAutoSpy` | ✅ identical |
303
- | `calledWith` / `mustBeCalledWith` | same | ✅ identical |
304
- | `calledWith(...).returnValue(v)` | same — `.returnValue` **and** `.mockReturnValue` both work | ✅ identical |
305
- | `resolveWith` / `rejectWith` / `resolveWithPerCall` | same | ✅ identical |
306
- | `nextWith` / `nextOneTimeWith` / `nextWithValues` / `nextWithPerCall` | same | ✅ identical |
307
- | `throwWith` / `complete` / `returnSubject` | same | ✅ identical |
308
- | `accessorSpies.getters/setters` | same | ✅ identical |
309
- | `createObservableWithValues` | same | ✅ identical |
310
- | underlying mock | `jest.fn()` → `vi.fn()` | 🔁 swapped |
300
+ | jest-auto-spies | vitest-auto-spy | Status |
301
+ | --------------------------------------------------------------------- | ---------------------------------------------------------- | ------------ |
302
+ | `createSpyFromClass` | `createSpyFromClass` | ✅ identical |
303
+ | `provideAutoSpy` | `provideAutoSpy` | ✅ identical |
304
+ | `calledWith` / `mustBeCalledWith` | same | ✅ identical |
305
+ | `calledWith(...).returnValue(v)` | same — `.returnValue` **and** `.mockReturnValue` both work | ✅ identical |
306
+ | `resolveWith` / `rejectWith` / `resolveWithPerCall` | same | ✅ identical |
307
+ | `nextWith` / `nextOneTimeWith` / `nextWithValues` / `nextWithPerCall` | same | ✅ identical |
308
+ | `throwWith` / `complete` / `returnSubject` | same | ✅ identical |
309
+ | `accessorSpies.getters/setters` | same | ✅ identical |
310
+ | `createObservableWithValues` | same | ✅ identical |
311
+ | underlying mock | `jest.fn()` → `vi.fn()` | 🔁 swapped |
311
312
 
312
313
  Just make sure your tests run under Vitest, and (for Angular) that `TestBed` is set up.
313
314
 
@@ -323,12 +324,36 @@ createSpyFromClass(MyService, ['getName', 'getAge']);
323
324
  // 3. full config object
324
325
  createSpyFromClass(MyService, {
325
326
  methodsToSpyOn: ['getName'],
327
+ instanceMethodsToSpyOn: ['count'], // callables that live on the instance, not the prototype
326
328
  observablePropsToSpyOn: ['products$'], // Observable *properties*
327
329
  gettersToSpyOn: ['userName'],
328
330
  settersToSpyOn: ['userName'],
329
331
  });
330
332
  ```
331
333
 
334
+ ### Spying instance-assigned callables (`signal()`, arrow props, `signalStore()`)
335
+
336
+ `createSpyFromClass` discovers methods by walking the **prototype**, so a callable assigned on the
337
+ instance is invisible to it — an arrow-function property, an Angular `signal()` / `computed()`
338
+ field, a method of an ngrx `signalStore()`. Listing such a name in `methodsToSpyOn` is not the
339
+ answer either: that option _restricts_ what gets spied, and the name is reported as a probable typo.
340
+
341
+ Name them in `instanceMethodsToSpyOn` instead. They are **added** to whatever the method resolution
342
+ produced, and never warn:
343
+
344
+ ```ts
345
+ class SettingsService {
346
+ readonly isReady = signal(false); // instance field — not on the prototype
347
+ load(): void {} // prototype method — auto-discovered
348
+ }
349
+
350
+ const spy = createSpyFromClass(SettingsService, { instanceMethodsToSpyOn: ['isReady'] });
351
+
352
+ spy.isReady.mockReturnValue(true);
353
+ expect(spy.isReady()).toBe(true);
354
+ expect(vi.isMockFunction(spy.load)).toBe(true); // still spied
355
+ ```
356
+
332
357
  ## Auto-mock by type (no class needed)
333
358
 
334
359
  `createSpyFromClass` reads a real class's prototype. When you only have a TypeScript **interface or
@@ -355,8 +380,8 @@ Every accessed method becomes a decorated spy with the **same typed control help
355
380
  `createSpyFromClass`, materialized lazily and cached (same reference on re-access):
356
381
 
357
382
  ```ts
358
- svc.getName.calledWith(1).mockReturnValue('Ada'); // sync, arg-matched
359
- svc.getUser.resolveWith({ id: 1, name: 'Ada' }); // promise helper
383
+ svc.getName.calledWith(1).mockReturnValue('Ada'); // sync, arg-matched
384
+ svc.getUser.resolveWith({ id: 1, name: 'Ada' }); // promise helper
360
385
  expect(svc.getName(1)).toBe('Ada');
361
386
  await expect(svc.getUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });
362
387
  ```
@@ -414,16 +439,12 @@ import 'vitest-auto-spy/rxjs';
414
439
 
415
440
  ```ts
416
441
  myService.getProducts$.nextWith([{ name: 'Product 1' }]); // emit, stream stays open
417
- myService.getProducts$.nextOneTimeWith([{ name: 'X' }]); // emit one value, then complete
418
- myService.getProducts$.throwWith('FAKE ERROR'); // error the stream
419
- myService.getProducts$.complete(); // complete the stream
442
+ myService.getProducts$.nextOneTimeWith([{ name: 'X' }]); // emit one value, then complete
443
+ myService.getProducts$.throwWith('FAKE ERROR'); // error the stream
444
+ myService.getProducts$.complete(); // complete the stream
420
445
 
421
446
  // emit a precise sequence — values, errors, completion, optional delays
422
- myService.getProducts$.nextWithValues([
423
- { value: [{ name: 'Product 1' }] },
424
- { errorValue: 'FAKE ERROR' },
425
- { complete: true },
426
- ]);
447
+ myService.getProducts$.nextWithValues([{ value: [{ name: 'Product 1' }] }, { errorValue: 'FAKE ERROR' }, { complete: true }]);
427
448
 
428
449
  // a fresh stream per call
429
450
  myService.getProducts$.nextWithPerCall([{ value: ['a'] }, { value: ['b'] }]);
@@ -485,8 +506,8 @@ pull it back out already typed as `Spy<T>`. `@nestjs/common` / `@nestjs/testing`
485
506
 
486
507
  ```ts
487
508
  import { Test, type TestingModule } from '@nestjs/testing';
488
- import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/nestjs';
489
509
  import { beforeEach, expect, it } from 'vitest';
510
+ import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/nestjs';
490
511
 
491
512
  import { AuthService } from './auth.service';
492
513
  import { UserService } from './user.service';
@@ -558,24 +579,22 @@ it('drives async deps and asserts the component called them', async () => {
558
579
  `@vue/test-utils`' `global.provide`; for a class-based Pinia store, spy it directly:
559
580
 
560
581
  ```ts
582
+ // (a) class-based service injected via provide / global.provide
583
+ import { UserService, UserServiceKey } from '@/services/user.service';
584
+ // (b) class-based Pinia store — every action becomes a spy
585
+ import { CartStore } from '@/stores/cart.store';
561
586
  import { mount } from '@vue/test-utils';
562
587
  import { createSpyFromClass, provideAutoSpy } from 'vitest-auto-spy/vue';
563
588
 
564
- // (a) class-based service injected via provide / global.provide
565
- import { UserServiceKey, UserService } from '@/services/user.service';
566
-
567
589
  const provide = provideAutoSpy(UserServiceKey, UserService); // { [UserServiceKey]: Spy<UserService> }
568
590
  provide[UserServiceKey].getName.mockReturnValue('Fake Name');
569
591
 
570
592
  const wrapper = mount(UserBadge, { global: { provide } });
571
593
  expect(provide[UserServiceKey].getName).toHaveBeenCalled();
572
594
 
573
- // (b) class-based Pinia store — every action becomes a spy
574
- import { CartStore } from '@/stores/cart.store';
575
-
576
595
  const store = createSpyFromClass(CartStore);
577
- store.itemCount.mockReturnValue(3); // sync action/getter
578
- store.checkout.resolveWith({ orderId: 'ord_42' }); // async action (Promise)
596
+ store.itemCount.mockReturnValue(3); // sync action/getter
597
+ store.checkout.resolveWith({ orderId: 'ord_42' }); // async action (Promise)
579
598
  await store.checkout('tok_abc');
580
599
  expect(store.checkout).toHaveBeenCalledWith('tok_abc');
581
600
  ```
@@ -589,6 +608,7 @@ one (props, context, or a mocked module):
589
608
  ```ts
590
609
  import { render } from '@testing-library/svelte';
591
610
  import { createSpyFromClass } from 'vitest-auto-spy/svelte';
611
+
592
612
  import Cart from './Cart.svelte';
593
613
  import { CartStore } from './cart-store';
594
614
 
@@ -615,7 +635,7 @@ it('shows the cart total from the store', () => {
615
635
  `provideAutoSpy` is the shorthand for providing an auto-spy in a `TestBed`:
616
636
 
617
637
  ```ts
618
- import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';
638
+ import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
619
639
 
620
640
  TestBed.configureTestingModule({
621
641
  providers: [
@@ -647,35 +667,64 @@ beforeEach(() => {
647
667
  #### Signal / readonly property mocking (bonus)
648
668
 
649
669
  ```ts
650
- import { mockReadonlyProp, mockReadonlyPropGetter, mockAccessorsProp } from 'vitest-auto-spy/angular';
670
+ import { mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from 'vitest-auto-spy/angular';
651
671
 
652
- mockReadonlyProp(service, 'isReady', true); // static value (incl. signals)
653
- mockReadonlyPropGetter(service, 'label', () => 'A'); // dynamic getter
654
- mockAccessorsProp(service, 'theme'); // spied get + set
672
+ mockReadonlyProp(service, 'isReady', true); // static value (incl. signals)
673
+ mockReadonlyPropGetter(service, 'label', () => 'A'); // dynamic getter
674
+ mockValueProp(service, 'retries', 3); // plain writable value
675
+ mockAccessorsProp(service, 'theme'); // spied get + set
676
+ mockAccessorsProp(input, 'valueAsNumber', { get, set }); // …with real implementations behind them
655
677
  ```
656
678
 
679
+ Each helper also returns the undo for _its own_ patch, for a stub that has to come off inside a
680
+ single test:
681
+
682
+ ```ts
683
+ const restoreNavigator = mockValueProp(globalThis, 'navigator', undefined);
684
+
685
+ try {
686
+ // …
687
+ } finally {
688
+ restoreNavigator();
689
+ }
690
+ ```
691
+
692
+ Every one of them records the descriptor it overwrote, so a single `restoreMockedProps()` puts them
693
+ all back. That matters when the patched object outlives the spec file — a global, a class
694
+ prototype, a singleton — which is always the case under Vitest's `isolate: false`:
695
+
696
+ ```ts
697
+ // test setup
698
+ afterEach(() => restoreMockedProps());
699
+ ```
700
+
701
+ Members the public type does not describe (`#private` fields, ad-hoc keys) are accepted too — the
702
+ helpers take a `PropertyKey` overload, so no `as never` dance.
703
+
657
704
  ## Utilities
658
705
 
659
706
  Beyond the spy factories, the package ships a set of small standalone helpers. Each one is a
660
707
  single-purpose utility you can pick up independently — they all ride on the same core:
661
708
 
662
- | Utility | Entry point | What it's for |
663
- | --- | --- | --- |
664
- | `injectSpy(token)` / `injectSpy(moduleRef, token)` | `/angular`, `/nestjs` | Pull a provided spy out of the DI container, already typed as `Spy<T>` — no casting |
665
- | `provideAutoSpy(Class, config?)` | `/angular`, `/nestjs`, `/vue` | One-liner `{ provide, useValue }` (or Vue `global.provide`) that builds the spy for you |
666
- | `createFunctionSpy(name)` | core | A single standalone function spy with the full helper set (`calledWith`, `resolveWith`, `nextWith`, …) — no class needed |
667
- | `createAutoMock<T>(overrides?)` | core | Proxy-based spy from a **type/interface** alone ([details](#auto-mock-by-type-no-class-needed)) |
668
- | `createObservableWithValues(configs, opts?)` | `/rxjs` | Build a fake `Observable` emitting a precise sequence of values / errors / completion |
669
- | `consoleInfoSpy` / `consoleWarnSpy` / … | `/console` | Silent typed spies over the global `console`, installed on import ([details](#console-spies--vitest-auto-spyconsole)) |
670
- | `mockReadonlyProp(obj, prop, value)` | `/angular` | Overwrite a `readonly` property (incl. Angular signals) with a static value |
671
- | `mockReadonlyPropGetter(obj, prop, getter)` | `/angular` | Same, but backed by a dynamic getter |
672
- | `mockAccessorsProp(obj, prop)` | `/angular` | Redefine a property with spied `get` + `set` |
673
- | `errorHandler` | core | The `mustBeCalledWith` argument-mismatch reporter — swap it to customize failure output |
709
+ | Utility | Entry point | What it's for |
710
+ | -------------------------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
711
+ | `injectSpy(token)` / `injectSpy(moduleRef, token)` | `/angular`, `/nestjs` | Pull a provided spy out of the DI container, already typed as `Spy<T>` — no casting |
712
+ | `provideAutoSpy(Class, config?)` | `/angular`, `/nestjs`, `/vue` | One-liner `{ provide, useValue }` (or Vue `global.provide`) that builds the spy for you |
713
+ | `createFunctionSpy(name)` | core | A single standalone function spy with the full helper set (`calledWith`, `resolveWith`, `nextWith`, …) — no class needed |
714
+ | `createAutoMock<T>(overrides?)` | core | Proxy-based spy from a **type/interface** alone ([details](#auto-mock-by-type-no-class-needed)) |
715
+ | `createObservableWithValues(configs, opts?)` | `/rxjs` | Build a fake `Observable` emitting a precise sequence of values / errors / completion |
716
+ | `consoleInfoSpy` / `consoleWarnSpy` / … | `/console` | Silent typed spies over the global `console`, installed on import ([details](#console-spies--vitest-auto-spyconsole)) |
717
+ | `mockReadonlyProp(obj, prop, value)` | `/angular` | Overwrite a `readonly` property (incl. Angular signals) with a static value |
718
+ | `mockReadonlyPropGetter(obj, prop, getter)` | `/angular` | Same, but backed by a dynamic getter |
719
+ | `mockValueProp(obj, prop, value)` | `/angular` | Overwrite a property with a plain **writable** value |
720
+ | `mockAccessorsProp(obj, prop, accessors?)` | `/angular` | Redefine a property with spied `get` + `set`, optionally backed by real implementations |
721
+ | `restoreMockedProps()` | `/angular` | Undo every patch the `mock*Prop` helpers applied — one call in `afterEach` (each helper also returns the undo for its own patch) |
722
+ | `errorHandler` | core | The `mustBeCalledWith` argument-mismatch reporter — swap it to customize failure output |
674
723
 
675
724
  A taste of the DI pair — provide the spy, inject it back fully typed:
676
725
 
677
726
  ```ts
678
- import { provideAutoSpy, injectSpy } from 'vitest-auto-spy/angular';
727
+ import { injectSpy, provideAutoSpy } from 'vitest-auto-spy/angular';
679
728
 
680
729
  TestBed.configureTestingModule({ providers: [provideAutoSpy(UserService)] });
681
730
  const userService = injectSpy(UserService); // Spy<UserService>, no `as` cast
@@ -729,21 +778,22 @@ expect(fakeConsole.info).toHaveBeenCalledWith('done');
729
778
 
730
779
  ## API reference
731
780
 
732
- | Export | Description |
733
- | --- | --- |
734
- | `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy<T>` from a class |
735
- | `createAutoMock<T>(overrides?)` | Build a `Spy<T>` from a **type/interface** alone (Proxy, no class) |
736
- | `mockDeep<T>(overrides?)` | Build a **recursive** auto-mock — `mock.repo.user.find()` chains without seeding |
737
- | `resetAutoSpy(spy)` / `clearAutoSpy(spy)` | Reset every spy in an auto-spy at once — `reset` also reverts return-value config (`calledWith` **and** a bare `mockReturnValue`); `clear` keeps it |
738
- | `provideAutoSpy(Class, methodsOrConfig?)` | Angular / NestJS `{ provide, useValue }` shorthand |
739
- | `provideAutoSpy(token, Class, methodsOrConfig?)` | Vue `{ [token]: Spy<T> }` for `global.provide` |
740
- | `injectSpy(token)` _(Angular)_ / `injectSpy(moduleRef, token)` _(NestJS)_ | Inject typed as `Spy<T>` |
741
- | `createFunctionSpy(name)` | A single standalone function spy with all helpers |
742
- | `createObservableWithValues(configs, opts?)` | Build an Observable from value configs |
743
- | `mockReadonlyProp` / `mockReadonlyPropGetter` / `mockAccessorsProp` | Mock readonly / accessor / signal props |
744
- | `consoleDebugSpy` … `consoleWarnSpy` _(`/console`)_ | Silent typed spies replacing the global `console` methods on import |
745
- | `installConsoleSpies()` / `resetConsoleSpies()` / `restoreConsole()` | Install / clear / undo the console spies |
746
- | `errorHandler` | The `mustBeCalledWith` argument-mismatch error helper |
781
+ | Export | Description |
782
+ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
783
+ | `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy<T>` from a class |
784
+ | `createAutoMock<T>(overrides?)` | Build a `Spy<T>` from a **type/interface** alone (Proxy, no class) |
785
+ | `mockDeep<T>(overrides?)` | Build a **recursive** auto-mock — `mock.repo.user.find()` chains without seeding |
786
+ | `resetAutoSpy(spy)` / `clearAutoSpy(spy)` | Reset every spy in an auto-spy at once — `reset` also reverts return-value config (`calledWith` **and** a bare `mockReturnValue`); `clear` keeps it |
787
+ | `provideAutoSpy(Class, methodsOrConfig?)` | Angular / NestJS `{ provide, useValue }` shorthand |
788
+ | `provideAutoSpy(token, Class, methodsOrConfig?)` | Vue `{ [token]: Spy<T> }` for `global.provide` |
789
+ | `injectSpy(token)` _(Angular)_ / `injectSpy(moduleRef, token)` _(NestJS)_ | Inject typed as `Spy<T>` |
790
+ | `createFunctionSpy(name)` | A single standalone function spy with all helpers |
791
+ | `createObservableWithValues(configs, opts?)` | Build an Observable from value configs |
792
+ | `mockReadonlyProp` / `mockReadonlyPropGetter` / `mockValueProp` / `mockAccessorsProp` | Mock readonly / writable / accessor / signal props |
793
+ | `restoreMockedProps()` | Undo every `mock*Prop` patch (descriptors restored newest-first) |
794
+ | `consoleDebugSpy` … `consoleWarnSpy` _(`/console`)_ | Silent typed spies replacing the global `console` methods on import |
795
+ | `installConsoleSpies()` / `resetConsoleSpies()` / `restoreConsole()` | Install / clear / undo the console spies |
796
+ | `errorHandler` | The `mustBeCalledWith` argument-mismatch error helper |
747
797
 
748
798
  **Spied sync method:** `mockReturnValue`, `calledWith(...)`, `mustBeCalledWith(...)` — `calledWith`
749
799
  also matches **asymmetric matchers** (`calledWith(expect.any(Number))`, `expect.objectContaining({...})`)
@@ -753,7 +803,8 @@ also matches **asymmetric matchers** (`calledWith(expect.any(Number))`, `expect.
753
803
  **Spied Observable method / property:** `nextWith`, `nextOneTimeWith`, `nextWithValues`,
754
804
  `nextWithPerCall`, `throwWith`, `complete`, `returnSubject`
755
805
 
756
- **Config (`ClassSpyConfiguration`):** `methodsToSpyOn`, `observablePropsToSpyOn`,
806
+ **Config (`ClassSpyConfiguration`):** `methodsToSpyOn`, `instanceMethodsToSpyOn` (callables that
807
+ live on the instance — `signal()` fields, arrow props, `signalStore()` methods), `observablePropsToSpyOn`,
757
808
  `gettersToSpyOn`, `settersToSpyOn`, `autoSpyAccessors` (discover every getter/setter),
758
809
  `lazySpies` (materialize method spies on first access — cheaper for wide classes; the `provideAutoSpy` default on Angular)
759
810
 
package/dist/angular.cjs CHANGED
@@ -404,6 +404,7 @@ function exposeAsSpy(spy) {
404
404
  // src/lib/create-spy-from-class.ts
405
405
  var EMPTY_CONFIGURATION = {
406
406
  methodsToSpyOn: [],
407
+ instanceMethodsToSpyOn: [],
407
408
  observablePropsToSpyOn: [],
408
409
  settersToSpyOn: [],
409
410
  gettersToSpyOn: [],
@@ -465,8 +466,8 @@ function resolveAccessors(prototype, config) {
465
466
  setters: [.../* @__PURE__ */ new Set([...config.settersToSpyOn, ...discovered.setters])]
466
467
  };
467
468
  }
468
- function warnOnUnknownMethods(ObjectClass, requested) {
469
- const available = new Set(getAllMethodNames(ObjectClass.prototype));
469
+ function warnOnUnknownMethods(ObjectClass, requested, instanceMethods) {
470
+ const available = /* @__PURE__ */ new Set([...getAllMethodNames(ObjectClass.prototype), ...instanceMethods]);
470
471
  const unknown = requested.filter((name) => !available.has(name));
471
472
  if (unknown.length === 0) {
472
473
  return;
@@ -475,14 +476,20 @@ function warnOnUnknownMethods(ObjectClass, requested) {
475
476
  `[vitest-auto-spy] createSpyFromClass(${ObjectClass.name}): requested method(s) not found on the class prototype: ${unknown.join(", ")}. A spy was still created, but the real code will never call it \u2014 check for typos.`
476
477
  );
477
478
  }
479
+ function materializeMethodSpy(autoSpy, methodName, value) {
480
+ Object.defineProperty(autoSpy, methodName, { configurable: true, enumerable: true, writable: true, value });
481
+ }
478
482
  function defineLazyMethodSpy(autoSpy, methodName) {
479
483
  Object.defineProperty(autoSpy, methodName, {
480
484
  configurable: true,
481
485
  enumerable: true,
482
486
  get() {
483
487
  const spy = createFunctionSpy(methodName);
484
- Object.defineProperty(autoSpy, methodName, { configurable: true, enumerable: true, writable: true, value: spy });
488
+ materializeMethodSpy(autoSpy, methodName, spy);
485
489
  return spy;
490
+ },
491
+ set(value) {
492
+ materializeMethodSpy(autoSpy, methodName, value);
486
493
  }
487
494
  });
488
495
  }
@@ -495,6 +502,7 @@ function resolveConfiguration(methodsToSpyOnOrConfig) {
495
502
  }
496
503
  return {
497
504
  methodsToSpyOn: methodsToSpyOnOrConfig.methodsToSpyOn ?? [],
505
+ instanceMethodsToSpyOn: methodsToSpyOnOrConfig.instanceMethodsToSpyOn ?? [],
498
506
  observablePropsToSpyOn: methodsToSpyOnOrConfig.observablePropsToSpyOn ?? [],
499
507
  settersToSpyOn: methodsToSpyOnOrConfig.settersToSpyOn ?? [],
500
508
  gettersToSpyOn: methodsToSpyOnOrConfig.gettersToSpyOn ?? [],
@@ -504,9 +512,10 @@ function resolveConfiguration(methodsToSpyOnOrConfig) {
504
512
  }
505
513
  function createSpyFromClass(ObjectClass, methodsToSpyOnOrConfig) {
506
514
  const config = resolveConfiguration(methodsToSpyOnOrConfig);
507
- const methodNames = config.methodsToSpyOn.length > 0 ? config.methodsToSpyOn : getAllMethodNames(ObjectClass.prototype);
515
+ const resolvedMethods = config.methodsToSpyOn.length > 0 ? config.methodsToSpyOn : getAllMethodNames(ObjectClass.prototype);
516
+ const methodNames = config.instanceMethodsToSpyOn.length > 0 ? [.../* @__PURE__ */ new Set([...resolvedMethods, ...config.instanceMethodsToSpyOn])] : resolvedMethods;
508
517
  if (config.methodsToSpyOn.length > 0) {
509
- warnOnUnknownMethods(ObjectClass, config.methodsToSpyOn);
518
+ warnOnUnknownMethods(ObjectClass, config.methodsToSpyOn, config.instanceMethodsToSpyOn);
510
519
  }
511
520
  const autoSpy = {};
512
521
  config.observablePropsToSpyOn.forEach((observablePropName) => {
@@ -544,15 +553,64 @@ function injectSpy(token) {
544
553
  const injected = testing.TestBed.inject(token);
545
554
  return injected;
546
555
  }
556
+ function getPatchedProps() {
557
+ return globalThis.__vitestAutoSpyPatchedProps__ ??= [];
558
+ }
559
+ function rememberProp(object, property) {
560
+ const patch = {
561
+ // The helpers only ever patch objects; the cast bridges the generic `T` of the public API.
562
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- `T` is unconstrained on the public signatures, but every caller passes an object (a service instance, a class prototype or a global).
563
+ object,
564
+ property,
565
+ descriptor: Object.getOwnPropertyDescriptor(object, property)
566
+ };
567
+ getPatchedProps().push(patch);
568
+ return () => {
569
+ const patchedProps = getPatchedProps();
570
+ const index = patchedProps.indexOf(patch);
571
+ if (index === -1) {
572
+ return;
573
+ }
574
+ patchedProps.splice(index, 1);
575
+ restorePatch(patch);
576
+ };
577
+ }
578
+ function restorePatch({ object, property, descriptor }) {
579
+ if (descriptor) {
580
+ Object.defineProperty(object, property, descriptor);
581
+ return;
582
+ }
583
+ Reflect.deleteProperty(object, property);
584
+ }
585
+ function restoreMockedProps() {
586
+ const patchedProps = getPatchedProps();
587
+ patchedProps.reverse().forEach(restorePatch);
588
+ patchedProps.length = 0;
589
+ }
547
590
  function mockReadonlyProp(object, property, value) {
591
+ const restore = rememberProp(object, property);
548
592
  Object.defineProperty(object, property, { get: () => value, configurable: true });
593
+ return restore;
549
594
  }
550
595
  function mockReadonlyPropGetter(object, property, getter) {
596
+ const restore = rememberProp(object, property);
551
597
  Object.defineProperty(object, property, { get: getter, configurable: true });
598
+ return restore;
552
599
  }
553
- function mockAccessorsProp(object, property) {
600
+ function mockValueProp(object, property, value) {
601
+ const restore = rememberProp(object, property);
602
+ Object.defineProperty(object, property, { value, writable: true, configurable: true });
603
+ return restore;
604
+ }
605
+ function mockAccessorsProp(object, property, accessors) {
554
606
  const adapter = getMockAdapter();
555
- Object.defineProperty(object, property, { get: adapter.createMockFn(), set: adapter.createMockFn(), configurable: true });
607
+ const restore = rememberProp(object, property);
608
+ Object.defineProperty(object, property, {
609
+ get: adapter.createMockFn(accessors?.get),
610
+ set: adapter.createMockFn(accessors?.set),
611
+ configurable: true
612
+ });
613
+ return restore;
556
614
  }
557
615
 
558
616
  // src/angular.ts
@@ -562,4 +620,6 @@ exports.injectSpy = injectSpy;
562
620
  exports.mockAccessorsProp = mockAccessorsProp;
563
621
  exports.mockReadonlyProp = mockReadonlyProp;
564
622
  exports.mockReadonlyPropGetter = mockReadonlyPropGetter;
623
+ exports.mockValueProp = mockValueProp;
565
624
  exports.provideAutoSpy = provideAutoSpy;
625
+ exports.restoreMockedProps = restoreMockedProps;