vitest-auto-spy 3.6.0 → 3.7.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.

Potentially problematic release.


This version of vitest-auto-spy might be problematic. Click here for more details.

package/AGENTS.md CHANGED
@@ -1067,6 +1067,7 @@ mechanisms, and a test that waits on the wrong one fails with a message that nam
1067
1067
  | effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
1068
1068
  | timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
1069
1069
  | a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
1070
+ | an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
1070
1071
 
1071
1072
  ```ts
1072
1073
  import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
@@ -1088,9 +1089,14 @@ Three rules worth stating outright, because each of them cost a day somewhere:
1088
1089
  and a non-zero exit code.
1089
1090
 
1090
1091
  `flushEventLoopUntil(isDone, { turns, label })` is the same thing with a condition and a budget —
1091
- for a `resource()` leaving `loading`, a chunk becoming reachable, an SDK reporting itself ready. Use
1092
- it instead of a hand-tuned turn count: the count depends on the dependency, not on the spec, and a
1093
- condition that never holds fails naming the `label` rather than hanging until the runner's timeout.
1092
+ for a chunk becoming reachable, an SDK reporting itself ready, a queue draining. Use it instead of a
1093
+ hand-tuned turn count: the count depends on the dependency, not on the spec, and a condition that
1094
+ never holds fails naming the `label` rather than hanging until the runner's timeout.
1095
+
1096
+ **Not for an Angular `resource()` / `httpResource()`.** Those need a change-detection _tick_, and
1097
+ this helper only takes event-loop turns — a resource awaited through it finishes the whole budget
1098
+ having issued zero requests. `settleResource(resource, { turns, label })` from
1099
+ `vitest-auto-spy/angular` is that wait.
1094
1100
 
1095
1101
  `flushEventLoop(turns?)` takes real event-loop turns even while the timers are faked, without
1096
1102
  touching the clock. It is the honest name for the `await vi.advanceTimersByTimeAsync(0)` trick,
@@ -1474,9 +1480,15 @@ spies.get(PricingService).total.mockReturnValue(100);
1474
1480
  // NOTE: Injector.create() — it does NOT accept EnvironmentProviders (provideHttpClient() etc.)
1475
1481
 
1476
1482
  // zoneless waiting
1477
- await stable(fixture); // flush effects, then await the fixture
1483
+ await stable(fixture); // flush effects, then await the fixture; fails at 2000 ms naming the cause
1484
+ await stable(fixture, { timeout: 5000, label: 'the products fixture' });
1478
1485
  flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
1479
1486
 
1487
+ // resources — one wait for httpResource(), resource() and rxResource()
1488
+ flushEffects(); // an httpResource issues NO request until something ticks
1489
+ httpTesting.expectOne('/api/products').flush([product]);
1490
+ await settleResource(products, { label: 'the product resource' });
1491
+
1480
1492
  // signal assertions
1481
1493
  registerSignalMatchers(); // once, in the setup file
1482
1494
  expect(component.total).toHaveSignalValue(3);
@@ -1489,6 +1501,12 @@ Two zoneless traps:
1489
1501
  - `expect(someSignal).toBeTruthy()` passes for **every** signal ever created — a signal is a
1490
1502
  function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake.
1491
1503
 
1504
+ And one resource trap, which is the same shape one level up: an `httpResource()` reports `loading`
1505
+ with its **default** value until a tick _and_ a microtask after its response is flushed, so a spec
1506
+ that asserts too early asserts the default and passes. `settleResource` fails instead of passing
1507
+ emptily. Note the order — `flushEffects()` first (the request is issued there, not on creation),
1508
+ then the flush, then the wait.
1509
+
1492
1510
  Per-file timing, to find which specs actually pay for `TestBed`:
1493
1511
 
1494
1512
  ```ts
@@ -1802,7 +1820,9 @@ packages, which a subpath export can never be.
1802
1820
  | 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…())` |
1803
1821
  | 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` |
1804
1822
  | `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()` |
1805
- | a hand-tuned number of turns waiting for a `resource()` to load | the hand-off count depends on the dependency, not on the spec | `await flushEventLoopUntil(() => r.status() !== 'loading', { label })` |
1823
+ | 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 })` |
1824
+ | 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 |
1825
+ | 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 |
1806
1826
  | `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 |
1807
1827
  | a template error in a `describe` that never patched anything | a spec `afterEach` threw and skipped `setupAutoSpy`'s, so a `mock*Prop` patch travelled | upgrade — an `onTestFinished` net restores it and names the cause (§10) |
