/* * Copyright (c) 2023 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import ExtendInterface from "./ExtendInterface.js"; import VerificationMode from "./VerificationMode.js"; import { ArgumentMatchers } from "./ArgumentMatchers.js"; interface IFunction extends Function { container: any; original: any; propName: string; originalFromPrototype: boolean mocker: MockKit } class MockKit { private mFunctions:Array = []; private stubs = new Map(); private recordCalls = new Map(); private currentSetKey = new Map(); private mockObj = null; private recordMockedMethod = new Map(); private originalMethod: any; constructor() { this.mFunctions = []; this.stubs = new Map(); this.recordCalls = new Map(); this.currentSetKey = new Map(); this.mockObj = null; this.recordMockedMethod = new Map(); } init() { this.reset(); } reset() { this.mFunctions = []; this.stubs = new Map() this.recordCalls = new Map(); this.currentSetKey = new Map(); this.mockObj = null; this.recordMockedMethod = new Map(); } clearAll() { this.reset(); } clear(obj: any) { if (!obj) throw Error("Please enter an object to be cleaned"); if (typeof (obj) != 'object') throw new Error('Not a object'); this.recordMockedMethod.forEach(function (value, key, map) { if (key) { obj[key] = value; } }); } ignoreMock(obj:any, method: any) { if (typeof (obj) != 'object') throw new Error('Not a object'); if (typeof (method) != 'function') throw new Error('Not a function'); let og = this.recordMockedMethod.get(method.propName); if (og) { obj[method.propName] = og; this.recordMockedMethod.set(method.propName, undefined); } } extend(dest: any, source:any) { dest["stub"] = source["stub"]; dest["afterReturn"] = source["afterReturn"]; dest["afterReturnNothing"] = source["afterReturnNothing"]; dest["afterAction"] = source["afterAction"]; dest["afterThrow"] = source["afterThrow"]; dest["stubMockedCall"] = source["stubMockedCall"]; dest["clear"] = source["clear"]; return dest; } stubApply(f: any, params:any, returnInfo:any) { let values = this.stubs.get(f); if (!values) { values = new Map(); } let key = params[0]; if (typeof key == "undefined") { key = "anonymous-mock-" + f.propName; } let matcher = new ArgumentMatchers(); if (matcher.matcheStubKey(key)) { key = matcher.matcheStubKey(key); if (key) { this.currentSetKey.set(f, key); } } values.set(key, returnInfo); this.stubs.set(f, values); } getReturnInfo(f: any, params:any) { let values = this.stubs.get(f); if (!values) { return undefined; } let retrunKet = params[0]; if (typeof retrunKet == "undefined") { retrunKet = "anonymous-mock-" + f.propName; } let stubSetKey = this.currentSetKey.get(f); if (stubSetKey && (typeof (retrunKet) != "undefined")) { retrunKet = stubSetKey; } let matcher = new ArgumentMatchers(); if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) != stubSetKey) { retrunKet = params[0]; } values.forEach(function (value: any, key: any, map: any) { if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) { retrunKet = key; } }); return values.get(retrunKet); } findName(obj: any, value: any) { let properties = this.findProperties(obj); let name = ''; properties.filter((item:any) => (item !== 'caller' && item !== 'arguments')).forEach( function (va1:any, idx:any, array:any) { if (obj[va1] === value) { name = va1; } } ); return name; } isFunctionFromPrototype(f: Function, container:Function, propName: string) { if (container.constructor != Object && container.constructor.prototype !== container) { return container.constructor.prototype[propName] === f; } return false; } findProperties(obj: any, ...arg: Array) { function getProperty(new_obj:any): Array { if (new_obj.__proto__ === null) { return []; } let properties = Object.getOwnPropertyNames(new_obj); return [...properties, ...getProperty(new_obj.__proto__)]; } return getProperty(obj); } recordMethodCall(originalMethod: any, args: any) { originalMethod['getName'] = function () { return this.name || this.toString().match(/function\s*([^(]*)\(/)[1]; } let name = originalMethod.getName(); let arglistString = name + '(' + Array.from(args).toString() + ')'; let records = this.recordCalls.get(arglistString); if (!records) { records = 0; } records++; this.recordCalls.set(arglistString, records); } mockFunc(originalObject:any, originalMethod:any) { let tmp = this; this.originalMethod = originalMethod; const _this = this; let f:any = function () { let args = arguments; let action = tmp.getReturnInfo(f, args); if (originalMethod) { tmp.recordMethodCall(originalMethod, args); } if (action) { return action.apply(_this, args); } }; f.container = null || originalObject; f.original = originalMethod || null; if (originalObject && originalMethod) { if (typeof (originalMethod) != 'function') throw new Error('Not a function'); var name = this.findName(originalObject, originalMethod); originalObject[name] = f; this.recordMockedMethod.set(name, originalMethod); f.propName = name; f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName); } f.mocker = this; this.mFunctions.push(f); this.extend(f, new ExtendInterface(this)); return f; } verify(methodName:any, argsArray:any) { if (!methodName) { throw Error("not a function name"); } let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')'); return new VerificationMode(a ? a : 0); } mockObject(object: any) { if (!object || typeof object === "string") { throw Error(`this ${object} cannot be mocked`); } const _this = this; let mockedObject:any = {}; let keys = Reflect.ownKeys(object); keys.filter(key => (typeof Reflect.get(object, key)) === 'function') .forEach((key:any) => { mockedObject[key] = object[key]; mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]); }); return mockedObject; } } function ifMockedFunction(f: any) { if (Object.prototype.toString.call(f) != "[object Function]" && Object.prototype.toString.call(f) != "[object AsyncFunction]") { throw Error("not a function"); } if (!f.stub) { throw Error("not a mock function"); } return true; } function when(f: any) { if (ifMockedFunction(f)) { return f.stub.bind(f); } } function MockSetup(target: Object, propertyName: string | Symbol, descriptor: TypedPropertyDescriptor<() => void>): void { const aboutToAppearOrigin = target.aboutToAppear; const setup = descriptor.value; target.aboutToAppear = function (...args: any[]) { if (target.__Param) { // copy attributes and params of the original context try { const map = target.__Param as Map; for (const [key, val] of map) { this[key] = val; // 'this' refers to context of current function } } catch (e) { throw new Error(`Mock setup param error: ${e}`); } } if (setup) { // apply the mock content try { setup.apply(this); } catch (e) { throw new Error(`Mock setup apply error: ${e}`); } } if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context aboutToAppearOrigin.apply(this, args); } } } export { MockSetup, MockKit, when };