/** Spy. */ export type Spy any> = T & Spy.CallInformation export namespace Spy { /** Information for calls on a spy. */ export interface CallInformation any> { /** Information for each call. */ readonly calls: ReadonlyArray> /** Information for each returned call. */ readonly returnedCalls: ReadonlyArray> /** Information for each thrown call. */ readonly thrownCalls: ReadonlyArray> /** Information of the first call. */ readonly firstCall: Call | null /** Information of the last call. */ readonly lastCall: Call | null /** Information of the first returned call. */ readonly firstReturnedCall: ReturnedCall | null /** Information of the last returned call. */ readonly lastReturnedCall: ReturnedCall | null /** Information of the first thrown call. */ readonly firstThrownCall: ThrownCall | null /** Information of the last thrown call. */ readonly lastThrownCall: ThrownCall | null /** Reset calls. */ reset(): void } /** Information for each call. */ export type Call any> = | ReturnedCall | ThrownCall /** Information for each returned call. */ export interface ReturnedCall any> { type: "return" this: This arguments: Parameters return: ReturnType } /** Information for each thrown call. */ export interface ThrownCall any> { type: "throw" this: This arguments: Parameters throw: any } } type This = T extends (this: infer TT, ...args: any[]) => any ? TT : undefined /** * Create a spy. */ export function spy(): Spy<() => void> /** * Create a spy with a certain behavior. * @param f The function of the spy's behavior. */ export function spy any>(f: T): Spy // Implementation export function spy any>(f?: T): Spy { const calls = [] as Spy.Call[] function spy(this: This, ...args: Parameters): ReturnType { let retv: ReturnType try { retv = f ? f.apply(this, args) : undefined } catch (error) { calls.push({ type: "throw", this: this, arguments: args, throw: error, }) throw error } calls.push({ type: "return", this: this, arguments: args, return: retv, }) return retv } Object.defineProperties(spy, { calls: descriptors.calls(calls), returnedCalls: descriptors.returnedCalls, thrownCalls: descriptors.thrownCalls, firstCall: descriptors.firstCall, lastCall: descriptors.lastCall, firstReturnedCall: descriptors.firstReturnedCall, lastReturnedCall: descriptors.lastReturnedCall, firstThrownCall: descriptors.firstThrownCall, lastThrownCall: descriptors.lastThrownCall, reset: descriptors.reset, toString: descriptors.toString(f), }) return spy as Spy } const descriptors = { calls(value: ReadonlyArray>): PropertyDescriptor { return { value, configurable: true } }, returnedCalls: { get: function returnedCalls( this: Spy.CallInformation, ): ReadonlyArray> { return this.calls.filter(isReturned) }, configurable: true, } as PropertyDescriptor, thrownCalls: { get: function thrownCalls( this: Spy.CallInformation, ): ReadonlyArray> { return this.calls.filter(isThrown) }, configurable: true, } as PropertyDescriptor, firstCall: { get: function firstCall( this: Spy.CallInformation, ): Spy.Call | null { return this.calls[0] || null }, configurable: true, } as PropertyDescriptor, lastCall: { get: function lastCall( this: Spy.CallInformation, ): Spy.Call | null { return this.calls[this.calls.length - 1] || null }, configurable: true, } as PropertyDescriptor, firstReturnedCall: { get: function firstReturnedCall( this: Spy.CallInformation, ): Spy.ReturnedCall | null { for (let i = 0; i < this.calls.length; ++i) { const call = this.calls[i] if (isReturned(call)) { return call } } return null }, configurable: true, } as PropertyDescriptor, lastReturnedCall: { get: function lastReturnedCall( this: Spy.CallInformation, ): Spy.ReturnedCall | null { for (let i = this.calls.length - 1; i >= 0; --i) { const call = this.calls[i] if (isReturned(call)) { return call } } return null }, configurable: true, } as PropertyDescriptor, firstThrownCall: { get: function firstThrownCall( this: Spy.CallInformation, ): Spy.ThrownCall | null { for (let i = 0; i < this.calls.length; ++i) { const call = this.calls[i] if (isThrown(call)) { return call } } return null }, configurable: true, } as PropertyDescriptor, lastThrownCall: { get: function lastThrownCall( this: Spy.CallInformation, ): Spy.ThrownCall | null { for (let i = this.calls.length - 1; i >= 0; --i) { const call = this.calls[i] if (isThrown(call)) { return call } } return null }, configurable: true, } as PropertyDescriptor, reset: { value: function reset(this: Spy.CallInformation): void { ;(this.calls as any[]).length = 0 }, configurable: true, } as PropertyDescriptor, toString(f: ((...args: any[]) => any) | undefined): PropertyDescriptor { return { value: function toString() { return `/* The spy of */ ${f ? f.toString() : "function(){}"}` }, configurable: true, } }, } function isReturned(call: Spy.Call): call is Spy.ReturnedCall { return call.type === "return" } function isThrown(call: Spy.Call): call is Spy.ThrownCall { return call.type === "throw" }