vitest-auto-spy 5.45.0 → 5.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,84 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.46.0] - 2026-09-27
14
+
15
+ ### Added
16
+
17
+ - **`stubAnimationFrame(...).lastHandle`: the handle the latest `requestAnimationFrame` returned.**
18
+ 5.40.0 moved the stub's handles above 2^30 so a native frame's cancel could be told apart, and a
19
+ spec that asserted `expect(frames.cancelAnimationFrame).toHaveBeenLastCalledWith(1)` broke on the
20
+ upgrade with nothing in that entry saying so. `lastHandle` is `undefined` before the first request,
21
+ so the assertion reads `toHaveBeenLastCalledWith(frames.lastHandle)` and survives any numbering.
22
+ - **`injectHttpTesting()` from `/angular-http`: the `HttpTestingController` `provideHttpTesting()`
23
+ installed.** A spec of a service that returns an Observable has its request out before the method
24
+ returns, so it calls `expectOne` / `match` / `expectNone` synchronously, and `expectRequest` (which
25
+ ticks and flushes asynchronously) is not a drop-in there; such specs kept a
26
+ `TestBed.inject(HttpTestingController)` beside `...provideHttpTesting()`. Without the providers it
27
+ throws the message `expectRequest` gives, naming `provideHttpTesting()`.
28
+ - **`expectRequest(...).error(status, { error })`: the payload the `HttpErrorResponse` carries.** The
29
+ helper always failed the request with `new ProgressEvent('error')`, so a spec that pinned
30
+ `HttpErrorResponse.error` stayed on `HttpTestingController`. The default is unchanged. `error(0)` is
31
+ documented as the network failure it is.
32
+ - **`{ tick: false }` on `expectRequest` and `expectNoRequest`.** Both tick the app first, and a module
33
+ that provides a partial `DOCUMENT` double fails that tick inside Angular
34
+ (`inject(...).body?.querySelector is not a function`). The option skips the tick before the lookup and
35
+ after the answer, for an `HttpClient` call whose request is already out.
36
+
37
+ ### Fixed
38
+
39
+ - **`toHaveSignalValue` and `toHaveResourceValue` compare the contents of a `Set` and a `Map`.** They
40
+ called the matcher context's `equals` without `iterableEquality`, so a collection had no own keys to
41
+ compare and any two Sets, or any two Maps, were equal:
42
+ `expect(signal(new Set(['a']))).toHaveSignalValue(new Set(['b']), { strict: true })` passed, nested
43
+ in an object as well. Both now compare with the testers `toEqual` uses, and `{ strict: true }` with
44
+ those of `toStrictEqual` (array holes inside a collection and `ArrayBuffer` bytes included). The
45
+ non-strict branch also ignored testers registered through `expect.addEqualityTesters`; both modes
46
+ apply them now. An assertion on a Set- or Map-valued signal that passed only because of this can
47
+ newly fail.
48
+ - **A tick that fails on a doubled `DOCUMENT` says so.** `expectRequest` / `expectNoRequest` failed with
49
+ Angular's bare `TypeError`; when the module's `DOCUMENT` is not the real document, the error now names
50
+ it and `{ tick: false }`, with Angular's error as `cause`. A tick failure under the real document is
51
+ rethrown as it was.
52
+ - **`jasmine.mapContaining` / `setContaining` / `arrayWithExactContents` compare a nested `Set` or
53
+ `Map` by its contents**, for the same reason: `new Map([['a', new Set([1])]])` contained
54
+ `new Map([['a', new Set([2])]])`.
55
+
56
+ ### Changed
57
+
58
+ - **`stubAnimationFrame`'s spies are typed `Mock<RequestAnimationFrameFn>` /
59
+ `Mock<CancelAnimationFrameFn>`** instead of `(...args: any[]) => any`, so
60
+ `frames.requestAnimationFrame.mock.calls` compiles and is typed. Both function types are exported
61
+ from `/dom-stubs` and spelled without the DOM lib. A spec that called the spy with arguments
62
+ `requestAnimationFrame` does not take now fails to compile.
63
+
64
+ ### Size
65
+
66
+ `/angular/matchers` **2799 → 2936 B** min+gzip (+137 B, +4.9 %) and +1.0 kB to import (22906 →
67
+ 23887 B, one module more): the shared equality module the signal and resource matchers now use.
68
+ `/jasmine` 19161 → 19203 B (+42 B) and +1.3 kB to import (148816 → 150096 B, one module more): the
69
+ same module, for the collection matchers. `/angular-http` 3770 → 3997 B (+227 B, +6.0 %) and +0.9 kB
70
+ to import (31370 → 32313 B): `injectHttpTesting`, the `{ tick }` option, the doubled-`DOCUMENT`
71
+ message and the `error` payload. `/dom-stubs` +14 B.
72
+
73
+ ### Documentation
74
+
75
+ - **The 5.40.0 handle range is a change a spec may have to act on.** A literal handle (`1`, `2`) in an
76
+ assertion against `frames.cancelAnimationFrame` fails from 5.40.0; assert against `frames.lastHandle`
77
+ or the value `requestAnimationFrame` returned. The frame page, `agent-docs/doubles.md`, the README
78
+ and the skill say so.
79
+ - **`SubjectOf` from `/angular` is still rxjs's `Subject` only with `vitest-auto-spy/rxjs` in the
80
+ program.** `agent-docs/return-helpers.md` and the API page recommended the `/angular` export for a
81
+ variable holding `returnSubject()` without repeating that; without the import it is `SubjectLike`,
82
+ so `next()` compiles and `pipe()` does not. `SubjectLike`'s member list in `agent-docs` gains
83
+ `unsubscribe`.
84
+ - **Asserting the errors `flushUnhandledObservableErrors()` returned, without `.map(({ error }) => error)`:**
85
+ `expect(flushUnhandledObservableErrors()).toMatchObject([{ error: new Error('502') }])` checks the
86
+ count and ignores `test`. The setup page and `agent-docs/setup.md` show it.
87
+ - **"Only these console channels" is `expect(Object.keys(consoleOutput())).toStrictEqual(['info'])`**,
88
+ keys in alphabetical order; `{ info: expect.any(Array) }` trips `no-unsafe-assignment`.
89
+ `agent-docs/setup.md` and the `consoleOutput` docs show it.
90
+
13
91
  ## [5.45.0] - 2026-09-27
14
92
 
15
93
  ### Added
@@ -5922,7 +6000,8 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
5922
6000
  This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
