diff --git a/spec/core/SpyRegistrySpec.js b/spec/core/SpyRegistrySpec.js index f3c81a47..11d1e5dd 100644 --- a/spec/core/SpyRegistrySpec.js +++ b/spec/core/SpyRegistrySpec.js @@ -407,6 +407,123 @@ describe('SpyRegistry', function() { expect(subject.toString).not.toBe('I am a spy'); expect(subject.hasOwnProperty).not.toBe('I am a spy'); }); + describe('when includeNonEnumerable is true', function() { + it('does not override Object.prototype methods', function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: function() { + return 'I am a spy'; + } + }); + var subject = { + spied1: function() {} + }; + + spyRegistry.spyOnAllFunctions(subject, true); + + expect(subject.spied1).toBe('I am a spy'); + expect(subject.toString).not.toBe('I am a spy'); + expect(subject.hasOwnProperty).not.toBe('I am a spy'); + }); + + it('overrides non-enumerable properties', function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: function() { + return 'I am a spy'; + } + }); + var subject = { + spied1: function() {}, + spied2: function() {} + }; + + Object.defineProperty(subject, 'spied2', { + enumerable: false, + writable: true, + configurable: true + }); + + spyRegistry.spyOnAllFunctions(subject, true); + + expect(subject.spied1).toBe('I am a spy'); + expect(subject.spied2).toBe('I am a spy'); + }); + + it('should not spy on non-enumerable functions named constructor', function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: function() { + return 'I am a spy'; + } + }); + var subject = { + constructor: function() {} + }; + + Object.defineProperty(subject, 'constructor', { + enumerable: false, + writable: true, + configurable: true + }); + + spyRegistry.spyOnAllFunctions(subject, true); + + expect(subject.constructor).not.toBe('I am a spy'); + }); + + it('should spy on enumerable functions named constructor', function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: function() { + return 'I am a spy'; + } + }); + var subject = { + constructor: function() {} + }; + + spyRegistry.spyOnAllFunctions(subject, true); + + expect(subject.constructor).toBe('I am a spy'); + }); + + it('should not throw an exception if we try and access strict mode restricted properties', function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: function() { + return 'I am a spy'; + } + }); + var subject = function() {}; + var fn = function() { + spyRegistry.spyOnAllFunctions(subject, true); + }; + + expect(fn).not.toThrow(); + }); + + it('should not spy on properties which are more permissable further up the prototype chain', function() { + var spyRegistry = new jasmineUnderTest.SpyRegistry({ + createSpy: function() { + return 'I am a spy'; + } + }); + var subjectParent = Object.defineProperty({}, 'sharedProp', { + value: function() {}, + writable: true, + configurable: true + }); + + var subject = Object.create(subjectParent); + + Object.defineProperty(subject, 'sharedProp', { + value: function() {} + }); + + var fn = function() { + spyRegistry.spyOnAllFunctions(subject, true); + }; + + expect(fn).not.toThrow(); + expect(subject).not.toBe('I am a spy'); + }); + }); }); describe('#clearSpies', function() { diff --git a/src/core/SpyRegistry.js b/src/core/SpyRegistry.js index 6e0bb33e..140bd990 100644 --- a/src/core/SpyRegistry.js +++ b/src/core/SpyRegistry.js @@ -163,7 +163,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) { return spy; }; - this.spyOnAllFunctions = function(obj) { + this.spyOnAllFunctions = function(obj, includeNonEnumerable) { if (j$.util.isUndefined(obj)) { throw new Error( 'spyOnAllFunctions could not find an object to spy upon' @@ -171,30 +171,27 @@ getJasmineRequireObj().SpyRegistry = function(j$) { } var pointer = obj, - props = [], - prop, - descriptor; + propsToSpyOn = [], + properties, + propertiesToSkip = []; - while (pointer) { - for (prop in pointer) { - if ( - Object.prototype.hasOwnProperty.call(pointer, prop) && - pointer[prop] instanceof Function - ) { - descriptor = Object.getOwnPropertyDescriptor(pointer, prop); - if ( - (descriptor.writable || descriptor.set) && - descriptor.configurable - ) { - props.push(prop); - } - } - } + while ( + pointer && + (!includeNonEnumerable || pointer !== Object.prototype) + ) { + properties = getProps(pointer, includeNonEnumerable); + properties = properties.filter(function(prop) { + return propertiesToSkip.indexOf(prop) === -1; + }); + propertiesToSkip = propertiesToSkip.concat(properties); + propsToSpyOn = propsToSpyOn.concat( + getSpyableFunctionProps(pointer, properties) + ); pointer = Object.getPrototypeOf(pointer); } - for (var i = 0; i < props.length; i++) { - this.spyOn(obj, props[i]); + for (var i = 0; i < propsToSpyOn.length; i++) { + this.spyOn(obj, propsToSpyOn[i]); } return obj; @@ -209,5 +206,49 @@ getJasmineRequireObj().SpyRegistry = function(j$) { }; } + function getProps(obj, includeNonEnumerable) { + var enumerableProperties = Object.keys(obj); + + if (!includeNonEnumerable) { + return enumerableProperties; + } + + return Object.getOwnPropertyNames(obj).filter(function(prop) { + return ( + prop !== 'constructor' || + enumerableProperties.indexOf('constructor') > -1 + ); + }); + } + + function getSpyableFunctionProps(obj, propertiesToCheck) { + var props = [], + prop; + for (var i = 0; i < propertiesToCheck.length; i++) { + prop = propertiesToCheck[i]; + if ( + Object.prototype.hasOwnProperty.call(obj, prop) && + isSpyableProp(obj, prop) + ) { + props.push(prop); + } + } + return props; + } + + function isSpyableProp(obj, prop) { + var value, descriptor; + try { + value = obj[prop]; + } catch (e) { + return false; + } + if (value instanceof Function) { + descriptor = Object.getOwnPropertyDescriptor(obj, prop); + return (descriptor.writable || descriptor.set) && descriptor.configurable; + } + return false; + } + return SpyRegistry; }; diff --git a/src/core/requireInterface.js b/src/core/requireInterface.js index 6473ffaa..71f7d276 100644 --- a/src/core/requireInterface.js +++ b/src/core/requireInterface.js @@ -285,10 +285,11 @@ getJasmineRequireObj().interface = function(jasmine, env) { * @function * @global * @param {Object} obj - The object upon which to install the {@link Spy}s + * @param {boolean} includeNonEnumerable - Whether or not to add spies to non-enumerable properties * @returns {Object} the spied object */ - spyOnAllFunctions: function(obj) { - return env.spyOnAllFunctions(obj); + spyOnAllFunctions: function(obj, includeNonEnumerable) { + return env.spyOnAllFunctions(obj, includeNonEnumerable); }, jsApiReporter: new jasmine.JsApiReporter({