vitest-auto-spy 3.13.1 → 3.15.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 (52) hide show
  1. package/AGENTS.md +330 -98
  2. package/README.md +380 -197
  3. package/dist/angular.d.ts +97 -8
  4. package/dist/angular.js +96 -6
  5. package/dist/bun-angular.d.ts +4 -4
  6. package/dist/bun-angular.js +8 -7
  7. package/dist/bun.d.ts +4 -4
  8. package/dist/bun.js +5 -4
  9. package/dist/{chunk-PCP6A6PD.js → chunk-3OX44LME.js} +6 -16
  10. package/dist/{chunk-ZY7KPBJJ.js → chunk-AQSVZK2R.js} +33 -3
  11. package/dist/{chunk-C3IZLTSJ.js → chunk-CR5Q3YMQ.js} +4 -3
  12. package/dist/{chunk-WVW4XRL5.js → chunk-DMOJ4NZH.js} +1 -1
  13. package/dist/{chunk-CEVEKNJZ.js → chunk-LIOV4WZK.js} +2 -2
  14. package/dist/chunk-MAKQSO2R.js +162 -0
  15. package/dist/chunk-PC3FAZT3.js +14 -0
  16. package/dist/chunk-XJ3UMJN3.js +8 -0
  17. package/dist/cli.js +689 -182
  18. package/dist/console.d.ts +1 -1
  19. package/dist/console.js +1 -1
  20. package/dist/eslint-plugin.cjs +430 -29
  21. package/dist/eslint-plugin.js +430 -29
  22. package/dist/{expect-emission-DMx5uXie.d.ts → expect-emission-COcg6YzT.d.ts} +1 -1
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +35 -4
  25. package/dist/jasmine-compat.d.ts +186 -0
  26. package/dist/jasmine-compat.js +1 -0
  27. package/dist/jasmine.d.ts +210 -0
  28. package/dist/jasmine.js +260 -0
  29. package/dist/nestjs.d.ts +2 -2
  30. package/dist/nestjs.js +4 -3
  31. package/dist/node.cjs +27 -3
  32. package/dist/node.d.ts +3 -3
  33. package/dist/node.js +5 -4
  34. package/dist/observer-spy.d.ts +133 -0
  35. package/dist/observer-spy.js +158 -0
  36. package/dist/{prop-mock-DyjtZVoA.d.ts → prop-mock-CEBKBA7k.d.ts} +1 -1
  37. package/dist/react.d.ts +3 -3
  38. package/dist/react.js +5 -4
  39. package/dist/rxjs.d.ts +2 -2
  40. package/dist/rxjs.js +1 -0
  41. package/dist/setup.d.ts +21 -2
  42. package/dist/setup.js +44 -12
  43. package/dist/shared-state.js +19 -7
  44. package/dist/svelte.d.ts +3 -3
  45. package/dist/svelte.js +5 -4
  46. package/dist/{track-injections-BQndbppn.d.ts → track-injections-BU-WSjji.d.ts} +1 -1
  47. package/dist/{types-Br-Usge9.d.ts → types-W5kEgzND.d.ts} +38 -7
  48. package/dist/vue.d.ts +4 -4
  49. package/dist/vue.js +6 -5
  50. package/dist/{zoneless-CEhs7o9p.d.ts → zoneless-DzUXGIYb.d.ts} +3 -3
  51. package/package.json +32 -3
  52. package/skills/vitest-auto-spy/SKILL.md +63 -3
package/AGENTS.md CHANGED
@@ -49,14 +49,19 @@ adapter installed and spies fail at runtime.
49
49
  | Vue / Pinia | `vitest-auto-spy/vue` |
50
50
  | Svelte | `vitest-auto-spy/svelte` |
51
51
 
52
- Four add-ons, orthogonal to the runner:
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
- | Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
58
- | Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
59
- | Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
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
+
63
+ `vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
64
+ `node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
60
65
 
61
66
  The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
62
67
  CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
