/* eslint-disable no-case-declarations */ import { getLookup } from '../helper/index.js'; import { ConstructorFunction, ConstructorParams, FunctionCallLookup, FunctionName, FunctionTypes, GetterTypes, IMocked, LookupParams, LookupType, OperatorFunction, SetterTypes, } from './contracts.js'; /** * Mocks a function on an existing Mock. * Allows function call verification to be performed later in the test. * You can optionally set a mock function implementation that will be called. * * @param functionName * @param mockFunction */ export function setupConstructor>(): OperatorFunction { return (mocked: IMocked) => { mocked.mockConstructor = class MockConstructor { constructor(...args: ConstructorParams) { trackConstructorCall(mocked, args as any); } } as C; mocked.constructorCallLookup['constructorFunction'] = []; return mocked; }; } /** * Mocks a function on an existing Mock. * Allows function call verification to be performed later in the test. * You can optionally set a mock function implementation that will be called. * * @param functionName * @param mockFunction */ export function setupFunction, U extends FunctionName>( functionName: U, mockFunction?: T[U], ): OperatorFunction { return (mocked: IMocked) => { const functionReplacement = (...args: LookupParams) => { let returnValue: any; const mockedFunction = mocked.functionReplacementLookup['function']?.[functionName as string]; if (mockedFunction instanceof Function) { returnValue = mockedFunction.apply(mocked.mock, args); } trackFunctionCall(mocked, 'function', functionName, args); return returnValue; }; const functionLookup = (mocked.functionReplacementLookup['function'] = mocked.functionReplacementLookup['function'] || {}) as Partial>; functionLookup[functionName as string] = mockFunction; // we do not replace an existing function in case it has already been destructured and sut already has a reference to it // we do replace the mocked implementation above though if ((mocked.mock[functionName] as unknown as Function)?.name != functionReplacement.name) { mocked.mock[functionName] = functionReplacement as any; } mocked.functionCallLookup[functionName] = []; return mocked; }; } /** * Mocks a static function on an existing Mock. * Allows function call verification to be performed later in the test. * You can optionally set a mock function implementation that will be called. * * @param functionName * @param mockFunction */ export function setupStaticFunction< T, C extends ConstructorFunction, U extends FunctionName, >(functionName: U, mockFunction?: C[U]): OperatorFunction { return (mocked: IMocked) => { const functionReplacement = (...args: LookupParams) => { let returnValue: any; const mockedFunction = mocked.functionReplacementLookup['staticFunction']?.[functionName as string]; if (mockedFunction instanceof Function) { returnValue = mockedFunction.apply(mocked.mock, args); } trackFunctionCall(mocked, 'staticFunction', functionName, args); return returnValue; }; const staticFunctionLookup = (mocked.functionReplacementLookup['staticFunction'] = mocked.functionReplacementLookup['staticFunction'] || {}) as Partial>; staticFunctionLookup[functionName as string] = mockFunction; if ((mocked.mockConstructor[functionName] as unknown as Function)?.name != functionReplacement.name) { mocked.mockConstructor[functionName] = functionReplacement as any; } mocked.staticFunctionCallLookup[functionName] = []; return mocked; }; } /** * Sets up a property on an existing Mock. * Allows the value of the property to be defined. * Enables get and set function call verification to be performed. * * @param propertyName * @param value */ export function setupProperty, U extends FunctionName>( propertyName: U, value?: T[U], ): OperatorFunction { return defineProperty(propertyName, value !== undefined ? () => value : undefined); } /** * Sets up a static property on an existing Mock. * Allows the value of the property to be defined. * Enables get and set function call verification to be performed. * * @param propertyName * @param value */ export function setupStaticProperty, U extends FunctionName>( propertyName: U, value?: C[U], ): OperatorFunction { return defineStaticProperty(propertyName, value !== undefined ? () => value : undefined); } /** * Sets up a property on an existing Mock. * Allows getter and setter functions to be set. * Enables get and set function call verification to be performed. * * @param propertyName * @param value */ export function defineProperty, U extends FunctionName>( propertyName: U, getter?: () => T[U], setter?: (value: T[U]) => void, ): OperatorFunction { return (mocked: IMocked) => { return definePropertyImpl(mocked, 'getter', propertyName, getter, setter); }; } /** * Sets up a static property on an existing Mock with constructor. * Allows getter and setter functions to be set. * Enables get and set function call verification to be performed. * * @param propertyName * @param value */ export function defineStaticProperty, U extends FunctionName>( propertyName: U, getter?: () => C[U], setter?: (value: C[U]) => void, ): OperatorFunction { return (mocked: IMocked) => { return definePropertyImpl(mocked, 'staticGetter', propertyName, getter, setter); }; } function definePropertyImpl< T, C extends ConstructorFunction, U extends GetterTypes, K extends FunctionName, >( mocked: IMocked, lookupType: U, propertyName: K, getter?: () => any, setter?: (value: any) => void, ): IMocked { const propertyGetter = () => { trackGetterCall(mocked, lookupType, propertyName); const mockedFunction = mocked.functionReplacementLookup[lookupType]?.[propertyName as string]; return mockedFunction ? mockedFunction() : undefined; }; let setterLookupType: SetterTypes; switch (lookupType) { case 'getter': setterLookupType = 'setter'; const getterLookup = (mocked.functionReplacementLookup['getter'] = mocked.functionReplacementLookup['getter'] || {}); getterLookup[propertyName as string] = getter; const setterLookup = (mocked.functionReplacementLookup['setter'] = mocked.functionReplacementLookup['setter'] || {}); setterLookup[propertyName as string] = setter; break; case 'staticGetter': const staticGetterLookup = (mocked.functionReplacementLookup['staticGetter'] = mocked.functionReplacementLookup['staticGetter'] || {}); staticGetterLookup[propertyName as string] = getter; const staticSetterLookup = (mocked.functionReplacementLookup['staticSetter'] = mocked.functionReplacementLookup['staticSetter'] || {}); staticSetterLookup[propertyName as string] = setter; setterLookupType = 'staticSetter'; break; } const setterProperty: FunctionName = propertyName as any; const propertySetter = (value: any) => { trackSetterCall(mocked, setterLookupType, setterProperty, [value]); const mockedFunction = mocked.functionReplacementLookup[setterLookupType]?.[propertyName as string]; if (mockedFunction != null) { mockedFunction.apply(mocked.mock, [value]); } }; let target: T | C; getLookup(mocked, lookupType)[propertyName] = []; switch (lookupType) { case 'staticGetter': target = mocked.mockConstructor; getLookup(mocked, 'staticSetter')[setterProperty] = []; break; default: target = mocked.mock; getLookup(mocked, 'setter')[setterProperty] = []; break; } if (Object.getOwnPropertyDescriptor(target, propertyName) == null) { Object.defineProperty(target, propertyName, { enumerable: true, get: propertyGetter, set: propertySetter, configurable: true, }); } return mocked; } export function trackConstructorCall>( mock: IMocked, params: LookupParams, ) { const lookup = getLookup(mock, 'constructorFunction'); trackCall(lookup, 'constructorFunction', params); } function trackFunctionCall< T, C extends ConstructorFunction, U extends FunctionTypes, K extends FunctionName, >(mock: IMocked, lookupType: U, functionName: K, params: LookupParams) { const lookup = getLookup(mock, lookupType); trackCall(lookup, functionName, params); } function trackGetterCall, U extends GetterTypes, K extends FunctionName>( mock: IMocked, lookupType: U, propertyName: K, ) { const lookup = getLookup(mock, lookupType); // it's easier to have an array of empty arrays than just keep track of a count // This allows us to use the same verification code as the setter and functions trackCall(lookup, propertyName, [] as LookupParams); } function trackSetterCall, U extends SetterTypes, K extends FunctionName>( mock: IMocked, lookupType: U, propertyName: K, param: LookupParams, ) { const lookup = getLookup(mock, lookupType); trackCall(lookup, propertyName, param); } function trackCall, U extends LookupType, K extends FunctionName>( lookup: FunctionCallLookup, name: K, params: LookupParams, ) { let functionCalls: LookupParams[] | undefined = lookup[name]; if (functionCalls === undefined) { functionCalls = [] as LookupParams[]; lookup[name] = functionCalls; } functionCalls.push(params); }