Add optional param to spyOnAllFunctions to include non-enumerable properties

This commit is contained in:
Darius Keeley
2021-04-22 18:24:20 +01:00
parent 9555cb9842
commit a4ef3687ee
3 changed files with 182 additions and 23 deletions

View File

@@ -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() {

View File

@@ -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;
};

View File

@@ -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({