5923
6001
  [full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
5924
6002
 
5925
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...HEAD
6003
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.46.0...HEAD
6004
+ [5.46.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.45.0...v5.46.0
5926
6005
  [5.45.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.44.0...v5.45.0
5927
6006
  [5.44.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.43.0...v5.44.0
5928
6007
  [5.43.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.42.0...v5.43.0
package/README.md CHANGED
@@ -1581,7 +1581,7 @@ Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runti
1581
1581
  | `vitest-auto-spy/angular/diagnostics` | `enableAngularDiagnostics` and the whole TestBed timing family — a companion to `/angular`, no core re-export; moved off `/angular` in 5.21.0 | `@angular/core` | ✅ |
1582
1582
  | `vitest-auto-spy/angular/doubles` | the `window` / `document` and Material-dialog doubles; registers the Vitest adapter, so its doubles spy out of the box; moved off `/angular` in 5.21.0 | `@angular/core` | ✅ |
1583
1583
  | `vitest-auto-spy/angular/matchers` | `registerDirectiveMatchers`, `registerResourceMatchers`, `registerSignalMatchers` — a companion to `/angular`, no core re-export; moved off `/angular` in 5.21.0 | `@angular/core`, `@angular/platform-browser` | ✅ |
1584
- | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1584
+ | `vitest-auto-spy/angular-http` | `provideHttpTesting`, `expectRequest`, `expectNoRequest`, `verifyNoPendingRequests`, `injectHttpTesting` — the `httpResource()` / `HttpClient` recipe in two lines, settling included | `@angular/common` | ✅ |
1585
1585
  | `vitest-auto-spy/angular-router` | `provideActivatedRoute`, `injectActivatedRoute`, `createActivatedRoute` — Angular's own `ActivatedRoute` over one record, streams and snapshot moved together; `provideRouterDouble` / `injectRouterDouble` for the `Router` beside it, with `collectRouterEvents()` asserting the events that follow; `provideLocationDouble` wrapping the `SpyLocation` Angular ships | `@angular/router`, `@angular/common` | ✅ |
1586
1586
  | `vitest-auto-spy/signal-forms` | `createForm` — Angular's own `form()` built where it can inject — and `registerFormMatchers()` for `toHaveFieldErrors` | `@angular/forms` (>=22) | ✅ |
1587
1587
  | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
@@ -3139,7 +3139,9 @@ it('loads the products', async () => {
3139
3139
  `expectRequest` ticks before it looks — an `httpResource()` has issued nothing until something does —
3140
3140
  and `flush()` / `error()` settle before they resolve, which is why the value reads on the next line.
3141
3141
  Match by URL (either `url` or `urlWithParams`), by `RegExp`, or by a predicate over the request, with
3142
- `{ method }` to tell a read from a write. `provideHttpTesting()` fails, by default, any test that
3142
+ `{ method }` to tell a read from a write. `error(0)` is a network failure, and
3143
+ `error(status, { error })` sets the payload the `HttpErrorResponse` carries. `{ tick: false }` skips
3144
+ the tick, for a module whose partial `DOCUMENT` double the tick cannot run against. `provideHttpTesting()` fails, by default, any test that
3143
3145
  ends holding a request nothing answered; `provideHttpTesting({ verifyOnTeardown: false })` turns that
3144
3146
  off, and `verifyNoPendingRequests()` runs the same check by hand. An interceptor under test goes in
3145
3147
  the same spread — `provideHttpTesting({ interceptors: [authInterceptor] })`, or
@@ -3289,7 +3291,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
3289
3291
  | `stubAbortController()` | `/dom-stubs` | A realm-consistent `AbortController`, so `addEventListener(…, { signal })` works under jsdom + zone.js |
3290
3292
  | `stubWebStorage(key?, opts?)` | `/dom-stubs` | An in-memory `localStorage` / `sessionStorage` for this test, with `snapshot()` to assert on ([details](#how-to-mock-localstorage-and-sessionstorage)) |
3291
3293
  | `stubWorker(opts?)` | `/dom-stubs` | A `Worker` whose script is the spec — listeners stack, replies arrive on a microtask, `emit()` / `fail()` from the worker's side ([details](#how-to-mock-a-web-worker)) |
3292
- | `stubAnimationFrame(opts?)` | `/dom-stubs` | `requestAnimationFrame` / `cancelAnimationFrame` run on the spot or on `flush(timestamp?)`; a frame requested inside a frame waits for the next flush; `onError` intercepts a throwing callback |
3294
+ | `stubAnimationFrame(opts?)` | `/dom-stubs` | `requestAnimationFrame` / `cancelAnimationFrame` run on the spot or on `flush(timestamp?)`; a frame requested inside a frame waits for the next flush; `onError` intercepts a throwing callback; `lastHandle` is the latest handle issued (they start above 2^30) |
3293
3295
  | `stubElementRect(el, rect?)` | `/dom-stubs` | `getBoundingClientRect()` answering a real `DOMRect` built from a partial `DOMRectInit`; returns the undo, plus the installed spy as `.getBoundingClientRect` |
3294
3296
  | `flushEventLoop(turns?)` / `settleDynamicImport(load, turns?)` | core | Real event-loop turns while the timers are faked — for a dynamic `import()` or native `async` in a dependency |
3295
3297
  | `flushEventLoopUntil(isDone, opts?)` | core | Real turns until a condition holds — a `resource()` leaving `loading` — with a budget instead of a hang |
@@ -4317,7 +4319,7 @@ another, and the next one is in that same file.
4317
4319
  | `stubAbortController()` | A realm-consistent `AbortController` / `AbortSignal` for jsdom + zone.js, statics (`abort`, `timeout`, `any`) included |
4318
4320
  | `stubWebStorage(key?, opts?)` _(`/dom-stubs`)_ | An in-memory `localStorage` / `sessionStorage` a spec installs for itself, seeded or empty; `snapshot()` reads it back as a plain record |
4319
4321
  | `stubWorker(opts?)` _(`/dom-stubs`)_ | A `Worker` a spec drives from the worker's side: `respond` per message, `messages` / `terminated` to assert, `emit()` / `fail()`; returns `WorkerStub<TIn, TOut>` |
4320
- | `stubAnimationFrame(opts?)` _(`/dom-stubs`)_ | Frames on the spec's schedule: `'immediate'` or `'queued'` + `flush()`, `pending`, both globals as spies; returns `AnimationFrameStub` |
4322
+ | `stubAnimationFrame(opts?)` _(`/dom-stubs`)_ | Frames on the spec's schedule: `'immediate'` or `'queued'` + `flush()`, `pending`, `lastHandle`, both globals as spies; returns `AnimationFrameStub` |
4321
4323
  | `stubElementRect(el, rect?)` _(`/dom-stubs`)_ | A box for `getBoundingClientRect()` — missing fields `0`, edges derived; returns `ElementRectRestore` (callable undo plus `.getBoundingClientRect`) |
4322
4324
  | `createComponentStub(Class, overrides?, opts?)` _(`/angular`)_ | A standalone stand-in for a child component, directive or pipe, built from its compiled definition so the selector cannot drift |
4323
4325
  | `flushEventLoop(turns?)` / `settleDynamicImport(load, turns?)` | Real event-loop turns under fake timers; wait for a dynamic `import()` |
@@ -4339,6 +4341,7 @@ another, and the next one is in that same file.
4339
4341
  | `settleResource(resource, opts?)` _(Angular)_ | Tick until an `httpResource()` / `resource()` / `rxResource()` leaves `loading` |
4340
4342
  | `provideHttpTesting(opts?)` / `expectRequest(matcher, opts?)` _(`/angular-http`)_ | `provideHttpClient(...features)` + `provideHttpClientTesting()` in one spread — `{ interceptors, features }` for the interceptors under test; find the one matching request and `flush` / `error` it with the settling included |
4341
4343
  | `expectNoRequest(matcher?, opts?)` / `verifyNoPendingRequests(opts?)` _(`/angular-http`)_ | Assert that nothing was requested / that the test left nothing unanswered; `{ ignoreCancelled: true }` — also accepted by `provideHttpTesting({ verifyOnTeardown })` — forgives a request the code under test unsubscribed from |
4344
+ | `injectHttpTesting()` _(`/angular-http`)_ | The `HttpTestingController` `provideHttpTesting()` installed, for a synchronous `expectOne` / `match` / `expectNone` on an Observable service; names the missing providers when there are none |
4342
4345
  | `provideActivatedRoute(init?)` / `injectActivatedRoute(injector?)` _(`/angular-router`)_ | An `ActivatedRoute` provider whose streams, `paramMap`s and snapshot share one record, and the handle whose setters move them together |
4343
4346
  | `createActivatedRoute(init?)` _(`/angular-router`)_ | The same double without a `TestBed` — `.route` plus the setters |
4344
4347
  | `provideRouterDouble(init?)` / `injectRouterDouble(injector?)` / `createRouterDouble(init?)` _(`/angular-router`)_ | A `Router` derived from one URL — `navigate` spies, `setUrl`, `emitNavigation`, `setCurrentNavigation`, and the router's own URL serialization |
@@ -478,6 +478,7 @@ resource's **default** value and passes.
478
478
  | `provideHttpTesting(opts?)` | `provideHttpClient(...features)` + `provideHttpClientTesting()`; `{ verifyOnTeardown }` defaults to `true`, and takes `{ ignoreCancelled }` instead of `false`; `{ interceptors: [fn] }` for functional interceptors under test, `{ features: [...] }` for any other `provideHttpClient()` feature |
479
479
  | `expectRequest(matcher, opts?)` | tick, then the one match — `.request`, `.flush(body, opts?)`, `.error(status, opts?)` |
480
480
  | `expectNoRequest(matcher?, opts?)` | tick, then assert nothing matched; no argument means "nothing at all was requested" |
481
+ | `injectHttpTesting()` | the `HttpTestingController` itself, for a synchronous `expectOne` / `match` / `expectNone` on an Observable service — no tick needed; throws the same missing-providers message |
481
482
  | `verifyNoPendingRequests(opts?)` | the teardown check by hand, for mid-test use or a suite with `verifyOnTeardown: false`; `{ ignoreCancelled: true }` forgives a request the code under test unsubscribed from |
482
483
 
483
484
  An interceptor under test goes into the same call, and the teardown check stays armed — do not
@@ -506,6 +507,10 @@ provided after it, so it wins over `withXhr()`.
506
507
 
507
508
  `matcher` is a URL (matched against either `url` or `urlWithParams`), a `RegExp` over
508
509
  `urlWithParams`, or a predicate `(request) => boolean`. `{ method }` is case-insensitive.
510
+ `.error(0)` is a network failure; `.error(status, { error })` sets the payload `HttpErrorResponse.error`
511
+ carries (default `new ProgressEvent('error')`). A module that provides a partial `DOCUMENT` double
512
+ fails Angular's tick (`inject(...).body?.querySelector is not a function`, reported as a doubled
513
+ `DOCUMENT`): pass `{ tick: false }` to `expectRequest` / `expectNoRequest` when the request is already out.
509
514
  `flush()` and `error()` are `async` because settling requires letting a microtask run — `await` them,
510
515
  and the next line reads the settled value.
511
516
 
@@ -156,7 +156,10 @@ running frame is queued in either mode, so a loop that requests its own next fra
156
156
  per `flush()`; `frames.flushAll()` runs such a chain until nothing is pending (and throws after 1000
157
157
  rounds, on a loop that never stops). A `cancelAnimationFrame` for a handle the stub did not issue — a
158
158
  native frame requested before it was installed, such as zoneless Angular's scheduler frame — is
159
- passed on to the real `cancelAnimationFrame`, so installing the stub after the render leaks no timer. A callback that throws stops `flush()` (or propagates out of `requestAnimationFrame`
159
+ passed on to the real `cancelAnimationFrame`, so installing the stub after the render leaks no timer.
160
+ The stub's own handles start above 2^30 (since 5.40.0), so assert a cancel against the handle it
161
+ issued — `expect(frames.cancelAnimationFrame).toHaveBeenLastCalledWith(frames.lastHandle)`, or the
162
+ value `requestAnimationFrame` returned — never against a literal such as `1`. A callback that throws stops `flush()` (or propagates out of `requestAnimationFrame`
160
163
  in `'immediate'` mode); pass `onError: (error) => { … }` to intercept it instead — rethrow from
161
164
  inside to keep the default for an error you did not mean to swallow. `stubElementRect` returns a real
162
165
  `DOMRect` per call, edges derived, plus the installed spy as `.getBoundingClientRect` for
@@ -168,6 +168,7 @@ Part of the agent reference [`AGENTS.md`](../AGENTS.md), which maps every sectio
168
168
  | `expectRequest: no GET …?page=2 — but GET …?page=3 was made; the query differs` | the same path, another query string | name the query the code sends, or match the path alone: `expectRequest('/api/products')` (§13) |
169
169
  | `expectRequest: no request matched … — nothing was requested at all` | an `httpResource()` whose `request()` returned no URL yet, or an `HttpClient` call nothing subscribed to | set the signal the resource reads, or subscribe to the call (§13) |
170
170
  | `this TestBed has no HttpTestingController` | `expectRequest` / `expectNoRequest` on a module that never got the testing backend | `TestBed.configureTestingModule({ providers: [...provideHttpTesting()] })` from `vitest-auto-spy/angular-http` (§13) |
171
+ | `TestBed.tick() failed, and this TestBed provides its own DOCUMENT` (cause: `inject(...).body?.querySelector is not a function`) | `expectRequest` / `expectNoRequest` ticked the app, and Angular's tick reads members a partial `DOCUMENT` double lacks | `expectRequest(url, { tick: false })` when the request is already out (an `HttpClient` call), or provide a real document |
171
172
  | `X was never answered (end of "…")` / `N requests were never answered` | `provideHttpTesting({ verifyOnTeardown })` or `enableAngularDiagnostics({ pendingRequests })` — the test left a request open; the message names its URL and the test | answer it with the line the message prints (`await expectRequest(url).flush(body)`, `controller.expectOne(url).flush(body)` under diagnostics in a module built on `provideHttpClientTesting()`), or assert its absence (`expectNoRequest(url)`); a cancelled one is forgiven with the `ignoreCancelled` opt-in the message names (§13) |
172
173
  | `injectActivatedRoute(): nothing provides ActivatedRoute here` | no `ActivatedRoute` provider in the injector it read | `provideActivatedRoute({ … })` from `vitest-auto-spy/angular-router` in `providers`; `injectActivatedRoute(fixture.debugElement.injector)` for a component-level one (§13) |
173
174
  | `the ActivatedRoute here is … not one provideActivatedRoute() built` | a later provider of `ActivatedRoute` won — `provideRouter()`, `RouterModule`, a `useValue`, `provideAutoSpy(ActivatedRoute)` | list `provideActivatedRoute()` last, or drop the other provider (§13) |
@@ -29,7 +29,7 @@ produce `nextWith is not a function` with nothing pointing at the duplicate. `Pr
29
29
 
30
30
  `returnSubject()` and `nextWithPerCall()` return `SubjectOf<T>` — rxjs's own `Subject<T>` wherever
31
31
  `import 'vitest-auto-spy/rxjs'` is in the TypeScript program, the structural `SubjectLike<T>`
32
- (`next` / `error` / `complete` / `asObservable` / `closed`) where it is not. The switch is one
32
+ (`next` / `error` / `complete` / `unsubscribe` / `asObservable` / `closed`) where it is not. The switch is one
33
33
  augmentable interface, `AutoSpyRxjsTypes<T>`, which `/rxjs` fills in with `subject: Subject<T>`;
34
34
  augment it yourself only to plug in a different subject type. If
35
35
  `const s: Subject<T> = spy.m.returnSubject()` fails to compile, the import is missing from the
@@ -48,6 +48,10 @@ the Subject it returns is dropped. Keep it when the spec pushes through it later
48
48
  `void spy.items$.returnSubject();` when it never does — the rule's default `ignoreVoid: true` accepts
49
49
  that.
50
50
 
51
+ `SubjectOf` is rxjs's `Subject` only where `import 'vitest-auto-spy/rxjs'` is in the program — the
52
+ `/angular` export does not add it. Without it the variable is `SubjectLike`: `next` / `error` /
53
+ `complete` compile, `pipe()` or an argument typed `Subject<T>` does not.
54
+
51
55
  ```ts
52
56
  // argument dispatch — other arguments return undefined
53
57
  users.getName.calledWith(1).mockReturnValue('Ada');
@@ -92,7 +92,9 @@ error as `cause`; one scheduled outside a test fails the file at the sweep. The
92
92
  it is still installed (`globalFakeTimers` is); a rethrow `config.onUnhandledError` takes reports
93
93
  nothing. Fix it where the stream is subscribed — an error callback or `catchError` — or assert the
94
94
  error. `flushUnhandledObservableErrors()` is the same check by hand: it returns `{ error, test }` (or
95
- `{ error, outsideTest }`) for each rethrow that threw.
95
+ `{ error, outsideTest }`) for each rethrow that threw; assert the errors alone with
96
+ `expect(flushUnhandledObservableErrors()).toMatchObject([{ error: new Error('502') }])` — it checks the
97
+ count and ignores `test`, with no `.map(({ error }) => error)`.
96
98
 
97
99
  **`countStrayTimers()` cannot see a timer scheduled under fake timers, and that does not compose
98
100
  away.** `vi.useFakeTimers()` assigns its own `setTimeout` over the tracking wrapper, so everything
@@ -768,6 +770,8 @@ Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/setup#_16-console-ou
768
770
  - **Pin everything a test logged in one assertion:** `consoleOutput()` from `/console` returns
769
771
  `{ error?, warn?, info?, log?, debug?, trace? }`, each the argument lists of its calls, only the
770
772
  channels written to — `expect(consoleOutput()).toStrictEqual({ info: [['done']] })`; `{}` is silence.
773
+ "Only these channels, whatever they said" is `expect(Object.keys(consoleOutput())).toStrictEqual(['info'])`
774
+ (keys in alphabetical order), which `no-unsafe-assignment` accepts where `{ info: expect.any(Array) }` does not.
771
775
  It throws while no spy is on `console`, so it never reports silence it did not hear.
772
776
  - The wrapper forwards every call unchanged, so Vitest's attribution and `onConsoleLog` still work.
773
777
  `guardStrayConsole(reaction)` registers the same guard on its own.
@@ -1,45 +1,18 @@
1
1
  import { HttpInterceptorFn, HttpFeature, HttpFeatureKind, HttpRequest } from '@angular/common/http';
2
- import { TestRequest } from '@angular/common/http/testing';
2
+ import { TestRequest, HttpTestingController } from '@angular/common/http/testing';
3
3
  import { EnvironmentProviders, Provider } from '@angular/core';
4
4
 
5
- /**
6
- * `provideHttpTesting()` + `expectRequest()` — the `httpResource()` and `HttpClient` dance in two
7
- * lines.
8
- *
9
- * Answering one request from a zoneless spec takes six steps today, and the order of them is not
10
- * guessable: tick (an `httpResource()` issues *nothing* until something does), inject the
11
- * `HttpTestingController`, `expectOne`, `flush`, let one microtask run so the response reaches the
12
- * resource, tick again so what read it is up to date. Miss the first tick and `expectOne` finds no
13
- * request; miss the microtask and the assertion reads the resource's *default* value — a green test
14
- * that asserts nothing until the day the default changes.
15
- *
16
- * ```ts
17
- * TestBed.configureTestingModule({ providers: [...provideHttpTesting()] });
18
- *
19
- * const products = TestBed.runInInjectionContext(() => httpResource<Product[]>(() => '/api/products'));
20
- *
21
- * await expectRequest('/api/products').flush([product]);
22
- *
23
- * expect(products.value()).toEqual([product]); // no tick, no microtask, no detectChanges
24
- * ```
25
- *
26
- * **This module is why `vitest-auto-spy/angular-http` is its own entry.** It is the only file in
27
- * the package that imports `@angular/common`, and `vitest-auto-spy/angular` has to keep loading in
28
- * a project that has `@angular/core` and not `@angular/common` — the same invariant that keeps
29
- * rxjs behind `vitest-auto-spy/rxjs`. The optional peer is paid for by the suites that import this
30
- * entry and by nobody else.
31
- *
32
- * It cooperates with `enableAngularDiagnostics({ pendingRequests })` rather than competing: both
33
- * take the open requests with `match(() => true)`, which is one-shot, so whichever looks first
34
- * owns them and one unanswered request is never reported twice.
35
- */
36
-
37
5
  /** How a request is named: a URL, a pattern, or a question asked of the request itself. */
38
6
  type RequestMatcher = RegExp | string | ((request: HttpRequest<unknown>) => boolean);
39
7
  /** Options for {@link expectRequest} and {@link expectNoRequest}. */
40
8
  interface ExpectRequestOptions {
41
9
  /** Narrow to one verb, for a URL that is both read and written in the same test. Case-insensitive. */
42
10
  method?: string;
11
+ /**
12
+ * `false` skips the `TestBed.tick()` before the lookup and after the answer, for a module whose
13
+ * doubled `DOCUMENT` the app's tick cannot run against. The request must already be out. Default `true`.
14
+ */
15
+ tick?: boolean;
43
16
  }
44
17
  /** Options for {@link provideHttpTesting}. */
45
18
  interface HttpTestingOptions {
@@ -73,8 +46,13 @@ interface HttpTestingOptions {
73
46
  type ResponseBody = Parameters<TestRequest['flush']>[0];
74
47
  /** `headers` / `status` / `statusText`, exactly as `TestRequest#flush` takes them. */
75
48
  type FlushOptions = NonNullable<Parameters<TestRequest['flush']>[1]>;
76
- /** `headers` / `statusText` for {@link RequestExpectation.error} — the status is its first argument. */
77
- type RequestErrorOptions = Omit<NonNullable<Parameters<TestRequest['error']>[1]>, 'status'>;
49
+ /**
50
+ * `headers` / `statusText` for {@link RequestExpectation.error} — the status is its first argument —
51
+ * and `error`, the payload `HttpErrorResponse.error` carries. Default `new ProgressEvent('error')`.
52
+ */
53
+ type RequestErrorOptions = Omit<NonNullable<Parameters<TestRequest['error']>[1]>, 'status'> & {
54
+ error?: Parameters<TestRequest['error']>[0];
55
+ };
78
56
  /** The one request {@link expectRequest} found, and the two ways to answer it. */
79
57
  interface RequestExpectation {
80
58
  /** The request as the code under test sent it — URL, method, headers, body. */
@@ -84,7 +62,10 @@ interface RequestExpectation {
84
62
  * reads it has been ticked.
85
63
  */
86
64
  flush(body: ResponseBody, options?: FlushOptions): Promise<void>;
87
- /** Fail the request with an HTTP status, then settle the same way {@link RequestExpectation.flush} does. */
65
+ /**
66
+ * Fail the request with an HTTP status, then settle the same way {@link RequestExpectation.flush} does.
67
+ * Status `0` is a network failure: no response reached the client.
68
+ */
88
69
  error(status: number, options?: RequestErrorOptions): Promise<void>;
89
70
  }
90
71
  /**
@@ -148,5 +129,10 @@ declare function expectNoRequest(matcher?: RequestMatcher, options?: ExpectReque
148
129
  declare function verifyNoPendingRequests(options?: {
149
130
  ignoreCancelled?: boolean;
150
131
  }): void;
132
+ /**
133
+ * The `HttpTestingController` `provideHttpTesting()` installed, for a synchronous `expectOne` / `match` /
134
+ * `expectNone` on an Observable service. Throws with the missing providers named when there is none.
135
+ */
136
+ declare function injectHttpTesting(): HttpTestingController;
151
137
 
152
- export { type ExpectRequestOptions, type FlushOptions, type HttpTestingOptions, type RequestErrorOptions, type RequestExpectation, type RequestMatcher, type ResponseBody, expectNoRequest, expectRequest, provideHttpTesting, verifyNoPendingRequests };
138
+ export { type ExpectRequestOptions, type FlushOptions, type HttpTestingOptions, type RequestErrorOptions, type RequestExpectation, type RequestMatcher, type ResponseBody, expectNoRequest, expectRequest, injectHttpTesting, provideHttpTesting, verifyNoPendingRequests };
@@ -5,6 +5,7 @@ import './chunk-HVNKTPKG.js';
5
5
  import { count, taskName } from './chunk-DYAS4OK4.js';
6
6
  import { angularHttpNoRequest, angularHttpPending, angularHttpController, angularHttpExpectRequest } from './chunk-5FQZGJLJ.js';
7
7
  import { withDocs } from './chunk-ENCEEH23.js';
8
+ import { DOCUMENT } from '@angular/common';
8
9
  import { withInterceptors, provideHttpClient } from '@angular/common/http';
9
10
  import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
10
11
  import { InjectionToken, provideEnvironmentInitializer } from '@angular/core';
@@ -166,15 +167,37 @@ Narrow it: \`expectRequest(url, { method: "POST" })\`, name the full \`urlWithPa
166
167
  )
167
168
  );
168
169
  }
169
- async function settle() {
170
+ async function settle(tick) {
170
171
  for (let round = 0; round < SETTLE_ROUNDS; round += 1) {
171
172
  await Promise.resolve();
173
+ if (tick) {
174
+ tickApp("expectRequest");
175
+ }
176
+ }
177
+ }
178
+ function tickApp(caller) {
179
+ try {
172
180
  flushEffects();
181
+ } catch (error) {
182
+ if (TestBed.inject(DOCUMENT) === globalThis.document) {
183
+ throw error;
184
+ }
185
+ throw new Error(
186
+ withDocs(
187
+ `[vitest-auto-spy] ${caller}: TestBed.tick() failed, and this TestBed provides its own DOCUMENT \u2014 Angular reads the real document's members while it ticks, which a partial double does not have.
188
+ Pass { tick: false } to answer without ticking (the request must already be out), or provide a DOCUMENT that is a real document.`,
189
+ angularHttpExpectRequest
190
+ ),
191
+ { cause: error }
192
+ );
173
193
  }
174
194
  }
175
195
  function expectRequest(matcher, options = {}) {
176
196
  const controller = readController("expectRequest");
177
- flushEffects();
197
+ const tick = options.tick ?? true;
198
+ if (tick) {
199
+ tickApp("expectRequest");
200
+ }
178
201
  const [first, ...rest] = controller.match(toPredicate(matcher, options));
179
202
  if (first === void 0) {
180
203
  throw noMatch(controller, matcher, options);
@@ -186,17 +209,19 @@ function expectRequest(matcher, options = {}) {
186
209
  request: first.request,
187
210
  flush: async (body, flushOptions) => {
188
211
  first.flush(body, flushOptions);
189
- await settle();
212
+ await settle(tick);
190
213
  },
191
- error: async (status, errorOptions) => {
192
- first.error(new ProgressEvent("error"), { ...errorOptions, status });
193
- await settle();
214
+ error: async (status, { error = new ProgressEvent("error"), ...errorOptions } = {}) => {
215
+ first.error(error, { ...errorOptions, status });
216
+ await settle(tick);
194
217
  }
195
218
  };
196
219
  }
197
220
  function expectNoRequest(matcher = () => true, options = {}) {
198
221
  const controller = readController("expectNoRequest");
199
- flushEffects();
222
+ if (options.tick ?? true) {
223
+ tickApp("expectNoRequest");
224
+ }
200
225
  const matched = controller.match(toPredicate(matcher, options));
201
226
  if (matched.length === 0) {
202
227
  return;
@@ -212,6 +237,9 @@ Something the test did asked for data it was asserted not to need \u2014 a resou
212
237
  function verifyNoPendingRequests(options = {}) {
213
238
  verifyPending(options.ignoreCancelled === true);
214
239
  }
240
+ function injectHttpTesting() {
241
+ return readController("injectHttpTesting");
242
+ }
215
243
  function pendingOf(request) {
216
244
  return { method: request.request.method, urlWithParams: request.request.urlWithParams, cancelled: request.cancelled };
217
245
  }
@@ -228,4 +256,4 @@ function verifyPending(ignoreCancelled, test) {
228
256
  }
229
257
  }
230
258
 
231
- export { expectNoRequest, expectRequest, provideHttpTesting, verifyNoPendingRequests };
259
+ export { expectNoRequest, expectRequest, injectHttpTesting, provideHttpTesting, verifyNoPendingRequests };
@@ -1,3 +1,4 @@
1
+ import { strictEquals, looseEquals } from './chunk-B6JUBFBA.js';
1
2
  import { sourceClassName, count } from './chunk-DYAS4OK4.js';
2
3
  import { angularDirectiveApplied } from './chunk-5FQZGJLJ.js';
3
4
  import { withDocs } from './chunk-ENCEEH23.js';
@@ -205,7 +206,7 @@ function toHaveResourceValue(received, expected) {
205
206
  };
206
207
  }
207
208
  const actual = received.value();
208
- const pass = this.equals(actual, expected);
209
+ const pass = looseEquals(this, actual, expected);
209
210
  return {
210
211
  pass,
211
212
  actual,
@@ -243,12 +244,6 @@ function registerResourceMatchers() {
243
244
  function isSpy(received) {
244
245
  return "mock" in received || "calls" in received;
245
246
  }
246
- function sameClass(a, b) {
247
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) && Array.isArray(b)) {
248
- return void 0;
249
- }
250
- return Object.getPrototypeOf(a) === Object.getPrototypeOf(b) ? void 0 : false;
251
- }
252
247
  function registerSignalMatchers() {
253
248
  expect.extend({
254
249
  toHaveSignalValue(received, expected, { strict = false } = {}) {
@@ -261,7 +256,7 @@ function registerSignalMatchers() {
261
256
  );
262
257
  }
263
258
  const actual = received();
264
- const pass = strict ? this.equals(actual, expected, [...this.customTesters, sameClass], true) : this.equals(actual, expected);
259
+ const pass = strict ? strictEquals(this, actual, expected) : looseEquals(this, actual, expected);
265
260
  return {
266
261
  pass,
267
262
  actual,
@@ -0,0 +1,25 @@
1
+ // src/lib/matcher-equality.ts
2
+ function sameClass(a, b) {
3
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) && Array.isArray(b)) {
4
+ return void 0;
5
+ }
6
+ return Object.getPrototypeOf(a) === Object.getPrototypeOf(b) ? void 0 : false;
7
+ }
8
+ function arrayBufferEquality(a, b) {
9
+ if (!(a instanceof ArrayBuffer) || !(b instanceof ArrayBuffer)) {
10
+ return void 0;
11
+ }
12
+ const left = new Uint8Array(a);
13
+ const right = new Uint8Array(b);
14
+ return left.length === right.length && left.every((byte, index) => byte === right[index]);
15
+ }
16
+ function looseEquals(context, actual, expected) {
17
+ return context.equals(actual, expected, [...context.customTesters, context.utils.iterableEquality]);
18
+ }
19
+ function strictEquals(context, actual, expected) {
20
+ const testers = [...context.customTesters, context.utils.iterableEquality, sameClass, arrayBufferEquality];
21
+ const sparseArrayEquality = (a, b) => Array.isArray(a) && Array.isArray(b) ? context.equals(a, b, testers, true) && context.equals(Object.keys(a), Object.keys(b)) : void 0;
22
+ return context.equals(actual, expected, [...testers, sparseArrayEquality], true);
23
+ }
24
+
25
+ export { looseEquals, strictEquals };
package/dist/cli.js CHANGED
@@ -3865,7 +3865,7 @@ function checkForeignPragma(graph) {
3865
3865
  }
3866
3866
 
3867
3867
  // src/cli/checks/export-map.generated.ts
3868
- var EXPORT_MAP_VERSION = "5.44.0";
3868
+ var EXPORT_MAP_VERSION = "5.45.0";
3869
3869
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular/diagnostics vitest-auto-spy/angular/doubles vitest-auto-spy/angular/matchers vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin vitest-auto-spy/perf-reporter";
3870
3870
  var EXPORTED_BY = {
3871
3871
  AccessorImplementations: "0 1 2 3 4 12 20 21 22",
@@ -3925,6 +3925,7 @@ var EXPORTED_BY = {
3925
3925
  BlockNetworkOptions: "23",
3926
3926
  CallbackSubscribable: "0 1 2 3 4 12 20 21 22",
3927
3927
  CallLog: "0 1 2 3 4 20 21 22",
3928
+ CancelAnimationFrameFn: "7",
3928
3929
  cancelStrayTimers: "23",
3929
3930
  captureArg: "0 1 2 3 4 20 21 22",
3930
3931
  CaptureArgOptions: "0 1 2 3 4 20 21 22",
@@ -4058,6 +4059,7 @@ var EXPORTED_BY = {
4058
4059
  hostElement: "2 12",
4059
4060
  HttpTestingOptions: "16",
4060
4061
  injectActivatedRoute: "17",
4062
+ injectHttpTesting: "16",
4061
4063
  InjectionLog: "12 19",
4062
4064
  injectLocationDouble: "17",
4063
4065
  injectMatDialogRef: "14",
@@ -4182,6 +4184,7 @@ var EXPORTED_BY = {
4182
4184
  RenderShallowOptions: "2 12",
4183
4185
  reportPropsOutsideHooks: "0 1 2 3 4 20 21 22",
4184
4186
  reportSpecTiming: "13",
4187
+ RequestAnimationFrameFn: "7",
4185
4188
  RequestErrorOptions: "16",
4186
4189
  RequestExpectation: "16",
4187
4190
  RequestMatcher: "16",
package/dist/console.d.ts CHANGED
@@ -50,7 +50,7 @@ declare global {
50
50
  declare function installConsoleSpies(): ConsoleSpies;
51
51
  /** A console method that writes, as `consoleOutput()` keys it. */
52
52
  type ConsoleChannel = 'debug' | 'error' | 'info' | 'log' | 'trace' | 'warn';
53
- /** What a test wrote, per channel: the arguments of each call. A channel nothing wrote to is absent. */
53
+ /** What a test wrote, per channel: the arguments of each call. A channel nothing wrote to is absent; keys come in alphabetical order. */
54
54
  type ConsoleOutput = Partial<Record<ConsoleChannel, unknown[][]>>;
55
55
  /**
56
56
  * Everything the console spies recorded, as one value. Only the channels written to appear, so an
@@ -61,6 +61,7 @@ type ConsoleOutput = Partial<Record<ConsoleChannel, unknown[][]>>;
61
61
  * cli.run(['--dry-run']);
62
62
  * expect(consoleOutput()).toStrictEqual({ info: [['done']] });
63
63
  * expect(consoleOutput()).toStrictEqual({}); // wrote nothing at all
64
+ * expect(Object.keys(consoleOutput())).toStrictEqual(['info']); // only info, whatever it said
64
65
  * ```
65
66
  */
66
67
  declare function consoleOutput(): ConsoleOutput;
@@ -1,7 +1,7 @@
1
1
  import { F as Func } from './types-Dxw-oWfr.js';
2
2
  export { W as WebStorageKey, a as WebStorageStub, b as WebStorageStubOptions, s as stubWebStorage } from './web-storage-dwQ9E7Lm.js';
3
+ import { Mock } from 'vitest';
3
4
  import { R as RestoreProp } from './prop-mock-WOpRQPUc.js';
4
- import 'vitest';
5
5
 
6
6
  /**
7
7
  * A host-runner mock function: a callable spy (Vitest `vi.fn()`, Bun `mock()`,
@@ -196,14 +196,23 @@ interface AnimationFrameStubOptions {
196
196
  */
197
197
  onError?: (error: unknown) => void;
198
198
  }
199
+ /** `requestAnimationFrame` as the stub installs it, spelled without the DOM lib. */
200
+ type RequestAnimationFrameFn = (callback: (timestamp: number) => void) => number;
201
+ /** `cancelAnimationFrame` as the stub installs it. */
202
+ type CancelAnimationFrameFn = (handle: number) => void;
199
203
  /** The handle {@link stubAnimationFrame} returns. */
200
204
  interface AnimationFrameStub {
201
205
  /** How many requested frames have not run yet. */
202
206
  readonly pending: number;
207
+ /**
208
+ * The handle the latest `requestAnimationFrame` call returned, or `undefined` before the first one.
209
+ * Handles start above 2^30, so assert against this rather than a literal such as `1`.
210
+ */
211
+ readonly lastHandle: number | undefined;
203
212
  /** The spy installed as `requestAnimationFrame`. */
204
- readonly requestAnimationFrame: MockFn;
213
+ readonly requestAnimationFrame: Mock<RequestAnimationFrameFn>;
205
214
  /** The spy installed as `cancelAnimationFrame`. */
206
- readonly cancelAnimationFrame: MockFn;
215
+ readonly cancelAnimationFrame: Mock<CancelAnimationFrameFn>;
207
216
  /**
208
217
  * Run every frame requested so far, as one browser frame. A frame requested from inside one of
209
218
  * them waits for the next `flush()`, as it waits for the next frame in a browser.
@@ -458,4 +467,4 @@ interface ResizeEntryRect {
458
467
  */
459
468
  declare function resizeEntry(target: Element, rect?: ResizeEntryRect): ResizeObserverEntry;
460
469
 
461
- export { type AnimationFrameMode, type AnimationFrameStub, type AnimationFrameStubOptions, type ElementRectRestore, type IntersectionEntryOverrides, type IntersectionObserverStubOptions, type MediaElementState, type MediaElementStub, type MediaElementStubOptions, type MutationRecordInit, type ObserverGlobal, type ObserverInstance, type ObserverStub, type ObserverStubOptions, type ResizeEntryRect, type WorkerInstance, type WorkerScript, type WorkerStub, type WorkerStubOptions, intersectionEntry, mutationRecord, resizeEntry, stubAbortController, stubAnimationFrame, stubElementRect, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, stubWorker };
470
+ export { type AnimationFrameMode, type AnimationFrameStub, type AnimationFrameStubOptions, type CancelAnimationFrameFn, type ElementRectRestore, type IntersectionEntryOverrides, type IntersectionObserverStubOptions, type MediaElementState, type MediaElementStub, type MediaElementStubOptions, type MutationRecordInit, type ObserverGlobal, type ObserverInstance, type ObserverStub, type ObserverStubOptions, type RequestAnimationFrameFn, type ResizeEntryRect, type WorkerInstance, type WorkerScript, type WorkerStub, type WorkerStubOptions, intersectionEntry, mutationRecord, resizeEntry, stubAbortController, stubAnimationFrame, stubElementRect, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, stubWorker };
package/dist/dom-stubs.js CHANGED
@@ -1279,18 +1279,22 @@ function stubAnimationFrame(options = {}) {
1279
1279
  get pending() {
1280
1280
  return queue.size;
1281
1281
  },
1282
- requestAnimationFrame: request,
1283
- cancelAnimationFrame: cancel,
1284
- flush,
1285
- flushAll: (timestamp) => {
1286
- flushUntilIdle(queue, () => flush(timestamp));
1282
+ get lastHandle() {
1283
+ return lastId === FIRST_HANDLE ? void 0 : lastId;
1287
1284
  },
1285
+ requestAnimationFrame: asRunnerMock2(request),
1286
+ cancelAnimationFrame: asRunnerMock2(cancel),
1287
+ flush,
1288
+ flushAll: (timestamp) => flushUntilIdle(queue, () => flush(timestamp)),
1288
1289
  restore() {
1289
1290
  [...restores].reverse().forEach((undo) => undo());
1290
1291
  queue.clear();
1291
1292
  }
1292
1293
  };
1293
1294
  }
1295
+ function asRunnerMock2(mock) {
1296
+ return mock;
1297
+ }
1294
1298
  function runQueued(queue, run) {
1295
1299
  for (const handle of [...queue.keys()]) {
1296
1300
  const callback = queue.get(handle);
package/dist/jasmine.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { enableJasmineCompat } from './chunk-3ZBRBPOL.js';
2
2
  export { enableJasmineCompat } from './chunk-3ZBRBPOL.js';
3
+ import { looseEquals } from './chunk-B6JUBFBA.js';
3
4
  import { createSpyFromClass } from './chunk-I6CB4CNT.js';
4
5
  import { useVitestAdapter } from './chunk-IB72SVKT.js';
5
6
  import './chunk-QGLUX55Q.js';
@@ -174,7 +175,7 @@ function registerValueMatchers() {
174
175
  function registerCollectionMatchers() {
175
176
  expect.extend({
176
177
  jasmineMapContaining(received, sample) {
177
- const missing = received instanceof Map ? mapMissing(received, sample, this.equals.bind(this)) : [];
178
+ const missing = received instanceof Map ? mapMissing(received, sample, (a, b) => looseEquals(this, a, b)) : [];
178
179
  const pass = received instanceof Map && missing.length === 0;
179
180
  return {
180
181
  pass,
@@ -182,7 +183,7 @@ function registerCollectionMatchers() {
182
183
  };
183
184
  },
184
185
  jasmineSetContaining(received, sample) {
185
- const missing = received instanceof Set ? setMissing(received, sample, this.equals.bind(this)) : [];
186
+ const missing = received instanceof Set ? setMissing(received, sample, (a, b) => looseEquals(this, a, b)) : [];
186
187
  const pass = received instanceof Set && missing.length === 0;
187
188
  return {
188
189
  pass,
@@ -190,7 +191,7 @@ function registerCollectionMatchers() {
190
191
  };
191
192
  },
192
193
  jasmineArrayWithExactContents(received, sample) {
193
- const diff = Array.isArray(received) ? exactContentsDiff(received, sample, this.equals.bind(this)) : { missing: [], extra: [] };
194
+ const diff = Array.isArray(received) ? exactContentsDiff(received, sample, (a, b) => looseEquals(this, a, b)) : { missing: [], extra: [] };
194
195
  const pass = Array.isArray(received) && diff.missing.length === 0 && diff.extra.length === 0;
195
196
  return {
196
197
  pass,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.45.0",
3
+ "version": "5.46.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",
@@ -187,7 +187,7 @@ it('loads', async () => {
187
187
  | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
188
188
  | a `signal()` / `computed()` field on the class under test | `mockSignalProp(obj, prop, initial)` — writes through a writable one, `asReadonly()` included; an `input()` is refused |
189
189
  | a resource field, when the HTTP round trip is not the point | `mockResourceProp(obj, prop, initial)` — `set` / `fail` / `loading`; `value()` after `fail()` throws |
190
- | the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included |
190
+ | the HTTP round trip _is_ the point | `expectRequest(url).flush(body)` — `/angular-http`, settling included; a synchronous `expectOne` on an Observable service: `injectHttpTesting()` |
191
191
  | an interceptor under test: `provideHttpClient(withInterceptors([...]))` + `provideHttpClientTesting()` | `provideHttpTesting({ interceptors: [fn], features: [withInterceptorsFromDi()] })` — keeps the end-of-test check |
192
192
  | a component or service that reads `ActivatedRoute` | `provideActivatedRoute({ params })`, then `injectActivatedRoute().setParams(…)` — `/angular-router` |
193
193
  | a guard or a class built with `new` that takes the route | `createActivatedRoute({ params })` — the same handle, no `TestBed` |
@@ -296,7 +296,7 @@ it('loads', async () => {
296
296
  | a hand-written `class MockChartComponent` restating a child's selector | `createComponentStub(ChartComponent)` (`/angular`) — selector, inputs, outputs read from `ɵcmp` |
297
297
  | a `TestingStorage` class for `localStorage` / `sessionStorage` | `stubWebStorage('localStorage', { items })` from `/dom-stubs` — `snapshot()` to assert |
298
298
  | a hand-written `MockWorker` whose `addEventListener` sets `onmessage` | `stubWorker({ respond })` from `/dom-stubs` — `last.emit(data)`, `last.messages` |
299
- | `window.requestAnimationFrame = (cb) => { cb(0); return 0; }`, or an array of callbacks looped over | `stubAnimationFrame()` / `stubAnimationFrame({ mode: 'queued' })` from `/dom-stubs` — `flush()`, `flushAll()` for nested frames, `pending`; a foreign handle's cancel reaches the real one |
299
+ | `window.requestAnimationFrame = (cb) => { cb(0); return 0; }`, or an array of callbacks looped over | `stubAnimationFrame()` / `stubAnimationFrame({ mode: 'queued' })` from `/dom-stubs` — `flush()`, `flushAll()` for nested frames, `pending`; a foreign handle's cancel reaches the real one; assert a cancel with `frames.lastHandle`, not `1` |
300
300
  | `mockValueProp(el, 'getBoundingClientRect', () => new DOMRect(…))`, or a `({ height })` literal | `stubElementRect(el, { width, height })` from `/dom-stubs` — missing fields are `0`, edges derived |
301
301
  | `'x' does not exist in type 'MethodReturns<{ y: any; }>'` on a generic class | spell out the type argument — `createSpyFromClass<Config>(Config, …)`; `provideAutoSpy` infers it |
302
302
  | `injectSpy(ModalRef)` reads a generic class at its constraint, not its default | `injectSpy<ModalRef<Data>>(ModalRef)` — a constructor taking `T` hides the default |