1808
1828
  | `Property 'mockReturnValue' does not exist on type 'never'` | a generic method with a conditional return type; the spy collapsed | upgrade — fixed in the types; the member now keeps its sync helper bundle |
package/README.md CHANGED
@@ -16,7 +16,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
16
16
  [![npm version](https://img.shields.io/npm/v/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
17
17
  [![npm downloads](https://img.shields.io/npm/dm/vitest-auto-spy?color=brightgreen&logo=npm)](https://www.npmjs.com/package/vitest-auto-spy)
18
18
  [![CI](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml/badge.svg)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
19
- [![minzipped size](https://img.shields.io/badge/minzip-12.5%20kB-brightgreen)](#install)
19
+ [![minzipped size](https://img.shields.io/badge/minzip-12.7%20kB-brightgreen)](#install)
20
20
  [![types](https://img.shields.io/npm/types/vitest-auto-spy?logo=typescript&logoColor=white)](https://www.npmjs.com/package/vitest-auto-spy)
21
21
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
22
22
  [![license](https://img.shields.io/npm/l/vitest-auto-spy?color=blue)](./LICENSE)
@@ -48,7 +48,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
48
48
  - 📡 First-class RxJS `Observable` spying (`nextWith`, `nextWithValues`, `throwWith`, …)
49
49
  - ⚙️ Getter / setter spies via `accessorSpies`
50
50
  - 🧰 DI & mocking utilities — `provideAutoSpy` / `injectSpy` (Angular, NestJS, Vue), `createFunctionSpy`, `mockReadonlyProp` for signals
51
- - ⚡ Angular speed & zoneless helpers — `renderShallow` (**1.7×** on real component specs), `createWithAutoSpies`, `stable` / `flushEffects`, `toHaveSignalValue`, per-file `TestBed` timings
51
+ - ⚡ Angular speed & zoneless helpers — `renderShallow` (**1.7×** on real component specs), `createWithAutoSpies`, `stable` / `flushEffects`, `settleResource` for `httpResource()`, `toHaveSignalValue`, per-file `TestBed` timings
52
52
  - 🧱 The providers a testing module cannot reach — `overrideComponentProvider`, `provideAutoSpyForToken`, `assertNgModuleScopes`, `createDirectiveHost`
53
53
  - 📡 Observable assertions that fail on silence — `expectEmission` / `expectEmissions` / `expectNoEmission` / `expectCompletion` / `expectError`, no rxjs required, Angular `output()` included
54
54
  - 🏗️ Doubles for what the code builds itself — `mockConstructor` / `stubConstructor` for `new`, plus `stubMediaElement`, `stubAbortController` and the observer stubs
@@ -112,6 +112,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
112
112
  - [Shallow component rendering](#shallow-component-rendering)
113
113
  - [Building a class with auto-spied dependencies](#building-a-class-with-auto-spied-dependencies)
114
114
  - [Zoneless waiting](#zoneless-waiting)
115
+ - [Settling a `resource()` or `httpResource()`](#settling-a-resource-or-httpresource)
115
116
  - [Asserting a signal's value](#asserting-a-signals-value)
116
117
  - [Where a spec spends its time](#where-a-spec-spends-its-time)
117
118
  - [NestJS](#nestjs)
@@ -711,7 +712,7 @@ Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runti
711
712
  | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | :----: |
712
713
  | `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
713
714
  | `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) + `createObservableWithValues` | `rxjs` | ✅ |
714
- | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, the `mock*Prop` helpers, signal matchers, TestBed diagnostics | `@angular/core` | ✅ |
715
+ | `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, the `mock*Prop` helpers, signal matchers, TestBed diagnostics | `@angular/core` | ✅ |
715
716
  | `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
716
717
  | `vitest-auto-spy/bun-angular` | Angular's `TestBed` under `bun test` — DOM, JIT `templateUrl` resolution and a zoneless environment, from one preload | `bun:test`, `@angular/core` | ✅ |
717
718
  | `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()` | `node:test` | ✅ |
@@ -1255,6 +1256,35 @@ app the state that matters is signal-derived and effects are what move it forwar
1255
1256
  both, in the right order; `flushEffects` prefers `TestBed.tick()` (Angular ≥ 20) and falls back to
1256
1257
  `ApplicationRef.tick()`.
1257
1258
 
1259
+ The wait is bounded: `stable` gives the fixture **2000 ms** and then throws the cause, instead of
1260
+ letting the runner report a 5 s file-level timeout that names neither the helper nor the fixture.
1261
+ Pass `{ timeout, label }` to change either; `{ timeout: 0 }` waits indefinitely. The watchdog runs
1262
+ on a timer captured at import, so `vi.useFakeTimers()` cannot freeze it.
1263
+
1264
+ #### Settling a `resource()` or `httpResource()`
1265
+
1266
+ ```ts
1267
+ import { flushEffects, settleResource } from 'vitest-auto-spy/angular';
1268
+
1269
+ const products = TestBed.runInInjectionContext(() => httpResource<Product[]>(() => '/api/products'));
1270
+
1271
+ flushEffects(); // the request is issued here — not when the resource was created
1272
+ TestBed.inject(HttpTestingController).expectOne('/api/products').flush([product]);
1273
+ await settleResource(products, { label: 'the product resource' });
1274
+
1275
+ expect(products.value()).toEqual([product]);
1276
+ ```
1277
+
1278
+ Angular's resource primitives need a **different wait each** — measured on 21.2.17, an
1279
+ `httpResource` settles one tick + one microtask after its flush, a plain `resource()` takes two
1280
+ rounds of the same, and neither has made a request at all until something ticks. Getting it wrong
1281
+ asserts against the resource's _default_ value, which is a green test proving nothing.
1282
+ `settleResource` is the loop both converge under, with a turn budget and a failure that names the
1283
+ resource and the flush it is missing.
1284
+
1285
+ `flushEventLoopUntil` cannot do this: it takes real event-loop turns and never ticks, so a resource
1286
+ awaited through it finishes the budget having issued zero requests.
1287
+
1258
1288
  #### Asserting a signal's value
1259
1289
 
1260
1290
  ```ts
@@ -1931,7 +1961,8 @@ Both are the same object at runtime; only the view changes.
1931
1961
  | `describeDuplicateCopies()` / `getPackageCopies()` | The duplicate-install report, and the copies behind it |
1932
1962
  | `renderShallow(Component, opts?)` _(Angular)_ | `TestBed` component, minus its children and (by default) its template |
1933
1963
  | `createWithAutoSpies(Class, opts?)` _(Angular)_ | Build a class through Angular DI with every unprovided token auto-spied |
1934
- | `stable(fixture)` / `flushEffects()` _(Angular)_ | Zoneless waiting: flush effects, then await the fixture |
1964
+ | `stable(fixture, opts?)` / `flushEffects()` _(Angular)_ | Zoneless waiting: flush effects, then await the fixture, with a 2 s budget that names the cause |
1965
+ | `settleResource(resource, opts?)` _(Angular)_ | Tick until an `httpResource()` / `resource()` / `rxResource()` leaves `loading` |
1935
1966
  | `registerSignalMatchers()` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)` |
1936
1967
  | `enableTestBedDiagnostics(opts?)` _(Angular)_ | Per-file report of how much of a spec's time went into `TestBed` |
1937
1968
  | `setupAngularTestEnv(opts)` _(Angular)_ | Zone and zoneless spec files in one worker, switching platforms per file |
package/dist/angular.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { g as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, S as ShallowRender, c as SpyRegistry, d as createWithAutoSpies, f as flushEffects, i as injectSpy, p as provideAutoSpy, h as provideAutoSpyForToken, r as renderShallow, e as runEffect, s as stable } from './run-effect-C_7mFldc.js';
1
+ export { l as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, m as provideAutoSpyForToken, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-DbVZ96iR.js';
2
2
  import { Type, Signal, WritableSignal } from '@angular/core';
3
3
  import { S as Spy, C as ClassType, a as ClassSpyConfiguration, O as OnlyMethodKeysOf } from './types-dZUFYsox.js';
4
4
  export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-CKFksCvA.js';
package/dist/angular.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { useVitestAdapter } from './chunk-DMSELR3S.js';
2
- export { createWithAutoSpies, flushEffects, injectSpy, provideAutoSpy, provideAutoSpyForToken, renderShallow, runEffect, stable } from './chunk-F3NMY5KU.js';
2
+ export { createWithAutoSpies, flushEffects, injectSpy, provideAutoSpy, provideAutoSpyForToken, renderShallow, runEffect, settleResource, stable } from './chunk-TMO2UFLD.js';
3
3
  import './chunk-OS2QFTIF.js';
4
- export { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, setEmissionTimeout } from './chunk-QGZRH5XG.js';
4
+ export { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, setEmissionTimeout } from './chunk-2PFOBMTZ.js';
5
5
  import { mockReadonlyProp } from './chunk-QGBNXDKU.js';
6
6
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-QGBNXDKU.js';
7
- import { createSpyFromClass } from './chunk-RJJJTLQ3.js';
8
- import './chunk-OEQTH7RA.js';
7
+ import { createSpyFromClass } from './chunk-DNHLQG45.js';
8
+ import './chunk-WT75WGQZ.js';
9
9
  import './chunk-TNB3Y3GI.js';
10
10
  import './chunk-DA5E36HQ.js';
11
11
  import { withDocs, DOCS_LINKS } from './chunk-VR5GJRBS.js';
@@ -3,7 +3,7 @@ export { AsInstances, AssertMockedOptions, ConstructorMock, ConstructorSpy, Flus
3
3
  export { A as AutoMockConfiguration, C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, c as autoMocked, d as createAutoMock, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-RtR1iYgI.js';
4
4
  export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-CKFksCvA.js';
5
5
  export { d as describeDuplicateCopies, g as getPackageCopies } from './package-identity-B1pqa-Sh.js';
6
- export { A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, S as ShallowRender, c as SpyRegistry, d as createWithAutoSpies, f as flushEffects, i as injectSpy, p as provideAutoSpy, r as renderShallow, e as runEffect, s as stable } from './run-effect-C_7mFldc.js';
6
+ export { A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, S as SettleResourceOptions, d as ShallowRender, e as SpyRegistry, f as StableOptions, g as createWithAutoSpies, h as flushEffects, i as injectSpy, p as provideAutoSpy, r as renderShallow, j as runEffect, s as settleResource, k as stable } from './zoneless-DbVZ96iR.js';
7
7
  import 'rxjs';
8
8
  import 'vitest';
9
9
  import '@angular/core';
@@ -1,11 +1,11 @@
1
- export { createWithAutoSpies, flushEffects, injectSpy, provideAutoSpy, renderShallow, runEffect, stable } from './chunk-F3NMY5KU.js';
1
+ export { createWithAutoSpies, flushEffects, injectSpy, provideAutoSpy, renderShallow, runEffect, settleResource, stable } from './chunk-TMO2UFLD.js';
2
2
  import './chunk-OWQR2YVQ.js';
3
3
  import './chunk-NQXXUJPS.js';
4
- export { asInstance, asInstances, asSpy, assertMocked, clearAutoSpy, compareTestRuns, createMock, createSpyClass, diffByField, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-LSDPJMNS.js';
5
- export { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, setEmissionTimeout } from './chunk-QGZRH5XG.js';
4
+ export { asInstance, asInstances, asSpy, assertMocked, clearAutoSpy, compareTestRuns, createMock, createSpyClass, diffByField, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-R5MFTE64.js';
5
+ export { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, setEmissionTimeout } from './chunk-2PFOBMTZ.js';
6
6
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-QGBNXDKU.js';
7
- export { autoMocked, createAutoMock, createSpyFromClass } from './chunk-RJJJTLQ3.js';
8
- export { createFunctionSpy, errorHandler } from './chunk-OEQTH7RA.js';
7
+ export { autoMocked, createAutoMock, createSpyFromClass } from './chunk-DNHLQG45.js';
8
+ export { createFunctionSpy, errorHandler } from './chunk-WT75WGQZ.js';
9
9
  import './chunk-TNB3Y3GI.js';
10
10
  export { describeDuplicateCopies, getPackageCopies } from './chunk-DA5E36HQ.js';
11
11
  import { withDocs, DOCS_LINKS } from './chunk-VR5GJRBS.js';
package/dist/bun.d.ts CHANGED
@@ -444,8 +444,9 @@ declare function stubConstructor<T, TArgs extends unknown[] = unknown[]>(target:
444
444
  * Give the runtime `turns` real event-loop turns, whatever the timers are doing.
445
445
  *
446
446
  * Reach for it when the thing being awaited crosses out of the zone / out of the test's own
447
- * promise chain: a dynamic `import()` triggered by production code, an Angular `httpResource()` /
448
- * `resource()` delivering its first value, a native `async` function inside a dependency.
447
+ * promise chain: a dynamic `import()` triggered by production code, a native `async` function
448
+ * inside a dependency, a stub that resolves a turn later. Not an Angular `httpResource()` /
449
+ * `resource()` — those need a *tick*, which is `settleResource()`, not this.
449
450
  *
450
451
  * ```ts
451
452
  * component.openModal(); // production code does `await import('./modal')`
@@ -472,19 +473,25 @@ interface FlushUntilOptions {
472
473
  /**
473
474
  * Take real event-loop turns until `isDone()` says so, then stop — or fail saying it never did.
474
475
  *
475
- * The shape behind every hand-rolled "settle" helper: an Angular `httpResource()` / `resource()` /
476
- * `rxResource` leaving `loading`, a lazily-loaded chunk becoming reachable, an SDK reporting itself
477
- * ready. Written by hand it is a fixed number of turns, tuned by trial until the suite goes green
478
- * which is both slower than it needs to be (it always waits the maximum) and quietly fragile (one
479
- * more hand-off in a dependency and the number is wrong again).
476
+ * The shape behind every hand-rolled "settle" helper: a lazily-loaded chunk becoming reachable, an
477
+ * SDK reporting itself ready, a queue draining. Written by hand it is a fixed number of turns, tuned
478
+ * by trial until the suite goes green which is both slower than it needs to be (it always waits
479
+ * the maximum) and quietly fragile (one more hand-off in a dependency and the number is wrong
480
+ * again).
480
481
  *
481
482
  * ```ts
482
- * const products = TestBed.runInInjectionContext(() => httpResource(() => '/api/products'));
483
+ * client.warmUp();
483
484
  *
484
- * await flushEventLoopUntil(() => products.status() !== 'loading', { label: 'the product resource' });
485
- * expect(products.value()).toEqual([product]);
485
+ * await flushEventLoopUntil(() => client.isReady(), { label: 'the SDK handshake' });
486
+ * expect(client.session()).toBeDefined();
486
487
  * ```
487
488
  *
489
+ * **Not for an Angular resource** — use `settleResource()` from `vitest-auto-spy/angular` for that.
490
+ * This helper takes real event-loop turns and never *ticks*, and an `httpResource()` issues no
491
+ * request at all until something does: measured, a resource awaited here finishes the whole budget
492
+ * having made zero requests, then fails saying the condition was never met. The docstring used to
493
+ * claim that use case and show it as the example; it never worked.
494
+ *
488
495
  * The budget is what separates this from a `while (true)`: a condition that never becomes true is
489
496
  * the normal way for this to be used wrongly — the request was never made, the stub never resolved
490
497
  * — and a test that hangs until the runner's timeout reports the file, not the wait.
package/dist/bun.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import './chunk-OWQR2YVQ.js';
2
2
  import './chunk-NQXXUJPS.js';
3
- export { asInstance, asInstances, asSpy, assertMocked, clearAutoSpy, compareTestRuns, createMock, createSpyClass, diffByField, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-LSDPJMNS.js';
4
- export { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, setEmissionTimeout } from './chunk-QGZRH5XG.js';
3
+ export { asInstance, asInstances, asSpy, assertMocked, clearAutoSpy, compareTestRuns, createMock, createSpyClass, diffByField, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-R5MFTE64.js';
4
+ export { expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, setEmissionTimeout } from './chunk-2PFOBMTZ.js';
5
5
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-QGBNXDKU.js';
6
- export { autoMocked, createAutoMock, createSpyFromClass } from './chunk-RJJJTLQ3.js';
7
- export { createFunctionSpy, errorHandler } from './chunk-OEQTH7RA.js';
6
+ export { autoMocked, createAutoMock, createSpyFromClass } from './chunk-DNHLQG45.js';
7
+ export { createFunctionSpy, errorHandler } from './chunk-WT75WGQZ.js';
8
8
  import './chunk-TNB3Y3GI.js';
9
9
  export { describeDuplicateCopies, getPackageCopies } from './chunk-DA5E36HQ.js';
10
10
  import './chunk-VR5GJRBS.js';
@@ -1,4 +1,4 @@
1
- import { serializeValue } from './chunk-OEQTH7RA.js';
1
+ import { serializeValue } from './chunk-WT75WGQZ.js';
2
2
 
3
3
  // src/lib/expect-emission.ts
4
4
  function subscribeToSource(source$, observer) {
@@ -1,4 +1,4 @@
1
- import { AUTO_SPY_MARK, createFunctionSpy, markAsMock } from './chunk-OEQTH7RA.js';
1
+ import { AUTO_SPY_MARK, createFunctionSpy, markAsMock } from './chunk-WT75WGQZ.js';
2
2
  import { requireObservableSupport } from './chunk-TNB3Y3GI.js';
3
3
  import { getMockAdapter } from './chunk-DA5E36HQ.js';
4
4
  import { withDocs, DOCS_LINKS } from './chunk-VR5GJRBS.js';
@@ -1,6 +1,6 @@
1
1
  import { mockValueProp, mockReadonlyPropGetter } from './chunk-QGBNXDKU.js';
2
- import { createSpyFromClass, createProxyPropStore, describeStoredProp, dropStoredProp, storeDefinedProp, writeStoredAccessor, writeStoredValue, readStoredAccessor, NOT_STORED, isProtocolKey, isDeletedProp } from './chunk-RJJJTLQ3.js';
3
- import { runClearHook, runConfigReset, createFunctionSpy, isMarkedMock, serializeValue } from './chunk-OEQTH7RA.js';
2
+ import { createSpyFromClass, createProxyPropStore, describeStoredProp, dropStoredProp, storeDefinedProp, writeStoredAccessor, writeStoredValue, readStoredAccessor, NOT_STORED, isProtocolKey, isDeletedProp } from './chunk-DNHLQG45.js';
3
+ import { runClearHook, runConfigReset, createFunctionSpy, isMarkedMock, readDeepChildren, serializeValue, DEEP_CHILDREN } from './chunk-WT75WGQZ.js';
4
4
  import { getMockAdapter } from './chunk-DA5E36HQ.js';
5
5
  import { withDocs, DOCS_LINKS } from './chunk-VR5GJRBS.js';
6
6
 
@@ -38,34 +38,43 @@ function readSpyMember(target, key, receiver, boundSpyMethods) {
38
38
  boundSpyMethods.set(key, bound);
39
39
  return bound;
40
40
  }
41
+ function readNodeMember(state, target, key, receiver) {
42
+ const patched = readStoredAccessor(state.store, key, receiver);
43
+ if (patched !== NOT_STORED) {
44
+ return patched;
45
+ }
46
+ if (state.store.values.has(key)) {
47
+ return state.store.values.get(key);
48
+ }
49
+ if (key === DEEP_CHILDREN) {
50
+ return state.children;
51
+ }
52
+ if (key === "then" || isProtocolKey(key)) {
53
+ return void 0;
54
+ }
55
+ if (getSpySurfaceKeys().has(key)) {
56
+ return readSpyMember(target, key, receiver, state.boundSpyMethods);
57
+ }
58
+ if (typeof key === "symbol" || isDeletedProp(state.store, key)) {
59
+ return void 0;
60
+ }
61
+ if (!state.children.has(key)) {
62
+ state.children.set(key, createDeepNode(`${state.name}.${String(key)}`, {}, state.selfReturning));
63
+ }
64
+ return state.children.get(key);
65
+ }
41
66
  function createDeepNode(name, overrides, selfReturning) {
42
67
  const spy = createFunctionSpy(name);
43
- const children = /* @__PURE__ */ new Map();
44
- const boundSpyMethods = /* @__PURE__ */ new Map();
45
- const store = createProxyPropStore(overrides);
68
+ const state = {
69
+ name,
70
+ selfReturning,
71
+ children: /* @__PURE__ */ new Map(),
72
+ boundSpyMethods: /* @__PURE__ */ new Map(),
73
+ store: createProxyPropStore(overrides)
74
+ };
75
+ const { store } = state;
46
76
  const handler = {
47
- get(target, key, receiver) {
48
- const patched = readStoredAccessor(store, key, receiver);
49
- if (patched !== NOT_STORED) {
50
- return patched;
51
- }
52
- if (store.values.has(key)) {
53
- return store.values.get(key);
54
- }
55
- if (key === "then" || isProtocolKey(key)) {
56
- return void 0;
57
- }
58
- if (getSpySurfaceKeys().has(key)) {
59
- return readSpyMember(target, key, receiver, boundSpyMethods);
60
- }
61
- if (typeof key === "symbol" || isDeletedProp(store, key)) {
62
- return void 0;
63
- }
64
- if (!children.has(key)) {
65
- children.set(key, createDeepNode(`${name}.${String(key)}`, {}, selfReturning));
66
- }
67
- return children.get(key);
68
- },
77
+ get: (target, key, receiver) => readNodeMember(state, target, key, receiver),
69
78
  set(_target, key, value, receiver) {
70
79
  if (!writeStoredAccessor(store, key, value, receiver)) {
71
80
  writeStoredValue(store, key, value);
@@ -105,7 +114,7 @@ function collectAccessorMocks(spy) {
105
114
  }
106
115
  return [...Object.values(bag.getters), ...Object.values(bag.setters)].filter(isMarkedMock);
107
116
  }
108
- function collectMocks(spy) {
117
+ function collectOwnMocks(spy) {
109
118
  const mocks = [];
110
119
  Object.keys(spy).forEach((key) => {
111
120
  const descriptor = Object.getOwnPropertyDescriptor(spy, key);
@@ -118,6 +127,24 @@ function collectMocks(spy) {
118
127
  });
119
128
  return [...mocks, ...collectAccessorMocks(spy)];
120
129
  }
130
+ function collectMocks(spy) {
131
+ const mocks = [];
132
+ const seen = /* @__PURE__ */ new Set();
133
+ const visit = (value) => {
134
+ if (seen.has(value)) {
135
+ return;
136
+ }
137
+ seen.add(value);
138
+ if (isMarkedMock(value)) {
139
+ mocks.push(value);
140
+ readDeepChildren(value).forEach(visit);
141
+ return;
142
+ }
143
+ collectOwnMocks(value).forEach(visit);
144
+ };
145
+ visit(spy);
146
+ return mocks;
147
+ }
121
148
  function clearAutoSpy(spy) {
122
149
  const adapter = getMockAdapter();
123
150
  collectMocks(spy).forEach((mock) => {
@@ -1,8 +1,8 @@
1
- import { createSpyFromClass, createAutoMock } from './chunk-RJJJTLQ3.js';
2
- import { isAutoSpyLike } from './chunk-OEQTH7RA.js';
1
+ import { createSpyFromClass, createAutoMock } from './chunk-DNHLQG45.js';
2
+ import { isAutoSpyLike } from './chunk-WT75WGQZ.js';
3
3
  import { withDocs, DOCS_LINKS } from './chunk-VR5GJRBS.js';
4
4
  import { TestBed } from '@angular/core/testing';
5
- import { NO_ERRORS_SCHEMA, Injector, runInInjectionContext, ApplicationRef, ɵSIGNAL as _SIGNAL } from '@angular/core';
5
+ import { Injector, runInInjectionContext, NO_ERRORS_SCHEMA, ApplicationRef, ɵSIGNAL as _SIGNAL } from '@angular/core';
6
6
 
7
7
  function provideAutoSpy(ObjectClass, methodsToSpyOnOrConfig) {
8
8
  return {
@@ -32,42 +32,6 @@ function warnWhenNotASpy(token, injected) {
32
32
  )
33
33
  );
34
34
  }
35
- function isStandalone(component) {
36
- const definition = Reflect.get(component, "\u0275cmp");
37
- return typeof definition === "object" && definition !== null && Reflect.get(definition, "standalone") === true;
38
- }
39
- function buildOverride(component, options) {
40
- const override = {};
41
- if (isStandalone(component)) {
42
- override.imports = options.keepChildren ?? [];
43
- override.schemas = [NO_ERRORS_SCHEMA];
44
- }
45
- if (!options.keepTemplate) {
46
- override.template = options.template ?? "";
47
- override.styles = [];
48
- }
49
- return override;
50
- }
51
- function applyInputs(fixture, inputs) {
52
- Object.entries(inputs ?? {}).forEach(([name, value]) => fixture.componentRef.setInput(name, value));
53
- }
54
- function renderShallow(component, options = {}) {
55
- const standalone = isStandalone(component);
56
- TestBed.configureTestingModule({
57
- imports: standalone ? [component, ...options.imports ?? []] : options.imports ?? [],
58
- declarations: standalone ? [] : [component],
59
- providers: options.providers ?? [],
60
- schemas: [NO_ERRORS_SCHEMA]
61
- });
62
- TestBed.overrideComponent(component, { set: buildOverride(component, options) });
63
- options.beforeCreate?.();
64
- const fixture = TestBed.createComponent(component);
65
- applyInputs(fixture, options.inputs);
66
- if (options.detectChanges ?? true) {
67
- fixture.detectChanges();
68
- }
69
- return { fixture, component: fixture.componentInstance };
70
- }
71
35
  function createSpyForToken(token) {
72
36
  if (typeof token === "function") {
73
37
  return createSpyFromClass(token, { lazySpies: true });
@@ -109,17 +73,41 @@ function createWithAutoSpies(target, options = {}) {
109
73
  }
110
74
  };
111
75
  }
112
- function flushEffects() {
113
- const testBed = TestBed;
114
- if (typeof testBed.tick === "function") {
115
- testBed.tick();
116
- return;
76
+ function isStandalone(component) {
77
+ const definition = Reflect.get(component, "\u0275cmp");
78
+ return typeof definition === "object" && definition !== null && Reflect.get(definition, "standalone") === true;
79
+ }
80
+ function buildOverride(component, options) {
81
+ const override = {};
82
+ if (isStandalone(component)) {
83
+ override.imports = options.keepChildren ?? [];
84
+ override.schemas = [NO_ERRORS_SCHEMA];
117
85
  }
118
- TestBed.inject(ApplicationRef).tick();
86
+ if (!options.keepTemplate) {
87
+ override.template = options.template ?? "";
88
+ override.styles = [];
89
+ }
90
+ return override;
119
91
  }
120
- async function stable(fixture) {
121
- flushEffects();
122
- await fixture.whenStable();
92
+ function applyInputs(fixture, inputs) {
93
+ Object.entries(inputs ?? {}).forEach(([name, value]) => fixture.componentRef.setInput(name, value));
94
+ }
95
+ function renderShallow(component, options = {}) {
96
+ const standalone = isStandalone(component);
97
+ TestBed.configureTestingModule({
98
+ imports: standalone ? [component, ...options.imports ?? []] : options.imports ?? [],
99
+ declarations: standalone ? [] : [component],
100
+ providers: options.providers ?? [],
101
+ schemas: [NO_ERRORS_SCHEMA]
102
+ });
103
+ TestBed.overrideComponent(component, { set: buildOverride(component, options) });
104
+ options.beforeCreate?.();
105
+ const fixture = TestBed.createComponent(component);
106
+ applyInputs(fixture, options.inputs);
107
+ if (options.detectChanges ?? true) {
108
+ fixture.detectChanges();
109
+ }
110
+ return { fixture, component: fixture.componentInstance };
123
111
  }
124
112
  function readReactiveNode(candidate) {
125
113
  if (typeof candidate !== "object" || candidate === null) {
@@ -150,5 +138,63 @@ function runEffect(effectRef) {
150
138
  }
151
139
  node.fn();
152
140
  }
141
+ var setTimer = globalThis.setTimeout.bind(globalThis);
142
+ var clearTimer = globalThis.clearTimeout.bind(globalThis);
143
+ function flushEffects() {
144
+ const testBed = TestBed;
145
+ if (typeof testBed.tick === "function") {
146
+ testBed.tick();
147
+ return;
148
+ }
149
+ TestBed.inject(ApplicationRef).tick();
150
+ }
151
+ async function stable(fixture, options = {}) {
152
+ flushEffects();
153
+ const timeout = options.timeout ?? 2e3;
154
+ if (timeout <= 0) {
155
+ await fixture.whenStable();
156
+ return;
157
+ }
158
+ let watchdog = void 0;
159
+ try {
160
+ await Promise.race([
161
+ fixture.whenStable(),
162
+ new Promise((_resolve, reject) => {
163
+ watchdog = setTimer(() => reject(unstableError(timeout, options.label)), timeout);
164
+ })
165
+ ]);
166
+ } finally {
167
+ clearTimer(watchdog);
168
+ }
169
+ }
170
+ function unstableError(timeout, label) {
171
+ const what = label ?? "the fixture";
172
+ return new Error(
173
+ withDocs(
174
+ `[vitest-auto-spy] stable: ${what} was still unstable after ${timeout} ms. A pending HttpClient request keeps a fixture unstable, and under \`provideHttpClientTesting\` only the spec can complete one \u2014 flush it before awaiting (\`TestBed.inject(HttpTestingController).expectOne(url).flush(body)\`), or use \`settleResource()\` if what you are waiting for is a resource. The other cause is a real timer: a \`setInterval\` or a long \`setTimeout\` the component started keeps Angular busy for as long as it runs, and \`setupFakeTimers()\` plus \`advanceTimers()\` is how a spec gets past that. Raise \`{ timeout }\` only once neither is true.`,
175
+ DOCS_LINKS.angular
176
+ )
177
+ );
178
+ }
179
+
180
+ // src/lib/settle-resource.ts
181
+ var PENDING_STATUSES = /* @__PURE__ */ new Set(["loading", "reloading"]);
182
+ async function settleResource(target, options = {}) {
183
+ const turns = options.turns ?? 20;
184
+ for (let turn = 0; turn <= turns; turn += 1) {
185
+ if (!PENDING_STATUSES.has(target.status())) {
186
+ return;
187
+ }
188
+ flushEffects();
189
+ await Promise.resolve();
190
+ }
191
+ const what = options.label ?? "the resource";
192
+ throw new Error(
193
+ withDocs(
194
+ `[vitest-auto-spy] settleResource: ${what} was still '${target.status()}' after ${turns} rounds of tick + microtask. A resource stays loading until its request completes, and under \`provideHttpClientTesting\` nothing but the spec can complete one \u2014 flush it first (\`TestBed.inject(HttpTestingController).expectOne(url).flush(body)\`), then await this again. If there is no request to flush, the resource never started: its \`request()\` computation reads a signal the test never set, or the injection context it was created in was discarded. And if the loader resolves on a *timer* rather than a promise, no number of turns will do it \u2014 \`advanceTimers()\` is what moves those.`,
195
+ DOCS_LINKS.angular
196
+ )
197
+ );
198
+ }
153
199
 
154
- export { createWithAutoSpies, flushEffects, injectSpy, provideAutoSpy, provideAutoSpyForToken, renderShallow, runEffect, stable };
200
+ export { createWithAutoSpies, flushEffects, injectSpy, provideAutoSpy, provideAutoSpyForToken, renderShallow, runEffect, settleResource, stable };