From baa1aee9e5766ff1bfcbc62d81ddaf3138174c54 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Mon, 11 Sep 2023 00:28:47 -0700 Subject: [PATCH] .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 --- docs/release-source/release/sandbox.md | 36 ++++++++++- lib/sinon/sandbox.js | 33 +++++++++- lib/sinon/util/core/default-config.js | 1 + test/sandbox-test.js | 87 +++++++++++++++++++++++++- 4 files changed, 153 insertions(+), 4 deletions(-) diff --git a/docs/release-source/release/sandbox.md b/docs/release-source/release/sandbox.md index 822c9ce4d..9c64ed8b9 100644 --- a/docs/release-source/release/sandbox.md +++ b/docs/release-source/release/sandbox.md @@ -4,7 +4,7 @@ title: Sandboxes - Sinon.JS breadcrumb: sandbox --- -Sandboxes removes the need to keep track of every fake created, which greatly simplifies cleanup. +Sandboxes remove the need to keep track of every fake created, which greatly simplifies cleanup. ```javascript var sandbox = require("sinon").createSandbox(); @@ -181,6 +181,40 @@ A convenience reference for [`sinon.assert`](./assertions) _Since `sinon@2.0.0`_ +#### `sandbox.define(object, property, value);` + +Defines the `property` on `object` with the value `value`. Attempts to define an already defined value cause an exception. + +`value` can be any value except `undefined`, including `spies`, `stubs` and `fakes`. + +```js +var myObject = {}; + +sandbox.define(myObject, "myValue", function () { + return "blackberry"; +}); + +sandbox.define(myObject, "myMethod", function () { + return "strawberry"; +}); + +console.log(myObject.myValue); +// blackberry + +console.log(myObject.myMethod()); +// strawberry + +sandbox.restore(); + +console.log(myObject.myValue); +// undefined + +console.log(myObject.myMethod); +// undefined +``` + +_Since `sinon@15.3.0`_ + #### `sandbox.replace(object, property, replacement);` Replaces `property` on `object` with `replacement` argument. Attempts to replace an already replaced value cause an exception. Returns the `replacement`. diff --git a/lib/sinon/sandbox.js b/lib/sinon/sandbox.js index 90aca375a..0a4c9a1ba 100644 --- a/lib/sinon/sandbox.js +++ b/lib/sinon/sandbox.js @@ -106,6 +106,10 @@ function Sandbox() { return sandbox.fake.apply(null, arguments); }; + obj.define = function () { + return sandbox.define.apply(null, arguments); + }; + obj.replace = function () { return sandbox.replace.apply(null, arguments); }; @@ -196,7 +200,7 @@ function Sandbox() { const descriptor = getPropertyDescriptor(object, property); function restorer() { - if (descriptor.isOwn) { + if (descriptor?.isOwn) { Object.defineProperty(object, property, descriptor); } else { delete object[property]; @@ -228,7 +232,7 @@ function Sandbox() { throw new TypeError( `Cannot replace non-existent property ${valueToString( property - )}` + )}. Perhaps you meant sandbox.define()?` ); } @@ -262,6 +266,31 @@ function Sandbox() { return replacement; }; + sandbox.define = function define(object, property, value) { + const descriptor = getPropertyDescriptor(object, property); + + if (descriptor) { + throw new TypeError( + `Cannot define the already existing property ${valueToString( + property + )}. Perhaps you meant sandbox.replace()?` + ); + } + + if (typeof value === "undefined") { + throw new TypeError("Expected value argument to be defined"); + } + + verifyNotReplaced(object, property); + + // store a function for restoring the defined property + push(fakeRestorers, getFakeRestorer(object, property)); + + object[property] = value; + + return value; + }; + sandbox.replaceGetter = function replaceGetter( object, property, diff --git a/lib/sinon/util/core/default-config.js b/lib/sinon/util/core/default-config.js index f177987bf..60170b442 100644 --- a/lib/sinon/util/core/default-config.js +++ b/lib/sinon/util/core/default-config.js @@ -10,6 +10,7 @@ module.exports = { "server", "requests", "fake", + "define", "replace", "replaceSetter", "replaceGetter", diff --git a/test/sandbox-test.js b/test/sandbox-test.js index 9ea67307f..d76fc970f 100644 --- a/test/sandbox-test.js +++ b/test/sandbox-test.js @@ -807,11 +807,96 @@ describe("Sandbox", function () { }); }); + describe(".define", function () { + beforeEach(function () { + this.sandbox = createSandbox(); + }); + + afterEach(function () { + this.sandbox.restore(); + }); + + it("should define a function property", function () { + function newFunction() { + return; + } + + const object = {}; + + this.sandbox.define(object, "property", newFunction); + + assert.equals(object.property, newFunction); + + this.sandbox.restore(); + + assert.isUndefined(object.property); + }); + + it("should define a non-function property", function () { + const newValue = "some-new-value"; + const object = {}; + + this.sandbox.define(object, "property", newValue); + + assert.equals(object.property, newValue); + + this.sandbox.restore(); + + assert.isUndefined(object.property); + }); + + it("should error on existing descriptor", function () { + const sandbox = this.sandbox; + + const existingValue = "123"; + const existingFunction = () => "abcd"; + + const object = { + existingValue: existingValue, + existingFunction: existingFunction, + }; + + assert.exception( + function () { + sandbox.define(object, "existingValue", "new value"); + }, + { + message: + "Cannot define the already existing property existingValue. Perhaps you meant sandbox.replace()?", + name: "TypeError", + } + ); + + assert.exception( + function () { + sandbox.define( + object, + "existingFunction", + () => "new function" + ); + }, + { + message: + "Cannot define the already existing property existingFunction. Perhaps you meant sandbox.replace()?", + name: "TypeError", + } + ); + + // Verify that the methods above, even though they failed, did not replace the values + assert.equals(object.existingValue, existingValue); + assert.equals(object.existingFunction, existingFunction); + }); + }); + describe(".replace", function () { beforeEach(function () { this.sandbox = createSandbox(); }); + afterEach(function () { + this.sandbox.restore(); + }); + it("should replace a function property", function () { const replacement = function replacement() { return; @@ -873,7 +958,7 @@ describe("Sandbox", function () { }, { message: - "Cannot replace non-existent property i-dont-exist", + "Cannot replace non-existent property i-dont-exist. Perhaps you meant sandbox.define()?", name: "TypeError", } );