1886da342Sopenharmony_ci/*
2886da342Sopenharmony_ci * Copyright (c) 2023 Huawei Device Co., Ltd.
3886da342Sopenharmony_ci * Licensed under the Apache License, Version 2.0 (the "License");
4886da342Sopenharmony_ci * you may not use this file except in compliance with the License.
5886da342Sopenharmony_ci * You may obtain a copy of the License at
6886da342Sopenharmony_ci *
7886da342Sopenharmony_ci *     http://www.apache.org/licenses/LICENSE-2.0
8886da342Sopenharmony_ci *
9886da342Sopenharmony_ci * Unless required by applicable law or agreed to in writing, software
10886da342Sopenharmony_ci * distributed under the License is distributed on an "AS IS" BASIS,
11886da342Sopenharmony_ci * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12886da342Sopenharmony_ci * See the License for the specific language governing permissions and
13886da342Sopenharmony_ci * limitations under the License.
14886da342Sopenharmony_ci */
15886da342Sopenharmony_ci
16886da342Sopenharmony_ciimport ExtendInterface from "./ExtendInterface.js";
17886da342Sopenharmony_ciimport VerificationMode from "./VerificationMode.js";
18886da342Sopenharmony_ciimport { ArgumentMatchers } from "./ArgumentMatchers.js";
19886da342Sopenharmony_ci
20886da342Sopenharmony_ciinterface IFunction extends Function {
21886da342Sopenharmony_ci    container: any;
22886da342Sopenharmony_ci    original: any;
23886da342Sopenharmony_ci    propName: string;
24886da342Sopenharmony_ci    originalFromPrototype: boolean
25886da342Sopenharmony_ci    mocker: MockKit
26886da342Sopenharmony_ci}
27886da342Sopenharmony_ci
28886da342Sopenharmony_ciclass MockKit {
29886da342Sopenharmony_ci
30886da342Sopenharmony_ci    private mFunctions:Array<any> = [];
31886da342Sopenharmony_ci    private stubs = new Map();
32886da342Sopenharmony_ci    private recordCalls = new Map();
33886da342Sopenharmony_ci    private currentSetKey = new Map();
34886da342Sopenharmony_ci    private mockObj = null;
35886da342Sopenharmony_ci    private recordMockedMethod = new Map();
36886da342Sopenharmony_ci    private originalMethod: any;
37886da342Sopenharmony_ci
38886da342Sopenharmony_ci    constructor() {
39886da342Sopenharmony_ci        this.mFunctions = [];
40886da342Sopenharmony_ci        this.stubs = new Map();
41886da342Sopenharmony_ci        this.recordCalls = new Map();
42886da342Sopenharmony_ci        this.currentSetKey = new Map();
43886da342Sopenharmony_ci        this.mockObj = null;
44886da342Sopenharmony_ci        this.recordMockedMethod = new Map();
45886da342Sopenharmony_ci    }
46886da342Sopenharmony_ci
47886da342Sopenharmony_ci    init() {
48886da342Sopenharmony_ci        this.reset();
49886da342Sopenharmony_ci    }
50886da342Sopenharmony_ci
51886da342Sopenharmony_ci    reset() {
52886da342Sopenharmony_ci        this.mFunctions = [];
53886da342Sopenharmony_ci        this.stubs = new Map()
54886da342Sopenharmony_ci        this.recordCalls = new Map();
55886da342Sopenharmony_ci        this.currentSetKey = new Map();
56886da342Sopenharmony_ci        this.mockObj = null;
57886da342Sopenharmony_ci        this.recordMockedMethod = new Map();
58886da342Sopenharmony_ci    }
59886da342Sopenharmony_ci
60886da342Sopenharmony_ci    clearAll() {
61886da342Sopenharmony_ci        this.reset();
62886da342Sopenharmony_ci    }
63886da342Sopenharmony_ci
64886da342Sopenharmony_ci    clear(obj: any) {
65886da342Sopenharmony_ci        if (!obj) throw Error("Please enter an object to be cleaned");
66886da342Sopenharmony_ci        if (typeof (obj) != 'object') throw new Error('Not a object');
67886da342Sopenharmony_ci        this.recordMockedMethod.forEach(function (value, key, map) {
68886da342Sopenharmony_ci            if (key) {
69886da342Sopenharmony_ci                obj[key] = value;
70886da342Sopenharmony_ci            }
71886da342Sopenharmony_ci        });
72886da342Sopenharmony_ci    }
73886da342Sopenharmony_ci
74886da342Sopenharmony_ci    ignoreMock(obj:any, method: any) {
75886da342Sopenharmony_ci        if (typeof (obj) != 'object') throw new Error('Not a object');
76886da342Sopenharmony_ci        if (typeof (method) != 'function') throw new Error('Not a function');
77886da342Sopenharmony_ci        let og = this.recordMockedMethod.get(method.propName);
78886da342Sopenharmony_ci        if (og) {
79886da342Sopenharmony_ci            obj[method.propName] = og;
80886da342Sopenharmony_ci            this.recordMockedMethod.set(method.propName, undefined);
81886da342Sopenharmony_ci        }
82886da342Sopenharmony_ci    }
83886da342Sopenharmony_ci
84886da342Sopenharmony_ci    extend(dest: any, source:any) {
85886da342Sopenharmony_ci        dest["stub"] = source["stub"];
86886da342Sopenharmony_ci        dest["afterReturn"] = source["afterReturn"];
87886da342Sopenharmony_ci        dest["afterReturnNothing"] = source["afterReturnNothing"];
88886da342Sopenharmony_ci        dest["afterAction"] = source["afterAction"];
89886da342Sopenharmony_ci        dest["afterThrow"] = source["afterThrow"];
90886da342Sopenharmony_ci        dest["stubMockedCall"] = source["stubMockedCall"];
91886da342Sopenharmony_ci        dest["clear"] = source["clear"];
92886da342Sopenharmony_ci        return dest;
93886da342Sopenharmony_ci    }
94886da342Sopenharmony_ci
95886da342Sopenharmony_ci    stubApply(f: any, params:any, returnInfo:any) {
96886da342Sopenharmony_ci        let values = this.stubs.get(f);
97886da342Sopenharmony_ci        if (!values) {
98886da342Sopenharmony_ci            values = new Map();
99886da342Sopenharmony_ci        }
100886da342Sopenharmony_ci        let key = params[0];
101886da342Sopenharmony_ci        if (typeof key == "undefined") {
102886da342Sopenharmony_ci            key = "anonymous-mock-" + f.propName;
103886da342Sopenharmony_ci        }
104886da342Sopenharmony_ci        let matcher = new ArgumentMatchers();
105886da342Sopenharmony_ci        if (matcher.matcheStubKey(key)) {
106886da342Sopenharmony_ci            key = matcher.matcheStubKey(key);
107886da342Sopenharmony_ci            if (key) {
108886da342Sopenharmony_ci                this.currentSetKey.set(f, key);
109886da342Sopenharmony_ci            }
110886da342Sopenharmony_ci        }
111886da342Sopenharmony_ci        values.set(key, returnInfo);
112886da342Sopenharmony_ci        this.stubs.set(f, values);
113886da342Sopenharmony_ci    }
114886da342Sopenharmony_ci
115886da342Sopenharmony_ci    getReturnInfo(f: any, params:any) {
116886da342Sopenharmony_ci        let values = this.stubs.get(f);
117886da342Sopenharmony_ci        if (!values) {
118886da342Sopenharmony_ci            return undefined;
119886da342Sopenharmony_ci        }
120886da342Sopenharmony_ci        let retrunKet = params[0];
121886da342Sopenharmony_ci        if (typeof retrunKet == "undefined") {
122886da342Sopenharmony_ci            retrunKet = "anonymous-mock-" + f.propName;
123886da342Sopenharmony_ci        }
124886da342Sopenharmony_ci        let stubSetKey = this.currentSetKey.get(f);
125886da342Sopenharmony_ci
126886da342Sopenharmony_ci        if (stubSetKey && (typeof (retrunKet) != "undefined")) {
127886da342Sopenharmony_ci            retrunKet = stubSetKey;
128886da342Sopenharmony_ci        }
129886da342Sopenharmony_ci        let matcher = new ArgumentMatchers();
130886da342Sopenharmony_ci        if (matcher.matcheReturnKey(params[0], undefined, stubSetKey) && matcher.matcheReturnKey(params[0], undefined, stubSetKey) != stubSetKey) {
131886da342Sopenharmony_ci            retrunKet = params[0];
132886da342Sopenharmony_ci        }
133886da342Sopenharmony_ci
134886da342Sopenharmony_ci        values.forEach(function (value: any, key: any, map: any) {
135886da342Sopenharmony_ci            if (ArgumentMatchers.isRegExp(key) && matcher.matcheReturnKey(params[0], key)) {
136886da342Sopenharmony_ci                retrunKet = key;
137886da342Sopenharmony_ci            }
138886da342Sopenharmony_ci        });
139886da342Sopenharmony_ci
140886da342Sopenharmony_ci        return values.get(retrunKet);
141886da342Sopenharmony_ci    }
142886da342Sopenharmony_ci
143886da342Sopenharmony_ci    findName(obj: any, value: any) {
144886da342Sopenharmony_ci        let properties = this.findProperties(obj);
145886da342Sopenharmony_ci        let name = '';
146886da342Sopenharmony_ci        properties.filter((item:any) => (item !== 'caller' && item !== 'arguments')).forEach(
147886da342Sopenharmony_ci            function (va1:any, idx:any, array:any) {
148886da342Sopenharmony_ci                if (obj[va1] === value) {
149886da342Sopenharmony_ci                    name = va1;
150886da342Sopenharmony_ci                }
151886da342Sopenharmony_ci            }
152886da342Sopenharmony_ci        );
153886da342Sopenharmony_ci        return name;
154886da342Sopenharmony_ci    }
155886da342Sopenharmony_ci
156886da342Sopenharmony_ci    isFunctionFromPrototype(f: Function, container:Function, propName: string) {
157886da342Sopenharmony_ci        if (container.constructor != Object && container.constructor.prototype !== container) {
158886da342Sopenharmony_ci            return container.constructor.prototype[propName] === f;
159886da342Sopenharmony_ci        }
160886da342Sopenharmony_ci        return false;
161886da342Sopenharmony_ci    }
162886da342Sopenharmony_ci
163886da342Sopenharmony_ci    findProperties(obj: any, ...arg: Array<any>) {
164886da342Sopenharmony_ci        function getProperty(new_obj:any): Array<any> {
165886da342Sopenharmony_ci            if (new_obj.__proto__ === null) {
166886da342Sopenharmony_ci                return [];
167886da342Sopenharmony_ci            }
168886da342Sopenharmony_ci            let properties = Object.getOwnPropertyNames(new_obj);
169886da342Sopenharmony_ci            return [...properties, ...getProperty(new_obj.__proto__)];
170886da342Sopenharmony_ci        }
171886da342Sopenharmony_ci        return getProperty(obj);
172886da342Sopenharmony_ci    }
173886da342Sopenharmony_ci
174886da342Sopenharmony_ci    recordMethodCall(originalMethod: any, args: any) {
175886da342Sopenharmony_ci        originalMethod['getName'] = function () {
176886da342Sopenharmony_ci            return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
177886da342Sopenharmony_ci        }
178886da342Sopenharmony_ci        let name = originalMethod.getName();
179886da342Sopenharmony_ci        let arglistString = name + '(' + Array.from(args).toString() + ')';
180886da342Sopenharmony_ci        let records = this.recordCalls.get(arglistString);
181886da342Sopenharmony_ci        if (!records) {
182886da342Sopenharmony_ci            records = 0;
183886da342Sopenharmony_ci        }
184886da342Sopenharmony_ci        records++;
185886da342Sopenharmony_ci        this.recordCalls.set(arglistString, records);
186886da342Sopenharmony_ci    }
187886da342Sopenharmony_ci
188886da342Sopenharmony_ci    mockFunc(originalObject:any, originalMethod:any) {
189886da342Sopenharmony_ci        let tmp = this;
190886da342Sopenharmony_ci        this.originalMethod = originalMethod;
191886da342Sopenharmony_ci        const _this = this;
192886da342Sopenharmony_ci        let f:any  = function () {
193886da342Sopenharmony_ci            let args = arguments;
194886da342Sopenharmony_ci            let action = tmp.getReturnInfo(f, args);
195886da342Sopenharmony_ci            if (originalMethod) {
196886da342Sopenharmony_ci                tmp.recordMethodCall(originalMethod, args);
197886da342Sopenharmony_ci            }
198886da342Sopenharmony_ci            if (action) {
199886da342Sopenharmony_ci                return <IFunction> action.apply(_this, args);
200886da342Sopenharmony_ci            }
201886da342Sopenharmony_ci        };
202886da342Sopenharmony_ci
203886da342Sopenharmony_ci        f.container = null || originalObject;
204886da342Sopenharmony_ci        f.original = originalMethod || null;
205886da342Sopenharmony_ci
206886da342Sopenharmony_ci        if (originalObject && originalMethod) {
207886da342Sopenharmony_ci            if (typeof (originalMethod) != 'function') throw new Error('Not a function');
208886da342Sopenharmony_ci            var name = this.findName(originalObject, originalMethod);
209886da342Sopenharmony_ci            originalObject[name] = f;
210886da342Sopenharmony_ci            this.recordMockedMethod.set(name, originalMethod);
211886da342Sopenharmony_ci            f.propName = name;
212886da342Sopenharmony_ci            f.originalFromPrototype = this.isFunctionFromPrototype(f.original, originalObject, f.propName);
213886da342Sopenharmony_ci        }
214886da342Sopenharmony_ci        f.mocker = this;
215886da342Sopenharmony_ci        this.mFunctions.push(f);
216886da342Sopenharmony_ci        this.extend(f, new ExtendInterface(this));
217886da342Sopenharmony_ci        return f;
218886da342Sopenharmony_ci    }
219886da342Sopenharmony_ci
220886da342Sopenharmony_ci    verify(methodName:any, argsArray:any) {
221886da342Sopenharmony_ci        if (!methodName) {
222886da342Sopenharmony_ci            throw Error("not a function name");
223886da342Sopenharmony_ci        }
224886da342Sopenharmony_ci        let a = this.recordCalls.get(methodName + '(' + argsArray.toString() + ')');
225886da342Sopenharmony_ci        return new VerificationMode(a ? a : 0);
226886da342Sopenharmony_ci    }
227886da342Sopenharmony_ci
228886da342Sopenharmony_ci    mockObject(object: any) {
229886da342Sopenharmony_ci        if (!object || typeof object === "string") {
230886da342Sopenharmony_ci            throw Error(`this ${object} cannot be mocked`);
231886da342Sopenharmony_ci        }
232886da342Sopenharmony_ci        const _this = this;
233886da342Sopenharmony_ci        let mockedObject:any = {};
234886da342Sopenharmony_ci        let keys = Reflect.ownKeys(object);
235886da342Sopenharmony_ci        keys.filter(key => (typeof Reflect.get(object, key)) === 'function')
236886da342Sopenharmony_ci            .forEach((key:any) => {
237886da342Sopenharmony_ci                mockedObject[key] = object[key];
238886da342Sopenharmony_ci                mockedObject[key] = _this.mockFunc(mockedObject, mockedObject[key]);
239886da342Sopenharmony_ci            });
240886da342Sopenharmony_ci        return mockedObject;
241886da342Sopenharmony_ci    }
242886da342Sopenharmony_ci}
243886da342Sopenharmony_ci
244886da342Sopenharmony_cifunction ifMockedFunction(f: any) {
245886da342Sopenharmony_ci    if (Object.prototype.toString.call(f) != "[object Function]" &&
246886da342Sopenharmony_ci        Object.prototype.toString.call(f) != "[object AsyncFunction]") {
247886da342Sopenharmony_ci        throw Error("not a function");
248886da342Sopenharmony_ci    }
249886da342Sopenharmony_ci    if (!f.stub) {
250886da342Sopenharmony_ci        throw Error("not a mock function");
251886da342Sopenharmony_ci    }
252886da342Sopenharmony_ci    return true;
253886da342Sopenharmony_ci}
254886da342Sopenharmony_ci
255886da342Sopenharmony_cifunction when(f: any) {
256886da342Sopenharmony_ci    if (ifMockedFunction(f)) {
257886da342Sopenharmony_ci        return f.stub.bind(f);
258886da342Sopenharmony_ci    }
259886da342Sopenharmony_ci}
260886da342Sopenharmony_ci
261886da342Sopenharmony_cifunction MockSetup(target: Object, propertyName: string | Symbol, descriptor: TypedPropertyDescriptor<() => void>): void {
262886da342Sopenharmony_ci    const aboutToAppearOrigin = target.aboutToAppear;
263886da342Sopenharmony_ci    const setup = descriptor.value;
264886da342Sopenharmony_ci    target.aboutToAppear = function (...args: any[]) {
265886da342Sopenharmony_ci        if (target.__Param) { // copy attributes and params of the original context
266886da342Sopenharmony_ci            try {
267886da342Sopenharmony_ci                const map = target.__Param as Map<string, unknown>;
268886da342Sopenharmony_ci                for (const [key, val] of map) {
269886da342Sopenharmony_ci                    this[key] = val; // 'this' refers to context of current function
270886da342Sopenharmony_ci                }
271886da342Sopenharmony_ci            } catch (e) {
272886da342Sopenharmony_ci                throw new Error(`Mock setup param error: ${e}`);
273886da342Sopenharmony_ci            }
274886da342Sopenharmony_ci        }
275886da342Sopenharmony_ci
276886da342Sopenharmony_ci        if (setup) { // apply the mock content
277886da342Sopenharmony_ci            try {
278886da342Sopenharmony_ci                setup.apply(this);
279886da342Sopenharmony_ci            } catch (e) {
280886da342Sopenharmony_ci                throw new Error(`Mock setup apply error: ${e}`);
281886da342Sopenharmony_ci            }
282886da342Sopenharmony_ci        }
283886da342Sopenharmony_ci
284886da342Sopenharmony_ci        if (aboutToAppearOrigin) { // append to aboutToAppear function of the original context
285886da342Sopenharmony_ci            aboutToAppearOrigin.apply(this, args);
286886da342Sopenharmony_ci        }
287886da342Sopenharmony_ci    }
288886da342Sopenharmony_ci}
289886da342Sopenharmony_ci
290886da342Sopenharmony_ciexport {
291886da342Sopenharmony_ci    MockSetup,
292886da342Sopenharmony_ci    MockKit,
293886da342Sopenharmony_ci    when
294886da342Sopenharmony_ci};