vitest-auto-spy 5.9.0 → 5.10.1
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 +30 -13
- package/README.md +19 -8
- package/dist/angular-router.d.ts +20 -5
- package/dist/angular-router.js +72 -15
- package/dist/angular.d.ts +21 -2
- package/dist/bun-angular.d.ts +1 -1
- package/dist/cli.js +23 -8
- package/dist/eslint-plugin.cjs +219 -5
- package/dist/{track-signal-runs-67EcdZwp.d.ts → track-signal-runs-BoA6j-74.d.ts} +12 -2
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +8 -3
package/AGENTS.md
CHANGED
|
@@ -2345,27 +2345,41 @@ converged on, and every part of it is a guess the component falls through: `of()
|
|
|
2345
2345
|
`url` is a string nobody updates, `routerState` is absent, `serializeUrl` throws on the first
|
|
2346
2346
|
redirect a guard builds. `provideRouterDouble(init)` keeps one URL and derives the rest.
|
|
2347
2347
|
|
|
2348
|
-
| Call | Does
|
|
2349
|
-
| ------------------------------- |
|
|
2350
|
-
| `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'`
|
|
2351
|
-
| `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)`
|
|
2352
|
-
| `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)`
|
|
2348
|
+
| Call | Does |
|
|
2349
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
2350
|
+
| `provideRouterDouble(init?)` | a `FactoryProvider` for the `Router` token — a fresh router per injector; `init` is `{ url }`, default `'/'` |
|
|
2351
|
+
| `injectRouterDouble(injector?)` | the handle: `.router`, `.navigate`, `.navigateByUrl`, `setUrl(url)`, `emitNavigation(event?)`, `setCurrentNavigation(navigation?)` |
|
|
2352
|
+
| `createRouterDouble(init?)` | the same handle without a `TestBed` — for `new AuthGuard(router)` |
|
|
2353
2353
|
|
|
2354
2354
|
Unlike the route, this is **not** an instance of Angular's class: a real `Router` drags the whole
|
|
2355
2355
|
routing stack in. It is a structural double that answers `url` (serialized as the real router does),
|
|
2356
2356
|
`events` (a `BehaviorSubject` starting at the `NavigationEnd` that put it there), `navigate` and
|
|
2357
2357
|
`navigateByUrl` (spies resolving `true`), `serializeUrl` / `parseUrl` / `createUrlTree` (the
|
|
2358
|
-
router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`)
|
|
2359
|
-
(Angular's own, its `snapshot.url` the URL)
|
|
2360
|
-
|
|
2358
|
+
router's own URL work, via `DefaultUrlSerializer` and `createUrlTreeFromSnapshot`), `routerState`
|
|
2359
|
+
(Angular's own, its `snapshot.url` the URL) and `currentNavigation` / `getCurrentNavigation()` (the
|
|
2360
|
+
navigation in flight, `null` while the router stands still). **Every other member `Router` declares
|
|
2361
|
+
throws by name** rather than reading `undefined` — and that holds for the instance fields too, which
|
|
2362
|
+
is why a hand-rolled double needs `instanceMethodsToSpyOn: ['currentNavigation']` and this one does
|
|
2363
|
+
not.
|
|
2361
2364
|
|
|
2362
|
-
|
|
2365
|
+
Five things to know:
|
|
2363
2366
|
|
|
2364
2367
|
- **It does not navigate.** `navigate()` records the call and resolves `true`; it does not move
|
|
2365
2368
|
`url`. `setUrl()` and `emitNavigation()` move it; `RouterTestingHarness` over a real
|
|
2366
2369
|
`provideRouter()` is what tests a navigation.
|
|
2367
2370
|
- **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
|
|
2368
2371
|
`NavigationEnd` moves the URL with it; any other event does not.
|
|
2372
|
+
- **The navigation in flight follows the events, and a terminal event ends it _after_ delivering
|
|
2373
|
+
it.** `setCurrentNavigation({ extras: { state } })` puts one up and `setCurrentNavigation(null)`
|
|
2374
|
+
ends it; a `NavigationStart` pushed through `emitNavigation()` starts one with that event's id,
|
|
2375
|
+
URL and trigger, and a `NavigationEnd`, `NavigationCancel`, `NavigationError` or
|
|
2376
|
+
`NavigationSkipped` drops it back to `null` once its subscribers have run. Angular's doc comment
|
|
2377
|
+
— "the current navigation becomes to null after the NavigationEnd event is emitted" — means
|
|
2378
|
+
_after_ literally: the router emits the terminal event from a `tap` with the navigation still in
|
|
2379
|
+
flight and clears it in the `finalize` below, and `events` is a Subject, so a synchronous
|
|
2380
|
+
subscriber runs between the two. **A component that reads `currentNavigation()` while handling a
|
|
2381
|
+
`NavigationEnd` gets the navigation that just finished**, here and in production; it reads `null`
|
|
2382
|
+
only once `navigate()` has resolved. Probed against a real `provideRouter()` on Angular 22.
|
|
2369
2383
|
- **Ordering does not matter here.** `Router` is `providedIn: 'root'` and `provideRouter()` does not
|
|
2370
2384
|
re-provide the token, so the double wins in either order — and a `TestBed` without it hands out a
|
|
2371
2385
|
real router, which is what `injectRouterDouble()` says by name.
|
|
@@ -2465,7 +2479,9 @@ Five things to know:
|
|
|
2465
2479
|
`InjectionToken<Window>`. The helper is generic over the token's type, so an
|
|
2466
2480
|
`InjectionToken<AppWindow>` has that interface's members checked in the overrides too.
|
|
2467
2481
|
- **A plain `{ … }` merges, anything else replaces.** `{ screen: { width: 1920 } }` keeps
|
|
2468
|
-
`screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole.
|
|
2482
|
+
`screen.colorDepth`; a `vi.fn()`, an array, a `URL` or a stub instance becomes the member whole. The
|
|
2483
|
+
slice goes three levels down, the way the merge does — `{ document: { location: { href: '' } } }` is
|
|
2484
|
+
checked and merged member by member.
|
|
2469
2485
|
- **Nothing to restore.** The globals are never patched — the double is a view over them, and every
|
|
2470
2486
|
write and delete lands on the view, which is also how a spec moves a value mid-test:
|
|
2471
2487
|
`Object.assign(TestBed.inject(WINDOW), { scrollY: 40 })` (`Object.assign` because lib.dom declares
|
|
@@ -3127,6 +3143,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
3127
3143
|
| `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 |
|
|
3128
3144
|
| `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>` |
|
|
3129
3145
|
| `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 |
|
|
3146
|
+
| `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 |
|
|
3130
3147
|
| `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled → delete it, and the `async` of a hook that awaits nothing else. **Silent until** `['error', { builder: 'inline-resources' }]`: under a JIT setup that loads resources at run time the call is load-bearing. **Keep the call for a component whose template holds a `@defer` block** — that ships async class metadata, which `TestBed` resolves in this very call whatever the builder did, and dropping it fails the test with `has unresolved metadata`; the rule cannot see another file's template, so such a call keeps its own `// eslint-disable-next-line vitest-auto-spy/no-compile-components -- @defer: async class metadata` |
|
|
3131
3148
|
| `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`, through `TestBed`, `getTestBed()`, a chain of those, or a name holding one → drop the `await`, and the `async` of a hook that then awaits nothing else; the suggestion does both. Reads **no types**, so it reports without `parserOptions.project`, where `@typescript-eslint/await-thenable` cannot. `inject` and `runInInjectionContext` are never reported: each answers whatever the token or the callback holds, which can be a promise |
|
|
3132
3149
|
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
|
|
@@ -3134,8 +3151,8 @@ blanket downgrade so those keep their severity; do not copy the two names into a
|
|
|
3134
3151
|
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
3135
3152
|
| `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 |
|
|
3136
3153
|
|
|
3137
|
-
Thirty-
|
|
3138
|
-
`no-stub-class-double`, `no-structural-double` and `no-instance-lifecycle-spy`**; three fix on their own,
|
|
3154
|
+
Thirty-seven rules, **every one an `error` since 4.0.0 except `prefer-render-shallow`,
|
|
3155
|
+
`no-stub-class-double`, `no-structural-double` and `no-instance-lifecycle-spy`**; three fix on their own, twelve offer suggestions. Thirty-four are syntactic; `no-private-member-access`, `no-mistyped-use-value` and
|
|
3139
3156
|
`no-unknown-use-value-key` read types, and all three report nothing at all without `parserOptions.project` / `projectService`
|
|
3140
3157
|
rather than guessing. `no-compile-components` waits the same way for a fact no file holds — which
|
|
3141
3158
|
builder the project has — and reports nothing until `{ builder: 'inline-resources' }` states it. The config used to be a graded mix of `error` / `warn` / `off`, which decided for the
|
|
@@ -3408,7 +3425,7 @@ packages, which a subpath export can never be.
|
|
|
3408
3425
|
| `injectActivatedRoute(): nothing provides ActivatedRoute here` | no `ActivatedRoute` provider in the injector it read | `provideActivatedRoute({ … })` from `vitest-auto-spy/angular-router` in `providers`; `injectActivatedRoute(fixture.debugElement.injector)` for a component-level one (§13) |
|
|
3409
3426
|
| `the ActivatedRoute here is … not one provideActivatedRoute() built` | a later provider of `ActivatedRoute` won — `provideRouter()`, `RouterModule`, a `useValue`, `provideAutoSpy(ActivatedRoute)` | list `provideActivatedRoute()` last, or drop the other provider (§13) |
|
|
3410
3427
|
| `provideActivatedRoute: the installed @angular/router does not wire ActivatedRoute …` | a router major builds `ActivatedRoute` with different internal constructors | report it with the `@angular/router` version; provide the route by hand until then (§13) |
|
|
3411
|
-
| `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `
|
|
3428
|
+
| `provideRouterDouble: the Router double has no …` | the code under test reached for a `Router` member the double does not cover — `isActive`, `resetConfig`, `lastSuccessfulNavigation` | the double covers `url`, `events`, `navigate`, `navigateByUrl`, `createUrlTree`, `serializeUrl`, `parseUrl`, `routerState` and `currentNavigation` / `getCurrentNavigation()`; past that, a real `provideRouter([])` with `RouterTestingHarness` (§13) |
|
|
3412
3429
|
| `injectRouterDouble(): the Router here is Angular's own …` | `Router` is `providedIn: 'root'`, so a `TestBed` without the double hands out a real one | add `provideRouterDouble({ … })` from `vitest-auto-spy/angular-router` to `providers` (§13) |
|
|
3413
3430
|
|
|
3414
3431
|
**Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
|
|
|
65
65
|
`mock.reset()`, so a `mock.fn()` your spec made by hand is untouched.
|
|
66
66
|
- 🌐 `httpResource()` and `HttpClient` in two lines — `provideHttpTesting()` + `await expectRequest(url).flush(body)` does the tick, the controller, the `expectOne`, the flush **and the settling**, so `resource.value()` reads on the next line; `@angular/common` stays an optional peer, confined to the `/angular-http` entry
|
|
67
67
|
- 🧭 An `ActivatedRoute` that cannot disagree with itself — `provideActivatedRoute({ params, queryParams, data })` provides Angular's own route, its streams, `paramMap`s and snapshot built from one record, and `injectActivatedRoute().setParams(…)` moves them together the way a navigation does; `@angular/router` stays an optional peer, confined to the `/angular-router` entry
|
|
68
|
-
- 🧱 The providers every suite hand-rolls, provided once — `provideRouterDouble({ url })` for a `Router` that derives `url`, `routerState` and `
|
|
68
|
+
- 🧱 The providers every suite hand-rolls, provided once — `provideRouterDouble({ url })` for a `Router` that derives `url`, `routerState`, `events` and `currentNavigation()` from one URL rather than four guesses; `provideWindowDouble(WINDOW, { screen })` and `provideDocumentDouble({ … })`, which merge what the spec names **over** the real jsdom object, so the member nobody thought of still answers; `provideMatDialogData(MAT_DIALOG_DATA, data)` and `provideMatDialogRef(MatDialogRef)` for the Material dialog trio, with Material itself staying out of this package's dependencies
|
|
69
69
|
- 🧪 A signal graph you can assert on — `mockSignalProp` writes **through** a member that is already a `signal()`, `model()` or `linkedSignal()` instead of replacing it, so a computed, an effect or a template that read it first is still connected; `setInputs(fixture, { … })` changes an input mid-test; `trackRecomputations` / `trackEffectRuns` count what actually re-ran; `runEffect` runs the previous cleanup first, the way Angular's own scheduler does
|
|
70
70
|
- 🪆 NestJS units from their own DI metadata — `createNestUnit(Target, { expose, providers })`, the solitary / sociable model of `@suites/unit` with the real prototype behind every spy and no `@nestjs` dependency
|
|
71
71
|
- 🧱 The providers a testing module cannot reach — `overrideComponentProvider` (which verifies the override actually applied), `provideAutoSpyForToken`, `assertNgModuleScopes`, `assertComponentDefIntact`, `createDirectiveHost`, and `createComponentStub` for a child whose selector and inputs are read off the real one, so the stub cannot drift
|
|
@@ -79,7 +79,7 @@ faster at suite scale ([benchmarks](#benchmarks)) — and for
|
|
|
79
79
|
- 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
|
|
80
80
|
- 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
|
|
81
81
|
- 🚚 A migration you can verify — `vitest-auto-spy/diagnostics`: `compareTestRuns` on the two JSON reports, `summarizeTestRun` / `formatTestRunComparison` to read the answer, `diffByField` for the assertion the reporter collapses, `explainSpy` for a double that answered something you did not configure
|
|
82
|
-
- 📏 Lint rules and one-line test-run hygiene — thirty-
|
|
82
|
+
- 📏 Lint rules and one-line test-run hygiene — thirty-seven rules in `vitest-auto-spy/eslint-plugin` (three `--fix`, twelve suggestions, four of them for a suite mid-migration off jasmine), `setupAutoSpy()` — with `preset: 'strict'` for every guard at its strictest grade
|
|
83
83
|
- 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
|
|
84
84
|
- 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-perf-codemod-and-init) — suite-level defects **that never fail a run**: a `tsconfig` `include` matching no file, a production module importing a spec, a `@jest-environment` pragma the runner never reads, config left behind for a runner that is gone. Read-only, no config, exits 1 in CI
|
|
85
85
|
- ⏱️ [`npx vitest-auto-spy perf`](#perf--where-the-cpu-time-actually-goes) — where a suite's CPU time actually goes, phase by phase, and which spec files to act on: the ones that reach no DOM and could run under `node`, the ones that import a barrel. Runs Vitest once with a reporter this package ships, reads `TestModule.diagnostic()`, names files, states the rule behind each finding
|
|
@@ -2856,7 +2856,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
2856
2856
|
| `runEffect(effectRef)` | `/angular` | Run one `effect()` body on demand, for an effect whose trigger a spec replaced with a static signal |
|
|
2857
2857
|
| `setInputs(fixture, inputs, opts?)` | `/angular` | Change a component input mid-test — one `setInput` per name, checked against the compiled definition, then one wait |
|
|
2858
2858
|
| `trackRecomputations(signal)` / `trackEffectRuns(ref)` | `/angular` | Count what the reactive graph actually did: `{ count, stop() }`, so "the filter change did not re-run the sync effect" is an assertion |
|
|
2859
|
-
| `provideRouterDouble(init?)` / `injectRouterDouble()` | `/angular-router` | A `Router` derived from one URL — real `serializeUrl` / `createUrlTree`, `navigate` spies, `emitNavigation()` over a `BehaviorSubject`
|
|
2859
|
+
| `provideRouterDouble(init?)` / `injectRouterDouble()` | `/angular-router` | A `Router` derived from one URL — real `serializeUrl` / `createUrlTree`, `navigate` spies, `emitNavigation()` over a `BehaviorSubject`, `currentNavigation()` and `setCurrentNavigation()` |
|
|
2860
2860
|
| `createForm(model, schema?)` / `registerFormMatchers()` | `/signal-forms` | A signal form built where `form()` can inject, and `expect(field).toHaveFieldErrors(['required'])` over what it produced |
|
|
2861
2861
|
| `provideWindowDouble(TOKEN, over?)` / `provideDocumentDouble(over?)` | `/angular` | A `window` / `document` merged **over** the real jsdom one, so members the spec never named still answer; the globals stay untouched |
|
|
2862
2862
|
| `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(Ref, init?)` | `/angular` | The Material dialog trio without `@angular/material` as a dependency — the token and the ref class are arguments, `afterClosed()` still answers after the close |
|
|
@@ -3424,6 +3424,7 @@ export can never be.
|
|
|
3424
3424
|
| `no-instance-lifecycle-spy` | `warn` | — | `vi.spyOn(component, 'ngOnInit')` and the other hooks a view reads off the prototype — Angular never calls the instance spy → spy on `Cls.prototype` before the component is created, or assert the hook's effect |
|
|
3425
3425
|
| `no-ts-expect-error-on-double` | `error` | — | `@ts-expect-error` / `@ts-ignore` above `nextWith`, `resolveWith`, `mockReturnValue`, `calledWith(…)` and the other helpers that check a stub against the method's signature — an overloaded method takes `Spy<X, { overload: { m: 'first' } }>`, anything else is a fixture of the wrong shape |
|
|
3426
3426
|
| `no-constant-expect` | `error` | — | `expect(true).toBe(true)`, `expect({ … }).toBeDefined()` — a value the spec spelled out, under a matcher whose answer that value already fixes → assert on what the code produced, or `expect.fail(…)` for a branch the test must not reach |
|
|
3427
|
+
| `no-redundant-smoke-test` | `error` | suggest | `it('should create', () => expect(pipe).toBeTruthy())` — a test whose whole body asks whether the subject is there, in a block whose other tests run the same `beforeEach` and would fail first, naming what they were doing → delete it; the suggestion does. A block whose only running test is that one is left alone |
|
|
3427
3428
|
| `no-compile-components` | `error` | suggest | `compileComponents()` under a builder that inlines `templateUrl` / `styleUrls` — a promise already settled; **silent** until `{ builder: 'inline-resources' }` says so, because a JIT setup that loads resources at run time needs the call. One exception the rule cannot see: a component whose template holds a `@defer` block ships async class metadata, so the call stays load-bearing there |
|
|
3428
3429
|
| `no-sync-testbed-await` | `error` | suggest | `await` on a TestBed call that answers the TestBed or a fixture — `configureTestingModule`, every `override*`, `resetTestingModule`, `createComponent`, `getLastFixture`; none of them is a promise, so the `await` waits for nothing and the `async` it forced on the hook awaits nothing either. The suggestion drops both. Reads no types, so it reports without `parserOptions.project` |
|
|
3429
3430
|
| `jasmine-namespace-without-entry` | `error` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compatibility layer nowhere |
|
|
@@ -3435,7 +3436,7 @@ Every message ends with a link to the matching [recipe](#how-to-mock): a rule th
|
|
|
3435
3436
|
"don't" moves the problem rather than solving it. Rules travel with the API they recommend, so they
|
|
3436
3437
|
are versioned together and stop being re-written in every project that installs the package.
|
|
3437
3438
|
|
|
3438
|
-
**Three of the thirty-
|
|
3439
|
+
**Three of the thirty-seven fix on their own, twelve offer suggestions**, and the split is not about how hard
|
|
3439
3440
|
the rewrite is. `no-mocked-for-spy` touches a _declaration_: get it wrong and the file stops
|
|
3440
3441
|
compiling, which is the loudest, cheapest failure there is — so `--fix` rewrites the type, adds
|
|
3441
3442
|
`import type { Spy } from 'vitest-auto-spy'` and drops the `Mocked` import once nothing else uses
|
|
@@ -3518,7 +3519,17 @@ reason is for. The report sits on the directive's line, so that comment reaches
|
|
|
3518
3519
|
`no-constant-expect` reports `expect(true).toBe(true)` and its relatives: a value the spec spelled out,
|
|
3519
3520
|
under a matcher whose answer that value already fixes. `@vitest/eslint-plugin` has no such rule. On
|
|
3520
3521
|
the same suite it reports four — two tests with nothing else to assert and two that import a barrel
|
|
3521
|
-
for coverage.
|
|
3522
|
+
for coverage.
|
|
3523
|
+
|
|
3524
|
+
`no-redundant-smoke-test` reports the other test that cannot fail: one whose whole body is
|
|
3525
|
+
`expect(pipe).toBeTruthy()`, standing in a block that already has tests which run the same
|
|
3526
|
+
`beforeEach` — a nullish subject fails one of those first, on the call it was making, rather than on a
|
|
3527
|
+
line named after construction. It is weighed against the tests that share its setup, the ones nested
|
|
3528
|
+
`describe`s declare included, and it says nothing where that smoke test is the only running test of
|
|
3529
|
+
its block. Measured over an Angular suite of 1771 spec files: 569 findings in 540 of them, 515 still carrying the
|
|
3530
|
+
`should create` / `should be created` title `ng generate` wrote; on a second suite of 845 files, 97 in 87. The suggestion deletes the test together with the blank line above it.
|
|
3531
|
+
|
|
3532
|
+
`no-compile-components` says nothing until `['error', { builder: 'inline-resources' }]`
|
|
3522
3533
|
states that the builder inlines `templateUrl` / `styleUrls`; then `compileComponents()` is a promise
|
|
3523
3534
|
already settled, and a suggestion deletes it together with the `async` of a hook that awaits nothing
|
|
3524
3535
|
else — 449 calls in 411 files there, 435 of them with the edit. Not every one of them, though: a
|
|
@@ -3573,7 +3584,7 @@ package's own — it needs its ESLint integration switched on.
|
|
|
3573
3584
|
### WebStorm and the other JetBrains IDEs
|
|
3574
3585
|
|
|
3575
3586
|
No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
|
|
3576
|
-
all run ESLint natively, so the thirty-
|
|
3587
|
+
all run ESLint natively, so the thirty-seven rules appear inline, in the **Problems** tool window, and
|
|
3577
3588
|
under **Code → Inspect Code** for the whole project.
|
|
3578
3589
|
|
|
3579
3590
|
```js
|
|
@@ -3590,7 +3601,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
|
|
|
3590
3601
|
the fixes and suggestions live.
|
|
3591
3602
|
|
|
3592
3603
|
A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
|
|
3593
|
-
and then keep a second copy of thirty-
|
|
3604
|
+
and then keep a second copy of thirty-seven rules, in Kotlin, in step with the TypeScript ones.
|
|
3594
3605
|
|
|
3595
3606
|
### VS Code, Cursor, Windsurf, VSCodium
|
|
3596
3607
|
|
|
@@ -3700,7 +3711,7 @@ another, and the next one is in that same file.
|
|
|
3700
3711
|
| `expectNoRequest(matcher?, opts?)` / `verifyNoPendingRequests()` _(`/angular-http`)_ | Assert that nothing was requested / that the test left nothing unanswered |
|
|
3701
3712
|
| `provideActivatedRoute(init?)` / `injectActivatedRoute(injector?)` _(`/angular-router`)_ | An `ActivatedRoute` provider whose streams, `paramMap`s and snapshot share one record, and the handle whose setters move them together |
|
|
3702
3713
|
| `createActivatedRoute(init?)` _(`/angular-router`)_ | The same double without a `TestBed` — `.route` plus the setters |
|
|
3703
|
-
| `provideRouterDouble(init?)` / `injectRouterDouble(injector?)` / `createRouterDouble(init?)` _(`/angular-router`)_ | A `Router` derived from one URL — `navigate` spies, `setUrl`, `emitNavigation`, and the router's own URL serialization
|
|
3714
|
+
| `provideRouterDouble(init?)` / `injectRouterDouble(injector?)` / `createRouterDouble(init?)` _(`/angular-router`)_ | A `Router` derived from one URL — `navigate` spies, `setUrl`, `emitNavigation`, `setCurrentNavigation`, and the router's own URL serialization |
|
|
3704
3715
|
| `createForm(model, schema?, options?)` / `registerFormMatchers()` _(`/signal-forms`)_ | Angular's own signal `form()`, built in the `TestBed`'s injection context, and the `toHaveFieldErrors` matcher over its `errors()` |
|
|
3705
3716
|
| `registerSignalMatchers()` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)` |
|
|
3706
3717
|
| `trackRecomputations(signal)` / `trackEffectRuns(effectRef)` _(Angular)_ | Count what the reactive graph re-ran — `{ count, stop() }`, undone by `restoreMockedProps()` |
|
package/dist/angular-router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type, Injector, FactoryProvider } from '@angular/core';
|
|
2
|
-
import { Params, Data, UrlSegment, ActivatedRoute, Route, Router, Event } from '@angular/router';
|
|
2
|
+
import { Params, Data, UrlSegment, ActivatedRoute, Route, Navigation, Router, Event } from '@angular/router';
|
|
3
3
|
import { g as AddSpyMethodsByReturnTypes } from './types-D3RkJjc_.js';
|
|
4
4
|
import 'vitest';
|
|
5
5
|
|
|
@@ -114,8 +114,9 @@ declare function injectActivatedRoute(injector?: Injector): ActivatedRouteDouble
|
|
|
114
114
|
* So the double keeps one URL and derives the rest of it: `url` is that URL serialized the way the
|
|
115
115
|
* real router serializes it, `routerState` is Angular's own `RouterState` over a root route holding
|
|
116
116
|
* its query parameters and fragment, `events` is a `BehaviorSubject` that starts at the
|
|
117
|
-
* `NavigationEnd` which put the router there,
|
|
118
|
-
*
|
|
117
|
+
* `NavigationEnd` which put the router there, `currentNavigation()` is `null` because a router
|
|
118
|
+
* standing at a URL is idle, and `createUrlTree` / `serializeUrl` / `parseUrl` are the router's real
|
|
119
|
+
* URL work rather than stubs.
|
|
119
120
|
*
|
|
120
121
|
* Navigation itself stays a spy, because a navigation in an application is asynchronous, runs
|
|
121
122
|
* guards and can be cancelled: a double that moved its own URL on `navigate()` would be testing the
|
|
@@ -125,10 +126,18 @@ declare function injectActivatedRoute(injector?: Injector): ActivatedRouteDouble
|
|
|
125
126
|
* reason: `@angular/router` is an optional peer, paid for by the suites that import this entry.
|
|
126
127
|
*/
|
|
127
128
|
|
|
128
|
-
/**
|
|
129
|
+
/** What a spec names about the navigation in flight; every other field is derived from where the router stands. */
|
|
130
|
+
type NavigationInit = Partial<Navigation>;
|
|
131
|
+
/** Where the router starts: the URL it stands at, and whether a navigation is running. */
|
|
129
132
|
interface RouterDoubleInit {
|
|
130
133
|
/** The URL the router reports until the spec moves it. Serialized as the router would. Default `'/'`. */
|
|
131
134
|
url?: string;
|
|
135
|
+
/**
|
|
136
|
+
* The navigation `currentNavigation()` and `getCurrentNavigation()` answer with. Default `null` —
|
|
137
|
+
* a router standing at a URL is idle, which is what the real one answers between navigations.
|
|
138
|
+
* Named here rather than only on the handle because a component reads it in a field initializer.
|
|
139
|
+
*/
|
|
140
|
+
currentNavigation?: NavigationInit | null;
|
|
132
141
|
}
|
|
133
142
|
/** The handle a spec drives the router through. */
|
|
134
143
|
interface RouterDouble {
|
|
@@ -140,6 +149,12 @@ interface RouterDouble {
|
|
|
140
149
|
readonly navigateByUrl: AddSpyMethodsByReturnTypes<Router['navigateByUrl']>;
|
|
141
150
|
/** Put the router at a URL: `url`, `routerState` and the root route move together. */
|
|
142
151
|
setUrl(url: string): void;
|
|
152
|
+
/**
|
|
153
|
+
* Put a navigation in flight, or end it with `null`. What the spec names is kept; `id`,
|
|
154
|
+
* `initialUrl` and `extractedUrl` come from where the router stands, `trigger` is `'imperative'`
|
|
155
|
+
* and `extras` is empty.
|
|
156
|
+
*/
|
|
157
|
+
setCurrentNavigation(navigation?: NavigationInit | null): void;
|
|
143
158
|
/** Push an event through `router.events`. A `NavigationEnd`, or a URL, moves the URL with it. */
|
|
144
159
|
emitNavigation(event?: Event | string): void;
|
|
145
160
|
}
|
|
@@ -182,4 +197,4 @@ declare function provideRouterDouble(init?: RouterDoubleInit): FactoryProvider;
|
|
|
182
197
|
*/
|
|
183
198
|
declare function injectRouterDouble(injector?: Injector): RouterDouble;
|
|
184
199
|
|
|
185
|
-
export { type ActivatedRouteChange, type ActivatedRouteDouble, type ActivatedRouteInit, type RouterDouble, type RouterDoubleInit, createActivatedRoute, createRouterDouble, injectActivatedRoute, injectRouterDouble, provideActivatedRoute, provideRouterDouble };
|
|
200
|
+
export { type ActivatedRouteChange, type ActivatedRouteDouble, type ActivatedRouteInit, type NavigationInit, type RouterDouble, type RouterDoubleInit, createActivatedRoute, createRouterDouble, injectActivatedRoute, injectRouterDouble, provideActivatedRoute, provideRouterDouble };
|
package/dist/angular-router.js
CHANGED
|
@@ -2,8 +2,9 @@ import { createFunctionSpy } from './chunk-NQNHA3R4.js';
|
|
|
2
2
|
import './chunk-OTOCFH6B.js';
|
|
3
3
|
import { withDocs, DOCS_LINKS } from './chunk-Q477VHHF.js';
|
|
4
4
|
import { TestBed } from '@angular/core/testing';
|
|
5
|
-
import { Router, DefaultUrlSerializer, PRIMARY_OUTLET, ActivatedRoute, RouterState, NavigationEnd, ActivatedRouteSnapshot, RouterStateSnapshot,
|
|
5
|
+
import { Router, DefaultUrlSerializer, PRIMARY_OUTLET, ActivatedRoute, RouterState, NavigationEnd, ActivatedRouteSnapshot, RouterStateSnapshot, NavigationStart, NavigationCancel, NavigationError, NavigationSkipped, UrlSegment, createUrlTreeFromSnapshot } from '@angular/router';
|
|
6
6
|
import { BehaviorSubject } from 'rxjs';
|
|
7
|
+
import { signal } from '@angular/core';
|
|
7
8
|
|
|
8
9
|
var doubles = /* @__PURE__ */ new WeakMap();
|
|
9
10
|
function toSegments(url) {
|
|
@@ -182,8 +183,16 @@ function injectActivatedRoute(injector) {
|
|
|
182
183
|
}
|
|
183
184
|
return double;
|
|
184
185
|
}
|
|
185
|
-
var COVERED = "url, events, navigate, navigateByUrl, createUrlTree, serializeUrl, parseUrl and
|
|
186
|
-
var
|
|
186
|
+
var COVERED = "url, events, navigate, navigateByUrl, createUrlTree, serializeUrl, parseUrl, routerState, currentNavigation and getCurrentNavigation";
|
|
187
|
+
var DECLARED_FIELDS = [
|
|
188
|
+
"navigated",
|
|
189
|
+
"routeReuseStrategy",
|
|
190
|
+
"onSameUrlNavigation",
|
|
191
|
+
"config",
|
|
192
|
+
"componentInputBindingEnabled",
|
|
193
|
+
"currentNavigation"
|
|
194
|
+
];
|
|
195
|
+
var DECLARED = /* @__PURE__ */ new Set([...Object.getOwnPropertyNames(Router.prototype), ...DECLARED_FIELDS]);
|
|
187
196
|
var doubles2 = /* @__PURE__ */ new WeakMap();
|
|
188
197
|
var serializer = new DefaultUrlSerializer();
|
|
189
198
|
function stateSnapshot(url, root) {
|
|
@@ -237,16 +246,39 @@ function createUrlState(initial) {
|
|
|
237
246
|
}
|
|
238
247
|
};
|
|
239
248
|
}
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
function startEvent(event, inFlight, navigationOf) {
|
|
250
|
+
if (!(event instanceof NavigationStart)) {
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
inFlight.set(
|
|
254
|
+
navigationOf({
|
|
255
|
+
id: event.id,
|
|
256
|
+
initialUrl: serializer.parse(event.url),
|
|
257
|
+
extractedUrl: serializer.parse(event.url),
|
|
258
|
+
trigger: event.navigationTrigger ?? "imperative"
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
return event.id;
|
|
262
|
+
}
|
|
263
|
+
function endsNavigation(event) {
|
|
264
|
+
return event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError || event instanceof NavigationSkipped;
|
|
265
|
+
}
|
|
266
|
+
function buildNavigation(state, id, named) {
|
|
267
|
+
return {
|
|
268
|
+
id,
|
|
269
|
+
initialUrl: state.tree(),
|
|
270
|
+
extractedUrl: state.tree(),
|
|
271
|
+
trigger: "imperative",
|
|
272
|
+
extras: {},
|
|
273
|
+
previousNavigation: null,
|
|
274
|
+
// A no-op rather than a spy: a spec that asserts the abort passes its own, and every other spec
|
|
275
|
+
// would be carrying a spy nothing reads.
|
|
276
|
+
abort: () => void 0,
|
|
277
|
+
...named
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function buildRouter(state, stream, parts) {
|
|
281
|
+
return guardMissingMembers({
|
|
250
282
|
get url() {
|
|
251
283
|
return state.url();
|
|
252
284
|
},
|
|
@@ -256,8 +288,8 @@ function createRouterDouble(init = {}) {
|
|
|
256
288
|
get routerState() {
|
|
257
289
|
return state.routerState;
|
|
258
290
|
},
|
|
259
|
-
navigate,
|
|
260
|
-
navigateByUrl,
|
|
291
|
+
navigate: parts.navigate,
|
|
292
|
+
navigateByUrl: parts.navigateByUrl,
|
|
261
293
|
createUrlTree: (commands, extras = {}) => createUrlTreeFromSnapshot(
|
|
262
294
|
(extras.relativeTo ?? state.routerState.root).snapshot,
|
|
263
295
|
commands,
|
|
@@ -266,22 +298,47 @@ function createRouterDouble(init = {}) {
|
|
|
266
298
|
),
|
|
267
299
|
serializeUrl: (target) => serializer.serialize(target),
|
|
268
300
|
parseUrl: (target) => serializer.parse(target),
|
|
301
|
+
get currentNavigation() {
|
|
302
|
+
return parts.currentNavigation;
|
|
303
|
+
},
|
|
304
|
+
getCurrentNavigation: () => parts.currentNavigation(),
|
|
269
305
|
// Inert, and present rather than guarded: Angular's injector reads `ngOnDestroy` off every
|
|
270
306
|
// value it builds, so a double that threw on it would throw on `TestBed.resetTestingModule()`.
|
|
271
307
|
ngOnDestroy: () => void 0,
|
|
272
308
|
dispose: () => void 0
|
|
273
309
|
});
|
|
310
|
+
}
|
|
311
|
+
function createRouterDouble(init = {}) {
|
|
312
|
+
const state = createUrlState(init.url ?? "/");
|
|
313
|
+
const events = new BehaviorSubject(new NavigationEnd(1, state.url(), state.url()));
|
|
314
|
+
const stream = events.asObservable();
|
|
315
|
+
let navigationId = 1;
|
|
316
|
+
const navigationOf = (named) => buildNavigation(state, navigationId, named);
|
|
317
|
+
const inFlight = signal(init.currentNavigation ? navigationOf(init.currentNavigation) : null);
|
|
318
|
+
const currentNavigation = inFlight.asReadonly();
|
|
319
|
+
const navigate = createFunctionSpy("Router.navigate");
|
|
320
|
+
const navigateByUrl = createFunctionSpy("Router.navigateByUrl");
|
|
321
|
+
navigate.resolveWith(true);
|
|
322
|
+
navigateByUrl.resolveWith(true);
|
|
323
|
+
const router = buildRouter(state, stream, { navigate, navigateByUrl, currentNavigation });
|
|
274
324
|
const double = {
|
|
275
325
|
router,
|
|
276
326
|
navigate,
|
|
277
327
|
navigateByUrl,
|
|
278
328
|
setUrl: state.set,
|
|
329
|
+
setCurrentNavigation: (navigation) => {
|
|
330
|
+
inFlight.set(navigation === null ? null : navigationOf(navigation ?? {}));
|
|
331
|
+
},
|
|
279
332
|
emitNavigation: (event) => {
|
|
280
333
|
const next = typeof event === "object" ? event : new NavigationEnd(++navigationId, event ?? state.url(), event ?? state.url());
|
|
281
334
|
if (next instanceof NavigationEnd) {
|
|
282
335
|
state.set(next.urlAfterRedirects);
|
|
283
336
|
}
|
|
337
|
+
navigationId = startEvent(next, inFlight, navigationOf) ?? navigationId;
|
|
284
338
|
events.next(next);
|
|
339
|
+
if (endsNavigation(next)) {
|
|
340
|
+
inFlight.set(null);
|
|
341
|
+
}
|
|
285
342
|
}
|
|
286
343
|
};
|
|
287
344
|
doubles2.set(router, double);
|
package/dist/angular.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { o as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, d as RunCounter, S as SettleResourceOptions, e as ShallowRender, f as SpyRegistry, g as StableOptions, h as createWithAutoSpies, i as flushEffects, j as injectSpy, p as provideAutoSpy, q as provideAutoSpyForToken, r as renderShallow, k as runEffect, s as setInputs, l as settleResource, m as stable, t as trackEffectRuns, n as trackRecomputations } from './track-signal-runs-
|
|
1
|
+
export { o as AngularTokenProvider, A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, d as RunCounter, S as SettleResourceOptions, e as ShallowRender, f as SpyRegistry, g as StableOptions, h as createWithAutoSpies, i as flushEffects, j as injectSpy, p as provideAutoSpy, q as provideAutoSpyForToken, r as renderShallow, k as runEffect, s as setInputs, l as settleResource, m as stable, t as trackEffectRuns, n as trackRecomputations } from './track-signal-runs-BoA6j-74.js';
|
|
2
2
|
import { InjectionToken, Provider, Type, WritableSignal, Signal, ProviderToken, FactoryProvider, AbstractType, Injector, ValueProvider } from '@angular/core';
|
|
3
3
|
import { A as AutoMockConfiguration, D as DeepReadonly } from './expect-emission-CmN9ADyN.js';
|
|
4
4
|
export { C as CallbackSubscribable, E as EmissionObserver, b as EmissionOptions, c as EmissionSource, S as SubscribableLike, g as expectCompletion, h as expectEmission, i as expectEmissions, j as expectError, k as expectNoEmission, s as setEmissionTimeout } from './expect-emission-CmN9ADyN.js';
|
|
@@ -841,12 +841,31 @@ declare function registerSignalMatchers(): void;
|
|
|
841
841
|
* `InjectionToken<AppWindow>` gets its own members checked in the overrides too.
|
|
842
842
|
*/
|
|
843
843
|
|
|
844
|
+
/** How many levels of slicing the type follows. Beyond it a member takes its whole type. */
|
|
845
|
+
type Levels = [never, 0, 1, 2, 3];
|
|
846
|
+
/**
|
|
847
|
+
* One member's override: the member's own type, or a slice of it wherever it is a plain-ish object.
|
|
848
|
+
*
|
|
849
|
+
* A method is matched whole and **first**: `Partial<(…) => …>` is a mapped type over a function,
|
|
850
|
+
* which drops the call signature and leaves `{}` — a slice branch that accepts anything at all. An
|
|
851
|
+
* array is matched whole too: slicing one by index describes nothing a spec means.
|
|
852
|
+
*/
|
|
853
|
+
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 | {
|
|
854
|
+
[K in keyof V]?: PlatformOverride<V[K], Levels[Depth]>;
|
|
855
|
+
} : V;
|
|
844
856
|
/**
|
|
845
857
|
* What a spec says about a platform object: a value per member, and a **slice** wherever the member
|
|
846
858
|
* is itself an object — `{ screen: { width: 1920 } }` leaves `screen.colorDepth` real.
|
|
859
|
+
*
|
|
860
|
+
* The slice goes as deep as the merge does, three levels of it: the proxy re-merges every plain
|
|
861
|
+
* object it hands out, so `{ document: { location: { href: '' } } }` works at run time, and a type
|
|
862
|
+
* that stopped at the first level sent a spec that wrote it to `createMock<Location>()` or to a
|
|
863
|
+
* cast for no reason anybody could state. Depth is bounded because `Window` is recursive —
|
|
864
|
+
* `window.window` is a `Window` — and an unbounded mapped type over it is a bill every call site
|
|
865
|
+
* pays.
|
|
847
866
|
*/
|
|
848
867
|
type PlatformOverrides<T> = {
|
|
849
|
-
[K in keyof T]?:
|
|
868
|
+
[K in keyof T]?: PlatformOverride<T[K], 3>;
|
|
850
869
|
};
|
|
851
870
|
/**
|
|
852
871
|
* The `window` double without a `TestBed` — for a class built with `new`, or a plain function.
|
package/dist/bun-angular.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { ArgCaptor, AsInstances, AssertMockedOptions, ConstructorMock, Construct
|
|
|
3
3
|
export { A as AutoMockConfiguration, a as AutoSpyDefaultEntry, C as CallbackSubscribable, E as EmissionObserver, b as EmissionOptions, c as EmissionSource, S as SubscribableLike, d as autoMocked, e as clearAutoSpyDefaults, f as createAutoMock, g as expectCompletion, h as expectEmission, i as expectEmissions, j as expectError, k as expectNoEmission, r as registerAutoSpyDefaults, s as setEmissionTimeout } from './expect-emission-CmN9ADyN.js';
|
|
4
4
|
export { c as createFunctionSpy, d as describeDuplicateCopies, g as getPackageCopies } from './package-identity-lm-gqZAR.js';
|
|
5
5
|
export { A as AccessorImplementations, O as OutsideHookReaction, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as reportPropsOutsideHooks, e as restoreMockedProps } from './prop-mock-DnYNBNJL.js';
|
|
6
|
-
export { A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, d as RunCounter, S as SettleResourceOptions, e as ShallowRender, f as SpyRegistry, g as StableOptions, h as createWithAutoSpies, i as flushEffects, j as injectSpy, p as provideAutoSpy, r as renderShallow, k as runEffect, s as setInputs, l as settleResource, m as stable, t as trackEffectRuns, n as trackRecomputations } from './track-signal-runs-
|
|
6
|
+
export { A as AngularValueProvider, a as AutoSpiedInstance, C as ComponentInputs, b as CreateWithAutoSpiesOptions, R as RenderShallowOptions, c as ResourceStatusLike, d as RunCounter, S as SettleResourceOptions, e as ShallowRender, f as SpyRegistry, g as StableOptions, h as createWithAutoSpies, i as flushEffects, j as injectSpy, p as provideAutoSpy, r as renderShallow, k as runEffect, s as setInputs, l as settleResource, m as stable, t as trackEffectRuns, n as trackRecomputations } from './track-signal-runs-BoA6j-74.js';
|
|
7
7
|
import 'vitest';
|
|
8
8
|
import '@angular/core';
|
|
9
9
|
import '@angular/core/testing';
|
package/dist/cli.js
CHANGED
|
@@ -2453,7 +2453,7 @@ function checkForeignPragma(graph) {
|
|
|
2453
2453
|
}
|
|
2454
2454
|
|
|
2455
2455
|
// src/cli/checks/export-map.generated.ts
|
|
2456
|
-
var EXPORT_MAP_VERSION = "5.
|
|
2456
|
+
var EXPORT_MAP_VERSION = "5.10.1";
|
|
2457
2457
|
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-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";
|
|
2458
2458
|
var EXPORTED_BY = {
|
|
2459
2459
|
AccessorImplementations: "0 1 2 3 4 12 17 18 19",
|
|
@@ -2671,6 +2671,7 @@ var EXPORTED_BY = {
|
|
|
2671
2671
|
mutationRecord: "7",
|
|
2672
2672
|
MutationRecordInit: "7",
|
|
2673
2673
|
narrow: "0 1 2 3 4 17 18 19",
|
|
2674
|
+
NavigationInit: "14",
|
|
2674
2675
|
NestModuleRef: "16",
|
|
2675
2676
|
NestUnit: "16",
|
|
2676
2677
|
NestUnitClass: "16",
|
|
@@ -3484,7 +3485,9 @@ Options
|
|
|
3484
3485
|
take (2), and the absolute floor under which it is noise
|
|
3485
3486
|
(500 ms).
|
|
3486
3487
|
--top <n> perf only. Rows in the "slowest files" and "slowest bodies"
|
|
3487
|
-
tables. 0 turns them off.
|
|
3488
|
+
tables. 0 turns them off. Asked for explicitly, a run whose
|
|
3489
|
+
slowest file is under the one-second floor says so rather
|
|
3490
|
+
than printing nothing.
|
|
3488
3491
|
--check init only. Write nothing; exit 1 if the block is out of date.
|
|
3489
3492
|
--dry-run init only. Print what would change and write nothing.
|
|
3490
3493
|
--uninstall init only. Remove the managed blocks and the files it created.
|
|
@@ -4389,6 +4392,10 @@ function formatHotspots(run, cwd, options = {}) {
|
|
|
4389
4392
|
}
|
|
4390
4393
|
return [...fileSection(files, totalTestMs(run, cwd), width), ...caseSection(caseHotspots(run, cwd, limit), width)].join("\n\n");
|
|
4391
4394
|
}
|
|
4395
|
+
function hotspotFloorNote(run, cwd, floorMs = HOTSPOT_DEFAULTS.floorMs) {
|
|
4396
|
+
const [slowest] = fileHotspots(run, cwd, 1);
|
|
4397
|
+
return slowest === void 0 ? "No hotspot tables: no file in this run finished a test body, so there is nothing to rank." : `No hotspot tables: the slowest file spent ${formatMs(slowest.ms)} in its test bodies, under the ${formatMs(floorMs)} floor. Below it a ranking is the reader\u2019s attention spent on the machine rather than on a decision somebody made.`;
|
|
4398
|
+
}
|
|
4392
4399
|
var UNIT_TEST_BUILDER2 = "@angular/build:unit-test";
|
|
4393
4400
|
var WORKSPACE_FILES = ["angular.json", "workspace.json"];
|
|
4394
4401
|
function targetsOf(project) {
|
|
@@ -4548,7 +4555,7 @@ var CONFIG_FILE = /(?:^|\/)vite(?:st)?[\w.-]*\.config\.[cm]?[jt]s$/;
|
|
|
4548
4555
|
var NO_ISOLATION = /\bisolate\s*:\s*false/;
|
|
4549
4556
|
var JSDOM = /\benvironment\s*:\s*["'`]jsdom["'`]/;
|
|
4550
4557
|
var HAPPY_DOM = /happy-dom/;
|
|
4551
|
-
var WORKER_CAP = /\bmaxWorkers\s
|
|
4558
|
+
var WORKER_CAP = /\bmaxWorkers\s*[,:=}]|\bmaxWorkers\s*$/m;
|
|
4552
4559
|
var LARGE_RUN_MS = 6e4;
|
|
4553
4560
|
var WORKER_RSS_MB = 155;
|
|
4554
4561
|
function remainder(total) {
|
|
@@ -4820,6 +4827,18 @@ function recordBaseline(run, cwd, path, io) {
|
|
|
4820
4827
|
perf baseline: recorded ${Object.keys(baseline.files).length} files at a median of ${formatMs(baseline.median)} into ${path}. Commit it \u2014 it is only worth what the next reader can diff.`
|
|
4821
4828
|
);
|
|
4822
4829
|
}
|
|
4830
|
+
function reportHotspots(run, cwd, top, io) {
|
|
4831
|
+
const hotspots = formatHotspots(run, cwd, top === void 0 ? {} : { limit: top });
|
|
4832
|
+
if (hotspots !== "") {
|
|
4833
|
+
io.out(`
|
|
4834
|
+
${hotspots}`);
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
if (top !== void 0 && top > 0) {
|
|
4838
|
+
io.out(`
|
|
4839
|
+
${hotspotFloorNote(run, cwd)}`);
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4823
4842
|
function renderPerf(source, profile, io, options = {}) {
|
|
4824
4843
|
const gate = options.gate;
|
|
4825
4844
|
if (!source.ok) {
|
|
@@ -4848,11 +4867,7 @@ function renderPerf(source, profile, io, options = {}) {
|
|
|
4848
4867
|
`);
|
|
4849
4868
|
}
|
|
4850
4869
|
io.out(formatPhases(analysis.phases));
|
|
4851
|
-
|
|
4852
|
-
if (hotspots !== "") {
|
|
4853
|
-
io.out(`
|
|
4854
|
-
${hotspots}`);
|
|
4855
|
-
}
|
|
4870
|
+
reportHotspots(source.run, profile.cwd, options.top, io);
|
|
4856
4871
|
reportFindings(analysis, io);
|
|
4857
4872
|
if (options.baseline?.update === true) {
|
|
4858
4873
|
recordBaseline(source.run, profile.cwd, options.baseline.path, io);
|
package/dist/eslint-plugin.cjs
CHANGED
|
@@ -757,14 +757,22 @@ function factoryBody(context, value) {
|
|
|
757
757
|
}
|
|
758
758
|
var SPY_FACTORIES = /* @__PURE__ */ new Set([
|
|
759
759
|
"autoMocked",
|
|
760
|
+
"createActivatedRoute",
|
|
760
761
|
"createAutoMock",
|
|
762
|
+
"createDocumentDouble",
|
|
761
763
|
"createMock",
|
|
764
|
+
"createRouterDouble",
|
|
762
765
|
"createSpyClass",
|
|
763
766
|
"createSpyFromClass",
|
|
767
|
+
"createWindowDouble",
|
|
764
768
|
"mockConstructor",
|
|
765
769
|
"mockDeep",
|
|
770
|
+
"provideActivatedRoute",
|
|
766
771
|
"provideAutoSpy",
|
|
767
|
-
"provideAutoSpyForToken"
|
|
772
|
+
"provideAutoSpyForToken",
|
|
773
|
+
"provideDocumentDouble",
|
|
774
|
+
"provideRouterDouble",
|
|
775
|
+
"provideWindowDouble"
|
|
768
776
|
]);
|
|
769
777
|
function isFactoryCall(node) {
|
|
770
778
|
return isCallExpression(node) && isIdentifier(node.callee) && SPY_FACTORIES.has(node.callee.name);
|
|
@@ -2067,6 +2075,206 @@ var preferProvideActivatedRoute = defineRule({
|
|
|
2067
2075
|
})
|
|
2068
2076
|
});
|
|
2069
2077
|
|
|
2078
|
+
// src/lib/eslint/smoke-test.ts
|
|
2079
|
+
var TESTS = /* @__PURE__ */ new Set(["fit", "it", "test", "xit", "xtest"]);
|
|
2080
|
+
var SKIPPED_TESTS = /* @__PURE__ */ new Set(["xit", "xtest"]);
|
|
2081
|
+
var SKIPPED_MEMBERS = /* @__PURE__ */ new Set(["skip", "todo"]);
|
|
2082
|
+
var EXISTS = /* @__PURE__ */ new Set(["toBeDefined", "toBeInstanceOf", "toBeTruthy"]);
|
|
2083
|
+
var MISSING = /* @__PURE__ */ new Set(["toBeFalsy", "toBeNull", "toBeUndefined"]);
|
|
2084
|
+
function calleeChain(node) {
|
|
2085
|
+
const members = [];
|
|
2086
|
+
let current = node.callee;
|
|
2087
|
+
for (; ; ) {
|
|
2088
|
+
if (isIdentifier(current)) {
|
|
2089
|
+
return { members, root: current.name };
|
|
2090
|
+
}
|
|
2091
|
+
if (isMemberExpression(current)) {
|
|
2092
|
+
const name = memberName(current);
|
|
2093
|
+
if (name === void 0) {
|
|
2094
|
+
return void 0;
|
|
2095
|
+
}
|
|
2096
|
+
members.unshift(name);
|
|
2097
|
+
current = current.object;
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
if (!isCallExpression(current)) {
|
|
2101
|
+
return void 0;
|
|
2102
|
+
}
|
|
2103
|
+
current = current.callee;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
var TITLES = /* @__PURE__ */ new Set(["Literal", "TemplateLiteral"]);
|
|
2107
|
+
function testCall(node) {
|
|
2108
|
+
const chain = calleeChain(node);
|
|
2109
|
+
const [title] = node.arguments;
|
|
2110
|
+
if (!chain || !TESTS.has(chain.root) || !title || !TITLES.has(title.type)) {
|
|
2111
|
+
return void 0;
|
|
2112
|
+
}
|
|
2113
|
+
return { skipped: SKIPPED_TESTS.has(chain.root) || chain.members.some((member) => SKIPPED_MEMBERS.has(member)) };
|
|
2114
|
+
}
|
|
2115
|
+
var SUBJECT_BUILDERS = /* @__PURE__ */ new Set(["get", "inject"]);
|
|
2116
|
+
function containsCall(node) {
|
|
2117
|
+
let current = node;
|
|
2118
|
+
while (isMemberExpression(current)) {
|
|
2119
|
+
if (isCallExpression(current.object)) {
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
current = current.object;
|
|
2123
|
+
}
|
|
2124
|
+
return isCallExpression(current);
|
|
2125
|
+
}
|
|
2126
|
+
function namesSubject(subject, matcher) {
|
|
2127
|
+
if (isIdentifier(subject)) {
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2130
|
+
if (isMemberExpression(subject)) {
|
|
2131
|
+
return !containsCall(subject);
|
|
2132
|
+
}
|
|
2133
|
+
if (!isCallExpression(subject)) {
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
if (matcher === "toBeInstanceOf") {
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
2139
|
+
if (isIdentifier(subject.callee)) {
|
|
2140
|
+
return subject.arguments.length === 0;
|
|
2141
|
+
}
|
|
2142
|
+
const [token, ...rest] = subject.arguments;
|
|
2143
|
+
return isMemberExpression(subject.callee) && SUBJECT_BUILDERS.has(memberName(subject.callee) ?? "") && !containsCall(subject.callee.object) && rest.length === 0 && token !== void 0 && (isIdentifier(token) || isMemberExpression(token) && !containsCall(token));
|
|
2144
|
+
}
|
|
2145
|
+
function subjectReference(subject, context) {
|
|
2146
|
+
if (isIdentifier(subject) || isMemberExpression(subject)) {
|
|
2147
|
+
return context.sourceCode.getText(subject);
|
|
2148
|
+
}
|
|
2149
|
+
let current = subject;
|
|
2150
|
+
while (isCallExpression(current) || isMemberExpression(current)) {
|
|
2151
|
+
current = isCallExpression(current) ? current.callee : current.object;
|
|
2152
|
+
}
|
|
2153
|
+
return isIdentifier(current) ? current.name : void 0;
|
|
2154
|
+
}
|
|
2155
|
+
function existenceSubject(expression) {
|
|
2156
|
+
if (!isCallExpression(expression) || !isMemberExpression(expression.callee)) {
|
|
2157
|
+
return void 0;
|
|
2158
|
+
}
|
|
2159
|
+
const matcher = memberName(expression.callee);
|
|
2160
|
+
let target = expression.callee.object;
|
|
2161
|
+
let negated = false;
|
|
2162
|
+
while (isMemberExpression(target) && memberName(target) === "not") {
|
|
2163
|
+
negated = !negated;
|
|
2164
|
+
target = target.object;
|
|
2165
|
+
}
|
|
2166
|
+
if (!matcher || !isCallExpression(target) || !isIdentifier(target.callee) || target.callee.name !== "expect") {
|
|
2167
|
+
return void 0;
|
|
2168
|
+
}
|
|
2169
|
+
const [subject] = target.arguments;
|
|
2170
|
+
if (!subject || !(negated ? MISSING.has(matcher) : EXISTS.has(matcher))) {
|
|
2171
|
+
return void 0;
|
|
2172
|
+
}
|
|
2173
|
+
return namesSubject(subject, matcher) ? subject : void 0;
|
|
2174
|
+
}
|
|
2175
|
+
function existenceOnly(callback) {
|
|
2176
|
+
const body = callback.body;
|
|
2177
|
+
if (!isBlockStatement(body)) {
|
|
2178
|
+
return existenceSubject(body);
|
|
2179
|
+
}
|
|
2180
|
+
const subjects = body.body.map((statement) => isExpressionStatement(statement) ? existenceSubject(statement.expression) : void 0);
|
|
2181
|
+
return subjects.length > 0 && subjects.every(Boolean) ? subjects[0] : void 0;
|
|
2182
|
+
}
|
|
2183
|
+
function removal2(context, statement) {
|
|
2184
|
+
return (fixer) => {
|
|
2185
|
+
const text = context.sourceCode.getText();
|
|
2186
|
+
let start = statement.range[0];
|
|
2187
|
+
while (start > 0 && /\s/.test(text.charAt(start - 1))) {
|
|
2188
|
+
start -= 1;
|
|
2189
|
+
}
|
|
2190
|
+
return fixer.replaceTextRange([start, statement.range[1]], "");
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
function recordName(names, node, context) {
|
|
2194
|
+
const text = context.sourceCode.getText(node);
|
|
2195
|
+
for (let scope = enclosingFunction(node); scope; scope = enclosingFunction(scope.parent)) {
|
|
2196
|
+
const seen = names.get(scope) ?? /* @__PURE__ */ new Set();
|
|
2197
|
+
seen.add(text);
|
|
2198
|
+
names.set(scope, seen);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
function countNested(blocks) {
|
|
2202
|
+
blocks.forEach((block, scope) => {
|
|
2203
|
+
let current = scope;
|
|
2204
|
+
for (; ; ) {
|
|
2205
|
+
const above = blocks.get(current);
|
|
2206
|
+
if (above) {
|
|
2207
|
+
above.below += block.proving;
|
|
2208
|
+
above.running.push(...block.running);
|
|
2209
|
+
}
|
|
2210
|
+
if (current === void 0) {
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
current = enclosingFunction(current.parent);
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
function reportBlock(context, block, names) {
|
|
2218
|
+
if (block.below === 0) {
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const referenced = new Set(block.running.flatMap((callback) => [...names.get(callback) ?? []]));
|
|
2222
|
+
block.smoke.forEach(({ node, subject }) => {
|
|
2223
|
+
const reference = subjectReference(subject, context);
|
|
2224
|
+
if (reference !== void 0 && !referenced.has(reference)) {
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const data = {
|
|
2228
|
+
siblings: block.below === 1 ? "another test under the same setup already runs against it" : `${block.below} other tests under the same setup already run against it`,
|
|
2229
|
+
subject: context.sourceCode.getText(subject)
|
|
2230
|
+
};
|
|
2231
|
+
const statement = node.parent;
|
|
2232
|
+
const report = { data, messageId: "noRedundantSmokeTest", node };
|
|
2233
|
+
context.report(
|
|
2234
|
+
isExpressionStatement(statement) ? { ...report, suggest: [{ desc: "Remove this test", fix: removal2(context, statement) }] } : report
|
|
2235
|
+
);
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
var noRedundantSmokeTest = defineRule({
|
|
2239
|
+
anchor: "-a-promise-a-test-forgets-to-await",
|
|
2240
|
+
description: "Delete a test whose whole body asserts that the subject exists, where the block already has tests that use it",
|
|
2241
|
+
hasSuggestions: true,
|
|
2242
|
+
messages: {
|
|
2243
|
+
noRedundantSmokeTest: "The whole of this test is `{{subject}}` existing, and {{siblings}}: they run the same `beforeEach`, so a subject that came back nullish fails them first, and names what it was doing when it did. This one cannot fail on its own and covers nothing \u2014 it is a green line in the report standing in for a test. Delete it. If the construction is what the spec is about \u2014 a factory that rejects a bad config, a constructor that reads an optional token \u2014 assert on that instead: `expect(() => new Subject(null)).toThrow(\u2026)`, or on the value the subject produced."
|
|
2244
|
+
},
|
|
2245
|
+
create: (context) => {
|
|
2246
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
2247
|
+
const names = /* @__PURE__ */ new Map();
|
|
2248
|
+
return {
|
|
2249
|
+
Identifier: (node) => recordName(names, node, context),
|
|
2250
|
+
MemberExpression: (node) => recordName(names, node, context),
|
|
2251
|
+
CallExpression: (node) => {
|
|
2252
|
+
const test = testCall(node);
|
|
2253
|
+
if (!test) {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const scope = enclosingFunction(node);
|
|
2257
|
+
const block = blocks.get(scope) ?? { below: 0, proving: 0, running: [], smoke: [] };
|
|
2258
|
+
const callback = node.arguments.find(isFunctionNode);
|
|
2259
|
+
const subject = callback && existenceOnly(callback);
|
|
2260
|
+
if (subject) {
|
|
2261
|
+
block.smoke.push({ node, subject });
|
|
2262
|
+
} else if (!test.skipped) {
|
|
2263
|
+
block.proving += 1;
|
|
2264
|
+
if (callback) {
|
|
2265
|
+
block.running.push(callback);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
blocks.set(scope, block);
|
|
2269
|
+
},
|
|
2270
|
+
"Program:exit": () => {
|
|
2271
|
+
countNested(blocks);
|
|
2272
|
+
blocks.forEach((block) => reportBlock(context, block, names));
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2070
2278
|
// src/lib/eslint/stub-class.ts
|
|
2071
2279
|
function countStubFields(node) {
|
|
2072
2280
|
return node.body.body.filter((member) => isStubField(member)).length;
|
|
@@ -2274,10 +2482,13 @@ function answersTheTestBed(context, node, seen) {
|
|
|
2274
2482
|
const link = memberName(node.callee);
|
|
2275
2483
|
return link !== void 0 && ANSWERS_THE_TEST_BED.has(link) && answersTheTestBed(context, node.callee.object, seen);
|
|
2276
2484
|
}
|
|
2277
|
-
function
|
|
2485
|
+
function answerOf(member) {
|
|
2486
|
+
return ANSWERS_A_FIXTURE.has(member) ? "the `ComponentFixture`" : "the TestBed";
|
|
2487
|
+
}
|
|
2488
|
+
function removal3(context, awaited, answers) {
|
|
2278
2489
|
const callback = asyncOnlyFor(awaited);
|
|
2279
2490
|
return {
|
|
2280
|
-
desc:
|
|
2491
|
+
desc: `Remove the await \u2014 the TestBed call answers ${answers}, not a promise`,
|
|
2281
2492
|
fix: (fixer) => {
|
|
2282
2493
|
const fixes = [fixer.replaceText(awaited, context.sourceCode.getText(awaited.argument))];
|
|
2283
2494
|
if (callback) {
|
|
@@ -2292,7 +2503,7 @@ var noSyncTestbedAwait = defineRule({
|
|
|
2292
2503
|
description: "Drop the await on a TestBed call that answers the TestBed or a fixture rather than a promise",
|
|
2293
2504
|
hasSuggestions: true,
|
|
2294
2505
|
messages: {
|
|
2295
|
-
noSyncTestbedAwait: "`{{member}}(\u2026)` answers
|
|
2506
|
+
noSyncTestbedAwait: "`{{member}}(\u2026)` answers {{answers}}, not a promise, so this `await` waits for nothing. Every `configureTestingModule` and `override*` returns `TestBed` itself \u2014 that is what lets them chain \u2014 and `createComponent` / `getLastFixture` return the `ComponentFixture`; awaiting one of those only resumes the hook a microtask later, while making a synchronous setup read as an asynchronous one. Drop the `await`, and the `async` of a `beforeEach` / `beforeAll` / `afterEach` / `afterAll` / `it` / `test` callback that then awaits nothing else \u2014 the hook is not asynchronous any more, and leaving the `async` behind is the half `@typescript-eslint/require-await` would still have to report. The TestBed calls that really do return a promise keep their `await`: `compileComponents()`, and on a fixture `whenStable()`, `whenRenderingDone()` and `getDeferBlocks()`. `TestBed.inject(TOKEN)` and `TestBed.runInInjectionContext(fn)` are never reported \u2014 each answers whatever the token or the callback holds, which is a promise often enough to be awaited on purpose."
|
|
2296
2507
|
},
|
|
2297
2508
|
create: (context) => ({
|
|
2298
2509
|
AwaitExpression: (node) => {
|
|
@@ -2307,7 +2518,8 @@ var noSyncTestbedAwait = defineRule({
|
|
|
2307
2518
|
if (!answersTheTestBed(context, argument.callee.object, /* @__PURE__ */ new Set())) {
|
|
2308
2519
|
return;
|
|
2309
2520
|
}
|
|
2310
|
-
|
|
2521
|
+
const answers = answerOf(member);
|
|
2522
|
+
context.report({ node, messageId: "noSyncTestbedAwait", data: { member, answers }, suggest: [removal3(context, node, answers)] });
|
|
2311
2523
|
}
|
|
2312
2524
|
})
|
|
2313
2525
|
});
|
|
@@ -2924,6 +3136,7 @@ var rules = {
|
|
|
2924
3136
|
"no-constant-expect": noConstantExpect,
|
|
2925
3137
|
"no-compile-components": noCompileComponents,
|
|
2926
3138
|
"no-sync-testbed-await": noSyncTestbedAwait,
|
|
3139
|
+
"no-redundant-smoke-test": noRedundantSmokeTest,
|
|
2927
3140
|
...jasmineRules
|
|
2928
3141
|
};
|
|
2929
3142
|
|
|
@@ -2962,6 +3175,7 @@ var recommendedRules = {
|
|
|
2962
3175
|
[`${PLUGIN_NAME}/no-constant-expect`]: "error",
|
|
2963
3176
|
[`${PLUGIN_NAME}/no-compile-components`]: "error",
|
|
2964
3177
|
[`${PLUGIN_NAME}/no-sync-testbed-await`]: "error",
|
|
3178
|
+
[`${PLUGIN_NAME}/no-redundant-smoke-test`]: "error",
|
|
2965
3179
|
[`${PLUGIN_NAME}/jasmine-namespace-without-entry`]: "error",
|
|
2966
3180
|
[`${PLUGIN_NAME}/no-jasmine-globals`]: "error",
|
|
2967
3181
|
[`${PLUGIN_NAME}/no-save-arguments-by-value`]: "error",
|
|
@@ -207,8 +207,18 @@ declare function createWithAutoSpies<T>(target: Type<T>, options?: CreateWithAut
|
|
|
207
207
|
* signals and DI — everything such a spec actually reads.
|
|
208
208
|
*/
|
|
209
209
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
210
|
+
/**
|
|
211
|
+
* The value `componentRef.setInput` expects for a member — signal inputs are set with the value, not
|
|
212
|
+
* the signal.
|
|
213
|
+
*
|
|
214
|
+
* The read type in the pattern is what makes a transform input work.
|
|
215
|
+
* `input(false, { transform: booleanAttribute })` is an `InputSignalWithTransform<boolean, unknown>`,
|
|
216
|
+
* and against a pattern whose read type is fixed to `unknown` that match **fails**: the node behind
|
|
217
|
+
* the signal carries the read type in both directions, so a wider one is not a supertype, and
|
|
218
|
+
* `never` matches nothing at all. The member fell through to itself, and the spec was told to pass
|
|
219
|
+
* an `InputSignalWithTransform` where the component takes a boolean.
|
|
220
|
+
*/
|
|
221
|
+
type InputValue<Member> = Member extends InputSignalWithTransform<any, infer Write> ? Write : Member extends InputSignal<infer Value> ? Value : Member extends ModelSignal<infer Value> ? Value : Member;
|
|
212
222
|
/** The inputs of a component, keyed as declared but typed as the values `setInput` takes. */
|
|
213
223
|
type ComponentInputs<T> = {
|
|
214
224
|
[Key in keyof T]?: InputValue<T[Key]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest-auto-spy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.1",
|
|
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: Write or fix tests that use vitest-auto-spy — typed spies on Vitest, bun:test, node:test and Rstest. Use when a spec imports `vitest-auto-spy` or a subpath (`/angular`, `/bun-angular`, `/bun`, `/node`, `/rstest`, `/rxjs`, `/nestjs`, `/jasmine`, `/setup`, `/zone`, `/eslint-plugin`), the user names createSpyFromClass, createAutoMock, createMock, mockDeep,
|
|
3
|
+
description: Write or fix tests that use vitest-auto-spy — typed spies on Vitest, bun:test, node:test and Rstest. Use when a spec imports `vitest-auto-spy` or a subpath (`/angular`, `/bun-angular`, `/bun`, `/node`, `/rstest`, `/rxjs`, `/nestjs`, `/jasmine`, `/setup`, `/zone`, `/eslint-plugin`), the user names createSpyFromClass, createAutoMock, createMock, mockDeep, subscribeSpyTo, provideAutoSpy, provideHttpTesting, expectRequest, injectSpy, createNestUnit, extendWithAutoSpies, renderShallow, createWithAutoSpies, createDirectiveHost, overrideComponentProvider, enableAngularDiagnostics, trackInjections, runEffect, settleResource, mockResourceProp, mockSignalProp, mockReadonlyProp, registerAutoSpyDefaults, provideActivatedRoute, setCurrentNavigation, createComponentStub, stubWebStorage, assertNoShadowedProviders, createSpyFromInstance, explainSpy, expectEmission, setupAutoSpy, takeStrictViolations, guardStrayConsole, setSpyEngine, assertMocked, Spy<T>, calledWith, mustBeCalledWith, onlyMethodsToSpyOn, onUnstubbedCall, resolveWith or nextWith, migrating off jest-auto-spies, jasmine-auto-spies or @ngneat/spectator (createSpyObject, mockProvider, SpectatorService), or a test fails with "No mock adapter registered", "Observable spies require rxjs", "not on the class prototype", "strict mode is on", "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
|
|
@@ -221,6 +221,7 @@ it('loads', async () => {
|
|
|
221
221
|
| "did the migration lose a test?" with matching counters | `compareTestRuns(before, after)` |
|
|
222
222
|
| an input that has to change after the first render | `await setInputs(fixture, { … })` — one `setInput` per name, one wait |
|
|
223
223
|
| a component that navigates, or reads `router.url` | `provideRouterDouble({ url })` + `injectRouterDouble()` — `/angular-router` |
|
|
224
|
+
| a component that reads `router.currentNavigation()` for `extras.state` or `trigger` | `setCurrentNavigation({ extras: { state } })` on the same handle — no `instanceMethodsToSpyOn` |
|
|
224
225
|
| a `window` or `document` behind a DI token | `provideWindowDouble(WINDOW, { screen })` / `provideDocumentDouble({ … })` |
|
|
225
226
|
| `MAT_DIALOG_DATA` and `MatDialogRef` provided by hand | `provideMatDialogData(TOKEN, data)` / `provideMatDialogRef(MatDialogRef)` + `injectMatDialogRef(Ref)` |
|
|
226
227
|
| asserting a `computed()` did **not** recompute | `trackRecomputations(sig)` / `trackEffectRuns(ref)` — `{ count, stop() }` |
|
|
@@ -360,7 +361,7 @@ npx vitest-auto-spy codemod --verify # after a migration: anything the transfor
|
|
|
360
361
|
Most of this library's guarantees are type-level, so a green run that does not type-check is not
|
|
361
362
|
done. Report failures with their output rather than describing them as passing.
|
|
362
363
|
|
|
363
|
-
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The thirty-
|
|
364
|
+
**After any `eslint --fix` over specs, run `npx tsc --noEmit`.** The thirty-seven rules in
|
|
364
365
|
`vitest-auto-spy/eslint-plugin` are lint, not typecheck: `no-mocked-for-spy` rewrites a declaration
|
|
365
366
|
to `Spy<T>` and cannot see what the name is assigned two lines below, so a clean lint pass is not
|
|
366
367
|
evidence that the types still hold. Where it cannot prove the rename it downgrades to a suggestion —
|
|
@@ -393,7 +394,11 @@ else is a fixture of the wrong shape — check it against `ReturnType<X['m']>` a
|
|
|
393
394
|
with `createMock<…>()`. A value outside the declared type on purpose keeps the directive under
|
|
394
395
|
`// eslint-disable-next-line vitest-auto-spy/no-ts-expect-error-on-double -- <why>`.
|
|
395
396
|
`no-constant-expect` reports `expect(true).toBe(true)` and its relatives — assert on what the code
|
|
396
|
-
produced, or `expect.fail(…)` for a branch the test must not reach.
|
|
397
|
+
produced, or `expect.fail(…)` for a branch the test must not reach.
|
|
398
|
+
`no-redundant-smoke-test` reports the generated `it('should create', () => expect(x).toBeTruthy())`
|
|
399
|
+
where the block — its nested `describe`s counted — already has tests that run the same `beforeEach`:
|
|
400
|
+
they fail first on a subject that came back nullish, and say what they were doing. Delete it; keep it
|
|
401
|
+
only where it is the block's one running test, or make it assert the construction itself. `no-compile-components` is silent
|
|
397
402
|
until `['error', { builder: 'inline-resources' }]`; with it, drop `compileComponents()` and the
|
|
398
403
|
`async` it forced — except for a component whose template holds a `@defer` block, which ships async
|
|
399
404
|
class metadata that the call resolves whatever the builder did. Keep that one behind
|