sinon
Version:
JavaScript test spies, stubs and mocks.
313 lines (264 loc) • 9.38 kB
JavaScript
;
var commons = require('@sinonjs/commons');
var behavior = require('./behavior.js');
var defaultBehaviors = require('./default-behaviors.js');
var proxy = require('./proxy.js');
var isNonExistentProperty = require('./util/core/is-non-existent-property.js');
var spy = require('./spy.js');
var extend = require('./util/core/extend.js');
var getPropertyDescriptor = require('./util/core/get-property-descriptor.js');
var isEsModule = require('./util/core/is-es-module.js');
var sinonType = require('./util/core/sinon-type.js');
var wrapMethod = require('./util/core/wrap-method.js');
var throwOnFalsyObject = require('./throw-on-falsy-object.js');
var walkObject = require('./util/core/walk-object.js');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var commons__default = /*#__PURE__*/_interopDefault(commons);
const { prototypes: commonsPrototypes, functionName, valueToString } = commons__default.default;
const { array: arrayProto, object: objectProto } = commonsPrototypes;
const { hasOwnProperty } = objectProto;
const forEach = arrayProto.forEach;
const pop = arrayProto.pop;
const slice = arrayProto.slice;
const sort = arrayProto.sort;
/**
* @callback SinonFunction
* @param {...unknown} args
* @returns {unknown}
*/
let uuid = 0;
/**
* Creates a stub from a function.
*
* @param {SinonFunction} originalFunc The function to stub
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction} The stub
*/
function createStub(originalFunc, context) {
// eslint-disable-next-line prefer-const
let proxy$1;
function functionStub() {
const args = slice(arguments);
const matchings = proxy$1.matchingFakes(args);
const fnStub =
pop(
sort(matchings, function (a, b) {
return (
a.matchingArguments.length - b.matchingArguments.length
);
}),
) || proxy$1;
return getCurrentBehavior(fnStub).invoke(this, arguments);
}
proxy$1 = proxy(functionStub, originalFunc || functionStub, context);
// Inherit spy API:
extend.nonEnum(proxy$1, spy);
// Inherit stub API:
extend.nonEnum(proxy$1, stub);
// Create a bound instantiateFake that preserves context
const instantiateFake = function (f) {
return createStub(f, context);
};
const name = originalFunc ? functionName(originalFunc) : null;
extend.nonEnum(proxy$1, {
fakes: [],
instantiateFake: instantiateFake,
displayName: name || "stub",
defaultBehavior: null,
behaviors: [],
id: `stub#${uuid++}`,
});
sinonType.set(proxy$1, "stub");
return proxy$1;
}
/**
* Implementation of stub with optional context.
*
* @param {object|undefined} object The object to stub
* @param {string|undefined} property The property name to stub
* @param {object} [context] The sinon context for callId tracking
* @returns {SinonFunction} The stub
*/
function stubImpl(object, property, context) {
if (isEsModule(object)) {
throw new TypeError("ES Modules cannot be stubbed");
}
throwOnFalsyObject(object, property);
if (isNonExistentProperty(object, property)) {
throw new TypeError(
`Cannot stub non-existent property ${valueToString(property)}`,
);
}
const actualDescriptor = getPropertyDescriptor(object, property);
assertValidPropertyDescriptor(actualDescriptor, property);
const isObjectOrFunction =
typeof object === "object" || typeof object === "function";
const isStubbingEntireObject =
typeof property === "undefined" && isObjectOrFunction;
const isCreatingNewStub = !object && typeof property === "undefined";
const isStubbingNonFuncProperty =
isObjectOrFunction &&
typeof property !== "undefined" &&
(typeof actualDescriptor === "undefined" ||
typeof actualDescriptor.value !== "function");
if (isStubbingEntireObject) {
return walkObject.walkObjectStrict(function (obj, prop) {
return stubImpl(obj, prop, context);
}, object);
}
if (isCreatingNewStub) {
return createStub(undefined, context);
}
const func =
typeof actualDescriptor.value === "function"
? actualDescriptor.value
: null;
const s = createStub(func, context);
extend.nonEnum(s, {
rootObj: object,
propName: property,
shadowsPropOnPrototype: !actualDescriptor.isOwn,
restore: function restore() {
if (actualDescriptor !== undefined && actualDescriptor.isOwn) {
Object.defineProperty(object, property, actualDescriptor);
return;
}
delete object[property];
},
});
return isStubbingNonFuncProperty ? s : wrapMethod(object, property, s);
}
/**
* Creates a stub (public API, backward compatible).
*
* @param {object} [object] The object to stub
* @param {string} [property] The property name to stub
* @returns {SinonFunction} The stub
*/
function stub(object, property) {
if (arguments.length > 2) {
throw new TypeError(
"stub(obj, 'meth', fn) has been removed, see documentation",
);
}
return stubImpl(object, property, undefined);
}
/**
* Creates a stub with a specific context (for sandbox use).
*
* @param {object} context The sinon context for callId tracking
* @param {object} [object] The object to stub
* @param {string} [property] The property name to stub
* @returns {SinonFunction} The stub
*/
stub.withContext = function (context, object, property) {
return stubImpl(object, property, context);
};
function assertValidPropertyDescriptor(descriptor, property) {
if (!descriptor || !property) {
return;
}
if (descriptor.isOwn && !descriptor.configurable && !descriptor.writable) {
throw new TypeError(
`The descriptor for property \`${property}\` is non-configurable and non-writable. ` +
`Sinon cannot stub properties that are immutable. ` +
`See https://sinonjs.org/faq#property-descriptor-errors for help fixing this issue.`,
);
}
if ((descriptor.get || descriptor.set) && !descriptor.configurable) {
throw new TypeError(
`Descriptor for accessor property ${property} is non-configurable`,
);
}
if (isDataDescriptor(descriptor) && !descriptor.writable) {
throw new TypeError(
`Descriptor for data property ${property} is non-writable`,
);
}
}
function isDataDescriptor(descriptor) {
return (
!descriptor.value &&
!descriptor.writable &&
!descriptor.set &&
!descriptor.get
);
}
function getParentBehaviour(stubInstance) {
return stubInstance.parent && getCurrentBehavior(stubInstance.parent);
}
function getDefaultBehavior(stubInstance) {
return (
stubInstance.defaultBehavior ||
getParentBehaviour(stubInstance) ||
behavior.create(stubInstance)
);
}
function getCurrentBehavior(stubInstance) {
const currentBehavior = stubInstance.behaviors[stubInstance.callCount - 1];
return currentBehavior && currentBehavior.isPresent()
? currentBehavior
: getDefaultBehavior(stubInstance);
}
const proto = {
resetBehavior: function () {
this.defaultBehavior = null;
this.behaviors = [];
delete this.returnValue;
delete this.returnArgAt;
delete this.throwArgAt;
delete this.resolveArgAt;
delete this.fakeFn;
this.returnThis = false;
this.resolveThis = false;
forEach(this.fakes, function (fake) {
fake.resetBehavior();
});
},
reset: function () {
this.resetHistory();
this.resetBehavior();
},
onCall: function onCall(index) {
if (!this.behaviors[index]) {
this.behaviors[index] = behavior.create(this);
}
return this.behaviors[index];
},
onFirstCall: function onFirstCall() {
return this.onCall(0);
},
onSecondCall: function onSecondCall() {
return this.onCall(1);
},
onThirdCall: function onThirdCall() {
return this.onCall(2);
},
withArgs: function withArgs() {
const fake = spy.withArgs.apply(this, arguments);
if (this.defaultBehavior && this.defaultBehavior.promiseLibrary) {
fake.defaultBehavior =
fake.defaultBehavior || behavior.create(fake);
fake.defaultBehavior.promiseLibrary =
this.defaultBehavior.promiseLibrary;
}
return fake;
},
};
forEach(Object.keys(behavior), function (method) {
if (
hasOwnProperty(behavior, method) &&
!hasOwnProperty(proto, method) &&
method !== "create" &&
method !== "invoke"
) {
proto[method] = behavior.createBehavior(method);
}
});
forEach(Object.keys(defaultBehaviors), function (method) {
if (hasOwnProperty(defaultBehaviors, method) && !hasOwnProperty(proto, method)) {
behavior.addBehavior(stub, method, defaultBehaviors[method]);
}
});
extend(stub, proto);
module.exports = stub;