selenium-webdriver
Version:
The official WebDriver JavaScript bindings from the Selenium project
1,645 lines (1,514 loc) • 98.3 kB
JavaScript
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @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
* about chaining together every single asynchronous action, which can be
* tedious and verbose. APIs may be layered on top of the control flow to read
* as if they were synchronous. For instance, the core
* {@linkplain ./webdriver.WebDriver WebDriver} API is built on top of the
* control flow, allowing users to write
*
* driver.get('http://www.google.com/ncr');
* driver.findElement({name: 'q'}).sendKeys('webdriver');
* driver.findElement({name: 'btnGn'}).click();
*
* instead of
*
* driver.get('http://www.google.com/ncr')
* .then(function() {
* return driver.findElement({name: 'q'});
* })
* .then(function(q) {
* return q.sendKeys('webdriver');
* })
* .then(function() {
* return driver.findElement({name: 'btnG'});
* })
* .then(function(btnG) {
* return btnG.click();
* });
*
* ## Tasks and Task Queues
*
* The control flow is based on the concept of tasks and task queues. Tasks are
* functions that define the basic unit of work for the control flow to execute.
* Each task is scheduled via {@link ControlFlow#execute()}, which will return
* a {@link ManagedPromise ManagedPromise} that will be resolved with the task's
* result.
*
* A task queue contains all of the tasks scheduled within a single turn of the
* [JavaScript event loop][JSEL]. The control flow will create a new task queue
* the first time a task is scheduled within an event loop.
*
* var flow = promise.controlFlow();
* flow.execute(foo); // Creates a new task queue and inserts foo.
* flow.execute(bar); // Inserts bar into the same queue as foo.
* setTimeout(function() {
* flow.execute(baz); // Creates a new task queue and inserts baz.
* }, 0);
*
* Whenever the control flow creates a new task queue, it will automatically
* begin executing tasks in the next available turn of the event loop. This
* execution is scheduled using a "micro-task" timer, such as a (native)
* `ManagedPromise.then()` callback.
*
* setTimeout(() => console.log('a'));
* ManagedPromise.resolve().then(() => console.log('b')); // A native promise.
* flow.execute(() => console.log('c'));
* ManagedPromise.resolve().then(() => console.log('d'));
* setTimeout(() => console.log('fin'));
* // b
* // c
* // d
* // a
* // fin
*
* In the example above, b/c/d is logged before a/fin because native promises
* and this module use "micro-task" timers, which have a higher priority than
* "macro-tasks" like `setTimeout`.
*
* ## Task Execution
*
* Upon creating a task queue, and whenever an exisiting queue completes a task,
* the control flow will schedule a micro-task timer to process any scheduled
* tasks. This ensures no task is ever started within the same turn of the
* JavaScript event loop in which it was scheduled, nor is a task ever started
* within the same turn that another finishes.
*
* When the execution timer fires, a single task will be dequeued and executed.
* There are several important events that may occur while executing a task
* function:
*
* 1. A new task queue is created by a call to {@link ControlFlow#execute()}.
* Any tasks scheduled within this task queue are considered subtasks of the
* current task.
* 2. The task function throws an error. Any scheduled tasks are immediately
* discarded and the task's promised result (previously returned by
* {@link ControlFlow#execute()}) is immediately rejected with the thrown
* error.
* 3. The task function returns sucessfully.
*
* If a task function created a new task queue, the control flow will wait for
* that queue to complete before processing the task result. If the queue
* completes without error, the flow will settle the task's promise with the
* value originaly returned by the task function. On the other hand, if the task
* queue termintes with an error, the task's promise will be rejected with that
* error.
*
* flow.execute(function() {
* flow.execute(() => console.log('a'));
* flow.execute(() => console.log('b'));
* });
* flow.execute(() => console.log('c'));
* // a
* // b
* // c
*
* ## ManagedPromise Integration
*
* In addition to the {@link ControlFlow} class, the promise module also exports
* a [ManagedPromise/A+] {@linkplain ManagedPromise implementation} that is deeply
* integrated with the ControlFlow. First and foremost, each promise
* {@linkplain ManagedPromise#then() callback} is scheduled with the
* control flow as a task. As a result, each callback is invoked in its own turn
* of the JavaScript event loop with its own task queue. If any tasks are
* scheduled within a callback, the callback's promised result will not be
* settled until the task queue has completed.
*
* promise.fulfilled().then(function() {
* flow.execute(function() {
* console.log('b');
* });
* }).then(() => console.log('a'));
* // b
* // a
*
* ### Scheduling ManagedPromise Callbacks <a id="scheduling_callbacks"></a>
*
* How callbacks are scheduled in the control flow depends on when they are
* attached to the promise. Callbacks attached to a _previously_ resolved
* promise are immediately enqueued as subtasks of the currently running task.
*
* var p = promise.fulfilled();
* flow.execute(function() {
* flow.execute(() => console.log('A'));
* p.then( () => console.log('B'));
* flow.execute(() => console.log('C'));
* p.then( () => console.log('D'));
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* When a promise is resolved while a task function is on the call stack, any
* callbacks also registered in that stack frame are scheduled as if the promise
* were already resolved:
*
* var d = promise.defer();
* flow.execute(function() {
* flow.execute( () => console.log('A'));
* d.promise.then(() => console.log('B'));
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* Callbacks attached to an _unresolved_ promise within a task function are
* only weakly scheduled as subtasks and will be dropped if they reach the
* front of the queue before the promise is resolved. In the example below, the
* callbacks for `B` & `D` are dropped as sub-tasks since they are attached to
* an unresolved promise when they reach the front of the task queue.
*
* var d = promise.defer();
* flow.execute(function() {
* flow.execute( () => console.log('A'));
* d.promise.then(() => console.log('B'));
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* setTimeout(d.fulfill, 20);
* }).then(function() {
* console.log('fin')
* });
* // A
* // C
* // fin
* // B
* // D
*
* If a promise is resolved while a task function is on the call stack, any
* previously registered and unqueued callbacks (i.e. either attached while no
* task was on the call stack, or previously dropped as described above) act as
* _interrupts_ and are inserted at the front of the task queue. If multiple
* promises are fulfilled, their interrupts are enqueued in the order the
* promises are resolved.
*
* var d1 = promise.defer();
* d1.promise.then(() => console.log('A'));
*
* var d2 = promise.defer();
* d2.promise.then(() => console.log('B'));
*
* flow.execute(function() {
* d1.promise.then(() => console.log('C'));
* flow.execute(() => console.log('D'));
* });
* flow.execute(function() {
* flow.execute(() => console.log('E'));
* flow.execute(() => console.log('F'));
* d1.fulfill();
* d2.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // D
* // A
* // C
* // B
* // E
* // F
* // fin
*
* Within a task function (or callback), each step of a promise chain acts as
* an interrupt on the task queue:
*
* var d = promise.defer();
* flow.execute(function() {
* d.promise.
* then(() => console.log('A')).
* then(() => console.log('B')).
* then(() => console.log('C')).
* then(() => console.log('D'));
*
* flow.execute(() => console.log('E'));
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // fin
*
* If there are multiple promise chains derived from a single promise, they are
* processed in the order created:
*
* var d = promise.defer();
* flow.execute(function() {
* var chain = d.promise.then(() => console.log('A'));
*
* chain.then(() => console.log('B')).
* then(() => console.log('C'));
*
* chain.then(() => console.log('D')).
* then(() => console.log('E'));
*
* flow.execute(() => console.log('F'));
*
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // F
* // fin
*
* Even though a subtask's promised result will never resolve while the task
* function is on the stack, it will be treated as a promise resolved within the
* task. In all other scenarios, a task's promise behaves just like a normal
* promise. In the sample below, `C/D` is loggged before `B` because the
* resolution of `subtask1` interrupts the flow of the enclosing task. Within
* the final subtask, `E/F` is logged in order because `subtask1` is a resolved
* promise when that task runs.
*
* flow.execute(function() {
* var subtask1 = flow.execute(() => console.log('A'));
* var subtask2 = flow.execute(() => console.log('B'));
*
* subtask1.then(() => console.log('C'));
* subtask1.then(() => console.log('D'));
*
* flow.execute(function() {
* flow.execute(() => console.log('E'));
* subtask1.then(() => console.log('F'));
* });
* }).then(function() {
* console.log('fin');
* });
* // A
* // C
* // D
* // B
* // E
* // F
* // fin
*
* Finally, consider the following:
*
* var d = promise.defer();
* d.promise.then(() => console.log('A'));
* d.promise.then(() => console.log('B'));
*
* flow.execute(function() {
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* flow.execute( () => console.log('E'));
* d.promise.then(() => console.log('F'));
*
* d.fulfill();
*
* flow.execute( () => console.log('G'));
* d.promise.then(() => console.log('H'));
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // F
* // G
* // H
* // fin
*
* In this example, callbacks are registered on `d.promise` both before and
* during the invocation of the task function. When `d.fulfill()` is called,
* the callbacks registered before the task (`A` & `B`) are registered as
* interrupts. The remaining callbacks were all attached within the task and
* are scheduled in the flow as standard tasks.
*
* ## Generator Support
*
* [Generators][GF] may be scheduled as tasks within a control flow or attached
* as callbacks to a promise. Each time the generator yields a promise, the
* control flow will wait for that promise to settle before executing the next
* iteration of the generator. The yielded promise's fulfilled value will be
* passed back into the generator:
*
* flow.execute(function* () {
* var d = promise.defer();
*
* setTimeout(() => console.log('...waiting...'), 25);
* setTimeout(() => d.fulfill(123), 50);
*
* console.log('start: ' + Date.now());
*
* var value = yield d.promise;
* console.log('mid: %d; value = %d', Date.now(), value);
*
* yield promise.delayed(10);
* console.log('end: ' + Date.now());
* }).then(function() {
* console.log('fin');
* });
* // start: 0
* // ...waiting...
* // mid: 50; value = 123
* // end: 60
* // fin
*
* Yielding the result of a promise chain will wait for the entire chain to
* complete:
*
* promise.fulfilled().then(function* () {
* console.log('start: ' + Date.now());
*
* var value = yield flow.
* execute(() => console.log('A')).
* then( () => console.log('B')).
* then( () => 123);
*
* console.log('mid: %s; value = %d', Date.now(), value);
*
* yield flow.execute(() => console.log('C'));
* }).then(function() {
* console.log('fin');
* });
* // start: 0
* // A
* // B
* // mid: 2; value = 123
* // C
* // fin
*
* Yielding a _rejected_ promise will cause the rejected value to be thrown
* within the generator function:
*
* flow.execute(function* () {
* console.log('start: ' + Date.now());
* try {
* yield promise.delayed(10).then(function() {
* throw Error('boom');
* });
* } catch (ex) {
* console.log('caught time: ' + Date.now());
* console.log(ex.message);
* }
* });
* // start: 0
* // caught time: 10
* // boom
*
* # Error Handling
*
* ES6 promises do not require users to handle a promise rejections. This can
* result in subtle bugs as the rejections are silently "swallowed" by the
* ManagedPromise class.
*
* ManagedPromise.reject(Error('boom'));
* // ... *crickets* ...
*
* Selenium's promise module, on the other hand, requires that every rejection
* be explicitly handled. When a {@linkplain ManagedPromise ManagedPromise} is
* rejected and no callbacks are defined on that promise, it is considered an
* _unhandled rejection_ and reproted to the active task queue. If the rejection
* remains unhandled after a single turn of the [event loop][JSEL] (scheduled
* with a micro-task), it will propagate up the stack.
*
* ## Error Propagation
*
* If an unhandled rejection occurs within a task function, that task's promised
* result is rejected and all remaining subtasks are discarded:
*
* flow.execute(function() {
* // No callbacks registered on promise -> unhandled rejection
* promise.rejected(Error('boom'));
* flow.execute(function() { console.log('this will never run'); });
* }).catch(function(e) {
* console.log(e.message);
* });
* // boom
*
* The promised results for discarded tasks are silently rejected with a
* cancellation error and existing callback chains will never fire.
*
* flow.execute(function() {
* promise.rejected(Error('boom'));
* flow.execute(function() { console.log('a'); }).
* then(function() { console.log('b'); });
* }).catch(function(e) {
* console.log(e.message);
* });
* // boom
*
* An unhandled rejection takes precedence over a task function's returned
* result, even if that value is another promise:
*
* flow.execute(function() {
* promise.rejected(Error('boom'));
* return flow.execute(someOtherTask);
* }).catch(function(e) {
* console.log(e.message);
* });
* // boom
*
* If there are multiple unhandled rejections within a task, they are packaged
* in a {@link MultipleUnhandledRejectionError}, which has an `errors` property
* that is a `Set` of the recorded unhandled rejections:
*
* flow.execute(function() {
* promise.rejected(Error('boom1'));
* promise.rejected(Error('boom2'));
* }).catch(function(ex) {
* console.log(ex instanceof MultipleUnhandledRejectionError);
* for (var e of ex.errors) {
* console.log(e.message);
* }
* });
* // boom1
* // boom2
*
* When a subtask is discarded due to an unreported rejection in its parent
* frame, the existing callbacks on that task will never settle and the
* callbacks will not be invoked. If a new callback is attached ot the subtask
* _after_ it has been discarded, it is handled the same as adding a callback
* to a cancelled promise: the error-callback path is invoked. This behavior is
* intended to handle cases where the user saves a reference to a task promise,
* as illustrated below.
*
* var subTask;
* flow.execute(function() {
* promise.rejected(Error('boom'));
* subTask = flow.execute(function() {});
* }).catch(function(e) {
* console.log(e.message);
* }).then(function() {
* return subTask.then(
* () => console.log('subtask success!'),
* (e) => console.log('subtask failed:\n' + e));
* });
* // boom
* // subtask failed:
* // DiscardedTaskError: Task was discarded due to a previous failure: boom
*
* When a subtask fails, its promised result is treated the same as any other
* promise: it must be handled within one turn of the rejection or the unhandled
* rejection is propagated to the parent task. This means users can catch errors
* from complex flows from the top level task:
*
* flow.execute(function() {
* flow.execute(function() {
* flow.execute(function() {
* throw Error('fail!');
* });
* });
* }).catch(function(e) {
* console.log(e.message);
* });
* // fail!
*
* ## Unhandled Rejection Events
*
* When an unhandled rejection propagates to the root of the control flow, the
* flow will emit an __uncaughtException__ event. If no listeners are registered
* on the flow, the error will be rethrown to the global error handler: an
* __uncaughtException__ event from the
* [`process`](https://nodejs.org/api/process.html) object in node, or
* `window.onerror` when running in a browser.
*
* Bottom line: you __*must*__ handle rejected promises.
*
* # ManagedPromise/A+ Compatibility
*
* This `promise` module is compliant with the [ManagedPromise/A+][] specification
* except for sections `2.2.6.1` and `2.2.6.2`:
*
* >
* > - `then` may be called multiple times on the same promise.
* > - If/when `promise` is fulfilled, all respective `onFulfilled` callbacks
* > must execute in the order of their originating calls to `then`.
* > - If/when `promise` is rejected, all respective `onRejected` callbacks
* > must execute in the order of their originating calls to `then`.
* >
*
* Specifically, the conformance tests contains the following scenario (for
* brevity, only the fulfillment version is shown):
*
* var p1 = ManagedPromise.resolve();
* p1.then(function() {
* console.log('A');
* p1.then(() => console.log('B'));
* });
* p1.then(() => console.log('C'));
* // A
* // C
* // B
*
* Since the [ControlFlow](#scheduling_callbacks) executes promise callbacks as
* tasks, with this module, the result would be
*
* var p2 = promise.fulfilled();
* p2.then(function() {
* console.log('A');
* p2.then(() => console.log('B');
* });
* p2.then(() => console.log('C'));
* // A
* // B
* // C
*
* [JSEL]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
* [GF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
* [ManagedPromise/A+]: https://promisesaplus.com/
*/
'use strict';
const error = require('./error');
const events = require('./events');
const logging = require('./logging');
/**
* Alias to help with readability and differentiate types.
* @const
*/
const NativePromise = Promise;
/**
* Whether to append traces of `then` to rejection errors.
* @type {boolean}
*/
var LONG_STACK_TRACES = false; // TODO: this should not be CONSTANT_CASE
/** @const */
const LOG = logging.getLogger('promise');
const UNIQUE_IDS = new WeakMap;
let nextId = 1;
function getUid(obj) {
let id = UNIQUE_IDS.get(obj);
if (!id) {
id = nextId;
nextId += 1;
UNIQUE_IDS.set(obj, id);
}
return id;
}
/**
* Runs the given function after a micro-task yield.
* @param {function()} fn The function to run.
*/
function asyncRun(fn) {
NativePromise.resolve().then(function() {
try {
fn();
} catch (ignored) {
// Do nothing.
}
});
}
/**
* @param {number} level What level of verbosity to log with.
* @param {(string|function(this: T): string)} loggable The message to log.
* @param {T=} opt_self The object in whose context to run the loggable
* function.
* @template T
*/
function vlog(level, loggable, opt_self) {
var logLevel = logging.Level.FINE;
if (level > 1) {
logLevel = logging.Level.FINEST;
} else if (level > 0) {
logLevel = logging.Level.FINER;
}
if (typeof loggable === 'function') {
loggable = loggable.bind(opt_self);
}
LOG.log(logLevel, loggable);
}
/**
* Generates an error to capture the current stack trace.
* @param {string} name Error name for this stack trace.
* @param {string} msg Message to record.
* @param {Function=} opt_topFn The function that should appear at the top of
* the stack; only applicable in V8.
* @return {!Error} The generated error.
*/
function captureStackTrace(name, msg, opt_topFn) {
var e = Error(msg);
e.name = name;
if (Error.captureStackTrace) {
Error.captureStackTrace(e, opt_topFn);
} else {
var stack = Error().stack;
if (stack) {
e.stack = e.toString();
e.stack += '\n' + stack;
}
}
return e;
}
/**
* Error used when the computation of a promise is cancelled.
*/
class CancellationError extends Error {
/**
* @param {string=} opt_msg The cancellation message.
*/
constructor(opt_msg) {
super(opt_msg);
/** @override */
this.name = this.constructor.name;
/** @private {boolean} */
this.silent_ = false;
}
/**
* Wraps the given error in a CancellationError.
*
* @param {*} error The error to wrap.
* @param {string=} opt_msg The prefix message to use.
* @return {!CancellationError} A cancellation error.
*/
static wrap(error, opt_msg) {
var message;
if (error instanceof CancellationError) {
return new CancellationError(
opt_msg ? (opt_msg + ': ' + error.message) : error.message);
} else if (opt_msg) {
message = opt_msg;
if (error) {
message += ': ' + error;
}
return new CancellationError(message);
}
if (error) {
message = error + '';
}
return new CancellationError(message);
}
}
/**
* Error used to cancel tasks when a control flow is reset.
* @final
*/
class FlowResetError extends CancellationError {
constructor() {
super('ControlFlow was reset');
this.silent_ = true;
}
}
/**
* Error used to cancel tasks that have been discarded due to an uncaught error
* reported earlier in the control flow.
* @final
*/
class DiscardedTaskError extends CancellationError {
/** @param {*} error The original error. */
constructor(error) {
if (error instanceof DiscardedTaskError) {
return /** @type {!DiscardedTaskError} */(error);
}
var msg = '';
if (error) {
msg = ': ' + (
typeof error.message === 'string' ? error.message : error);
}
super('Task was discarded due to a previous failure' + msg);
this.silent_ = true;
}
}
/**
* Error used when there are multiple unhandled promise rejections detected
* within a task or callback.
*
* @final
*/
class MultipleUnhandledRejectionError extends Error {
/**
* @param {!(Set<*>)} errors The errors to report.
*/
constructor(errors) {
super('Multiple unhandled promise rejections reported');
/** @override */
this.name = this.constructor.name;
/** @type {!Set<*>} */
this.errors = errors;
}
}
/**
* Property used to flag constructor's as implementing the Thenable interface
* for runtime type checking.
* @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.
*
* @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.
* @param {function(new: Thenable, ...?)} ctor The
* constructor whose prototype to modify.
*/
static addImplementation(ctor) {
addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL);
}
/**
* Checks if an object has been tagged for implementing the Thenable
* interface as defined by {@link Thenable.addImplementation}.
* @param {*} object The object to test.
* @return {boolean} Whether the object is an implementation of the Thenable
* interface.
*/
static isImplementation(object) {
return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL);
}
/**
* Registers listeners for when this instance is resolved.
*
* @param {?(function(T): (R|IThenable<R>))=} opt_callback The
* function to call if this promise is successfully resolved. The function
* should expect a single argument: the promise's resolved value.
* @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 {!Thenable<R>} A new promise which will be resolved with the result
* of the invoked callback.
* @template R
*/
then(opt_callback, opt_errback) {}
/**
* Registers a listener for when this promise is rejected. This is synonymous
* with the {@code catch} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } catch (ex) {
* console.error(ex);
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().catch(function(ex) {
* console.error(ex);
* });
*
* @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 {!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 {
/**
* @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 {(string|Error)=} opt_reason The reason this promise is being
* cancelled. This value will be wrapped in a {@link CancellationError}.
*/
cancel(opt_reason) {}
}
/**
* @enum {string}
*/
const PromiseState = {
PENDING: 'pending',
BLOCKED: 'blocked',
REJECTED: 'rejected',
FULFILLED: 'fulfilled'
};
/**
* Internal map used to store cancellation handlers for {@link ManagedPromise}
* objects. This is an internal implementation detail used by the
* {@link TaskQueue} class to monitor for when a promise is cancelled without
* generating an extra promise via then().
*
* @const {!WeakMap<!ManagedPromise, function(!CancellationError)>}
*/
const ON_CANCEL_HANDLER = new WeakMap;
/**
* Represents the eventual value of a completed operation. Each promise may be
* in one of three states: pending, fulfilled, or rejected. Each promise starts
* in the pending state and may make a single transition to either a
* fulfilled or rejected state, at which point the promise is considered
* resolved.
*
* @implements {CancellableThenable<T>}
* @template T
* @see http://promises-aplus.github.io/promises-spec/
*/
class ManagedPromise {
/**
* @param {function(
* function((T|IThenable<T>|Thenable)=),
* function(*=))} resolver
* Function that is invoked immediately to begin computation of this
* promise's value. The function should accept a pair of callback
* functions, one for fulfilling the promise and another for rejecting it.
* @param {ControlFlow=} opt_flow The control flow
* 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} */
this.flow_ = opt_flow || controlFlow();
/** @private {Error} */
this.stack_ = null;
if (LONG_STACK_TRACES) {
this.stack_ = captureStackTrace('ManagedPromise', 'new', this.constructor);
}
/** @private {Thenable<?>} */
this.parent_ = null;
/** @private {Array<!Task>} */
this.callbacks_ = null;
/** @private {PromiseState} */
this.state_ = PromiseState.PENDING;
/** @private {boolean} */
this.handled_ = false;
/** @private {*} */
this.value_ = undefined;
/** @private {TaskQueue} */
this.queue_ = null;
try {
var self = this;
resolver(function(value) {
self.resolve_(PromiseState.FULFILLED, value);
}, function(reason) {
self.resolve_(PromiseState.REJECTED, reason);
});
} catch (ex) {
this.resolve_(PromiseState.REJECTED, ex);
}
}
/**
* 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) +
' {[[PromiseStatus]]: "' + this.state_ + '"}';
}
/**
* Resolves this promise. If the new value is itself a promise, this function
* will wait for it to be resolved before notifying the registered listeners.
* @param {PromiseState} newState The promise's new state.
* @param {*} newValue The promise's new value.
* @throws {TypeError} If {@code newValue === this}.
* @private
*/
resolve_(newState, newValue) {
if (PromiseState.PENDING !== this.state_) {
return;
}
if (newValue === this) {
// See promise a+, 2.3.1
// http://promises-aplus.github.io/promises-spec/#point-48
newValue = new TypeError('A promise may not resolve to itself');
newState = PromiseState.REJECTED;
}
this.parent_ = null;
this.state_ = PromiseState.BLOCKED;
if (newState !== PromiseState.REJECTED) {
if (Thenable.isImplementation(newValue)) {
// 2.3.2
newValue = /** @type {!Thenable} */(newValue);
this.parent_ = newValue;
newValue.then(
this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
return;
} else if (newValue
&& (typeof newValue === 'object' || typeof newValue === 'function')) {
// 2.3.3
try {
// 2.3.3.1
var then = newValue['then'];
} catch (e) {
// 2.3.3.2
this.state_ = PromiseState.REJECTED;
this.value_ = e;
this.scheduleNotifications_();
return;
}
if (typeof then === 'function') {
// 2.3.3.3
this.invokeThen_(/** @type {!Object} */(newValue), then);
return;
}
}
}
if (newState === PromiseState.REJECTED &&
isError(newValue) && newValue.stack && this.stack_) {
newValue.stack += '\nFrom: ' + (this.stack_.stack || this.stack_);
}
// 2.3.3.4 and 2.3.4
this.state_ = newState;
this.value_ = newValue;
this.scheduleNotifications_();
}
/**
* Invokes a thenable's "then" method according to 2.3.3.3 of the promise
* A+ spec.
* @param {!Object} x The thenable object.
* @param {!Function} then The "then" function to invoke.
* @private
*/
invokeThen_(x, then) {
var called = false;
var self = this;
var resolvePromise = function(value) {
if (!called) { // 2.3.3.3.3
called = true;
// 2.3.3.3.1
self.unblockAndResolve_(PromiseState.FULFILLED, value);
}
};
var rejectPromise = function(reason) {
if (!called) { // 2.3.3.3.3
called = true;
// 2.3.3.3.2
self.unblockAndResolve_(PromiseState.REJECTED, reason);
}
};
try {
// 2.3.3.3
then.call(x, resolvePromise, rejectPromise);
} catch (e) {
// 2.3.3.3.4.2
rejectPromise(e);
}
}
/**
* @param {PromiseState} newState The promise's new state.
* @param {*} newValue The promise's new value.
* @private
*/
unblockAndResolve_(newState, newValue) {
if (this.state_ === PromiseState.BLOCKED) {
this.state_ = PromiseState.PENDING;
this.resolve_(newState, newValue);
}
}
/**
* @private
*/
scheduleNotifications_() {
vlog(2, () => this + ' scheduling notifications', this);
ON_CANCEL_HANDLER.delete(this);
if (this.value_ instanceof CancellationError
&& this.value_.silent_) {
this.callbacks_ = null;
}
if (!this.queue_) {
this.queue_ = this.flow_.getActiveQueue_();
}
if (!this.handled_ &&
this.state_ === PromiseState.REJECTED &&
!(this.value_ instanceof CancellationError)) {
this.queue_.addUnhandledRejection(this);
}
this.queue_.scheduleCallbacks(this);
}
/** @override */
cancel(opt_reason) {
if (!canCancel(this)) {
return;
}
if (this.parent_ && canCancel(this.parent_)) {
/** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason);
} else {
var reason = CancellationError.wrap(opt_reason);
let onCancel = ON_CANCEL_HANDLER.get(this);
if (onCancel) {
onCancel(reason);
ON_CANCEL_HANDLER.delete(this);
}
if (this.state_ === PromiseState.BLOCKED) {
this.unblockAndResolve_(PromiseState.REJECTED, reason);
} else {
this.resolve_(PromiseState.REJECTED, reason);
}
}
function canCancel(promise) {
if (!(promise instanceof ManagedPromise)) {
return CancellableThenable.isImplementation(promise);
}
return promise.state_ === PromiseState.PENDING
|| promise.state_ === PromiseState.BLOCKED;
}
}
/** @override */
then(opt_callback, opt_errback) {
return this.addCallback_(
opt_callback, opt_errback, 'then', ManagedPromise.prototype.then);
}
/** @override */
catch(errback) {
return this.addCallback_(
null, errback, 'catch', ManagedPromise.prototype.catch);
}
/**
* @param {function(): (R|IThenable<R>)} callback
* @return {!ManagedPromise<R>}
* @template R
* @see ./promise.finally()
*/
finally(callback) {
let result = thenFinally(this, callback);
return /** @type {!ManagedPromise} */(result);
}
/**
* Registers a new callback with this promise
* @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
* fulfillment callback.
* @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
* rejection callback.
* @param {string} name The callback name.
* @param {!Function} fn The function to use as the top of the stack when
* recording the callback's creation point.
* @return {!ManagedPromise<R>} A new promise which will be resolved with the
* esult of the invoked callback.
* @template R
* @private
*/
addCallback_(callback, errback, name, fn) {
if (typeof callback !== 'function' && typeof errback !== 'function') {
return this;
}
this.handled_ = true;
if (this.queue_) {
this.queue_.clearUnhandledRejection(this);
}
var cb = new Task(
this.flow_,
this.invokeCallback_.bind(this, callback, errback),
name,
LONG_STACK_TRACES ? {name: 'Promise', top: fn} : undefined);
cb.promise.parent_ = this;
if (this.state_ !== PromiseState.PENDING &&
this.state_ !== PromiseState.BLOCKED) {
this.flow_.getActiveQueue_().enqueue(cb);
} else {
if (!this.callbacks_) {
this.callbacks_ = [];
}
this.callbacks_.push(cb);
cb.blocked = true;
this.flow_.getActiveQueue_().enqueue(cb);
}
return cb.promise;
}
/**
* Invokes a callback function attached to this promise.
* @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
* fulfillment callback.
* @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
* rejection callback.
* @template R
* @private
*/
invokeCallback_(callback, errback) {
var callbackFn = callback;
if (this.state_ === PromiseState.REJECTED) {
callbackFn = errback;
}
if (typeof callbackFn === 'function') {
if (isGenerator(callbackFn)) {
return consume(callbackFn, null, this.value_);
}
return callbackFn(this.value_);
} else if (this.state_ === PromiseState.REJECTED) {
throw this.value_;
} else {
return this.value_;
}
}
}
CancellableThenable.addImplementation(ManagedPromise);
/**
* @param {!ManagedPromise} promise
* @return {boolean}
*/
function isPending(promise) {
return promise.state_ === PromiseState.PENDING;
}
/**
* Represents a value that will be resolved at some point in the future. This
* class represents the protected "producer" half of a ManagedPromise - each Deferred
* has a {@code promise} property that may be returned to consumers for
* registering callbacks, reserving the ability to resolve the deferred to the
* producer.
*
* If this Deferred is rejected and there are no listeners registered before
* the next turn of the event loop, the rejection will be passed to the
* {@link ControlFlow} as an unhandled failure.
*
* @template T
*/
class Deferred {
/**
* @param {ControlFlow=} opt_flow The control flow this instance was
* created under. This should only be provided during unit tests.
*/
constructor(opt_flow) {
var fulfill, reject;
/** @type {!ManagedPromise<T>} */
this.promise = new ManagedPromise(function(f, r) {
fulfill = f;
reject = r;
}, opt_flow);
var self = this;
var checkNotSelf = function(value) {
if (value === self) {
throw new TypeError('May not resolve a Deferred with itself');
}
};
/**
* Resolves this deferred with the given value. It is safe to call this as a
* normal function (with no bound "this").
* @param {(T|IThenable<T>|Thenable)=} opt_value The fulfilled value.
*/
this.fulfill = function(opt_value) {
checkNotSelf(opt_value);
fulfill(opt_value);
};
/**
* Rejects this promise with the given reason. It is safe to call this as a
* normal function (with no bound "this").
* @param {*=} opt_reason The rejection reason.
*/
this.reject = function(opt_reason) {
checkNotSelf(opt_reason);
reject(opt_reason);
};
}
}
/**
* Tests if a value is an Error-like object. This is more than an straight
* instanceof check since the value may originate from another context.
* @param {*} value The value to test.
* @return {boolean} Whether the value is an error.
*/
function isError(value) {
return value instanceof Error ||
(!!value && typeof value === 'object'
&& typeof value.message === 'string');
}
/**
* Determines whether a {@code value} should be treated as a promise.
* Any object whose "then" property is a function will be considered a promise.
*
* @param {?} value The value to test.
* @return {boolean} Whether the value is a promise.
*/
function isPromise(value) {
try {
// Use array notation so the Closure compiler does not obfuscate away our
// contract.
return value
&& (typeof value === 'object' || typeof value === 'function')
&& typeof value['then'] === 'function';
} catch (ex) {
return false;
}
}
/**
* 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 {!Thenable} The promise.
*/
function delayed(ms) {
return createPromise(resolve => {
setTimeout(() => resolve(), ms);
});
}
/**
* Creates a new deferred object.
* @return {!Deferred<T>} The new deferred object.
* @template T
*/
function defer() {
return new Deferred();
}
/**
* 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) {
return ManagedPromise.resolve(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<?>} The rejected promise.
* @deprecated Use {@link ManagedPromise#reject Promise.reject(reason)}.
*/
function rejected(opt_reason) {
return ManagedPromise.reject(opt_reason);
}
/**
* Wraps a function that expects a node-style callback as its final
* argument. This callback expects two arguments: an error value (which will be
* null if the call succeeded), and the success value as the second argument.
* The callback will the resolve or reject the returned promise, based on its
* arguments.
* @param {!Function} fn The function to wrap.
* @param {...?} var_args The arguments to apply to the function, excluding the
* final callback.
* @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 createPromise(function(fulfill, reject) {
try {
args.push(function(error, value) {
error ? reject(error) : fulfill(value);
});
fn.apply(undefined, args);
} catch (ex) {
reject(ex);
}
});
}
/**
* 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
* that will be resolved when the value is. If {@code value} is not a promise,
* then the return promise will be immediately resolved.
* @param {*} value The value to observe.
* @param {Function=} opt_callback The function to call when the value is
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
* @return {!Thenable} A new promise.
*/
function when(value, opt_callback, opt_errback) {
if (Thenable.isImplementation(value)) {
return value.then(opt_callback, opt_errback);
}
return createPromise(resolve => resolve(value))
.then(opt_callback, opt_errback);
}
/**
* Invokes the appropriate callback function as soon as a promised `value` is
* resolved. This function is similar to `when()`, except it does not return
* a new promise.
* @param {*} value The value to observe.
* @param {Function} callback The function to call when the value is
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
*/
function asap(value, callback, opt_errback) {
if (isPromise(value)) {
value.then(callback, opt_errback);
} else if (callback) {
callback(value);
}
}
/**
* Given an array of promi