Skip to content

Commit

Permalink
Merge 91036eb into fe799e7
Browse files Browse the repository at this point in the history
  • Loading branch information
fatso83 authored Aug 11, 2023
2 parents fe799e7 + 91036eb commit 96255d9
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 34 deletions.
6 changes: 4 additions & 2 deletions docs/release-source/release/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,13 @@ A convenience reference for [`sinon.assert`](./assertions)

_Since `[email protected]`_

#### `sandbox.replace(object, property, replacement);`
#### `sandbox.replace(object, property, replacement, options);`

Replaces `property` on `object` with `replacement` argument. Attempts to replace an already replaced value cause an exception. Returns the `replacement`.

`replacement` can be any value, including `spies`, `stubs` and `fakes`.

This method only works on non-accessor properties, for replacing accessors, use `sandbox.replaceGetter()` and `sandbox.replaceSetter()`.
By default, this method only works on non-accessor properties, for replacing accessors, use `sandbox.replaceGetter()` and `sandbox.replaceSetter()`.

```js
var myObject = {
Expand All @@ -204,6 +204,8 @@ console.log(myObject.myMethod());
// strawberry
```

You _can_ pass the option `{forceAssignment: boolean}` to avoid throwing on encountering a setter. This will pass the replacement into setter function and vice-versa use the getter to get the value used for restoring later on. One use case can be to do ESM module stubbing without resorting to external machinery such as module loaders (see [#2403](https://github.com/sinonjs/sinon/issues/2403)).

#### `sandbox.replaceGetter();`

Replaces getter for `property` on `object` with `replacement` argument. Attempts to replace an already replaced getter cause an exception.
Expand Down
35 changes: 25 additions & 10 deletions lib/sinon/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,14 @@ function Sandbox() {
sandbox.injectedKeys.length = 0;
};

function getFakeRestorer(object, property) {
function getFakeRestorer(object, property, forceAssignment) {
const descriptor = getPropertyDescriptor(object, property);
const value = object[property];

function restorer() {
if (descriptor.isOwn) {
if (forceAssignment) {
object[property] = value;
} else if (descriptor.isOwn) {
Object.defineProperty(object, property, descriptor);
} else {
delete object[property];
Expand All @@ -237,7 +240,17 @@ function Sandbox() {
});
}

sandbox.replace = function replace(object, property, replacement) {
/**
* Replace an existing property
*
* @param {object|Function} object
* @param {string} property
* @param {*} replacement a fake, stub, spy or any other value
* @param {object} [options]
* @param {boolean} options.forceAssignment allows you to force use of assignment in the presence of a setter
* @returns {*}
*/
sandbox.replace = function replace(object, property, replacement, options) {
const descriptor = getPropertyDescriptor(object, property);

if (typeof descriptor === "undefined") {
Expand All @@ -252,26 +265,28 @@ function Sandbox() {
throw new TypeError("Expected replacement argument to be defined");
}

if (typeof descriptor.get === "function") {
throw new Error("Use sandbox.replaceGetter for replacing getters");
}
if (!options?.forceAssignment) {
if (typeof descriptor.get === "function") {
throw new Error("Use sandbox.replaceGetter for replacing getters");
}

if (typeof descriptor.set === "function") {
throw new Error("Use sandbox.replaceSetter for replacing setters");
if (typeof descriptor.set === "function") {
throw new Error("Use sandbox.replaceSetter for replacing setters");
}
}

if (typeof object[property] !== typeof replacement) {
throw new TypeError(
`Cannot replace ${typeof object[
property
]} with ${typeof replacement}`
]} with ${typeof replacement}`
);
}

verifyNotReplaced(object, property);

// store a function for restoring the replaced property
push(fakeRestorers, getFakeRestorer(object, property));
push(fakeRestorers, getFakeRestorer(object, property, options?.forceAssignment));

object[property] = replacement;

Expand Down
72 changes: 50 additions & 22 deletions test/sandbox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("Sandbox", function () {
});

it("can be reset without failing when pre-configured to use a fake server", function () {
const sandbox = createSandbox({ useFakeServer: true });
const sandbox = createSandbox({useFakeServer: true});
refute.exception(function () {
sandbox.reset();
});
Expand Down Expand Up @@ -387,7 +387,7 @@ describe("Sandbox", function () {
foo: sinonStub().returns(3),
});
},
{ message: "Cannot stub foo. Property does not exist!" }
{message: "Cannot stub foo. Property does not exist!"}
);
});

Expand All @@ -398,6 +398,7 @@ describe("Sandbox", function () {
constructor() {
this.privateGetter = () => 42;
}

getValue() {
return this.privateGetter();
}
Expand Down Expand Up @@ -563,7 +564,7 @@ describe("Sandbox", function () {

describe("stub anything", function () {
beforeEach(function () {
this.object = { property: 42 };
this.object = {property: 42};
this.sandbox = new Sandbox();
});

Expand Down Expand Up @@ -910,7 +911,7 @@ describe("Sandbox", function () {
function () {
sandbox.replace(object, "property", replacement);
},
{ message: "Cannot replace string with function" }
{message: "Cannot replace string with function"}
);
});

Expand All @@ -927,7 +928,7 @@ describe("Sandbox", function () {
function () {
sandbox.replace(object, "property", replacement);
},
{ message: "Cannot replace function with string" }
{message: "Cannot replace function with string"}
);
});

Expand Down Expand Up @@ -987,8 +988,8 @@ describe("Sandbox", function () {
assert.equals(actual, replacement);
});

describe("when asked to replace a getter", function () {
it("should throw an Error", function () {
describe("when asked to replace a property with a getter", function () {
it("should throw an Error by default that informs of replaceGetter", function () {
const sandbox = this.sandbox;
const object = {
get foo() {
Expand All @@ -1008,7 +1009,7 @@ describe("Sandbox", function () {
});
});

describe("when asked to replace a setter", function () {
describe("when asked to replace a property with a setter", function () {
it("should throw an Error", function () {
const sandbox = this.sandbox;
const object = {
Expand All @@ -1028,6 +1029,26 @@ describe("Sandbox", function () {
}
);
});

it("should allow forcing assignment", function () {
const sandbox = this.sandbox;
let hiddenFoo = () => 'original';
const object = {
// eslint-disable-next-line accessor-pairs
get foo(){
return hiddenFoo;
},
set foo(value) {
hiddenFoo = value;
},
};

assert.equals(object.foo(),'original')
sandbox.replace(object, "foo", sinonFake.returns('fake'), {forceAssignment: true});
assert.equals(object.foo(),'fake')
sandbox.restore()
assert.equals(object.foo(),'original');
})
});
});

Expand Down Expand Up @@ -1409,8 +1430,8 @@ describe("Sandbox", function () {
const sandbox = (this.sandbox = createSandbox());
const fakes = sandbox.getFakes();

fakes.push({ resetBehavior: sinonSpy() });
fakes.push({ resetBehavior: sinonSpy() });
fakes.push({resetBehavior: sinonSpy()});
fakes.push({resetBehavior: sinonSpy()});
});

it("calls resetBehavior on all fakes", function () {
Expand Down Expand Up @@ -1496,13 +1517,13 @@ describe("Sandbox", function () {
"useFakeTimers"
).returns({});

this.sandbox.useFakeTimers({ toFake: ["Date", "setTimeout"] });
this.sandbox.useFakeTimers({toFake: ["Date", "setTimeout"]});
this.sandbox.useFakeTimers({
toFake: ["setTimeout", "clearTimeout", "setInterval"],
});

assert(
useFakeTimersStub.calledWith({ toFake: ["Date", "setTimeout"] })
useFakeTimersStub.calledWith({toFake: ["Date", "setTimeout"]})
);
assert(
useFakeTimersStub.calledWith({
Expand Down Expand Up @@ -1772,8 +1793,14 @@ describe("Sandbox", function () {
/* eslint-disable no-empty-function, accessor-pairs */
this.sandbox.inject(this.obj);

const myObj = { a: function () {} };
function MyClass() {}
const myObj = {
a: function () {
}
};

function MyClass() {
}

MyClass.prototype.method1 = noop;
Object.defineProperty(myObj, "b", {
get: function () {
Expand All @@ -1782,7 +1809,8 @@ describe("Sandbox", function () {
configurable: true,
});
Object.defineProperty(myObj, "c", {
set: function () {},
set: function () {
},
configurable: true,
});

Expand Down Expand Up @@ -1868,8 +1896,8 @@ describe("Sandbox", function () {
const sandbox = createSandbox();
const fakes = sandbox.getFakes();

fakes.push({ verify: sinonSpy() });
fakes.push({ verify: sinonSpy() });
fakes.push({verify: sinonSpy()});
fakes.push({verify: sinonSpy()});

sandbox.verify();

Expand Down Expand Up @@ -1927,7 +1955,7 @@ describe("Sandbox", function () {
describe("configurable sandbox", function () {
beforeEach(function () {
this.requests = [];
this.fakeServer = { requests: this.requests };
this.fakeServer = {requests: this.requests};

this.useFakeTimersSpy = sinonSpy(sinonClock, "useFakeTimers");
sinonStub(fakeServer, "create").returns(this.fakeServer);
Expand Down Expand Up @@ -1987,7 +2015,7 @@ describe("Sandbox", function () {
};
const clock = {};
const spy = false;
const object = { server: server, clock: clock, spy: spy };
const object = {server: server, clock: clock, spy: spy};
const sandbox = createSandbox(
sinonConfig({
properties: ["server", "clock", "spy"],
Expand Down Expand Up @@ -2114,7 +2142,7 @@ describe("Sandbox", function () {
const sandbox = createSandbox(
sinonConfig({
properties: ["clock"],
useFakeTimers: { toFake: ["Date", "setTimeout"] },
useFakeTimers: {toFake: ["Date", "setTimeout"]},
})
);

Expand Down Expand Up @@ -2266,14 +2294,14 @@ describe("Sandbox", function () {
function () {
sandboxA.assert.fail("Some message");
},
{ name: "CustomErrorA" }
{name: "CustomErrorA"}
);

assert.exception(
function () {
sandboxB.assert.fail("Some message");
},
{ name: "CustomErrorB" }
{name: "CustomErrorB"}
);

sandboxA.restore();
Expand Down

0 comments on commit 96255d9

Please sign in to comment.