The old style of merging all of a function's variable declarations into a single statement made some sense back in the days of var, but there's no reason to keep doing it now that we use const and let.
286 lines
7.4 KiB
JavaScript
286 lines
7.4 KiB
JavaScript
getJasmineRequireObj().SpyRegistry = function(j$) {
|
|
'use strict';
|
|
|
|
const spyOnMsg = j$.private.formatErrorMsg(
|
|
'<spyOn>',
|
|
'spyOn(<object>, <methodName>)'
|
|
);
|
|
const spyOnPropertyMsg = j$.private.formatErrorMsg(
|
|
'<spyOnProperty>',
|
|
'spyOnProperty(<object>, <propName>, [accessType])'
|
|
);
|
|
|
|
function SpyRegistry(options) {
|
|
options = options || {};
|
|
const global = options.global || j$.getGlobal();
|
|
const createSpy = options.createSpy;
|
|
const currentSpies =
|
|
options.currentSpies ||
|
|
function() {
|
|
return [];
|
|
};
|
|
|
|
this.allowRespy = function(allow) {
|
|
this.respy = allow;
|
|
};
|
|
|
|
this.spyOn = function(obj, methodName) {
|
|
const getErrorMsg = spyOnMsg;
|
|
|
|
if (obj === undefined || obj === null) {
|
|
throw new Error(
|
|
getErrorMsg(
|
|
'could not find an object to spy upon for ' + methodName + '()'
|
|
)
|
|
);
|
|
}
|
|
|
|
if (methodName === undefined || methodName === null) {
|
|
throw new Error(getErrorMsg('No method name supplied'));
|
|
}
|
|
|
|
if (obj[methodName] === undefined) {
|
|
throw new Error(getErrorMsg(methodName + '() method does not exist'));
|
|
}
|
|
|
|
// Spying on mock clock timing fns would prevent the real ones from being
|
|
// restored.
|
|
if (
|
|
obj[methodName] &&
|
|
obj[methodName][j$.private.Clock.IsMockClockTimingFn]
|
|
) {
|
|
throw new Error("Mock clock timing functions can't be spied on");
|
|
}
|
|
|
|
if (obj[methodName] && j$.isSpy(obj[methodName])) {
|
|
if (this.respy) {
|
|
return obj[methodName];
|
|
} else {
|
|
throw new Error(
|
|
getErrorMsg(methodName + ' has already been spied upon')
|
|
);
|
|
}
|
|
}
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
|
|
|
|
if (descriptor && !(descriptor.writable || descriptor.set)) {
|
|
throw new Error(
|
|
getErrorMsg(methodName + ' is not declared writable or has no setter')
|
|
);
|
|
}
|
|
|
|
const originalMethod = obj[methodName];
|
|
const spiedMethod = createSpy(methodName, originalMethod);
|
|
let restoreStrategy;
|
|
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(obj, methodName) ||
|
|
(obj === global && methodName === 'onerror')
|
|
) {
|
|
restoreStrategy = function() {
|
|
obj[methodName] = originalMethod;
|
|
};
|
|
} else {
|
|
restoreStrategy = function() {
|
|
try {
|
|
delete obj[methodName];
|
|
// eslint-disable-next-line no-unused-vars
|
|
} catch (e) {
|
|
obj[methodName] = originalMethod;
|
|
}
|
|
};
|
|
}
|
|
|
|
currentSpies().push({
|
|
restoreObjectToOriginalState: restoreStrategy
|
|
});
|
|
|
|
obj[methodName] = spiedMethod;
|
|
|
|
// Check if setting the property actually worked. Some objects, such as
|
|
// localStorage in Firefox and later Safari versions, have no-op setters.
|
|
if (obj[methodName] !== spiedMethod) {
|
|
throw new Error(
|
|
j$.private.formatErrorMsg('<spyOn>')(
|
|
`Can't spy on ${methodName} because assigning to it had no effect`
|
|
)
|
|
);
|
|
}
|
|
|
|
return spiedMethod;
|
|
};
|
|
|
|
this.spyOnProperty = function(obj, propertyName, accessType) {
|
|
const getErrorMsg = spyOnPropertyMsg;
|
|
|
|
accessType = accessType || 'get';
|
|
|
|
if (!obj) {
|
|
throw new Error(
|
|
getErrorMsg(
|
|
'spyOn could not find an object to spy upon for ' +
|
|
propertyName +
|
|
''
|
|
)
|
|
);
|
|
}
|
|
|
|
if (propertyName === undefined) {
|
|
throw new Error(getErrorMsg('No property name supplied'));
|
|
}
|
|
|
|
const descriptor = j$.private.util.getPropertyDescriptor(
|
|
obj,
|
|
propertyName
|
|
);
|
|
|
|
if (!descriptor) {
|
|
throw new Error(getErrorMsg(propertyName + ' property does not exist'));
|
|
}
|
|
|
|
if (!descriptor.configurable) {
|
|
throw new Error(
|
|
getErrorMsg(propertyName + ' is not declared configurable')
|
|
);
|
|
}
|
|
|
|
if (!descriptor[accessType]) {
|
|
throw new Error(
|
|
getErrorMsg(
|
|
'Property ' +
|
|
propertyName +
|
|
' does not have access type ' +
|
|
accessType
|
|
)
|
|
);
|
|
}
|
|
|
|
if (j$.isSpy(descriptor[accessType])) {
|
|
if (this.respy) {
|
|
return descriptor[accessType];
|
|
} else {
|
|
throw new Error(
|
|
getErrorMsg(
|
|
propertyName + '#' + accessType + ' has already been spied upon'
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const originalDescriptor = j$.private.util.clone(descriptor);
|
|
const spy = createSpy(propertyName, descriptor[accessType]);
|
|
let 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.spyOnAllFunctions = function(obj, includeNonEnumerable) {
|
|
if (!obj) {
|
|
throw new Error(
|
|
'spyOnAllFunctions could not find an object to spy upon'
|
|
);
|
|
}
|
|
|
|
let pointer = obj;
|
|
let propsToSpyOn = [];
|
|
let properties;
|
|
let propertiesToSkip = [];
|
|
|
|
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 (const prop of propsToSpyOn) {
|
|
this.spyOn(obj, prop);
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
|
|
this.clearSpies = function() {
|
|
const spies = currentSpies();
|
|
for (let i = spies.length - 1; i >= 0; i--) {
|
|
const spyEntry = spies[i];
|
|
spyEntry.restoreObjectToOriginalState();
|
|
}
|
|
};
|
|
}
|
|
|
|
function getProps(obj, includeNonEnumerable) {
|
|
const 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) {
|
|
const props = [];
|
|
|
|
for (const prop of propertiesToCheck) {
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(obj, prop) &&
|
|
isSpyableProp(obj, prop)
|
|
) {
|
|
props.push(prop);
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
|
|
function isSpyableProp(obj, prop) {
|
|
let value;
|
|
try {
|
|
value = obj[prop];
|
|
// eslint-disable-next-line no-unused-vars
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
|
|
if (value instanceof Function) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
|
|
return (descriptor.writable || descriptor.set) && descriptor.configurable;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return SpyRegistry;
|
|
};
|