diff --git a/src/sinon/default-behaviors.js b/src/sinon/default-behaviors.js index df5a00a76..446681332 100644 --- a/src/sinon/default-behaviors.js +++ b/src/sinon/default-behaviors.js @@ -4,6 +4,7 @@ const { prototypes } = commons; import isPropertyConfigurable from "./util/core/is-property-configurable.js"; import exportAsyncBehaviors from "./util/core/export-async-behaviors.js"; import extend from "./util/core/extend.js"; +import spy from "./spy.js"; const { slice } = prototypes.array; @@ -280,8 +281,15 @@ const defaultBehaviors = { get: function get(fake, getterFunction) { const rootStub = fake.stub || fake; + // Wrap the replacement getter in a spy so that reading the property + // records the access (callCount, calledOnce, ...). The spy is exposed + // on the stub as `stub.getter`, mirroring the `spy.get` accessor that + // `sinon.spy(obj, "prop", ["get"])` produces. See #1741. + const getterSpy = spy(getterFunction); + rootStub.getter = getterSpy; + Object.defineProperty(rootStub.rootObj, rootStub.propName, { - get: getterFunction, + get: getterSpy, configurable: isPropertyConfigurable( rootStub.rootObj, rootStub.propName, @@ -294,12 +302,19 @@ const defaultBehaviors = { set: function set(fake, setterFunction) { const rootStub = fake.stub || fake; + // Wrap the replacement setter in a spy so that writing the property + // records the access (callCount, calledWith, ...). The spy is exposed + // on the stub as `stub.setter`, mirroring the `spy.set` accessor that + // `sinon.spy(obj, "prop", ["set"])` produces. See #1741. + const setterSpy = spy(setterFunction); + rootStub.setter = setterSpy; + Object.defineProperty( rootStub.rootObj, rootStub.propName, // eslint-disable-next-line accessor-pairs { - set: setterFunction, + set: setterSpy, configurable: isPropertyConfigurable( rootStub.rootObj, rootStub.propName, diff --git a/test/src/stub-test.js b/test/src/stub-test.js index adc016925..0a924c04a 100644 --- a/test/src/stub-test.js +++ b/test/src/stub-test.js @@ -3668,6 +3668,25 @@ describe("stub", function () { assert.equals(myObj.prop, "bar"); }); + + it("records calls to the stubbed getter on stub.getter", function () { + const myObj = { + prop: "foo", + }; + + const stub = createStub(myObj, "prop").get(function getterFn() { + return "bar"; + }); + + const first = myObj.prop; + const second = myObj.prop; + + assert.equals(first, "bar"); + assert.equals(second, "bar"); + assert.equals(myObj.prop, "bar"); + assert.equals(stub.getter.callCount, 3); + assert(stub.getter.calledThrice); + }); }); describe(".set", function () { @@ -3757,6 +3776,24 @@ describe("stub", function () { myObj.prop = "foo"; assert.equals(myObj.otherProp, "bar"); }); + + it("records calls to the stubbed setter on stub.setter", function () { + const myObj = { + prop: "foo", + }; + + const stub = createStub(myObj, "prop").set(function setterFn(val) { + myObj.example = val; + }); + + myObj.prop = "bar"; + myObj.prop = "baz"; + + assert.equals(myObj.example, "baz"); + assert.equals(stub.setter.callCount, 2); + assert(stub.setter.calledTwice); + assert(stub.setter.calledWith("baz")); + }); }); describe(".value", function () {