@applicvision/js-toolbox
Version:
A collection of tools for modern JavaScript development
100 lines (87 loc) • 2.52 kB
JavaScript
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; };
}