vitest-auto-spy 5.59.1 → 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 +1 -0
- package/CHANGELOG.md +35 -1
- package/README.md +2 -0
- package/agent-docs/angular.md +26 -0
- package/agent-docs/eslint.md +7 -4
- package/agent-docs/waiting.md +13 -3
- package/dist/angular-doubles.d.ts +35 -4
- package/dist/angular-doubles.js +29 -1
- package/dist/cli.js +4 -1
- package/dist/eslint-plugin.cjs +134 -25
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +7 -1
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,37 @@ 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
|
+
|
|
28
|
+
## [5.60.0] - 2026-10-06
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`no-redundant-as-instance`** in `vitest-auto-spy/eslint-plugin`, `warn` in `recommended`:
|
|
33
|
+
`expect(service.open()).toBe(asInstance(ref))`. The three comparing matchers take their argument as
|
|
34
|
+
a free type parameter, so the unwrap buys nothing there — it keeps an import alive and reads as if
|
|
35
|
+
the types needed it. `--fix` drops the wrapper and, when that was the import's last use, the import
|
|
36
|
+
too. Slots typed against the real class are untouched: `toHaveBeenCalledWith(asInstance(cart))`
|
|
37
|
+
can be load-bearing, and so can a wrapper that is part of the argument (`[asInstance(x)]`,
|
|
38
|
+
`asInstance(x).id`). Three such call sites in two consumer suites (5.59.1). The plugin now ships
|
|
39
|
+
fifty-eight rules. `/eslint-plugin` is 53.45 → 53.84 kB (+0.39 kB, +0.7 %): the rule, minus the
|
|
40
|
+
matcher set that `no-constant-expect`, `prefer-to-have-signal-value` and
|
|
41
|
+
`no-vacuous-absence-assertion` each carried a copy of, and the `matcherOf` walk `no-constant-expect`
|
|
42
|
+
duplicated — both now shared out of `absence-assertion.ts`.
|
|
43
|
+
|
|
13
44
|
## [5.59.1] - 2026-09-30
|
|
14
45
|
|
|
15
46
|
## [5.59.0] - 2026-09-30
|
|
@@ -6825,7 +6856,10 @@ moved under 1 %: `/angular` +192 B (+1.0 %) for the override queue and the HTTP
|
|
|
6825
6856
|
This copy ships with the package and carries 5.x only. Releases before 5.0.0 are in the
|
|
6826
6857
|
[full changelog](https://github.com/ASDAlexey/vitest-auto-spy/blob/master/CHANGELOG.md).
|
|
6827
6858
|
|
|
6828
|
-
[Unreleased]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.
|
|
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
|
|
6862
|
+
[5.60.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.59.1...v5.60.0
|
|
6829
6863
|
[5.59.1]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.59.0...v5.59.1
|
|
6830
6864
|
[5.59.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.58.0...v5.59.0
|
|
6831
6865
|
[5.58.0]: https://github.com/ASDAlexey/vitest-auto-spy/compare/v5.57.0...v5.58.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) |
|
|
@@ -3596,6 +3597,7 @@ Rolling it out on a large suite, and the options each rule takes:
|
|
|
3596
3597
|
| [`no-instance-lifecycle-spy`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-instance-lifecycle-spy) | `warn` | — | Spy on an Angular lifecycle hook through the class prototype — Angular never calls the one on an instance |
|
|
3597
3598
|
| [`no-ts-expect-error-on-double`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-ts-expect-error-on-double) | `error` | — | Do not suppress a type error on a double's configuration — name the overload, or fix the fixture |
|
|
3598
3599
|
| [`no-constant-expect`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-constant-expect) | `error` | — | Assert on a value the code under test produced, not on one the spec spelled out |
|
|
3600
|
+
| [`no-redundant-as-instance`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-redundant-as-instance) | `warn` | `--fix` | Drop asInstance() around a comparing matcher's argument — the unwrap buys nothing there and keeps an import alive |
|
|
3599
3601
|
| [`no-redundant-smoke-test`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-redundant-smoke-test) | `error` | suggest | Delete a test whose whole body asserts that the subject exists, where the block already has tests that use it |
|
|
3600
3602
|
| [`no-self-called-spy`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-self-called-spy) | `error` | — | Do not assert a call the test made itself — drive the production path that should make it |
|
|
3601
3603
|
| [`no-vacuous-absence-assertion`](https://asdalexey.github.io/vitest-auto-spy/utilities/eslint-rules#no-vacuous-absence-assertion) | `error` | — | Do not let a whole test rest on assertions that a stream which never emits already satisfies |
|
package/agent-docs/angular.md
CHANGED
|
@@ -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
|
package/agent-docs/eslint.md
CHANGED
|
@@ -91,6 +91,7 @@ export default [
|
|
|
91
91
|
| `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(instance, 'ngOnInit')` (and `ngOnDestroy`, `ngDoCheck`, `ngAfterContent*`, `ngAfterView*`) — a view calls the hook read off the prototype, so the instance spy is never called and its stub never runs → `vi.spyOn(Cls.prototype, …)` before `createComponent`, or assert the effect. `ngOnChanges` is exempt: Angular calls it through the instance |
|
|
92
92
|
| `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `returnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature → an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`; otherwise the fixture is the wrong shape, checked against `ReturnType<X['m']>`. A reason after the directive does not silence it; a deliberate out-of-type value keeps it under `eslint-disable-next-line … -- <why>` |
|
|
93
93
|
| `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer it already fixes (`toBe` / `toEqual` / `toStrictEqual` against a literal; `toBeTruthy`, `toBeDefined`, `toBeNull`… for any literal) → assert on what the code produced, or `expect.fail(…)` for an unreachable branch |
|
|
94
|
+
| `no-redundant-as-instance` | `warn` | `--fix` | `expect(x).toBe(asInstance(ref))` → `.toBe(ref)`: the comparing matchers take their argument as a free type parameter, so the bridge buys nothing there; `--fix` drops the wrapper and the import once it is unused. `toHaveBeenCalledWith(asInstance(cart))` is left alone — that slot is typed against the real class and the unwrap can be load-bearing; so is a wrapper that is part of the argument (`[asInstance(x)]`, `asInstance(x).id`) |
|
|
94
95
|
| `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — every statement of the body an `expect(x)` under `toBeTruthy` / `toBeDefined` / `toBeInstanceOf` (or their negated twins), weighed against the tests that run the same setup: the rest of the block, and everything the blocks nested in it declare → delete it; the suggestion removes the test and the blank line above it. Silent where that test is the block's only running one, and a skipped sibling does not count as proof |
|
|
95
96
|
| `no-self-called-spy` | `error` | — | `vi.spyOn(obj, 'm')`, then `obj.m(…)` written by the test itself, then a positive `toHaveBeenCalled*` — the assertion is satisfied by the test's own line, so the test survives the deletion of the behaviour its title names. Order decides it: a call written **before** the spy is arrangement, and a `mockClear` between the call and the assertion, a negated matcher or arguments the call did not pass all silence it |
|
|
96
97
|
| `no-vacuous-absence-assertion` | `error` | — | a test **all** of whose assertions are satisfied by the stream never emitting. The carrier is a `const` / `let` declared in the test whose every write outside the declaration sits in a `subscribe` callback of that test (the assignment and the `push` forms both count), or a `vi.fn()` the test hands to `subscribe` and never calls itself. Silence satisfies an equality matcher repeating the initialiser's source text, `toBeUndefined` / `not.toBeDefined` on a `let` holding `undefined`, `toBeNull` on one holding `null`, `toBeFalsy` / `not.toBeTruthy` on any falsy literal, `toHaveLength(0)` on `[]` or `''`, and `not.toHaveBeenCalled` / `not.toHaveBeenCalledWith` / `toHaveBeenCalledTimes(0)` on any subject — read literally, so `toBeNull()` on a `let` with no initialiser is **not** reported. One `expect()` a silent source could fail silences the rule, which is what exempts the "nothing yet, trigger, now the value" shape; a test asserting through a local helper is skipped entirely → `await expectNoEmission(source$)`, or `expect(await expectEmission(source$))` |
|
|
@@ -111,22 +112,24 @@ export default [
|
|
|
111
112
|
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
112
113
|
| `prefer-native-spy-api` | `error` | `--fix` / suggest | `.and` / `.calls` where the spy's own API says the same thing — turn it on for the last mile off the jasmine shim. The fix renames only the member (`and.returnValue` → `mockReturnValue`, `withArgs` → `calledWith`), so comments stay; a rewrite that would drop one is a suggestion |
|
|
113
114
|
|
|
114
|
-
Fifty-
|
|
115
|
+
Fifty-eight rules, **every one an `error` except `prefer-render-shallow`,
|
|
115
116
|
`no-stub-class-double`, `no-structural-double`, `prefer-create-mock`, `no-instance-lifecycle-spy`,
|
|
116
117
|
`prefer-set-inputs`, `no-unasserted-argument`, `prefer-spy-on-own-method`, `prefer-to-have-signal-value`,
|
|
117
|
-
`no-unasserted-console-spy`, `no-real-wait-in-test
|
|
118
|
+
`no-unasserted-console-spy`, `no-real-wait-in-test`, `no-inline-test-data` and
|
|
119
|
+
`no-redundant-as-instance`**; nine fix on their own, nineteen offer suggestions. Fifty-four are syntactic; `no-private-member-access`, `no-mistyped-use-value`,
|
|
118
120
|
`no-unknown-use-value-key` and `prefer-to-have-signal-value` read types, and all four report nothing at all without `parserOptions.project` / `projectService`
|
|
119
121
|
rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
|
|
120
122
|
builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it;
|
|
121
123
|
`no-relative-mock-under-builder` waits for a builder target serving the file, or `{ builder: 'unit-test' }`.
|
|
122
124
|
|
|
123
|
-
The
|
|
125
|
+
The thirteen `warn`s do not rank findings by importance. Each names something other than a defect: a
|
|
124
126
|
cheaper render (`prefer-render-shallow`), a heuristic reading with no `provide:` to settle it
|
|
125
127
|
(`no-stub-class-double`, `no-structural-double`, `no-instance-lifecycle-spy`), a repair that turns
|
|
126
128
|
many files red at once (`prefer-create-mock`), a question only the author can answer
|
|
127
129
|
(`no-unasserted-argument`, `no-unasserted-console-spy`, `no-real-wait-in-test`), a place data should
|
|
128
130
|
live rather than a defect (`no-inline-test-data`), or a preferred spelling
|
|
129
|
-
of working code (`prefer-spy-on-own-method`, `prefer-set-inputs`, `prefer-to-have-signal-value
|
|
131
|
+
of working code (`prefer-spy-on-own-method`, `prefer-set-inputs`, `prefer-to-have-signal-value`,
|
|
132
|
+
`no-redundant-as-instance`). Set
|
|
130
133
|
any of the `warn`s to `'error'` once the batch is done.
|
|
131
134
|
|
|
132
135
|
Three rules can report on a _correct_ project, and only one has an option:
|
package/agent-docs/waiting.md
CHANGED
|
@@ -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
|
|
34
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/angular-doubles.js
CHANGED
|
@@ -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.
|
|
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",
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -480,13 +480,13 @@ function runnerBehind(callee) {
|
|
|
480
480
|
}
|
|
481
481
|
return isCallExpression(callee) ? runnerBehind(callee.callee) : void 0;
|
|
482
482
|
}
|
|
483
|
-
function
|
|
483
|
+
function enclosingRunner(node) {
|
|
484
484
|
const callback = enclosingFunction(node);
|
|
485
485
|
if (!callback) {
|
|
486
|
-
return
|
|
486
|
+
return void 0;
|
|
487
487
|
}
|
|
488
488
|
const call = callback.parent;
|
|
489
|
-
return isCallExpression(call) && call.arguments.includes(callback)
|
|
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\`
|
|
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
|
-
|
|
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";
|
|
@@ -632,7 +701,7 @@ function repairFor(node, subscribeCall) {
|
|
|
632
701
|
var TESTS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
633
702
|
var NEVER_CALLED = /* @__PURE__ */ new Set(["toHaveBeenCalled", "toHaveBeenCalledWith"]);
|
|
634
703
|
var UNDEFINED_MATCHERS = /* @__PURE__ */ new Set(["toBeDefined", "toBeUndefined"]);
|
|
635
|
-
var
|
|
704
|
+
var EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
636
705
|
var FALSY = /* @__PURE__ */ new Set(["0", "''", '""', "``", "NaN", "false", "null", "undefined"]);
|
|
637
706
|
var EMPTY = /* @__PURE__ */ new Set(["[]", "''", '""', "``"]);
|
|
638
707
|
function testCallOf(fn) {
|
|
@@ -697,7 +766,7 @@ function satisfiedByHeldValue(context, held, matcher) {
|
|
|
697
766
|
if (matcher.negated) {
|
|
698
767
|
return UNDEFINED_MATCHERS.has(matcher.name) ? held === "undefined" : matcher.name === "toBeTruthy" && FALSY.has(held);
|
|
699
768
|
}
|
|
700
|
-
if (
|
|
769
|
+
if (EQUALITY_MATCHERS.has(matcher.name)) {
|
|
701
770
|
return matcher.args.length === 1 && argument !== void 0 && tight(context.sourceCode.getText(argument)) === tight(held);
|
|
702
771
|
}
|
|
703
772
|
if (matcher.name === "toHaveLength") {
|
|
@@ -1724,7 +1793,6 @@ var noUnassertedConsoleSpy = defineRule({
|
|
|
1724
1793
|
});
|
|
1725
1794
|
|
|
1726
1795
|
// src/lib/eslint/constant-expect.ts
|
|
1727
|
-
var COMPARES = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
1728
1796
|
var CLASSIFIES = /* @__PURE__ */ new Set(["toBeDefined", "toBeFalsy", "toBeNaN", "toBeNull", "toBeTruthy", "toBeUndefined"]);
|
|
1729
1797
|
var OBJECT_LITERALS = /* @__PURE__ */ new Set([
|
|
1730
1798
|
"ArrayExpression",
|
|
@@ -1758,17 +1826,8 @@ function isConstant(node) {
|
|
|
1758
1826
|
return LITERALS.has(value.type);
|
|
1759
1827
|
}
|
|
1760
1828
|
}
|
|
1761
|
-
function matcherOf2(expectCall) {
|
|
1762
|
-
let member = expectCall.parent;
|
|
1763
|
-
while (memberName(member) === "not") {
|
|
1764
|
-
member = member.parent;
|
|
1765
|
-
}
|
|
1766
|
-
const name = memberName(member);
|
|
1767
|
-
const call = member.parent;
|
|
1768
|
-
return name && isCallExpression(call) && call.callee === member ? { name, args: call.arguments } : void 0;
|
|
1769
|
-
}
|
|
1770
1829
|
function isDecided(actual, matcher) {
|
|
1771
|
-
if (
|
|
1830
|
+
if (EQUALITY_MATCHERS.has(matcher.name)) {
|
|
1772
1831
|
return isConstant(actual) && matcher.args.every(isConstant);
|
|
1773
1832
|
}
|
|
1774
1833
|
return CLASSIFIES.has(matcher.name) && (OBJECT_LITERALS.has(unwrapped(actual).type) || isConstant(actual));
|
|
@@ -1782,7 +1841,7 @@ var noConstantExpect = defineRule({
|
|
|
1782
1841
|
create: (context) => ({
|
|
1783
1842
|
'CallExpression[callee.name="expect"]': (node) => {
|
|
1784
1843
|
const [actual] = node.arguments;
|
|
1785
|
-
const matcher =
|
|
1844
|
+
const matcher = matcherOf(node);
|
|
1786
1845
|
if (actual && matcher && isDecided(actual, matcher)) {
|
|
1787
1846
|
context.report({ node, messageId: "noConstantExpect", data: { actual: excerpt(context, actual, 40), matcher: matcher.name } });
|
|
1788
1847
|
}
|
|
@@ -3443,6 +3502,55 @@ var noInstanceLifecycleSpy = defineRule({
|
|
|
3443
3502
|
})
|
|
3444
3503
|
});
|
|
3445
3504
|
|
|
3505
|
+
// src/lib/eslint/matcher-as-instance.ts
|
|
3506
|
+
var AS_INSTANCE = "asInstance";
|
|
3507
|
+
function importedAsInstance(context, callee) {
|
|
3508
|
+
const binding = findBinding(context.sourceCode.getScope(callee), callee.name);
|
|
3509
|
+
return binding?.defs.some((definition) => definition.type === "ImportBinding") ? binding : void 0;
|
|
3510
|
+
}
|
|
3511
|
+
function unwrap2(context, fixer, wrapper, value, binding) {
|
|
3512
|
+
const edits = [fixer.replaceText(wrapper, context.sourceCode.getText(value))];
|
|
3513
|
+
const orphaned = binding.references.length === 1 ? dropNamedImport(context.sourceCode, fixer, binding) : void 0;
|
|
3514
|
+
if (orphaned) {
|
|
3515
|
+
edits.push(orphaned);
|
|
3516
|
+
}
|
|
3517
|
+
return edits;
|
|
3518
|
+
}
|
|
3519
|
+
var noRedundantAsInstance = defineRule({
|
|
3520
|
+
name: "no-redundant-as-instance",
|
|
3521
|
+
description: "Pass the value itself to a comparing matcher; asInstance() bridges slots typed as the real class",
|
|
3522
|
+
fixable: true,
|
|
3523
|
+
messages: {
|
|
3524
|
+
noRedundantAsInstance: "`asInstance({{value}})` buys nothing under `.{{matcher}}()`: the matcher takes its argument as a free type parameter, so the value compiles as it is. Pass `{{value}}` directly."
|
|
3525
|
+
},
|
|
3526
|
+
create: (context) => ({
|
|
3527
|
+
'CallExpression[callee.name="expect"]': (node) => {
|
|
3528
|
+
const matcher = matcherOf(node);
|
|
3529
|
+
if (!matcher || !EQUALITY_MATCHERS.has(matcher.name)) {
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
for (const argument of matcher.args) {
|
|
3533
|
+
if (!isCallExpression(argument) || !isIdentifier(argument.callee) || argument.callee.name !== AS_INSTANCE) {
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
const [value, ...extra] = argument.arguments;
|
|
3537
|
+
if (value === void 0 || extra.length > 0) {
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
const binding = importedAsInstance(context, argument.callee);
|
|
3541
|
+
if (binding) {
|
|
3542
|
+
context.report({
|
|
3543
|
+
node: argument,
|
|
3544
|
+
messageId: "noRedundantAsInstance",
|
|
3545
|
+
data: { matcher: matcher.name, value: excerpt(context, value, 40) },
|
|
3546
|
+
fix: (fixer) => unwrap2(context, fixer, argument, value, binding)
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
})
|
|
3552
|
+
});
|
|
3553
|
+
|
|
3446
3554
|
// src/lib/eslint/use-value-types.ts
|
|
3447
3555
|
var PRIMITIVE_CHECKER_METHODS = [
|
|
3448
3556
|
"getTypeAtLocation",
|
|
@@ -5101,7 +5209,6 @@ var preferRenderShallow = defineRule({
|
|
|
5101
5209
|
});
|
|
5102
5210
|
|
|
5103
5211
|
// src/lib/eslint/prefer-to-have-signal-value.ts
|
|
5104
|
-
var MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
5105
5212
|
var VALUE_MATCHERS = /* @__PURE__ */ new Map([
|
|
5106
5213
|
["toBeNull", "null"],
|
|
5107
5214
|
["toBeUndefined", "undefined"]
|
|
@@ -5110,7 +5217,7 @@ var SIGNAL_BRAND = "\u0275SIGNAL";
|
|
|
5110
5217
|
var SIGNAL_BRAND_PROPERTY = /^__@(?:SIGNAL|ɵSIGNAL|ɵWRITABLE_SIGNAL)@\d+$/;
|
|
5111
5218
|
function signalAssertionOf(node) {
|
|
5112
5219
|
const callee = node.callee;
|
|
5113
|
-
if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property) || !(
|
|
5220
|
+
if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property) || !(EQUALITY_MATCHERS.has(callee.property.name) || VALUE_MATCHERS.has(callee.property.name))) {
|
|
5114
5221
|
return void 0;
|
|
5115
5222
|
}
|
|
5116
5223
|
let expected = callee.object;
|
|
@@ -6537,7 +6644,7 @@ function isObjectPattern(node) {
|
|
|
6537
6644
|
function isUnwrapped(node) {
|
|
6538
6645
|
return isCast(node) || node.type === "TSNonNullExpression";
|
|
6539
6646
|
}
|
|
6540
|
-
function
|
|
6647
|
+
function unwrap3(node) {
|
|
6541
6648
|
return isUnwrapped(node) ? node.expression : node;
|
|
6542
6649
|
}
|
|
6543
6650
|
function methodString(node) {
|
|
@@ -6545,7 +6652,7 @@ function methodString(node) {
|
|
|
6545
6652
|
return typeof value === "string" && METHOD_NAME.test(value) ? value : void 0;
|
|
6546
6653
|
}
|
|
6547
6654
|
function listedMethod(value) {
|
|
6548
|
-
const list =
|
|
6655
|
+
const list = unwrap3(value);
|
|
6549
6656
|
const elements = isArrayExpression(list) ? list.elements : [];
|
|
6550
6657
|
const names = elements.filter((element) => element !== null).map(methodString);
|
|
6551
6658
|
return elements.length === 1 ? names[0] : void 0;
|
|
@@ -6586,7 +6693,7 @@ function readsMethod(member, method) {
|
|
|
6586
6693
|
return named === method && !(isAssignmentExpression(parent) && parent.left === member);
|
|
6587
6694
|
}
|
|
6588
6695
|
function isRealObject(scope, node, follow) {
|
|
6589
|
-
const value =
|
|
6696
|
+
const value = unwrap3(node);
|
|
6590
6697
|
if (isIdentifier(value)) {
|
|
6591
6698
|
const bound = follow ? boundValueOf(scope, value) : void 0;
|
|
6592
6699
|
return bound ? isRealObject(scope, bound, false) : DOM_GLOBALS.has(value.name) && bindingState(scope, value.name) === "free";
|
|
@@ -7049,6 +7156,7 @@ var rules = {
|
|
|
7049
7156
|
"no-mistyped-use-value": noMistypedUseValue,
|
|
7050
7157
|
"no-unknown-use-value-key": noUnknownUseValueKey,
|
|
7051
7158
|
"no-instance-lifecycle-spy": noInstanceLifecycleSpy,
|
|
7159
|
+
"no-redundant-as-instance": noRedundantAsInstance,
|
|
7052
7160
|
"no-ts-expect-error-on-double": noTsExpectErrorOnDouble,
|
|
7053
7161
|
"no-constant-expect": noConstantExpect,
|
|
7054
7162
|
"no-compile-components": noCompileComponents,
|
|
@@ -7127,7 +7235,8 @@ var recommendedRules = {
|
|
|
7127
7235
|
[`${PLUGIN_NAME}/no-relative-mock-under-builder`]: "error",
|
|
7128
7236
|
[`${PLUGIN_NAME}/no-real-wait-in-test`]: "warn",
|
|
7129
7237
|
[`${PLUGIN_NAME}/no-disabled-testbed-teardown`]: "error",
|
|
7130
|
-
[`${PLUGIN_NAME}/no-inline-test-data`]: "warn"
|
|
7238
|
+
[`${PLUGIN_NAME}/no-inline-test-data`]: "warn",
|
|
7239
|
+
[`${PLUGIN_NAME}/no-redundant-as-instance`]: "warn"
|
|
7131
7240
|
};
|
|
7132
7241
|
var typeErrorRules = {
|
|
7133
7242
|
[`${PLUGIN_NAME}/prefer-as-spy`]: "error",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
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
|
|
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
|