aboutsummaryrefslogtreecommitdiff
path: root/node_modules/selenium-webdriver/lib/promise.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/selenium-webdriver/lib/promise.js')
-rw-r--r--node_modules/selenium-webdriver/lib/promise.js927
1 files changed, 588 insertions, 339 deletions
diff --git a/node_modules/selenium-webdriver/lib/promise.js b/node_modules/selenium-webdriver/lib/promise.js
index b98e7cf2e..32d0c98e6 100644
--- a/node_modules/selenium-webdriver/lib/promise.js
+++ b/node_modules/selenium-webdriver/lib/promise.js
@@ -17,6 +17,42 @@
/**
* @fileoverview
+ *
+ * > ### IMPORTANT NOTICE
+ * >
+ * > The promise manager contained in this module is in the process of being
+ * > phased out in favor of native JavaScript promises. This will be a long
+ * > process and will not be completed until there have been two major LTS Node
+ * > releases (approx. Node v10.0) that support
+ * > [async functions](https://tc39.github.io/ecmascript-asyncawait/).
+ * >
+ * > At this time, the promise manager can be disabled by setting an environment
+ * > variable, `SELENIUM_PROMISE_MANAGER=0`. In the absence of async functions,
+ * > users may use generators with the
+ * > {@link ./promise.consume promise.consume()} function to write "synchronous"
+ * > style tests:
+ * >
+ * > ```js
+ * > const {Builder, By, promise, until} = require('selenium-webdriver');
+ * >
+ * > let result = promise.consume(function* doGoogleSearch() {
+ * > let driver = new Builder().forBrowser('firefox').build();
+ * > yield driver.get('http://www.google.com/ncr');
+ * > yield driver.findElement(By.name('q')).sendKeys('webdriver');
+ * > yield driver.findElement(By.name('btnG')).click();
+ * > yield driver.wait(until.titleIs('webdriver - Google Search'), 1000);
+ * > yield driver.quit();
+ * > });
+ * >
+ * > result.then(_ => console.log('SUCCESS!'),
+ * > e => console.error('FAILURE: ' + e));
+ * > ```
+ * >
+ * > The motiviation behind this change and full deprecation plan are documented
+ * > in [issue 2969](https://github.com/SeleniumHQ/selenium/issues/2969).
+ * >
+ * >
+ *
* The promise module is centered around the {@linkplain ControlFlow}, a class
* that coordinates the execution of asynchronous tasks. The ControlFlow allows
* users to focus on the imperative commands for their script without worrying
@@ -592,6 +628,7 @@
'use strict';
+const error = require('./error');
const events = require('./events');
const logging = require('./logging');
@@ -643,7 +680,6 @@ function asyncRun(fn) {
});
}
-
/**
* @param {number} level What level of verbosity to log with.
* @param {(string|function(this: T): string)} loggable The message to log.
@@ -799,32 +835,56 @@ class MultipleUnhandledRejectionError extends Error {
* @const
*/
const IMPLEMENTED_BY_SYMBOL = Symbol('promise.Thenable');
+const CANCELLABLE_SYMBOL = Symbol('promise.CancellableThenable');
+
+
+/**
+ * @param {function(new: ?)} ctor
+ * @param {!Object} symbol
+ */
+function addMarkerSymbol(ctor, symbol) {
+ try {
+ ctor.prototype[symbol] = true;
+ } catch (ignored) {
+ // Property access denied?
+ }
+}
+
+
+/**
+ * @param {*} object
+ * @param {!Object} symbol
+ * @return {boolean}
+ */
+function hasMarkerSymbol(object, symbol) {
+ if (!object) {
+ return false;
+ }
+ try {
+ return !!object[symbol];
+ } catch (e) {
+ return false; // Property access seems to be forbidden.
+ }
+}
/**
* Thenable is a promise-like object with a {@code then} method which may be
* used to schedule callbacks on a promised value.
*
- * @interface
+ * @record
* @extends {IThenable<T>}
* @template T
*/
class Thenable {
/**
* Adds a property to a class prototype to allow runtime checks of whether
- * instances of that class implement the Thenable interface. This function
- * will also ensure the prototype's {@code then} function is exported from
- * compiled code.
+ * instances of that class implement the Thenable interface.
* @param {function(new: Thenable, ...?)} ctor The
* constructor whose prototype to modify.
*/
static addImplementation(ctor) {
- ctor.prototype['then'] = ctor.prototype.then;
- try {
- ctor.prototype[IMPLEMENTED_BY_SYMBOL] = true;
- } catch (ignored) {
- // Property access denied?
- }
+ addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL);
}
/**
@@ -835,30 +895,10 @@ class Thenable {
* interface.
*/
static isImplementation(object) {
- if (!object) {
- return false;
- }
- try {
- return !!object[IMPLEMENTED_BY_SYMBOL];
- } catch (e) {
- return false; // Property access seems to be forbidden.
- }
+ return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL);
}
/**
- * Cancels the computation of this promise's value, rejecting the promise in
- * the process. This method is a no-op if the promise has already been
- * resolved.
- *
- * @param {(string|Error)=} opt_reason The reason this promise is being
- * cancelled. This value will be wrapped in a {@link CancellationError}.
- */
- cancel(opt_reason) {}
-
- /** @return {boolean} Whether this promise's value is still being computed. */
- isPending() {}
-
- /**
* Registers listeners for when this instance is resolved.
*
* @param {?(function(T): (R|IThenable<R>))=} opt_callback The
@@ -867,8 +907,8 @@ class Thenable {
* @param {?(function(*): (R|IThenable<R>))=} opt_errback
* The function to call if this promise is rejected. The function should
* expect a single argument: the rejection reason.
- * @return {!ManagedPromise<R>} A new promise which will be
- * resolved with the result of the invoked callback.
+ * @return {!Thenable<R>} A new promise which will be resolved with the result
+ * of the invoked callback.
* @template R
*/
then(opt_callback, opt_errback) {}
@@ -892,49 +932,53 @@ class Thenable {
* @param {function(*): (R|IThenable<R>)} errback The
* function to call if this promise is rejected. The function should
* expect a single argument: the rejection reason.
- * @return {!ManagedPromise<R>} A new promise which will be
- * resolved with the result of the invoked callback.
+ * @return {!Thenable<R>} A new promise which will be resolved with the result
+ * of the invoked callback.
* @template R
*/
catch(errback) {}
+}
+
+/**
+ * Marker interface for objects that allow consumers to request the cancellation
+ * of a promies-based operation. A cancelled promise will be rejected with a
+ * {@link CancellationError}.
+ *
+ * This interface is considered package-private and should not be used outside
+ * of selenium-webdriver.
+ *
+ * @interface
+ * @extends {Thenable<T>}
+ * @template T
+ * @package
+ */
+class CancellableThenable {
/**
- * Registers a listener to invoke when this promise is resolved, regardless
- * of whether the promise's value was successfully computed. This function
- * is synonymous with the {@code finally} clause in a synchronous API:
- *
- * // Synchronous API:
- * try {
- * doSynchronousWork();
- * } finally {
- * cleanUp();
- * }
- *
- * // Asynchronous promise API:
- * doAsynchronousWork().finally(cleanUp);
- *
- * __Note:__ similar to the {@code finally} clause, if the registered
- * callback returns a rejected promise or throws an error, it will silently
- * replace the rejection error (if any) from this promise:
- *
- * try {
- * throw Error('one');
- * } finally {
- * throw Error('two'); // Hides Error: one
- * }
- *
- * promise.rejected(Error('one'))
- * .finally(function() {
- * throw Error('two'); // Hides Error: one
- * });
+ * @param {function(new: CancellableThenable, ...?)} ctor
+ */
+ static addImplementation(ctor) {
+ Thenable.addImplementation(ctor);
+ addMarkerSymbol(ctor, CANCELLABLE_SYMBOL);
+ }
+
+ /**
+ * @param {*} object
+ * @return {boolean}
+ */
+ static isImplementation(object) {
+ return hasMarkerSymbol(object, CANCELLABLE_SYMBOL);
+ }
+
+ /**
+ * Requests the cancellation of the computation of this promise's value,
+ * rejecting the promise in the process. This method is a no-op if the promise
+ * has already been resolved.
*
- * @param {function(): (R|IThenable<R>)} callback The function to call when
- * this promise is resolved.
- * @return {!ManagedPromise<R>} A promise that will be fulfilled
- * with the callback result.
- * @template R
+ * @param {(string|Error)=} opt_reason The reason this promise is being
+ * cancelled. This value will be wrapped in a {@link CancellationError}.
*/
- finally(callback) {}
+ cancel(opt_reason) {}
}
@@ -967,7 +1011,7 @@ const ON_CANCEL_HANDLER = new WeakMap;
* fulfilled or rejected state, at which point the promise is considered
* resolved.
*
- * @implements {Thenable<T>}
+ * @implements {CancellableThenable<T>}
* @template T
* @see http://promises-aplus.github.io/promises-spec/
*/
@@ -983,6 +1027,12 @@ class ManagedPromise {
* this instance was created under. Defaults to the currently active flow.
*/
constructor(resolver, opt_flow) {
+ if (!usePromiseManager()) {
+ throw TypeError(
+ 'Unable to create a managed promise instance: the promise manager has'
+ + ' been disabled by the SELENIUM_PROMISE_MANAGER environment'
+ + ' variable: ' + process.env['SELENIUM_PROMISE_MANAGER']);
+ }
getUid(this);
/** @private {!ControlFlow} */
@@ -1024,6 +1074,30 @@ class ManagedPromise {
}
}
+ /**
+ * Creates a promise that is immediately resolved with the given value.
+ *
+ * @param {T=} opt_value The value to resolve.
+ * @return {!ManagedPromise<T>} A promise resolved with the given value.
+ * @template T
+ */
+ static resolve(opt_value) {
+ if (opt_value instanceof ManagedPromise) {
+ return opt_value;
+ }
+ return new ManagedPromise(resolve => resolve(opt_value));
+ }
+
+ /**
+ * Creates a promise that is immediately rejected with the given reason.
+ *
+ * @param {*=} opt_reason The rejection reason.
+ * @return {!ManagedPromise<?>} A new rejected promise.
+ */
+ static reject(opt_reason) {
+ return new ManagedPromise((_, reject) => reject(opt_reason));
+ }
+
/** @override */
toString() {
return 'ManagedPromise::' + getUid(this) +
@@ -1176,7 +1250,7 @@ class ManagedPromise {
}
if (this.parent_ && canCancel(this.parent_)) {
- this.parent_.cancel(opt_reason);
+ /** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason);
} else {
var reason = CancellationError.wrap(opt_reason);
let onCancel = ON_CANCEL_HANDLER.get(this);
@@ -1194,7 +1268,7 @@ class ManagedPromise {
function canCancel(promise) {
if (!(promise instanceof ManagedPromise)) {
- return Thenable.isImplementation(promise);
+ return CancellableThenable.isImplementation(promise);
}
return promise.state_ === PromiseState.PENDING
|| promise.state_ === PromiseState.BLOCKED;
@@ -1202,11 +1276,6 @@ class ManagedPromise {
}
/** @override */
- isPending() {
- return this.state_ === PromiseState.PENDING;
- }
-
- /** @override */
then(opt_callback, opt_errback) {
return this.addCallback_(
opt_callback, opt_errback, 'then', ManagedPromise.prototype.then);
@@ -1218,21 +1287,15 @@ class ManagedPromise {
null, errback, 'catch', ManagedPromise.prototype.catch);
}
- /** @override */
+ /**
+ * @param {function(): (R|IThenable<R>)} callback
+ * @return {!ManagedPromise<R>}
+ * @template R
+ * @see ./promise.finally()
+ */
finally(callback) {
- var error;
- var mustThrow = false;
- return this.then(function() {
- return callback();
- }, function(err) {
- error = err;
- mustThrow = true;
- return callback();
- }).then(function() {
- if (mustThrow) {
- throw error;
- }
- });
+ let result = thenFinally(this, callback);
+ return /** @type {!ManagedPromise} */(result);
}
/**
@@ -1308,7 +1371,16 @@ class ManagedPromise {
}
}
}
-Thenable.addImplementation(ManagedPromise);
+CancellableThenable.addImplementation(ManagedPromise);
+
+
+/**
+ * @param {!ManagedPromise} promise
+ * @return {boolean}
+ */
+function isPending(promise) {
+ return promise.state_ === PromiseState.PENDING;
+}
/**
@@ -1405,19 +1477,11 @@ function isPromise(value) {
* Creates a promise that will be resolved at a set time in the future.
* @param {number} ms The amount of time, in milliseconds, to wait before
* resolving the promise.
- * @return {!ManagedPromise} The promise.
+ * @return {!Thenable} The promise.
*/
function delayed(ms) {
- var key;
- return new ManagedPromise(function(fulfill) {
- key = setTimeout(function() {
- key = null;
- fulfill();
- }, ms);
- }).catch(function(e) {
- clearTimeout(key);
- key = null;
- throw e;
+ return createPromise(resolve => {
+ setTimeout(() => resolve(), ms);
});
}
@@ -1436,15 +1500,11 @@ function defer() {
* Creates a promise that has been resolved with the given value.
* @param {T=} opt_value The resolved value.
* @return {!ManagedPromise<T>} The resolved promise.
+ * @deprecated Use {@link ManagedPromise#resolve Promise.resolve(value)}.
* @template T
*/
function fulfilled(opt_value) {
- if (opt_value instanceof ManagedPromise) {
- return opt_value;
- }
- return new ManagedPromise(function(fulfill) {
- fulfill(opt_value);
- });
+ return ManagedPromise.resolve(opt_value);
}
@@ -1452,16 +1512,11 @@ function fulfilled(opt_value) {
* Creates a promise that has been rejected with the given reason.
* @param {*=} opt_reason The rejection reason; may be any value, but is
* usually an Error or a string.
- * @return {!ManagedPromise<T>} The rejected promise.
- * @template T
+ * @return {!ManagedPromise<?>} The rejected promise.
+ * @deprecated Use {@link ManagedPromise#reject Promise.reject(reason)}.
*/
function rejected(opt_reason) {
- if (opt_reason instanceof ManagedPromise) {
- return opt_reason;
- }
- return new ManagedPromise(function(_, reject) {
- reject(opt_reason);
- });
+ return ManagedPromise.reject(opt_reason);
}
@@ -1474,12 +1529,12 @@ function rejected(opt_reason) {
* @param {!Function} fn The function to wrap.
* @param {...?} var_args The arguments to apply to the function, excluding the
* final callback.
- * @return {!ManagedPromise} A promise that will be resolved with the
+ * @return {!Thenable} A promise that will be resolved with the
* result of the provided function's callback.
*/
function checkedNodeCall(fn, var_args) {
let args = Array.prototype.slice.call(arguments, 1);
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
try {
args.push(function(error, value) {
error ? reject(error) : fulfill(value);
@@ -1491,6 +1546,59 @@ function checkedNodeCall(fn, var_args) {
});
}
+/**
+ * Registers a listener to invoke when a promise is resolved, regardless
+ * of whether the promise's value was successfully computed. This function
+ * is synonymous with the {@code finally} clause in a synchronous API:
+ *
+ * // Synchronous API:
+ * try {
+ * doSynchronousWork();
+ * } finally {
+ * cleanUp();
+ * }
+ *
+ * // Asynchronous promise API:
+ * doAsynchronousWork().finally(cleanUp);
+ *
+ * __Note:__ similar to the {@code finally} clause, if the registered
+ * callback returns a rejected promise or throws an error, it will silently
+ * replace the rejection error (if any) from this promise:
+ *
+ * try {
+ * throw Error('one');
+ * } finally {
+ * throw Error('two'); // Hides Error: one
+ * }
+ *
+ * let p = Promise.reject(Error('one'));
+ * promise.finally(p, function() {
+ * throw Error('two'); // Hides Error: one
+ * });
+ *
+ * @param {!IThenable<?>} promise The promise to add the listener to.
+ * @param {function(): (R|IThenable<R>)} callback The function to call when
+ * the promise is resolved.
+ * @return {!IThenable<R>} A promise that will be resolved with the callback
+ * result.
+ * @template R
+ */
+function thenFinally(promise, callback) {
+ let error;
+ let mustThrow = false;
+ return promise.then(function() {
+ return callback();
+ }, function(err) {
+ error = err;
+ mustThrow = true;
+ return callback();
+ }).then(function() {
+ if (mustThrow) {
+ throw error;
+ }
+ });
+}
+
/**
* Registers an observer on a promised {@code value}, returning a new promise
@@ -1501,16 +1609,15 @@ function checkedNodeCall(fn, var_args) {
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
- * @return {!ManagedPromise} A new promise.
+ * @return {!Thenable} A new promise.
*/
function when(value, opt_callback, opt_errback) {
if (Thenable.isImplementation(value)) {
return value.then(opt_callback, opt_errback);
}
- return new ManagedPromise(function(fulfill) {
- fulfill(value);
- }).then(opt_callback, opt_errback);
+ return createPromise(resolve => resolve(value))
+ .then(opt_callback, opt_errback);
}
@@ -1542,14 +1649,14 @@ function asap(value, callback, opt_errback) {
*
* @param {!Array<(T|!ManagedPromise<T>)>} arr An array of
* promises to wait on.
- * @return {!ManagedPromise<!Array<T>>} A promise that is
+ * @return {!Thenable<!Array<T>>} A promise that is
* fulfilled with an array containing the fulfilled values of the
* input array, or rejected with the same reason as the first
* rejected value.
* @template T
*/
function all(arr) {
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = [];
@@ -1603,12 +1710,12 @@ function all(arr) {
* @template TYPE, SELF
*/
function map(arr, fn, opt_self) {
- return fulfilled(arr).then(function(v) {
+ return createPromise(resolve => resolve(arr)).then(v => {
if (!Array.isArray(v)) {
throw TypeError('not an array');
}
var arr = /** @type {!Array} */(v);
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = new Array(n);
(function processNext(i) {
@@ -1661,12 +1768,12 @@ function map(arr, fn, opt_self) {
* @template TYPE, SELF
*/
function filter(arr, fn, opt_self) {
- return fulfilled(arr).then(function(v) {
+ return createPromise(resolve => resolve(arr)).then(v => {
if (!Array.isArray(v)) {
throw TypeError('not an array');
}
var arr = /** @type {!Array} */(v);
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = [];
var valuesLength = 0;
@@ -1714,7 +1821,7 @@ function filter(arr, fn, opt_self) {
* promise.fullyResolved(value); // Stack overflow.
*
* @param {*} value The value to fully resolve.
- * @return {!ManagedPromise} A promise for a fully resolved version
+ * @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
function fullyResolved(value) {
@@ -1728,7 +1835,7 @@ function fullyResolved(value) {
/**
* @param {*} value The value to fully resolve. If a promise, assumed to
* already be resolved.
- * @return {!ManagedPromise} A promise for a fully resolved version
+ * @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
function fullyResolveValue(value) {
@@ -1755,13 +1862,13 @@ function fullyResolveValue(value) {
return fullyResolveKeys(/** @type {!Object} */ (value));
}
- return fulfilled(value);
+ return createPromise(resolve => resolve(value));
}
/**
* @param {!(Array|Object)} obj the object to resolve.
- * @return {!ManagedPromise} A promise that will be resolved with the
+ * @return {!Thenable} A promise that will be resolved with the
* input object once all of its values have been fully resolved.
*/
function fullyResolveKeys(obj) {
@@ -1773,8 +1880,9 @@ function fullyResolveKeys(obj) {
}
return n;
})();
+
if (!numKeys) {
- return fulfilled(obj);
+ return createPromise(resolve => resolve(obj));
}
function forEachProperty(obj, fn) {
@@ -1788,7 +1896,7 @@ function fullyResolveKeys(obj) {
}
var numResolved = 0;
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var forEachKey = isArray ? forEachElement: forEachProperty;
forEachKey(obj, function(partialValue, key) {
@@ -1822,6 +1930,236 @@ function fullyResolveKeys(obj) {
//////////////////////////////////////////////////////////////////////////////
+/**
+ * Defines methods for coordinating the execution of asynchronous tasks.
+ * @record
+ */
+class Scheduler {
+ /**
+ * Schedules a task for execution. If the task function is a generator, the
+ * task will be executed using {@link ./promise.consume consume()}.
+ *
+ * @param {function(): (T|IThenable<T>)} fn The function to call to start the
+ * task.
+ * @param {string=} opt_description A description of the task for debugging
+ * purposes.
+ * @return {!Thenable<T>} A promise that will be resolved with the task
+ * result.
+ * @template T
+ */
+ execute(fn, opt_description) {}
+
+ /**
+ * Creates a new promise using the given resolver function.
+ *
+ * @param {function(
+ * function((T|IThenable<T>|Thenable|null)=),
+ * function(*=))} resolver
+ * @return {!Thenable<T>}
+ * @template T
+ */
+ promise(resolver) {}
+
+ /**
+ * Schedules a `setTimeout` call.
+ *
+ * @param {number} ms The timeout delay, in milliseconds.
+ * @param {string=} opt_description A description to accompany the timeout.
+ * @return {!Thenable<void>} A promise that will be resolved when the timeout
+ * fires.
+ */
+ timeout(ms, opt_description) {}
+
+ /**
+ * Schedules a task to wait for a condition to hold.
+ *
+ * If the condition is defined as a function, it may return any value. Promies
+ * will be resolved before testing if the condition holds (resolution time
+ * counts towards the timeout). Once resolved, values are always evaluated as
+ * booleans.
+ *
+ * If the condition function throws, or returns a rejected promise, the
+ * wait task will fail.
+ *
+ * If the condition is defined as a promise, the scheduler will wait for it to
+ * settle. If the timeout expires before the promise settles, the promise
+ * returned by this function will be rejected.
+ *
+ * If this function is invoked with `timeout === 0`, or the timeout is
+ * omitted, this scheduler will wait indefinitely for the condition to be
+ * satisfied.
+ *
+ * @param {(!IThenable<T>|function())} condition The condition to poll,
+ * or a promise to wait on.
+ * @param {number=} opt_timeout How long to wait, in milliseconds, for the
+ * condition to hold before timing out. If omitted, the flow will wait
+ * indefinitely.
+ * @param {string=} opt_message An optional error message to include if the
+ * wait times out; defaults to the empty string.
+ * @return {!Thenable<T>} A promise that will be fulfilled
+ * when the condition has been satisified. The promise shall be rejected
+ * if the wait times out waiting for the condition.
+ * @throws {TypeError} If condition is not a function or promise or if timeout
+ * is not a number >= 0.
+ * @template T
+ */
+ wait(condition, opt_timeout, opt_message) {}
+}
+
+
+let USE_PROMISE_MANAGER;
+function usePromiseManager() {
+ if (typeof USE_PROMISE_MANAGER !== 'undefined') {
+ return !!USE_PROMISE_MANAGER;
+ }
+ return process.env['SELENIUM_PROMISE_MANAGER'] === undefined
+ || !/^0|false$/i.test(process.env['SELENIUM_PROMISE_MANAGER']);
+}
+
+
+/**
+ * @param {function(
+ * function((T|IThenable<T>|Thenable|null)=),
+ * function(*=))} resolver
+ * @return {!Thenable<T>}
+ * @template T
+ */
+function createPromise(resolver) {
+ let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
+ return new ctor(resolver);
+}
+
+
+/**
+ * @param {!Scheduler} scheduler The scheduler to use.
+ * @param {(!IThenable<T>|function())} condition The condition to poll,
+ * or a promise to wait on.
+ * @param {number=} opt_timeout How long to wait, in milliseconds, for the
+ * condition to hold before timing out. If omitted, the flow will wait
+ * indefinitely.
+ * @param {string=} opt_message An optional error message to include if the
+ * wait times out; defaults to the empty string.
+ * @return {!Thenable<T>} A promise that will be fulfilled
+ * when the condition has been satisified. The promise shall be rejected
+ * if the wait times out waiting for the condition.
+ * @throws {TypeError} If condition is not a function or promise or if timeout
+ * is not a number >= 0.
+ * @template T
+ */
+function scheduleWait(scheduler, condition, opt_timeout, opt_message) {
+ let timeout = opt_timeout || 0;
+ if (typeof timeout !== 'number' || timeout < 0) {
+ throw TypeError('timeout must be a number >= 0: ' + timeout);
+ }
+
+ if (isPromise(condition)) {
+ return scheduler.execute(function() {
+ if (!timeout) {
+ return condition;
+ }
+ return scheduler.promise(function(fulfill, reject) {
+ let start = Date.now();
+ let timer = setTimeout(function() {
+ timer = null;
+ reject(
+ new error.TimeoutError(
+ (opt_message ? opt_message + '\n' : '')
+ + 'Timed out waiting for promise to resolve after '
+ + (Date.now() - start) + 'ms'));
+ }, timeout);
+
+ /** @type {Thenable} */(condition).then(
+ function(value) {
+ timer && clearTimeout(timer);
+ fulfill(value);
+ },
+ function(error) {
+ timer && clearTimeout(timer);
+ reject(error);
+ });
+ });
+ }, opt_message || '<anonymous wait: promise resolution>');
+ }
+
+ if (typeof condition !== 'function') {
+ throw TypeError('Invalid condition; must be a function or promise: ' +
+ typeof condition);
+ }
+
+ if (isGenerator(condition)) {
+ let original = condition;
+ condition = () => consume(original);
+ }
+
+ return scheduler.execute(function() {
+ var startTime = Date.now();
+ return scheduler.promise(function(fulfill, reject) {
+ pollCondition();
+
+ function pollCondition() {
+ var conditionFn = /** @type {function()} */(condition);
+ scheduler.execute(conditionFn).then(function(value) {
+ var elapsed = Date.now() - startTime;
+ if (!!value) {
+ fulfill(value);
+ } else if (timeout && elapsed >= timeout) {
+ reject(
+ new error.TimeoutError(
+ (opt_message ? opt_message + '\n' : '')
+ + `Wait timed out after ${elapsed}ms`));
+ } else {
+ // Do not use asyncRun here because we need a non-micro yield
+ // here so the UI thread is given a chance when running in a
+ // browser.
+ setTimeout(pollCondition, 0);
+ }
+ }, reject);
+ }
+ });
+ }, opt_message || '<anonymous wait>');
+}
+
+
+/**
+ * A scheduler that executes all tasks immediately, with no coordination. This
+ * class is an event emitter for API compatibility with the {@link ControlFlow},
+ * however, it emits no events.
+ *
+ * @implements {Scheduler}
+ */
+class SimpleScheduler extends events.EventEmitter {
+ /** @override */
+ execute(fn) {
+ return this.promise((resolve, reject) => {
+ try {
+ if (isGenerator(fn)) {
+ consume(fn).then(resolve, reject);
+ } else {
+ resolve(fn.call(undefined));
+ }
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ /** @override */
+ promise(resolver) {
+ return new NativePromise(resolver);
+ }
+
+ /** @override */
+ timeout(ms) {
+ return this.promise(resolve => setTimeout(_ => resolve(), ms));
+ }
+
+ /** @override */
+ wait(condition, opt_timeout, opt_message) {
+ return scheduleWait(this, condition, opt_timeout, opt_message);
+ }
+}
+const SIMPLE_SCHEDULER = new SimpleScheduler;
+
/**
* Handles the execution of scheduled tasks, each of which may be an
@@ -1848,13 +2186,20 @@ function fullyResolveKeys(obj) {
* If there are no listeners registered with the flow, the error will be
* rethrown to the global error handler.
*
- * Refer to the {@link ./promise} module documentation fora detailed
+ * Refer to the {@link ./promise} module documentation for a detailed
* explanation of how the ControlFlow coordinates task execution.
*
+ * @implements {Scheduler}
* @final
*/
class ControlFlow extends events.EventEmitter {
constructor() {
+ if (!usePromiseManager()) {
+ throw TypeError(
+ 'Cannot instantiate control flow when the promise manager has'
+ + ' been disabled');
+ }
+
super();
/** @private {boolean} */
@@ -2024,21 +2369,7 @@ class ControlFlow extends events.EventEmitter {
return this.activeQueue_;
}
- /**
- * Schedules a task for execution. If there is nothing currently in the
- * queue, the task will be executed in the next turn of the event loop. If
- * the task function is a generator, the task will be executed using
- * {@link ./promise.consume consume()}.
- *
- * @param {function(): (T|ManagedPromise<T>)} fn The function to
- * call to start the task. If the function returns a
- * {@link ManagedPromise}, this instance will wait for it to be
- * resolved before starting the next task.
- * @param {string=} opt_description A description of the task.
- * @return {!ManagedPromise<T>} A promise that will be resolved
- * with the result of the action.
- * @template T
- */
+ /** @override */
execute(fn, opt_description) {
if (isGenerator(fn)) {
let original = fn;
@@ -2060,126 +2391,21 @@ class ControlFlow extends events.EventEmitter {
return task.promise;
}
- /**
- * Inserts a {@code setTimeout} into the command queue. This is equivalent to
- * a thread sleep in a synchronous programming language.
- *
- * @param {number} ms The timeout delay, in milliseconds.
- * @param {string=} opt_description A description to accompany the timeout.
- * @return {!ManagedPromise} A promise that will be resolved with
- * the result of the action.
- */
+ /** @override */
+ promise(resolver) {
+ return new ManagedPromise(resolver, this);
+ }
+
+ /** @override */
timeout(ms, opt_description) {
- return this.execute(function() {
- return delayed(ms);
+ return this.execute(() => {
+ return this.promise(resolve => setTimeout(() => resolve(), ms));
}, opt_description);
}
- /**
- * Schedules a task that shall wait for a condition to hold. Each condition
- * function may return any value, but it will always be evaluated as a
- * boolean.
- *
- * Condition functions may schedule sub-tasks with this instance, however,
- * their execution time will be factored into whether a wait has timed out.
- *
- * In the event a condition returns a ManagedPromise, the polling loop will wait for
- * it to be resolved before evaluating whether the condition has been
- * satisfied. The resolution time for a promise is factored into whether a
- * wait has timed out.
- *
- * If the condition function throws, or returns a rejected promise, the
- * wait task will fail.
- *
- * If the condition is defined as a promise, the flow will wait for it to
- * settle. If the timeout expires before the promise settles, the promise
- * returned by this function will be rejected.
- *
- * If this function is invoked with `timeout === 0`, or the timeout is
- * omitted, the flow will wait indefinitely for the condition to be satisfied.
- *
- * @param {(!ManagedPromise<T>|function())} condition The condition to poll,
- * or a promise to wait on.
- * @param {number=} opt_timeout How long to wait, in milliseconds, for the
- * condition to hold before timing out. If omitted, the flow will wait
- * indefinitely.
- * @param {string=} opt_message An optional error message to include if the
- * wait times out; defaults to the empty string.
- * @return {!ManagedPromise<T>} A promise that will be fulfilled
- * when the condition has been satisified. The promise shall be rejected
- * if the wait times out waiting for the condition.
- * @throws {TypeError} If condition is not a function or promise or if timeout
- * is not a number >= 0.
- * @template T
- */
+ /** @override */
wait(condition, opt_timeout, opt_message) {
- var timeout = opt_timeout || 0;
- if (typeof timeout !== 'number' || timeout < 0) {
- throw TypeError('timeout must be a number >= 0: ' + timeout);
- }
-
- if (isPromise(condition)) {
- return this.execute(function() {
- if (!timeout) {
- return condition;
- }
- return new ManagedPromise(function(fulfill, reject) {
- var start = Date.now();
- var timer = setTimeout(function() {
- timer = null;
- reject(Error((opt_message ? opt_message + '\n' : '') +
- 'Timed out waiting for promise to resolve after ' +
- (Date.now() - start) + 'ms'));
- }, timeout);
-
- /** @type {Thenable} */(condition).then(
- function(value) {
- timer && clearTimeout(timer);
- fulfill(value);
- },
- function(error) {
- timer && clearTimeout(timer);
- reject(error);
- });
- });
- }, opt_message || '<anonymous wait: promise resolution>');
- }
-
- if (typeof condition !== 'function') {
- throw TypeError('Invalid condition; must be a function or promise: ' +
- typeof condition);
- }
-
- if (isGenerator(condition)) {
- let original = condition;
- condition = () => consume(original);
- }
-
- var self = this;
- return this.execute(function() {
- var startTime = Date.now();
- return new ManagedPromise(function(fulfill, reject) {
- pollCondition();
-
- function pollCondition() {
- var conditionFn = /** @type {function()} */(condition);
- self.execute(conditionFn).then(function(value) {
- var elapsed = Date.now() - startTime;
- if (!!value) {
- fulfill(value);
- } else if (timeout && elapsed >= timeout) {
- reject(new Error((opt_message ? opt_message + '\n' : '') +
- 'Wait timed out after ' + elapsed + 'ms'));
- } else {
- // Do not use asyncRun here because we need a non-micro yield
- // here so the UI thread is given a chance when running in a
- // browser.
- setTimeout(pollCondition, 0);
- }
- }, reject);
- }
- });
- }, opt_message || '<anonymous wait>');
+ return scheduleWait(this, condition, opt_timeout, opt_message);
}
/**
@@ -2510,6 +2736,9 @@ class TaskQueue extends events.EventEmitter {
/** @private {({task: !Task, q: !TaskQueue}|null)} */
this.pending_ = null;
+ /** @private {TaskQueue} */
+ this.subQ_ = null;
+
/** @private {TaskQueueState} */
this.state_ = TaskQueueState.NEW;
@@ -2690,7 +2919,7 @@ class TaskQueue extends events.EventEmitter {
var task;
do {
task = this.getNextTask_();
- } while (task && !task.promise.isPending());
+ } while (task && !isPending(task.promise));
if (!task) {
this.state_ = TaskQueueState.FINISHED;
@@ -2701,20 +2930,30 @@ class TaskQueue extends events.EventEmitter {
return;
}
- var self = this;
- var subQ = new TaskQueue(this.flow_);
- subQ.once('end', () => self.onTaskComplete_(result))
- .once('error', (e) => self.onTaskFailure_(result, e));
- vlog(2, () => self + ' created ' + subQ + ' for ' + task);
+ let result = undefined;
+ this.subQ_ = new TaskQueue(this.flow_);
+
+ this.subQ_.once('end', () => { // On task completion.
+ this.subQ_ = null;
+ this.pending_ && this.pending_.task.fulfill(result);
+ });
+
+ this.subQ_.once('error', e => { // On task failure.
+ this.subQ_ = null;
+ if (Thenable.isImplementation(result)) {
+ result.cancel(CancellationError.wrap(e));
+ }
+ this.pending_ && this.pending_.task.reject(e);
+ });
+ vlog(2, () => `${this} created ${this.subQ_} for ${task}`);
- var result = undefined;
try {
- this.pending_ = {task: task, q: subQ};
+ this.pending_ = {task: task, q: this.subQ_};
task.promise.queue_ = this;
- result = subQ.execute_(task.execute);
- subQ.start();
+ result = this.subQ_.execute_(task.execute);
+ this.subQ_.start();
} catch (ex) {
- subQ.abort_(ex);
+ this.subQ_.abort_(ex);
}
}
@@ -2805,28 +3044,6 @@ class TaskQueue extends events.EventEmitter {
}
/**
- * @param {*} value the value originally returned by the task function.
- * @private
- */
- onTaskComplete_(value) {
- if (this.pending_) {
- this.pending_.task.fulfill(value);
- }
- }
-
- /**
- * @param {*} taskFnResult the value originally returned by the task function.
- * @param {*} error the error that caused the task function to terminate.
- * @private
- */
- onTaskFailure_(taskFnResult, error) {
- if (Thenable.isImplementation(taskFnResult)) {
- taskFnResult.cancel(CancellationError.wrap(error));
- }
- this.pending_.task.reject(error);
- }
-
- /**
* @return {(Task|undefined)} the next task scheduled within this queue,
* if any.
* @private
@@ -2857,9 +3074,9 @@ class TaskQueue extends events.EventEmitter {
/**
* The default flow to use if no others are active.
- * @type {!ControlFlow}
+ * @type {ControlFlow}
*/
-var defaultFlow = new ControlFlow();
+var defaultFlow;
/**
@@ -2878,6 +3095,11 @@ var activeFlows = [];
* @throws {Error} If the default flow is not currently active.
*/
function setDefaultFlow(flow) {
+ if (!usePromiseManager()) {
+ throw Error(
+ 'You may not change set the control flow when the promise'
+ +' manager is disabled');
+ }
if (activeFlows.length) {
throw Error('You may only change the default flow while it is active');
}
@@ -2887,10 +3109,21 @@ function setDefaultFlow(flow) {
/**
* @return {!ControlFlow} The currently active control flow.
+ * @suppress {checkTypes}
*/
function controlFlow() {
- return /** @type {!ControlFlow} */ (
- activeFlows.length ? activeFlows[activeFlows.length - 1] : defaultFlow);
+ if (!usePromiseManager()) {
+ return SIMPLE_SCHEDULER;
+ }
+
+ if (activeFlows.length) {
+ return activeFlows[activeFlows.length - 1];
+ }
+
+ if (!defaultFlow) {
+ defaultFlow = new ControlFlow;
+ }
+ return defaultFlow;
}
@@ -2900,8 +3133,7 @@ function controlFlow() {
* a promise that resolves to the callback result.
* @param {function(!ControlFlow)} callback The entry point
* to the newly created flow.
- * @return {!ManagedPromise} A promise that resolves to the callback
- * result.
+ * @return {!Thenable} A promise that resolves to the callback result.
*/
function createFlow(callback) {
var flow = new ControlFlow;
@@ -2955,53 +3187,51 @@ function isGenerator(fn) {
* @param {Object=} opt_self The object to use as "this" when invoking the
* initial generator.
* @param {...*} var_args Any arguments to pass to the initial generator.
- * @return {!ManagedPromise<?>} A promise that will resolve to the
+ * @return {!Thenable<?>} A promise that will resolve to the
* generator's final result.
* @throws {TypeError} If the given function is not a generator.
*/
-function consume(generatorFn, opt_self, var_args) {
+function consume(generatorFn, opt_self, ...var_args) {
if (!isGenerator(generatorFn)) {
throw new TypeError('Input is not a GeneratorFunction: ' +
generatorFn.constructor.name);
}
- var deferred = defer();
- var generator = generatorFn.apply(
- opt_self, Array.prototype.slice.call(arguments, 2));
- callNext();
- return deferred.promise;
-
- /** @param {*=} opt_value . */
- function callNext(opt_value) {
- pump(generator.next, opt_value);
- }
-
- /** @param {*=} opt_error . */
- function callThrow(opt_error) {
- // Dictionary lookup required because Closure compiler's built-in
- // externs does not include GeneratorFunction.prototype.throw.
- pump(generator['throw'], opt_error);
- }
+ let ret;
+ return ret = createPromise((resolve, reject) => {
+ let generator = generatorFn.apply(opt_self, var_args);
+ callNext();
- function pump(fn, opt_arg) {
- if (!deferred.promise.isPending()) {
- return; // Defererd was cancelled; silently abort.
+ /** @param {*=} opt_value . */
+ function callNext(opt_value) {
+ pump(generator.next, opt_value);
}
- try {
- var result = fn.call(generator, opt_arg);
- } catch (ex) {
- deferred.reject(ex);
- return;
+ /** @param {*=} opt_error . */
+ function callThrow(opt_error) {
+ pump(generator.throw, opt_error);
}
- if (result.done) {
- deferred.fulfill(result.value);
- return;
- }
+ function pump(fn, opt_arg) {
+ if (ret instanceof ManagedPromise && !isPending(ret)) {
+ return; // Defererd was cancelled; silently abort.
+ }
- asap(result.value, callNext, callThrow);
- }
+ try {
+ var result = fn.call(generator, opt_arg);
+ } catch (ex) {
+ reject(ex);
+ return;
+ }
+
+ if (result.done) {
+ resolve(result.value);
+ return;
+ }
+
+ asap(result.value, callNext, callThrow);
+ }
+ });
}
@@ -3009,12 +3239,14 @@ function consume(generatorFn, opt_self, var_args) {
module.exports = {
+ CancellableThenable: CancellableThenable,
CancellationError: CancellationError,
ControlFlow: ControlFlow,
Deferred: Deferred,
MultipleUnhandledRejectionError: MultipleUnhandledRejectionError,
Thenable: Thenable,
Promise: ManagedPromise,
+ Scheduler: Scheduler,
all: all,
asap: asap,
captureStackTrace: captureStackTrace,
@@ -3025,6 +3257,7 @@ module.exports = {
defer: defer,
delayed: delayed,
filter: filter,
+ finally: thenFinally,
fulfilled: fulfilled,
fullyResolved: fullyResolved,
isGenerator: isGenerator,
@@ -3034,6 +3267,22 @@ module.exports = {
setDefaultFlow: setDefaultFlow,
when: when,
+ /**
+ * Indicates whether the promise manager is currently enabled. When disabled,
+ * attempting to use the {@link ControlFlow} or {@link ManagedPromise Promise}
+ * classes will generate an error.
+ *
+ * The promise manager is currently enabled by default, but may be disabled
+ * by setting the environment variable `SELENIUM_PROMISE_MANAGER=0` or by
+ * setting this property to false. Setting this property will always take
+ * precedence ove the use of the environment variable.
+ *
+ * @return {boolean} Whether the promise manager is enabled.
+ * @see <https://github.com/SeleniumHQ/selenium/issues/2969>
+ */
+ get USE_PROMISE_MANAGER() { return usePromiseManager(); },
+ set USE_PROMISE_MANAGER(/** boolean */value) { USE_PROMISE_MANAGER = value; },
+
get LONG_STACK_TRACES() { return LONG_STACK_TRACES; },
set LONG_STACK_TRACES(v) { LONG_STACK_TRACES = v; },
};