vitest-auto-spy 3.14.0 → 3.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AGENTS.md +98 -12
  2. package/README.md +50 -19
  3. package/dist/angular.d.ts +97 -8
  4. package/dist/angular.js +174 -63
  5. package/dist/bun-angular.d.ts +4 -4
  6. package/dist/bun-angular.js +7 -8
  7. package/dist/bun.d.ts +4 -4
  8. package/dist/bun.js +5 -5
  9. package/dist/chunk-4K5F2NVN.js +91 -0
  10. package/dist/{chunk-Y5KHK2GG.js → chunk-52DZMI55.js} +96 -129
  11. package/dist/{chunk-USCBO2MG.js → chunk-GEXYXX4P.js} +2 -1
  12. package/dist/{chunk-OF4YYOUI.js → chunk-IZO7U34R.js} +4 -3
  13. package/dist/{chunk-GVTSBOZ3.js → chunk-MMKKLL5J.js} +1 -1
  14. package/dist/{chunk-3JTZD6ME.js → chunk-VPRUMHEL.js} +2 -2
  15. package/dist/console.d.ts +1 -1
  16. package/dist/console.js +2 -2
  17. package/dist/eslint-plugin.cjs +37 -0
  18. package/dist/eslint-plugin.js +37 -0
  19. package/dist/{expect-emission-DMx5uXie.d.ts → expect-emission-COcg6YzT.d.ts} +1 -1
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +113 -61
  22. package/dist/jasmine-compat.d.ts +1 -1
  23. package/dist/jasmine.d.ts +2 -2
  24. package/dist/jasmine.js +3 -3
  25. package/dist/nestjs.d.ts +2 -2
  26. package/dist/nestjs.js +4 -4
  27. package/dist/node.cjs +119 -59
  28. package/dist/node.d.ts +3 -3
  29. package/dist/node.js +5 -5
  30. package/dist/{prop-mock-DyjtZVoA.d.ts → prop-mock-CEBKBA7k.d.ts} +1 -1
  31. package/dist/react.d.ts +3 -3
  32. package/dist/react.js +5 -5
  33. package/dist/rxjs.d.ts +2 -10
  34. package/dist/rxjs.js +119 -76
  35. package/dist/setup.d.ts +21 -2
  36. package/dist/setup.js +41 -6
  37. package/dist/svelte.d.ts +3 -3
  38. package/dist/svelte.js +5 -5
  39. package/dist/{track-injections-BQndbppn.d.ts → track-injections-BU-WSjji.d.ts} +1 -1
  40. package/dist/{types-Br-Usge9.d.ts → types-W5kEgzND.d.ts} +38 -7
  41. package/dist/vue.d.ts +4 -4
  42. package/dist/vue.js +6 -6
  43. package/dist/{zoneless-CEhs7o9p.d.ts → zoneless-DzUXGIYb.d.ts} +3 -3
  44. package/package.json +4 -2
  45. package/skills/vitest-auto-spy/SKILL.md +14 -2
  46. package/dist/chunk-J5O72UHX.js +0 -6
package/AGENTS.md CHANGED
@@ -51,14 +51,14 @@ adapter installed and spies fail at runtime.
51
51
 
52
52
  Five add-ons, orthogonal to the runner:
53
53
 
54
- | Add-on | Import | Needed for |
55
- | ---------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
56
- | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
57
- | 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 |
58
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
59
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
60
- | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
61
- | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
54
+ | Add-on | Import | Needed for |
55
+ | ----------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
56
+ | Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
57
+ | 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 |
58
+ | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
59
+ | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
60
+ | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
61
+ | jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
62
62
 
63
63
  `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
64
64
  `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
@@ -349,7 +349,7 @@ Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementat
349
349
 
350
350
  | Return type | Helpers added |
351
351
  | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
352
- | anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)`, `mustBeCalledWith(...args)` → same |
352
+ | anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)` |
353
353
  | `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
354
354
  | `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
355
355
 
