UNPKG

selenium-webdriver

Version:

The official WebDriver JavaScript bindings from the Selenium project

1,583 lines (1,383 loc) 79.3 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 A promise implementation based on the CommonJS promise/A and * promise/B proposals. For more information, see * http://wiki.commonjs.org/wiki/Promises. */ 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 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; /** * 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. * * @param {string=} opt_msg The cancellation message. * @constructor * @extends {DebugError} * @final */ promise.CancellationError = function(opt_msg) { DebugError.call(this, opt_msg); /** @override */ this.name = 'CancellationError'; }; goog.inherits(promise.CancellationError, DebugError); /** * Wraps the given error in a CancellationError. Will trivially return * the error itself if it is an instanceof CancellationError. * * @param {*} error The error to wrap. * @param {string=} opt_msg The prefix message to use. * @return {!promise.CancellationError} A cancellation error. */ promise.CancellationError.wrap = function(error, opt_msg) { if (error instanceof promise.CancellationError) { return /** @type {!promise.CancellationError} */(error); } else if (opt_msg) { var message = opt_msg; if (error) { message += ': ' + error; } return new promise.CancellationError(message); } var message; if (error) { message = error + ''; } return new promise.CancellationError(message); }; /** * 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 = function() {}; /** * 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. */ promise.Thenable.prototype.cancel = function(opt_reason) {}; /** @return {boolean} Whether this promise's value is still being computed. */ promise.Thenable.prototype.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 */ promise.Thenable.prototype.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 */ promise.Thenable.prototype.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 */ promise.Thenable.prototype.thenFinally = function(callback) {}; /** * Property used to flag constructor's as implementing the Thenable interface * for runtime type checking. * @type {string} * @const */ var IMPLEMENTED_BY_PROP = '$webdriver_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. * @param {function(new: promise.Thenable, ...?)} ctor The * constructor whose prototype to modify. */ promise.Thenable.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. */ promise.Thenable.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. } }; /** * @enum {string} */ var PromiseState = { PENDING: 'pending', BLOCKED: 'blocked', REJECTED: 'rejected', FULFILLED: 'fulfilled' }; /** * 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. * * @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 * @implements {promise.Thenable<T>} * @template T * @see http://promises-aplus.github.io/promises-spec/ */ promise.Promise = 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<!Callback>} */ this.callbacks_ = null; /** @private {PromiseState} */ this.state_ = PromiseState.PENDING; /** @private {boolean} */ this.handled_ = false; /** @private {boolean} */ this.pendingNotifications_ = false; /** @private {*} */ this.value_ = undefined; 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); } }; promise.Thenable.addImplementation(promise.Promise); /** @override */ promise.Promise.prototype.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 */ promise.Promise.prototype.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 throw new TypeError('A promise may not resolve to itself'); } this.parent_ = null; this.state_ = PromiseState.BLOCKED; 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 */ promise.Promise.prototype.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 */ promise.Promise.prototype.unblockAndResolve_ = function(newState, newValue) { if (this.state_ === PromiseState.BLOCKED) { this.state_ = PromiseState.PENDING; this.resolve_(newState, newValue); } }; /** * @private */ promise.Promise.prototype.scheduleNotifications_ = function() { if (!this.pendingNotifications_) { this.pendingNotifications_ = true; this.flow_.suspend_(); var activeFrame; if (!this.handled_ && this.state_ === PromiseState.REJECTED && !(this.value_ instanceof promise.CancellationError)) { activeFrame = this.flow_.getActiveFrame_(); activeFrame.pendingRejection = true; } if (this.callbacks_ && this.callbacks_.length) { activeFrame = this.flow_.getRunningFrame_(); var self = this; this.callbacks_.forEach(function(callback) { if (!callback.frame_.getParent()) { activeFrame.addChild(callback.frame_); } }); } asyncRun(goog.bind(this.notifyAll_, this, activeFrame)); } }; /** * Notifies all of the listeners registered with this promise that its state * has changed. * @param {Frame} frame The active frame from when this round of * notifications were scheduled. * @private */ promise.Promise.prototype.notifyAll_ = function(frame) { this.flow_.resume_(); this.pendingNotifications_ = false; if (!this.handled_ && this.state_ === PromiseState.REJECTED && !(this.value_ instanceof promise.CancellationError)) { this.flow_.abortFrame_(this.value_, frame); } if (this.callbacks_) { var callbacks = this.callbacks_; this.callbacks_ = null; callbacks.forEach(this.notify_, this); } }; /** * Notifies a single callback of this promise's change ins tate. * @param {Callback} callback The callback to notify. * @private */ promise.Promise.prototype.notify_ = function(callback) { callback.notify(this.state_, this.value_); }; /** @override */ promise.Promise.prototype.cancel = function(opt_reason) { if (!this.isPending()) { return; } if (this.parent_) { this.parent_.cancel(opt_reason); } else { this.resolve_( PromiseState.REJECTED, promise.CancellationError.wrap(opt_reason)); } }; /** @override */ promise.Promise.prototype.isPending = function() { return this.state_ === PromiseState.PENDING; }; /** @override */ promise.Promise.prototype.then = function(opt_callback, opt_errback) { return this.addCallback_( opt_callback, opt_errback, 'then', promise.Promise.prototype.then); }; /** @override */ promise.Promise.prototype.thenCatch = function(errback) { return this.addCallback_( null, errback, 'thenCatch', promise.Promise.prototype.thenCatch); }; /** @override */ promise.Promise.prototype.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 */ promise.Promise.prototype.addCallback_ = function(callback, errback, name, fn) { if (!goog.isFunction(callback) && !goog.isFunction(errback)) { return this; } this.handled_ = true; var cb = new Callback(this, callback, errback, name, fn); if (!this.callbacks_) { this.callbacks_ = []; } this.callbacks_.push(cb); if (this.state_ !== PromiseState.PENDING && this.state_ !== PromiseState.BLOCKED) { this.flow_.getSchedulingFrame_().addChild(cb.frame_); this.scheduleNotifications_(); } return cb.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. * * @param {promise.ControlFlow=} opt_flow The control flow * this instance was created under. This should only be provided during * unit tests. * @constructor * @implements {promise.Thenable<T>} * @template T */ promise.Deferred = 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); }; }; promise.Thenable.addImplementation(promise.Deferred); /** @override */ promise.Deferred.prototype.isPending = function() { return this.promise.isPending(); }; /** @override */ promise.Deferred.prototype.cancel = function(opt_reason) { this.promise.cancel(opt_reason); }; /** * @override * @deprecated Use {@code then} from the promise property directly. */ promise.Deferred.prototype.then = function(opt_cb, opt_eb) { return this.promise.then(opt_cb, opt_eb); }; /** * @override * @deprecated Use {@code thenCatch} from the promise property directly. */ promise.Deferred.prototype.thenCatch = function(opt_eb) { return this.promise.thenCatch(opt_eb); }; /** * @override * @deprecated Use {@code thenFinally} from the promise property directly. */ promise.Deferred.prototype.thenFinally = function(opt_cb) { return this.promise.thenFinally(opt_cb); }; /** * 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 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. */ promise.asap = function(value, callback, opt_errback) { if (promise.isPromise(value)) { value.then(callback, opt_errback); // Maybe a Dojo-like deferred object? } else if (!!value && goog.isObject(value) && goog.isFunction(value.addCallbacks)) { value.addCallbacks(callback, opt_errback); // A raw value, return a resolved promise. } else if (callback) { callback(value); } }; /** * Given an array of promises, will return a promise that will be fulfilled * with the fulfillment values of the input array's values. If any of the * input array's promises are rejected, the returned promise will be rejected * with the same reason. * * @param {!Array<(T|!promise.Promise<T>)>} arr An array of * promises to wait on. * @return {!promise.Promise<!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 */ promise.all = function(arr) { return new promise.Promise(function(fulfill, reject) { var n = arr.length; var values = []; if (!n) { fulfill(values); return; } var toFulfill = n; var onFulfilled = function(index, value) { values[index] = value; toFulfill--; if (toFulfill == 0) { fulfill(values); } }; for (var i = 0; i < n; ++i) { promise.asap(arr[i], goog.partial(onFulfilled, i), reject); } }); }; /** * Calls a function for each element in an array and inserts the result into a * new array, which is used as the fulfillment value of the promise returned * by this function. * * If the return value of the mapping function is a promise, this function * will wait for it to be fulfilled before inserting it into the new array. * * If the mapping function throws or returns a rejected promise, the * promise returned by this function will be rejected with the same reason. * Only the first failure will be reported; all subsequent errors will be * silently ignored. * * @param {!(Array<TYPE>|promise.Promise<!Array<TYPE>>)} arr The * array to iterator over, or a promise that will resolve to said array. * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The * function to call for each element in the array. This function should * expect three arguments (the element, the index, and the array itself. * @param {SELF=} opt_self The object to be used as the value of 'this' within * {@code fn}. * @template TYPE, SELF */ promise.map = function(arr, fn, opt_self) { return promise.fulfilled(arr).then(function(arr) { goog.asserts.assertNumber(arr.length, 'not an array like value'); return new promise.Promise(function(fulfill, reject) { var n = arr.length; var values = new Array(n); (function processNext(i) { for (; i < n; i++) { if (i in arr) { break; } } if (i >= n) { fulfill(values); return; } try { promise.asap( fn.call(opt_self, arr[i], i, /** @type {!Array} */(arr)), function(value) { values[i] = value; processNext(i + 1); }, reject); } catch (ex) { reject(ex); } })(0); }); }); }; /** * Calls a function for each element in an array, and if the function returns * true adds the element to a new array. * * If the return value of the filter function is a promise, this function * will wait for it to be fulfilled before determining whether to insert the * element into the new array. * * If the filter function throws or returns a rejected promise, the promise * returned by this function will be rejected with the same reason. Only the * first failure will be reported; all subsequent errors will be silently * ignored. * * @param {!(Array<TYPE>|promise.Promise<!Array<TYPE>>)} arr The * array to iterator over, or a promise that will resolve to said array. * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ( * boolean|promise.Promise<boolean>)} fn The function * to call for each element in the array. * @param {SELF=} opt_self The object to be used as the value of 'this' within * {@code fn}. * @template TYPE, SELF */ promise.filter = function(arr, fn, opt_self) { return promise.fulfilled(arr).then(function(arr) { goog.asserts.assertNumber(arr.length, 'not an array like value'); return new promise.Promise(function(fulfill, reject) { var n = arr.length; var values = []; var valuesLength = 0; (function processNext(i) { for (; i < n; i++) { if (i in arr) { break; } } if (i >= n) { fulfill(values); return; } try { var value = arr[i]; var include = fn.call(opt_self, value, i, /** @type {!Array} */(arr)); promise.asap(include, function(include) { if (include) { values[valuesLength++] = value; } processNext(i + 1); }, reject); } catch (ex) { reject(ex); } })(0); }); }); }; /** * Returns a promise that will be resolved with the input value in a * fully-resolved state. If the value is an array, each element will be fully * resolved. Likewise, if the value is an object, all keys will be fully * resolved. In both cases, all nested arrays and objects will also be * fully resolved. All fields are resolved in place; the returned promise will * resolve on {@code value} and not a copy. * * Warning: This function makes no checks against objects that contain * cyclical references: * * var value = {}; * value['self'] = value; * promise.fullyResolved(value); // Stack overflow. * * @param {*} value The value to fully resolve. * @return {!promise.Promise} A promise for a fully resolved version * of the input value. */ promise.fullyResolved = function(value) { if (promise.isPromise(value)) { return promise.when(value, fullyResolveValue); } return fullyResolveValue(value); }; /** * @param {*} value The value to fully resolve. If a promise, assumed to * already be resolved. * @return {!promise.Promise} A promise for a fully resolved version * of the input value. */ function fullyResolveValue(value) { switch (goog.typeOf(value)) { case 'array': return fullyResolveKeys(/** @type {!Array} */ (value)); case 'object': if (promise.isPromise(value)) { // We get here when the original input value is a promise that // resolves to itself. When the user provides us with such a promise, // trust that it counts as a "fully resolved" value and return it. // Of course, since it's already a promise, we can just return it // to the user instead of wrapping it in another promise. return /** @type {!promise.Promise} */ (value); } if (goog.isNumber(value.nodeType) && goog.isObject(value.ownerDocument) && goog.isNumber(value.ownerDocument.nodeType)) { // DOM node; return early to avoid infinite recursion. Should we // only support objects with a certain level of nesting? return promise.fulfilled(value); } return fullyResolveKeys(/** @type {!Object} */ (value)); default: // boolean, function, null, number, string, undefined return promise.fulfilled(value); } }; /** * @param {!(Array|Object)} obj the object to resolve. * @return {!promise.Promise} A promise that will be resolved with the * input object once all of its values have been fully resolved. */ function fullyResolveKeys(obj) { var isArray = goog.isArray(obj); var numKeys = isArray ? obj.length : Objects.getCount(obj); if (!numKeys) { return promise.fulfilled(obj); } var numResolved = 0; return new promise.Promise(function(fulfill, reject) { // In pre-IE9, goog.array.forEach will not iterate properly over arrays // containing undefined values because "index in array" returns false // when array[index] === undefined (even for x = [undefined, 1]). To get // around this, we need to use our own forEach implementation. // DO NOT REMOVE THIS UNTIL WE NO LONGER SUPPORT IE8. This cannot be // reproduced in IE9 by changing the browser/document modes, it requires an // actual pre-IE9 browser. Yay, IE! var forEachKey = !isArray ? Objects.forEach : function(arr, fn) { var n = arr.length; for (var i = 0; i < n; ++i) { fn.call(null, arr[i], i, arr); } }; forEachKey(obj, function(partialValue, key) { var type = goog.typeOf(partialValue); if (type != 'array' && type != 'object') { maybeResolveValue(); return; } promise.fullyResolved(partialValue).then( function(resolvedValue) { obj[key] = resolvedValue; maybeResolveValue(); }, reject); }); function maybeResolveValue() { if (++numResolved == numKeys) { fulfill(obj); } } }); }; ////////////////////////////////////////////////////////////////////////////// // // promise.ControlFlow // ////////////////////////////////////////////////////////////////////////////// /** * Handles the execution of scheduled tasks, each of which may be an * asynchronous operation. The control flow will ensure tasks are executed in * the ordered scheduled, starting each task only once those before it have * completed. * * Each task scheduled within this flow may return a * {@link webdriver.promise.Promise} to indicate it is an asynchronous * operation. The ControlFlow will wait for such promises to be resolved before * marking the task as completed. * * Tasks and each callback registered on a {@link webdriver.promise.Promise} * will be run in their own ControlFlow frame. Any tasks scheduled within a * frame will take priority over previously scheduled tasks. Furthermore, if any * of the tasks in the frame fail, the remainder of the tasks in that frame will * be discarded and the failure will be propagated to the user through the * callback/task's promised result. * * Each time a ControlFlow empties its task queue, it will fire an * {@link webdriver.promise.ControlFlow.EventType.IDLE IDLE} event. Conversely, * whenever the flow terminates due to an unhandled error, it will remove all * remaining tasks in its queue and fire an * {@link webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION * UNCAUGHT_EXCEPTION} event. If there are no listeners registered with the * flow, the error will be rethrown to the global error handler. * * @constructor * @extends {EventEmitter} * @final */ promise.ControlFlow = function() { EventEmitter.call(this); goog.getUid(this); /** * Tracks the active execution frame for this instance. Lazily initialized * when the first task is scheduled. * @private {Frame} */ this.activeFrame_ = null; /** * A reference to the frame which is currently top of the stack in * {@link #runInFrame_}. The {@link #activeFrame_} will always be an ancestor * of the {@link #runningFrame_}, but the two will often not be the same. The * active frame represents which frame is currently executing a task while the * running frame represents either the task itself or a promise callback which * has fired asynchronously. * @private {Frame} */ this.runningFrame_ = null; /** * A reference to the frame in which new tasks should be scheduled. If * {@code null}, tasks will be scheduled within the active frame. When forcing * a function to run in the context of a new frame, this pointer is used to * ensure tasks are scheduled within the newly created frame, even though it * won't be active yet. * @private {Frame} * @see {#runInFrame_} */ this.schedulingFrame_ = null; /** * Micro task that controls shutting down the control flow. Upon shut down, * the flow will emit an {@link webdriver.promise.ControlFlow.EventType.IDLE} * event. Idle events always follow a brief timeout in order to catch latent * errors from the last completed task. If this task had a callback * registered, but no errback, and the task fails, the unhandled failure would * not be reported by the promise system until the next turn of the event * loop: * * // Schedule 1 task that fails. * var result = promise.controlFlow().schedule('example', * function() { return promise.rejected('failed'); }); * // Set a callback on the result. This delays reporting the unhandled * // failure for 1 turn of the event loop. * result.then(goog.nullFunction); * * @private {MicroTask} */ this.shutdownTask_ = null; /** * Micro task used to trigger execution of this instance's event loop. * @private {MicroTask} */ this.eventLoopTask_ = null; /** * ID for a long running interval used to keep a Node.js process running * while a control flow's event loop has yielded. This is a cheap hack * required since the {@link #runEventLoop_} is only scheduled to run when * there is _actually_ something to run. When a control flow is waiting on * a task, there will be nothing in the JS event loop and the process would * terminate without this. * * An alternative solution would be to change {@link #runEventLoop_} to run * as an interval rather than as on-demand micro-tasks. While this approach * (which was previously used) requires fewer micro-task allocations, it * results in many unnecessary invocations of {@link #runEventLoop_}. * * @private {?number} */ this.hold_ = null; /** * The number of holds placed on this flow. These represent points where the * flow must not execute any further actions so an asynchronous action may * run first. One such example are notifications fired by a * {@link webdriver.promise.Promise}: the Promise spec requires that callbacks * are invoked in a turn of the event loop after they are scheduled. To ensure * tasks within a callback are scheduled in the correct frame, a promise will * make the parent flow yield before its notifications are fired. * @private {number} */ this.yieldCount_ = 0; }; goog.inherits(promise.ControlFlow, EventEmitter); /** * Events that may be emitted by an {@link webdriver.promise.ControlFlow}. * @enum {string} */ promise.ControlFlow.EventType = { /** Emitted when all tasks have been successfully executed. */ IDLE: 'idle', /** Emitted when a ControlFlow has been reset. */ RESET: 'reset', /** Emitted whenever a new task has been scheduled. */ SCHEDULE_TASK: 'scheduleTask', /** * Emitted whenever a control flow aborts due to an unhandled promise * rejection. This event will be emitted along with the offending rejection * reason. Upon emitting this event, the control flow will empty its task * queue and revert to its initial state. */ UNCAUGHT_EXCEPTION: 'uncaughtException' }; /** * Returns a string representation of this control flow, which is its current * {@link #getSchedule() schedule}, sans task stack traces. * @return {string} The string representation of this contorl flow. * @override */ promise.ControlFlow.prototype.toString = function() { return this.getSchedule(); }; /** * Resets this instance, clearing its queue and removing all event listeners. */ promise.ControlFlow.prototype.reset = function() { this.activeFrame_ = null; this.schedulingFrame_ = null; this.emit(promise.ControlFlow.EventType.RESET); this.removeAllListeners(); this.cancelShutdown_(); this.cancelEventLoop_(); }; /** * Generates an annotated string describing the internal state of this control * flow, including the currently executing as well as pending tasks. If * {@code opt_includeStackTraces === true}, the string will include the * stack trace from when each task was scheduled. * @param {string=} opt_includeStackTraces Whether to include the stack traces * from when each task was scheduled. Defaults to false. * @return {string} String representation of this flow's internal state. */ promise.ControlFlow.prototype.getSchedule = function(opt_includeStackTraces) { var ret = 'ControlFlow::' + goog.getUid(this); var activeFrame = this.activeFrame_; var runningFrame = this.runningFrame_; if (!activeFrame) { return ret; } var childIndent = '| '; return ret + '\n' + toStringHelper(activeFrame.getRoot(), childIndent); /** * @param {!(Frame|Task)} node . * @param {string} indent . * @param {boolean=} opt_isPending . * @return {string} . */ function toStringHelper(node, indent, opt_isPending) { var ret = node.toString(); if (opt_isPending) { ret = '(pending) ' + ret; } if (node === activeFrame) { ret = '(active) ' + ret; } if (node === runningFrame) { ret = '(running) ' + ret; } if (node instanceof Frame) { if (node.getPendingTask()) { ret += '\n' + toStringHelper( /** @type {!Task} */(node.getPendingTask()), childIndent, true); } if (node.children_) { node.children_.forEach(function(child) { if (!node.getPendingTask() || node.getPendingTask().getFrame() !== child) { ret += '\n' + toStringHelper(child, childIndent); } }); } } else { var task = /** @type {!Task} */(node); if (opt_includeStackTraces && task.promise.stack_) { ret += '\n' + childIndent + (task.promise.stack_.stack || task.promise.stack_). replace(/\n/g, '\n' + childIndent); } if (task.getFrame()) { ret += '\n' + toStringHelper( /** @type {!Frame} */(task.getFrame()), childIndent); } } return indent + ret.replace(/\n/g, '\n' + indent); } }; /** * @return {!Frame} The active frame for this flow. * @private */ promise.ControlFlow.prototype.getActiveFrame_ = function() { this.cancelShutdown_(); if (!this.activeFrame_) { this.activeFrame_ = new Frame(this); this.activeFrame_.once(Frame.ERROR_EVENT, this.abortNow_, this); this.scheduleEventLoopStart_(); } return this.activeFrame_; }; /** * @return {!Frame} The frame that new items should be added to. * @private */ promise.ControlFlow.prototype.getSchedulingFrame_ = function() { return this.schedulingFrame_ || this.getActiveFrame_(); }; /** * @return {!Frame} The frame that is current executing. * @private */ promise.ControlFlow.prototype.getRunningFrame_ = function() { return this.runningFrame_ || this.getActiveFrame_(); }; /** * 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 webdriver.promise.consume}. * * @param {function(): (T|promise.Promise<T>)} fn The function to * call to start the task. If the function returns a * {@link webdriver.promise.Promise}, this instance will wait for it to be * resolved before starting the next task. * @param {string=} opt_description A description of the task. * @return {!promise.Promise<T>} A promise that will be resolved * with the result of the action. * @template T */ promise.ControlFlow.prototype.execute = function(fn, opt_description) { if (promise.isGenerator(fn)) { fn = goog.partial(promise.consume, fn); } if (!this.hold_) { var holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js this.hold_ = setInterval(goog.nullFunction, holdIntervalMs); } var description = opt_description || '<anonymous>'; var task = new Task(this, fn, description); task.promise.stack_ = promise.captureStackTrace('Task', description, promise.ControlFlow.prototype.execute); this.getSchedulingFrame_().addChild(task); this.emit(promise.ControlFlow.EventType.SCHEDULE_TASK, opt_description); this.scheduleEventLoopStart_(); 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 {!promise.Promise} A promise that will be resolved with * the result of the action. */ promise.ControlFlow.prototype.timeout = function(ms, opt_description) { return this.execute(function() { return promise.delayed(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 Promise, 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 {(!promise.Promise<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 {!promise.Promise<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 */ promise.ControlFlow.prototype.wait = function( condition, opt_timeout, opt_message) { var timeout = opt_timeout || 0; if (!goog.isNumber(timeout) || timeout < 0) { throw TypeError('timeout must be a number >= 0: ' + timeout); } if (promise.isPromise(condition)) { return this.execute(function() { if (!timeout) { return condition; } return new promise.Promise(function(fulfill, reject) { var start = goog.now(); var timer = setTimeout(function() { timer = null; reject(Error((opt_message ? opt_message + '\n' : '') + 'Timed out waiting for promise to resolve after ' + (goog.now() - start) + 'ms')); }, timeout);