From 56191cfb2e24103c62ce4f81529a44c922225728 Mon Sep 17 00:00:00 2001 From: Sukharev Maxim Date: Wed, 30 Dec 2015 13:01:19 +0700 Subject: [PATCH 1/2] Implement spies for get/set functions on accessor properties --- spec/core/SpyRegistrySpec.js | 167 +++++++++++++++++++++++++++++++++++ spec/core/UtilSpec.js | 19 ++++ src/core/SpyRegistry.js | 60 +++++++++++++ src/core/util.js | 12 +++ 4 files changed, 258 insertions(+) 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 f33fb67e..e2bc1d90 100644 --- a/spec/core/UtilSpec.js +++ b/spec/core/UtilSpec.js @@ -25,4 +25,23 @@ describe("jasmineUnderTest.util", function() { expect(jasmineUnderTest.util.isUndefined(undefined)).toBe(false); }); }); + + describe("getPropertyDescriptor", function() { + 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/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/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; }; From 8e23c263835fb9d911f6659fe5a588fc4f5d3ffb Mon Sep 17 00:00:00 2001 From: Henry Blyth Date: Mon, 16 May 2016 10:15:36 +0100 Subject: [PATCH 2/2] Make spyOnProperty available in tests Needed adding to the env and require interface --- src/core/Env.js | 4 ++++ src/core/requireInterface.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/core/Env.js b/src/core/Env.js index ea2fa417..3152bfe5 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -293,6 +293,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/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() }),