vitest-auto-spy 3.13.0 → 3.14.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 +250 -95
- package/README.md +353 -183
- package/dist/angular.js +72 -15
- package/dist/bun-angular.js +8 -7
- package/dist/bun.js +5 -4
- package/dist/{chunk-JV5XRAS4.js → chunk-3JTZD6ME.js} +2 -2
- package/dist/{chunk-R4EWAG7Y.js → chunk-GVTSBOZ3.js} +1 -1
- package/dist/chunk-MAKQSO2R.js +162 -0
- package/dist/{chunk-IKH4OTMV.js → chunk-OF4YYOUI.js} +4 -3
- package/dist/chunk-PC3FAZT3.js +14 -0
- package/dist/{chunk-7YTPHYP7.js → chunk-USCBO2MG.js} +6 -16
- package/dist/chunk-XJ3UMJN3.js +8 -0
- package/dist/{chunk-3AWKPZCE.js → chunk-Y5KHK2GG.js} +70 -14
- package/dist/cli.js +689 -182
- package/dist/console.js +1 -1
- package/dist/eslint-plugin.cjs +393 -29
- package/dist/eslint-plugin.js +393 -29
- package/dist/index.js +72 -15
- 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.js +4 -3
- package/dist/node.cjs +64 -14
- package/dist/node.js +5 -4
- package/dist/observer-spy.d.ts +133 -0
- package/dist/observer-spy.js +158 -0
- package/dist/react.js +5 -4
- package/dist/setup.js +6 -9
- package/dist/shared-state.js +22 -7
- package/dist/svelte.js +5 -4
- package/dist/vue.js +6 -5
- package/package.json +32 -3
- package/skills/vitest-auto-spy/SKILL.md +58 -2
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 | Import | Needed for
|
|
55
|
-
| ---------------- | ------------------------------- |
|
|
54
|
+
| Add-on | Import | Needed for |
|
|
55
|
+
| ---------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
56
56
|
| Observable spies | `import 'vitest-auto-spy/rxjs'` | `nextWith` & friends. **Side-effect import, once.** |
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
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 —
|
|
@@ -358,6 +363,8 @@ users.getName.calledWith(1).mockReturnValue('Ada');
|
|
|
358
363
|
users.getName.mustBeCalledWith(1).mockReturnValue('Ada');
|
|
359
364
|
// asymmetric matchers work in both
|
|
360
365
|
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(true);
|
|
366
|
+
// re-registering the same arguments replaces the answer — matcher arguments included
|
|
367
|
+
users.save.calledWith(expect.objectContaining({ id: 1 })).mockReturnValue(false);
|
|
361
368
|
|
|
362
369
|
// promises
|
|
363
370
|
users.load.resolveWith({ id: 1 });
|
|
@@ -374,6 +381,13 @@ feed.items$.throwWith('FAKE ERROR');
|
|
|
374
381
|
const subject = feed.items$.returnSubject(); // ReplaySubject, for anything the helpers miss
|
|
375
382
|
```
|
|
376
383
|
|
|
384
|
+
An exact argument list is matched before the asymmetric configs, and those are tried in
|
|
385
|
+
registration order — a narrow config written first keeps its calls. Two matchers count as the same
|
|
386
|
+
argument when they accept the same values (same matcher class, sample and inversion), which is what
|
|
387
|
+
makes a second `calledWith(1, expect.anything())` an override rather than a second config sitting
|
|
388
|
+
behind the first. A hand-rolled `{ asymmetricMatch }` object is compared by identity instead: its
|
|
389
|
+
verdict is a closure, so only re-registering that same instance overrides.
|
|
390
|
+
|
|
377
391
|
`mock.settledResults` is native on Vitest and polyfilled on Bun / `node:test`, so it is identical on
|
|
378
392
|
all three. Entries are `{ type: 'fulfilled' | 'incomplete' | 'rejected', value }`.
|
|
379
393
|
|
|
@@ -961,7 +975,7 @@ export const navigation = { setFocus: keepMockRegistered(vi.fn()) };
|
|
|
961
975
|
The mocks it keeps, it also guards. Staying registered means `vi.resetAllMocks()` reaches them too —
|
|
962
976
|
it walks the same set and calls `mockReset()`, which puts an implementation back only when it was
|
|
963
977
|
passed to `vi.fn(implementation)`; behaviour chained on with `.mockReturnValue(…)` or
|
|
964
|
-
`.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a
|
|
978
|
+
`.mockReturnThis()` is simply lost. Under `isolate: false` that surfaces in a _different_ file later
|
|
965
979
|
in the same worker, inside application code, as `Cannot read properties of undefined` against a
|
|
966
980
|
shared double that spec never touched — and `vi.restoreAllMocks()` does not cause it (Vitest 4 walks
|
|
967
981
|
`MOCK_RESTORE` there, which only `vi.spyOn` writes to), so probing with that one comes back green and
|
|
@@ -1584,7 +1598,7 @@ TypeError: Cannot read properties of undefined (reading 'provide')
|
|
|
1584
1598
|
|
|
1585
1599
|
Both obvious cures fail: `await import('@scope/lib')` at the top of `beforeEach` is already too late,
|
|
1586
1600
|
and a static import at the top of the spec does not fix the order this bundler emits. Worse, the spec
|
|
1587
|
-
that breaks is one nobody touched — chunk boundaries move with file
|
|
1601
|
+
that breaks is one nobody touched — chunk boundaries move with file _contents_, so editing a type
|
|
1588
1602
|
next door is enough.
|
|
1589
1603
|
|
|
1590
1604
|
```ts
|
|
@@ -1932,24 +1946,31 @@ import autoSpy from 'vitest-auto-spy/eslint-plugin';
|
|
|
1932
1946
|
export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
1933
1947
|
```
|
|
1934
1948
|
|
|
1935
|
-
| Rule
|
|
1936
|
-
|
|
|
1937
|
-
| `no-expect-in-subscribe`
|
|
1938
|
-
| `no-object-define-property`
|
|
1939
|
-
| `prefer-provide-auto-spy`
|
|
1940
|
-
| `prefer-create-spy-from-class`
|
|
1941
|
-
| `prefer-inject-spy`
|
|
1942
|
-
| `no-shared-module-level-mock`
|
|
1943
|
-
| `no-mocked-for-spy`
|
|
1944
|
-
| `prefer-as-spy`
|
|
1945
|
-
| `no-done-callback`
|
|
1946
|
-
| `no-floating-assertion`
|
|
1947
|
-
| `no-overridden-provider`
|
|
1948
|
-
| `no-inject-before-override`
|
|
1949
|
-
| `no-import-time-spread`
|
|
1950
|
-
| `no-unregistered-inject-spy`
|
|
1951
|
-
|
|
1952
|
-
|
|
1949
|
+
| Rule | Level | Fix | Flags |
|
|
1950
|
+
| --------------------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1951
|
+
| `no-expect-in-subscribe` | `error` | suggest | `expect()` inside `subscribe()` → `expectEmission` / `firstValueFrom` |
|
|
1952
|
+
| `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
|
|
1953
|
+
| `prefer-provide-auto-spy` | `warn` | — | a hand-rolled `useValue` **or** `useFactory` → `provideAutoSpy(Class)` / `provideAutoSpyForToken(TOKEN)` |
|
|
1954
|
+
| `prefer-create-spy-from-class` | `warn` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
|
|
1955
|
+
| `prefer-inject-spy` | `warn` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
|
|
1956
|
+
| `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
|
|
1957
|
+
| `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 |
|
|
1958
|
+
| `prefer-as-spy` | `warn` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
|
|
1959
|
+
| `no-done-callback` | `error` | — | `it('x', (done) => …)` → `async` + an awaited assertion, and `done.fail(…)` at the call site |
|
|
1960
|
+
| `no-floating-assertion` | `error` | — | `expect()` in a `.then()` nobody awaits → `expect(await promise)` |
|
|
1961
|
+
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
|
|
1962
|
+
| `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
1963
|
+
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
1964
|
+
| `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 |
|
|
1965
|
+
| `jasmine-namespace-without-entry` | `warn` | — | `.and` / `.calls` / `.withArgs` on a library spy in a file that installs the compat layer nowhere — option: `{ setupModules: […] }` |
|
|
1966
|
+
| `no-jasmine-globals` | `error` | — | `jasmine.*`, bare `spyOn(` / `spyOnProperty(` / `spyOnAllFunctions(` / `fail(` / `pending(`, `.withContext(` |
|
|
1967
|
+
| `no-save-arguments-by-value` | `error` | — | `spy.calls.saveArgumentsByValue()` — a no-op here, so the spec silently asserts on post-mutation state |
|
|
1968
|
+
| `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 |
|
|
1969
|
+
|
|
1970
|
+
Eighteen rules; three fix on their own, seven offer suggestions. The last four are for a suite
|
|
1971
|
+
mid-migration off `jasmine-auto-spies` (§20); `prefer-native-spy-api` is **off** in the recommended
|
|
1972
|
+
config because it reports working code — the compatibility layer is what a suite runs on while it is
|
|
1973
|
+
being migrated. `no-mocked-for-spy` only ever touches a `no-mocked-for-spy` only ever touches a
|
|
1953
1974
|
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
1954
1975
|
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
1955
1976
|
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
|
@@ -2064,75 +2085,80 @@ packages, which a subpath export can never be.
|
|
|
2064
2085
|
|
|
2065
2086
|
## 17. Error → fix
|
|
2066
2087
|
|
|
2067
|
-
| Message contains
|
|
2068
|
-
|
|
|
2069
|
-
| `No mock adapter registered`
|
|
2070
|
-
| `Observable spies require rxjs`
|
|
2071
|
-
| `
|
|
2072
|
-
| `
|
|
2073
|
-
| `
|
|
2074
|
-
| `
|
|
2075
|
-
| `
|
|
2076
|
-
| `
|
|
2077
|
-
| `
|
|
2078
|
-
| `
|
|
2079
|
-
| `
|
|
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
|
-
| a
|
|
2117
|
-
| a
|
|
2118
|
-
| `
|
|
2119
|
-
|
|
|
2120
|
-
|
|
|
2121
|
-
| a
|
|
2122
|
-
|
|
|
2123
|
-
| `
|
|
2124
|
-
| `
|
|
2125
|
-
|
|
|
2126
|
-
|
|
|
2127
|
-
| `
|
|
2128
|
-
| `
|
|
2129
|
-
| `
|
|
2130
|
-
| `
|
|
2131
|
-
|
|
|
2132
|
-
| `
|
|
2133
|
-
|
|
|
2134
|
-
| a
|
|
2135
|
-
|
|
|
2088
|
+
| Message contains | Cause | Fix |
|
|
2089
|
+
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
2090
|
+
| `No mock adapter registered` | no runtime entry was imported, or the wrong one | import `vitest-auto-spy` (Vitest) / `…/bun` / `…/node` once before creating spies |
|
|
2091
|
+
| `Observable spies require rxjs` | the rxjs layer was never loaded | `import 'vitest-auto-spy/rxjs';` once, in the setup file |
|
|
2092
|
+
| `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) |
|
|
2093
|
+
| `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) |
|
|
2094
|
+
| `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) |
|
|
2095
|
+
| `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)` |
|
|
2096
|
+
| 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) |
|
|
2097
|
+
| `requested method(s) not found on the class prototype` | typo, or an instance-field callable | fix the name, or move it to `instanceMethodsToSpyOn` |
|
|
2098
|
+
| `was configured with 'mustBeCalledWith'` | the code called the spy with other arguments | that is the assertion firing — fix the code, or relax to `calledWith` |
|
|
2099
|
+
| `advanceTimers() requires fake timers` | no fake timers installed | `setupFakeTimers()` or `vi.useFakeTimers()` first |
|
|
2100
|
+
| `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) |
|
|
2101
|
+
| `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 |
|
|
2102
|
+
| `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 |
|
|
2103
|
+
| `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) |
|
|
2104
|
+
| `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) |
|
|
2105
|
+
| `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) |
|
|
2106
|
+
| `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) |
|
|
2107
|
+
| `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) |
|
|
2108
|
+
| `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) |
|
|
2109
|
+
| `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 |
|
|
2110
|
+
| `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) |
|
|
2111
|
+
| `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) |
|
|
2112
|
+
| `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 })` |
|
|
2113
|
+
| 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 |
|
|
2114
|
+
| `no DOM could be installed` | `bun-angular` preload with no DOM package | `bun add -d @happy-dom/global-registrator` (or `jsdom`) |
|
|
2115
|
+
| `cannot read "…" referenced by …` | a `templateUrl` / `styleUrls` path does not resolve | fix the path, relative to the component file |
|
|
2116
|
+
| duplicate-copy report from `setupAutoSpy()` | two installs, or one loaded as both ESM and CJS | dedupe the dependency; `setupAutoSpy({ duplicateCopies: 'warn' })` to downgrade |
|
|
2117
|
+
| `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) |
|
|
2118
|
+
| a spy is never called, no warning | the method is an instance field, not on the prototype | `instanceMethodsToSpyOn`, or `createAutoMock<T>()` |
|
|
2119
|
+
| `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 |
|
|
2120
|
+
| `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) |
|
|
2121
|
+
| `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) |
|
|
2122
|
+
| `signal read during notification phase` | same — a stray `requestAnimationFrame` callback | same |
|
|
2123
|
+
| 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) |
|
|
2124
|
+
| 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) |
|
|
2125
|
+
| `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 |
|
|
2126
|
+
| `… .destroy is not a function` | an ngrx `rxMethod` replaced with a bare mock | `Object.assign(vi.fn(), { destroy: vi.fn() })` |
|
|
2127
|
+
| `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)` |
|
|
2128
|
+
| `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 |
|
|
2129
|
+
| `X is not a constructor`, stack in production code | a `vi.fn(() => …)` where the code does `new X()` | `mockConstructor` / `stubConstructor` / `createSpyClass` (§12) |
|
|
2130
|
+
| `Date is not a constructor` | `vi.spyOn(globalThis, 'Date')` — the fakes own it | `mockSystemTime(date)` / `vi.setSystemTime` |
|
|
2131
|
+
| `TS2352: … 'accessorSpies' is missing in type 'X'` | `TestBed.inject(X) as Spy<X>` | `asSpy(TestBed.inject(X))` — never a double assertion (§6) |
|
|
2132
|
+
| `TS2739` / `TS2740` / `TS2345` with `Spy<X>` on the left | a spy handed to an API typed against the class | `asInstance(spy)` (§6) |
|
|
2133
|
+
| `is missing the following properties: _private, …` | declared as Vitest's `Mocked<T>` | declare `Spy<T>` (§6) |
|
|
2134
|
+
| `AddPromiseSpyMethods<unknown>` vs `WithMockReturnValue<…>` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` (§6) |
|
|
2135
|
+
| `'addEventListener' called on an object that is not a valid instance of EventTarget` | Node's `AbortSignal` under jsdom + zone.js | `stubAbortController()` (§12) |
|
|
2136
|
+
| `vi.requireMock is not a function` | a mechanical `jest.` → `vi.` rename | there is no equivalent — provide the double through the TestBed instead |
|
|
2137
|
+
| 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 |
|
|
2138
|
+
| 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 |
|
|
2139
|
+
| `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({ … }))` |
|
|
2140
|
+
| `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 |
|
|
2141
|
+
| `Cannot set base providers because it has already been called` | zone and zoneless spec files sharing one worker | `setupAngularTestEnv({ zoneless, initZone, initZoneless })` (§13) |
|
|
2142
|
+
| 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…())` |
|
|
2143
|
+
| 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` |
|
|
2144
|
+
| `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()` |
|
|
2145
|
+
| 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 })` |
|
|
2146
|
+
| 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 |
|
|
2147
|
+
| 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 |
|
|
2148
|
+
| `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 |
|
|
2149
|
+
| `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) |
|
|
2150
|
+
| `nextWith` demanding `HttpEvent<T>` on a generated API client | `Parameters`/`ReturnType` read the **last** overload | `asSpy<Client, { overload: 'first' }>(…)`, or `Overload<M, 0>` |
|
|
2151
|
+
| `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) |
|
|
2152
|
+
| `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` |
|
|
2153
|
+
| `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) |
|
|
2154
|
+
| `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` |
|
|
2155
|
+
| `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 |
|
|
2156
|
+
| `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` |
|
|
2157
|
+
| 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 |
|
|
2158
|
+
| `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` |
|
|
2159
|
+
| 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 |
|
|
2160
|
+
| 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) |
|
|
2161
|
+
| a component's `afterNextRender` state is empty | `detectChanges()` does not run the after-render phase | `await stable(fixture)` (§11) |
|
|
2136
2162
|
|
|
2137
2163
|
**Seven of these are library defects, not spec ones.** On anything below 3.8.0, upgrade and change
|
|
2138
2164
|
nothing in the spec: `of(double)` never emitting; `accessorSpies.setters.X` still `undefined` after
|
|
@@ -2265,3 +2291,132 @@ source.replace(from, () => to); // not source.replace(from, to)
|
|
|
2265
2291
|
**`node.getStart()` excludes leading comments.** A codemod that replaces a range starting there
|
|
2266
2292
|
silently eats the `// eslint-disable-next-line` above the node. Use `node.getFullStart()`, or count
|
|
2267
2293
|
the comments before and after and compare against `HEAD`.
|
|
2294
|
+
|
|
2295
|
+
---
|
|
2296
|
+
|
|
2297
|
+
## 20. Migrating a suite off `jasmine-auto-spies`
|
|
2298
|
+
|
|
2299
|
+
`jasmine-auto-spies` and `jest-auto-spies` are the same library twice — both thin layers over
|
|
2300
|
+
`@hirez_io/auto-spies-core`, with identical configuration keys (`methodsToSpyOn`,
|
|
2301
|
+
`observablePropsToSpyOn`, `gettersToSpyOn`, `settersToSpyOn`) and identical helper names.
|
|
2302
|
+
**Exactly one thing differs**: upstream parks its async helpers behind `.and`, because that is where
|
|
2303
|
+
jasmine keeps its own spy strategies.
|
|
2304
|
+
|
|
2305
|
+
```ts
|
|
2306
|
+
spy.load.and.nextWith(account); // jasmine-auto-spies
|
|
2307
|
+
spy.load.nextWith(account); // jest-auto-spies, and this library
|
|
2308
|
+
```
|
|
2309
|
+
|
|
2310
|
+
**Land it green first, rewrite second.** `vitest-auto-spy/jasmine` puts `.and`, `.calls` and
|
|
2311
|
+
`.withArgs` back on every spy, so the import specifier is the only edit needed to get the suite
|
|
2312
|
+
running — and the first red run then has one candidate cause instead of two:
|
|
2313
|
+
|
|
2314
|
+
```diff
|
|
2315
|
+
- import { createSpyFromClass, provideAutoSpy, type Spy } from 'jasmine-auto-spies';
|
|
2316
|
+
+ import { createSpyFromClass, provideAutoSpy, type Spy } from 'vitest-auto-spy/jasmine';
|
|
2317
|
+
```
|
|
2318
|
+
|
|
2319
|
+
```bash
|
|
2320
|
+
npx vitest-auto-spy codemod --from jasmine # dry run
|
|
2321
|
+
npx vitest-auto-spy codemod --from jasmine --write # apply, then drop the import
|
|
2322
|
+
npx vitest-auto-spy codemod --from jasmine --verify # match the result, not the diff
|
|
2323
|
+
```
|
|
2324
|
+
|
|
2325
|
+
`import { jasmine } from 'vitest-auto-spy/jasmine'` restores the whole `jasmine` namespace
|
|
2326
|
+
(`objectContaining`, `any`, `createSpyObj`, `clock()`, `addMatchers`, and the eight asymmetric
|
|
2327
|
+
matchers Vitest has no twin for) for the specs that never touched auto-spies. Nothing is installed
|
|
2328
|
+
on `globalThis` — it is one explicit line per file, which the codemod later deletes.
|
|
2329
|
+
|
|
2330
|
+
### The renames, once the suite is green
|
|
2331
|
+
|
|
2332
|
+
| jasmine | here |
|
|
2333
|
+
| ------------------------------------------ | ---------------------------------------------------------- |
|
|
2334
|
+
| `spy.m.and.returnValue(v)` | `spy.m.mockReturnValue(v)` |
|
|
2335
|
+
| `spy.m.and.callFake(fn)` | `spy.m.mockImplementation(fn)` |
|
|
2336
|
+
| `spy.m.and.stub()` | `spy.m.mockImplementation(() => undefined)` |
|
|
2337
|
+
| `spy.m.and.returnValues(a, b)` | `.mockReturnValueOnce(a).mockReturnValueOnce(b)` |
|
|
2338
|
+
| `spy.m.and.throwError('boom')` | `.mockImplementation(() => { throw new Error('boom'); })` |
|
|
2339
|
+
| `spy.m.and.resolveTo(v)` | `spy.m.mockResolvedValue(v)` |
|
|
2340
|
+
| `spy.m.and.nextWith(v)` / `resolveWith(v)` | `spy.m.nextWith(v)` / `spy.m.resolveWith(v)` — drop `.and` |
|
|
2341
|
+
| `spy.m.withArgs(a).and.returnValue(v)` | `spy.m.calledWith(a).mockReturnValue(v)` |
|
|
2342
|
+
| `spy.m.calls.count()` / `argsFor(i)` | `spy.m.mock.calls.length` / `spy.m.mock.calls[i]` |
|
|
2343
|
+
| `spy.m.calls.reset()` | `spy.m.mockClear()` |
|
|
2344
|
+
| `jasmine.createSpy('n')` | `vi.fn()` — or `createFunctionSpy<F>('n')`, which is typed |
|
|
2345
|
+
| `jasmine.any(X)` / `objectContaining({…})` | `expect.any(X)` / `expect.objectContaining({…})` |
|
|
2346
|
+
| `jasmine.clock().tick(n)` | `vi.advanceTimersByTime(n)` — or `await advanceTimers(n)` |
|
|
2347
|
+
| `jasmine.SpyObj<T>` / `jasmine.Spy` | `Spy<T>` from this package / `Mock` from `vitest` |
|
|
2348
|
+
| `expect(x).toBeTrue()` / `toHaveSize(n)` | `.toBe(true)` / `.toHaveLength(n)` |
|
|
2349
|
+
| `expect(spy).toHaveBeenCalledOnceWith(a)` | `.toHaveBeenCalledExactlyOnceWith(a)` |
|
|
2350
|
+
| `fail(msg)` | `expect.fail(msg)` — there is no `vi.fail` |
|
|
2351
|
+
|
|
2352
|
+
### Four traps, all of them silent
|
|
2353
|
+
|
|
2354
|
+
1. **`spyOn(o, 'm')` inverts.** jasmine's `spyOn` installs a **stub**; `vi.spyOn` **calls through**.
|
|
2355
|
+
A bare rename compiles, passes, and starts running the real implementation inside every spec that
|
|
2356
|
+
installed the spy in order to stop it. Write
|
|
2357
|
+
`vi.spyOn(o, 'm').mockImplementation(() => undefined)` — the codemod appends exactly that, and a
|
|
2358
|
+
file whose only jasmine construct is `spyOn(` is deliberately **not** auto-detected: that suite
|
|
2359
|
+
has to say `--from jasmine` out loud.
|
|
2360
|
+
2. **`.withContext('msg')` does not throw — it loses the message.** Vitest's chai layer ships an
|
|
2361
|
+
`@internal` method of that name expecting a _flags object_; handed a string it walks the string's
|
|
2362
|
+
character indices, sets nonsense flags and returns the assertion. The chain runs, the failure
|
|
2363
|
+
reads `AssertionError: expected 2 to be 3`, and the label is gone. Write
|
|
2364
|
+
`expect(actual, 'msg').toBe(expected)`.
|
|
2365
|
+
3. **`.calls.saveArgumentsByValue()` is a no-op here.** No runner in this family copies call
|
|
2366
|
+
arguments. The call still runs, nothing fails, and a spec that relied on it silently starts
|
|
2367
|
+
asserting on post-mutation state. Take the copy at call time, inside a `mockImplementation`.
|
|
2368
|
+
4. **`.and.callThrough()` means something different.** Upstream had no original to call through to
|
|
2369
|
+
and silently answered `undefined`; here it restores **this library's own dispatch**, so a
|
|
2370
|
+
`calledWith` chain decides the value again. The codemod leaves it byte-for-byte and names the
|
|
2371
|
+
line, because there is no expression it could become.
|
|
2372
|
+
|
|
2373
|
+
`jasmine.DEFAULT_TIMEOUT_INTERVAL` is a config setting, not a statement: set **both**
|
|
2374
|
+
`testTimeout` (default 5 000 ms) and `hookTimeout` (default 10 000 ms), or
|
|
2375
|
+
`vi.setConfig({ testTimeout: n, hookTimeout: n })` per file. Assigning to it on the namespace warns
|
|
2376
|
+
once naming both rather than silently swallowing the write.
|
|
2377
|
+
|
|
2378
|
+
`it('x', (done) => …)` is **not** rewritten by anything — a callback signature is a control-flow
|
|
2379
|
+
shape, not a name. Use `async` + `await`; `no-done-callback` reports both the parameter and any
|
|
2380
|
+
`done.fail(…)`.
|
|
2381
|
+
|
|
2382
|
+
On Bun and `node:test` the entry cannot be imported (it registers the Vitest adapter, which means
|
|
2383
|
+
importing `vitest`). Call `enableJasmineCompat()` from `vitest-auto-spy/jasmine-compat` once, in a
|
|
2384
|
+
setup file — spies built **before** the call do not get the namespaces. Observables still come from
|
|
2385
|
+
`vitest-auto-spy/rxjs`. A project that never imports the entry pays one `undefined` check per spy
|
|
2386
|
+
and ships none of the code.
|
|
2387
|
+
|
|
2388
|
+
### `@hirez_io/observer-spy` comes with it
|
|
2389
|
+
|
|
2390
|
+
That package sits beside `jasmine-auto-spies` in almost every suite that has one, and is the larger
|
|
2391
|
+
of the two by an order of magnitude. `vitest-auto-spy/rxjs` exports the same surface —
|
|
2392
|
+
`subscribeSpyTo(source$, config?)`, `SubscriberSpy<T>`, `ObserverSpy<T>`, `ObserverSpyConfig` — so a
|
|
2393
|
+
migration does not have to rewrite every stream assertion in the same commit:
|
|
2394
|
+
|
|
2395
|
+
```ts
|
|
2396
|
+
import { subscribeSpyTo } from 'vitest-auto-spy/observer-spy';
|
|
2397
|
+
|
|
2398
|
+
const spy = subscribeSpyTo(service.load());
|
|
2399
|
+
|
|
2400
|
+
expect(spy.getValues()).toEqual(['a', 'b']);
|
|
2401
|
+
expect(spy.receivedComplete()).toBe(true);
|
|
2402
|
+
```
|
|
2403
|
+
|
|
2404
|
+
Four departures from upstream, each closing a defect: `getValues()` returns a **copy** (upstream
|
|
2405
|
+
hands back its live array) and is typed `T[]` (upstream: `any[]`); `getFirstValue()` /
|
|
2406
|
+
`getValueAt(i)` **throw** on an empty spy rather than returning `undefined` from a `T` signature;
|
|
2407
|
+
and an **unexpected** error is thrown by the value readers, carrying the original as `cause`, rather
|
|
2408
|
+
than rethrown out of the observer — under rxjs 7 that rethrow goes through `reportUnhandledError`
|
|
2409
|
+
and never reaches the subscribing line. `{ expectErrors: true }` keeps the readers open, as upstream.
|
|
2410
|
+
|
|
2411
|
+
`autoUnsubscribe()`, `queueForAutoUnsubscribe()` and `fakeTime()` are **not implemented**. Use
|
|
2412
|
+
`using spy = subscribeSpyTo(source$)` — `SubscriberSpy` is disposable — and `setupFakeTimers()` with
|
|
2413
|
+
`await advanceTimers(ms)`, or rxjs's `TestScheduler` directly.
|
|
2414
|
+
|
|
2415
|
+
**It is a bridge, and the destination differs in kind.** observer-spy is synchronous inspection:
|
|
2416
|
+
subscribe, let things happen, then read. A stream that never emits leaves a spy with no values, so a
|
|
2417
|
+
spec reading `getValues()` gets `[]`, asserts about it, and passes having observed nothing. §8's
|
|
2418
|
+
`expectEmission` / `expectEmissions` invert that — the assertion _is_ the await, and silence is a
|
|
2419
|
+
failure with a watchdog. Land the suite green on `subscribeSpyTo`, then move the assertions over.
|
|
2420
|
+
|
|
2421
|
+
Full mapping, including what upstream cannot do at all:
|
|
2422
|
+
<https://asdalexey.github.io/vitest-auto-spy/migrating-jasmine>.
|