vitest-auto-spy 5.60.0 → 5.62.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/AGENTS.md CHANGED
@@ -455,6 +455,7 @@ In [`agent-docs/doubles.md`](./agent-docs/doubles.md). Read it when production c
455
455
  ## 13. Angular
456
456
 
457
457
  In [`agent-docs/angular.md`](./agent-docs/angular.md). Read it for any spec that uses `TestBed`, signals, Angular HTTP, the router or an Angular double.
458
+ For selected void window methods, use `createWindowSpies` from `/angular/doubles`; the window recipe is in that file.
458
459
 
459
460
  <!-- agents-outline: agent-docs/angular.md -->
460
461
 
package/CHANGELOG.md CHANGED
@@ -10,6 +10,21 @@ The latest released version here must always match the one published on
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [5.62.0] - 2026-10-20
14
+
15
+ ### Fixed
16
+
17
+ - **`prefer-settle-dynamic-import` recognises Angular provider setup in setup hooks.** A namespace
18
+ loaded for providers is allowed after logger or fixture preparation. Imports after application
19
+ actions remain checked. The English and Russian rule guides distinguish provider loading from
20
+ waiting for handlers, and clarify that `Promise.resolve()` does not take an event-loop turn.
21
+
22
+ ## [5.61.0] - 2026-10-07
23
+
24
+ ### Added
25
+
26
+ - `createWindowSpies` in `/angular/doubles`: typed, configured no-op spies for selected void window methods, including `location.reload` and `parent.postMessage`, composable with existing window overrides.
27
+
13
28
  ## [5.60.0] - 2026-10-06
14
29
 
15
30
  ### Added
@@ -6841,7 +6856,9 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
6841
6856
  This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
