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

Resubmit: Set wrappedMethod on getters/setters #2378

Merged
merged 2 commits into from May 25, 2021
Merged
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
3 changes: 1 addition & 2 deletions docs/release-source/release/stubs.md
Expand Up @@ -492,7 +492,6 @@ You can restore values by calling the `restore` method:

#### `stub.wrappedMethod`

Holds a reference to the original method/function this stub has
wrapped. `undefined` for the property accessors.
Holds a reference to the original method/function this stub has wrapped.

[matchers]: ../matchers
98 changes: 74 additions & 24 deletions lib/sinon/util/core/wrap-method.js
Expand Up @@ -5,6 +5,7 @@ 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 (
Expand All @@ -21,6 +22,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++) {
if (descriptor[accessors[i]] && descriptor[accessors[i]].name === method.name) {
return accessors[i];
}
}
return null;
}

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

Expand Down Expand Up @@ -67,7 +80,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 @@ -109,6 +124,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 @@ -129,49 +145,83 @@ module.exports = function wrapMethod(object, property, method) {
simplePropertyAssignment();
}

extend.nonEnum(method, {
displayName: property,
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],

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
34 changes: 32 additions & 2 deletions test/spy-test.js
Expand Up @@ -329,8 +329,38 @@ describe("spy", function () {
assert(spy.get.calledOnce);
});

describe("global.Error", function () {
beforeEach(function () {
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