export interface Spy any> { (...args: Parameters): ReturnType; readonly calls: ReadonlyArray>; readonly callCount: number; reset(): void; } function createDefaultImplementation(): (...args: unknown[]) => unknown { return () => undefined; } export function createSpy any>( implementation?: T, thisArg?: unknown, ): Spy { const recordedCalls: Parameters[] = []; const impl = (implementation ?? (createDefaultImplementation() as T)) as T; const spy = function (this: unknown, ...args: Parameters): ReturnType { recordedCalls.push(args); const context = thisArg !== undefined ? thisArg : this; return impl.apply(context, args); } as Spy; Object.defineProperty(spy, 'calls', { value: recordedCalls, writable: false, }); Object.defineProperty(spy, 'callCount', { get() { return recordedCalls.length; }, }); Object.defineProperty(spy, 'reset', { value() { recordedCalls.length = 0; }, }); return spy; } export function spyOn( target: T, property: K, ): Spy any>> { const original = target[property]; if (typeof original !== 'function') { throw new TypeError(`Property "${String(property)}" is not callable`); } const spy = createSpy(original as Extract any>, target); (target as Record)[property] = spy as unknown as T[K]; return spy; }