diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index cf3cf971..3577abe5 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -297,6 +297,18 @@ getJasmineRequireObj().util = function() { return cloned; }; + util.getPropertyDescriptor = function(obj, methodName) { + var descriptor, + proto = obj; + + do { + descriptor = Object.getOwnPropertyDescriptor(proto, methodName); + proto = Object.getPrototypeOf(proto); + } while (!descriptor && proto); + + return descriptor; + }; + return util; }; @@ -794,6 +806,10 @@ getJasmineRequireObj().Env = function(j$) { return spyRegistry.spyOn.apply(spyRegistry, arguments); }; + this.spyOnProperty = function() { + return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); + }; + var suiteFactory = function(description) { var suite = new j$.Suite({ env: self, @@ -2227,6 +2243,66 @@ getJasmineRequireObj().SpyRegistry = function(j$) { return spiedMethod; }; + this.spyOnProperty = function (obj, propertyName, accessType) { + accessType = accessType || 'get'; + + if (j$.util.isUndefined(obj)) { + throw new Error('spyOn could not find an object to spy upon for ' + propertyName + ''); + } + + if (j$.util.isUndefined(propertyName)) { + throw new Error('No property name supplied'); + } + + var descriptor; + try { + descriptor = j$.util.getPropertyDescriptor(obj, propertyName); + } catch(e) { + // IE 8 doesn't support `definePropery` on non-DOM nodes + } + + if (!descriptor) { + throw new Error(propertyName + ' property does not exist'); + } + + if (!descriptor.configurable) { + throw new Error(propertyName + ' is not declared configurable'); + } + + if(!descriptor[accessType]) { + throw new Error('Property ' + propertyName + ' does not have access type ' + accessType); + } + + if (j$.isSpy(descriptor[accessType])) { + //TODO?: should this return the current spy? Downside: may cause user confusion about spy state + throw new Error(propertyName + ' has already been spied upon'); + } + + var originalDescriptor = j$.util.clone(descriptor), + spy = j$.createSpy(propertyName, descriptor[accessType]), + restoreStrategy; + + if (Object.prototype.hasOwnProperty.call(obj, propertyName)) { + restoreStrategy = function() { + Object.defineProperty(obj, propertyName, originalDescriptor); + }; + } else { + restoreStrategy = function() { + delete obj[propertyName]; + }; + } + + currentSpies().push({ + restoreObjectToOriginalState: restoreStrategy + }); + + descriptor[accessType] = spy; + + Object.defineProperty(obj, propertyName, descriptor); + + return spy; + }; + this.clearSpies = function() { var spies = currentSpies(); for (var i = spies.length - 1; i >= 0; i--) { @@ -3753,6 +3829,10 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.spyOn(obj, methodName); }, + spyOnProperty: function(obj, methodName, accessType) { + return env.spyOnProperty(obj, methodName, accessType); + }, + jsApiReporter: new jasmine.JsApiReporter({ timer: new jasmine.Timer() }), diff --git a/spec/core/SpyRegistrySpec.js b/spec/core/SpyRegistrySpec.js index bbe45903..df68b50f 100644 --- a/spec/core/SpyRegistrySpec.js +++ b/spec/core/SpyRegistrySpec.js @@ -77,6 +77,126 @@ describe("SpyRegistry", function() { }); }); + describe("#spyOnProperty", function() { + // IE 8 doesn't support `definePropery` on non-DOM nodes + if (jasmine.getEnv().ieVersion < 9) { return; } + + it("checks for the existence of the object", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(); + expect(function() { + spyRegistry.spyOnProperty(void 0, 'pants'); + }).toThrowError(/could not find an object/); + }); + + it("checks that a property name was passed", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(), + subject = {}; + + expect(function() { + spyRegistry.spyOnProperty(subject); + }).toThrowError(/No property name supplied/); + }); + + it("checks for the existence of the method", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(), + subject = {}; + + expect(function() { + spyRegistry.spyOnProperty(subject, 'pants'); + }).toThrowError(/property does not exist/); + }); + + it("checks for the existence of access type", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(), + subject = {}; + + Object.defineProperty(subject, 'pants', { + get: function() { return 1; }, + configurable: true + }); + + expect(function() { + spyRegistry.spyOnProperty(subject, 'pants', 'set'); + }).toThrowError(/does not have access type/); + }); + + it("checks if it has already been spied upon", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(), + subject = {}; + + Object.defineProperty(subject, 'spiedProp', { + get: function() { return 1; }, + configurable: true + }); + + spyRegistry.spyOnProperty(subject, 'spiedProp'); + + expect(function() { + spyRegistry.spyOnProperty(subject, 'spiedProp'); + }).toThrowError(/has already been spied upon/); + }); + + it("checks if it can be spied upon", function() { + var subject = {}; + + Object.defineProperty(subject, 'myProp', { + get: function() {} + }); + + Object.defineProperty(subject, 'spiedProp', { + get: function() {}, + configurable: true + }); + + var spyRegistry = new jasmineUnderTest.SpyRegistry(); + + expect(function() { + spyRegistry.spyOnProperty(subject, 'myProp'); + }).toThrowError(/is not declared configurable/); + + expect(function() { + spyRegistry.spyOnProperty(subject, 'spiedProp'); + }).not.toThrowError(/is not declared configurable/); + }); + + it("overrides the property getter on the object and returns the spy", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(), + subject = {}, + returnValue = 1; + + Object.defineProperty(subject, 'spiedProperty', { + get: function() { return returnValue; }, + configurable: true + }); + + expect(subject.spiedProperty).toEqual(returnValue); + + var spy = spyRegistry.spyOnProperty(subject, 'spiedProperty'); + var getter = Object.getOwnPropertyDescriptor(subject, 'spiedProperty').get; + + expect(getter).toEqual(spy); + expect(subject.spiedProperty).toBeUndefined(); + }); + + it("overrides the property setter on the object and returns the spy", function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry(), + subject = {}, + returnValue = 1; + + Object.defineProperty(subject, 'spiedProperty', { + get: function() { return returnValue; }, + set: function() {}, + configurable: true + }); + + var spy = spyRegistry.spyOnProperty(subject, 'spiedProperty', 'set'); + var setter = Object.getOwnPropertyDescriptor(subject, 'spiedProperty').set; + + expect(subject.spiedProperty).toEqual(returnValue); + expect(setter).toEqual(spy); + }); + }); + describe("#clearSpies", function() { it("restores the original functions on the spied-upon objects", function() { var spies = [], @@ -152,4 +272,51 @@ describe("SpyRegistry", function() { expect(jasmineUnderTest.isSpy(subject.spiedFunc)).toBe(false); }); }); + + describe('spying on properties', function() { + it("restores the original properties on the spied-upon objects", function() { + // IE 8 doesn't support `definePropery` on non-DOM nodes + if (jasmine.getEnv().ieVersion < 9) { return; } + + var spies = [], + spyRegistry = new jasmineUnderTest.SpyRegistry({currentSpies: function() { return spies; }}), + originalReturn = 1, + subject = {}; + + Object.defineProperty(subject, 'spiedProp', { + get: function() { return originalReturn; }, + configurable: true + }); + + spyRegistry.spyOnProperty(subject, 'spiedProp'); + spyRegistry.clearSpies(); + + expect(subject.spiedProp).toBe(originalReturn); + }); + + it("does not add a property that the spied-upon object didn't originally have", function() { + // IE 8 doesn't support `Object.create` + if (jasmine.getEnv().ieVersion < 9) { return; } + + var spies = [], + spyRegistry = new jasmineUnderTest.SpyRegistry({currentSpies: function() { return spies; }}), + originalReturn = 1, + subjectParent = {}; + + Object.defineProperty(subjectParent, 'spiedProp', { + get: function() { return originalReturn; }, + configurable: true + }); + + var subject = Object.create(subjectParent); + + expect(subject.hasOwnProperty('spiedProp')).toBe(false); + + spyRegistry.spyOnProperty(subject, 'spiedProp'); + spyRegistry.clearSpies(); + + expect(subject.hasOwnProperty('spiedProp')).toBe(false); + expect(subject.spiedProp).toBe(originalReturn); + }); + }); }); diff --git a/spec/core/UtilSpec.js b/spec/core/UtilSpec.js index 9b693ff8..e547edf1 100644 --- a/spec/core/UtilSpec.js +++ b/spec/core/UtilSpec.js @@ -41,4 +41,26 @@ describe("jasmineUnderTest.util", function() { expect(jasmineUnderTest.util.isUndefined(undefined)).toBe(false); }); }); + + describe("getPropertyDescriptor", function() { + // IE 8 doesn't support `definePropery` on non-DOM nodes + if (jasmine.getEnv().ieVersion < 9) { return; } + + it("get property descriptor from object", function() { + var obj = {prop: 1}, + actual = jasmineUnderTest.util.getPropertyDescriptor(obj, 'prop'), + expected = Object.getOwnPropertyDescriptor(obj, 'prop'); + + expect(actual).toEqual(expected); + }); + + it("get property descriptor from object property", function() { + var proto = {prop: 1}, + obj = Object.create(proto), + actual = jasmineUnderTest.util.getPropertyDescriptor(proto, 'prop'), + expected = Object.getOwnPropertyDescriptor(proto, 'prop'); + + expect(actual).toEqual(expected); + }); + }); }); diff --git a/src/core/Env.js b/src/core/Env.js index ef5582b4..344ac5e8 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -288,6 +288,10 @@ getJasmineRequireObj().Env = function(j$) { return spyRegistry.spyOn.apply(spyRegistry, arguments); }; + this.spyOnProperty = function() { + return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); + }; + var suiteFactory = function(description) { var suite = new j$.Suite({ env: self, diff --git a/src/core/SpyRegistry.js b/src/core/SpyRegistry.js index f53bf0ad..ce4f1b1f 100644 --- a/src/core/SpyRegistry.js +++ b/src/core/SpyRegistry.js @@ -68,6 +68,66 @@ getJasmineRequireObj().SpyRegistry = function(j$) { return spiedMethod; }; + this.spyOnProperty = function (obj, propertyName, accessType) { + accessType = accessType || 'get'; + + if (j$.util.isUndefined(obj)) { + throw new Error('spyOn could not find an object to spy upon for ' + propertyName + ''); + } + + if (j$.util.isUndefined(propertyName)) { + throw new Error('No property name supplied'); + } + + var descriptor; + try { + descriptor = j$.util.getPropertyDescriptor(obj, propertyName); + } catch(e) { + // IE 8 doesn't support `definePropery` on non-DOM nodes + } + + if (!descriptor) { + throw new Error(propertyName + ' property does not exist'); + } + + if (!descriptor.configurable) { + throw new Error(propertyName + ' is not declared configurable'); + } + + if(!descriptor[accessType]) { + throw new Error('Property ' + propertyName + ' does not have access type ' + accessType); + } + + if (j$.isSpy(descriptor[accessType])) { + //TODO?: should this return the current spy? Downside: may cause user confusion about spy state + throw new Error(propertyName + ' has already been spied upon'); + } + + var originalDescriptor = j$.util.clone(descriptor), + spy = j$.createSpy(propertyName, descriptor[accessType]), + restoreStrategy; + + if (Object.prototype.hasOwnProperty.call(obj, propertyName)) { + restoreStrategy = function() { + Object.defineProperty(obj, propertyName, originalDescriptor); + }; + } else { + restoreStrategy = function() { + delete obj[propertyName]; + }; + } + + currentSpies().push({ + restoreObjectToOriginalState: restoreStrategy + }); + + descriptor[accessType] = spy; + + Object.defineProperty(obj, propertyName, descriptor); + + return spy; + }; + this.clearSpies = function() { var spies = currentSpies(); for (var i = spies.length - 1; i >= 0; i--) { diff --git a/src/core/requireInterface.js b/src/core/requireInterface.js index 840eaaed..c9632574 100644 --- a/src/core/requireInterface.js +++ b/src/core/requireInterface.js @@ -56,6 +56,10 @@ getJasmineRequireObj().interface = function(jasmine, env) { return env.spyOn(obj, methodName); }, + spyOnProperty: function(obj, methodName, accessType) { + return env.spyOnProperty(obj, methodName, accessType); + }, + jsApiReporter: new jasmine.JsApiReporter({ timer: new jasmine.Timer() }), diff --git a/src/core/util.js b/src/core/util.js index 11d19347..5b53bf8d 100644 --- a/src/core/util.js +++ b/src/core/util.js @@ -55,5 +55,17 @@ getJasmineRequireObj().util = function() { return cloned; }; + util.getPropertyDescriptor = function(obj, methodName) { + var descriptor, + proto = obj; + + do { + descriptor = Object.getOwnPropertyDescriptor(proto, methodName); + proto = Object.getPrototypeOf(proto); + } while (!descriptor && proto); + + return descriptor; + }; + return util; };