import { MockInstance, Mock } from 'vitest'; /** * Public type surface (mirrors `@hirez_io/auto-spies-core`). * * These types describe the *shape* of an auto-spy: which helpers get attached * to a method spy based on its return type, how accessor spies are exposed, and * what the configuration object accepts. */ /** * Structural stand-in for an rxjs `Observable`, and the reason this file names no rxjs type. * * `import type { Observable } from 'rxjs'` in a shipped declaration is neither free nor erasable: * TypeScript resolves the module for the type-only form exactly as it does for the value form, so * the emitted `dist/types-*.d.ts` pulled **189 rxjs `.d.ts` files** into the program of every * React / Vue / Svelte / Node consumer — against this package's own 1 494 lines — and raised * `TS2307` for anyone without the optional peer and `skipLibCheck: false`. Both halves were * measured against `import type` and it changes neither. Only removing the reference does. * * **`forEach` carries the element type, not `subscribe`.** TypeScript infers a type argument from * an overloaded source method by pairing the *trailing* signature, and rxjs 7's last `subscribe` * overload is the deprecated positional `subscribe(next?, error?, complete?)`; inferring `T` * through it yields `unknown` — the same trap {@link https://rxjs.dev/deprecations/subscribe-arguments | rxjs's own deprecation} * set for `expectEmission`, which is why `CallbackSubscribable` exists in `expect-emission.ts`. * `forEach` also keeps the match *tight*, which matters now that the check is structural rather * than nominal: `Array.prototype.forEach` returns `void` rather than `Promise`, and * Angular's `OutputEmitterRef` and `Signal` have no `forEach` at all, so none of them starts * counting as an observable. What now matches that did not before is a second copy of rxjs in the * tree — `Subject` is nominal (`private currentObservers`), so a duplicated `Observable` used * to fall through to the plain-spy branch with no hint of why. */ interface ObservableLike { subscribe(...args: never[]): { unsubscribe(): void; }; forEach(next: (value: T) => void, ...rest: never[]): Promise; } /** * Structural stand-in for an rxjs `Subject` — what {@link SubjectOf} resolves to when the * observable layer's types are not in the program. */ interface SubjectLike extends ObservableLike { next(value: T): void; error(err: unknown): void; complete(): void; unsubscribe(): void; asObservable(): ObservableLike; readonly closed: boolean; } /** * The augmentation seam `vitest-auto-spy/rxjs` fills in, and the whole reason `returnSubject()` * still hands back a real rxjs `Subject` to the suites that have one. * * Empty here on purpose. `import 'vitest-auto-spy/rxjs'` — the same one import that makes the * observable helpers *exist* at runtime — merges `subject: Subject` into this interface, and * every {@link SubjectOf} in the program resolves to rxjs's own type from that point on. A * consumer that never registers the observable layer never names rxjs, so nothing loads it. * * The consequence worth knowing: the type follows the *import*, not the installed package. A suite * whose only `import 'vitest-auto-spy/rxjs'` sits in a setup file outside its `tsconfig` program * gets {@link SubjectLike} back, which is honest — those helpers are not registered there either — * but reads as a puzzle. Put the import where the compiler sees it. */ interface AutoSpyRxjsTypes { /** * Phantom. Never present, never read, and not a key to augment. * * A registry interface has no members of its own — that is what makes it a registry — but one * that never mentions its own type parameter fails `noUnusedParameters`, and the parameter * cannot be renamed to `_T` to silence it: interface merging requires every declaration to * spell the parameter the same way, so an augmentation written `` against a `<_T>` * declaration fails with `TS2428: All declarations of 'AutoSpyRxjsTypes' must have identical * type parameters` — a message that explains none of this. */ readonly '~element'?: T; } /** rxjs's `Subject` where {@link AutoSpyRxjsTypes} is augmented, {@link SubjectLike} otherwise. */ type SubjectOf = AutoSpyRxjsTypes extends { subject: infer S; } ? S : SubjectLike; /** Any callable. */ type Func = (...args: any[]) => any; /** * A class this library can read: its prototype, its name, its statics. * * The construct signature is **abstract**, and that is the point of the type rather than a detail. * Nothing here ever calls `new` on what it is handed — `createSpyFromClass` walks the prototype * chain, `createSpyClass` builds a constructor of its own — so demanding a *concrete* one bought no * safety while rejecting the most common Angular DI-token shape there is: * * ```ts * abstract class LocalStorage extends AbstractStorage {} * // { provide: LocalStorage, useClass: BrowserLocalStorage } in production * provideAutoSpy(LocalStorage) // used to be `Argument of type 'typeof LocalStorage' is not assignable…` * ``` * * A concrete constructor is assignable to an abstract one, so this only widens what is accepted — * no existing call changes meaning. What an abstract class cannot give is *methods*: they are * declared and never emitted, so its prototype is empty. That half is handled at runtime, by * {@link ClassType}'s only consumer that cares — see the empty-prototype fallback in * `createSpyFromClass`. */ type ClassType = abstract new (...args: any[]) => T; type StringKeysForPropertyType = Extract<{ [Key in keyof ObjectType]: [ObjectType[Key]] extends [PropType] ? Key : never; }[keyof ObjectType], string>; /** Keys of `T` that are methods. */ type OnlyMethodKeysOf = StringKeysForPropertyType, Func>; /** Keys of `T` that are `Observable` properties. */ type OnlyObservablePropsOf = StringKeysForPropertyType>; /** Keys of `T` that are *not* methods (plain props, getters, setters). */ type OnlyPropsOf = Extract<{ [Key in keyof ObjectType]: ObjectType[Key] extends Func ? never : Key; }[keyof ObjectType], string>; /** * Keys that may name an accessor — every string key of `T`, whatever its value type. * * Whether a member is a getter is a property of the *descriptor*, not of the value: a getter * returning a function is still a getter. Filtering by "not callable" (as {@link OnlyPropsOf} does) * therefore rejects exactly the case Angular's signal-based services are made of — * `get isCompactMode(): Signal`, where `Signal` is `(() => T) & { … }` and so *is* callable. * For a service whose readonly state is all signals, that leaves no nameable getter at all and the * element type collapses to `never`, reported as `Type 'string' is not assignable to type 'never'` * — a message with nothing in it about signals. * * Naming a member that has no accessor on the prototype is caught at runtime instead, with a * warning that says so, in the same place a mistyped `onlyMethodsToSpyOn` name is. */ type AccessorKeysOf = Extract; /** A single value to emit/resolve on a specific call. */ type ValueConfigPerCall = { value: T; delay?: number; doNotComplete?: boolean; }; /** Emit a value (optionally delayed). */ type NextValueConfig = { value: T; delay?: number; }; /** Error the stream (optionally delayed). */ type ErrorValueConfig = { errorValue: unknown; delay?: number; }; /** Complete the stream (optionally delayed). */ type CompleteValueConfig = { complete?: boolean; delay?: number; }; /** One entry in a precise emission sequence. */ type ValueConfig = CompleteValueConfig | ErrorValueConfig | NextValueConfig; /** * The one helper every method spy carries regardless of what it returns. * * Kept apart from the three return-type bundles because it is attached at a different place — once, * to the spy itself — and because putting it inside any of them would claim it for the observable * *properties* that reuse {@link AddObservableSpyMethods}, where nothing attaches it. */ interface AddThrowHelper { /** * Throw `error` on every call, until something else configures the spy. * * The sync counterpart of {@link AddPromiseSpyMethods.rejectWith} and of the observable * {@link AddObservableSpyMethods.throwWith}, and the cross-runtime answer to Vitest 4.1's * `mockThrow`: Bun and `node:test` ship no equivalent, so a suite that has to run on all three * would otherwise write `mockImplementation(() => { throw error })` by hand — and no runtime at * all can make one `calledWith` chain throw while its siblings answer normally. * * ```ts * cart.checkout.failWith(new HttpErrorResponse({ status: 500 })); * expect(() => cart.checkout(1)).toThrow(); * * cart.checkout.calledWith(BAD_ID).failWith(new Error('unknown cart')); * ``` * * **Not `throwWith`.** That name belongs to the observable helper that errors the stream * ({@link AddObservableSpyMethods.throwWith}), and at runtime every spy carries every bundle — * only the types tell them apart. One name for both would mean whichever bundle is attached last * silently wins, on every spy in the run. */ failWith(error?: unknown): void; } /** Helpers attached to an `Observable`-returning spy. */ interface AddObservableSpyMethods { /** * Emit `value` — buffered, so a subscription taken afterwards still receives it. * * @remarks * Every helper here exists only once the observable layer is registered: `import * 'vitest-auto-spy/rxjs'` once, in the setup file. Without it they are absent from a method spy * (`… .nextWith is not a function`) and `observablePropsToSpyOn` throws * `Observable spies require rxjs`. The buffer is a `ReplaySubject(1)` and it is *configuration*, * in the sense a `calledWith` chain is — `vi.clearAllMocks()` cannot reach it, so a spy that * outlives its test (a `TestBed` built in `beforeAll`) replays the previous test's value ahead of * whatever this one configured; call `resetAutoSpy(spy)` in `beforeEach` there. Prefer * {@link returnSubject} when the spec drives the stream itself, {@link nextWithPerCall} when each * call needs its own. */ nextWith(value?: T): void; /** Emit one value then complete. */ nextOneTimeWith(value?: T): void; nextWithValues(valuesConfigs: ValueConfig[]): void; nextWithPerCall(valuesPerCall?: ValueConfigPerCall[]): SubjectOf[]; throwWith(value: unknown): void; complete(): void; returnSubject(): SubjectOf; } /** Helpers attached to a `Promise`-returning spy. */ interface AddPromiseSpyMethods { resolveWith(value?: T): void; rejectWith(value?: unknown): void; resolveWithPerCall(valuesPerCall: ValueConfigPerCall[]): void; } /** * A configured return-value continuation for a `calledWith`/`mustBeCalledWith` * chain. `mockReturnValue` is the native name; `returnValue` is the * `jest-auto-spies` alias, kept so migrating tests are a pure import swap. */ type WithMockReturnValue = AddThrowHelper & { mockReturnValue: (value: ReturnType) => void; returnValue: (value: ReturnType) => void; }; /** Argument-matching helpers attached to a plain (sync) spy. */ interface AddCalledWithSpyMethods { /** * Configure what the spy answers for exactly these arguments. * * @remarks * **Lenient on a miss:** a call matching no chain falls through to the spy's default * (`mockReturnValue`, `resolveWith`, … — or `undefined`), so a wrong expectation here is silent. * {@link mustBeCalledWith} throws instead, printing wanted beside actual. Matching is positional * and arity-exact — `calledWith(1)` never answers `load(1, undefined)` — and accepts asymmetric * matchers (`expect.any(String)`, `expect.objectContaining({…})`). The chain is built at the call, * from the spy this helper is reached through, so `const { calledWith } = spy.load` compiles and * then throws `calledWith was called off its spy`: call it as a method — `spy.load.calledWith(…)` * — or bind it first. */ calledWith(...args: Parameters): WithMockReturnValue; mustBeCalledWith(...args: Parameters): WithMockReturnValue; } /** * Argument-matching helpers for a method whose return type is `any`, where every bundle is honest. * * `[any] extends [Promise]` is **true**, so such a method used to be typed as a promise * spy and nothing else: `calledWith(1).mockReturnValue(…)` did not compile — the chain offered * `resolveWith` / `rejectWith` instead — on a member that at runtime answers to all of them. A * legacy service and a JavaScript wrapper are where `any` comes from, which is to say a migration * off `jest-auto-spies`, where the same line compiled. */ type AddCalledWithAny = { /** Argument-matched and lenient on a miss — see {@link AddCalledWithSpyMethods.calledWith}. */ calledWith(...args: Parameters): AnyReturnHelpers & WithMockReturnValue; mustBeCalledWith(...args: Parameters): AnyReturnHelpers & WithMockReturnValue; }; /** Every value-shaped bundle at once — what a member typed `any` can legitimately be told to answer. */ type AnyReturnHelpers = AddObservableSpyMethods & AddPromiseSpyMethods; /** Argument-matching helpers that resolve to observable helpers. */ type AddCalledWithObservable = { /** Argument-matched and lenient on a miss — see {@link AddCalledWithSpyMethods.calledWith}. */ calledWith(...args: Parameters): AddObservableSpyMethods & AddThrowHelper; mustBeCalledWith(...args: Parameters): AddObservableSpyMethods & AddThrowHelper; }; /** Argument-matching helpers that resolve to promise helpers. */ type AddCalledWithPromise = { /** Argument-matched and lenient on a miss — see {@link AddCalledWithSpyMethods.calledWith}. */ calledWith(...args: Parameters): AddPromiseSpyMethods

& AddThrowHelper; mustBeCalledWith(...args: Parameters): AddPromiseSpyMethods

& AddThrowHelper; }; /** * The zero-argument `mockReturnValue()` a `void` method should accept. * * The runner's own `Mock` types the argument as `any`, which is not `void`, so * `spy.dispose.mockReturnValue()` fails with `TS2554: Expected 1 arguments, but got 0` on a method * whose whole point is that it returns nothing. This is an *added* overload, not a replacement: a * call that does pass a value still resolves against the runner's signature first and keeps its * chainable return type. */ interface AddVoidReturnHelpers { mockReturnValue(value?: undefined): void; returnValue(value?: undefined): void; } /** * Wrap a method's spy with the helper bundle chosen by its return type. * * Two details here are load-bearing rather than stylistic, and both come from the same bug report: * a spy member that silently became `never`, reported as * `Property 'mockReturnValue' does not exist on type 'never'` with nothing connecting it to the * method it came from. * * 1. **The fallback is the sync bundle, not `never`.** A generic method with a conditional return * type — `get(k: K): this[K] extends Stringified ? R : never`, the * shape of every typed configuration service — does *not* match * `(...args: any[]) => infer ReturnType`: the return type cannot be inferred to anything * concrete, so the conditional takes its false branch. Annihilating the member there threw away * every helper it should have had. * 2. **Each comparison is on tuples** (`[X] extends [Y]`), which switches distribution off. A bare * `ReturnType extends Promise<…>` distributes over the `infer`-bound parameter, and distributing * over `never` yields `never` — the same collapse by a different route, for a method whose * return type does resolve, to `never`. * 3. **The mock surface is `MockInstance`, not `Mock`.** `Mock` is `Mock` here, and * `Procedure` is `(...args: any[]) => any` — so it contributed a call signature that swallows * anything. An intersection accepts a call matching *either* member, so on a double of * `read(key: string)` all three of `read(1)`, `read('ok', 'extra')` and `read()` compiled, none * of them compiles on the real instance, and the spec stayed green while calling the double the * way production code never could. `MockInstance` is the same surface *without* the call and * construct signatures, so the only call signature left is `Method`'s own. Side effect worth * knowing: with one call signature instead of two, `expectTypeOf(spy.method).parameters` and * `.returns` resolve instead of collapsing to `never`. * 4. **It is `MockInstance`, and the type argument is the whole point.** Left bare it * defaults to `Procedure` again, and every *configuration* helper on the surface then took * `any`: `spy.getPosters.mockReturnValue(42)` compiled on a method returning `Poster[][]`, so * did `mockReturnValue(undefined)`, and so did `mockImplementation(() => of(null))` on one * returning `Observable`. That is the half of the promise a typed spy exists for — the * *arguments* had been checked since the `Mock` → `MockInstance` change above, the *stub* had * not — and the asymmetry was visible inside this very file: the `calledWith(…)` continuation * ({@link WithMockReturnValue}) has always typed `mockReturnValue` against `ReturnType`, * while the bare call next to it accepted anything. With the argument in place the runner types * `mockReturnValue` / `mockReturnValueOnce` as `MockReturnType`, `mockImplementation` / * `mockImplementationOnce` / `withImplementation` as `(...args: Parameters) => * ReturnType`, `mockResolvedValue` as the awaited return, and `mock.calls` / * `mock.lastCall` / `getMockImplementation()` follow. Reported from a migration off * `jest-auto-spies` by two people who had each written a standalone `tsc --strict` probe, * because a spy that does not reject a wrong stub reads as a spy that is not typed at all. * Cost: 274 type instantiations on the `types:budget` fixture, 9044 → 9318 of a budget of * 11 000. * 5. **`any` is answered first**, by `0 extends ReturnType & 1` — the one test that tells `any` from * every other type. Without it `[any] extends [Promise]` is true, so a method a legacy * service declares as `any` was typed as a promise spy and lost `mockReturnValue` on its * `calledWith` chain. See {@link AddCalledWithAny}. */ type AddSpyMethodsByReturnTypes = AddThrowHelper & Method & MockInstance & (Method extends (...args: any[]) => infer ReturnType ? 0 extends ReturnType & 1 ? AddCalledWithAny & AnyReturnHelpers : [ReturnType] extends [Promise] ? AddCalledWithPromise & AddPromiseSpyMethods

: [ReturnType] extends [ObservableLike] ? AddCalledWithObservable & AddObservableSpyMethods : [ReturnType] extends [void] ? AddCalledWithSpyMethods & AddVoidReturnHelpers : AddCalledWithSpyMethods : AddCalledWithSpyMethods); /** * Every call signature of `F`, in declaration order. * * `Parameters` and `ReturnType` read the **last** overload, which for a generated API client * is the one nobody calls: `ng-openapi-gen` and `openapi-generator` emit `observe: 'body'` first and * `observe: 'events'` last, so `Spy` types a method against `HttpEvent` and * `nextWith(body)` stops compiling — with no hint that overload order is what happened. * * Four signatures is the practical ceiling (the generated `observe` clients have three or four); * a function with fewer matches the same pattern, with the extra slots repeating what it has. */ type Overloads = F extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; } ? [(...args: A1) => R1, (...args: A2) => R2, (...args: A3) => R3, (...args: A4) => R4] : never; /** One call signature of an overloaded function, by index — `Overload`. */ type Overload = Overloads[N]; /** Which call signature to read, for one method or for the whole double. */ type OverloadChoice = 'first' | 'last'; /** How {@link Spy} should read a method that has more than one call signature. */ interface SpyOptions { /** * Which overload the spy's helpers are typed against. Default `'last'`, which is what * `Parameters` / `ReturnType` do on their own and therefore what every existing `Spy` means. * * **The symptom that leads here** is a stub of the real response shape being rejected, with * nothing in the message about overloads — * `TS2345: Argument of type 'Page' is not assignable to parameter of type 'HttpEvent'` on a * `nextWith(body)` / `resolveWith(body)` / `mockReturnValue(of(body))` against a generated * `observe` client. Neither the double nor the stub is wrong; both are being checked against the * signature nobody calls. `@ts-expect-error` on that line is the wrong answer twice over — it * stops checking the response shape, which is the thing the line exists to describe. * * **A map picks per method**, which is what a real type usually needs: `'first'` applied to the * whole double moves *every* overloaded member at once, and on a type as wide as `Response` or * `Performance` that breaks the members nobody was complaining about — five `TS2769`s on * `download` in the file where `getEntriesByType` was the one being fixed. * * ```ts * let perf: Spy; * ``` * * A name the type does not have is not an error — it simply never matches, so a rename leaves a * dead entry rather than a red build. That is the same trade `instanceMethodsToSpyOn` makes, and * for the same reason: the map is keyed by `string` so that a member reached through an * augmentation, or one only present under some `lib`, can still be named. */ overload?: OverloadChoice | Record; /** * The getter names `gettersToSpyOn` was called with, so the {@link AddAccessorsSpies} bag offers * exactly the members the runtime built — a key in neither this list nor * {@link SpyOptions.settersToSpyOn} is a compile error instead of a `Mock` that reads * `undefined` at runtime. * * ```ts * const thermo = createSpyFromClass(Thermo, { * gettersToSpyOn: ['level'], * }); * thermo.accessorSpies.getters.level.mockReturnValue(3); * thermo.accessorSpies.setters.level(3); // a declared pair is mirrored into both bags * ``` * * A type-level echo of a value the factory already takes, and no more: the names still reach the * runtime through the configuration argument, and an options type that pins no list — the default * `Spy` among them — keeps the bag over every key of `T`, exactly as before this field * existed, because a list the options leave open is a list the type cannot see. A non-literal * `string[]` pins no name either and falls back to the same every-key bag. */ gettersToSpyOn?: readonly string[]; /** The setter names `settersToSpyOn` was called with — see {@link SpyOptions.gettersToSpyOn}. */ settersToSpyOn?: readonly string[]; } /** The choice that applies to one member: the map's entry, the flat value, or the default. */ type OverloadFor = Options['overload'] extends OverloadChoice ? Options['overload'] : Key extends keyof Options['overload'] ? Options['overload'][Key] : 'last'; /** Apply {@link SpyOptions.overload} to one method type. */ type SelectOverload = OverloadFor extends 'first' ? (Overload extends Func ? Overload : Method) : Method; /** * The `[Symbol.dispose]()` every double this package builds carries. * * It calls `resetAutoSpy(this)`, so `using` releases the double at the end of the block and the * `afterEach` that existed only to reset one spy can go: * * ```ts * it('loads', () => { * using users = createSpyFromClass(UserService); // reset when the block ends * users.load.resolveWith([]); * }); * ``` * * Declared here rather than referenced as the global `Disposable`, for the same reason Vitest * declares its own: `Disposable` lives in `lib.esnext.disposable`, and a consumer whose `lib` stops * at ES2022 and who has no `@types/node` would see the published `.d.ts` fail on the name. A * structural declaration needs only `Symbol.dispose` to be typed, which is what a runtime capable of * `using` guarantees — and `Spy` stays assignable to `Disposable` wherever that type does exist. * * A type alias rather than an interface, deliberately: an interface has no implicit index * signature, and `Spy` picking one up in its intersection would stop it being assignable to * `Record` — which specs and helpers here do rely on. * * There is deliberately no `[Symbol.asyncDispose]`. `resetAutoSpy` is synchronous, so an async half * would add a microtask and advertise teardown that does not exist; `await using` already accepts a * sync-disposable (the spec falls back to `@@dispose` when `@@asyncDispose` is absent), so nothing * is lost. */ type SpyDisposable = { [Symbol.dispose](): void; }; /** * One configured accessor list off `Spy`'s options, or `undefined` for a list the options never * pinned down. * * Read through a conditional rather than `Options['gettersToSpyOn']`, because an indexed access * errors on an options type that omits the field — `Spy` is legal and * stays legal. The probe requires the property, so the merely optional declaration on * {@link SpyOptions} itself also reads as never pinned, which is what keeps a default `Spy` on * the every-key bag. */ type ConfiguredAccessorList = Options extends Record ? Exclude : undefined; /** The element keys of one configured accessor list; an unpinned list contributes none. */ type AccessorListKeys = List extends readonly (infer Keys)[] ? Keys : never; /** The bag itself, over whichever keys {@link AddAccessorsSpies} settled on. */ type AccessorSpiesBag = { accessorSpies: { getters: { [K in Keys]: Mock<() => T[K]>; }; setters: { [K in Keys]: Mock<(value: T[K]) => void>; }; }; }; /** * The `accessorSpies` bag added to every auto-spy. * * Each half is typed against the member it stands for, not as a bare `Mock`: `Mock` is * `Mock`, so `accessorSpies.getters.count.mockReturnValue('not a number')` compiled on * `get count(): number` — the read-side twin of the hole the method surface closed by taking * `MockInstance` (see {@link AddSpyMethodsByReturnTypes}). `Mock<…>` rather than * `MockInstance<…>` keeps the call signature the bag has always had, so a spec that invokes the * accessor spy directly still compiles. * * **The keys are the configured lists, not all of `T`.** The runtime builds the bag from nothing * but `gettersToSpyOn` / `settersToSpyOn` (plus `autoSpyAccessors` discovery), so the total bag * this type used to map over `keyof T` typed `spy.accessorSpies.setters.name` as a callable `Mock` * on a double no setter was configured on — and the read answered `undefined` at runtime, three * steps from the configuration that caused it. The two parameters carry those lists from * {@link SpyOptions}; each defaults to `undefined`, a list the options never pinned, and two * unpinned lists select the every-key bag `Spy` has always had — through the same plain mapped * type as before, so a double nobody narrowed pays nothing for the option existing. * * A pinned list narrows **both halves** to the union of the two: the runtime promotes a configured * getter into the setter list when the prototype declares the pair (and the other way round), so a * key in either list can honestly sit in both bags. A `string` element — a non-literal list — * widens the union right back to every string key, so such a configuration degrades to the old * bag instead of collapsing it to `never`. */ type AddAccessorsSpies = [Getters, Setters] extends [undefined, undefined] ? AccessorSpiesBag : AccessorSpiesBag | AccessorListKeys>>; /** * A recursively-mocked `T`: object properties become nested deep mocks (so * `mock.repo.user.find()` works without seeding), methods become spies, and * primitive properties keep their type (seed them via `overrides` or `mockValueProp`). * * The mapping keeps `readonly`, for the reason spelled out on {@link Spy}: a plain assignment is * silently inert on a member replaced by a spied accessor, and `mockValueProp` is not. */ type DeepMockProxy = SpyDisposable & { [K in keyof T]: T[K] extends Func ? AddSpyMethodsByReturnTypes : T[K] extends object ? DeepMockProxy : T[K]; }; /** * Fully-typed spy of `T`. * * ```ts * let cart: Spy; * // a generated client whose useful overload is the first one: * let cinemas: Spy; * // the accessorSpies bag keyed by the configured accessor lists: * let thermo: Spy; * ``` * * @remarks * A **mapped type**, so it drops `#private` and `private` members and is therefore *not* assignable * to `T`. Declare the variable as `Spy` — never as `T`, `Mocked` or `MockedObject` — and * call `asInstance(spy)` at the one place a real `T` is wanted. The compiler reports this as missing * private fields and says nothing about the real cause, which is why the `no-mocked-for-spy` lint * rule exists. `vi.mocked(…)` is not the bridge either: it retypes the value as `MockedObject`, * which brings `T`'s private members back and carries the runner's `mockReturnValue` but none of * `calledWith`, `resolveWith` or `accessorSpies` — so the assignment it was reached for still fails * and the helpers stop compiling. `asInstance` and `asSpy` are typed identity functions; nothing * about them changes at runtime. * * The mapping is homomorphic, so it **keeps** `readonly`, and that is deliberate rather than * incidental. Stripping the modifier was tried and reverted: it made a plain assignment compile * everywhere, including on a member replaced by a spied accessor (`gettersToSpyOn`), where the write * reaches the setter spy and the getter goes on answering `undefined`. That trades a loud `TS2540` * fixable in one line for a silent runtime no-op, which is the defect class this file's other types * exist to remove. Keeping it pushes the spec to `mockValueProp(spy, 'token', value)` — the answer * in both cases, and one that needs no type at all, because `readonly` does not take a key out of * `keyof T`. * * `Reflect.set(spy, 'token', value)` is **not** the escape hatch it looks like: it invokes the same * `[[Set]]`, so it is equally inert on a spied accessor, and it additionally returns `true` — which * misleads a caller that checks the result. Probed on node, against a spied accessor whose getter * answers `undefined`: plain assignment and `Reflect.set` both leave the getter at `undefined` and * both record on the setter spy; only `Object.defineProperty` — what `mockValueProp` does — makes * the value readable. * * {@link Mutable} stays the secondary answer for a spec that would rather write plain assignments to * plain data members. It does not help on a spied accessor either. */ type Spy = AddAccessorsSpies, ConfiguredAccessorList> & SpyDisposable & { [K in keyof T]: Required[K] extends Func ? AddSpyMethodsByReturnTypes[K], Options, K>> : T[K] extends ObservableLike ? AddObservableSpyMethods & T[K] : T[K]; }; /** * What a `mock*Prop` helper accepts as the stand-in for a member typed `V`. * * Exactly `V`, except for two deliberate widenings. * * A **callable** member also accepts a bare function of the same shape. The reason is `Spy`: the * recommended way to reach a service in an Angular spec is `injectSpy(X)` / * `asSpy(TestBed.inject(X))`, and on that object a signal-valued member is typed * `Signal & Mock & …`. Requiring the exact member type would mean no real signal could ever be * written into it — so the spec would have to keep the instance under a second name purely to patch * it, which is what this avoids. * * **Every** member also accepts `null` and `undefined`. "This member is absent in this test" is a * normal thing for a spec to say — `mockValueProp(navigation, 'currentFocus', null)` for a service * that reads the field as *has focus / has none*, `mockValueProp(window, 'AudioContext', undefined)` * for an API a TV platform does not ship — and interface declarations routinely omit the `| null` * the runtime has. * * Be clear about what that buys, because it is less than it looks: such a call *already* compiled, * by falling through to the untyped escape-hatch overload each of these helpers carries. Nothing * a `mock*Prop` helper is handed is ever rejected, and that is deliberate — the escape hatch is a * routine tool, not a last resort (a partial fixture for a fat type, a synthetic DOM event, a * `#private` field). What the widening changes is which overload answers: the checked one, whose * `K extends keyof T` gives the property name completions and a spelling check. The value is not * checked either way. */ type PropStubValue = (V extends (...args: infer Args) => infer Return ? V | ((...args: Args) => Return) : V) | null | undefined; /** * `T` with every `readonly` modifier removed. * * ```ts * const options: Mutable = { ...defaults }; * * options.retries = 0; * ``` * * {@link Spy} is a homomorphic mapped type, so it *preserves* `readonly` — and an abstract class * whose useful members are getters (`abstract get pathname(): string`, the shape `createAutoMock` * exists for) therefore produces a double the spec cannot assign to: `TS2540: Cannot assign to * 'pathname' because it is a read-only property`, even though the Proxy's `set` trap handles the * write perfectly well at runtime. * * ```ts * const location: Mutable> = createAutoMock(); * * location.pathname = '/movies'; * ``` * * **`mockValueProp(location, 'pathname', '/movies')` is the primary answer, and this is the * secondary one.** `mockValueProp` needs no type at all — `readonly` does not take a key out of * `keyof T`, so its checked overload accepts the member as it stands — it defines the value rather * than assigning it, so it also works on a member replaced by a spied accessor, and * `restoreMockedProps()` undoes it. Reach for `Mutable` when the spec assigns directly, repeatedly, * to plain data members and does not want the bookkeeping; it is the same `[[Set]]` a bare * assignment is, so it is just as inert on a spied accessor. */ type Mutable = { -readonly [K in keyof T]: T[K]; }; /** * A partial `T` that stays partial all the way down. * * `Partial` is one level deep, so a fixture for a configuration object, an account token or a * route snapshot — a tree the test reads one leaf of — has to name the type of every nested level * and build it with its own call. What it buys over `as T` is what must survive: a key that `T` does * not have, or that a refactor removed, is still rejected at any depth. * * **Every level also accepts the real value**, which is the `T |` in the object branch. Without it a * deep partial stops accepting the object it is a partial *of*: `{@link BuiltIn}` lists the values * that must be handed back untouched, but it can only list types from ECMAScript — naming `Node` or * `NodeList` here would put `lib: ["DOM"]` into the published `.d.ts`, and this package is imported * from `/node`, `/nestjs` and `/bun` as well. So a host object was mapped over instead, and a real * `NodeList` stopped being assignable to the mapping of itself: * * ```ts * createMock({ addedNodes: nodeList, target: element }); * // ^ Type 'NodeList' is not assignable to type '{ readonly baseURI?: … }' * ``` * * The union costs nothing at the check that matters: excess-property checking against a union * accepts a key present in *some* member, and both members here have exactly the keys of `T`, so a * key `T` does not have is still rejected — at any depth. */ type DeepPartial = T extends Func ? T | ((...args: Parameters) => ReturnType) : T extends BuiltIn ? T : T extends readonly (infer Element)[] ? DeepPartial[] : T extends object ? T | { [K in keyof T]?: DeepPartial; } : T; /** * Values {@link DeepPartial} must hand back untouched. * * Mapping over a `Date` or a `Map` would turn it into an object of optional methods — accepted by * the compiler, useless at runtime, and impossible to write a fixture against. */ type BuiltIn = Date | Error | Func | Promise | ReadonlyMap | ReadonlySet | RegExp; /** * Return values for a spy's methods, keyed by method name. * * Written as part of the configuration so that a provider needs no second statement — which is what * pushes a shared double into module scope in the first place, where under `isolate: false` every * importing file then shares its spies. * * A method `Object.prototype` also has (`toString`) accepts the inherited member too: every literal * carries it, so `{ returns: { reload: undefined } }` was rejected on a type declaring `toString()`. */ type MethodReturns = { [K in Exclude, ObjectPrototypeKey>]?: Required[K] extends Func ? ReturnType[K]> : never; } & { [K in Extract, ObjectPrototypeKey>]?: Required[K] extends Func ? ObjectPrototypeMembers[K] | ReturnType[K]> : never; }; type ObjectPrototypeMembers = typeof Object.prototype; type ObjectPrototypeKey = keyof ObjectPrototypeMembers; /** Everything the strict-mode handler is told about a call nobody configured. */ interface UnstubbedCall { /** The class the double was built from — `undefined` for a type-driven `createAutoMock`. */ className: string | undefined; /** The method that was called. */ method: string; /** The arguments it was called with. */ args: unknown[]; } /** * What to do about a call nobody configured. * * Whatever it returns becomes that call's return value, so a handler can supply a blanket default * (`vitest-mock-extended`'s `fallbackMockImplementation`) instead of failing; throwing from it is * what `strict: true` does. */ type UnstubbedCallHandler = (call: UnstubbedCall) => unknown; /** What the read hook is told about a member of a double that was read — or subscribed to — with nothing configured. */ interface UnstubbedRead { /** The class the double was built from — `undefined` for a type-driven `createAutoMock` given no `name`. */ className: string | undefined; /** The spied getter that was read, or the observable property that was subscribed to. */ member: string; /** `'getter'` for a spied accessor, `'observable'` for an `observablePropsToSpyOn` stream. */ kind: 'getter' | 'observable'; /** How many times the test read the getter, or subscribed to the stream. */ count: number; } /** * What to do about a member a test read with nothing configured — see * {@link StrictSpyConfiguration.onUnstubbedRead}. Called after the test, once per member. */ type UnstubbedReadHandler = (read: UnstubbedRead) => void; /** * Strict-mode configuration, shared by every factory that builds a double. * * The failure this exists for is specific to wide services, which is where this library is used * most: on a forty-method collaborator with one method left unstubbed, the call returns `undefined` * and the test fails three frames later on something that has nothing to do with the omission. The * only tool before this was {@link ClassSpyConfiguration.onlyMethodsToSpyOn}, which *removes* the * method — so the failure reads `service.load is not a function` and blames the spy rather than the * spec. * * **What counts as stubbed.** Anything that configures the method at all: `mockReturnValue`, * `mockImplementation`, `returns:` in the configuration, `resolveWith` / `rejectWith` / * `resolveWithPerCall`, `nextWith` / `throwWith` / `complete` / `returnSubject`, or *any* * `calledWith` / `mustBeCalledWith` chain. * * **A `calledWith` chain that does not match the call is still stubbed**, and deliberately so. * `calledWith(1)` is a statement that this method is configured, and the argument-level version of * strictness already has a name — `mustBeCalledWith`, which throws showing wanted next to actual. * Making `strict` throw on an argument miss would silently reclassify every existing `calledWith` * into `mustBeCalledWith` and print a worse message than the one that tool already prints. Strict * mode answers "nobody configured this method", not "nobody configured this call". */ interface StrictSpyConfiguration { /** * Throw when a method nobody configured is called, naming the class, the method and the * arguments, instead of returning `undefined`. * * ```ts * const users = createSpyFromClass(UserService, { strict: true }); * * users.load.resolveWith([]); * users.remove(1); // throws: nothing configured UserService.remove * ``` * * Sugar for an {@link onUnstubbedCall} that throws. An explicit `strict: false` also switches off * a default installed globally. */ strict?: boolean | undefined; /** * The general form of {@link strict}: run this instead of returning `undefined`, and use whatever * it returns as the call's result. * * ```ts * createSpyFromClass(UserService, { * onUnstubbedCall: ({ className, method }) => { * unstubbed.push(`${className}.${method}`); // record, don't fail * }, * }); * ``` * * Wins over {@link strict} when both are given, and over anything installed globally. */ onUnstubbedCall?: UnstubbedCallHandler | undefined; /** * Take this double's unconfigured reads instead of the report `setupAutoSpy({ unconfiguredReads })` * makes of them: a spied getter the test read that nothing configured, and an observable property * the test subscribed to that nothing fed by the time it ended. * * ```ts * createSpyFromClass(Router, { * gettersToSpyOn: ['url'], * onUnstubbedRead: ({ className, member, count }) => { * unread.push(`${className}.${member} ×${count}`); // survey, don't fail * }, * }); * ``` * * Called after each test, once per member, and only under `setupAutoSpy` — a test is what it marks * out. Wins over {@link strict} and over a suite-wide handler; the read itself still answers * `undefined`. */ onUnstubbedRead?: UnstubbedReadHandler | undefined; } /** Restricts/extends what `createSpyFromClass` spies on. */ interface ClassSpyConfiguration extends StrictSpyConfiguration { /** * Extra callables to spy on, **added** to the methods discovered on the prototype. * * @remarks * **Additive, not a whitelist** — `jest-auto-spies`' semantics, and the single most repeated * mistake in this API. Discovery already finds every prototype method, so the only names worth * passing are the ones it cannot see: an arrow-function property, an Angular `signal()` field, a * method of an ngrx `signalStore()`. {@link instanceMethodsToSpyOn} is the same behaviour under a * name that says so. The exhaustive whitelist is {@link onlyMethodsToSpyOn}, which skips discovery * entirely. Dropping either option usually fixes more than it breaks. */ methodsToSpyOn?: OnlyMethodKeysOf[]; /** * Spy on these methods and no others — prototype discovery is skipped entirely. * * @remarks * Every other method is then absent from the spy, so code under test that calls one fails with * `… is not a function`. Occasionally that is the point (a wide collaborator where an unexpected * call should be loud); when it is not, {@link methodsToSpyOn} is the additive option. A name here * that the prototype does not have is reported as a probable typo, since under a restricting list * a misspelling silently un-spies the real method. */ onlyMethodsToSpyOn?: OnlyMethodKeysOf[]; /** * Callables that live on the *instance* rather than on the prototype — an arrow-function * property, an Angular `signal()` / `computed()` field, a method of an ngrx `signalStore()` — * **added** to whatever prototype discovery produced. * * @remarks * Behaviourally identical to {@link methodsToSpyOn}; prefer this name in new code. */ instanceMethodsToSpyOn?: OnlyMethodKeysOf[]; observablePropsToSpyOn?: OnlyObservablePropsOf[]; /** * Getters to replace with a spied accessor. * * Any string key of `T` may be named: whether a member is an accessor is decided by its * descriptor on the prototype, not by the type of the value it returns — and a name that has no * accessor there is reported at runtime, with the reason. For a **signal-valued** getter prefer * `mockSignalProp(service, 'state', initial)` (`/angular`): a spied getter returns `undefined` * until it is configured, while a real signal keeps everything downstream of it reactive. * * These names reach the *type of* the {@link AddAccessorsSpies} bag only through the `Options` * type argument — see {@link SpyOptions.gettersToSpyOn}; from this list alone the bag stays over * every key of `T`. */ gettersToSpyOn?: AccessorKeysOf[]; /** Setters to replace with a spied accessor. See {@link gettersToSpyOn}. */ settersToSpyOn?: AccessorKeysOf[]; /** Auto-discover and spy every getter/setter on the prototype chain (merged with the explicit lists). */ autoSpyAccessors?: boolean; /** * Answer a member the prototype never named with a spy, instead of leaving it absent. * * For a **partially abstract** class — `abstract` declarations plus at least one concrete member, * the ordinary Angular DI-token shape. `abstract read(): string` is erased before it reaches a * prototype, so discovery finds only the concrete members; the empty-prototype fallback that * covers a *fully* abstract class does not fire, and every abstract member is missing while * `Spy` types it as present. The read yields `undefined` and the failure lands in production * code as `… is not a function`. * * ```ts * abstract class LocalStorage { * abstract read(key: string): string | null; * clear(): void {} * } * * provideAutoSpy(LocalStorage, { fillMissing: true }); * ``` * * Opt-in, and it has to be: TypeScript erases `abstract`, so at runtime this class and a concrete * one are indistinguishable, and filling every unknown key by default would silence a genuine * typo on every class in the suite. Naming the members in {@link instanceMethodsToSpyOn} stays the * alternative when the list is short and worth stating. */ fillMissing?: boolean; /** * What each named method returns, applied as the spy is built. * * ```ts * providers: [provideAutoSpy(ProductsService, { returns: { getProducts: of([]) } })]; * ``` * * The alternative is a second statement in every `beforeEach` — `injectSpy(X).m.mockReturnValue(…)` * — and the shortcut people take instead is an exported `const` provider carrying the values, * which under `isolate: false` is one set of spies shared by every file that imports it. * * It is the method's default: a `calledWith(…)` chain configured afterwards still decides the value * for its arguments, and a later `resolveWith` / `failWith` replaces it. */ returns?: MethodReturns; /** * Methods that answer **the double itself** — the `returns` entry a literal cannot spell, because * the double does not exist yet when the configuration is written. * * ```ts * provideAutoSpy(QueryBuilder, { selfReturning: ['where', 'orderBy'] }); * ``` * * For a fluent call the code under test chains off, where an unconfigured link answers `undefined` * and the next hop throws. It is a default in the same container as {@link returns}: it counts as * configured under `strict`, a later `calledWith` / `mockReturnValue` still wins, and a method also * named in `returns` answers that value — which is how a spec overrides a registered chain. * `mockDeep`'s boolean `selfReturning` is the same idea for every node of a deep double. */ selfReturning?: OnlyMethodKeysOf[]; /** * Values for members that are **not** method results — an Observable property the code under test * subscribes to, a plain field, a signal. * * The counterpart of {@link returns}, and the symmetry that was missing: `provideAutoSpyForToken` * has taken property seeds since it was introduced, while the class-based factory took only * method configuration, so a double needing both had to be provided in one statement and finished * in another. Seeded last, so a member named here wins over anything discovery or * `observablePropsToSpyOn` produced for the same key. * * ```ts * provideAutoSpy(FavoritesService, { * overrides: { favoritesCacheUpdated$: of(undefined), favoriteItems: [] }, * returns: { load: of([]) }, * }); * ``` * * A seeded key is stored exactly as written and is **not** a spy — that is the difference from * `returns`, which configures the spy the factory built. Seed a real `Subject` here when the spec * drives the stream itself; name the method in `returns` when it should stay assertable. */ overrides?: DeepPartial; /** * Materialize each method spy on first access instead of building all of them up front. * * **On by default.** On a forty-method class where a test touches two, holding two thousand spies * costs 27 ms and 35 MB lazily against 257 ms and 425 MB eagerly. The reverse case — a test that * calls every method — pays 5% in time and 1% in memory for the accessor indirection, which is why * this is a default rather than a choice. * * Set `false` only when a spec inspects the spy through property descriptors; enumeration * (`Object.keys`, spread, snapshots) already works, since the placeholders are enumerable. * * `'proxy'` is the same laziness with a different placeholder: one trap object for the whole * class instead of one `Object.defineProperty` accessor per method. What an untouched double * retains then stops scaling with the width of the class, which is the figure that ends a CI job * under `isolate: false` — and it is the only reason to reach for it, since a `Proxy` cannot * remove itself and taxes every read and every call for the life of the double. Worth it on the * wide generated clients (orval, ng-openapi-gen, ngrx facades) and a loss on an ordinary * five-method service; `docs-site/core/performance.md` has the break-even. */ lazySpies?: boolean | 'proxy'; } /** * The configuration `createSpyFromInstance` takes: {@link ClassSpyConfiguration}, plus the one option * that only makes sense when a real object is being patched. */ interface InstanceSpyConfiguration extends ClassSpyConfiguration { /** * Record every call, but run the real method until the test configures that method. * * ```ts * const users = createSpyFromInstance(TestBed.inject(UserService), { passthrough: true }); * * await component.save(); // the real UserService.save ran, and was recorded * expect(users.save).toHaveBeenCalledWith({ id: 1 }); * * users.load.resolveWith([]); // only load is a double now * ``` * * Any configuration takes over the **whole** method — `calledWith`, `resolveWith`, `returns`, * `mockReturnValue` — and `resetAutoSpy` hands it back to the real one. The real method runs * with the instance as `this`. Contradicts an explicit `strict: true` or `onUnstubbedCall` on the * same call, which throws; a suite-wide `strict` or a registered one yields to it. */ passthrough?: boolean | undefined; } export type { AccessorKeysOf as A, SubjectOf as B, ClassType as C, DeepMockProxy as D, ErrorValueConfig as E, Func as F, UnstubbedCallHandler as G, UnstubbedRead as H, InstanceSpyConfiguration as I, UnstubbedReadHandler as J, ValueConfigPerCall as K, MethodReturns as M, NextValueConfig as N, ObservableLike as O, PropStubValue as P, Spy as S, UnstubbedCall as U, ValueConfig as V, WithMockReturnValue as W, AddAccessorsSpies as a, AddCalledWithAny as b, AddCalledWithObservable as c, AddCalledWithPromise as d, AddCalledWithSpyMethods as e, AddObservableSpyMethods as f, AddPromiseSpyMethods as g, AddSpyMethodsByReturnTypes as h, AddThrowHelper as i, AddVoidReturnHelpers as j, AnyReturnHelpers as k, AutoSpyRxjsTypes as l, ClassSpyConfiguration as m, CompleteValueConfig as n, DeepPartial as o, Mutable as p, OnlyMethodKeysOf as q, OnlyObservablePropsOf as r, OnlyPropsOf as s, Overload as t, OverloadChoice as u, Overloads as v, SpyDisposable as w, SpyOptions as x, StrictSpyConfiguration as y, SubjectLike as z };