Skip to content

Commit cac5184

Browse files
authored
Enable use of assignment in the presence of accessors (#2538)
1 parent f8c20e5 commit cac5184

File tree

6 files changed

+119
-41
lines changed

6 files changed

+119
-41
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ docs/releases/
88
docs/_releases/
99
docs/assets/js/
1010
docs/vendor
11+
vendor/
1112

1213
# has linting of its own
1314
docs/release-source/release/examples

docs/release-source/release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ If you need to support old runtimes you can try [Sinon 9][compat-doc-v9].
3636

3737
{% include docs/contribute.md %}
3838

39-
[compat-doc]: https://github.com/sinonjs/sinon/COMPATIBILITY.md
39+
[compat-doc]: https://github.com/sinonjs/sinon/blob/main/COMPATIBILITY.md
4040
[compat-doc-v9]: https://github.com/sinonjs/sinon/blob/v9.2.4/COMPATIBILITY.md

docs/release-source/release/sandbox.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,17 @@ console.log(myObject.myMethod());
236236
// strawberry
237237
```
238238

239-
#### `sandbox.replaceGetter();`
239+
#### `sandbox.replace.usingAccessor(object, property, value);`
240240

241-
Replaces getter for `property` on `object` with `replacement` argument. Attempts to replace an already replaced getter cause an exception.
241+
Usually one intends to _replace_ the value or getter of a field, but there are use cases where one actually wants to _assign_ a value to a property using an existing setter. `#replace.usingAccessor(object, property, value)` will do just that; pass the value into setter function and vice-versa use the getter to get the value used for restoring later on.
242+
243+
##### Use case: no-frills dependency injection in ESM with cleanup
244+
245+
One use case can be to conveniently allow ESM module stubbing using pure dependency injection, having Sinon help you with the cleanup, without resorting to external machinery such as module loaders or require hooks (see [#2403](https://github.com/sinonjs/sinon/issues/2403)). This would then work regardless of bundler, browser or server environment.
246+
247+
#### `sandbox.replaceGetter(object, property, replacementFunction);`
248+
249+
Replaces an existing getter for `property` on `object` with the `replacementFunction` argument. Attempts to replace an already replaced getter cause an exception.
242250

243251
`replacement` must be a `Function`, and can be instances of `spies`, `stubs` and `fakes`.
244252

@@ -257,9 +265,9 @@ console.log(myObject.myProperty);
257265
// strawberry
258266
```
259267

260-
#### `sandbox.replaceSetter();`
268+
#### `sandbox.replaceSetter(object, property, replacementFunction);`
261269

262-
Replaces setter for `property` on `object` with `replacement` argument. Attempts to replace an already replaced setter cause an exception.
270+
Replaces an existing setter for `property` on `object` with the `replacementFunction` argument. Attempts to replace an already replaced setter cause an exception.
263271

264272
`replacement` must be a `Function`, and can be instances of `spies`, `stubs` and `fakes`.
265273

lib/sinon/sandbox.js

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,40 @@ function applyOnEach(fakes, method) {
3535
});
3636
}
3737

38+
function throwOnAccessors(descriptor) {
39+
if (typeof descriptor.get === "function") {
40+
throw new Error("Use sandbox.replaceGetter for replacing getters");
41+
}
42+
43+
if (typeof descriptor.set === "function") {
44+
throw new Error("Use sandbox.replaceSetter for replacing setters");
45+
}
46+
}
47+
48+
function verifySameType(object, property, replacement) {
49+
if (typeof object[property] !== typeof replacement) {
50+
throw new TypeError(
51+
`Cannot replace ${typeof object[
52+
property
53+
]} with ${typeof replacement}`
54+
);
55+
}
56+
}
57+
58+
function checkForValidArguments(descriptor, property, replacement) {
59+
if (typeof descriptor === "undefined") {
60+
throw new TypeError(
61+
`Cannot replace non-existent property ${valueToString(
62+
property
63+
)}. Perhaps you meant sandbox.define()?`
64+
);
65+
}
66+
67+
if (typeof replacement === "undefined") {
68+
throw new TypeError("Expected replacement argument to be defined");
69+
}
70+
}
71+
3872
function Sandbox() {
3973
const sandbox = this;
4074
let fakeRestorers = [];
@@ -66,11 +100,6 @@ function Sandbox() {
66100
return collection;
67101
};
68102

69-
// this is for testing only
70-
sandbox.getRestorers = function () {
71-
return fakeRestorers;
72-
};
73-
74103
sandbox.createStubInstance = function createStubInstance() {
75104
const stubbed = sinonCreateStubInstance.apply(null, arguments);
76105

@@ -196,11 +225,22 @@ function Sandbox() {
196225
sandbox.injectedKeys.length = 0;
197226
};
198227

199-
function getFakeRestorer(object, property) {
228+
/**
229+
* Creates a restorer function for the property
230+
*
231+
* @param {object|Function} object
232+
* @param {string} property
233+
* @param {boolean} forceAssignment
234+
* @returns {Function} restorer function
235+
*/
236+
function getFakeRestorer(object, property, forceAssignment = false) {
200237
const descriptor = getPropertyDescriptor(object, property);
238+
const value = object[property];
201239

202240
function restorer() {
203-
if (descriptor?.isOwn) {
241+
if (forceAssignment) {
242+
object[property] = value;
243+
} else if (descriptor?.isOwn) {
204244
Object.defineProperty(object, property, descriptor);
205245
} else {
206246
delete object[property];
@@ -225,41 +265,43 @@ function Sandbox() {
225265
});
226266
}
227267

268+
/**
269+
* Replace an existing property
270+
*
271+
* @param {object|Function} object
272+
* @param {string} property
273+
* @param {*} replacement a fake, stub, spy or any other value
274+
* @returns {*}
275+
*/
228276
sandbox.replace = function replace(object, property, replacement) {
229277
const descriptor = getPropertyDescriptor(object, property);
278+
checkForValidArguments(descriptor, property, replacement);
279+
throwOnAccessors(descriptor);
280+
verifySameType(object, property, replacement);
230281

231-
if (typeof descriptor === "undefined") {
232-
throw new TypeError(
233-
`Cannot replace non-existent property ${valueToString(
234-
property
235-
)}. Perhaps you meant sandbox.define()?`
236-
);
237-
}
282+
verifyNotReplaced(object, property);
238283

239-
if (typeof replacement === "undefined") {
240-
throw new TypeError("Expected replacement argument to be defined");
241-
}
284+
// store a function for restoring the replaced property
285+
push(fakeRestorers, getFakeRestorer(object, property));
242286

243-
if (typeof descriptor.get === "function") {
244-
throw new Error("Use sandbox.replaceGetter for replacing getters");
245-
}
287+
object[property] = replacement;
246288

247-
if (typeof descriptor.set === "function") {
248-
throw new Error("Use sandbox.replaceSetter for replacing setters");
249-
}
289+
return replacement;
290+
};
250291

251-
if (typeof object[property] !== typeof replacement) {
252-
throw new TypeError(
253-
`Cannot replace ${typeof object[
254-
property
255-
]} with ${typeof replacement}`
256-
);
257-
}
292+
sandbox.replace.usingAccessor = function replaceUsingAccessor(
293+
object,
294+
property,
295+
replacement
296+
) {
297+
const descriptor = getPropertyDescriptor(object, property);
298+
checkForValidArguments(descriptor, property, replacement);
299+
verifySameType(object, property, replacement);
258300

259301
verifyNotReplaced(object, property);
260302

261303
// store a function for restoring the replaced property
262-
push(fakeRestorers, getFakeRestorer(object, property));
304+
push(fakeRestorers, getFakeRestorer(object, property, true));
263305

264306
object[property] = replacement;
265307

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"dev-docs": "cd docs; cp -rl release-source releases/dev; npm run serve-docs",
4848
"build-docs": "cd docs; bundle exec jekyll build",
4949
"serve-docs": "cd docs; bundle exec jekyll serve --incremental --verbose --livereload",
50-
"lint": "eslint --max-warnings 99 '**/*.{js,cjs,mjs}'",
50+
"lint": "eslint --max-warnings 101 '**/*.{js,cjs,mjs}'",
5151
"unimported": "unimported .",
5252
"pretest-webworker": "npm run build",
5353
"prebuild": "rimraf pkg && npm run check-dependencies",

test/sandbox-test.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ describe("Sandbox", function () {
398398
constructor() {
399399
this.privateGetter = () => 42;
400400
}
401+
401402
getValue() {
402403
return this.privateGetter();
403404
}
@@ -1072,8 +1073,8 @@ describe("Sandbox", function () {
10721073
assert.equals(actual, replacement);
10731074
});
10741075

1075-
describe("when asked to replace a getter", function () {
1076-
it("should throw an Error", function () {
1076+
describe("when asked to replace a property with a getter", function () {
1077+
it("should throw an Error by default that informs of replaceGetter", function () {
10771078
const sandbox = this.sandbox;
10781079
const object = {
10791080
get foo() {
@@ -1093,7 +1094,7 @@ describe("Sandbox", function () {
10931094
});
10941095
});
10951096

1096-
describe("when asked to replace a setter", function () {
1097+
describe("when asked to replace a property with a setter", function () {
10971098
it("should throw an Error", function () {
10981099
const sandbox = this.sandbox;
10991100
const object = {
@@ -1116,6 +1117,28 @@ describe("Sandbox", function () {
11161117
});
11171118
});
11181119

1120+
describe(".replace.usingAccessor", function () {
1121+
it("should allow using assignment when replacing a value", function () {
1122+
const sandbox = createSandbox();
1123+
let quaziPrivateStateOfObject = "original";
1124+
const object = {
1125+
// eslint-disable-next-line accessor-pairs
1126+
get foo() {
1127+
return quaziPrivateStateOfObject;
1128+
},
1129+
set foo(value) {
1130+
quaziPrivateStateOfObject = value;
1131+
},
1132+
};
1133+
1134+
assert.equals(object.foo, "original");
1135+
sandbox.replace.usingAccessor(object, "foo", "fake");
1136+
assert.equals(object.foo, "fake");
1137+
sandbox.restore();
1138+
assert.equals(object.foo, "original");
1139+
});
1140+
});
1141+
11191142
describe(".replaceGetter", function () {
11201143
beforeEach(function () {
11211144
this.sandbox = createSandbox();
@@ -1857,8 +1880,12 @@ describe("Sandbox", function () {
18571880
/* eslint-disable no-empty-function, accessor-pairs */
18581881
this.sandbox.inject(this.obj);
18591882

1860-
const myObj = { a: function () {} };
1883+
const myObj = {
1884+
a: function () {},
1885+
};
1886+
18611887
function MyClass() {}
1888+
18621889
MyClass.prototype.method1 = noop;
18631890
Object.defineProperty(myObj, "b", {
18641891
get: function () {

0 commit comments

Comments
 (0)