UNPKG

selenium-webdriver

Version:

The official WebDriver JavaScript bindings from the Selenium project

1,578 lines (1,452 loc) 96 kB
// 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. /** * @license Portions of this code are from the Dojo toolkit, received under the * BSD License: * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the Dojo Foundation nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * @fileoverview * The promise module is centered around the * {@linkplain webdriver.promise.ControlFlow 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 webdriver.promise.ControlFlow#execute() ControlFlow#execute()}, which * will return a {@link webdriver.promise.Promise Promise} 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) * `Promise.then()` callback. * * setTimeout(() => console.log('a')); * Promise.resolve().then(() => console.log('b')); // A native promise. * flow.execute(() => console.log('c')); * Promise.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 webdriver.promise.ControlFlow#execute 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 webdriver.promise.ControlFlow#execute 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 * * ## Promise Integration * * In addition to the {@link webdriver.promise.ControlFlow ControlFlow} class, * the promise module also exports a [Promise/A+] * {@linkplain webdriver.promise.Promise implementation} that is deeply * integrated with the ControlFlow. First and foremost, each promise * {@linkplain webdriver.promise.Promise#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 Promise 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 * * If a promise is resolved while a task function is on the call stack, any * previously registered callbacks (i.e. attached while the task was _not_ on * the call stack), 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() { * flow.execute(() => console.log('C')); * flow.execute(() => console.log('D')); * d1.fulfill(); * d2.fulfill(); * }).then(function() { * console.log('fin'); * }); * // A * // B * // C * // D * // 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 * * __Note__: while the ControlFlow will wait for * {@linkplain webdriver.promise.ControlFlow#execute tasks} and * {@linkplain webdriver.promise.Promise#then callbacks} to complete, it * _will not_ wait for unresolved promises created within a task: * * flow.execute(function() { * var p = new promise.Promise(function(fulfill) { * setTimeout(fulfill, 100); * }); * * p.then(() => console.log('promise resolved!')); * * }).then(function() { * console.log('task complete!'); * }); * // task complete! * // promise resolved! * * 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 * Promise class. * * Promise.reject(Error('boom')); * // ... *crickets* ... * * Selenium's {@link webdriver.promise promise} module, on the other hand, * requires that every rejection be explicitly handled. When a * {@linkplain webdriver.promise.Promise Promise} 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'); }); * }).thenCatch(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'); }); * }).thenCatch(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); * }).thenCatch(function(e) { * console.log(e.message); * }); * // boom * * If there are multiple unhandled rejections within a task, they are packaged * in a {@link webdriver.promise.MultipleUnhandledRejectionError * 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')); * }).thenCatch(function(ex) { * console.log(ex instanceof promise.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() {}); * }).thenCatch(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!'); * }); * }); * }).thenCatch(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. * * # Promise/A+ Compatibility * * This `promise` module is compliant with the [Promise/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 = Promise.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* * [Promise/A+]: https://promisesaplus.com/ */ goog.module('webdriver.promise'); goog.module.declareLegacyNamespace(); var Arrays = goog.require('goog.array'); var asserts = goog.require('goog.asserts'); var asyncRun = goog.require('goog.async.run'); var throwException = goog.require('goog.async.throwException'); var DebugError = goog.require('goog.debug.Error'); var log = goog.require('goog.log'); var Objects = goog.require('goog.object'); var EventEmitter = goog.require('webdriver.EventEmitter'); var stacktrace = goog.require('webdriver.stacktrace'); /** * @define {boolean} Whether to append traces of {@code then} to rejection * errors. */ goog.define('webdriver.promise.LONG_STACK_TRACES', false); /** @const */ var promise = exports; /** @const */ var LOG = log.getLogger('webdriver.promise'); /** * @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 = log.Level.FINE; if (level > 1) { logLevel = log.Level.FINEST; } else if (level > 0) { logLevel = log.Level.FINER; } if (typeof loggable === 'function') { loggable = loggable.bind(opt_self); } log.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} topFn The function that should appear at the top of the * stack; only applicable in V8. * @return {!Error} The generated error. */ promise.captureStackTrace = function(name, msg, topFn) { var e = Error(msg); e.name = name; if (Error.captureStackTrace) { Error.captureStackTrace(e, topFn); } else { var stack = stacktrace.getStack(e); e.stack = e.toString(); if (stack) { e.stack += '\n' + stack; } } return e; }; /** * Error used when the computation of a promise is cancelled. * * @unrestricted */ promise.CancellationError = goog.defineClass(DebugError, { /** * @param {string=} opt_msg The cancellation message. */ constructor: function(opt_msg) { promise.CancellationError.base(this, 'constructor', opt_msg); /** @override */ this.name = 'CancellationError'; /** @private {boolean} */ this.silent_ = false; }, statics: { /** * Wraps the given error in a CancellationError. * * @param {*} error The error to wrap. * @param {string=} opt_msg The prefix message to use. * @return {!promise.CancellationError} A cancellation error. */ wrap: function(error, opt_msg) { var message; if (error instanceof promise.CancellationError) { return new promise.CancellationError( opt_msg ? (opt_msg + ': ' + error.message) : error.message); } else if (opt_msg) { message = opt_msg; if (error) { message += ': ' + error; } return new promise.CancellationError(message); } if (error) { message = error + ''; } return new promise.CancellationError(message); } } }); /** * Error used to cancel tasks when a control flow is reset. * @unrestricted * @final */ var FlowResetError = goog.defineClass(promise.CancellationError, { constructor: function() { FlowResetError.base(this, 'constructor', 'ControlFlow was reset'); /** @override */ this.name = 'FlowResetError'; this.silent_ = true; } }); /** * Error used to cancel tasks that have been discarded due to an uncaught error * reported earlier in the control flow. * @unrestricted * @final */ var DiscardedTaskError = goog.defineClass(promise.CancellationError, { /** @param {*} error The original error. */ constructor: function(error) { if (error instanceof DiscardedTaskError) { return /** @type {!DiscardedTaskError} */(error); } var msg = ''; if (error) { msg = ': ' + (typeof error.message === 'string' ? error.message : error); } DiscardedTaskError.base(this, 'constructor', 'Task was discarded due to a previous failure' + msg); /** @override */ this.name = 'DiscardedTaskError'; this.silent_ = true; } }); /** * Error used when there are multiple unhandled promise rejections detected * within a task or callback. * * @unrestricted * @final */ promise.MultipleUnhandledRejectionError = goog.defineClass(DebugError, { /** * @param {!(Set<*>)} errors The errors to report. */ constructor: function(errors) { promise.MultipleUnhandledRejectionError.base( this, 'constructor', 'Multiple unhandled promise rejections reported'); /** @override */ this.name = 'MultipleUnhandledRejectionError'; /** @type {!Set<*>} */ this.errors = errors; } }); /** * Property used to flag constructor's as implementing the Thenable interface * for runtime type checking. * @type {string} * @const */ var IMPLEMENTED_BY_PROP = '$webdriver_Thenable'; /** * Thenable is a promise-like object with a {@code then} method which may be * used to schedule callbacks on a promised value. * * @interface * @extends {IThenable<T>} * @template T */ promise.Thenable = goog.defineClass(null, { statics: { /** * 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. * @param {function(new: promise.Thenable, ...?)} ctor The * constructor whose prototype to modify. */ addImplementation: function(ctor) { // Based on goog.promise.Thenable.isImplementation. ctor.prototype['then'] = ctor.prototype.then; try { // Old IE7 does not support defineProperty; IE8 only supports it for // DOM elements. Object.defineProperty( ctor.prototype, IMPLEMENTED_BY_PROP, {'value': true, 'enumerable': false}); } catch (ex) { ctor.prototype[IMPLEMENTED_BY_PROP] = true; } }, /** * Checks if an object has been tagged for implementing the Thenable * interface as defined by * {@link webdriver.promise.Thenable.addImplementation}. * @param {*} object The object to test. * @return {boolean} Whether the object is an implementation of the Thenable * interface. */ isImplementation: function(object) { // Based on goog.promise.Thenable.isImplementation. if (!object) { return false; } try { return !!object[IMPLEMENTED_BY_PROP]; } catch (e) { return false; // Property access seems to be forbidden. } } }, /** * 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|promise.CancellationError)=} opt_reason The reason this * promise is being cancelled. */ cancel: function(opt_reason) {}, /** @return {boolean} Whether this promise's value is still being computed. */ isPending: function() {}, /** * 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 {!promise.Promise<R>} A new promise which will be * resolved with the result of the invoked callback. * @template R */ then: function(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().thenCatch(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 {!promise.Promise<R>} A new promise which will be * resolved with the result of the invoked callback. * @template R */ thenCatch: function(errback) {}, /** * 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().thenFinally(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')) * .thenFinally(function() { * throw Error('two'); // Hides Error: one * }); * * @param {function(): (R|IThenable<R>)} callback The function * to call when this promise is resolved. * @return {!promise.Promise<R>} A promise that will be fulfilled * with the callback result. * @template R */ thenFinally: function(callback) {} }); /** * @enum {string} */ var PromiseState = { PENDING: 'pending', BLOCKED: 'blocked', REJECTED: 'rejected', FULFILLED: 'fulfilled' }; /** * Internal symbol used to store a cancellation handler for * {@link promise.Promise} 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(). */ var CANCEL_HANDLER_SYMBOL = Symbol('on cancel'); /** * 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 {promise.Thenable<T>} * @template T * @see http://promises-aplus.github.io/promises-spec/ * @unrestricted // For using CANCEL_HANDLER_SYMBOL. */ promise.Promise = goog.defineClass(null, { /** * @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 {promise.ControlFlow=} opt_flow The control flow * this instance was created under. Defaults to the currently active flow. */ constructor: function(resolver, opt_flow) { goog.getUid(this); /** @private {!promise.ControlFlow} */ this.flow_ = opt_flow || promise.controlFlow(); /** @private {Error} */ this.stack_ = null; if (promise.LONG_STACK_TRACES) { this.stack_ = promise.captureStackTrace( 'Promise', 'new', promise.Promise); } /** @private {promise.Promise<?>} */ 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; /** @private {(function(promise.CancellationError)|null)} */ this[CANCEL_HANDLER_SYMBOL] = 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); } }, /** @override */ toString: function() { return 'Promise::' + goog.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_: function(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 (promise.Thenable.isImplementation(newValue)) { // 2.3.2 newValue = /** @type {!promise.Thenable} */(newValue); newValue.then( this.unblockAndResolve_.bind(this, PromiseState.FULFILLED), this.unblockAndResolve_.bind(this, PromiseState.REJECTED)); return; } else if (goog.isObject(newValue)) { // 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; } // NB: goog.isFunction is loose and will accept instanceof Function. if (typeof then === 'function') { // 2.3.3.3 this.invokeThen_(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_: function(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_: function(newState, newValue) { if (this.state_ === PromiseState.BLOCKED) { this.state_ = PromiseState.PENDING; this.resolve_(newState, newValue); } }, /** * @private */ scheduleNotifications_: function() { vlog(2, () => this + ' scheduling notifications', this); this[CANCEL_HANDLER_SYMBOL] = null; if (this.value_ instanceof promise.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 promise.CancellationError)) { this.queue_.addUnhandledRejection(this); } this.queue_.scheduleCallbacks(this); }, /** @override */ cancel: function(opt_reason) { if (!canCancel(this)) { return; } if (this.parent_ && canCancel(this.parent_)) { this.parent_.cancel(opt_reason); } else { var reason = promise.CancellationError.wrap(opt_reason); if (this[CANCEL_HANDLER_SYMBOL]) { this[CANCEL_HANDLER_SYMBOL](reason); this[CANCEL_HANDLER_SYMBOL] = null; } if (this.state_ === PromiseState.BLOCKED) { this.unblockAndResolve_(PromiseState.REJECTED, reason); } else { this.resolve_(PromiseState.REJECTED, reason); } } function canCancel(promise) { return promise.state_ === PromiseState.PENDING || promise.state_ === PromiseState.BLOCKED; } }, /** @override */ isPending: function() { return this.state_ === PromiseState.PENDING; }, /** @override */ then: function(opt_callback, opt_errback) { return this.addCallback_( opt_callback, opt_errback, 'then', promise.Promise.prototype.then); }, /** @override */ thenCatch: function(errback) { return this.addCallback_( null, errback, 'thenCatch', promise.Promise.prototype.thenCatch); }, /** @override */ thenFinally: function(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; } }); }, /** * 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 {!promise.Promise<R>} A new promise which will be resolved with the * esult of the invoked callback. * @template R * @private */ addCallback_: function(callback, errback, name, fn) { if (!goog.isFunction(callback) && !goog.isFunction(errback)) { 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, promise.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.isVolatile = 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_: function(callback, errback) { var callbackFn = callback; if (this.state_ === PromiseState.REJECTED) { callbackFn = errback; } if (goog.isFunction(callbackFn)) { if (promise.isGenerator(callbackFn)) { return promise.consume(callbackFn, null, this.value_); } return callbackFn(this.value_); } else if (this.state_ === PromiseState.REJECTED) { throw this.value_; } else { return this.value_; } } }); promise.Thenable.addImplementation(promise.Promise); /** * Represents a value that will be resolved at some point in the future. This * class represents the protected "producer" half of a Promise - 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 webdriver.promise.ControlFlow} as an unhandled failure. * * @implements {promise.Thenable<T>} * @template T */ promise.Deferred = goog.defineClass(null, { /** * @param {promise.ControlFlow=} opt_flow The control flow this instance was * created under. This should only be provided during unit tests. */ constructor: function(opt_flow) { var fulfill, reject; /** @type {!promise.Promise<T>} */ this.promise = new promise.Promise(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); }; }, /** @override */ isPending: function() { return this.promise.isPending(); }, /** @override */ cancel: function(opt_reason) { this.promise.cancel(opt_reason); }, /** * @override * @deprecated Use {@code then} from the promise property directly. */ then: function(opt_cb, opt_eb) { return this.promise.then(opt_cb, opt_eb); }, /** * @override * @deprecated Use {@code thenCatch} from the promise property directly. */ thenCatch: function(opt_eb) { return this.promise.thenCatch(opt_eb); }, /** * @override * @deprecated Use {@code thenFinally} from the promise property directly. */ thenFinally: function(opt_cb) { return this.promise.thenFinally(opt_cb); } }); promise.Thenable.addImplementation(promise.Deferred); /** * 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 || goog.isObject(value) && (goog.isString(value.message) || // A special test for goog.testing.JsUnitException. value.isJsUnitException); } /** * 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. */ promise.isPromise = function(value) { return !!value && goog.isObject(value) && // Use array notation so the Closure compiler does not obfuscate away our // contract. Use typeof rather than goog.isFunction because // goog.isFunction accepts instanceof Function, which the promise spec // does not. typeof value['then'] === 'function'; }; /** * 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 {!promise.Promise} The promise. */ promise.delayed = function(ms) { var key; return new promise.Promise(function(fulfill) { key = setTimeout(function() { key = null; fulfill(); }, ms); }).thenCatch(function(e) { clearTimeout(key); key = null; throw e; }); }; /** * Creates a new deferred object. * @return {!promise.Deferred<T>} The new deferred object. * @template T */ promise.defer = function() { return new promise.Deferred(); }; /** * Creates a promise that has been resolved with the given value. * @param {T=} opt_value The resolved value. * @return {!promise.Promise<T>} The resolved promise. * @template T */ promise.fulfilled = function(opt_value) { if (opt_value instanceof promise.Promise) { return opt_value; } return new promise.Promise(function(fulfill) { fulfill(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 {!promise.Promise<T>} The rejected promise. * @template T */ promise.rejected = function(opt_reason) { if (opt_reason instanceof promise.Promise) { return opt_reason; } return new promise.Promise(function(_, reject) { 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 {!promise.Promise} A promise that will be resolved with the * result of the provided function's callback. */ promise.checkedNodeCall = function(fn, var_args) { var args = Arrays.slice(arguments, 1); return new promise.Promise(function(fulfill, reject) { try { args.push(function(error, value) { error ? reject(error) : fulfill(value); }); fn.apply(undefined, args); } catch (ex) { reject(ex); } }); }; /** * 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 {!promise.Promise} A new promise. */ promise.when = function(value, opt_callback, opt_errback) { if (promise.Thenable.isImplementation(value)) { return value.then(opt_callback, opt_errback); } return new promise.Promise(function(fulfill, reject) { promise.asap(value, fulfill, reject); }).then(opt_callback, opt_errback); }; /** * Invokes the appropriate callback function as soon as a promised * {@code value} is resolved. This function is similar to * {@link webdriver.promise.when}, except it does not return a new promise. * @param {*} value The value