Files
jasmine/src/core/QueueRunner.js
2022-02-21 16:45:34 -08:00

294 lines
8.4 KiB
JavaScript

getJasmineRequireObj().QueueRunner = function(j$) {
var nextid = 1;
function StopExecutionError() {}
StopExecutionError.prototype = new Error();
j$.StopExecutionError = StopExecutionError;
function once(fn, onTwice) {
var called = false;
return function(arg) {
if (called) {
if (onTwice) {
onTwice();
}
} else {
called = true;
// Direct call using single parameter, because cleanup/next does not need more
fn(arg);
}
return null;
};
}
function fallbackOnMultipleDone() {
console.error(
new Error(
"An asynchronous function called its 'done' " +
'callback more than once, in a QueueRunner without a onMultipleDone ' +
'handler.'
)
);
}
function emptyFn() {}
function QueueRunner(attrs) {
this.id_ = nextid++;
this.queueableFns = attrs.queueableFns || [];
this.onComplete = attrs.onComplete || emptyFn;
this.clearStack =
attrs.clearStack ||
function(fn) {
fn();
};
this.onException = attrs.onException || emptyFn;
this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone;
this.userContext = attrs.userContext || new j$.UserContext();
this.timeout = attrs.timeout || {
setTimeout: setTimeout,
clearTimeout: clearTimeout
};
this.fail = attrs.fail || emptyFn;
this.globalErrors = attrs.globalErrors || {
pushListener: emptyFn,
popListener: emptyFn
};
const SkipPolicy = attrs.SkipPolicy || j$.NeverSkipPolicy;
this.skipPolicy_ = new SkipPolicy(this.queueableFns);
this.errored_ = false;
if (typeof this.onComplete !== 'function') {
throw new Error('invalid onComplete ' + JSON.stringify(this.onComplete));
}
this.deprecated = attrs.deprecated;
}
QueueRunner.prototype.execute = function() {
var self = this;
this.handleFinalError = function(message, source, lineno, colno, error) {
// Older browsers would send the error as the first parameter. HTML5
// specifies the the five parameters above. The error instance should
// be preffered, otherwise the call stack would get lost.
self.onException(error || message);
};
this.globalErrors.pushListener(this.handleFinalError);
this.run(0);
};
QueueRunner.prototype.clearTimeout = function(timeoutId) {
Function.prototype.apply.apply(this.timeout.clearTimeout, [
j$.getGlobal(),
[timeoutId]
]);
};
QueueRunner.prototype.setTimeout = function(fn, timeout) {
return Function.prototype.apply.apply(this.timeout.setTimeout, [
j$.getGlobal(),
[fn, timeout]
]);
};
QueueRunner.prototype.attempt = function attempt(iterativeIndex) {
var self = this,
completedSynchronously = true,
handleError = function handleError(error) {
// TODO probably shouldn't next() right away here.
// That makes debugging async failures much more confusing.
onException(error);
},
cleanup = once(function cleanup() {
if (timeoutId !== void 0) {
self.clearTimeout(timeoutId);
}
self.globalErrors.popListener(handleError);
}),
next = once(
function next(err) {
cleanup();
if (typeof err !== 'undefined') {
if (!(err instanceof StopExecutionError) && !err.jasmineMessage) {
self.fail(err);
}
self.recordError_(iterativeIndex);
}
function runNext() {
self.run(self.nextFnIx_(iterativeIndex));
}
if (completedSynchronously) {
self.setTimeout(runNext);
} else {
runNext();
}
},
function() {
try {
if (!timedOut) {
self.onMultipleDone();
}
} catch (error) {
// Any error we catch here is probably due to a bug in Jasmine,
// and it's not likely to end up anywhere useful if we let it
// propagate. Log it so it can at least show up when debugging.
console.error(error);
}
}
),
timedOut = false,
queueableFn = self.queueableFns[iterativeIndex],
timeoutId,
maybeThenable;
next.fail = function nextFail() {
self.fail.apply(null, arguments);
self.recordError_(iterativeIndex);
next();
};
self.globalErrors.pushListener(handleError);
if (queueableFn.timeout !== undefined) {
var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL;
timeoutId = self.setTimeout(function() {
timedOut = true;
var error = new Error(
'Timeout - Async function did not complete within ' +
timeoutInterval +
'ms ' +
(queueableFn.timeout
? '(custom timeout)'
: '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)')
);
// TODO Need to decide what to do about a successful completion after a
// timeout. That should probably not be a deprecation, and maybe not
// an error in 4.0. (But a diagnostic of some sort might be helpful.)
onException(error);
next();
}, timeoutInterval);
}
try {
if (queueableFn.fn.length === 0) {
maybeThenable = queueableFn.fn.call(self.userContext);
if (maybeThenable && j$.isFunction_(maybeThenable.then)) {
maybeThenable.then(
wrapInPromiseResolutionHandler(next),
onPromiseRejection
);
completedSynchronously = false;
return { completedSynchronously: false };
}
} else {
maybeThenable = queueableFn.fn.call(self.userContext, next);
this.diagnoseConflictingAsync_(queueableFn.fn, maybeThenable);
completedSynchronously = false;
return { completedSynchronously: false };
}
} catch (e) {
onException(e);
self.recordError_(iterativeIndex);
}
cleanup();
return { completedSynchronously: true };
function onException(e) {
self.onException(e);
self.recordError_(iterativeIndex);
}
function onPromiseRejection(e) {
onException(e);
next();
}
};
QueueRunner.prototype.run = function(recursiveIndex) {
var length = this.queueableFns.length,
self = this,
iterativeIndex;
for (
iterativeIndex = recursiveIndex;
iterativeIndex < length;
iterativeIndex = this.nextFnIx_(iterativeIndex)
) {
var result = this.attempt(iterativeIndex);
if (!result.completedSynchronously) {
return;
}
}
this.clearStack(function() {
self.globalErrors.popListener(self.handleFinalError);
if (self.errored_) {
self.onComplete(new StopExecutionError());
} else {
self.onComplete();
}
});
};
QueueRunner.prototype.nextFnIx_ = function(currentFnIx) {
const result = this.skipPolicy_.skipTo(currentFnIx);
if (result === currentFnIx) {
throw new Error("Can't skip to the same queueable fn that just finished");
}
return result;
};
QueueRunner.prototype.recordError_ = function(currentFnIx) {
this.errored_ = true;
this.skipPolicy_.fnErrored(currentFnIx);
};
QueueRunner.prototype.diagnoseConflictingAsync_ = function(fn, retval) {
var msg;
if (retval && j$.isFunction_(retval.then)) {
// Issue a warning that matches the user's code.
// Omit the stack trace because there's almost certainly no user code
// on the stack at this point.
if (j$.isAsyncFunction_(fn)) {
this.onException(
'An asynchronous before/it/after ' +
'function was defined with the async keyword but also took a ' +
'done callback. Either remove the done callback (recommended) or ' +
'remove the async keyword.'
);
} else {
this.onException(
'An asynchronous before/it/after ' +
'function took a done callback but also returned a promise. ' +
'Either remove the done callback (recommended) or change the ' +
'function to not return a promise.'
);
}
this.deprecated(msg, { omitStackTrace: true });
}
};
function wrapInPromiseResolutionHandler(fn) {
return function(maybeArg) {
if (j$.isError_(maybeArg)) {
fn(maybeArg);
} else {
fn();
}
};
}
return QueueRunner;
};