Files
jasmine/src/core/SpyRegistry.js
Steve Gravrock 434575f49d Use one declaration per statement
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.
2026-03-11 06:30:46 -07:00

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