sinon
Version:
JavaScript test spies, stubs and mocks.
273 lines (229 loc) • 8.36 kB
JavaScript
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);
;