@@ -344,7 +349,7 @@ Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementat
344
349
 
345
350
  | Return type | Helpers added |
346
351
  | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
347
- | 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)` |
348
353
  | `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
349
354
  | `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
350
355
 
@@ -374,8 +379,24 @@ feed.items$.nextWithValues([{ value: a }, { value: b, delay: 100 }, { complete:
374
379
  const [first$, second$] = feed.watch$.nextWithPerCall([{ value: 'a' }, { value: 'b', doNotComplete: true }]);
375
380
  feed.items$.throwWith('FAKE ERROR');
376
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
377
386
  ```
378
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
+
379
400
  An exact argument list is matched before the asymmetric configs, and those are tried in
380
401
  registration order — a narrow config written first keeps its calls. Two matchers count as the same
381
402
  argument when they accept the same values (same matcher class, sample and inversion), which is what
@@ -917,7 +938,21 @@ setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors
917
938
  The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
918
939
  `cancelStrayTimers()` (returns how many it cancelled) and `countStrayTimers()`, all from
919
940
  `vitest-auto-spy/setup`. Use `expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak
920
- 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.
921
956
 
922
957
  **The one that keeps a suite green while it is wrong:** zone.js replaces the global `Promise`, and a
923
958
  rejection nobody handled is drained into `console.error` and no further — it never reaches
@@ -939,6 +974,12 @@ undo), `flushStrayRejections()` (takes what was captured and starts again from e
939
974
  `countStrayRejections()`. The `no-floating-assertion` lint rule catches the commonest shape before
940
975
  it ever runs (§16).
941
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
+
942
983
  A rejection the runner has **already** blamed the finished test for is not reported again. An
943
984
  `async` test that fails an assertion leaves its own `AssertionError` in both places, so a red run
944
985
  used to print two messages per failure and the second one sent the reader hunting for a defect that
@@ -970,7 +1011,7 @@ export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
970
1011
  The mocks it keeps, it also guards. Staying registered means `vi.resetAllMocks()` reaches them too —
971
1012
  it walks the same set and calls `mockReset()`, which puts an implementation back only when it was
972
1013
  passed to `vi.fn(implementation)`; behaviour chained on with `.mockReturnValue(…)` or
973
- `.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a *different* file later
1014
+ `.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a _different_ file later
974
1015
  in the same worker, inside application code, as `Cannot read properties of undefined` against a
975
1016
  shared double that spec never touched — and `vi.restoreAllMocks()` does not cause it (Vitest 4 walks
976
1017
  `MOCK_RESTORE` there, which only `vi.spyOn` writes to), so probing with that one comes back green and
@@ -1349,6 +1390,44 @@ const myService = injectSpy(MyService); // Spy<MyService>
1349
1390
  `{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
1350
1391
  zone.js alike.
1351
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
+
1352
1431
  The token may be an **abstract class** — `abstract class LocalStorage extends AbstractStorage {}`,
1353
1432
  the shape production provides with `useClass`. Its members are erased before they reach a prototype,
1354
1433
  so there is nothing to discover; the factory notices and returns the `createAutoMock` proxy, which
@@ -1593,7 +1672,7 @@ TypeError: Cannot read properties of undefined (reading 'provide')
1593
1672
 
1594
1673
  Both obvious cures fail: `await import('@scope/lib')` at the top of `beforeEach` is already too late,
1595
1674
  and a static import at the top of the spec does not fix the order this bundler emits. Worse, the spec
1596
- that breaks is one nobody touched — chunk boundaries move with file *contents*, so editing a type
1675
+ that breaks is one nobody touched — chunk boundaries move with file _contents_, so editing a type
1597
1676
  next door is enough.
1598
1677
 
1599
1678
  ```ts
@@ -1941,24 +2020,32 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
1941
2020
  export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
1942
2021
  ```
1943
2022
 
1944
- | Rule | Level | Fix | Flags |
1945
- | ------------------------------ | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
1946
- | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
1947
- | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
1948
- | `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
1949
- | `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
1950
- | `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
1951
- | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
1952
- | `no-mocked-for-spy` | `warn` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
1953
- | `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
1954
- | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion |
1955
- | `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
1956
- | `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
1957
- | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
1958
- | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
1959
- | `no-unregistered-inject-spy` | `warn` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
1960
-
1961
- Fourteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
2023
+ | Rule | Level | Fix | Flags |
2024
+ | --------------------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
2025
+ | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
2026
+ | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
2027
+ | `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
2028
+ | `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
2029
+ | `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
2030
+ | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
2031
+ | `no-mocked-for-spy` | `warn` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
2032
+ | `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
2033
+ | `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
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 |
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 |
2037
+ | `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
2038
+ | `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
2039
+ | `no-unregistered-inject-spy` | `warn` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
2040
+ | `jasmine-namespace-without-entry` | `warn` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
2041
+ | `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
2042
+ | `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
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 |
2044
+
2045
+ Nineteen rules; three fix on their own, seven offer suggestions. The last four are for a suite
2046
+ mid-migration off `jasmine-auto-spies` (§20); `prefer-native-spy-api` is **off** in the recommended
2047
+ config because it reports working code — the compatibility layer is what a suite runs on while it is
2048
+ being migrated. `no-mocked-for-spy` only ever touches a
1962
2049
  **type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
1963
2050
  meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
1964
2051
  the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
@@ -2073,75 +2160,83 @@ packages, which a subpath export can never be.
2073
2160
 
2074
2161
  ## 17. Error → fix
2075
2162
 
2076
- | Message contains | Cause | Fix |
2077
- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
2078
- | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
2079
- | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
2080
- | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
2081
- | `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
2082
- | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
2083
- | `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) |
2084
- | `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 |
2085
- | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2086
- | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2087
- | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
2088
- | `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
2089
- | `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
2090
- | `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
2091
- | `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
2092
- | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
2093
- | `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
2094
- | `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
2095
- | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
2096
- | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
2097
- | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
2098
- | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
2099
- | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
2100
- | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
2101
- | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2102
- | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
2103
- | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
2104
- | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
2105
- | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2106
- | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
2107
- | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
2108
- | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
2109
- | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
2110
- | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
2111
- | `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
2112
- | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
2113
- | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
2114
- | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
2115
- | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
2116
- | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
2117
- | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
2118
- | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
2119
- | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
2120
- | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
2121
- | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
2122
- | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
2123
- | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
2124
- | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
2125
- | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
2126
- | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
2127
- | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
2128
- | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
2129
- | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
2130
- | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
2131
- | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
2132
- | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
2133
- | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
2134
- | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
2135
- | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
2136
- | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
2137
- | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
2138
- | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic, so it keeps the `readonly` of an abstract getter | `Mutable<Spy<T>>` for direct assignment, or `mockValueProp` for a patch that is undone |
2139
- | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
2140
- | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
2141
- | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
2142
- | 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 |
2143
- | 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) |
2144
- | a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
2163
+ | Message contains | Cause | Fix |
2164
+ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
2165
+ | `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
2166
+ | `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
2167
+ | `Cannot read properties of undefined (reading 'returnValue')` on `spy.m.and.…` | the jasmine namespaces are not installed — the spy was built before them, or nothing installed them at all | import from `vitest-auto-spy/jasmine`; on Bun / `node:test`, `enableJasmineCompat()` in the setup file (§20) |
2168
+ | `spy.withArgs is not a function` | the same, seen from the argument-matching side | the same — or write `spy.m.calledWith(a).mockReturnValue(v)`, which needs no layer (§20) |
2169
+ | `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
2170
+ | `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
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) |
2172
+ | `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
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 |
2175
+ | `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
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) |
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 |
2178
+ | `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
2179
+ | `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
2180
+ | `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
2181
+ | `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
2182
+ | `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
2183
+ | `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
2184
+ | `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
2185
+ | `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
2186
+ | `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
2187
+ | `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
2188
+ | `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
2189
+ | setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
2190
+ | `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
2191
+ | `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
2192
+ | duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
2193
+ | `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
2194
+ | a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
2195
+ | `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
2196
+ | `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
2197
+ | `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
2198
+ | `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
2199
+ | an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
2200
+ | an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
2201
+ | `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
2202
+ | `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
2203
+ | `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
2204
+ | `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
2205
+ | `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
2206
+ | `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
2207
+ | `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
2208
+ | `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
2209
+ | `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
2210
+ | `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
2211
+ | `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
2212
+ | `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
2213
+ | a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
2214
+ | a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
2215
+ | `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
2216
+ | `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
2217
+ | `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
2218
+ | a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
2219
+ | a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
2220
+ | `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
2221
+ | a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
2222
+ | a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
2223
+ | a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
2224
+ | `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
2225
+ | `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
2226
+ | `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
2227
+ | `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
2228
+ | `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
2229
+ | `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
2230
+ | `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
2231
+ | `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic, so it keeps the `readonly` of an abstract getter | `Mutable<Spy<T>>` for direct assignment, or `mockValueProp` for a patch that is undone |
2232
+ | `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
2233
+ | half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
2234
+ | `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
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 |
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) |
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) |
2145
2240
 
2146
2241
  **Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
2147
2242
  nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
@@ -2225,6 +2320,14 @@ npx tsc --noEmit # Spy<T> mistakes are compile errors, not
2225
2320
  npx vitest-auto-spy doctor # suite-level defects that never fail a run
2226
2321
  ```
2227
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
+
2228
2331
  Type errors matter here more than usual: most of this library's guarantees are type-level, so a
2229
2332
  suite that runs green but does not type-check is not done.
2230
2333
 
@@ -2274,3 +2377,132 @@ source.replace(from, () => to); // not source.replace(from, to)
2274
2377
  **`node.getStart()` excludes leading comments.** A codemod that replaces a range starting there
2275
2378
  silently eats the `// eslint-disable-next-line` above the node. Use `node.getFullStart()`, or count
2276
2379
  the comments before and after and compare against `HEAD`.
2380
+
2381
+ ---
2382
+
2383
+ ## 20. Migrating a suite off `jasmine-auto-spies`
2384
+
2385
+ `jasmine-auto-spies` and `jest-auto-spies` are the same library twice — both thin layers over
2386
+ `@hirez_io/auto-spies-core`, with identical configuration keys (`methodsToSpyOn`,
2387
+ `observablePropsToSpyOn`, `gettersToSpyOn`, `settersToSpyOn`) and identical helper names.
2388
+ **Exactly one thing differs**: upstream parks its async helpers behind `.and`, because that is where
2389
+ jasmine keeps its own spy strategies.
2390
+
2391
+ ```ts
2392
+ spy.load.and.nextWith(account); // jasmine-auto-spies
2393
+ spy.load.nextWith(account); // jest-auto-spies, and this library
2394
+ ```
2395
+
2396
+ **Land it green first, rewrite second.** `vitest-auto-spy/jasmine` puts `.and`, `.calls` and
2397
+ `.withArgs` back on every spy, so the import specifier is the only edit needed to get the suite
2398
+ running — and the first red run then has one candidate cause instead of two:
2399
+
2400
+ ```diff
2401
+ - import { createSpyFromClass, provideAutoSpy, type Spy } from 'jasmine-auto-spies';
2402
+ + import { createSpyFromClass, provideAutoSpy, type Spy } from 'vitest-auto-spy/jasmine';
2403
+ ```
2404
+
2405
+ ```bash
2406
+ npx vitest-auto-spy codemod --from jasmine # dry run
2407
+ npx vitest-auto-spy codemod --from jasmine --write # apply, then drop the import
2408
+ npx vitest-auto-spy codemod --from jasmine --verify # match the result, not the diff
2409
+ ```
2410
+
2411
+ `import { jasmine } from 'vitest-auto-spy/jasmine'` restores the whole `jasmine` namespace
2412
+ (`objectContaining`, `any`, `createSpyObj`, `clock()`, `addMatchers`, and the eight asymmetric
2413
+ matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
2414
+ on `globalThis` — it is one explicit line per file, which the codemod later deletes.
2415
+
2416
+ ### The renames, once the suite is green
2417
+
2418
+ | jasmine | here |
2419
+ | ------------------------------------------ | ---------------------------------------------------------- |
2420
+ | `spy.m.and.returnValue(v)` | `spy.m.mockReturnValue(v)` |
2421
+ | `spy.m.and.callFake(fn)` | `spy.m.mockImplementation(fn)` |
2422
+ | `spy.m.and.stub()` | `spy.m.mockImplementation(() => undefined)` |
2423
+ | `spy.m.and.returnValues(a, b)` | `.mockReturnValueOnce(a).mockReturnValueOnce(b)` |
2424
+ | `spy.m.and.throwError('boom')` | `.mockImplementation(() => { throw new Error('boom'); })` |
2425
+ | `spy.m.and.resolveTo(v)` | `spy.m.mockResolvedValue(v)` |
2426
+ | `spy.m.and.nextWith(v)` / `resolveWith(v)` | `spy.m.nextWith(v)` / `spy.m.resolveWith(v)` — drop `.and` |
2427
+ | `spy.m.withArgs(a).and.returnValue(v)` | `spy.m.calledWith(a).mockReturnValue(v)` |
2428
+ | `spy.m.calls.count()` / `argsFor(i)` | `spy.m.mock.calls.length` / `spy.m.mock.calls[i]` |
2429
+ | `spy.m.calls.reset()` | `spy.m.mockClear()` |
2430
+ | `jasmine.createSpy('n')` | `vi.fn()` — or `createFunctionSpy<F>('n')`, which is typed |
2431
+ | `jasmine.any(X)` / `objectContaining({…})` | `expect.any(X)` / `expect.objectContaining({…})` |
2432
+ | `jasmine.clock().tick(n)` | `vi.advanceTimersByTime(n)` — or `await advanceTimers(n)` |
2433
+ | `jasmine.SpyObj<T>` / `jasmine.Spy` | `Spy<T>` from this package / `Mock` from `vitest` |
2434
+ | `expect(x).toBeTrue()` / `toHaveSize(n)` | `.toBe(true)` / `.toHaveLength(n)` |
2435
+ | `expect(spy).toHaveBeenCalledOnceWith(a)` | `.toHaveBeenCalledExactlyOnceWith(a)` |
2436
+ | `fail(msg)` | `expect.fail(msg)` — there is no `vi.fail` |
2437
+
2438
+ ### Four traps, all of them silent
2439
+
2440
+ 1. **`spyOn(o, 'm')` inverts.** jasmine's `spyOn` installs a **stub**; `vi.spyOn` **calls through**.
2441
+ A bare rename compiles, passes, and starts running the real implementation inside every spec that
2442
+ installed the spy in order to stop it. Write
2443
+ `vi.spyOn(o, 'm').mockImplementation(() => undefined)` — the codemod appends exactly that, and a
2444
+ file whose only jasmine construct is `spyOn(` is deliberately **not** auto-detected: that suite
2445
+ has to say `--from jasmine` out loud.
2446
+ 2. **`.withContext('msg')` does not throw — it loses the message.** Vitest's chai layer ships an
2447
+ `@internal` method of that name expecting a _flags object_; handed a string it walks the string's
2448
+ character indices, sets nonsense flags and returns the assertion. The chain runs, the failure
2449
+ reads `AssertionError: expected 2 to be 3`, and the label is gone. Write
2450
+ `expect(actual, 'msg').toBe(expected)`.
2451
+ 3. **`.calls.saveArgumentsByValue()` is a no-op here.** No runner in this family copies call
2452
+ arguments. The call still runs, nothing fails, and a spec that relied on it silently starts
2453
+ asserting on post-mutation state. Take the copy at call time, inside a `mockImplementation`.
2454
+ 4. **`.and.callThrough()` means something different.** Upstream had no original to call through to
2455
+ and silently answered `undefined`; here it restores **this library's own dispatch**, so a
2456
+ `calledWith` chain decides the value again. The codemod leaves it byte-for-byte and names the
2457
+ line, because there is no expression it could become.
2458
+
2459
+ `jasmine.DEFAULT_TIMEOUT_INTERVAL` is a config setting, not a statement: set **both**
2460
+ `testTimeout` (default 5 000 ms) and `hookTimeout` (default 10 000 ms), or
2461
+ `vi.setConfig({ testTimeout: n, hookTimeout: n })` per file. Assigning to it on the namespace warns
2462
+ once naming both rather than silently swallowing the write.
2463
+
2464
+ `it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
2465
+ shape, not a name. Use `async` + `await`; `no-done-callback` reports both the parameter and any
2466
+ `done.fail(…)`.
2467
+
2468
+ On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
2469
+ importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a
2470
+ setup file — spies built **before** the call do not get the namespaces. Observables still come from
2471
+ `vitest-auto-spy/rxjs`. A project that never imports the entry pays one `undefined` check per spy
2472
+ and ships none of the code.
2473
+
2474
+ ### `@hirez_io/observer-spy` comes with it
2475
+
2476
+ That package sits beside `jasmine-auto-spies` in almost every suite that has one, and is the larger
2477
+ of the two by an order of magnitude. `vitest-auto-spy/rxjs` exports the same surface —
2478
+ `subscribeSpyTo(source$, config?)`, `SubscriberSpy<T>`, `ObserverSpy<T>`, `ObserverSpyConfig` — so a
2479
+ migration does not have to rewrite every stream assertion in the same commit:
2480
+
2481
+ ```ts
2482
+ import { subscribeSpyTo } from 'vitest-auto-spy/observer-spy';
2483
+
2484
+ const spy = subscribeSpyTo(service.load());
2485
+
2486
+ expect(spy.getValues()).toEqual(['a', 'b']);
2487
+ expect(spy.receivedComplete()).toBe(true);
2488
+ ```
2489
+
2490
+ Four departures from upstream, each closing a defect: `getValues()` returns a **copy** (upstream
2491
+ hands back its live array) and is typed `T[]` (upstream: `any[]`); `getFirstValue()` /
2492
+ `getValueAt(i)` **throw** on an empty spy rather than returning `undefined` from a `T` signature;
2493
+ and an **unexpected** error is thrown by the value readers, carrying the original as `cause`, rather
2494
+ than rethrown out of the observer — under rxjs 7 that rethrow goes through `reportUnhandledError`
2495
+ and never reaches the subscribing line. `{ expectErrors: true }` keeps the readers open, as upstream.
2496
+
2497
+ `autoUnsubscribe()`, `queueForAutoUnsubscribe()` and `fakeTime()` are **not implemented**. Use
2498
+ `using spy = subscribeSpyTo(source$)` — `SubscriberSpy` is disposable — and `setupFakeTimers()` with
2499
+ `await advanceTimers(ms)`, or rxjs's `TestScheduler` directly.
2500
+
2501
+ **It is a bridge, and the destination differs in kind.** observer-spy is synchronous inspection:
2502
+ subscribe, let things happen, then read. A stream that never emits leaves a spy with no values, so a
2503
+ spec reading `getValues()` gets `[]`, asserts about it, and passes having observed nothing. §8's
2504
+ `expectEmission` / `expectEmissions` invert that — the assertion _is_ the await, and silence is a
2505
+ failure with a watchdog. Land the suite green on `subscribeSpyTo`, then move the assertions over.
2506
+
2507
+ Full mapping, including what upstream cannot do at all:
2508
+ <https://asdalexey.github.io/vitest-auto-spy/migrating-jasmine>.