nightwatch
Version:
Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.
190 lines (152 loc) • 4.61 kB
JavaScript
const EventEmitter = require('events');
const createPromise = require('./createPromise');
class PeriodicPromise extends EventEmitter {
get rescheduleInterval() {
return this.__rescheduleIntervalMs;
}
get ms() {
return this.__timeoutMs;
}
get elapsedTime() {
return new Date().getTime() - this.startTime;
}
get queue() {
return this.__queue;
}
constructor({
rescheduleInterval,
timeout
}) {
super();
this.__queue = [];
this.__rescheduleIntervalMs = rescheduleInterval;
this.__timeoutMs = timeout;
this.retries = 0;
}
queueAction(opts) {
this.__queue.push(opts);
return this;
}
async runAction({prevResult, prevQueuePromise}) {
if (!this.queue.length) {
return null;
}
const queuePromise = this.queue.shift();
const deferred = createPromise();
try {
const result = await this.perform(queuePromise, {prevResult, prevQueuePromise}, deferred);
if (this.queue.length) {
return await this.runAction({
prevResult: result,
prevQueuePromise: queuePromise
});
}
return result;
} catch (err) {
if (err.name === 'TypeError' || err.name === 'ReferenceError' || !queuePromise.errorHandler) {
throw err;
}
if (queuePromise.errorHandler) {
return queuePromise.errorHandler(err);
}
}
}
async perform({
action,
validate,
isResultStale,
successHandler = (r) => Promise.resolve(r),
shouldRetryOnError = () => true,
retryOnSuccess = false,
retryOnFailure = true
}, {prevResult, prevQueuePromise}, deferred) {
let currentResult;
try {
// running the current action using the result from the previous action
currentResult = await action(prevResult);
// if the current action returns a stale reference to the previous result, re-run the previous action
if (isResultStale && isResultStale(currentResult)) {
const freshResult = await prevQueuePromise.action({cacheElementId: false});
if (freshResult.error instanceof Error) {
throw freshResult.error;
}
currentResult = await action(freshResult);
}
} catch (err) {
currentResult = {
status: -1,
message: err.message,
stack: err.stack
};
}
const isValidResult = validate(currentResult);
if (this.shouldRetry({retryOnSuccess, isValidResult, currentResult, shouldRetryOnError, retryOnFailure})) {
this.reschedule(arguments[0], arguments[1], deferred);
return deferred.promise;
}
if (isValidResult) {
currentResult = await successHandler(currentResult);
deferred.resolve(currentResult);
return deferred.promise;
}
throw this.getError(currentResult);
}
run() {
this.startTime = new Date().getTime();
return this.runAction({});
}
reschedule(promise, opts, deferred) {
setTimeout(() => {
this.retries++;
this.perform(promise, opts, deferred).catch(err => {
deferred.reject(err);
});
}, this.rescheduleInterval);
}
shouldRetry({retryOnFailure, retryOnSuccess, shouldRetryOnError, isValidResult, currentResult}) {
const now = new Date().getTime();
const timePassed = (now - this.startTime) >= this.ms;
const retryOnFailureResult = typeof retryOnFailure == 'function' ? retryOnFailure() : retryOnFailure;
if (timePassed) {
return false;
}
if (retryOnFailureResult && !isValidResult) {
return shouldRetryOnError(currentResult);
}
const needsRetryOnSuccess = typeof retryOnSuccess == 'function' ? retryOnSuccess(currentResult) : retryOnSuccess;
return (needsRetryOnSuccess && isValidResult);
}
getError(response) {
if (response instanceof Error) {
return response;
}
const {startTime, retries} = this;
let {message = 'timeout error'} = response;
if (response.error) {
message = response.error;
if (response.value && response.value.message) {
message = response.value.message;
} else if (message.message) {
message = message.message;
}
}
return new TimeoutError({
message,
now: Date.now(),
startTime,
response,
retries
});
}
}
class TimeoutError extends Error {
constructor({message, now, response, startTime, retries}) {
super(message);
this.name = 'TimeoutError';
this.now = now;
this.response = response;
this.startTime = startTime;
this.retries = retries;
}
}
module.exports = PeriodicPromise;