UNPKG

terse-mock

Version:

Easy mock creation and automocking

949 lines 35.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TM_ANY = void 0; exports.tunmock = tunmock; exports.tinfo = tinfo; exports.treset = treset; exports.tglobalopt = tglobalopt; exports.tlocalopt = tlocalopt; exports.tmock = tmock; exports.tstub = tstub; exports.tset = tset; // TODO: split into files const PATHBUILDER = Symbol('PATHBUILDER'); const MOCK_PROXY_TO_OBJECT = Symbol('MOCK_PROXY_TO_OBJECT'); const RESET_MOCK_PROXY = Symbol('RESET_MOCK_PROXY'); const SET_MOCK_PROXY_RETURN_VALUES = Symbol('SET_MOCK_PROXY_RETURN_VALUES'); const EXTERNAL_MOCK = Symbol('EXTERNAL_MOCK'); const ARGS_TO_RETURN_VALUES = Symbol('ARGS_TO_RETURN_VALUES'); const IS_A_MOCK_PROXY = Symbol('IS_A_MOCK_PROXY'); const IS_A_SPY = Symbol('IS_A_SPY'); const IS_A_STUB = Symbol('IS_A_STUB'); const ORIGINAL_FUNCTION = Symbol('ORIGINAL_FUNCTION'); const IS_A_MOCKED_FUNCTION = Symbol('IS_A_MOCKED_FUNCTION'); exports.TM_ANY = Symbol('TM_ANY'); const ANY_ARGS = argsToString([exports.TM_ANY]); function getOrAddObjPropVal(obj, prop, val = {}) { if (!obj.hasOwnProperty(prop)) { obj[prop] = val; } return obj[prop]; } function functionToString(data) { // if (simplify) { // Stringified functions are always simplified now const found = /(\S*)\s*\(/.exec(String(data)); if (!found || found[1] === 'function') { return '<function>'; } const name = found[1]; let text = '<arrow function>'; if (name) { if (globalOptions.exposeFunctionNames) { text = '<function ' + name + '>'; } else { text = '<function>'; } } return text; // } // return String(data); } function toString(data, options) { if (isArray(data)) { const dataStr = '[' + data.map((item) => toString(item, options)).join(', ') + ']'; return collapseIfNeeded(dataStr, '[...]'); } if (isObject(data)) { const props = Object.keys(data); const dataStr = '{' + props.map((key) => key + ': ' + toString(data[key])).join(', ') + '}'; return collapseIfNeeded(dataStr, '{...}'); } if (isFunction(data)) { if (isMockProxy(data)) { const pathBuilder = data[PATHBUILDER]; return collapseIfNeeded(pathBuilder.pathToBeShown, '<...>'); } return functionToString(data); } if (isString(data)) { return globalOptions.quoteSymbol + data + globalOptions.quoteSymbol; } return String(data); function collapseIfNeeded(dataStr, collapsedData) { if (!options) { return dataStr; } if (options.collapseLongValues && dataStr.length > options.collapseThreshold) { return collapsedData; } return dataStr; } } function isString(data) { return typeof data === 'string'; } function isFunction(data) { return typeof data === 'function'; } function isScalar(data) { return !isObjectOrArrayOrClass(data) && !isFunction(data); } function isObject(data) { return isObjectOrClass(data) && (!data.constructor || data.constructor.name === 'Object'); // !data.constructor handles Object.create(null) } function isInstanceofObject(data) { // jest.fn() instances also fall here. return data instanceof Object; } function isArray(data) { return Array.isArray(data); } function isMockProxy(data) { return data && data[IS_A_MOCK_PROXY] === true; } function isStub(data) { return data && data[IS_A_STUB] === true; } function isSpy(data) { return data && hasSymbol(data, IS_A_SPY); } function isMockedFunction(data) { return data && hasSymbol(data, IS_A_MOCKED_FUNCTION); } function isInitCouple(data) { return data.length === 2 && isFunction(data[0]); } function isObjectOrClass(data) { return isObjectOrArrayOrClass(data) && !isArray(data); } function isObjectOrArrayOrClass(data) { return typeof data === 'object' && data !== null; } function hasSymbol(obj, sym) { return Object.getOwnPropertySymbols(obj).includes(sym); } function argsToString(args, options) { return '(' + args.map((arg) => toString(arg, options)).join(', ') + ')'; } function deepClone(source) { if (isObject(source)) { return shallowMergeFromTo(source, {}, (val) => deepClone(val)); } // if (isArray(source)) { // Is not required for now. Commented for 100% coverage. // return source.map((item) => deepClone(item)); // } return source; } function deepCloneAndSpyify(_source, _pathBuilder, _externalMock, _thisArg = null) { const visitedObjects = []; return deepCloneAndSpyifyInternal(_source, _pathBuilder, _externalMock, _thisArg); function deepCloneAndSpyifyInternal(source, pathBuilder, externalMock, thisArg) { if (isMockProxy(source)) { return source; } if (isObject(source)) { if (visitedObjects.includes(source)) { return source; } visitedObjects.push(source); return shallowMergeFromTo(source, {}, (val, prop) => deepCloneAndSpyifyInternal(val, pathBuilder.withProp(prop), externalMock, source)); } if (isArray(source)) { return source.map((item, index) => deepCloneAndSpyifyInternal(item, pathBuilder.withProp(index), externalMock, source)); } if (isFunction(source)) { if (!isInstanceofObject(source)) { // Special case for entities that has typeof === 'function' but that are not instanceof Object. // jest.fn() instances fall here, we make this check to return jest.fn() instance as is, // otherwise we may end up with problems like incorrect results of jest matchers. return createSpy(pathBuilder, source, externalMock); } const result = createSpy(pathBuilder, source.bind(thisArg), externalMock); // TODO: test this binding shallowMergeFromTo(source, result, (val) => deepCloneAndSpyifyInternal(val, pathBuilder, externalMock, source)); return result; } return source; } } function shallowMergeFromTo(from, to, mapper = (val) => val, except = []) { for (const prop in from) { copyPropOrSymbol(prop); } for (const symbol of Object.getOwnPropertySymbols(from)) { copyPropOrSymbol(symbol); } return to; function copyPropOrSymbol(propOrSymbol) { if (!except.includes(propOrSymbol)) { const descriptor = Object.getOwnPropertyDescriptor(from, propOrSymbol); if (descriptor.value) { descriptor.value = mapper(descriptor.value, propOrSymbol); } Object.defineProperty(to, propOrSymbol, descriptor); } } } const defaultProxyTarget = new Function(); function createMockFunction(_thisArg, defaultReturnSetter = () => undefined) { const context = { argsToReturnValues: {}, }; const mockFunction = function (...args) { const keyFromArgs = argsToString(args); return context.argsToReturnValues.hasOwnProperty(keyFromArgs) ? context.argsToReturnValues[keyFromArgs] : defaultReturnSetter(context); }; mockFunction[IS_A_MOCKED_FUNCTION] = true; mockFunction[ARGS_TO_RETURN_VALUES] = context.argsToReturnValues; return mockFunction; } function createSpy(pathBuilder, originalFunction, externalMock) { const spy = function () { const args = [...arguments]; const unmockedArgs = tunmock(args); totalCallLog.push(new CallInfo(null, args, pathBuilder.withCall(args))); if (externalMock) { spy[EXTERNAL_MOCK](...unmockedArgs); } return originalFunction(...unmockedArgs); }; shallowMergeFromTo(originalFunction, spy); if (externalMock) { spy[EXTERNAL_MOCK] = externalMock.create(); } spy[PATHBUILDER] = pathBuilder; spy[IS_A_SPY] = true; spy[ORIGINAL_FUNCTION] = originalFunction; return spy; } // TODO: implement mock times. class CallInfo { constructor(thisArg, args, pathBuilder) { this.this_ = thisArg; this.args_ = args; this.pathBuilder_ = pathBuilder; } get this() { return this.this_; } get args() { return this.args_; } get pathBuilder() { return this.pathBuilder_; } } class Undefinable { constructor(value) { this.hasValue_ = false; if (arguments.length > 0) { this.setValue(value); } } setValue(value) { this.value_ = value; this.hasValue_ = true; } hasValue() { return this.hasValue_; } getValue() { return this.value_; } } class PathBuilder { constructor(groupId, pathToBeShown, options) { this.groupId_ = ''; this.path_ = ''; this.latestPathChunk_ = ''; this.pathToBeShown_ = ''; this.groupId_ = groupId; this.pathToBeShown_ = pathToBeShown; this.options_ = options; } withCall(args) { var _a; const latestPathChunk = argsToString(args); const pathToBeShownChunk = !((_a = this.options_) === null || _a === void 0 ? void 0 : _a.collapseLongValues) ? latestPathChunk : argsToString(args, this.options_); const newPathBuilder = new PathBuilder(this.groupId_, this.pathToBeShown_ + pathToBeShownChunk, this.options_); newPathBuilder.path_ = this.path_ + latestPathChunk; newPathBuilder.latestPathChunk_ = latestPathChunk; newPathBuilder.parentPathBuilder_ = this; return newPathBuilder; } withProp(prop) { const latestPathChunkStringified = prop.toString(); const latestPathChunkWithSeparators = typeof prop === 'symbol' ? '[' + latestPathChunkStringified + ']' : '.' + latestPathChunkStringified; const newPathBuilder = new PathBuilder(this.groupId_, this.pathToBeShown_ ? this.pathToBeShown_ + latestPathChunkWithSeparators : latestPathChunkStringified, this.options_); newPathBuilder.path_ = this.path_ + latestPathChunkWithSeparators; newPathBuilder.latestPathChunk_ = prop; newPathBuilder.parentPathBuilder_ = this; return newPathBuilder; } get groupId() { return this.groupId_; } get path() { return this.path_; } get pathToBeShown() { return this.pathToBeShown_; } get latestPathChunk() { return this.latestPathChunk_; } get parentPathBuilder() { return this.parentPathBuilder_; } get options() { return this.options_; } } class MockTreeNode { constructor(pathBuilder) { this.propsToChildPaths_ = {}; this.argsToChildPaths_ = {}; this.isFinal_ = false; this.isTemp_ = false; this.calls_ = []; this.value_ = new Undefinable(); this.pathBuilder_ = pathBuilder; } static fromUserNodeAndSutNode(userNode, sutNode) { if (sutNode.isFinal()) { return sutNode; } if (userNode.isFinal()) { return userNode; } const node = new MockTreeNode(userNode.pathBuilder_); node.propsToChildPaths_ = Object.assign(Object.assign({}, userNode.propsToChildPaths_), sutNode.propsToChildPaths_); node.argsToChildPaths_ = Object.assign(Object.assign({}, userNode.argsToChildPaths_), sutNode.argsToChildPaths_); node.calls_ = [...userNode.calls_, ...sutNode.calls_]; if (userNode.isTemp_ && sutNode.isTemp_) { node.value_ = sutNode.value_; node.isTemp_ = true; } return node; } linkChildViaArgs(callInfo, pathBuilder) { this.calls_.push(callInfo); this.argsToChildPaths_[pathBuilder.latestPathChunk] = pathBuilder.path; } getArgsToChildPaths() { return this.argsToChildPaths_; } linkChildViaProp(pathBuilder) { this.propsToChildPaths_[pathBuilder.latestPathChunk] = pathBuilder.path; } getPropsToChildPaths() { return this.propsToChildPaths_; } hasCalls() { return this.calls_.length > 0; } applyCallsToFuntion(f) { this.calls_.forEach((callInfo) => f.apply(callInfo.this, tunmock(callInfo.args))); } hasValue() { return this.value_.hasValue(); } getValue() { return this.value_.getValue(); } toMockFunction() { return createMockFunction({}); } isFinal() { return this.isFinal_; } isTemp() { return this.isTemp_; } } class TreeNodeTemp extends MockTreeNode { constructor(pathBuilder) { super(pathBuilder); this.isTemp_ = true; this.value_.setValue(pathBuilder.pathToBeShown); } } class TreeNodeFinal extends MockTreeNode { constructor(pathBuilder, value) { super(pathBuilder); this.isFinal_ = true; this.value_.setValue(value); } } class MockTree { constructor() { // TODO: describe. this.tree = {}; } static fromUserTreeAndSutTree(userTree, sutTree) { const result = new MockTree(); for (const prop in userTree.tree) { const userNode = userTree.tree[prop]; const sutNode = sutTree.tree[prop]; result.tree[prop] = sutNode ? MockTreeNode.fromUserNodeAndSutNode(userNode, sutNode) : userNode; } for (const prop in sutTree.tree) { if (!result.tree[prop]) { result.tree[prop] = sutTree.tree[prop]; } } return result; } getNode(path) { return this.tree[path]; } setNode(path, node) { // this.deleteSubtree(path); // Uncomment to remove dead nodes and significantly slow down the process. this.tree[path] = node; return node; } isNodeNotExistOrIsTempNode(path) { const existingNode = this.getNode(path); return !existingNode || existingNode.isTemp(); } getOrAddOrReplaceTemp(path, node) { if (this.isNodeNotExistOrIsTempNode(path)) { return this.setNode(path, node); } return this.getNode(path); } addChildIfNotExistOrReplaceTemp(childNode, childNodePathBuilder, callInfo) { // If parent node is temp node then replace it with a new node that doesn't have a value. // Only leaf nodes can have values. const parentNode = this.getOrAddOrReplaceTemp(childNodePathBuilder.parentPathBuilder.path, new MockTreeNode(childNodePathBuilder.parentPathBuilder)); const childNodePath = childNodePathBuilder.path; if (this.isNodeNotExistOrIsTempNode(childNodePath)) { this.setNode(childNodePath, childNode); if (callInfo) { parentNode.linkChildViaArgs(callInfo, childNodePathBuilder); } else { parentNode.linkChildViaProp(childNodePathBuilder); } } } addChild(childNode, childNodePathBuilder) { const parentNode = this.getOrAddOrReplaceTemp(childNodePathBuilder.parentPathBuilder.path, new MockTreeNode(childNodePathBuilder.parentPathBuilder)); this.setNode(childNodePathBuilder.path, childNode); parentNode.linkChildViaProp(childNodePathBuilder); } deleteSubtree(path) { if (!path) { this.tree = {}; return; } for (const key in this.tree) { if (key.length > path.length && (key.startsWith(path + '.') || key.startsWith(path + '('))) { // By construction: if node index starts with path. or path( then the node is in subtree of node with index = path. delete this.tree[key]; } } delete this.tree[path]; } forEachSubtreeNode(path, action) { for (const key in this.tree) { if (key.length > path.length && (key.startsWith(path + '.') || key.startsWith(path + '('))) { // By construction: if node index starts with path. or path( then the node is in subtree of node with index = path. action(this.tree[key]); } } if (this.tree[path]) { action(this.tree[path]); } } // Convert tree to hierarchy of objects and functions based on node paths and node values. toObject(pathBuilder) { const options = pathBuilder.options; const tree = this.tree; return toObjectInternal(pathBuilder.path); function toObjectInternal(path) { const node = tree[path]; if (node.hasValue()) { let value = node.getValue(); if (node.isTemp()) { value = options.autoValuesPrefix + (value || '<mock>'); } return new Undefinable(value); } const collectValuesFromChildren = (res, propsOrArgsToChildPaths) => shallowMergeFromTo(propsOrArgsToChildPaths, res, (val) => toObjectInternal(val).getValue()); let result = {}; if (node.hasCalls()) { // node represents something collable. result = node.toMockFunction(); collectValuesFromChildren(result[ARGS_TO_RETURN_VALUES], node.getArgsToChildPaths()); } collectValuesFromChildren(result, node.getPropsToChildPaths()); return new Undefinable(result); } } } let totalCallLog = []; let totalMocksCounter = 0; const userMockTrees = {}; const sutMockTrees = {}; // This function removes all tm proxies from data. function tunmock(data) { const visitedObjects = []; return tunmockInternal(data); function tunmockInternal(dataInternal) { if (!isInstanceofObject(dataInternal)) { return dataInternal; } if (isObjectOrClass(dataInternal)) { if (visitedObjects.includes(dataInternal)) { return dataInternal; } visitedObjects.push(dataInternal); } if (isMockProxy(dataInternal)) { return tunmockInternal(dataInternal[MOCK_PROXY_TO_OBJECT]); } if (isArray(dataInternal)) { return dataInternal.map((item) => tunmockInternal(item)); } if (isSpy(dataInternal)) { const originalFunction = dataInternal[ORIGINAL_FUNCTION]; shallowMergeFromTo(dataInternal, originalFunction, (val) => val, [IS_A_SPY, ORIGINAL_FUNCTION]); return originalFunction; } const result = isFunction(dataInternal) ? dataInternal : {}; shallowMergeFromTo(dataInternal, result, (val) => tunmockInternal(val)); return result; } } function tinfo(mockOrSpy, pathInsideMock) { if (pathInsideMock && !isMockProxy(mockOrSpy)) { throw new Error('tinfo: pathInsideMock is allowed only for mocks'); } if (!mockOrSpy) { return { externalMock: undefined, calls: totalCallLog.map((call) => tunmock(call.args)), callLog: totalCallLog.map((call) => call.pathBuilder.pathToBeShown), }; } let pathBuilder = mockOrSpy[PATHBUILDER]; let externalMock; if (isMockProxy(mockOrSpy)) { if (pathInsideMock) { const proxyContext = { pathBuilder: pathBuilder, }; const proxy = getTinfoProxy(proxyContext); pathInsideMock(proxy); pathBuilder = proxyContext.pathBuilder; } externalMock = getExternalMockWithCallsApplied(pathBuilder); } else if (isSpy(mockOrSpy)) { externalMock = mockOrSpy[EXTERNAL_MOCK]; } else { throw new Error('tinfo: argument should be either mock or spy'); } return { externalMock: externalMock, calls: fillterByDirectCallsOrPath(pathBuilder, true).map((call) => tunmock(call.args)), callLog: fillterByDirectCallsOrPath(pathBuilder).map((call) => call.pathBuilder.pathToBeShown), }; function getTinfoProxy(proxyContext) { const proxy = new Proxy(defaultProxyTarget, { get(_target, prop) { proxyContext.pathBuilder = proxyContext.pathBuilder.withProp(prop); return proxy; }, apply(_target, _thisArg, args) { proxyContext.pathBuilder = proxyContext.pathBuilder.withCall(args); return proxy; }, }); return proxy; } function fillterByDirectCallsOrPath(pathBuilderInternal, filterByDirectCallsOnly = false) { const calls = totalCallLog.filter((call) => call.pathBuilder.groupId === pathBuilderInternal.groupId && call.pathBuilder.parentPathBuilder.path === pathBuilderInternal.path); if (calls.length > 0 || filterByDirectCallsOnly) { return calls; } // Return everything that starts with path return totalCallLog.filter((call) => call.pathBuilder.groupId === pathBuilderInternal.groupId && call.pathBuilder.path.startsWith(pathBuilderInternal.path)); } function getExternalMockWithCallsApplied(pathBuilderInternal) { const options = pathBuilderInternal.options; if (!options.externalMock) { return undefined; } const userMockTree = userMockTrees[pathBuilderInternal.groupId]; const sutMockTree = sutMockTrees[pathBuilderInternal.groupId]; const combinedTree = MockTree.fromUserTreeAndSutTree(userMockTree, sutMockTree); const node = combinedTree.getNode(pathBuilderInternal.path); if (!node) { return undefined; } if (node.hasValue()) { const nodeValue = node.getValue(); if (isSpy(nodeValue)) { return nodeValue[EXTERNAL_MOCK]; } } const externalMock = options.externalMock.create(); node.applyCallsToFuntion(externalMock); return externalMock; } } function treset(mock) { if (mock) { mock(RESET_MOCK_PROXY); return; } Object.values(sutMockTrees).forEach((mockTree) => mockTree.deleteSubtree()); totalCallLog = []; localOptions = {}; } function applyCouplesToStub(stub, initCouples) { let returnValue = stub; initCouples.forEach(initCouple => { const proxyContext = { stubRef: returnValue, prevStubRef: returnValue, prevProp: '', stubReplacement: undefined, pathBuilder: new PathBuilder('', ''), }; const initializationProxy = getStubInitializationProxy(proxyContext); // Traverse initialization expression. initCouple[0](initializationProxy); if (proxyContext.prevProp) { proxyContext.prevStubRef[proxyContext.prevProp] = initCouple[1]; } else { returnValue = initCouple[1]; } if (proxyContext.stubReplacement) { returnValue = proxyContext.stubReplacement; } }); return returnValue; } let globalOptions = { automock: true, collapseLongValues: true, // Should this be done by default? collapseThreshold: 40, defaultMockName: '<mock>', quoteSymbol: '\'', exposeFunctionNames: false, autoValuesPrefix: '', }; let localOptions = {}; function tglobalopt(options) { if (options) { globalOptions = Object.assign(Object.assign({}, globalOptions), deepClone(options)); } return globalOptions; } function tlocalopt(options) { if (options) { localOptions = Object.assign(Object.assign({}, localOptions), deepClone(options)); } return localOptions; } function getStubInitializationProxy(proxyContext) { const initializationProxy = new Proxy(defaultProxyTarget, { get(_target, prop) { proxyContext.pathBuilder = proxyContext.pathBuilder.withProp(prop); let stubRef = proxyContext.stubRef; if (isScalar(stubRef)) { // isScalar(stubRef) is deprecated ? stubRef = replaceStub({}); } if (isScalar(stubRef[prop])) { stubRef[prop] = {}; } proxyContext.prevStubRef = stubRef; proxyContext.prevProp = prop; proxyContext.stubRef = stubRef[prop]; return initializationProxy; }, apply(_target, thisArg, args) { proxyContext.pathBuilder = proxyContext.pathBuilder.withCall(args); const key = proxyContext.pathBuilder.latestPathChunk; let stubRef = proxyContext.stubRef; if (!isMockedFunction(stubRef)) { const stubFunc = createMockFunction(thisArg, (context) => context.argsToReturnValues[ANY_ARGS]); shallowMergeFromTo(stubRef, stubFunc); stubRef = replaceStub(stubFunc); } stubRef = stubRef[ARGS_TO_RETURN_VALUES]; proxyContext.prevStubRef = stubRef; proxyContext.prevProp = key; proxyContext.stubRef = getOrAddObjPropVal(stubRef, key); return initializationProxy; }, }); return initializationProxy; function replaceStub(newStub) { if (proxyContext.prevProp) { proxyContext.prevStubRef[String(proxyContext.prevProp)] = newStub; } else { proxyContext.stubReplacement = newStub; } return newStub; } } function getMockInitializationProxy(proxyContext) { const proxy = new Proxy(defaultProxyTarget, { get(_target, prop) { proxyContext.pathBuilder = proxyContext.pathBuilder.withProp(prop); proxyContext.tree.addChildIfNotExistOrReplaceTemp(new MockTreeNode(proxyContext.pathBuilder), proxyContext.pathBuilder); return pickProxy(); }, apply(_target, thisArg, args) { proxyContext.pathBuilder = proxyContext.pathBuilder.withCall(args); proxyContext.tree.addChildIfNotExistOrReplaceTemp(new MockTreeNode(proxyContext.pathBuilder), proxyContext.pathBuilder, new CallInfo(thisArg, args, proxyContext.pathBuilder)); return pickProxy(); }, }); return proxy; function pickProxy() { const node = proxyContext.tree.getNode(proxyContext.pathBuilder.path); if (node.isFinal()) { const value = node.getValue(); const stubProxyContext = { stubRef: value, prevStubRef: value, prevProp: '', stubReplacement: undefined, pathBuilder: new PathBuilder('', ''), }; proxyContext.stubProxyContext = stubProxyContext; return getStubInitializationProxy(stubProxyContext); } return proxy; } } function applyCouplesToMock(initCouples, pathBuilder) { const userMockTree = userMockTrees[pathBuilder.groupId]; initCouples.forEach(initCouple => { const proxyContext = { tree: userMockTree, pathBuilder: pathBuilder, }; const initializationProxy = getMockInitializationProxy(proxyContext); // Traverse initialization expression. initCouple[0](initializationProxy); const stubProxyContext = proxyContext.stubProxyContext; const path = proxyContext.pathBuilder.path; if (pathBuilder.path === path) { throw new Error('Mocking at root level is not allowed'); } const val = deepCloneAndSpyify(initCouple[1], proxyContext.pathBuilder, pathBuilder.options.externalMock); if (!stubProxyContext) { userMockTree.setNode(path, new TreeNodeFinal(proxyContext.pathBuilder, val)); } else { if (stubProxyContext.stubReplacement) { userMockTree.setNode(path, new TreeNodeFinal(proxyContext.pathBuilder, stubProxyContext.stubReplacement)); } else if (!stubProxyContext.prevProp) { userMockTree.setNode(path, new TreeNodeFinal(proxyContext.pathBuilder, val)); } if (stubProxyContext.prevProp) { stubProxyContext.prevStubRef[stubProxyContext.prevProp] = val; } } }); } function getFinalNode(tree, pathBuilder, callInfo) { const node = tree.getNode(pathBuilder.path); if (node === null || node === void 0 ? void 0 : node.isFinal()) { return node; } if (callInfo) { const pathBuilderAny = pathBuilder.parentPathBuilder.withCall([exports.TM_ANY]); const node = tree.getNode(pathBuilderAny.path); if (node === null || node === void 0 ? void 0 : node.isFinal()) { return node; } } } // TODO: use options from path builder? function traversePropOrCall(pathBuilder, callInfo) { if (callInfo) { totalCallLog.push(callInfo); } const sutMockTree = sutMockTrees[pathBuilder.groupId]; const nodeSetInSut = getFinalNode(sutMockTree, pathBuilder, callInfo); if (nodeSetInSut) { return nodeSetInSut.getValue(); } const userMockTree = userMockTrees[pathBuilder.groupId]; const nodeSetByUser = getFinalNode(userMockTree, pathBuilder, callInfo); if (nodeSetByUser) { return nodeSetByUser.getValue(); } if (!pathBuilder.options.automock) { // Stop proxing if automock is false and propertry has not been explicitly mocked. return undefined; } sutMockTree.addChildIfNotExistOrReplaceTemp(new TreeNodeTemp(pathBuilder), pathBuilder, callInfo); return getSutProxy(pathBuilder); } function getSutProxy(pathBuilder) { const userMockTree = userMockTrees[pathBuilder.groupId]; const sutMockTree = sutMockTrees[pathBuilder.groupId]; const path = pathBuilder.path; const sutProxy = new Proxy(defaultProxyTarget, { get(_target, prop) { // Handle special properties. switch (prop) { case IS_A_MOCK_PROXY: return true; case PATHBUILDER: return pathBuilder; case MOCK_PROXY_TO_OBJECT: return MockTree .fromUserTreeAndSutTree(userMockTree, sutMockTree) .toObject(pathBuilder) .getValue(); case 'hasOwnProperty': return () => true; case Symbol.toPrimitive: return () => pathBuilder.pathToBeShown; // Prevent from 'TypeError: Cannot convert object to primitive value'. } return traversePropOrCall(pathBuilder.withProp(prop)); }, set(_target, prop, value) { const newPathBuilder = pathBuilder.withProp(prop); sutMockTree.addChild(new TreeNodeFinal(newPathBuilder, value), newPathBuilder); return true; }, apply(_target, thisArg, args) { if (args.length > 0) { const firstArg = args[0]; // Handle special arguments. if (firstArg === SET_MOCK_PROXY_RETURN_VALUES) { const initCoulpes = args[1]; applyCouplesToMock(initCoulpes, pathBuilder); return sutProxy; } else if (firstArg === RESET_MOCK_PROXY) { if (sutMockTree.getNode(path)) { sutMockTree.deleteSubtree(path); sutMockTree.setNode(path, new TreeNodeTemp(pathBuilder)); // Reset nested mocks. userMockTree.forEachSubtreeNode(path, (node) => { const nodeValue = node.getValue(); if (isMockProxy(nodeValue)) { treset(nodeValue); } }); } totalCallLog = totalCallLog .filter((call) => call.pathBuilder.groupId !== pathBuilder.groupId || !call.pathBuilder.path.startsWith(path)); return undefined; } } const pathBuilderWithCall = pathBuilder.withCall(args); return traversePropOrCall(pathBuilderWithCall, new CallInfo(thisArg, args, pathBuilderWithCall)); }, }); return sutProxy; } function initializersToArrayOfCouples(initializerArg) { let initializers; if (!isArray(initializerArg)) { initializers = [initializerArg]; } else if (isInitCouple(initializerArg)) { initializers = [initializerArg]; } else { initializers = initializerArg; } const initCouples = initializers.filter(initializer => isInitCouple(initializer)); const initObjects = initializers.filter(initializer => isObject(initializer)); for (const initObject of initObjects) { // Make init couples from init object. for (const [prop, val] of Object.entries(initObject)) { initCouples.unshift([(p) => p[prop], val]); } } return initCouples; } function parseTmockArgs(nameOrInitializersArg, initializersArg) { let name; let initializers; if (nameOrInitializersArg !== undefined) { if (isString(nameOrInitializersArg)) { name = nameOrInitializersArg; } else { initializers = initializersToArrayOfCouples(nameOrInitializersArg); } } if (initializersArg) { if (initializers) { throw new Error('tmock: multiple initializer arguments not allowed'); } initializers = initializersToArrayOfCouples(initializersArg); } if (!initializers) { initializers = []; } return { name, initializers }; } function tmock(nameOrInitializerArg, initializerArg) { const parsedArgs = parseTmockArgs(nameOrInitializerArg, initializerArg); const options = Object.assign(Object.assign({}, globalOptions), localOptions); if (parsedArgs.name !== undefined) { options.defaultMockName = parsedArgs.name; } const mockId = (totalMocksCounter++).toString(); const pathBuilder = new PathBuilder(mockId, options.defaultMockName, options); const userMockTree = new MockTree(); userMockTree.setNode('', new TreeNodeTemp(pathBuilder)); const sutMockTree = new MockTree(); sutMockTree.setNode('', new TreeNodeTemp(pathBuilder)); userMockTrees[mockId] = userMockTree; sutMockTrees[mockId] = sutMockTree; const sutMockProxy = getSutProxy(pathBuilder); if (parsedArgs.initializers.length) { tset(sutMockProxy, parsedArgs.initializers); } return sutMockProxy; } function tstub(initializer) { const initCouples = initializersToArrayOfCouples(initializer); const stub = applyCouplesToStub({}, initCouples); if (isObjectOrArrayOrClass(stub) || isFunction(stub)) { Object.defineProperty(stub, IS_A_STUB, { value: true }); // Make stubs identifiable by setting non-enumerable property that is not exposed to user. } return stub; } function tset(mockOrStub, initializer) { const initCouples = initializersToArrayOfCouples(initializer); if (isMockProxy(mockOrStub)) { mockOrStub(SET_MOCK_PROXY_RETURN_VALUES, initCouples); } else if (isStub(mockOrStub)) { const newStub = applyCouplesToStub(mockOrStub, initCouples); if (newStub != mockOrStub) { throw new Error('tset: cannot replace stub root'); } } else { throw new Error('tset: first argument should be either mock or stub'); } } exports.default = { ANY: exports.TM_ANY, mock: tmock, stub: tstub, set: tset, reset: treset, unmock: tunmock, info: tinfo, globalopt: tglobalopt, localopt: tlocalopt, }; //# sourceMappingURL=terse-mock.js.map