UNPKG

@applicvision/js-toolbox

Version:

A collection of tools for modern JavaScript development

100 lines (87 loc) 2.52 kB
class FunctionSpy extends Function { _calls = [] _constructs = [] _addCall(args) { this._calls.push(args) } _addConstruct(args) { this._constructs.push(args) } get calls() { return this._calls.length } getCall(index) { return this._calls.at(index) } get lastCall() { return this._calls.at(-1) } } export function spy(implementation = function emptyFunction() { }) { return new Proxy(new FunctionSpy(), { apply(target, thisArg, args) { target._addCall(args); return implementation.apply(thisArg, args); }, construct(target, args) { target._addConstruct(args); return new implementation(...args); } }); } /** * Intercept function calls in order to change behaviour or monitor call count. * @param {any} object The object to intercept methods of * @param {string[]} methods The methods to intercept * @param {(() => void)?} callHandler * @param {{times?: number, callsOriginal?: boolean}} options The number of times the interceptor is in place, before it clears itself. * Pass 0 in order not to clear interceptor autmatically. And whether the original methods are called. */ export function intercept(object, methods, callHandler = () => { }, options = {}) { const { times = 0, callsOriginal = true } = options const methodsToIntercept = [].concat(methods) const mock = { calls: {}, get callCount() { return Object.values(this.calls).reduce((sum, callCount) => sum + callCount, 0) }, get called() { return this.callCount > 0 }, restore() { interceptors.forEach(({ original, method }) => { object[method] = original }) } } const interceptors = methodsToIntercept.map(method => { const original = object[method] const replacement = (...args) => { mock.calls[method] = (mock.calls[method] ?? 0) + 1 let returnValue = callHandler(mock, method) if (callsOriginal) { returnValue = original.call(object, ...args) } if (times && mock.callCount >= times) { mock.restore() } return returnValue } replacement.mock = mock return { original, replacement, method } }) interceptors.forEach(({ replacement, method }) => { object[method] = replacement }) return mock } export async function spyOn(moduleSpecifier, exportedFunction, implementation) { const { default: spiedModule } = await import(moduleSpecifier); const original = spiedModule[exportedFunction]; spiedModule[exportedFunction] = spy(implementation); return () => { spiedModule[exportedFunction] = original; }; }