Added jasmine.spyOnGlobalErrorsAsync
* Allows testing code that's expected to prodeuce global errors or unhandled promise rejections * Fixes #1843 * Fixes #1453
This commit is contained in:
@@ -24,9 +24,23 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
new j$.MockDate(global)
|
||||
);
|
||||
|
||||
const runableResources = new j$.RunableResources(function() {
|
||||
const r = runner.currentRunable();
|
||||
return r ? r.id : null;
|
||||
const globalErrors = new j$.GlobalErrors();
|
||||
const installGlobalErrors = (function() {
|
||||
let installed = false;
|
||||
return function() {
|
||||
if (!installed) {
|
||||
globalErrors.install();
|
||||
installed = true;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
const runableResources = new j$.RunableResources({
|
||||
getCurrentRunableId: function() {
|
||||
const r = runner.currentRunable();
|
||||
return r ? r.id : null;
|
||||
},
|
||||
globalErrors
|
||||
});
|
||||
|
||||
let reporter;
|
||||
@@ -133,20 +147,9 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
verboseDeprecations: false
|
||||
};
|
||||
|
||||
let globalErrors = null;
|
||||
|
||||
function installGlobalErrors() {
|
||||
if (globalErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalErrors = new j$.GlobalErrors();
|
||||
globalErrors.install();
|
||||
}
|
||||
|
||||
if (!options.suppressLoadErrors) {
|
||||
installGlobalErrors();
|
||||
globalErrors.pushListener(function(
|
||||
globalErrors.pushListener(function loadtimeErrorHandler(
|
||||
message,
|
||||
filename,
|
||||
lineno,
|
||||
@@ -619,6 +622,47 @@ getJasmineRequireObj().Env = function(j$) {
|
||||
);
|
||||
};
|
||||
|
||||
this.spyOnGlobalErrorsAsync = async function(fn) {
|
||||
const spy = this.createSpy('global error handler');
|
||||
const associatedRunable = runner.currentRunable();
|
||||
let cleanedUp = false;
|
||||
|
||||
globalErrors.setOverrideListener(spy, () => {
|
||||
if (!cleanedUp) {
|
||||
const message =
|
||||
'Global error spy was not uninstalled. (Did you ' +
|
||||
'forget to await the return value of spyOnGlobalErrorsAsync?)';
|
||||
associatedRunable.addExpectationResult(false, {
|
||||
matcherName: '',
|
||||
passed: false,
|
||||
expected: '',
|
||||
actual: '',
|
||||
message,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
cleanedUp = true;
|
||||
});
|
||||
|
||||
try {
|
||||
const maybePromise = fn(spy);
|
||||
|
||||
if (!j$.isPromiseLike(maybePromise)) {
|
||||
throw new Error(
|
||||
'The callback to spyOnGlobalErrorsAsync must be an async or promise-returning function'
|
||||
);
|
||||
}
|
||||
|
||||
await maybePromise;
|
||||
} finally {
|
||||
if (!cleanedUp) {
|
||||
cleanedUp = true;
|
||||
globalErrors.removeOverrideListener();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function ensureIsNotNested(method) {
|
||||
const runable = runner.currentRunable();
|
||||
if (runable !== null && runable !== undefined) {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
getJasmineRequireObj().GlobalErrors = function(j$) {
|
||||
function GlobalErrors(global) {
|
||||
const handlers = [];
|
||||
global = global || j$.getGlobal();
|
||||
|
||||
const onerror = function onerror() {
|
||||
const handlers = [];
|
||||
let overrideHandler = null,
|
||||
onRemoveOverrideHandler = null;
|
||||
|
||||
function onerror(message, source, lineno, colno, error) {
|
||||
if (overrideHandler) {
|
||||
overrideHandler(error || message);
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = handlers[handlers.length - 1];
|
||||
|
||||
if (handler) {
|
||||
@@ -11,7 +19,7 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
|
||||
} else {
|
||||
throw arguments[0];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.originalHandlers = {};
|
||||
this.jasmineHandlers = {};
|
||||
@@ -42,6 +50,11 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
|
||||
|
||||
const handler = handlers[handlers.length - 1];
|
||||
|
||||
if (overrideHandler) {
|
||||
overrideHandler(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handler) {
|
||||
handler(error);
|
||||
} else {
|
||||
@@ -126,6 +139,24 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
|
||||
|
||||
handlers.pop();
|
||||
};
|
||||
|
||||
this.setOverrideListener = function(listener, onRemove) {
|
||||
if (overrideHandler) {
|
||||
throw new Error("Can't set more than one override listener at a time");
|
||||
}
|
||||
|
||||
overrideHandler = listener;
|
||||
onRemoveOverrideHandler = onRemove;
|
||||
};
|
||||
|
||||
this.removeOverrideListener = function() {
|
||||
if (onRemoveOverrideHandler) {
|
||||
onRemoveOverrideHandler();
|
||||
}
|
||||
|
||||
overrideHandler = null;
|
||||
onRemoveOverrideHandler = null;
|
||||
};
|
||||
}
|
||||
|
||||
return GlobalErrors;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
getJasmineRequireObj().RunableResources = function(j$) {
|
||||
class RunableResources {
|
||||
constructor(getCurrentRunableId) {
|
||||
constructor(options) {
|
||||
this.byRunableId_ = {};
|
||||
this.getCurrentRunableId_ = getCurrentRunableId;
|
||||
this.getCurrentRunableId_ = options.getCurrentRunableId;
|
||||
this.globalErrors_ = options.globalErrors;
|
||||
|
||||
this.spyFactory = new j$.SpyFactory(
|
||||
() => {
|
||||
@@ -53,6 +54,7 @@ getJasmineRequireObj().RunableResources = function(j$) {
|
||||
}
|
||||
|
||||
clearForRunable(runableId) {
|
||||
this.globalErrors_.removeOverrideListener();
|
||||
this.spyRegistry.clearSpies();
|
||||
delete this.byRunableId_[runableId];
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ getJasmineRequireObj().Spec = function(j$) {
|
||||
|
||||
Spec.prototype.addExpectationResult = function(passed, data, isError) {
|
||||
const expectationResult = j$.buildExpectationResult(data);
|
||||
|
||||
if (passed) {
|
||||
this.result.passedExpectations.push(expectationResult);
|
||||
} else {
|
||||
@@ -77,6 +78,11 @@ getJasmineRequireObj().Spec = function(j$) {
|
||||
this.onLateError(expectationResult);
|
||||
} else {
|
||||
this.result.failedExpectations.push(expectationResult);
|
||||
|
||||
// TODO: refactor so that we don't need to override cached status
|
||||
if (this.result.status) {
|
||||
this.result.status = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.throwOnExpectationFailure && !isError) {
|
||||
|
||||
@@ -227,6 +227,11 @@ getJasmineRequireObj().Suite = function(j$) {
|
||||
this.onLateError(expectationResult);
|
||||
} else {
|
||||
this.result.failedExpectations.push(expectationResult);
|
||||
|
||||
// TODO: refactor so that we don't need to override cached status
|
||||
if (this.result.status) {
|
||||
this.result.status = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.throwOnExpectationFailure) {
|
||||
|
||||
@@ -421,4 +421,47 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
|
||||
j$.debugLog = function(msg) {
|
||||
j$.getEnv().debugLog(msg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces Jasmine's global error handling with a spy. This prevents Jasmine
|
||||
* from treating uncaught exceptions and unhandled promise rejections
|
||||
* as spec failures and allows them to be inspected using the spy's
|
||||
* {@link Spy#calls|calls property} and related matchers such as
|
||||
* {@link matchers#toHaveBeenCalledWith|toHaveBeenCalledWith}.
|
||||
*
|
||||
* After installing the spy, spyOnGlobalErrorsAsync immediately calls its
|
||||
* argument, which must be an async or promise-returning function. The spy
|
||||
* will be passed as the first argument to that callback. Normal error
|
||||
* handling will be restored when the promise returned from the callback is
|
||||
* settled.
|
||||
*
|
||||
* Note: The JavaScript runtime may deliver uncaught error events and unhandled
|
||||
* rejection events asynchronously, especially in browsers. If the event
|
||||
* occurs after the promise returned from the callback is settled, it won't
|
||||
* be routed to the spy even if the underlying error occurred previously.
|
||||
* It's up to you to ensure that the returned promise isn't resolved until
|
||||
* all of the error/rejection events that you want to handle have occurred.
|
||||
*
|
||||
* You must await the return value of spyOnGlobalErrorsAsync.
|
||||
* @name jasmine.spyOnGlobalErrorsAsync
|
||||
* @function
|
||||
* @async
|
||||
* @param {AsyncFunction} fn - A function to run, during which the global error spy will be effective
|
||||
* @example
|
||||
* it('demonstrates global error spies', async function() {
|
||||
* await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
|
||||
* setTimeout(function() {
|
||||
* throw new Error('the expected error');
|
||||
* });
|
||||
* await new Promise(function(resolve) {
|
||||
* setTimeout(resolve);
|
||||
* });
|
||||
* const expected = new Error('the expected error');
|
||||
* expect(globalErrorSpy).toHaveBeenCalledWith(expected);
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
j$.spyOnGlobalErrorsAsync = async function(fn) {
|
||||
await jasmine.getEnv().spyOnGlobalErrorsAsync(fn);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user