1/* 2 * Copyright (c) 2022-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import ExtendInterface from './ExtendInterface'; 17import VerificationMode from './VerificationMode'; 18import ArgumentMatchers from './ArgumentMatchers'; 19 20class MockKit { 21 constructor() { 22 this.mFunctions = []; 23 this.stubs = new Map(); 24 this.recordCalls = new Map(); 25 this.currentSetKey = new Map(); 26 this.mockObj = null; 27 this.recordMockedMethod = new Map(); 28 } 29 30 init() { 31 this.reset(); 32 } 33 34 reset() { 35 this.mFunctions = []; 36 this.stubs = {}; 37 this.recordCalls = {}; 38 this.currentSetKey = new Map(); 39 this.mockObj = null; 40 this.recordMockedMethod = new Map(); 41 } 42 43 clearAll() { 44 this.reset(); 45 var props = Object.keys(this); 46 for (var i = 0; i < props.length; i++) { 47 delete this[props[i]]; 48 } 49 50 var props = Object.getOwnPropertyNames(this); 51 for (var i = 0; i < props.length; i++) { 52 delete this[props[i]]; 53 } 54 for (var key in this) { 55 delete this[key]; 56 } 57 } 58 59 clear(obj) { 60 if (!obj) { 61 throw Error('Please enter an object to be cleaned'); 62 } 63 if (typeof obj !== 'object' && typeof obj !== 'function') { 64 throw new Error('Not a object or static class'); 65 } 66 this.recordMockedMethod.forEach(function (value, key, map) { 67 if (key) { 68 obj[key] = value; 69 } 70 }); 71 } 72 73 ignoreMock(obj, method) { 74 if (typeof obj !== 'object' && typeof obj !== 'function') { 75 throw new Error('Not a object or static class'); 76 } 77 if (typeof method !== 'function') { 78 throw new Error('Not a function'); 79 } 80 let og = this.recordMockedMethod.get(method.propName); 81 if (og) { 82 obj[method.propName] = og; 83 this.recordMockedMethod.set(method.propName, undefined); 84 } 85 } 86 87 extend(dest, source) { 88 dest['stub'] = source['stub']; 89 dest['afterReturn'] = source['afterReturn']; 90 dest['afterReturnNothing'] = source['afterReturnNothing']; 91 dest['afterAction'] = source['afterAction']; 92 dest['afterThrow'] = source['afterThrow']; 93 dest['stubMockedCall'] = source['stubMockedCall']; 94 dest['clear'] = source['clear']; 95 return dest; 96 } 97 98 stubApply(f, params, returnInfo) { 99 let values = this.stubs.get(f); 100 if (!values) { 101 values = new Map(); 102 } 103 let key = params[0]; 104 if (typeof key === 'undefined') { 105 key = 'anonymous-mock-' + f.propName; 106 } else { 107 key = []; 108 const matcher = new ArgumentMatchers(); 109 for (let i = 0; i < params.length; i++) { 110 const param = params[i]; 111 const matchKey = matcher.matcheStubKey(param); 112 if (matchKey) { 113 key.push(matchKey); 114 } else { 115 key.push(param); 116 } 117 } 118 } 119 this.currentSetKey.set(f, key); 120 values.set(key, returnInfo); 121 this.stubs.set(f, values); 122 } 123 124 getReturnInfo(f, params) { 125 let values = this.stubs.get(f); 126 if (!values) { 127 return undefined; 128 } 129 let returnKet = params[0]; 130 const anonymousName = 'anonymous-mock-' + f.propName; 131 if (typeof returnKet === 'undefined') { 132 returnKet = anonymousName; 133 let stubSetKey = this.currentSetKey.get(f); 134 135 if (stubSetKey && (typeof (returnKet) !== 'undefined')) { 136 returnKet = stubSetKey; 137 } 138 } else { 139 returnKet = this.getReturnKet(values, params, returnKet, anonymousName); 140 } 141 142 return values.get(returnKet); 143 } 144 145 getReturnKet(values, params, defaultValue, anonymousName) { 146 let flag = true; 147 let returnKet = defaultValue; 148 values.forEach((value, paramsKey, map) => { 149 if ( 150 flag && 151 paramsKey !== anonymousName && 152 paramsKey.length === params.length && 153 this.checkIsRightValue(paramsKey, params) 154 ) { 155 returnKet = paramsKey; 156 flag = false; 157 } 158 }); 159 return returnKet; 160 } 161 162 checkIsRightValue(paramsKey, params) { 163 const matcher = new ArgumentMatchers(); 164 return paramsKey.every((key, j) => { 165 if (ArgumentMatchers.isRegExp(key) && typeof params[j] === 'string') { 166 return key.test(params[j]); 167 } 168 const matchKey = matcher.matcheReturnKey(params[j], undefined, key); 169 if (matchKey && matchKey === key) { 170 return true; 171 } else if (this.checkIsEqual(params[j], key)) { 172 return true; 173 } 174 return false; 175 }); 176 } 177 178 checkIsEqual(value1, value2) { 179 if (value1 === value2) { 180 return true; 181 } 182 if (typeof value1 !== typeof value2) { 183 return false; 184 } 185 if (Array.isArray(value1) && Array.isArray(value2)) { 186 if (value1.length !== value2.length) { 187 return false; 188 } 189 for (let i = 0; i < value1.length; i++) { 190 if (!this.checkIsEqual(value1[i], value2[i])) { 191 return false; 192 } 193 } 194 return true; 195 } 196 if (Object.prototype.toString.call(value1) === '[object Object]' && 197 Object.prototype.toString.call(value2) === '[object Object]') { 198 const keys1 = Object.keys(value1); 199 const keys2 = Object.keys(value2); 200 if (keys1.length !== keys2.length) { 201 return false; 202 } 203 for (let key of keys1) { 204 if (!keys2.includes(key) || !this.checkIsEqual(value1[key], value2[key])) { 205 return false; 206 } 207 } 208 return true; 209 } 210 if (Object.prototype.toString.call(value1) === '[object Date]' && 211 Object.prototype.toString.call(value2) === '[object Date]' && 212 value1.getTime() === value2.getTime()) { 213 return true; 214 } 215 if (Object.prototype.toString.call(value1) === '[object RegExp]' && 216 Object.prototype.toString.call(value2) === '[object RegExp]' && 217 value1.toString() === value2.toString()) { 218 return true; 219 } 220 return false; 221 } 222 223 findName(obj, value) { 224 let properties = this.findProperties(obj); 225 let name = null; 226 properties 227 .filter((item) => item !== 'caller' && item !== 'arguments') 228 .forEach(function (va1, idx, array) { 229 if (obj[va1] === value) { 230 name = va1; 231 } 232 }); 233 return name; 234 } 235 236 isFunctionFromPrototype(f, container, propName) { 237 if ( 238 container.constructor !== Object && 239 container.constructor.prototype !== container 240 ) { 241 return container.constructor.prototype[propName] === f; 242 } 243 return false; 244 } 245 246 findProperties(obj, ...arg) { 247 function getProperty(newObj) { 248 if (newObj.__proto__ === null) { 249 return []; 250 } 251 let properties = Object.getOwnPropertyNames(newObj); 252 return [...properties, ...getProperty(newObj.__proto__)]; 253 } 254 return getProperty(obj); 255 } 256 257 recordMethodCall(originalMethod, args) { 258 Function.prototype.getName = function () { 259 return this.name || this.toString().match(/function\s*([^(]*)\(/)[1]; 260 }; 261 let name = originalMethod.getName(); 262 let arglistString = name + '(' + Array.from(args).toString() + ')'; 263 let records = this.recordCalls.get(arglistString); 264 if (!records) { 265 records = 0; 266 } 267 records++; 268 this.recordCalls.set(arglistString, records); 269 } 270 271 mockFunc(originalObject, originalMethod) { 272 let tmp = this; 273 this.originalMethod = originalMethod; 274 let f = function () { 275 let args = arguments; 276 let action = tmp.getReturnInfo(f, args); 277 if (originalMethod) { 278 tmp.recordMethodCall(originalMethod, args); 279 } 280 if (action) { 281 return action.apply(this, args); 282 } 283 }; 284 285 f.container = null || originalObject; 286 f.original = originalMethod || null; 287 288 if (originalObject && originalMethod) { 289 if (typeof originalMethod !== 'function') { 290 throw new Error('Not a function'); 291 } 292 var name = this.findName(originalObject, originalMethod); 293 originalObject[name] = f; 294 this.recordMockedMethod.set(name, originalMethod); 295 f.propName = name; 296 f.originalFromPrototype = this.isFunctionFromPrototype( 297 f.original, 298 originalObject, 299 f.propName 300 ); 301 } 302 f.mocker = this; 303 this.mFunctions.push(f); 304 this.extend(f, new ExtendInterface(this)); 305 return f; 306 } 307 308 verify(methodName, argsArray) { 309 if (!methodName) { 310 throw Error('not a function name'); 311 } 312 let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')'); 313 return new VerificationMode(a ? a : 0); 314 } 315 316 mockObject(object) { 317 if (!object || typeof object === 'string') { 318 throw Error(`this ${object} cannot be mocked`); 319 } 320 const _this = this; 321 let mockedObject = {}; 322 let keys = Reflect.ownKeys(object); 323 keys 324 .filter((key) => typeof Reflect.get(object, key) === 'function') 325 .forEach((key) => { 326 mockedObject[key] = object[key]; 327 mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]); 328 }); 329 return mockedObject; 330 } 331} 332 333function ifMockedFunction(f) { 334 if ( 335 Object.prototype.toString.call(f) !== '[object Function]' && 336 Object.prototype.toString.call(f) !== '[object AsyncFunction]' 337 ) { 338 throw Error('not a function'); 339 } 340 if (!f.stub) { 341 throw Error('not a mock function'); 342 } 343 return true; 344} 345 346function when(f) { 347 if (!ifMockedFunction(f)) { 348 throw Error('not a mock function'); 349 } 350 return f.stub.bind(f); 351} 352 353export { MockKit, when }; 354