@@ -379,8 +379,24 @@ feed.items$.nextWithValues([{ value: a }, { value: b, delay: 100 }, { complete:
379
379
  const [first$, second$] = feed.watch$.nextWithPerCall([{ value: 'a' }, { value: 'b', doNotComplete: true }]);
380
380
  feed.items$.throwWith('FAKE ERROR');
381
381
  const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the helpers miss
382
+
383
+ // throwing — `failWith`, on a spy of any return type
384
+ cart.checkout.failWith(new HttpErrorResponse({ status: 500 })); // every call throws
385
+ cart.checkout.calledWith(BAD_ID).failWith(new Error('unknown cart')); // only these arguments throw
382
386
  ```
383
387
 
388
+ `failWith` is the sync counterpart of `rejectWith`, and it is **not** called `throwWith` — that name
389
+ belongs to the observable helper above, which errors the stream. At runtime every spy carries every
390
+ bundle; only the return type in `Spy<T>` tells them apart, so one name for both would mean whichever
391
+ is attached last silently wins.
392
+
393
+ On Vitest, `mockThrow` / `mockThrowOnce` (4.1) do the spy-level half of this too. `failWith` exists
394
+ because Bun and `node:test` ship neither, and because **no** runtime can make one `calledWith` chain
395
+ throw while its siblings answer normally — `mockImplementation` replaces the whole dispatch, which is
396
+ the opposite of configuring one set of arguments. A `failWith` supersedes a `resolveWith` /
397
+ `nextWith` / per-call batch configured before it, and is superseded by one configured after, so the
398
+ outcome never depends on the order the spec happens to be written in.
399
+
384
400
  An exact argument list is matched before the asymmetric configs, and those are tried in
385
401
  registration order — a narrow config written first keeps its calls. Two matchers count as the same
386
402
  argument when they accept the same values (same matcher class, sample and inversion), which is what
@@ -922,7 +938,21 @@ setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors
922
938
  The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
923
939
  `cancelStrayTimers()` (returns how many it cancelled) and `countStrayTimers()`, all from
924
940
  `vitest-auto-spy/setup`. Use `expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak
925
- fail rather than be tidied away.
941
+ fail rather than be tidied away, or take the per-file count from the sweep itself:
942
+
943
+ ```ts
944
+ setupAutoSpy({ strayTimers: true, onStrayTimers: ({ cancelled }) => expect(cancelled).toBe(0) });
945
+ ```
946
+
947
+ **With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
948
+ arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
949
+ Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
950
+ reports **no leaks** for a file that leaks. Cancelling is still the right default — a callback firing
951
+ during a later file is the more expensive failure — so when both are on and no `onStrayTimers` is
952
+ given, the sweep prints one line to stderr saying how many it took away. To see _where_ each one was
953
+ scheduled, re-run that file with `strayTimers` off and read Vitest's report; the code frame points
954
+ at the `setTimeout` in the spec, because the library's own scheduler wrappers go through
955
+ `vi.defineHelper` and are dropped from the stack.
926
956
 
927
957
  **The one that keeps a suite green while it is wrong:** zone.js replaces the global `Promise`, and a
928
958
  rejection nobody handled is drained into `console.error` and no further — it never reaches
@@ -944,6 +974,12 @@ undo), `flushStrayRejections()` (takes what was captured and starts again from e
944
974
  `countStrayRejections()`. The `no-floating-assertion` lint rule catches the commonest shape before
945
975
  it ever runs (§16).
946
976
 
977
+ **`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
978
+ about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
979
+ into `console.error` before `process.on('unhandledRejection')` would fire, so there is nothing for
980
+ that callback to filter. Use `onUnhandledError` to triage the native failures Vitest already reports;
981
+ use `strayRejections` to find the ones it never sees.
982
+
947
983
  A rejection the runner has **already** blamed the finished test for is not reported again. An
948
984
  `async` test that fails an assertion leaves its own `AssertionError` in both places, so a red run
949
985
  used to print two messages per failure and the second one sent the reader hunting for a defect that
@@ -1354,6 +1390,44 @@ const myService = injectSpy(MyService); // Spy<MyService>
1354
1390
  `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
1355
1391
  zone.js alike.
1356
1392
 
1393
+ ### The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
1394
+
1395
+ The block above, written once instead of once per dependency, with the types inferred rather than
1396
+ declared:
1397
+
1398
+ ```ts
1399
+ import { test as base } from 'vitest';
1400
+ import { extendWithAutoSpies } from 'vitest-auto-spy/angular';
1401
+
1402
+ const test = extendWithAutoSpies(base, {
1403
+ cart: CartService,
1404
+ api: [ApiService, { returns: { get: of([]) } }],
1405
+ passcode: PASSCODE_TOKEN,
1406
+ });
1407
+
1408
+ test('checks out', async ({ cart }) => {
1409
+ cart.checkout.resolveWith(true);
1410
+
1411
+ await expect(cart.checkout(1)).resolves.toBe(true);
1412
+ });
1413
+ ```
1414
+
1415
+ No `let cart: Spy<CartService>` that is `undefined` between tests, and a test that never destructures
1416
+ `api` never builds it. Entries are a class, `[Class, config]` with whatever `provideAutoSpy` takes,
1417
+ or an `InjectionToken` (built from the token's own type, like `provideAutoSpyForToken`). Extra
1418
+ providers — the component under test, a real service, `provideHttpClient()` — go in the third
1419
+ argument and are registered in the same call, ahead of the generated ones, so a token named there
1420
+ wins.
1421
+
1422
+ **It takes the whole map at once, and that is a `TestBed` constraint rather than a typing one.** The
1423
+ composing form — `base.extend('cart', …).extend('api', …)` — cannot work: fixtures resolve
1424
+ independently, so `cart` would configure the testing module _and_ inject, which instantiates it, and
1425
+ `api` would then reach `configureTestingModule` after instantiation and fail with Angular's own
1426
+ "Cannot configure the test module when the test module has already been instantiated". A `beforeEach`
1427
+ that configures the module further still composes, because it runs before any fixture resolves and
1428
+ repeated `configureTestingModule` calls merge right up until the first injection. A `beforeEach` that
1429
+ **injects** does not, and nothing can repair that from here.
1430
+
1357
1431
  The token may be an **abstract class** — `abstract class LocalStorage extends AbstractStorage {}`,
1358
1432
  the shape production provides with `useClass`. Its members are erased before they reach a prototype,
1359
1433
  so there is nothing to discover; the factory notices and returns the `createAutoMock` proxy, which
@@ -1958,6 +2032,7 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
1958
2032
  | `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
1959
2033
  | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
1960
2034
  | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
2035
+ | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
1961
2036
  | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
1962
2037
  | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
1963
2038
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
@@ -1967,10 +2042,10 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
1967
2042
  | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
1968
2043
  | `prefer-native-spy-api` | `off` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim |
1969
2044
 
1970
- Eighteen rules; three fix on their own, seven offer suggestions. The last four are for a suite
2045
+ Nineteen rules; three fix on their own, seven offer suggestions. The last four are for a suite
1971
2046
  mid-migration off `jasmine-auto-spies` (§20); `prefer-native-spy-api` is **off** in the recommended
1972
2047
  config because it reports working code — the compatibility layer is what a suite runs on while it is
1973
- being migrated. `no-mocked-for-spy` only ever touches a `no-mocked-for-spy` only ever touches a
2048
+ being migrated. `no-mocked-for-spy` only ever touches a
1974
2049
  **type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
1975
2050
  meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
1976
2051
  the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
@@ -2096,6 +2171,7 @@ packages, which a subpath export can never be.
2096
2171
  | a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
2097
2172
  | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2098
2173
  | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2174
+ | `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
2099
2175
  | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2100
2176
  | `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
2101
2177
  | `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
@@ -2159,6 +2235,8 @@ packages, which a subpath export can never be.
2159
2235
  | two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
2160
2236
  | a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
2161
2237
  | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
2238
+ | a green test whose only line about a call is `spy.m.calledWith(1);` | that is a **stub**, not an assertion — chai's `expect(fn).to.have.been.calledWith(x)` is the one that checks | continue the chain, or `expect(spy.m).toHaveBeenCalledWith(1)` — `no-bare-called-with` (§16) |
2239
+ | `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | re-run that file with `strayTimers` off; the warning on stderr says how many were taken (§10) |
2162
2240
 
2163
2241
  **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
2164
2242
  nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
@@ -2242,6 +2320,14 @@ npx tsc --noEmit # Spy<T> mistakes are compile errors, not
2242
2320
  npx vitest-auto-spy doctor # suite-level defects that never fail a run
2243
2321
  ```
2244
2322
 
2323
+ **If you are an agent, add `--reporter=agent`** (Vitest 4.1). It is Vitest's own reporter, added for
2324
+ exactly this: the same failures, without the passing-test roll call and the repeated banners that
2325
+ make a run's output expensive to read and expensive to carry.
2326
+
2327
+ ```bash
2328
+ npx vitest run --reporter=agent path/to/file.spec.ts
2329
+ ```
2330
+
2245
2331
  Type errors matter here more than usual: most of this library's guarantees are type-level, so a
2246
2332
  suite that runs green but does not type-check is not done.
2247
2333
 
package/README.md CHANGED
@@ -19,7 +19,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
19
19
  [![npm version](https://img.shields.io/npm/v/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
20
20
  [![npm downloads](https://img.shields.io/npm/dm/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
21
21
  [![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)
22
- [![minzipped size](https://img.shields.io/badge/minzip-14.9%20kB-brightgreen)](#install)
22
+ [![minzipped size](https://img.shields.io/badge/minzip-15.1%20kB-brightgreen)](#install)
23
23
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
24
24
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
25
25
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -63,7 +63,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
63
63
  - 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
64
64
  - 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
65
65
  - 🚚 A migration you can verify — `compareTestRuns` on the two JSON reports, `diffByField` for the assertion the reporter collapses
66
- - 📏 Lint rules and one-line test-run hygiene — eighteen rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, seven suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
66
+ - 📏 Lint rules and one-line test-run hygiene — nineteen rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, seven suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()`
67
67
  - 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
68
68
  - 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
69
69
  - 🚚 [`npx vitest-auto-spy codemod`](#codemod--migrating-a-suite-off-jest-auto-spies) — thirteen transforms that move a suite off `jest-auto-spies` and Jest, or off `jasmine-auto-spies` and jasmine (`--from jasmine`), dry-run by default, with a `--verify` pass that also checks a file somebody edited by hand
@@ -604,6 +604,23 @@ cart.total.mockReturnValue(42);
604
604
  `CartService` and the spy has it. A hand-written `{ provide: CartService, useValue: { total:
605
605
  vi.fn() } }` silently keeps mocking yesterday's class.
606
606
 
607
+ On **Vitest 4.1+**, the same thing as fixtures — one statement, types inferred, and a test that
608
+ never names a dependency never builds it:
609
+
610
+ ```ts
611
+ import { test as base } from 'vitest';
612
+ import { extendWithAutoSpies } from 'vitest-auto-spy/angular';
613
+
614
+ const test = extendWithAutoSpies(base, { cart: CartService, api: [ApiService, { returns: { get: of([]) } }] });
615
+
616
+ test('checks out', ({ cart }) => cart.total.mockReturnValue(42));
617
+ ```
618
+
619
+ It takes the whole map at once because every provider has to be known before the first injection —
620
+ [why, in full](https://asdalexey.github.io/vitest-auto-spy/adapters/angular#fixtures-instead-of-let-beforeeach-extendwithautospies).
621
+ On an older Vitest it throws `needs Vitest 4.1 or newer` up front, instead of letting the object-form
622
+ `extend` register nonsense and every test die on `undefined`.
623
+
607
624
  ### How to mock: a service without DI
608
625
 
609
626
  ```ts
@@ -668,6 +685,18 @@ Not `source$.subscribe(value => expect(value).toEqual(…))`: if the stream neve
668
685
  never runs, nothing is asserted, and the test is green and empty. `expectEmission` **is** the
669
686
  assertion — a silent stream fails it, with the stream's name and the timeout in the message.
670
687
 
688
+ ### How to mock: a call that has to throw
689
+
690
+ ```ts
691
+ cart.checkout.failWith(new HttpErrorResponse({ status: 500 })); // every call throws
692
+ cart.checkout.calledWith(BAD_ID).failWith(new Error('unknown cart')); // only these arguments
693
+ ```
694
+
695
+ `failWith` works on a spy of any return type. Vitest 4.1's `mockThrow` covers the first line; Bun
696
+ and `node:test` have no equivalent, and **no** runtime has one for the second — `mockImplementation`
697
+ replaces the whole dispatch, which is the opposite of configuring one set of arguments. It is not
698
+ called `throwWith` because that name already means _error the stream_ on an observable spy.
699
+
671
700
  ### How to mock: a promise a test forgets to await
672
701
 
673
702
  ```ts
@@ -2235,20 +2264,21 @@ expensive to diagnose when it is missing. The first three are on by default:
2235
2264
  timer in sight: a request matching no Express route is ended by `finalhandler` on `setImmediate`,
2236
2265
  so the 404 is never written and a routing mistake is reported as a slow test.
2237
2266
 
2238
- | Option | Default | Notes |
2239
- | --------------------- | --------- | ------------------------------------------------------------------------------- |
2240
- | `duplicateCopies` | `'throw'` | `'warn'` to report without failing, `'off'` to skip the check |
2241
- | `restoreProps` | `true` | `restoreMockedProps()` in a global `afterEach` |
2242
- | `restoreMocks` | `false` | `vi.restoreAllMocks()` in a global `afterEach` — turn on for `isolate: false` |
2243
- | `strayTimers` | `false` | Cancel timeouts, intervals and frames that outlive their file |
2244
- | `strayRejections` | `false` | Fail the test a rejection zone.js swallowed surfaced in — needs zone.js |
2245
- | `blockNetwork` | `false` | Close every network channel the environment has — `true`, or a narrowing object |
2246
- | `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
2247
- | `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
2248
- | `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
2249
- | `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
2250
- | `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
2251
- | `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
2267
+ | Option | Default | Notes |
2268
+ | --------------------- | --------- | ------------------------------------------------------------------------------------- |
2269
+ | `duplicateCopies` | `'throw'` | `'warn'` to report without failing, `'off'` to skip the check |
2270
+ | `restoreProps` | `true` | `restoreMockedProps()` in a global `afterEach` |
2271
+ | `restoreMocks` | `false` | `vi.restoreAllMocks()` in a global `afterEach` — turn on for `isolate: false` |
2272
+ | `strayTimers` | `false` | Cancel timeouts, intervals and frames that outlive their file |
2273
+ | `onStrayTimers` | — | Takes the per-file count the sweep cancelled — see the note on `--detect-async-leaks` |
2274
+ | `strayRejections` | `false` | Fail the test a rejection zone.js swallowed surfaced in — needs zone.js |
2275
+ | `blockNetwork` | `false` | Close every network channel the environment has — `true`, or a narrowing object |
2276
+ | `guardGlobals` | `'off'` | Report a test that redefines a global property as non-configurable |
2277
+ | `globalFakeTimers` | `false` | Fake timers for every test **and between them** — Jest's `enableGlobally` |
2278
+ | `restoreTimerGlobals` | `true` | Put back timer globals that uninstalling the fakes deleted |
2279
+ | `pruneMockRegistry` | `false` | Keep @vitest/spy's ever-growing mock registry to the mocks that outlive a file |
2280
+ | `hookTimeoutHint` | `true` | Explain a hook that ran out of `hookTimeout` while `testTimeout` is larger |
2281
+ | `frozenClockHint` | `true` | Explain a timeout that happened because nothing advanced the fake clock |
2252
2282
 
2253
2283
  `restoreMocks` is off by default because it also drops `vi.spyOn` stubs a suite installed in
2254
2284
  `beforeAll`; it is the knob to reach for when the run shares one environment across files.
@@ -2395,6 +2425,7 @@ export can never be.
2395
2425
  | `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
2396
2426
  | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
2397
2427
  | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
2428
+ | `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement of its own — a stub nobody continued, asserting nothing |
2398
2429
  | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
2399
2430
  | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2400
2431
  | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
@@ -2408,7 +2439,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
2408
2439
  "don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
2409
2440
  are versioned together and stop being re-written in every project that installs the package.
2410
2441
 
2411
- **Three of the eighteen fix on their own, seven offer suggestions**, and the split is not about how hard
2442
+ **Three of the nineteen fix on their own, seven offer suggestions**, and the split is not about how hard
2412
2443
  the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
2413
2444
  compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
2414
2445
  `import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
@@ -2510,7 +2541,7 @@ package's own — it needs its ESLint integration switched on.
2510
2541
  ### WebStorm and the other JetBrains IDEs
2511
2542
 
2512
2543
  No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
2513
- all run ESLint natively, so the eighteen rules appear inline, in the **Problems** tool window, and
2544
+ all run ESLint natively, so the nineteen rules appear inline, in the **Problems** tool window, and
2514
2545
  under **Code → Inspect Code** for the whole project.
2515
2546
 
2516
2547
  ```js
@@ -2527,7 +2558,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
2527
2558
  the fixes and suggestions live.
2528
2559
 
2529
2560
  A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
2530
- and then keep a second copy of eighteen rules, in Kotlin, in step with the TypeScript ones.
2561
+ and then keep a second copy of nineteen rules, in Kotlin, in step with the TypeScript ones.
2531
2562
 
2532
2563
  ### VS Code, Cursor, Windsurf, VSCodium
2533
2564
 
package/dist/angular.d.ts CHANGED
@@ -1,12 +1,101 @@
1
- export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-CEhs7o9p.js';
2
- import { Type, Signal, WritableSignal } from '@angular/core';
3
- import { S as Spy, C as ClassType, a as ClassSpyConfiguration, O as OnlyMethodKeysOf, j as AddSpyMethodsByReturnTypes } from './types-Br-Usge9.js';
4
- export { I as InjectionLog, T as TrackInjectionsOptions, a as TrackedProvider, t as trackInjections } from './track-injections-BQndbppn.js';
5
- export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-DyjtZVoA.js';
6
- export { C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-DMx5uXie.js';
1
+ export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-DzUXGIYb.js';
2
+ import { InjectionToken, Provider, Type, Signal, WritableSignal } from '@angular/core';
3
+ import { TestAPI } from 'vitest';
4
+ import { C as ClassType, S as Spy, a as ClassSpyConfiguration, O as OnlyMethodKeysOf, j as AddSpyMethodsByReturnTypes } from './types-W5kEgzND.js';
5
+ export { I as InjectionLog, T as TrackInjectionsOptions, a as TrackedProvider, t as trackInjections } from './track-injections-BU-WSjji.js';
6
+ export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-CEBKBA7k.js';
7
+ export { C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-COcg6YzT.js';
7
8
  import '@angular/core/testing';
8
9
  import 'rxjs';
9
- import 'vitest';
10
+
11
+ /**
12
+ * `extendWithAutoSpies` — the `TestBed` half of a spec, moved into Vitest fixtures.
13
+ *
14
+ * The shape it replaces is in every Angular suite ever migrated:
15
+ *
16
+ * ```ts
17
+ * let cart: Spy<CartService>;
18
+ * let api: Spy<ApiService>;
19
+ *
20
+ * beforeEach(() => {
21
+ * TestBed.configureTestingModule({ providers: [provideAutoSpy(CartService), provideAutoSpy(ApiService)] });
22
+ * cart = injectSpy(CartService);
23
+ * api = injectSpy(ApiService);
24
+ * });
25
+ * ```
26
+ *
27
+ * Five lines of ceremony per dependency, a `let` that is `undefined` between tests and a
28
+ * declaration whose type has to be repeated by hand. Vitest 4.1's builder form of `test.extend`
29
+ * infers the type from the factory, so the whole block becomes one statement — and a test that does
30
+ * not destructure `api` never builds it.
31
+ *
32
+ * **Why one call rather than a chain of `.extend`s.** The obvious composing form —
33
+ * `base.extend('cart', autoSpy(CartService)).extend('api', autoSpy(ApiService))` — cannot work, and
34
+ * the reason is `TestBed`, not typing. Fixtures resolve lazily and independently, so `cart` would
35
+ * configure the testing module and inject, which *instantiates* it; `api` would then reach
36
+ * `configureTestingModule` after instantiation and fail with Angular's own
37
+ * "Cannot configure the test module when the test module has already been instantiated". Every
38
+ * provider has to be known before the first injection, so the helper has to see them all at once.
39
+ *
40
+ * A `beforeEach` that configures the module further still composes, because it runs before any
41
+ * fixture resolves and `configureTestingModule` may be called repeatedly right up until the first
42
+ * injection. A `beforeEach` that *injects* does not — and that is the one case this cannot paper
43
+ * over, since by then Angular has already made the decision.
44
+ */
45
+
46
+ /**
47
+ * One entry of the map: a class, a class with the configuration {@link provideAutoSpy} takes, or an
48
+ * `InjectionToken` whose type argument is what the double is built from.
49
+ */
50
+ type AutoSpyFixture = ClassType<any> | InjectionToken<unknown> | readonly [ClassType<any>, ...unknown[]];
51
+ /** What each entry of the map becomes in the test's context. */
52
+ type SpiedFixtures<Spec> = {
53
+ [Name in keyof Spec]: Spec[Name] extends InjectionToken<infer T> ? Spy<T> : Spec[Name] extends readonly [ClassType<infer T>, ...unknown[]] ? Spy<T> : Spec[Name] extends ClassType<infer T> ? Spy<T> : never;
54
+ };
55
+ /** Options for {@link extendWithAutoSpies}. */
56
+ interface ExtendWithAutoSpiesOptions {
57
+ /**
58
+ * Providers registered alongside the spies, in the same `configureTestingModule` call — the
59
+ * component under test, a real service the spec deliberately keeps, `provideHttpClient()`.
60
+ *
61
+ * They go in *before* the generated ones, so a provider named here wins over the auto-spy that
62
+ * would otherwise be made for the same token.
63
+ */
64
+ providers?: Provider[];
65
+ }
66
+ /**
67
+ * Turn a map of dependencies into typed `TestBed` fixtures.
68
+ *
69
+ * ```ts
70
+ * import { test as base } from 'vitest';
71
+ * import { extendWithAutoSpies } from 'vitest-auto-spy/angular';
72
+ *
73
+ * const test = extendWithAutoSpies(base, {
74
+ * cart: CartService,
75
+ * api: [ApiService, { methodsToSpyOn: ['get'] }],
76
+ * passcode: PASSCODE_TOKEN,
77
+ * });
78
+ *
79
+ * test('checks out', async ({ cart }) => {
80
+ * cart.checkout.resolveWith(true);
81
+ *
82
+ * await expect(cart.checkout(1)).resolves.toBe(true);
83
+ * });
84
+ * ```
85
+ *
86
+ * The testing module is configured **once per test**, the first time any of the fixtures is
87
+ * touched, and the flag that remembers it is released on cleanup — a fixture object outlives the
88
+ * test that used it, and a module configured for the previous one is the shape that makes an
89
+ * Angular suite fail depending on order.
90
+ *
91
+ * Needs **Vitest 4.1 or newer**: the builder form of `test.extend` it is written against is what
92
+ * infers a fixture's type from its factory, and older versions have only the object form.
93
+ *
94
+ * @param base The `test` to extend — Vitest's own, or one already carrying fixtures of yours.
95
+ * @param spec Fixture name to dependency. A bare class, `[Class, config]` with what
96
+ * {@link provideAutoSpy} accepts, or an `InjectionToken`.
97
+ */
98
+ declare function extendWithAutoSpies<Context, const Spec extends Record<string, AutoSpyFixture>>(base: TestAPI<Context>, spec: Spec, { providers }?: ExtendWithAutoSpiesOptions): TestAPI<Context & SpiedFixtures<Spec>>;
10
99
 
11
100
  /**
12
101
  * Overriding what a *component* provides, and the diagnostic for the bundle that makes it necessary.
@@ -615,4 +704,4 @@ declare function reportSpecTiming(timing: SpecTiming): void;
615
704
  */
616
705
  declare function enableTestBedDiagnostics(options?: TestBedDiagnosticsOptions): void;
617
706
 
618
- export { type AngularDiagnosticsOptions, type AngularTestEnvMode, type AngularTestEnvOptions, type AutoSpyOverride, type DirectiveHostOptions, type MockedResource, type ResourceDouble, type ResourceDoubleStatus, type ResourceLike, type SignalLike, type SpecTiming, type TestBedDiagnosticsOptions, assertComponentDefIntact, assertNgModuleScopes, assertNoPendingRequests, createDirectiveHost, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, mockResourceProp, mockSignalProp, overrideAutoSpy, overrideComponentProvider, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, reportSpecTiming, setupAngularTestEnv };
707
+ export { type AngularDiagnosticsOptions, type AngularTestEnvMode, type AngularTestEnvOptions, type AutoSpyFixture, type AutoSpyOverride, type DirectiveHostOptions, type ExtendWithAutoSpiesOptions, type MockedResource, type ResourceDouble, type ResourceDoubleStatus, type ResourceLike, type SignalLike, type SpecTiming, type SpiedFixtures, type TestBedDiagnosticsOptions, assertComponentDefIntact, assertNgModuleScopes, assertNoPendingRequests, createDirectiveHost, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, extendWithAutoSpies, formatSpecTiming, getTestBedTiming, instrumentTestBed, mockResourceProp, mockSignalProp, overrideAutoSpy, overrideComponentProvider, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, reportSpecTiming, setupAngularTestEnv };