Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set wrappedMethod on getters/setters #2251

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
99 changes: 74 additions & 25 deletions lib/sinon/util/core/wrap-method.js
Expand Up @@ -4,6 +4,7 @@ var getPropertyDescriptor = require("./get-property-descriptor");
var extend = require("./extend");
var hasOwnProperty = require("@sinonjs/commons").prototypes.object.hasOwnProperty;
var valueToString = require("@sinonjs/commons").valueToString;
var push = require("@sinonjs/commons").prototypes.array.push;

function isFunction(obj) {
return typeof obj === "function" || Boolean(obj && obj.constructor && obj.call && obj.apply);
Expand All @@ -17,6 +18,18 @@ function mirrorProperties(target, source) {
}
}

function getAccessor(object, property, method) {
var accessors = ["get", "set"];
var descriptor = getPropertyDescriptor(object, property);

for (var i = 0; i < accessors.length; i++) {
mroderick marked this conversation as resolved.
Show resolved Hide resolved
if (descriptor[accessors[i]] && descriptor[accessors[i]].name === method.name) {
mroderick marked this conversation as resolved.
Show resolved Hide resolved
return accessors[i];
mroderick marked this conversation as resolved.
Show resolved Hide resolved
}
}
return null;
}

// Cheap way to detect if we have ES5 support.
var hasES5Support = "keys" in Object;

Expand Down Expand Up @@ -51,7 +64,9 @@ module.exports = function wrapMethod(object, property, method) {
}
}

var error, wrappedMethod, i, wrappedMethodDesc;
var error, wrappedMethods, wrappedMethod, i, wrappedMethodDesc, target, accessor;

wrappedMethods = [];

function simplePropertyAssignment() {
wrappedMethod = object[property];
Expand Down Expand Up @@ -86,6 +101,7 @@ module.exports = function wrapMethod(object, property, method) {
for (i = 0; i < types.length; i++) {
wrappedMethod = wrappedMethodDesc[types[i]];
checkWrappedMethod(wrappedMethod);
push(wrappedMethods, wrappedMethod);
}

mirrorProperties(methodDesc, wrappedMethodDesc);
Expand All @@ -106,49 +122,82 @@ module.exports = function wrapMethod(object, property, method) {
simplePropertyAssignment();
}

extend.nonEnum(method, {
displayName: property,
extendObjectWithWrappedMethods();

function extendObjectWithWrappedMethods() {
for (i = 0; i < wrappedMethods.length; i++) {
mroderick marked this conversation as resolved.
Show resolved Hide resolved
accessor = getAccessor(object, property, wrappedMethods[i]);
mroderick marked this conversation as resolved.
Show resolved Hide resolved
target = accessor ? method[accessor] : method;
extend.nonEnum(target, {
displayName: property,
wrappedMethod: wrappedMethods[i],
mroderick marked this conversation as resolved.
Show resolved Hide resolved

wrappedMethod: wrappedMethod,
// 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"),

// 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
});

restore: function() {
// For prototype properties try to reset by delete first.
// If this fails (ex: localStorage on mobile safari) then force a reset
// via direct assignment.
target.restore.sinon = true;
if (!hasES5Support) {
mirrorProperties(target, wrappedMethod);
}
}
}

function restore() {
accessor = getAccessor(object, property, this.wrappedMethod);
var 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) {
// In some cases `delete` may throw an error
try {
delete object[property];
// 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) {
Object.defineProperty(object, property, wrappedMethodDesc);
descriptor = getPropertyDescriptor(object, property);
descriptor[accessor] = wrappedMethodDesc[accessor];
Object.defineProperty(object, property, descriptor);
}

if (hasES5Support) {
var descriptor = getPropertyDescriptor(object, property);
if (descriptor && descriptor.value === method) {
object[property] = wrappedMethod;
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] === method) {
object[property] = wrappedMethod;
if (object[property][accessor] === target) {
object[property][accessor] = this.wrappedMethod;
}
}
}
});

method.restore.sinon = true;
} else {
if (!owned) {
try {
delete object[property];
} catch (e) {} // eslint-disable-line no-empty
} else if (hasES5Support) {
Object.defineProperty(object, property, wrappedMethodDesc);
}

if (!hasES5Support) {
mirrorProperties(method, wrappedMethod);
if (hasES5Support) {
descriptor = getPropertyDescriptor(object, property);
if (descriptor && descriptor.value === target) {
object[property] = this.wrappedMethod;
}
} else {
if (object[property] === target) {
object[property] = this.wrappedMethod;
}
}
}
}

return method;
Expand Down
30 changes: 30 additions & 0 deletions test/spy-test.js
Expand Up @@ -329,6 +329,36 @@ describe("spy", function() {
assert(spy.get.calledOnce);
});

it("sets wrappedMethod on getter and setter", function() {
var object = {
get test() {
return this.property;
},
set test(value) {
this.property = value;
}
};

var descriptor1 = Object.getOwnPropertyDescriptor(object, "test");
var spy = createSpy(object, "test", ["get", "set"]);
var descriptor2 = Object.getOwnPropertyDescriptor(object, "test");

refute.equals(descriptor1, descriptor2);

refute.isUndefined(spy.get.wrappedMethod);
refute.isUndefined(spy.get.restore);
refute.isUndefined(spy.set.wrappedMethod);
refute.isUndefined(spy.set.restore);
assert.isUndefined(spy.wrappedMethod);
assert.isUndefined(spy.restore);

spy.get.restore();
spy.set.restore();

var descriptor3 = Object.getOwnPropertyDescriptor(object, "test");
assert.equals(descriptor1, descriptor3);
});

describe("global.Error", function() {
beforeEach(function() {
this.originalError = globalContext.Error;
Expand Down