vitest-auto-spy 3.6.0 → 3.8.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 +94 -24
- package/README.md +136 -7
- package/dist/angular.d.ts +150 -8
- package/dist/angular.js +121 -6
- package/dist/bun-angular.d.ts +5 -5
- package/dist/bun-angular.js +5 -5
- package/dist/bun.d.ts +75 -22
- package/dist/bun.js +4 -4
- package/dist/{chunk-QGZRH5XG.js → chunk-2PFOBMTZ.js} +1 -1
- package/dist/{chunk-RJJJTLQ3.js → chunk-DNHLQG45.js} +1 -1
- package/dist/{chunk-LSDPJMNS.js → chunk-ISSGEVLO.js} +101 -29
- package/dist/{chunk-F3NMY5KU.js → chunk-TMO2UFLD.js} +95 -49
- package/dist/{chunk-OEQTH7RA.js → chunk-WT75WGQZ.js} +72 -31
- package/dist/cli.js +1150 -0
- package/dist/console.d.ts +1 -1
- package/dist/console.js +1 -1
- package/dist/{expect-emission-RtR1iYgI.d.ts → expect-emission-Cpj2GGcD.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/nestjs.d.ts +1 -1
- package/dist/nestjs.js +2 -2
- package/dist/node.cjs +170 -56
- package/dist/node.d.cts +60 -1821
- package/dist/node.d.ts +4 -4
- package/dist/node.js +4 -4
- package/dist/{prop-mock-CKFksCvA.d.ts → prop-mock-CeHqAeIG.d.ts} +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/react.js +4 -4
- package/dist/rxjs.d.ts +2 -2
- package/dist/setup.d.ts +2 -2
- package/dist/svelte.d.ts +4 -4
- package/dist/svelte.js +4 -4
- package/dist/{types-dZUFYsox.d.ts → types-W3lPrwC7.d.ts} +3 -3
- package/dist/vue.d.ts +5 -5
- package/dist/vue.js +5 -5
- package/dist/{run-effect-C_7mFldc.d.ts → zoneless-Ch2Ym_6z.d.ts} +149 -82
- package/package.json +7 -3
- package/skills/vitest-auto-spy/SKILL.md +22 -3
package/AGENTS.md
CHANGED
|
@@ -269,7 +269,7 @@ import { type Spy, injectSpy, mockReadonlyProp, provideAutoSpy } from 'vitest-au
|
|
|
269
269
|
|
|
270
270
|
describe('TaskService', () => {
|
|
271
271
|
let projects: Spy<ProjectStore>;
|
|
272
|
-
let feed: Spy<
|
|
272
|
+
let feed: Spy<NewsFeedService>;
|
|
273
273
|
let service: TaskService;
|
|
274
274
|
|
|
275
275
|
beforeEach(() => {
|
|
@@ -277,12 +277,12 @@ describe('TaskService', () => {
|
|
|
277
277
|
providers: [
|
|
278
278
|
provideAutoSpy(NotificationService), // plain service — nothing to configure
|
|
279
279
|
provideAutoSpy(ProjectStore, { instanceMethodsToSpyOn: ['current', 'isEmpty'] }), // signals
|
|
280
|
-
provideAutoSpy(
|
|
280
|
+
provideAutoSpy(NewsFeedService, { observablePropsToSpyOn: ['connected$'] }), // Observable props
|
|
281
281
|
],
|
|
282
282
|
});
|
|
283
283
|
|
|
284
284
|
projects = injectSpy(ProjectStore);
|
|
285
|
-
feed = injectSpy(
|
|
285
|
+
feed = injectSpy(NewsFeedService);
|
|
286
286
|
|
|
287
287
|
feed.connected$.nextWith(true); // seed the defaults every test needs, once
|
|
288
288
|
projects.save.mockReturnValue(of(true));
|
|
@@ -369,6 +369,27 @@ const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the
|
|
|
369
369
|
`mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
|
|
370
370
|
all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
|
|
371
371
|
|
|
372
|
+
When the argument worth asserting on is one the **code under test built** — a callback, a config
|
|
373
|
+
object, an `AbortSignal` — describing its shape is the wrong tool. `expect.any(Function)` says a
|
|
374
|
+
function was passed; `captureArg` hands it to you so the test can call it:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import { captureArg } from 'vitest-auto-spy';
|
|
378
|
+
|
|
379
|
+
const onDone = captureArg<() => void>();
|
|
380
|
+
|
|
381
|
+
expect(notifier.subscribe).toHaveBeenCalledWith('ready', onDone);
|
|
382
|
+
|
|
383
|
+
onDone.value(); // and now exercise what was passed
|
|
384
|
+
expect(component.finished()).toBe(true);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`.values` holds every match, oldest first; `.captured` asks without triggering the "nothing was
|
|
388
|
+
captured" throw; `.reset()` lets one captor serve two phases. **Assertions only** — a captor matches
|
|
389
|
+
every value, so putting one in `calledWith` would configure a return for every call, which is
|
|
390
|
+
`mockReturnValue` spelled less clearly, and `calledWith` is typed to the method's own parameters so
|
|
391
|
+
it will not compile anyway.
|
|
392
|
+
|
|
372
393
|
**The observable helpers are backed by a `ReplaySubject(1)` that belongs to the spy, and it is
|
|
373
394
|
configuration — so it must be reset with the rest of it.** Two failures used to come out of that
|
|
374
395
|
buffer outliving the test that filled it, and both were silent:
|
|
@@ -569,8 +590,8 @@ constructor and produces `Service<any>`, and the `any` surfaces much later as a
|
|
|
569
590
|
it says "type parameter":
|
|
570
591
|
|
|
571
592
|
```ts
|
|
572
|
-
const config = asSpy<
|
|
573
|
-
const config = injectSpy<
|
|
593
|
+
const config = asSpy<FeatureFlagService>(TestBed.inject(FeatureFlagService)); // ✅
|
|
594
|
+
const config = injectSpy<FeatureFlagService>(FeatureFlagService); // ✅
|
|
574
595
|
```
|
|
575
596
|
|
|
576
597
|
---
|
|
@@ -1067,6 +1088,7 @@ mechanisms, and a test that waits on the wrong one fails with a message that nam
|
|
|
1067
1088
|
| effects + `afterNextRender` + CD | `await stable(fixture)` (`…/angular`) | `detectChanges()` alone |
|
|
1068
1089
|
| timers, debounces, polling | `await advanceTimers(ms)` (`…/setup`) | `await Promise.resolve()` |
|
|
1069
1090
|
| a dynamic `import()`, native `async` in a dep | `await flushEventLoop()` / `settleDynamicImport()` | `tick()`, `flushMicrotasks()`, microtasks |
|
|
1091
|
+
| an `httpResource()` / `resource()` / `rxResource` | `await settleResource(r)` (`…/angular`) | `flushEventLoopUntil` — it never ticks |
|
|
1070
1092
|
|
|
1071
1093
|
```ts
|
|
1072
1094
|
import { flushEventLoop, settleDynamicImport } from 'vitest-auto-spy';
|
|
@@ -1088,9 +1110,14 @@ Three rules worth stating outright, because each of them cost a day somewhere:
|
|
|
1088
1110
|
and a non-zero exit code.
|
|
1089
1111
|
|
|
1090
1112
|
`flushEventLoopUntil(isDone, { turns, label })` is the same thing with a condition and a budget —
|
|
1091
|
-
for a
|
|
1092
|
-
|
|
1093
|
-
|
|
1113
|
+
for a chunk becoming reachable, an SDK reporting itself ready, a queue draining. Use it instead of a
|
|
1114
|
+
hand-tuned turn count: the count depends on the dependency, not on the spec, and a condition that
|
|
1115
|
+
never holds fails naming the `label` rather than hanging until the runner's timeout.
|
|
1116
|
+
|
|
1117
|
+
**Not for an Angular `resource()` / `httpResource()`.** Those need a change-detection _tick_, and
|
|
1118
|
+
this helper only takes event-loop turns — a resource awaited through it finishes the whole budget
|
|
1119
|
+
having issued zero requests. `settleResource(resource, { turns, label })` from
|
|
1120
|
+
`vitest-auto-spy/angular` is that wait.
|
|
1094
1121
|
|
|
1095
1122
|
`flushEventLoop(turns?)` takes real event-loop turns even while the timers are faked, without
|
|
1096
1123
|
touching the clock. It is the honest name for the `await vi.advanceTimersByTimeAsync(0)` trick,
|
|
@@ -1160,10 +1187,10 @@ inlined when the mock would be installed, so the real implementation runs and th
|
|
|
1160
1187
|
for the wrong reason or fails somewhere unrelated.
|
|
1161
1188
|
|
|
1162
1189
|
```ts
|
|
1163
|
-
import * as engine from '@app/
|
|
1190
|
+
import * as engine from '@app/pricing-engine';
|
|
1164
1191
|
|
|
1165
|
-
vi.mock('@app/
|
|
1166
|
-
beforeEach(() => assertMocked(engine, { specifier: '@app/
|
|
1192
|
+
vi.mock('@app/pricing-engine');
|
|
1193
|
+
beforeEach(() => assertMocked(engine, { specifier: '@app/pricing-engine', exports: ['createEngine'] }));
|
|
1167
1194
|
```
|
|
1168
1195
|
|
|
1169
1196
|
And when a mocked dependency probes itself with `mod.default ?? mod` — every package that ships both
|
|
@@ -1209,7 +1236,7 @@ result:
|
|
|
1209
1236
|
```ts
|
|
1210
1237
|
provideAutoSpy(FavoritesService, {
|
|
1211
1238
|
returns: { load: of([]) },
|
|
1212
|
-
overrides: { favoritesCacheUpdated$: of(undefined),
|
|
1239
|
+
overrides: { favoritesCacheUpdated$: of(undefined), favoriteItems: [] },
|
|
1213
1240
|
});
|
|
1214
1241
|
|
|
1215
1242
|
provideAutoSpyForToken(PRODUCTS, undefined, { returns: { getProducts: of([]), getById: of(null) } });
|
|
@@ -1332,8 +1359,8 @@ nodes, so the helper silently rips the element out of the fixture it was just as
|
|
|
1332
1359
|
Worth reading before the rest of this section: it has now come up twice in one migration wave, and
|
|
1333
1360
|
both times the failure landed in a different file from its cause.
|
|
1334
1361
|
|
|
1335
|
-
`@Component({ providers: [
|
|
1336
|
-
injector, and a module-level `provideAutoSpy(
|
|
1362
|
+
`@Component({ providers: [DeleteAccountService] })` declares the provider on the **element**
|
|
1363
|
+
injector, and a module-level `provideAutoSpy(DeleteAccountService)` in `configureTestingModule`
|
|
1337
1364
|
loses to it — so the component builds the **real** service. Nothing warns. What fails is whatever
|
|
1338
1365
|
the real service touches first: in the observed case a logger, with
|
|
1339
1366
|
`TypeError: Cannot read properties of undefined (reading 'pipe')`, which names neither the component
|
|
@@ -1343,10 +1370,10 @@ Two things fix it, and which one depends on whether the double is wanted:
|
|
|
1343
1370
|
|
|
1344
1371
|
```ts
|
|
1345
1372
|
// keep a double, but put it where the component will look
|
|
1346
|
-
const menu = overrideComponentProvider(
|
|
1373
|
+
const menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService);
|
|
1347
1374
|
|
|
1348
1375
|
// or take the component's own provider away, so the module-level one is reached again
|
|
1349
|
-
TestBed.overrideComponent(ProfileComponent, { remove: { providers: [
|
|
1376
|
+
TestBed.overrideComponent(ProfileComponent, { remove: { providers: [DeleteAccountService] } });
|
|
1350
1377
|
```
|
|
1351
1378
|
|
|
1352
1379
|
`overrideComponentProvider` is the one to reach for by default — it also queues the component with
|
|
@@ -1371,10 +1398,10 @@ do that, because a testing-module provider loses to one the component declares:
|
|
|
1371
1398
|
```ts
|
|
1372
1399
|
import { overrideAutoSpy, overrideComponentProvider } from 'vitest-auto-spy/angular';
|
|
1373
1400
|
|
|
1374
|
-
const menu = overrideComponentProvider(
|
|
1401
|
+
const menu = overrideComponentProvider(CatalogPageComponent, NavigationBuilderService); // → Spy<NavigationBuilderService>
|
|
1375
1402
|
|
|
1376
1403
|
// or, when the component is already in the testing module:
|
|
1377
|
-
TestBed.configureTestingModule({ … }).overrideProvider(
|
|
1404
|
+
TestBed.configureTestingModule({ … }).overrideProvider(PaymentMethodService, overrideAutoSpy(PaymentMethodService));
|
|
1378
1405
|
```
|
|
1379
1406
|
|
|
1380
1407
|
`overrideProvider(X, provideAutoSpy(X))` is **not** broken, contrary to what this section used to
|
|
@@ -1474,12 +1501,29 @@ spies.get(PricingService).total.mockReturnValue(100);
|
|
|
1474
1501
|
// NOTE: Injector.create() — it does NOT accept EnvironmentProviders (provideHttpClient() etc.)
|
|
1475
1502
|
|
|
1476
1503
|
// zoneless waiting
|
|
1477
|
-
await stable(fixture); // flush effects, then await the fixture
|
|
1504
|
+
await stable(fixture); // flush effects, then await the fixture; fails at 2000 ms naming the cause
|
|
1505
|
+
await stable(fixture, { timeout: 5000, label: 'the products fixture' });
|
|
1478
1506
|
flushEffects(); // the no-fixture half: services, stores, runInInjectionContext
|
|
1479
1507
|
|
|
1508
|
+
// resources — one wait for httpResource(), resource() and rxResource()
|
|
1509
|
+
flushEffects(); // an httpResource issues NO request until something ticks
|
|
1510
|
+
httpTesting.expectOne('/api/products').flush([product]);
|
|
1511
|
+
await settleResource(products, { label: 'the product resource' });
|
|
1512
|
+
|
|
1513
|
+
// ...or skip the request entirely when it is not what the spec is about
|
|
1514
|
+
const products = mockResourceProp(service, 'products', []);
|
|
1515
|
+
products.set([product]); // 'resolved' products.loading() products.fail('offline')
|
|
1516
|
+
expect(products.reload).toHaveBeenCalled(); // reload is spied, and re-issues nothing
|
|
1517
|
+
|
|
1480
1518
|
// signal assertions
|
|
1481
1519
|
registerSignalMatchers(); // once, in the setup file
|
|
1482
1520
|
expect(component.total).toHaveSignalValue(3);
|
|
1521
|
+
|
|
1522
|
+
// resource assertions — value AND status, which is the whole point
|
|
1523
|
+
registerResourceMatchers(); // once, in the setup file
|
|
1524
|
+
expect(component.products).toBeLoading();
|
|
1525
|
+
expect(component.products).toHaveResourceValue([product]);
|
|
1526
|
+
expect(component.products).toHaveResourceError(/503/);
|
|
1483
1527
|
```
|
|
1484
1528
|
|
|
1485
1529
|
Two zoneless traps:
|
|
@@ -1489,6 +1533,19 @@ Two zoneless traps:
|
|
|
1489
1533
|
- `expect(someSignal).toBeTruthy()` passes for **every** signal ever created — a signal is a
|
|
1490
1534
|
function. Use `toHaveSignalValue`, which also rejects the missing-parentheses mistake.
|
|
1491
1535
|
|
|
1536
|
+
And one resource trap, which is the same shape one level up: an `httpResource()` reports `loading`
|
|
1537
|
+
with its **default** value until a tick _and_ a microtask after its response is flushed, so a spec
|
|
1538
|
+
that asserts too early asserts the default and passes. `settleResource` fails instead of passing
|
|
1539
|
+
emptily. Note the order — `flushEffects()` first (the request is issued there, not on creation),
|
|
1540
|
+
then the flush, then the wait.
|
|
1541
|
+
|
|
1542
|
+
`toHaveResourceValue` is the matcher form of that trap and the reason to prefer it over
|
|
1543
|
+
`expect(products.value()).toEqual(...)`: it **fails an unresolved resource even when the default
|
|
1544
|
+
value matches**, and names the status it was in. And when the request is not what the spec is about
|
|
1545
|
+
at all, do not arrange one — `mockResourceProp(service, 'products', [])` replaces the property with
|
|
1546
|
+
a double whose `set` / `fail` / `loading` move it directly, built from real `signal()`s so a
|
|
1547
|
+
`computed()` downstream still recomputes. Nothing is in flight, so there is nothing to await.
|
|
1548
|
+
|
|
1492
1549
|
Per-file timing, to find which specs actually pay for `TestBed`:
|
|
1493
1550
|
|
|
1494
1551
|
```ts
|
|
@@ -1510,8 +1567,10 @@ if (process.env['SPEC_TIMING']) {
|
|
|
1510
1567
|
preload = ["vitest-auto-spy/bun-angular"]
|
|
1511
1568
|
```
|
|
1512
1569
|
|
|
1513
|
-
It re-exports everything in this section except `registerSignalMatchers
|
|
1514
|
-
|
|
1570
|
+
It re-exports everything in this section except `registerSignalMatchers`,
|
|
1571
|
+
`registerResourceMatchers`, `mockSignalProp` / `mockResourceProp` and the TestBed diagnostics — the
|
|
1572
|
+
matchers and diagnostics need the runner's `expect.extend` and suite-level hooks, and the `mock*Prop`
|
|
1573
|
+
family is not re-exported there either.
|
|
1515
1574
|
|
|
1516
1575
|
---
|
|
1517
1576
|
|
|
@@ -1539,8 +1598,8 @@ which platform and which providers is not this library's decision.
|
|
|
1539
1598
|
### A dependency behind an `InjectionToken`
|
|
1540
1599
|
|
|
1541
1600
|
```ts
|
|
1542
|
-
providers: [provideAutoSpyForToken(
|
|
1543
|
-
const
|
|
1601
|
+
providers: [provideAutoSpyForToken(PASSCODE_SERVICE_TOKEN)];
|
|
1602
|
+
const passcode = injectSpy(PASSCODE_SERVICE_TOKEN); // Spy<PasscodeService>
|
|
1544
1603
|
```
|
|
1545
1604
|
|
|
1546
1605
|
A token typed with an interface has no class to read, so the habit is a `…Mock` class written in the
|
|
@@ -1802,7 +1861,9 @@ packages, which a subpath export can never be.
|
|
|
1802
1861
|
| a stub that works in the first test of the file and in no other | installed at `describe` level or in `beforeAll`, then restored away | install it in `beforeEach`, or `installPerTest(() => stub…())` |
|
|
1803
1862
|
| a third-party library failing every other run, no test named | a test sealed a global with `Object.defineProperty` (non-configurable) | `setupAutoSpy({ guardGlobals: 'throw' })` names the file; then `mockValueProp` |
|
|
1804
1863
|
| `expected [ { at: 1, …(5) }, …(8) ] to deeply equal [ { …(6) }, … ]` | one field moved in every element — usually a frozen clock or an id | `expect(diffByField(actual, expected)).toBeUndefined()` |
|
|
1805
|
-
| a hand-tuned number of turns waiting for a `resource()` to load
|
|
1864
|
+
| a hand-tuned number of turns waiting for a `resource()` to load | a resource needs a change-detection **tick**, not event-loop turns; `flushEventLoopUntil` never ticks and the resource never even issues its request | `flushEffects()`, flush the request, then `await settleResource(r, { label })` |
|
|
1865
|
+
| a `resource()` assertion that passes but reads the **default** value | the spec asserted before the resource left `loading` | `await settleResource(r, { label })` — it fails loudly instead |
|
|
1866
|
+
| a spec dying on the runner's 5 s file timeout right after `await stable(fixture)` | the fixture never stabilised — an unflushed request, a real `setInterval` | `stable` now fails at 2000 ms naming the cause; raise `{ timeout }` only once neither is true |
|
|
1806
1867
|
| `flushEventLoopUntil` timing out on the **first** such test only, the rest green | a cold dynamic `import()` outran the turn budget; later tests hit the module cache | `await settleDynamicImport(() => import('…'))` — await the module, do not count turns |
|
|
1807
1868
|
| a template error in a `describe` that never patched anything | a spec `afterEach` threw and skipped `setupAutoSpy`'s, so a `mock*Prop` patch travelled | upgrade — an `onTestFinished` net restores it and names the cause (§10) |
|
|
1808
1869
|
| `Property 'mockReturnValue' does not exist on type 'never'` | a generic method with a conditional return type; the spy collapsed | upgrade — fixed in the types; the member now keeps its sync helper bundle |
|
|
@@ -1898,11 +1959,20 @@ Run what the project actually has — check its `package.json` first.
|
|
|
1898
1959
|
```bash
|
|
1899
1960
|
npx vitest run path/to/file.spec.ts # or: bun test path/to/file.test.ts
|
|
1900
1961
|
npx tsc --noEmit # Spy<T> mistakes are compile errors, not runtime ones
|
|
1962
|
+
npx vitest-auto-spy doctor # suite-level defects that never fail a run
|
|
1901
1963
|
```
|
|
1902
1964
|
|
|
1903
1965
|
Type errors matter here more than usual: most of this library's guarantees are type-level, so a
|
|
1904
1966
|
suite that runs green but does not type-check is not done.
|
|
1905
1967
|
|
|
1968
|
+
`doctor` is read-only and finds what a green run cannot: a `tsconfig` `include` pattern that
|
|
1969
|
+
matches no file (so it type-checks nothing while `tsc --noEmit` still reports success), a
|
|
1970
|
+
production module importing a `*.spec.ts`, a spec importing another spec, a `@jest-environment`
|
|
1971
|
+
pragma the runner never reads, and configuration left behind for a runner that is gone. It is
|
|
1972
|
+
worth one run after any large edit to a test suite — especially after a codemod, which is where
|
|
1973
|
+
the eaten glob below came from. Full reference:
|
|
1974
|
+
<https://asdalexey.github.io/vitest-auto-spy/utilities/cli>.
|
|
1975
|
+
|
|
1906
1976
|
### If you are writing a codemod over specs
|
|
1907
1977
|
|
|
1908
1978
|
Two traps, both found the hard way on rxjs-heavy code.
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
16
16
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
17
17
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
18
18
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
19
|
-
[](#install)
|
|
20
20
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
21
21
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
22
22
|
[](./LICENSE)
|
|
@@ -48,20 +48,21 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
48
48
|
- 📡 First-class RxJS `Observable` spying (`nextWith`, `nextWithValues`, `throwWith`, …)
|
|
49
49
|
- ⚙️ Getter / setter spies via `accessorSpies`
|
|
50
50
|
- 🧰 DI & mocking utilities — `provideAutoSpy` / `injectSpy` (Angular, NestJS, Vue), `createFunctionSpy`, `mockReadonlyProp` for signals
|
|
51
|
-
- ⚡ Angular speed & zoneless helpers — `renderShallow` (**1.7×** on real component specs), `createWithAutoSpies`, `stable` / `flushEffects`, `toHaveSignalValue`, per-file `TestBed` timings
|
|
51
|
+
- ⚡ Angular speed & zoneless helpers — `renderShallow` (**1.7×** on real component specs), `createWithAutoSpies`, `stable` / `flushEffects`, `settleResource` for `httpResource()`, `toHaveSignalValue`, per-file `TestBed` timings
|
|
52
52
|
- 🧱 The providers a testing module cannot reach — `overrideComponentProvider`, `provideAutoSpyForToken`, `assertNgModuleScopes`, `createDirectiveHost`
|
|
53
53
|
- 📡 Observable assertions that fail on silence — `expectEmission` / `expectEmissions` / `expectNoEmission` / `expectCompletion` / `expectError`, no rxjs required, Angular `output()` included
|
|
54
54
|
- 🏗️ Doubles for what the code builds itself — `mockConstructor` / `stubConstructor` for `new`, plus `stubMediaElement`, `stubAbortController` and the observer stubs
|
|
55
55
|
- ⏳ Waiting that is not a guess — `flushEventLoop`, `settleDynamicImport`, `flushEventLoopUntil`, and a clock that survives fake timers (`mockSystemTime`, `useCountingClock`)
|
|
56
56
|
- 🌀 `fakeAsync` / `waitForAsync` on Vitest — one import of `vitest-auto-spy/zone`; zone.js stays out of every other entry
|
|
57
57
|
- 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
|
|
58
|
-
- 🧾 Fixtures without casts — deep-partial `createMock`, `narrow()`, `withOverrides()`, `asInstances()`
|
|
58
|
+
- 🧾 Fixtures without casts — deep-partial `createMock`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
|
|
59
59
|
- 🚚 A migration you can verify — `compareTestRuns` on the two JSON reports, `diffByField` for the assertion the reporter collapses
|
|
60
60
|
- 📏 Lint rules and one-line test-run hygiene — twelve rules in `vitest-auto-spy/eslint-plugin` (two `--fix`, three suggestions), `setupAutoSpy()`
|
|
61
61
|
- 🩺 [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
|
|
62
|
+
- 🔎 [`npx vitest-auto-spy doctor`](#the-cli--doctor-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
|
|
62
63
|
- 🔇 Console spies — `import { consoleInfoSpy } from 'vitest-auto-spy/console'` silences `console` and asserts its calls
|
|
63
64
|
- 🧭 [**Spec patterns**](https://asdalexey.github.io/vitest-auto-spy/recipes) — the shapes a ~370-file Angular suite converged on, and the traps that only surface at scale
|
|
64
|
-
- 🤖 Built for AI agents too — an offline [`AGENTS.md`](#using-this-library-with-an-ai-agent) inside the package, a [per-agent map](#which-file-your-agent-reads) for **Claude Code**, **OpenAI Codex**, **GLM/z.ai**, **Cursor**, **Copilot**, **Gemini CLI** and the rest, `llms.txt` on the docs site, a Claude Code skill, and errors that name their own fix
|
|
65
|
+
- 🤖 Built for AI agents too — one `npx vitest-auto-spy init` writes the pointer into the files your agents actually read and specialises it for this repository, backed by an offline [`AGENTS.md`](#using-this-library-with-an-ai-agent) inside the package, a [per-agent map](#which-file-your-agent-reads) for **Claude Code**, **OpenAI Codex**, **GLM/z.ai**, **Cursor**, **Copilot**, **Gemini CLI** and the rest, `llms.txt` on the docs site, a Claude Code skill, and errors that name their own fix
|
|
65
66
|
- 🟢 100% test coverage, **zero runtime dependencies** (in-tree arg serializer, no `javascript-stringify`)
|
|
66
67
|
|
|
67
68
|
## Table of contents
|
|
@@ -69,6 +70,9 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
69
70
|
- [Install](#install)
|
|
70
71
|
- [Requirements](#requirements)
|
|
71
72
|
- [Peer dependencies](#peer-dependencies)
|
|
73
|
+
- [The CLI — `doctor` and `init`](#the-cli--doctor-and-init)
|
|
74
|
+
- [`doctor` — defects that never fail](#doctor--defects-that-never-fail)
|
|
75
|
+
- [`init` — the pointer an agent reads](#init--the-pointer-an-agent-reads)
|
|
72
76
|
- [Using this library with an AI agent](#using-this-library-with-an-ai-agent)
|
|
73
77
|
- [Point your agent at it once](#point-your-agent-at-it-once)
|
|
74
78
|
- [Which file your agent reads](#which-file-your-agent-reads)
|
|
@@ -112,6 +116,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
112
116
|
- [Shallow component rendering](#shallow-component-rendering)
|
|
113
117
|
- [Building a class with auto-spied dependencies](#building-a-class-with-auto-spied-dependencies)
|
|
114
118
|
- [Zoneless waiting](#zoneless-waiting)
|
|
119
|
+
- [Settling a `resource()` or `httpResource()`](#settling-a-resource-or-httpresource)
|
|
115
120
|
- [Asserting a signal's value](#asserting-a-signals-value)
|
|
116
121
|
- [Where a spec spends its time](#where-a-spec-spends-its-time)
|
|
117
122
|
- [NestJS](#nestjs)
|
|
@@ -189,6 +194,65 @@ them only for the matching entry point. The package itself has **zero runtime de
|
|
|
189
194
|
| `rxjs` | `vitest-auto-spy/rxjs` observable spies (and `Spy<T>` type-checking) — `>=7`, **no upper bound** (the rxjs 8 line included) | yes |
|
|
190
195
|
| `@angular/core` | `vitest-auto-spy/angular` helpers | yes |
|
|
191
196
|
|
|
197
|
+
## The CLI — `doctor` and `init`
|
|
198
|
+
|
|
199
|
+
The package ships one executable, with no dependencies and nothing to configure:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx vitest-auto-spy doctor # read-only. Exits 1 when it finds something
|
|
203
|
+
npx vitest-auto-spy init # writes the agent instructions pointer
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `doctor` — defects that never fail
|
|
207
|
+
|
|
208
|
+
Every check shares one property: **nothing consumes the result**. The suite is green,
|
|
209
|
+
`tsc --noEmit` reports zero errors, and the only reader of the stale thing is whoever opens the
|
|
210
|
+
file. That is why they survive for years, and why a per-file linter cannot find most of them — the
|
|
211
|
+
evidence is spread across files.
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
$ npx vitest-auto-spy doctor
|
|
215
|
+
vitest-auto-spy doctor — /work/app
|
|
216
|
+
1284 files, runner: vitest, entry: vitest-auto-spy/angular
|
|
217
|
+
|
|
218
|
+
error tsconfig-glob-matches-nothing libs/users/tsconfig.spec.json
|
|
219
|
+
The "include" pattern "src*.spec.ts" matches no file.
|
|
220
|
+
→ A pattern that matches nothing type-checks nothing, and `tsc --noEmit` still reports
|
|
221
|
+
zero errors. Fix the glob or delete the entry.
|
|
222
|
+
|
|
223
|
+
3 errors, 4 warnings, 1 note
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Check | What it finds |
|
|
227
|
+
| ------------------------------ | -------------------------------------------------------------------- |
|
|
228
|
+
| `tsconfig-glob-matches-nothing` | An `include` pattern that matches no file — so it type-checks nothing |
|
|
229
|
+
| `tsconfig-file-missing` | A `files` entry naming a file that is gone |
|
|
230
|
+
| `spec-imported-by-non-spec` | A production module importing a `*.spec.ts` |
|
|
231
|
+
| `spec-exports-fixture` | A spec importing another spec, whose hooks then run in a foreign file |
|
|
232
|
+
| `foreign-runner-pragma` | `@jest-environment` left in a spec, which Vitest never reads |
|
|
233
|
+
| `dead-runner-config` | `jest.config.*` / `karma.conf.*` for a runner that is not installed |
|
|
234
|
+
| `orphan-runner-file` | A setup file only that dead config referenced |
|
|
235
|
+
| `angular-build-splitting-off` | `@angular/build` in `[22.1.5, 22.1.7)` — the OOM under `--coverage` |
|
|
236
|
+
| `no-agent-instructions` | No instruction file names the package. A note, not an error |
|
|
237
|
+
|
|
238
|
+
The check that motivated the tool: a spec showing `Cannot find name 'vi'` in the editor while
|
|
239
|
+
`tsc --noEmit` reported zero errors. A migration codemod editing `include` had eaten a `/**`,
|
|
240
|
+
turning `src/**/*.spec.ts` into `src*.spec.ts` — a valid glob that matches nothing. Nine of 152
|
|
241
|
+
spec tsconfigs still covered their specs.
|
|
242
|
+
|
|
243
|
+
`doctor` never writes. There is no `--fix`.
|
|
244
|
+
|
|
245
|
+
### `init` — the pointer an agent reads
|
|
246
|
+
|
|
247
|
+
No coding agent scans dependencies for instructions, so the `AGENTS.md` and the skill shipped
|
|
248
|
+
inside this package's tarball are never discovered on their own. `init` writes the pointer into
|
|
249
|
+
the files that *are* read — `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, a Claude Code skill stub, and a
|
|
250
|
+
glob-scoped rule file for each tool whose directory already exists — and specialises it for this
|
|
251
|
+
repository's runner, framework and setup file. Everything sits between markers, so a re-run is a
|
|
252
|
+
no-op and `--uninstall` puts the files back.
|
|
253
|
+
|
|
254
|
+
Full reference, including the flags and the CI form: **[The CLI](https://asdalexey.github.io/vitest-auto-spy/utilities/cli)**.
|
|
255
|
+
|
|
192
256
|
## Using this library with an AI agent
|
|
193
257
|
|
|
194
258
|
Most tests are now written with an assistant in the loop, so this package ships documentation
|
|
@@ -244,7 +308,14 @@ already exists.
|
|
|
244
308
|
|
|
245
309
|
### Install it in your agent
|
|
246
310
|
|
|
247
|
-
|
|
311
|
+
One command covers every tool in that table, and specialises the text for this repository:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
npx vitest-auto-spy init # write it
|
|
315
|
+
npx vitest-auto-spy init --check # CI: fail when it is missing or out of date
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
By hand, the same thing is two commands at the repository root:
|
|
248
319
|
|
|
249
320
|
```bash
|
|
250
321
|
# 1 — AGENTS.md: Codex, Cursor, Copilot, Cline, Windsurf, Zed, OpenCode, Qwen, Roo, Junie, Aider…
|
|
@@ -711,7 +782,7 @@ Node / Bun / React / Vue project pulls **neither rxjs nor Angular into its runti
|
|
|
711
782
|
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | :----: |
|
|
712
783
|
| `vitest-auto-spy` | `createSpyFromClass`, `createAutoMock`, `createFunctionSpy`, sync + promise + accessor spies, `errorHandler`, types | `vitest` | ✅ |
|
|
713
784
|
| `vitest-auto-spy/rxjs` | observable spies (`nextWith`, `nextWithValues`, `observablePropsToSpyOn`, …) + `createObservableWithValues` | `rxjs` | ✅ |
|
|
714
|
-
| `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, the `mock*Prop` helpers, signal matchers, TestBed diagnostics | `@angular/core` | ✅ |
|
|
785
|
+
| `vitest-auto-spy/angular` | `provideAutoSpy`, `injectSpy`, `renderShallow`, `createWithAutoSpies`, `stable`/`flushEffects`, `settleResource`, `mockResourceProp`, the `mock*Prop` helpers, signal & resource matchers, TestBed diagnostics | `@angular/core` | ✅ |
|
|
715
786
|
| `vitest-auto-spy/bun` | the same core, driven by Bun's `bun:test` mocks | `bun:test` | ✅ |
|
|
716
787
|
| `vitest-auto-spy/bun-angular` | Angular's `TestBed` under `bun test` — DOM, JIT `templateUrl` resolution and a zoneless environment, from one preload | `bun:test`, `@angular/core` | ✅ |
|
|
717
788
|
| `vitest-auto-spy/node` | the same core, driven by `node:test`'s `mock.fn()` | `node:test` | ✅ |
|
|
@@ -1255,6 +1326,60 @@ app the state that matters is signal-derived and effects are what move it forwar
|
|
|
1255
1326
|
both, in the right order; `flushEffects` prefers `TestBed.tick()` (Angular ≥ 20) and falls back to
|
|
1256
1327
|
`ApplicationRef.tick()`.
|
|
1257
1328
|
|
|
1329
|
+
The wait is bounded: `stable` gives the fixture **2000 ms** and then throws the cause, instead of
|
|
1330
|
+
letting the runner report a 5 s file-level timeout that names neither the helper nor the fixture.
|
|
1331
|
+
Pass `{ timeout, label }` to change either; `{ timeout: 0 }` waits indefinitely. The watchdog runs
|
|
1332
|
+
on a timer captured at import, so `vi.useFakeTimers()` cannot freeze it.
|
|
1333
|
+
|
|
1334
|
+
#### Settling a `resource()` or `httpResource()`
|
|
1335
|
+
|
|
1336
|
+
```ts
|
|
1337
|
+
import { flushEffects, settleResource } from 'vitest-auto-spy/angular';
|
|
1338
|
+
|
|
1339
|
+
const products = TestBed.runInInjectionContext(() => httpResource<Product[]>(() => '/api/products'));
|
|
1340
|
+
|
|
1341
|
+
flushEffects(); // the request is issued here — not when the resource was created
|
|
1342
|
+
TestBed.inject(HttpTestingController).expectOne('/api/products').flush([product]);
|
|
1343
|
+
await settleResource(products, { label: 'the product resource' });
|
|
1344
|
+
|
|
1345
|
+
expect(products.value()).toEqual([product]);
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
Angular's resource primitives need a **different wait each** — measured on 21.2.17, an
|
|
1349
|
+
`httpResource` settles one tick + one microtask after its flush, a plain `resource()` takes two
|
|
1350
|
+
rounds of the same, and neither has made a request at all until something ticks. Getting it wrong
|
|
1351
|
+
asserts against the resource's _default_ value, which is a green test proving nothing.
|
|
1352
|
+
`settleResource` is the loop both converge under, with a turn budget and a failure that names the
|
|
1353
|
+
resource and the flush it is missing.
|
|
1354
|
+
|
|
1355
|
+
`flushEventLoopUntil` cannot do this: it takes real event-loop turns and never ticks, so a resource
|
|
1356
|
+
awaited through it finishes the budget having issued zero requests.
|
|
1357
|
+
|
|
1358
|
+
#### Driving a resource with no HTTP at all
|
|
1359
|
+
|
|
1360
|
+
```ts
|
|
1361
|
+
import { mockResourceProp, registerResourceMatchers } from 'vitest-auto-spy/angular';
|
|
1362
|
+
|
|
1363
|
+
const products = mockResourceProp(service, 'products', []);
|
|
1364
|
+
|
|
1365
|
+
products.set([product]); // 'resolved'
|
|
1366
|
+
products.loading(); // back in flight
|
|
1367
|
+
products.fail('offline'); // 'error', error() is Error('offline')
|
|
1368
|
+
|
|
1369
|
+
expect(products.reload).toHaveBeenCalled(); // reload is spied and re-issues nothing
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
Everything above is the answer when the request _is_ the point. Often it is not — the spec is about
|
|
1373
|
+
the component's own logic and the value was chosen in advance. `mockResourceProp` replaces the
|
|
1374
|
+
property with a double the spec moves directly, so nothing is ever in flight: no tick, no
|
|
1375
|
+
`HttpTestingController`, no budget. It is built from real `signal()`s, so a `computed()` reading
|
|
1376
|
+
`products.value()` still recomputes and an `effect()` still runs. Undone by `restoreMockedProps()`.
|
|
1377
|
+
|
|
1378
|
+
And `registerResourceMatchers()` adds `toBeLoading` / `toHaveResourceValue` / `toHaveResourceError`,
|
|
1379
|
+
which read the value **and** the status. `toHaveResourceValue` is the one that earns its place: it
|
|
1380
|
+
fails an unresolved resource **even when its default value matches**, which is exactly the assertion
|
|
1381
|
+
`expect(products.value()).toEqual([])` lets through.
|
|
1382
|
+
|
|
1258
1383
|
#### Asserting a signal's value
|
|
1259
1384
|
|
|
1260
1385
|
```ts
|
|
@@ -1468,12 +1593,15 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
1468
1593
|
| `assertMocked(namespace, opts?)` | core | Fail when the `vi.mock()` a spec relies on silently did not apply (a bundled alias, `isolate: false`) |
|
|
1469
1594
|
| `moduleNamespace(exports, opts?)` | core | The `vi.mock` factory result an interop probe recognises — `default` + `__esModule` in place |
|
|
1470
1595
|
| `diffByField(actual, expected)` | core | Which field of an array of records moved, and in how many elements — the diff the reporter collapses |
|
|
1596
|
+
| `captureArg<T>()` | core | Take hold of a callback or config the code under test built, instead of describing its shape — assertions only, never `calledWith` |
|
|
1471
1597
|
| `asInstances(...spies)` | core | `asInstance` for a whole argument list — one edit against one compiler error, not five |
|
|
1472
1598
|
| `narrow(value, guard)` / `narrow.byKey` / `narrow.observable` | core | The branch of a union a test knows it got, failing with the shape the value actually had |
|
|
1473
1599
|
| `withOverrides(model, overrides?)` | core | A fixture from a model instance: its getters read once, as data — a spread drops them |
|
|
1474
1600
|
| `compareTestRuns(a, b, root?)` | core | Whether a migration lost a test — the set of `file::name`, which matching counters cannot answer |
|
|
1475
1601
|
| `provideAutoSpyForToken(TOKEN, overrides?)` | `/angular` | The provider for a dependency behind an `InjectionToken` — no stand-in class to write |
|
|
1476
1602
|
| `createDirectiveHost({ template, scope, props })` | `/angular` | A standalone host for a directive under test, with its scope where the compiler reads it |
|
|
1603
|
+
| `mockResourceProp(obj, prop, initial)` | `/angular` | Drive a resource with no HTTP — `set` / `fail` / `loading`, plus a spied `reload` |
|
|
1604
|
+
| `registerResourceMatchers()` | `/angular` | Adds `toBeLoading` / `toHaveResourceValue` / `toHaveResourceError`; the value matcher fails an unresolved resource |
|
|
1477
1605
|
| `registerDirectiveMatchers()` | `/angular` | Adds `expect(fixture).toHaveDirectiveApplied(Directive, selector?)` |
|
|
1478
1606
|
| `installProxyZonePatch(opts?)` | `/zone` | `fakeAsync` / `waitForAsync` on Vitest — the patch `zone.js/testing` does not ship; `scope: 'callback'` per callback |
|
|
1479
1607
|
| `autoMocked<T>(overrides?)` | core | `createAutoMock` typed as `T & Spy<T>`, for a collaborator passed as an argument rather than injected |
|
|
@@ -1931,7 +2059,8 @@ Both are the same object at runtime; only the view changes.
|
|
|
1931
2059
|
| `describeDuplicateCopies()` / `getPackageCopies()` | The duplicate-install report, and the copies behind it |
|
|
1932
2060
|
| `renderShallow(Component, opts?)` _(Angular)_ | `TestBed` component, minus its children and (by default) its template |
|
|
1933
2061
|
| `createWithAutoSpies(Class, opts?)` _(Angular)_ | Build a class through Angular DI with every unprovided token auto-spied |
|
|
1934
|
-
| `stable(fixture)` / `flushEffects()` _(Angular)_
|
|
2062
|
+
| `stable(fixture, opts?)` / `flushEffects()` _(Angular)_ | Zoneless waiting: flush effects, then await the fixture, with a 2 s budget that names the cause |
|
|
2063
|
+
| `settleResource(resource, opts?)` _(Angular)_ | Tick until an `httpResource()` / `resource()` / `rxResource()` leaves `loading` |
|
|
1935
2064
|
| `registerSignalMatchers()` _(Angular)_ | Adds `expect(sig).toHaveSignalValue(value)` |
|
|
1936
2065
|
| `enableTestBedDiagnostics(opts?)` _(Angular)_ | Per-file report of how much of a spec's time went into `TestBed` |
|
|
1937
2066
|
| `setupAngularTestEnv(opts)` _(Angular)_ | Zone and zoneless spec files in one worker, switching platforms per file |
|