Skip to content

Commit baa1aee

Browse files
authored
.define method (#2539)
* Restore sandbox after each .replace test Otherwise a failed test may not restore it. * .define method for temporarily defining new properties during the tests * better comment * detailed exception messages * properly delete the property during the cleanup * Add .define to more places * Document .define * Fix test * Code review suggestions * prettier --write
1 parent 2ddf7ff commit baa1aee

File tree

4 files changed

+153
-4
lines changed

4 files changed

+153
-4
lines changed

docs/release-source/release/sandbox.md

+35-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Sandboxes - Sinon.JS
44
breadcrumb: sandbox
55
---
66

7-
Sandboxes removes the need to keep track of every fake created, which greatly simplifies cleanup.
7+
Sandboxes remove the need to keep track of every fake created, which greatly simplifies cleanup.
88

99
```javascript
1010
var sandbox = require("sinon").createSandbox();
@@ -181,6 +181,40 @@ A convenience reference for [`sinon.assert`](./assertions)
181181

182182
183183

184+
#### `sandbox.define(object, property, value);`
185+
186+
Defines the `property` on `object` with the value `value`. Attempts to define an already defined value cause an exception.
187+
188+
`value` can be any value except `undefined`, including `spies`, `stubs` and `fakes`.
189+
190+
```js
191+
var myObject = {};
192+
193+
sandbox.define(myObject, "myValue", function () {
194+
return "blackberry";
195+
});
196+
197+
sandbox.define(myObject, "myMethod", function () {
198+
return "strawberry";
199+
});
200+
201+
console.log(myObject.myValue);
202+
// blackberry
203+
204+
console.log(myObject.myMethod());
205+
// strawberry
206+
207+
sandbox.restore();
208+
209+
console.log(myObject.myValue);
210+
// undefined
211+
212+
console.log(myObject.myMethod);
213+
// undefined
214+
```
215+
216+
217+
184218
#### `sandbox.replace(object, property, replacement);`
185219

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

lib/sinon/sandbox.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ function Sandbox() {
106106
return sandbox.fake.apply(null, arguments);
107107
};
108108

109+
obj.define = function () {
110+
return sandbox.define.apply(null, arguments);
111+
};
112+
109113
obj.replace = function () {
110114
return sandbox.replace.apply(null, arguments);
111115
};
@@ -196,7 +200,7 @@ function Sandbox() {
196200
const descriptor = getPropertyDescriptor(object, property);
197201

198202
function restorer() {
199-
if (descriptor.isOwn) {
203+
if (descriptor?.isOwn) {
200204
Object.defineProperty(object, property, descriptor);
201205
} else {
202206
delete object[property];
@@ -228,7 +232,7 @@ function Sandbox() {
228232
throw new TypeError(
229233
`Cannot replace non-existent property ${valueToString(
230234
property
231-
)}`
235+
)}. Perhaps you meant sandbox.define()?`
232236
);
233237
}
234238

@@ -262,6 +266,31 @@ function Sandbox() {
262266
return replacement;
263267
};
264268

269+
sandbox.define = function define(object, property, value) {
270+
const descriptor = getPropertyDescriptor(object, property);
271+
272+
if (descriptor) {
273+
throw new TypeError(
274+
`Cannot define the already existing property ${valueToString(
275+
property
276+
)}. Perhaps you meant sandbox.replace()?`
277+
);
278+
}
279+
280+
if (typeof value === "undefined") {
281+
throw new TypeError("Expected value argument to be defined");
282+
}
283+
284+
verifyNotReplaced(object, property);
285+
286+
// store a function for restoring the defined property
287+
push(fakeRestorers, getFakeRestorer(object, property));
288+
289+
object[property] = value;
290+
291+
return value;
292+
};
293+
265294
sandbox.replaceGetter = function replaceGetter(
266295
object,
267296
property,

lib/sinon/util/core/default-config.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
"server",
1111
"requests",
1212
"fake",
13+
"define",
1314
"replace",
1415
"replaceSetter",
1516
"replaceGetter",

test/sandbox-test.js

+86-1
Original file line numberDiff line numberDiff line change
@@ -807,11 +807,96 @@ describe("Sandbox", function () {
807807
});
808808
});
809809

810+
describe(".define", function () {
811+
beforeEach(function () {
812+
this.sandbox = createSandbox();
813+
});
814+
815+
afterEach(function () {
816+
this.sandbox.restore();
817+
});
818+
819+
it("should define a function property", function () {
820+
function newFunction() {
821+
return;
822+
}
823+
824+
const object = {};
825+
826+
this.sandbox.define(object, "property", newFunction);
827+
828+
assert.equals(object.property, newFunction);
829+
830+
this.sandbox.restore();
831+
832+
assert.isUndefined(object.property);
833+
});
834+
835+
it("should define a non-function property", function () {
836+
const newValue = "some-new-value";
837+
const object = {};
838+
839+
this.sandbox.define(object, "property", newValue);
840+
841+
assert.equals(object.property, newValue);
842+
843+
this.sandbox.restore();
844+
845+
assert.isUndefined(object.property);
846+
});
847+
848+
it("should error on existing descriptor", function () {
849+
const sandbox = this.sandbox;
850+
851+
const existingValue = "123";
852+
const existingFunction = () => "abcd";
853+
854+
const object = {
855+
existingValue: existingValue,
856+
existingFunction: existingFunction,
857+
};
858+
859+
assert.exception(
860+
function () {
861+
sandbox.define(object, "existingValue", "new value");
862+
},
863+
{
864+
message:
865+
"Cannot define the already existing property existingValue. Perhaps you meant sandbox.replace()?",
866+
name: "TypeError",
867+
}
868+
);
869+
870+
assert.exception(
871+
function () {
872+
sandbox.define(
873+
object,
874+
"existingFunction",
875+
() => "new function"
876+
);
877+
},
878+
{
879+
message:
880+
"Cannot define the already existing property existingFunction. Perhaps you meant sandbox.replace()?",
881+
name: "TypeError",
882+
}
883+
);
884+
885+
// Verify that the methods above, even though they failed, did not replace the values
886+
assert.equals(object.existingValue, existingValue);
887+
assert.equals(object.existingFunction, existingFunction);
888+
});
889+
});
890+
810891
describe(".replace", function () {
811892
beforeEach(function () {
812893
this.sandbox = createSandbox();
813894
});
814895

896+
afterEach(function () {
897+
this.sandbox.restore();
898+
});
899+
815900
it("should replace a function property", function () {
816901
const replacement = function replacement() {
817902
return;
@@ -873,7 +958,7 @@ describe("Sandbox", function () {
873958
},
874959
{
875960
message:
876-
"Cannot replace non-existent property i-dont-exist",
961+
"Cannot replace non-existent property i-dont-exist. Perhaps you meant sandbox.define()?",
877962
name: "TypeError",
878963
}
879964
);

0 commit comments

Comments
 (0)