6842
6857
  [full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
6843
6858
 
6844
- [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.60.0...HEAD
6859
+ [Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.62.0...HEAD
6860
+ [5.62.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.61.0...v5.62.0
6861
+ [5.61.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.60.0...v5.61.0
6845
6862
  [5.60.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.59.1...v5.60.0
6846
6863
  [5.59.1]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.59.0...v5.59.1
6847
6864
  [5.59.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.58.0...v5.59.0
package/README.md CHANGED
@@ -3125,6 +3125,7 @@ only the ones you need. Every helper, with its signature: [API](https://asdalexe
3125
3125
  | `provideLocationDouble()` / `injectLocationDouble()` / `createLocationDouble()` | `/angular-router` | Angular's own `SpyLocation` and `MockLocationStrategy` in one call — a real history, the `urlChanges` journal, `simulateUrlPop()` / `simulateHashChange()` for the browser's half of the contract; `Location` is `providedIn: 'root'`, so a spec that forgets the provider silently drives the platform's real one |
3126
3126
  | `createForm(model, schema?)` / `registerFormMatchers()` | `/signal-forms` | A signal form built where `form()` can inject, and `expect(field).toHaveFieldErrors(['required'])` over what it produced |
3127
3127
  | `provideWindowDouble(TOKEN, over?)` / `provideDocumentDouble(over?)` | `/angular/doubles` | A `window` / `document` merged **over** the real jsdom one, so members the spec never named still answer; the globals stay untouched |
3128
+ | `createWindowSpies(selection)` | `/angular/doubles` | Typed no-op spies for selected void window methods: `createWindowSpies({ location: ['reload'], parent: ['postMessage'] })`; pass them as window overrides. |
3128
3129
  | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(Ref, init?)` | `/angular/doubles` | The Material dialog trio without `@angular/material` as a dependency — the token and the ref class are arguments, `afterClosed()` still answers after the close |
3129
3130
  | `providePlatform(platform, flags?)` | `/angular/doubles` | `PLATFORM_ID` plus the app's own `isBrowser` / `isServer` tokens, always agreeing |
3130
3131
  | `provideDomSanitizerDouble()` / `provideChangeDetectorRefDouble()` | `/angular/doubles` | A `DomSanitizer` whose bypass spies return Angular's real safe values; a `ChangeDetectorRef` of four spies answering `undefined` (`create…Double()` for the bare double) |
@@ -862,6 +862,32 @@ Five things to know:
862
862
  `win.Promise.resolve()`, `win.Object.keys(x)` and `new win.Event('x')` work, and
863
863
  `win.Event === window.Event` holds — which is what `instanceof` in production code needs.
864
864
 
865
+ `createWindowSpies` creates typed spies for selected `void` methods, including nested window members.
866
+ The spies return `undefined` without invoking browser methods.
867
+
868
+ ```ts
869
+ import { InjectionToken } from '@angular/core';
870
+ import { TestBed } from '@angular/core/testing';
871
+ import { expect } from 'vitest';
872
+ import { createWindowSpies, provideWindowDouble } from 'vitest-auto-spy/angular/doubles';
873
+
874
+ const WINDOW = new InjectionToken<Window>('WINDOW');
875
+ const spies = createWindowSpies({ location: ['reload'], parent: ['postMessage'] });
876
+ TestBed.configureTestingModule({
877
+ providers: [provideWindowDouble(WINDOW, { ...spies, location: { ...spies.location, hostname: 'tv.kion.ru' } })],
878
+ });
879
+ const win = TestBed.inject(WINDOW);
880
+ win.location.reload();
881
+ win.parent.postMessage('ready', '*');
882
+ expect(spies.location.reload).toHaveBeenCalledOnce();
883
+ expect(spies.parent.postMessage).toHaveBeenCalledWith('ready', '*');
884
+ ```
885
+
886
+ Pass an array for root methods: `createWindowSpies(['close', 'focus'])`. Each call builds fresh spies.
887
+ The object returned by `createWindowSpies` contains only the selected methods. Their parameter types and spy helpers are preserved.
888
+ For methods that return a value, use `createFunctionSpy` and configure that value explicitly.
889
+ When adding location values, spread `spies.location` too; replacing it would discard the reload spy.
890
+
865
891
  ### The Material dialog trio — `provideMatDialogData` / `provideMatDialogRef`
866
892
 
867
893
  ```ts
@@ -22,6 +22,17 @@ fixture.debugElement.query(By.css('.open')).nativeElement.click(); // production
22
22
  await settleDynamicImport(() => import('./profile-select.modal'));
23
23
  ```
24
24
 
25
+ **Loading provider classes is setup, not a wait for application work.** Keep
26
+ `const { PremiumService } = await import('./premium.service')` when its exports configure Angular
27
+ providers before creating the service under test. `settleDynamicImport` is useful after an action
28
+ starts a lazy import; it adds no needed wait merely to read provider classes. Preserve dynamic
29
+ imports when module evaluation order matters, including `vi.resetModules()` / `vi.doMock()` setup.
30
+ The ESLint rule recognises provider setup in `beforeEach` / `beforeAll`. Earlier fixture factories
31
+ must be imported functions whose bound results also supply providers. Member calls and other
32
+ actions prevent this exemption, except recognised module-registry resets.
33
+
34
+ `await Promise.resolve()` yields to microtasks; it is not equivalent to `flushEventLoop(1)`.
35
+
25
36
  Three rules worth stating outright, because each of them cost a day somewhere:
26
37
 
27
38
  - **`afterNextRender` does not run on `detectChanges()`.** A component that fills a form there is
@@ -30,9 +41,8 @@ Three rules worth stating outright, because each of them cost a day somewhere:
30
41
  - **`fixture.whenRenderingDone()` is not a stronger `whenStable()`.** With an animation renderer
31
42
  installed it degrades to `Promise.resolve()`. Use `stable(fixture)`.
32
43
  - **`fakeAsync` / `tick()` / `flushMicrotasks()` never reach the module loader.** Spinning
33
- `await Promise.resolve()` ten times looks like it works and instead lands the continuation after
34
- teardown — a green run with `NG0205: Injector has already been destroyed` in "Unhandled Errors"
35
- and a non-zero exit code.
44
+ `await Promise.resolve()` ten times may leave a continuation until after teardown, causing
45
+ `NG0205: Injector has already been destroyed` in "Unhandled Errors" and a non-zero exit code.
36
46
 
37
47
  **`stable()` and `flushEffects()` are not zoneless-only, and their tick runs inside the `NgZone` so
38
48
  that they are not.** `TestBed.createComponent` builds the component inside `ngZone.run(…)`, so an
@@ -1,6 +1,6 @@
1
1
  import './vitest-mock-types-B17-xaWt.js';
2
2
  import { InjectionToken, AbstractType, Injector, ValueProvider, FactoryProvider, ProviderToken, ChangeDetectorRef } from '@angular/core';
3
- import { h as AddSpyMethodsByReturnTypes, S as Spy, F as Func } from './types-BdYUK_Zv.js';
3
+ import { h as AddSpyMethodsByReturnTypes, S as Spy, F as Func, p as FunctionSpy } from './types-BdYUK_Zv.js';
4
4
  import { DomSanitizer } from '@angular/platform-browser';
5
5
  import 'vitest';
6
6
 
@@ -183,7 +183,7 @@ declare function injectMatDialogRef<Ref extends DialogRefLike>(RefClass: Abstrac
183
183
  */
184
184
 
185
185
  /** How many levels of slicing the type follows. Beyond it a member takes its whole type. */
186
- type Levels = [never, 0, 1, 2, 3];
186
+ type Levels$1 = [never, 0, 1, 2, 3];
187
187
  /**
188
188
  * One member's override: the member's own type, or a slice of it wherever it is a plain-ish object.
189
189
  *
@@ -192,7 +192,7 @@ type Levels = [never, 0, 1, 2, 3];
192
192
  * array is matched whole too: slicing one by index describes nothing a spec means.
193
193
  */
194
194
  type PlatformOverride<V, Depth extends number> = V extends (...args: never[]) => unknown ? V : V extends readonly unknown[] ? V : V extends object ? Depth extends 0 ? V : V | {
195
- [K in keyof V]?: PlatformOverride<V[K], Levels[Depth]>;
195
+ [K in keyof V]?: PlatformOverride<V[K], Levels$1[Depth]>;
196
196
  } : V;
197
197
  /**
198
198
  * What a spec says about a platform object: a value per member, and a **slice** wherever the member
@@ -470,4 +470,35 @@ declare function provideOverlayDouble<O extends OverlayLike>(OverlayClass: Abstr
470
470
  */
471
471
  declare function injectOverlayDouble<O extends OverlayLike>(OverlayClass: AbstractType<O>, injector?: Injector): OverlayDouble<O>;
472
472
 
473
- export { type AttachedComponent, type DialogComponent, type DialogDataOf, type DialogRefLike, type DialogResult, type MatDialogRefDouble, type MatDialogRefInit, type OverlayDouble, type OverlayDoubleInit, type OverlayLike, type OverlayRefDouble, type OverlayRefStub, type PlatformFlagTokens, type PlatformName, type PlatformOverrides, type PositionCall, createChangeDetectorRefDouble, createDocumentDouble, createDomSanitizerDouble, createMatDialogRef, createOverlayDouble, createWindowDouble, injectMatDialogRef, injectOverlayDouble, provideChangeDetectorRefDouble, provideDocumentDouble, provideDomSanitizerDouble, provideMatDialogData, provideMatDialogRef, provideOverlayDouble, providePlatform, provideWindowDouble };
473
+ type Levels = [never, 0, 1, 2, 3];
474
+ type VoidMethodKeys<T> = {
475
+ [K in keyof T]: T[K] extends (...args: never[]) => infer R ? (R extends void ? K : never) : never;
476
+ }[keyof T];
477
+ /** Select void methods at each object level; only named methods become spies. */
478
+ type WindowSpySelection<T = Window, Depth extends number = 3> = readonly VoidMethodKeys<T>[] | (Depth extends 0 ? never : {
479
+ [K in keyof T as T[K] extends object ? (T[K] extends (...args: never[]) => unknown ? never : K) : never]?: WindowSpySelection<NonNullable<T[K]>, Levels[Depth]>;
480
+ });
481
+ /** The selected methods, with their original call signatures and the auto-spy helpers. */
482
+ type WindowSpies<T, Selection> = Selection extends readonly (infer K)[] ? {
483
+ [P in Extract<K, keyof T>]: T[P] extends (...args: never[]) => unknown ? FunctionSpy<T[P]> : never;
484
+ } : {
485
+ [P in keyof Selection & keyof T]: WindowSpies<NonNullable<T[P]>, Selection[P]>;
486
+ };
487
+ type ValidSelection<T, Selection> = Selection extends readonly unknown[] ? readonly VoidMethodKeys<T>[] : {
488
+ [K in keyof Selection]: K extends keyof T ? T[K] extends (...args: never[]) => unknown ? never : ValidSelection<NonNullable<T[K]>, Selection[K]> : never;
489
+ };
490
+ /**
491
+ * Build typed, already configured no-op spies for selected void window methods.
492
+ *
493
+ * ```ts
494
+ * const spies = createWindowSpies({ location: ['reload'], parent: ['postMessage'] });
495
+ * provideWindowDouble(WINDOW, { ...spies, location: { ...spies.location, hostname: 'tv.kion.ru' } });
496
+ * expect(spies.location.reload).toHaveBeenCalledOnce();
497
+ * ```
498
+ *
499
+ * No global is read or patched. Pass the result to `createWindowDouble` or `provideWindowDouble`;
500
+ * unnamed members still use the real window. Use `createFunctionSpy` for methods returning values.
501
+ */
502
+ declare function createWindowSpies<const Selection extends WindowSpySelection>(selection: Selection & ValidSelection<Window, Selection>): WindowSpies<Window, Selection>;
503
+
504
+ export { type AttachedComponent, type DialogComponent, type DialogDataOf, type DialogRefLike, type DialogResult, type MatDialogRefDouble, type MatDialogRefInit, type OverlayDouble, type OverlayDoubleInit, type OverlayLike, type OverlayRefDouble, type OverlayRefStub, type PlatformFlagTokens, type PlatformName, type PlatformOverrides, type PositionCall, type WindowSpies, type WindowSpySelection, createChangeDetectorRefDouble, createDocumentDouble, createDomSanitizerDouble, createMatDialogRef, createOverlayDouble, createWindowDouble, createWindowSpies, injectMatDialogRef, injectOverlayDouble, provideChangeDetectorRefDouble, provideDocumentDouble, provideDomSanitizerDouble, provideMatDialogData, provideMatDialogRef, provideOverlayDouble, providePlatform, provideWindowDouble };
@@ -464,7 +464,35 @@ function injectOverlayDouble(OverlayClass, injector) {
464
464
  return double;
465
465
  }
466
466
 
467
+ // src/lib/window-spies.ts
468
+ function buildSpies(selection, path) {
469
+ const result = {};
470
+ if (Array.isArray(selection)) {
471
+ for (const key of selection) {
472
+ Object.defineProperty(result, key, {
473
+ value: createFunctionSpy(`${path}.${String(key)}`).mockReturnValue(void 0),
474
+ enumerable: true,
475
+ configurable: true,
476
+ writable: true
477
+ });
478
+ }
479
+ } else {
480
+ for (const key of Reflect.ownKeys(selection)) {
481
+ Object.defineProperty(result, key, {
482
+ value: buildSpies(Reflect.get(selection, key), `${path}.${String(key)}`),
483
+ enumerable: true,
484
+ configurable: true,
485
+ writable: true
486
+ });
487
+ }
488
+ }
489
+ return result;
490
+ }
491
+ function createWindowSpies(selection) {
492
+ return buildSpies(selection, "window");
493
+ }
494
+
467
495
  // src/angular-doubles.ts
468
496
  useVitestAdapter();
469
497
 
470
- export { createChangeDetectorRefDouble, createDocumentDouble, createDomSanitizerDouble, createMatDialogRef, createOverlayDouble, createWindowDouble, injectMatDialogRef, injectOverlayDouble, provideChangeDetectorRefDouble, provideDocumentDouble, provideDomSanitizerDouble, provideMatDialogData, provideMatDialogRef, provideOverlayDouble, providePlatform, provideWindowDouble };
498
+ export { createChangeDetectorRefDouble, createDocumentDouble, createDomSanitizerDouble, createMatDialogRef, createOverlayDouble, createWindowDouble, createWindowSpies, injectMatDialogRef, injectOverlayDouble, provideChangeDetectorRefDouble, provideDocumentDouble, provideDomSanitizerDouble, provideMatDialogData, provideMatDialogRef, provideOverlayDouble, providePlatform, provideWindowDouble };
package/dist/cli.js CHANGED
@@ -1667,7 +1667,7 @@ function foreignPragmaPass() {
1667
1667
  }
1668
1668
 
1669
1669
  // src/cli/checks/export-map.generated.ts
1670
- var EXPORT_MAP_VERSION = "5.59.1";
1670
+ var EXPORT_MAP_VERSION = "5.61.0";
1671
1671
  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";
1672
1672
  var EXPORTED_BY = {
1673
1673
  AccessorImplementations: "0 1 2 3 4 12 20 21 22",
@@ -1800,6 +1800,7 @@ var EXPORTED_BY = {
1800
1800
  createSpyFromInstance: "0 1 2 3 4 20 21 22",
1801
1801
  createSpyObj: "9",
1802
1802
  createWindowDouble: "14",
1803
+ createWindowSpies: "14",
1803
1804
  createWithAutoSpies: "2 12",
1804
1805
  CreateWithAutoSpiesOptions: "2 12",
1805
1806
  DeepMockProxy: "0 1 2 3 4 20 21 22",
@@ -2158,6 +2159,8 @@ var EXPORTED_BY = {
2158
2159
  WebStorageKey: "7",
2159
2160
  WebStorageStub: "7",
2160
2161
  WebStorageStubOptions: "7",
2162
+ WindowSpies: "14",
2163
+ WindowSpySelection: "14",
2161
2164
  withFakeTimers: "23",
2162
2165
  WithMockReturnValue: "0 1 2 3 4 20 21 22",
2163
2166
  withoutStrayTimerTracking: "23",
@@ -480,13 +480,13 @@ function runnerBehind(callee) {
480
480
  }
481
481
  return isCallExpression(callee) ? runnerBehind(callee.callee) : void 0;
482
482
  }
483
- function insideRunnerCallback(node) {
483
+ function enclosingRunner(node) {
484
484
  const callback = enclosingFunction(node);
485
485
  if (!callback) {
486
- return false;
486
+ return void 0;
487
487
  }
488
488
  const call = callback.parent;
489
- return isCallExpression(call) && call.arguments.includes(callback) && runnerBehind(call.callee) !== void 0;
489
+ return isCallExpression(call) && call.arguments.includes(callback) ? runnerBehind(call.callee) : void 0;
490
490
  }
491
491
  function consumptionOf(node) {
492
492
  const { parent } = node;
@@ -518,6 +518,74 @@ function takesTheNamespaceFirst(node, bound) {
518
518
  const before2 = siblings2.slice(0, siblings2.indexOf(statement)).reverse().find((sibling) => !bound.has(sibling));
519
519
  return before2 === void 0 ? statement.parent === callback.body : resetsRegistry(before2);
520
520
  }
521
+ function providerReference(context, reference, callback) {
522
+ if (enclosingFunction(reference) !== callback) {
523
+ return false;
524
+ }
525
+ let current = reference;
526
+ while (current !== callback) {
527
+ if (propertyName(current) === "providers") {
528
+ const configuration = current.parent;
529
+ const call = configuration.parent;
530
+ if (!isCallExpression(call) || !call.arguments.includes(configuration)) {
531
+ return false;
532
+ }
533
+ if (!isMemberExpression(call.callee) || memberName(call.callee) !== "configureTestingModule" || !isIdentifier(call.callee.object)) {
534
+ return false;
535
+ }
536
+ const receiver = call.callee.object;
537
+ const binding = findBinding(context.sourceCode.getScope(receiver), receiver.name);
538
+ const specifier = binding && importSpecifierOf(binding);
539
+ return specifier ? isIdentifier(specifier.imported) && specifier.imported.name === "TestBed" && specifier.parent.source.value === "@angular/core/testing" : binding === void 0 && receiver.name === "TestBed";
540
+ }
541
+ current = current.parent;
542
+ }
543
+ return false;
544
+ }
545
+ function suppliesProvider(context, awaited, callback) {
546
+ const holder = awaited.parent;
547
+ const pattern = isVariableDeclarator(holder) ? holder.id : isAssignmentExpression(holder) ? holder.left : void 0;
548
+ return pattern !== void 0 && anyInSubtree(
549
+ context,
550
+ pattern,
551
+ (candidate) => {
552
+ if (!isIdentifier(candidate)) {
553
+ return false;
554
+ }
555
+ const binding = findBinding(context.sourceCode.getScope(candidate), candidate.name);
556
+ const writes = binding?.references.filter((reference) => reference.writeExpr) ?? [];
557
+ return writes.length === 1 && writes[0]?.writeExpr === awaited && binding?.references.some(
558
+ (reference) => reference.identifier.range[0] > awaited.range[1] && providerReference(context, reference.identifier, callback)
559
+ ) === true;
560
+ },
561
+ false
562
+ );
563
+ }
564
+ function preparesProvider(context, call, callback) {
565
+ if (!isCallExpression(call) || !isIdentifier(call.callee)) {
566
+ return false;
567
+ }
568
+ const binding = findBinding(context.sourceCode.getScope(call), call.callee.name);
569
+ return binding?.defs.some((definition) => definition.type === "ImportBinding") === true && suppliesProvider(context, call, callback);
570
+ }
571
+ function loadsProviderForSetup(context, node, runner) {
572
+ const callback = enclosingFunction(node);
573
+ const statement = bindingStatement(node.parent);
574
+ if (!callback || statement?.parent !== callback.body || !isBlockStatement(callback.body)) {
575
+ return false;
576
+ }
577
+ if (runner !== "beforeAll" && runner !== "beforeEach" || !suppliesProvider(context, node.parent, callback)) {
578
+ return false;
579
+ }
580
+ return callback.body.body.slice(0, callback.body.body.indexOf(statement)).every(
581
+ (before2) => (before2.type === "VariableDeclaration" || isExpressionStatement(before2)) && !anyInSubtree(
582
+ context,
583
+ before2,
584
+ (candidate) => isAssignmentExpression(candidate) && !isIdentifier(candidate.left) || candidate.type === "UnaryExpression" && Reflect.get(candidate, "operator") === "delete" || (isCallExpression(candidate) || candidate.type === "NewExpression" || candidate.type === "AwaitExpression" || candidate.type === "UpdateExpression") && !preparesProvider(context, candidate, callback) && !(candidate.type === "AwaitExpression" && suppliesProvider(context, candidate, callback) && anyInSubtree(context, candidate, (child) => child.type === "ImportExpression" && child.parent === candidate, false)) && !isRunnerCall(candidate, REGISTRY_RESETS),
585
+ false
586
+ )
587
+ );
588
+ }
521
589
  function wrap(context, node) {
522
590
  const state = bindingState(context.sourceCode.getScope(node), HELPER);
523
591
  if (state === "taken") {
@@ -540,7 +608,7 @@ var preferSettleDynamicImport = defineRule({
540
608
  description: "Load a module the code under test lazy-loads with settleDynamicImport(), not a bare await import()",
541
609
  hasSuggestions: true,
542
610
  messages: {
543
- awaitedDynamicImport: `\`await {{load}}\` waits for the module, not for the code under test that was loading it: its continuation after its own \`await\` has not run yet, so the assertions below read the state one turn early. ${REPAIR}`,
611
+ awaitedDynamicImport: `\`await {{load}}\` waits for the module, not for the code under test that was loading it: its continuation after its own \`await\` may still be pending, so the assertions below may read state too early. ${REPAIR}`,
544
612
  thenedDynamicImport: `\`{{load}}.then(\u2026)\` waits for the module, not for the code under test that was loading it, and its callback runs later still, after the test ends if nothing awaits it. ${REPAIR}`
545
613
  },
546
614
  create: (context) => {
@@ -548,7 +616,8 @@ var preferSettleDynamicImport = defineRule({
548
616
  return {
549
617
  ImportExpression: (node) => {
550
618
  const consumption = consumptionOf(node);
551
- if (consumption === void 0 || !insideRunnerCallback(node) || consumption === "await" && takesTheNamespaceFirst(node, bound)) {
619
+ const runner = enclosingRunner(node);
620
+ if (consumption === void 0 || runner === void 0 || consumption === "await" && (takesTheNamespaceFirst(node, bound) || loadsProviderForSetup(context, node, runner))) {
552
621
  return;
553
622
  }
554
623
  const messageId = consumption === "await" ? "awaitedDynamicImport" : "thenedDynamicImport";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.60.0",
3
+ "version": "5.62.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",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vitest-auto-spy
3
- description: Typed spies on Vitest, bun:test, node:test and Rstest. Use when a spec imports vitest-auto-spy or a subpath (/angular, /angular/*, /signal-forms, /bun-angular, /bun, /node, /rstest, /rxjs, /diagnostics, /nestjs, /jasmine, /setup, /zone, /eslint-plugin), naming createSpyFromClass, createAutoMock, createMock, mockDeep, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, stubResponse, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, enableAngularDiagnostics, trackInjections, runEffect, settleResource, mockResourceProp, mockSignalProp, mockReadonlyProp, registerAutoSpyDefaults, provideActivatedRoute, provideLocationDouble, collectRouterEvents, createLog, createComponentStub, createForm, toHaveFieldErrors, stubWebStorage, assertNoShadowedProviders, createSpyFromInstance, adoptMock, spyOnOwnMethod, spyOnVoidMethod, explainSpy, compareTestRuns, expectEmission, setupAutoSpy, setSpyEngine, assertMocked, calledWith, mustBeCalledWith, captureArg, ArgumentCaptor, onlyMethodsToSpyOn, onUnstubbedCall, passthrough, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator, or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not a method of", "strict double has nothing configured", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController", "localStorage.setItem is not a function" or "Spy<T> is not assignable".
3
+ description: Typed spies on Vitest, bun:test, node:test and Rstest. Use when a spec imports vitest-auto-spy (/angular, /angular/*, /signal-forms, /bun-angular, /bun, /node, /rstest, /rxjs, /diagnostics, /nestjs, /jasmine, /setup, /zone, /eslint-plugin), naming createSpyFromClass, createAutoMock, createMock, mockDeep, subscribeSpyTo, provideAutoSpy, createWindowSpies, provideHttpTesting, expectRequest, stubResponse, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, enableAngularDiagnostics, trackInjections, runEffect, settleResource, mockResourceProp, mockSignalProp, mockReadonlyProp, registerAutoSpyDefaults, provideActivatedRoute, provideLocationDouble, collectRouterEvents, createLog, createComponentStub, createForm, toHaveFieldErrors, stubWebStorage, assertNoShadowedProviders, createSpyFromInstance, adoptMock, spyOnOwnMethod, spyOnVoidMethod, explainSpy, compareTestRuns, expectEmission, setupAutoSpy, setSpyEngine, assertMocked, calledWith, mustBeCalledWith, captureArg, ArgumentCaptor, onlyMethodsToSpyOn, onUnstubbedCall, passthrough, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator, or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not a method of", "strict double has nothing configured", "the override did not apply", "is not a constructor", "Expected to be running in 'ProxyZone'", "jasmine is not defined", "no HttpTestingController", "localStorage.setItem is not a function" or "Spy<T> is not assignable".
4
4
  ---
5
5
 
6
6
  # vitest-auto-spy
@@ -329,11 +329,17 @@ it('loads', async () => {
329
329
  | a component that reads `location.back()`, or asserts where a redirect landed | `provideLocationDouble()` + `injectLocationDouble()` — Angular's own `SpyLocation`; without the provider, `Location` is `providedIn: 'root'` and the real one answers |
330
330
  | "did A run before B?" across three collaborators | `createLog<'drop-cache' \| 'flush'>()` — hand `log.fn('drop-cache')` to each, assert `log.result()` once |
331
331
  | a `window` or `document` behind a DI token | `provideWindowDouble(WINDOW, { screen })` / `provideDocumentDouble({ … })` — `/angular/doubles` |
332
+ | selected void window methods, such as `location.reload` or `parent.postMessage` | `createWindowSpies({ location: ['reload'], parent: ['postMessage'] })` from `/angular/doubles` — pass the result to `provideWindowDouble(WINDOW, spies)`; each spy returns `undefined` without calling the browser. For root methods, use `createWindowSpies(['close', 'focus'])`. |
332
333
  | `MAT_DIALOG_DATA` and `MatDialogRef` provided by hand | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(MatDialogRef)` + `injectMatDialogRef(Ref)` — `/angular/doubles` |
333
334
  | asserting a `computed()` did **not** recompute | `trackRecomputations(sig)` / `trackEffectRuns(ref)` — `{ count, stop() }` |
334
335
  | `form()` in a spec, or `errors()` read by hand | `createForm(model, schema)` + `registerFormMatchers()` — then `toHaveFieldErrors([…])` (`/signal-forms`) |
335
336
  | any of those doubles without a `TestBed` | `createRouterDouble` (`/angular-router`) / `createWindowDouble` / `createDocumentDouble` / `createMatDialogRef` (`/angular/doubles`) |
336
337
 
338
+ When composing window spies with data overrides, keep the nested spies too:
339
+ `provideWindowDouble(WINDOW, { ...spies, location: { ...spies.location, hostname: 'example.test' } })`.
340
+ Assert calls on `spies.location.reload` or `spies.parent.postMessage`. For methods returning a value,
341
+ use `createFunctionSpy` from the core and configure that value explicitly.
342
+
337
343
  ## Rules that prevent most of the mistakes
338
344
 
339
345
  - Declare the variable as **`Spy<T>`, never as `T`** — `Spy<T>` is a mapped type and drops private