UNPKG

sinon

Version:

JavaScript test spies, stubs and mocks.

273 lines (229 loc) 8.36 kB
"use strict"; const arrayProto = require("@sinonjs/commons").prototypes.array; const extend = require("./util/core/extend"); const functionName = require("@sinonjs/commons").functionName; const nextTick = require("./util/core/next-tick"); const valueToString = require("@sinonjs/commons").valueToString; const exportAsyncBehaviors = require("./util/core/export-async-behaviors"); const concat = arrayProto.concat; const join = arrayProto.join; const reverse = arrayProto.reverse; const slice = arrayProto.slice; const useLeftMostCallback = -1; const useRightMostCallback = -2; function getCallback(behavior, args) { const callArgAt = behavior.callArgAt; if (callArgAt >= 0) { return args[callArgAt]; } let argumentList; if (callArgAt === useLeftMostCallback) { argumentList = args; } if (callArgAt === useRightMostCallback) { argumentList = reverse(slice(args)); } const callArgProp = behavior.callArgProp; for (let i = 0, l = argumentList.length; i < l; ++i) { if (!callArgProp && typeof argumentList[i] === "function") { return argumentList[i]; } if ( callArgProp && argumentList[i] && typeof argumentList[i][callArgProp] === "function" ) { return argumentList[i][callArgProp]; } } return null; } function getCallbackError(behavior, func, args) { if (behavior.callArgAt < 0) { let msg; if (behavior.callArgProp) { msg = `${functionName( behavior.stub, )} expected to yield to '${valueToString( behavior.callArgProp, )}', but no object with such a property was passed.`; } else { msg = `${functionName( behavior.stub, )} expected to yield, but no callback was passed.`; } if (args.length > 0) { msg += ` Received [${join(args, ", ")}]`; } return msg; } return `argument at index ${behavior.callArgAt} is not a function: ${func}`; } function ensureArgs(name, behavior, args) { // map function name to internal property // callsArg => callArgAt const property = name.replace(/sArg/, "ArgAt"); const index = behavior[property]; if (index >= args.length) { throw new TypeError( `${name} failed: ${index + 1} arguments required but only ${ args.length } present`, ); } } function callCallback(behavior, args) { if (typeof behavior.callArgAt === "number") { ensureArgs("callsArg", behavior, args); const func = getCallback(behavior, args); if (typeof func !== "function") { throw new TypeError(getCallbackError(behavior, func, args)); } if (behavior.callbackAsync) { nextTick(function () { func.apply( behavior.callbackContext, behavior.callbackArguments, ); }); } else { return func.apply( behavior.callbackContext, behavior.callbackArguments, ); } } return undefined; } const proto = { create: function create(stub) { const behavior = extend({}, proto); delete behavior.create; delete behavior.addBehavior; delete behavior.createBehavior; behavior.stub = stub; if (stub.defaultBehavior && stub.defaultBehavior.promiseLibrary) { behavior.promiseLibrary = stub.defaultBehavior.promiseLibrary; } return behavior; }, isPresent: function isPresent() { return ( typeof this.callArgAt === "number" || this.exception || this.exceptionCreator || typeof this.returnArgAt === "number" || this.returnThis || typeof this.resolveArgAt === "number" || this.resolveThis || typeof this.throwArgAt === "number" || this.fakeFn || this.returnValueDefined ); }, /*eslint complexity: ["error", 20]*/ invoke: function invoke(context, args) { /* * callCallback (conditionally) calls ensureArgs * * Note: callCallback intentionally happens before * everything else and cannot be moved lower */ const returnValue = callCallback(this, args); if (this.exception) { throw this.exception; } else if (this.exceptionCreator) { this.exception = this.exceptionCreator(); this.exceptionCreator = undefined; throw this.exception; } else if (typeof this.returnArgAt === "number") { ensureArgs("returnsArg", this, args); return args[this.returnArgAt]; } else if (this.returnThis) { return context; } else if (typeof this.throwArgAt === "number") { ensureArgs("throwsArg", this, args); throw args[this.throwArgAt]; } else if (this.fakeFn) { return this.fakeFn.apply(context, args); } else if (typeof this.resolveArgAt === "number") { ensureArgs("resolvesArg", this, args); return (this.promiseLibrary || Promise).resolve( args[this.resolveArgAt], ); } else if (this.resolveThis) { return (this.promiseLibrary || Promise).resolve(context); } else if (this.resolve) { return (this.promiseLibrary || Promise).resolve(this.returnValue); } else if (this.reject) { return (this.promiseLibrary || Promise).reject(this.returnValue); } else if (this.callsThrough) { const wrappedMethod = this.effectiveWrappedMethod(); return wrappedMethod.apply(context, args); } else if (this.callsThroughWithNew) { // Get the original method (assumed to be a constructor in this case) const WrappedClass = this.effectiveWrappedMethod(); // Turn the arguments object into a normal array const argsArray = slice(args); // Call the constructor const F = WrappedClass.bind.apply( WrappedClass, concat([null], argsArray), ); return new F(); } else if (typeof this.returnValue !== "undefined") { return this.returnValue; } else if (typeof this.callArgAt === "number") { return returnValue; } return this.returnValue; }, effectiveWrappedMethod: function effectiveWrappedMethod() { for (let stubb = this.stub; stubb; stubb = stubb.parent) { if (stubb.wrappedMethod) { return stubb.wrappedMethod; } } throw new Error("Unable to find wrapped method"); }, onCall: function onCall(index) { return this.stub.onCall(index); }, onFirstCall: function onFirstCall() { return this.stub.onFirstCall(); }, onSecondCall: function onSecondCall() { return this.stub.onSecondCall(); }, onThirdCall: function onThirdCall() { return this.stub.onThirdCall(); }, withArgs: function withArgs(/* arguments */) { throw new Error( 'Defining a stub by invoking "stub.onCall(...).withArgs(...)" ' + 'is not supported. Use "stub.withArgs(...).onCall(...)" ' + "to define sequential behavior for calls with certain arguments.", ); }, }; function createBehavior(behaviorMethod) { return function () { this.defaultBehavior = this.defaultBehavior || proto.create(this); this.defaultBehavior[behaviorMethod].apply( this.defaultBehavior, arguments, ); return this; }; } function addBehavior(stub, name, fn) { proto[name] = function () { fn.apply(this, concat([this], slice(arguments))); return this.stub || this; }; stub[name] = createBehavior(name); } proto.addBehavior = addBehavior; proto.createBehavior = createBehavior; const asyncBehaviors = exportAsyncBehaviors(proto); module.exports = extend.nonEnum({}, proto, asyncBehaviors);