sinon
Version:
JavaScript test spies, stubs and mocks.
246 lines (214 loc) • 8.74 kB
JavaScript
// eslint-disable-next-line no-empty-function
const noop = () => {};
const getPropertyDescriptor = require("./get-property-descriptor");
const extend = require("./extend");
const sinonType = require("./sinon-type");
const hasOwnProperty =
require("@sinonjs/commons").prototypes.object.hasOwnProperty;
const valueToString = require("@sinonjs/commons").valueToString;
const push = require("@sinonjs/commons").prototypes.array.push;
function isFunction(obj) {
return (
typeof obj === "function" ||
Boolean(obj && obj.constructor && obj.call && obj.apply)
);
}
function mirrorProperties(target, source) {
for (const prop in source) {
if (!hasOwnProperty(target, prop)) {
target[prop] = source[prop];
}
}
}
function getAccessor(object, property, method) {
const accessors = ["get", "set"];
const descriptor = getPropertyDescriptor(object, property);
for (let i = 0; i < accessors.length; i++) {
if (
descriptor[accessors[i]] &&
descriptor[accessors[i]].name === method.name
) {
return accessors[i];
}
}
return null;
}
// Cheap way to detect if we have ES5 support.
const hasES5Support = "keys" in Object;
module.exports = function wrapMethod(object, property, method) {
if (!object) {
throw new TypeError("Should wrap property of object");
}
if (typeof method !== "function" && typeof method !== "object") {
throw new TypeError(
"Method wrapper should be a function or a property descriptor",
);
}
function checkWrappedMethod(wrappedMethod) {
let error;
if (!isFunction(wrappedMethod)) {
error = new TypeError(
`Attempted to wrap ${typeof wrappedMethod} property ${valueToString(
property,
)} as function`,
);
} else if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
error = new TypeError(
`Attempted to wrap ${valueToString(
property,
)} which is already wrapped`,
);
} else if (wrappedMethod.calledBefore) {
const verb = wrappedMethod.returns ? "stubbed" : "spied on";
error = new TypeError(
`Attempted to wrap ${valueToString(
property,
)} which is already ${verb}`,
);
}
if (error) {
if (wrappedMethod && wrappedMethod.stackTraceError) {
error.stack += `\n--------------\n${wrappedMethod.stackTraceError.stack}`;
}
throw error;
}
}
let error, wrappedMethod, i, wrappedMethodDesc, target, accessor;
const wrappedMethods = [];
function simplePropertyAssignment() {
wrappedMethod = object[property];
checkWrappedMethod(wrappedMethod);
object[property] = method;
method.displayName = property;
}
// Firefox has a problem when using hasOwn.call on objects from other frames.
const owned = object.hasOwnProperty
? object.hasOwnProperty(property) // eslint-disable-line @sinonjs/no-prototype-methods/no-prototype-methods
: hasOwnProperty(object, property);
if (hasES5Support) {
const methodDesc =
typeof method === "function" ? { value: method } : method;
wrappedMethodDesc = getPropertyDescriptor(object, property);
if (!wrappedMethodDesc) {
error = new TypeError(
`Attempted to wrap ${typeof wrappedMethod} property ${property} as function`,
);
} else if (
wrappedMethodDesc.restore &&
wrappedMethodDesc.restore.sinon
) {
error = new TypeError(
`Attempted to wrap ${property} which is already wrapped`,
);
}
if (error) {
if (wrappedMethodDesc && wrappedMethodDesc.stackTraceError) {
error.stack += `\n--------------\n${wrappedMethodDesc.stackTraceError.stack}`;
}
throw error;
}
const types = Object.keys(methodDesc);
for (i = 0; i < types.length; i++) {
wrappedMethod = wrappedMethodDesc[types[i]];
checkWrappedMethod(wrappedMethod);
push(wrappedMethods, wrappedMethod);
}
mirrorProperties(methodDesc, wrappedMethodDesc);
for (i = 0; i < types.length; i++) {
mirrorProperties(methodDesc[types[i]], wrappedMethodDesc[types[i]]);
}
// you are not allowed to flip the configurable prop on an
// existing descriptor to anything but false (#2514)
if (!owned) {
methodDesc.configurable = true;
}
Object.defineProperty(object, property, methodDesc);
// catch failing assignment
// this is the converse of the check in `.restore` below
if (typeof method === "function" && object[property] !== method) {
// correct any wrongdoings caused by the defineProperty call above,
// such as adding new items (if object was a Storage object)
delete object[property];
simplePropertyAssignment();
}
} else {
simplePropertyAssignment();
}
extendObjectWithWrappedMethods();
function extendObjectWithWrappedMethods() {
for (i = 0; i < wrappedMethods.length; i++) {
accessor = getAccessor(object, property, wrappedMethods[i]);
target = accessor ? method[accessor] : method;
extend.nonEnum(target, {
displayName: property,
wrappedMethod: wrappedMethods[i],
// Set up an Error object for a stack trace which can be used later to find what line of
// code the original method was created on.
stackTraceError: new Error("Stack Trace for original"),
restore: restore,
});
target.restore.sinon = true;
if (!hasES5Support) {
mirrorProperties(target, wrappedMethod);
}
}
}
function restore() {
accessor = getAccessor(object, property, this.wrappedMethod);
let descriptor;
// For prototype properties try to reset by delete first.
// If this fails (ex: localStorage on mobile safari) then force a reset
// via direct assignment.
if (accessor) {
if (!owned) {
try {
// In some cases `delete` may throw an error
delete object[property][accessor];
} catch (e) {} // eslint-disable-line no-empty
// For native code functions `delete` fails without throwing an error
// on Chrome < 43, PhantomJS, etc.
} else if (hasES5Support) {
descriptor = getPropertyDescriptor(object, property);
descriptor[accessor] = wrappedMethodDesc[accessor];
Object.defineProperty(object, property, descriptor);
}
if (hasES5Support) {
descriptor = getPropertyDescriptor(object, property);
if (descriptor && descriptor.value === target) {
object[property][accessor] = this.wrappedMethod;
}
} else {
// Use strict equality comparison to check failures then force a reset
// via direct assignment.
if (object[property][accessor] === target) {
object[property][accessor] = this.wrappedMethod;
}
}
} else {
if (!owned) {
try {
delete object[property];
} catch (e) {} // eslint-disable-line no-empty
} else if (hasES5Support) {
Object.defineProperty(object, property, wrappedMethodDesc);
}
if (hasES5Support) {
descriptor = getPropertyDescriptor(object, property);
if (descriptor && descriptor.value === target) {
object[property] = this.wrappedMethod;
}
} else {
if (object[property] === target) {
object[property] = this.wrappedMethod;
}
}
}
if (sinonType.get(object) === "stub-instance") {
// this is simply to avoid errors after restoring if something should
// traverse the object in a cleanup phase, ref #2477
object[property] = noop;
}
}
return method;
};
;