vitest-auto-spy 3.13.1 → 3.15.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 +330 -98
- package/README.md +380 -197
- package/dist/angular.d.ts +97 -8
- package/dist/angular.js +96 -6
- package/dist/bun-angular.d.ts +4 -4
- package/dist/bun-angular.js +8 -7
- package/dist/bun.d.ts +4 -4
- package/dist/bun.js +5 -4
- package/dist/{chunk-PCP6A6PD.js → chunk-3OX44LME.js} +6 -16
- package/dist/{chunk-ZY7KPBJJ.js → chunk-AQSVZK2R.js} +33 -3
- package/dist/{chunk-C3IZLTSJ.js → chunk-CR5Q3YMQ.js} +4 -3
- package/dist/{chunk-WVW4XRL5.js → chunk-DMOJ4NZH.js} +1 -1
- package/dist/{chunk-CEVEKNJZ.js → chunk-LIOV4WZK.js} +2 -2
- package/dist/chunk-MAKQSO2R.js +162 -0
- package/dist/chunk-PC3FAZT3.js +14 -0
- package/dist/chunk-XJ3UMJN3.js +8 -0
- package/dist/cli.js +689 -182
- package/dist/console.d.ts +1 -1
- package/dist/console.js +1 -1
- package/dist/eslint-plugin.cjs +430 -29
- package/dist/eslint-plugin.js +430 -29
- package/dist/{expect-emission-DMx5uXie.d.ts → expect-emission-COcg6YzT.d.ts} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -4
- package/dist/jasmine-compat.d.ts +186 -0
- package/dist/jasmine-compat.js +1 -0
- package/dist/jasmine.d.ts +210 -0
- package/dist/jasmine.js +260 -0
- package/dist/nestjs.d.ts +2 -2
- package/dist/nestjs.js +4 -3
- package/dist/node.cjs +27 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js +5 -4
- package/dist/observer-spy.d.ts +133 -0
- package/dist/observer-spy.js +158 -0
- package/dist/{prop-mock-DyjtZVoA.d.ts → prop-mock-CEBKBA7k.d.ts} +1 -1
- package/dist/react.d.ts +3 -3
- package/dist/react.js +5 -4
- package/dist/rxjs.d.ts +2 -2
- package/dist/rxjs.js +1 -0
- package/dist/setup.d.ts +21 -2
- package/dist/setup.js +44 -12
- package/dist/shared-state.js +19 -7
- package/dist/svelte.d.ts +3 -3
- package/dist/svelte.js +5 -4
- package/dist/{track-injections-BQndbppn.d.ts → track-injections-BU-WSjji.d.ts} +1 -1
- package/dist/{types-Br-Usge9.d.ts → types-W5kEgzND.d.ts} +38 -7
- package/dist/vue.d.ts +4 -4
- package/dist/vue.js +6 -5
- package/dist/{zoneless-CEhs7o9p.d.ts → zoneless-DzUXGIYb.d.ts} +3 -3
- package/package.json +32 -3
- package/skills/vitest-auto-spy/SKILL.md +63 -3
package/AGENTS.md
CHANGED
|
@@ -49,14 +49,19 @@ adapter installed and spies fail at runtime.
|
|
|
49
49
|
| Vue / Pinia | `vitest-auto-spy/vue` |
|
|
50
50
|
| Svelte | `vitest-auto-spy/svelte` |
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Five add-ons, orthogonal to the runner:
|
|
53
53
|
|
|
54
|
-
| Add-on
|
|
55
|
-
|
|
|
56
|
-
| Observable spies
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
54
|
+
| Add-on | Import | Needed for |
|
|
55
|
+
| ----------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
|
|
57
|
+
| observer-spy shim | `vitest-auto-spy/observer-spy` | `subscribeSpyTo` — the `@hirez_io/observer-spy` surface (§20). Its own entry so `/rxjs` does not carry it |
|
|
58
|
+
| Console spies | `vitest-auto-spy/console` | silent typed spies over the global `console` |
|
|
59
|
+
| Setup helpers | `vitest-auto-spy/setup` | `setupAutoSpy()`, `setupFakeTimers()` |
|
|
60
|
+
| Zone patch | `import 'vitest-auto-spy/zone'` | `fakeAsync` / `waitForAsync` on Vitest (§14) |
|
|
61
|
+
| jasmine compat | `vitest-auto-spy/jasmine` | `.and` / `.calls` / `.withArgs`, the `jasmine` namespace (§20) |
|
|
62
|
+
|
|
63
|
+
`vitest-auto-spy/jasmine` is Vitest-only, because it registers the Vitest adapter. On `bun test` and
|
|
64
|
+
`node --test` call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` instead.
|
|
60
65
|
|
|
61
66
|
The package is **ESM**. Only `vitest-auto-spy/node` and `vitest-auto-spy/eslint-plugin` also ship a
|
|
62
67
|
CommonJS build; every other subpath is ESM-only (a `require()` of a Vitest-backed entry always threw —
|
|
@@ -344,7 +349,7 @@ Every spied method is a real runner mock, so `mockReturnValue`, `mockImplementat
|
|
|
344
349
|
|
|
345
350
|
| Return type | Helpers added |
|
|
346
351
|
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
347
|
-
| anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)`, `mustBeCalledWith(...args)` → same
|
|
352
|
+
| anything | `calledWith(...args)` → `.mockReturnValue(v)` / `.returnValue(v)` / `.failWith(err)`, `mustBeCalledWith(...args)` → same, `failWith(err)` |
|
|
348
353
|
| `Promise<T>` | `resolveWith(v)`, `rejectWith(v)`, `resolveWithPerCall([{ value }, …])` |
|
|
349
354
|
| `Observable<T>` | `nextWith(v)`, `nextOneTimeWith(v)`, `nextWithValues(configs)`, `nextWithPerCall(configs)`, `throwWith(v)`, `complete()`, `returnSubject()` |
|
|
350
355
|
|
|
@@ -374,8 +379,24 @@ feed.items$.nextWithValues([{ value: a }, { value: b, delay: 100 }, { complete:
|
|
|
374
379
|
const [first$, second$] = feed.watch$.nextWithPerCall([{ value: 'a' }, { value: 'b', doNotComplete: true }]);
|
|
375
380
|
feed.items$.throwWith('FAKE ERROR');
|
|
376
381
|
const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the helpers miss
|
|
382
|
+
|
|
383
|
+
// throwing — `failWith`, on a spy of any return type
|
|
384
|
+
cart.checkout.failWith(new HttpErrorResponse({ status: 500 })); // every call throws
|
|
385
|
+
cart.checkout.calledWith(BAD_ID).failWith(new Error('unknown cart')); // only these arguments throw
|
|
377
386
|
```
|
|
378
387
|
|
|
388
|
+
`failWith` is the sync counterpart of `rejectWith`, and it is **not** called `throwWith` — that name
|
|
389
|
+
belongs to the observable helper above, which errors the stream. At runtime every spy carries every
|
|
390
|
+
bundle; only the return type in `Spy<T>` tells them apart, so one name for both would mean whichever
|
|
391
|
+
is attached last silently wins.
|
|
392
|
+
|
|
393
|
+
On Vitest, `mockThrow` / `mockThrowOnce` (4.1) do the spy-level half of this too. `failWith` exists
|
|
394
|
+
because Bun and `node:test` ship neither, and because **no** runtime can make one `calledWith` chain
|
|
395
|
+
throw while its siblings answer normally — `mockImplementation` replaces the whole dispatch, which is
|
|
396
|
+
the opposite of configuring one set of arguments. A `failWith` supersedes a `resolveWith` /
|
|
397
|
+
`nextWith` / per-call batch configured before it, and is superseded by one configured after, so the
|
|
398
|
+
outcome never depends on the order the spec happens to be written in.
|
|
399
|
+
|
|
379
400
|
An exact argument list is matched before the asymmetric configs, and those are tried in
|
|
380
401
|
registration order — a narrow config written first keeps its calls. Two matchers count as the same
|
|
381
402
|
argument when they accept the same values (same matcher class, sample and inversion), which is what
|
|
@@ -917,7 +938,21 @@ setupAutoSpy({ strayTimers: true }); // wrap the schedulers, sweep the survivors
|
|
|
917
938
|
The pieces are exported too — `trackStrayTimers()` (idempotent, returns the undo),
|
|
918
939
|
`cancelStrayTimers()` (returns how many it cancelled) and `countStrayTimers()`, all from
|
|
919
940
|
`vitest-auto-spy/setup`. Use `expect(countStrayTimers()).toBe(0)` in an `afterEach` to make a leak
|
|
920
|
-
fail rather than be tidied away
|
|
941
|
+
fail rather than be tidied away, or take the per-file count from the sweep itself:
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
setupAutoSpy({ strayTimers: true, onStrayTimers: ({ cancelled }) => expect(cancelled).toBe(0) });
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
**With Vitest 4.1's `--detect-async-leaks`, run one or the other — not both silently.** The two
|
|
948
|
+
arrive at the same timer from opposite ends and the quiet one wins: the sweep cancels in `afterAll`,
|
|
949
|
+
Vitest collects its leaks afterwards, and a cancelled timeout is no longer referenced, so the run
|
|
950
|
+
reports **no leaks** for a file that leaks. Cancelling is still the right default — a callback firing
|
|
951
|
+
during a later file is the more expensive failure — so when both are on and no `onStrayTimers` is
|
|
952
|
+
given, the sweep prints one line to stderr saying how many it took away. To see _where_ each one was
|
|
953
|
+
scheduled, re-run that file with `strayTimers` off and read Vitest's report; the code frame points
|
|
954
|
+
at the `setTimeout` in the spec, because the library's own scheduler wrappers go through
|
|
955
|
+
`vi.defineHelper` and are dropped from the stack.
|
|
921
956
|
|
|
922
957
|
**The one that keeps a suite green while it is wrong:** zone.js replaces the global `Promise`, and a
|
|
923
958
|
rejection nobody handled is drained into `console.error` and no further — it never reaches
|
|
@@ -939,6 +974,12 @@ undo), `flushStrayRejections()` (takes what was captured and starts again from e
|
|
|
939
974
|
`countStrayRejections()`. The `no-floating-assertion` lint rule catches the commonest shape before
|
|
940
975
|
it ever runs (§16).
|
|
941
976
|
|
|
977
|
+
**`onUnhandledError` is not this.** Vitest 4.0 added a config callback for errors the _runner_ hears
|
|
978
|
+
about, and under zone.js the runner never hears about these at all — zone.js drains the rejection
|
|
979
|
+
into `console.error` before `process.on('unhandledRejection')` would fire, so there is nothing for
|
|
980
|
+
that callback to filter. Use `onUnhandledError` to triage the native failures Vitest already reports;
|
|
981
|
+
use `strayRejections` to find the ones it never sees.
|
|
982
|
+
|
|
942
983
|
A rejection the runner has **already** blamed the finished test for is not reported again. An
|
|
943
984
|
`async` test that fails an assertion leaves its own `AssertionError` in both places, so a red run
|
|
944
985
|
used to print two messages per failure and the second one sent the reader hunting for a defect that
|
|
@@ -970,7 +1011,7 @@ export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
|
|
|
970
1011
|
The mocks it keeps, it also guards. Staying registered means `vi.resetAllMocks()` reaches them too —
|
|
971
1012
|
it walks the same set and calls `mockReset()`, which puts an implementation back only when it was
|
|
972
1013
|
passed to `vi.fn(implementation)`; behaviour chained on with `.mockReturnValue(…)` or
|
|
973
|
-
`.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a
|
|
1014
|
+
`.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a _different_ file later
|
|
974
1015
|
in the same worker, inside application code, as `Cannot read properties of undefined` against a
|
|
975
1016
|
shared double that spec never touched — and `vi.restoreAllMocks()` does not cause it (Vitest 4 walks
|
|
976
1017
|
`MOCK_RESTORE` there, which only `vi.spyOn` writes to), so probing with that one comes back green and
|
|
@@ -1349,6 +1390,44 @@ const myService = injectSpy(MyService); // Spy<MyService>
|
|
|
1349
1390
|
`{ lazySpies: false }` to opt out. The spies never touch `NgZone`, so they work zoneless and with
|
|
1350
1391
|
zone.js alike.
|
|
1351
1392
|
|
|
1393
|
+
### The same thing as fixtures — `extendWithAutoSpies` (Vitest 4.1+)
|
|
1394
|
+
|
|
1395
|
+
The block above, written once instead of once per dependency, with the types inferred rather than
|
|
1396
|
+
declared:
|
|
1397
|
+
|
|
1398
|
+
```ts
|
|
1399
|
+
import { test as base } from 'vitest';
|
|
1400
|
+
import { extendWithAutoSpies } from 'vitest-auto-spy/angular';
|
|
1401
|
+
|
|
1402
|
+
const test = extendWithAutoSpies(base, {
|
|
1403
|
+
cart: CartService,
|
|
1404
|
+
api: [ApiService, { returns: { get: of([]) } }],
|
|
1405
|
+
passcode: PASSCODE_TOKEN,
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
test('checks out', async ({ cart }) => {
|
|
1409
|
+
cart.checkout.resolveWith(true);
|
|
1410
|
+
|
|
1411
|
+
await expect(cart.checkout(1)).resolves.toBe(true);
|
|
1412
|
+
});
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
No `let cart: Spy<CartService>` that is `undefined` between tests, and a test that never destructures
|
|
1416
|
+
`api` never builds it. Entries are a class, `[Class, config]` with whatever `provideAutoSpy` takes,
|
|
1417
|
+
or an `InjectionToken` (built from the token's own type, like `provideAutoSpyForToken`). Extra
|
|
1418
|
+
providers — the component under test, a real service, `provideHttpClient()` — go in the third
|
|
1419
|
+
argument and are registered in the same call, ahead of the generated ones, so a token named there
|
|
1420
|
+
wins.
|
|
1421
|
+
|
|
1422
|
+
**It takes the whole map at once, and that is a `TestBed` constraint rather than a typing one.** The
|
|
1423
|
+
composing form — `base.extend('cart', …).extend('api', …)` — cannot work: fixtures resolve
|
|
1424
|
+
independently, so `cart` would configure the testing module _and_ inject, which instantiates it, and
|
|
1425
|
+
`api` would then reach `configureTestingModule` after instantiation and fail with Angular's own
|
|
1426
|
+
"Cannot configure the test module when the test module has already been instantiated". A `beforeEach`
|
|
1427
|
+
that configures the module further still composes, because it runs before any fixture resolves and
|
|
1428
|
+
repeated `configureTestingModule` calls merge right up until the first injection. A `beforeEach` that
|
|
1429
|
+
**injects** does not, and nothing can repair that from here.
|
|
1430
|
+
|
|
1352
1431
|
The token may be an **abstract class** — `abstract class LocalStorage extends AbstractStorage {}`,
|
|
1353
1432
|
the shape production provides with `useClass`. Its members are erased before they reach a prototype,
|
|
1354
1433
|
so there is nothing to discover; the factory notices and returns the `createAutoMock` proxy, which
|
|
@@ -1593,7 +1672,7 @@ TypeError: Cannot read properties of undefined (reading 'provide')
|
|
|
1593
1672
|
|
|
1594
1673
|
Both obvious cures fail: `await import('@scope/lib')` at the top of `beforeEach` is already too late,
|
|
1595
1674
|
and a static import at the top of the spec does not fix the order this bundler emits. Worse, the spec
|
|
1596
|
-
that breaks is one nobody touched — chunk boundaries move with file
|
|
1675
|
+
that breaks is one nobody touched — chunk boundaries move with file _contents_, so editing a type
|
|
1597
1676
|
next door is enough.
|
|
1598
1677
|
|
|
1599
1678
|
```ts
|
|
@@ -1941,24 +2020,32 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
|
1941
2020
|
export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
1942
2021
|
```
|
|
1943
2022
|
|
|
1944
|
-
| Rule
|
|
1945
|
-
|
|
|
1946
|
-
| `no-expect-in-subscribe`
|
|
1947
|
-
| `no-object-define-property`
|
|
1948
|
-
| `prefer-provide-auto-spy`
|
|
1949
|
-
| `prefer-create-spy-from-class`
|
|
1950
|
-
| `prefer-inject-spy`
|
|
1951
|
-
| `no-shared-module-level-mock`
|
|
1952
|
-
| `no-mocked-for-spy`
|
|
1953
|
-
| `prefer-as-spy`
|
|
1954
|
-
| `no-done-callback`
|
|
1955
|
-
| `no-floating-assertion`
|
|
1956
|
-
| `no-
|
|
1957
|
-
| `no-
|
|
1958
|
-
| `no-
|
|
1959
|
-
| `no-
|
|
1960
|
-
|
|
1961
|
-
|
|
2023
|
+
| Rule | Level | Fix | Flags |
|
|
2024
|
+
| --------------------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2025
|
+
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
2026
|
+
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
2027
|
+
| `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
|
|
2028
|
+
| `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
|
|
2029
|
+
| `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
|
|
2030
|
+
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
2031
|
+
| `no-mocked-for-spy` | `warn` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
|
|
2032
|
+
| `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
|
|
2033
|
+
| `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
|
|
2034
|
+
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
2035
|
+
| `no-bare-called-with` | `error` | — | `spy.m.calledWith(1);` as a statement — a stub nobody continued, asserting nothing; chai's `expect(fn).to.have.been.calledWith()` exempt |
|
|
2036
|
+
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
|
|
2037
|
+
| `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
2038
|
+
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
2039
|
+
| `no-unregistered-inject-spy` | `warn` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
|
|
2040
|
+
| `jasmine-namespace-without-entry` | `warn` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
|
|
2041
|
+
| `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
|
|
2042
|
+
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
2043
|
+
| `prefer-native-spy-api` | `off` | `--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 |
|
|
2044
|
+
|
|
2045
|
+
Nineteen rules; three fix on their own, seven offer suggestions. The last four are for a suite
|
|
2046
|
+
mid-migration off `jasmine-auto-spies` (§20); `prefer-native-spy-api` is **off** in the recommended
|
|
2047
|
+
config because it reports working code — the compatibility layer is what a suite runs on while it is
|
|
2048
|
+
being migrated. `no-mocked-for-spy` only ever touches a
|
|
1962
2049
|
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
1963
2050
|
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
1964
2051
|
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
|
@@ -2073,75 +2160,83 @@ packages, which a subpath export can never be.
|
|
|
2073
2160
|
|
|
2074
2161
|
## 17. Error → fix
|
|
2075
2162
|
|
|
2076
|
-
| Message contains
|
|
2077
|
-
|
|
|
2078
|
-
| `No mock adapter registered`
|
|
2079
|
-
| `Observable spies require rxjs`
|
|
2080
|
-
| `
|
|
2081
|
-
| `
|
|
2082
|
-
| `
|
|
2083
|
-
| `
|
|
2084
|
-
| `
|
|
2085
|
-
| `
|
|
2086
|
-
| `
|
|
2087
|
-
| `
|
|
2088
|
-
| `
|
|
2089
|
-
| `
|
|
2090
|
-
| `
|
|
2091
|
-
| `
|
|
2092
|
-
| `
|
|
2093
|
-
| `
|
|
2094
|
-
| `
|
|
2095
|
-
| `
|
|
2096
|
-
|
|
|
2097
|
-
| `
|
|
2098
|
-
| `
|
|
2099
|
-
|
|
|
2100
|
-
| `
|
|
2101
|
-
|
|
|
2102
|
-
|
|
|
2103
|
-
| `
|
|
2104
|
-
| `
|
|
2105
|
-
|
|
|
2106
|
-
|
|
|
2107
|
-
|
|
|
2108
|
-
| `
|
|
2109
|
-
|
|
|
2110
|
-
| `
|
|
2111
|
-
| `
|
|
2112
|
-
|
|
|
2113
|
-
| `
|
|
2114
|
-
| `
|
|
2115
|
-
|
|
|
2116
|
-
| `
|
|
2117
|
-
| `
|
|
2118
|
-
| `
|
|
2119
|
-
| `
|
|
2120
|
-
|
|
|
2121
|
-
|
|
|
2122
|
-
| `
|
|
2123
|
-
| `
|
|
2124
|
-
| `
|
|
2125
|
-
|
|
|
2126
|
-
| a
|
|
2127
|
-
| `
|
|
2128
|
-
|
|
|
2129
|
-
|
|
|
2130
|
-
|
|
|
2131
|
-
|
|
|
2132
|
-
|
|
|
2133
|
-
| `
|
|
2134
|
-
|
|
|
2135
|
-
| `
|
|
2136
|
-
|
|
|
2137
|
-
| `
|
|
2138
|
-
| `
|
|
2139
|
-
| `
|
|
2140
|
-
|
|
|
2141
|
-
| `
|
|
2142
|
-
|
|
|
2143
|
-
|
|
|
2144
|
-
| a
|
|
2163
|
+
| Message contains | Cause | Fix |
|
|
2164
|
+
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
2165
|
+
| `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
|
|
2166
|
+
| `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
|
|
2167
|
+
| `Cannot read properties of undefined (reading 'returnValue')` on `spy.m.and.…` | the jasmine namespaces are not installed — the spy was built before them, or nothing installed them at all | import from `vitest-auto-spy/jasmine`; on Bun / `node:test`, `enableJasmineCompat()` in the setup file (§20) |
|
|
2168
|
+
| `spy.withArgs is not a function` | the same, seen from the argument-matching side | the same — or write `spy.m.calledWith(a).mockReturnValue(v)`, which needs no layer (§20) |
|
|
2169
|
+
| `jasmine is not defined` | jasmine's global is the runner's, and Vitest declares none | `import { jasmine } from 'vitest-auto-spy/jasmine'`, then `codemod --from jasmine` to rewrite the members (§20) |
|
|
2170
|
+
| `done.fail is not a function` | Vitest passes a `TestContext`, not jasmine's `done`; the line usually sits in an `error` callback nobody awaits, so the run stays **green** | assert on the failure — `await expect(firstValueFrom(src$)).rejects.toMatchObject({ … })`, or `expect.fail(message)` |
|
|
2171
|
+
| a `.withContext('…')` message that never appears in the failure output | Vitest's chai layer has an `@internal` `withContext(flags)`; handed a string it walks the character indices, sets nonsense flags and returns `this` | `expect(actual, 'message').toBe(expected)` — the second argument of `expect` is the label. Nothing throws, so nothing warns (§20) |
|
|
2172
|
+
| `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
|
|
2173
|
+
| `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
|
|
2174
|
+
| `extendWithAutoSpies needs Vitest 4.1 or newer` | the `test` handed in has only the object-form `extend` (Vitest ≤ 4.0); the builder form the helper is written against arrived in 4.1 | upgrade Vitest, or keep `provideAutoSpy` + `injectSpy` in a `beforeEach` until then |
|
|
2175
|
+
| `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
|
|
2176
|
+
| `Nothing configured X.method, and strict mode is on` | a strict double was asked for a method no line configured | configure it (`mockReturnValue` / `resolveWith` / `nextWith` / `calledWith`), or `{ strict: false }` on that double (§5) |
|
|
2177
|
+
| `Cannot redefine property: …` from a library accessor spy | the target is an ES module namespace a bundler inlined | no spy library can win this — give the code a real seam (inject it, pass it in) and spy on that |
|
|
2178
|
+
| `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
|
|
2179
|
+
| `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
|
|
2180
|
+
| `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
|
|
2181
|
+
| `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
|
|
2182
|
+
| `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
|
|
2183
|
+
| `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
|
|
2184
|
+
| `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
|
|
2185
|
+
| `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
|
|
2186
|
+
| `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
|
|
2187
|
+
| `Spread syntax requires ...iterable[Symbol.iterator] to be a function`, at load time | a module-scope spread of an imported binding, inside a bundle | build the array in a function or a getter — `no-import-time-spread` (§16) |
|
|
2188
|
+
| `the timers APIs are not mocked` in a nested `describe`'s `beforeAll` | fakes armed in `beforeEach` only; Jest armed them for the whole file | `setupFakeTimers(cfg, { betweenTests: true })` / `setupAutoSpy({ globalFakeTimers: true })` |
|
|
2189
|
+
| setup-file hooks reaching only the first spec file of a worker | the setup module stayed cached (Angular unit-test builder + coverage) | run coverage with `--isolate`, or call `setupAutoSpy()` from a per-file module |
|
|
2190
|
+
| `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
|
|
2191
|
+
| `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
|
|
2192
|
+
| duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
|
|
2193
|
+
| `Type 'Spy<T>' is not assignable to type 'T'` | `Spy<T>` drops private members — by design | declare as `Spy<T>`, or use `asInstance()` / `asSpy()` (§6) |
|
|
2194
|
+
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
2195
|
+
| `Cannot access '__vi_import_N__' before initialization` | `vi.mock()` on `@angular/core` or a relative path | you cannot mock it — the specs are bundled. Assert the result instead |
|
|
2196
|
+
| `AggregateError at Object.dispatchError`, for a request nothing asserts on | jsdom really served an `XMLHttpRequest` — `blockNetwork` used to cover only `fetch` | `setupAutoSpy({ blockNetwork: true })`, or `{ xhr: 'empty' }` for tracker pings (§10) |
|
|
2197
|
+
| `Schedulers cannot synchronously execute watches while scheduling` | a timer from a **previous** file, under `isolate: false` | track and cancel pending timers/frames in the setup file (§10) |
|
|
2198
|
+
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
2199
|
+
| an assertion error printed to stderr, every test green and the run exiting 0 | zone.js swallowed a rejection nobody handled | `setupAutoSpy({ strayRejections: true })` fails the test it surfaced in (§10) |
|
|
2200
|
+
| an `expect()` inside a `.then()` that never seems to run | nothing awaits the chain, so the test ended first | `await` the promise and assert the settled value — `no-floating-assertion` (§16) |
|
|
2201
|
+
| `trackStrayRejections() found no zone.js on the host` | `strayRejections` turned on where zone.js is not loaded | `import 'zone.js';` in the setup file, or drop the option |
|
|
2202
|
+
| `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
|
|
2203
|
+
| `NullInjectorError` for a service you did provide | it is a component-level provider, not a module one | `asSpy(fixture.debugElement.injector.get(X))`, not `injectSpy(X)` |
|
|
2204
|
+
| `runEffect(): … not an EffectRef returned by effect()` | passed the callback, a signal, or an unassigned field | pass what `effect()` returned; a field may need its lifecycle hook to run first |
|
|
2205
|
+
| `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
|
|
2206
|
+
| `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
|
|
2207
|
+
| `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
|
|
2208
|
+
| `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
|
|
2209
|
+
| `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
|
|
2210
|
+
| `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
|
|
2211
|
+
| `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
|
|
2212
|
+
| `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
|
|
2213
|
+
| a `vi.mock()` factory that never applies, only sometimes | under `isolate: false` the module was already in the worker's graph | do not mock it; inject the dependency, or `vi.hoisted()` + a real seam |
|
|
2214
|
+
| a `vi.mock()` of a workspace alias that never applies at all | a bundler inlined the module before the mock could be installed | `assertMocked(ns, { specifier })` to prove it, then inject instead of mocking |
|
|
2215
|
+
| `No "default" export is defined on the mock`, thrown inside a dependency | a factory returning bare named exports; the dep probes `default` | `vi.mock('x', () => moduleNamespace({ … }))` |
|
|
2216
|
+
| `Not implemented: HTMLMediaElement.play`, or `duration` is `NaN` and cannot be set | jsdom implements the media elements as a shell | `stubMediaElement({ duration })`, then `media.set(el, …)` to fire the events |
|
|
2217
|
+
| `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
|
|
2218
|
+
| 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…())` |
|
|
2219
|
+
| 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` |
|
|
2220
|
+
| `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()` |
|
|
2221
|
+
| 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 })` |
|
|
2222
|
+
| 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 |
|
|
2223
|
+
| 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 |
|
|
2224
|
+
| `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 |
|
|
2225
|
+
| `Expected to be running in 'ProxyZone', but it was not found` | `zone.js/testing` patches jasmine/mocha/jest, not Vitest | `import 'vitest-auto-spy/zone'` after zone.js, with `globals: true` (§14) |
|
|
2226
|
+
| `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
|
|
2227
|
+
| `NG0303` / `NG0304` / nothing at all, from a directive spec | the host is `standalone: false`, or the module is in the TestBed | `createDirectiveHost({ template, scope: [Module] })` (§13) |
|
|
2228
|
+
| `TS2540: Cannot assign to 'X' because it is a read-only property` | a `readonly` field of an object under test | `mockValueProp(obj, 'X', value)` — on a class **getter**, `mockReadonlyProp` |
|
|
2229
|
+
| `TypeError: Cannot read properties of undefined (reading 'mockReturnValue')` | the member is an **instance field**, not on the prototype — `Router.currentNavigation` since Angular 20 | `provideAutoSpy(Router, { instanceMethodsToSpyOn: ['currentNavigation'] })` (§5) |
|
|
2230
|
+
| `throwWith` reaching the success branch first, with the previous test's value | the spy's `ReplaySubject(1)` outlived the test that filled it | upgrade; and `resetAutoSpy(spy)` in `beforeEach` when the TestBed is built in `beforeAll` |
|
|
2231
|
+
| `TS2540` on a `Spy<T>` / `createAutoMock` member, which the runtime writes fine | `Spy<T>` is homomorphic, so it keeps the `readonly` of an abstract getter | `Mutable<Spy<T>>` for direct assignment, or `mockValueProp` for a patch that is undone |
|
|
2232
|
+
| `delete mock.optionalMethod` leaving the member present and truthy | the Proxy had no `deleteProperty` trap before 3.5.0 | upgrade; before that, `mock.optionalMethod = undefined` |
|
|
2233
|
+
| half a double's methods reaching the real implementation after `Object.assign` | `ownKeys` on a type-driven Proxy lists only the keys already read | `createSpyFromClass(X)` — a real object, with enumerable method keys |
|
|
2234
|
+
| `let s: MockInstance<() => unknown>` not matching anything | `MockInstance<F>` is invariant in `F`; Jest's `SpyInstance` was not | `MockInstance<T['method']>`, or better `injectSpy(X).method` |
|
|
2235
|
+
| two runs with the same totals, one of them missing a suite | a lost `describe` and a fixed flake cancel out in the counters | `compareTestRuns(before, after)` — compare the set of names, not the numbers |
|
|
2236
|
+
| a 30 s timeout, in a different file each run | module-level `vi.fn()` in a fixture shared by files | make the fixture a factory (§10) |
|
|
2237
|
+
| a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
|
|
2238
|
+
| a green test whose only line about a call is `spy.m.calledWith(1);` | that is a **stub**, not an assertion — chai's `expect(fn).to.have.been.calledWith(x)` is the one that checks | continue the chain, or `expect(spy.m).toHaveBeenCalledWith(1)` — `no-bare-called-with` (§16) |
|
|
2239
|
+
| `--detect-async-leaks` reporting no leaks in a suite that visibly leaks timers | `setupAutoSpy({ strayTimers: true })` cancelled them before Vitest collected | re-run that file with `strayTimers` off; the warning on stderr says how many were taken (§10) |
|
|
2145
2240
|
|
|
2146
2241
|
**Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
|
|
2147
2242
|
nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
|
|
@@ -2225,6 +2320,14 @@ npx tsc --noEmit # Spy<T> mistakes are compile errors, not
|
|
|
2225
2320
|
npx vitest-auto-spy doctor # suite-level defects that never fail a run
|
|
2226
2321
|
```
|
|
2227
2322
|
|
|
2323
|
+
**If you are an agent, add `--reporter=agent`** (Vitest 4.1). It is Vitest's own reporter, added for
|
|
2324
|
+
exactly this: the same failures, without the passing-test roll call and the repeated banners that
|
|
2325
|
+
make a run's output expensive to read and expensive to carry.
|
|
2326
|
+
|
|
2327
|
+
```bash
|
|
2328
|
+
npx vitest run --reporter=agent path/to/file.spec.ts
|
|
2329
|
+
```
|
|
2330
|
+
|
|
2228
2331
|
Type errors matter here more than usual: most of this library's guarantees are type-level, so a
|
|
2229
2332
|
suite that runs green but does not type-check is not done.
|
|
2230
2333
|
|
|
@@ -2274,3 +2377,132 @@ source.replace(from, () => to); // not source.replace(from, to)
|
|
|
2274
2377
|
**`node.getStart()` excludes leading comments.** A codemod that replaces a range starting there
|
|
2275
2378
|
silently eats the `// eslint-disable-next-line` above the node. Use `node.getFullStart()`, or count
|
|
2276
2379
|
the comments before and after and compare against `HEAD`.
|
|
2380
|
+
|
|
2381
|
+
---
|
|
2382
|
+
|
|
2383
|
+
## 20. Migrating a suite off `jasmine-auto-spies`
|
|
2384
|
+
|
|
2385
|
+
`jasmine-auto-spies` and `jest-auto-spies` are the same library twice — both thin layers over
|
|
2386
|
+
`@hirez_io/auto-spies-core`, with identical configuration keys (`methodsToSpyOn`,
|
|
2387
|
+
`observablePropsToSpyOn`, `gettersToSpyOn`, `settersToSpyOn`) and identical helper names.
|
|
2388
|
+
**Exactly one thing differs**: upstream parks its async helpers behind `.and`, because that is where
|
|
2389
|
+
jasmine keeps its own spy strategies.
|
|
2390
|
+
|
|
2391
|
+
```ts
|
|
2392
|
+
spy.load.and.nextWith(account); // jasmine-auto-spies
|
|
2393
|
+
spy.load.nextWith(account); // jest-auto-spies, and this library
|
|
2394
|
+
```
|
|
2395
|
+
|
|
2396
|
+
**Land it green first, rewrite second.** `vitest-auto-spy/jasmine` puts `.and`, `.calls` and
|
|
2397
|
+
`.withArgs` back on every spy, so the import specifier is the only edit needed to get the suite
|
|
2398
|
+
running — and the first red run then has one candidate cause instead of two:
|
|
2399
|
+
|
|
2400
|
+
```diff
|
|
2401
|
+
- import { createSpyFromClass, provideAutoSpy, type Spy } from 'jasmine-auto-spies';
|
|
2402
|
+
+ import { createSpyFromClass, provideAutoSpy, type Spy } from 'vitest-auto-spy/jasmine';
|
|
2403
|
+
```
|
|
2404
|
+
|
|
2405
|
+
```bash
|
|
2406
|
+
npx vitest-auto-spy codemod --from jasmine # dry run
|
|
2407
|
+
npx vitest-auto-spy codemod --from jasmine --write # apply, then drop the import
|
|
2408
|
+
npx vitest-auto-spy codemod --from jasmine --verify # match the result, not the diff
|
|
2409
|
+
```
|
|
2410
|
+
|
|
2411
|
+
`import { jasmine } from 'vitest-auto-spy/jasmine'` restores the whole `jasmine` namespace
|
|
2412
|
+
(`objectContaining`, `any`, `createSpyObj`, `clock()`, `addMatchers`, and the eight asymmetric
|
|
2413
|
+
matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
|
|
2414
|
+
on `globalThis` — it is one explicit line per file, which the codemod later deletes.
|
|
2415
|
+
|
|
2416
|
+
### The renames, once the suite is green
|
|
2417
|
+
|
|
2418
|
+
| jasmine | here |
|
|
2419
|
+
| ------------------------------------------ | ---------------------------------------------------------- |
|
|
2420
|
+
| `spy.m.and.returnValue(v)` | `spy.m.mockReturnValue(v)` |
|
|
2421
|
+
| `spy.m.and.callFake(fn)` | `spy.m.mockImplementation(fn)` |
|
|
2422
|
+
| `spy.m.and.stub()` | `spy.m.mockImplementation(() => undefined)` |
|
|
2423
|
+
| `spy.m.and.returnValues(a, b)` | `.mockReturnValueOnce(a).mockReturnValueOnce(b)` |
|
|
2424
|
+
| `spy.m.and.throwError('boom')` | `.mockImplementation(() => { throw new Error('boom'); })` |
|
|
2425
|
+
| `spy.m.and.resolveTo(v)` | `spy.m.mockResolvedValue(v)` |
|
|
2426
|
+
| `spy.m.and.nextWith(v)` / `resolveWith(v)` | `spy.m.nextWith(v)` / `spy.m.resolveWith(v)` — drop `.and` |
|
|
2427
|
+
| `spy.m.withArgs(a).and.returnValue(v)` | `spy.m.calledWith(a).mockReturnValue(v)` |
|
|
2428
|
+
| `spy.m.calls.count()` / `argsFor(i)` | `spy.m.mock.calls.length` / `spy.m.mock.calls[i]` |
|
|
2429
|
+
| `spy.m.calls.reset()` | `spy.m.mockClear()` |
|
|
2430
|
+
| `jasmine.createSpy('n')` | `vi.fn()` — or `createFunctionSpy<F>('n')`, which is typed |
|
|
2431
|
+
| `jasmine.any(X)` / `objectContaining({…})` | `expect.any(X)` / `expect.objectContaining({…})` |
|
|
2432
|
+
| `jasmine.clock().tick(n)` | `vi.advanceTimersByTime(n)` — or `await advanceTimers(n)` |
|
|
2433
|
+
| `jasmine.SpyObj<T>` / `jasmine.Spy` | `Spy<T>` from this package / `Mock` from `vitest` |
|
|
2434
|
+
| `expect(x).toBeTrue()` / `toHaveSize(n)` | `.toBe(true)` / `.toHaveLength(n)` |
|
|
2435
|
+
| `expect(spy).toHaveBeenCalledOnceWith(a)` | `.toHaveBeenCalledExactlyOnceWith(a)` |
|
|
2436
|
+
| `fail(msg)` | `expect.fail(msg)` — there is no `vi.fail` |
|
|
2437
|
+
|
|
2438
|
+
### Four traps, all of them silent
|
|
2439
|
+
|
|
2440
|
+
1. **`spyOn(o, 'm')` inverts.** jasmine's `spyOn` installs a **stub**; `vi.spyOn` **calls through**.
|
|
2441
|
+
A bare rename compiles, passes, and starts running the real implementation inside every spec that
|
|
2442
|
+
installed the spy in order to stop it. Write
|
|
2443
|
+
`vi.spyOn(o, 'm').mockImplementation(() => undefined)` — the codemod appends exactly that, and a
|
|
2444
|
+
file whose only jasmine construct is `spyOn(` is deliberately **not** auto-detected: that suite
|
|
2445
|
+
has to say `--from jasmine` out loud.
|
|
2446
|
+
2. **`.withContext('msg')` does not throw — it loses the message.** Vitest's chai layer ships an
|
|
2447
|
+
`@internal` method of that name expecting a _flags object_; handed a string it walks the string's
|
|
2448
|
+
character indices, sets nonsense flags and returns the assertion. The chain runs, the failure
|
|
2449
|
+
reads `AssertionError: expected 2 to be 3`, and the label is gone. Write
|
|
2450
|
+
`expect(actual, 'msg').toBe(expected)`.
|
|
2451
|
+
3. **`.calls.saveArgumentsByValue()` is a no-op here.** No runner in this family copies call
|
|
2452
|
+
arguments. The call still runs, nothing fails, and a spec that relied on it silently starts
|
|
2453
|
+
asserting on post-mutation state. Take the copy at call time, inside a `mockImplementation`.
|
|
2454
|
+
4. **`.and.callThrough()` means something different.** Upstream had no original to call through to
|
|
2455
|
+
and silently answered `undefined`; here it restores **this library's own dispatch**, so a
|
|
2456
|
+
`calledWith` chain decides the value again. The codemod leaves it byte-for-byte and names the
|
|
2457
|
+
line, because there is no expression it could become.
|
|
2458
|
+
|
|
2459
|
+
`jasmine.DEFAULT_TIMEOUT_INTERVAL` is a config setting, not a statement: set **both**
|
|
2460
|
+
`testTimeout` (default 5 000 ms) and `hookTimeout` (default 10 000 ms), or
|
|
2461
|
+
`vi.setConfig({ testTimeout: n, hookTimeout: n })` per file. Assigning to it on the namespace warns
|
|
2462
|
+
once naming both rather than silently swallowing the write.
|
|
2463
|
+
|
|
2464
|
+
`it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
|
|
2465
|
+
shape, not a name. Use `async` + `await`; `no-done-callback` reports both the parameter and any
|
|
2466
|
+
`done.fail(…)`.
|
|
2467
|
+
|
|
2468
|
+
On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
|
|
2469
|
+
importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a
|
|
2470
|
+
setup file — spies built **before** the call do not get the namespaces. Observables still come from
|
|
2471
|
+
`vitest-auto-spy/rxjs`. A project that never imports the entry pays one `undefined` check per spy
|
|
2472
|
+
and ships none of the code.
|
|
2473
|
+
|
|
2474
|
+
### `@hirez_io/observer-spy` comes with it
|
|
2475
|
+
|
|
2476
|
+
That package sits beside `jasmine-auto-spies` in almost every suite that has one, and is the larger
|
|
2477
|
+
of the two by an order of magnitude. `vitest-auto-spy/rxjs` exports the same surface —
|
|
2478
|
+
`subscribeSpyTo(source$, config?)`, `SubscriberSpy<T>`, `ObserverSpy<T>`, `ObserverSpyConfig` — so a
|
|
2479
|
+
migration does not have to rewrite every stream assertion in the same commit:
|
|
2480
|
+
|
|
2481
|
+
```ts
|
|
2482
|
+
import { subscribeSpyTo } from 'vitest-auto-spy/observer-spy';
|
|
2483
|
+
|
|
2484
|
+
const spy = subscribeSpyTo(service.load());
|
|
2485
|
+
|
|
2486
|
+
expect(spy.getValues()).toEqual(['a', 'b']);
|
|
2487
|
+
expect(spy.receivedComplete()).toBe(true);
|
|
2488
|
+
```
|
|
2489
|
+
|
|
2490
|
+
Four departures from upstream, each closing a defect: `getValues()` returns a **copy** (upstream
|
|
2491
|
+
hands back its live array) and is typed `T[]` (upstream: `any[]`); `getFirstValue()` /
|
|
2492
|
+
`getValueAt(i)` **throw** on an empty spy rather than returning `undefined` from a `T` signature;
|
|
2493
|
+
and an **unexpected** error is thrown by the value readers, carrying the original as `cause`, rather
|
|
2494
|
+
than rethrown out of the observer — under rxjs 7 that rethrow goes through `reportUnhandledError`
|
|
2495
|
+
and never reaches the subscribing line. `{ expectErrors: true }` keeps the readers open, as upstream.
|
|
2496
|
+
|
|
2497
|
+
`autoUnsubscribe()`, `queueForAutoUnsubscribe()` and `fakeTime()` are **not implemented**. Use
|
|
2498
|
+
`using spy = subscribeSpyTo(source$)` — `SubscriberSpy` is disposable — and `setupFakeTimers()` with
|
|
2499
|
+
`await advanceTimers(ms)`, or rxjs's `TestScheduler` directly.
|
|
2500
|
+
|
|
2501
|
+
**It is a bridge, and the destination differs in kind.** observer-spy is synchronous inspection:
|
|
2502
|
+
subscribe, let things happen, then read. A stream that never emits leaves a spy with no values, so a
|
|
2503
|
+
spec reading `getValues()` gets `[]`, asserts about it, and passes having observed nothing. §8's
|
|
2504
|
+
`expectEmission` / `expectEmissions` invert that — the assertion _is_ the await, and silence is a
|
|
2505
|
+
failure with a watchdog. Land the suite green on `subscribeSpyTo`, then move the assertions over.
|
|
2506
|
+
|
|
2507
|
+
Full mapping, including what upstream cannot do at all:
|
|
2508
|
+
<https://asdalexey.github.io/vitest-auto-spy/migrating-jasmine>.
|