# lib/webdriver/promise.js

    1// Copyright 2011 Software Freedom Conservancy. All Rights Reserved.2//3// Licensed under the Apache License, Version 2.0 (the "License");4// you may not use this file except in compliance with the License.5// You may obtain a copy of the License at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// distributed under the License is distributed on an "AS IS" BASIS,11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12// See the License for the specific language governing permissions and13// limitations under the License.1415/**16 * @license Portions of this code are from the Dojo toolkit, received under the17 * BSD License:18 * Redistribution and use in source and binary forms, with or without19 * modification, are permitted provided that the following conditions are met:20 *21 *   * Redistributions of source code must retain the above copyright notice,22 *     this list of conditions and the following disclaimer.23 *   * Redistributions in binary form must reproduce the above copyright notice,24 *     this list of conditions and the following disclaimer in the documentation25 *     and/or other materials provided with the distribution.26 *   * Neither the name of the Dojo Foundation nor the names of its contributors27 *     may be used to endorse or promote products derived from this software28 *     without specific prior written permission.29 *30 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"31 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE32 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE33 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE34 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR35 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF36 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS37 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN38 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)39 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE40 * POSSIBILITY OF SUCH DAMAGE.41 */4243/**44 * @fileoverview A promise implementation based on the CommonJS promise/A and45 * promise/B proposals. For more information, see46 * http://wiki.commonjs.org/wiki/Promises.47 */4849goog.provide('webdriver.promise');50goog.provide('webdriver.promise.ControlFlow');51goog.provide('webdriver.promise.ControlFlow.Timer');52goog.provide('webdriver.promise.Deferred');53goog.provide('webdriver.promise.Promise');54goog.provide('webdriver.promise.Thenable');5556goog.require('goog.array');57goog.require('goog.debug.Error');58goog.require('goog.object');59goog.require('webdriver.EventEmitter');60goog.require('webdriver.stacktrace.Snapshot');61626364/**65 * Error used when the computation of a promise is cancelled.66 *67 * @param {string=} opt_msg The cancellation message.68 * @constructor69 * @extends {goog.debug.Error}70 * @final71 */72webdriver.promise.CancellationError = function(opt_msg) {73  goog.debug.Error.call(this, opt_msg);7475  /** @override */76  this.name = 'CancellationError';77};78goog.inherits(webdriver.promise.CancellationError, goog.debug.Error);79808182/**83 * Thenable is a promise-like object with a {@code then} method which may be84 * used to schedule callbacks on a promised value.85 *86 * @interface87 * @template T88 */89webdriver.promise.Thenable = function() {};909192/**93 * Cancels the computation of this promise's value, rejecting the promise in the94 * process. This method is a no-op if the promise has alreayd been resolved.95 *96 * @param {string=} opt_reason The reason this promise is being cancelled.97 */98webdriver.promise.Thenable.prototype.cancel = function(opt_reason) {};99100101/** @return {boolean} Whether this promise's value is still being computed. */102webdriver.promise.Thenable.prototype.isPending = function() {};103104105/**106 * Registers listeners for when this instance is resolved.107 *108 * @param {?(function(T): (R|webdriver.promise.Promise.<R>))=} opt_callback The109 *     function to call if this promise is successfully resolved. The function110 *     should expect a single argument: the promise's resolved value.111 * @param {?(function(*): (R|webdriver.promise.Promise.<R>))=} opt_errback The112 *     function to call if this promise is rejected. The function should expect113 *     a single argument: the rejection reason.114 * @return {!webdriver.promise.Promise.<R>} A new promise which will be115 *     resolved with the result of the invoked callback.116 * @template R117 */118webdriver.promise.Thenable.prototype.then = function(119    opt_callback, opt_errback) {};120121122/**123 * Registers a listener for when this promise is rejected. This is synonymous124 * with the {@code catch} clause in a synchronous API:125 * <pre><code>126 *   // Synchronous API:127 *   try {128 *     doSynchronousWork();129 *   } catch (ex) {130 *     console.error(ex);131 *   }132 *133 *   // Asynchronous promise API:134 *   doAsynchronousWork().thenCatch(function(ex) {135 *     console.error(ex);136 *   });137 * </code></pre>138 *139 * @param {function(*): (R|webdriver.promise.Promise.<R>)} errback The function140 *     to call if this promise is rejected. The function should expect a single141 *     argument: the rejection reason.142 * @return {!webdriver.promise.Promise.<R>} A new promise which will be143 *     resolved with the result of the invoked callback.144 * @template R145 */146webdriver.promise.Thenable.prototype.thenCatch = function(errback) {};147148149/**150 * Registers a listener to invoke when this promise is resolved, regardless151 * of whether the promise's value was successfully computed. This function152 * is synonymous with the {@code finally} clause in a synchronous API:153 * <pre><code>154 *   // Synchronous API:155 *   try {156 *     doSynchronousWork();157 *   } finally {158 *     cleanUp();159 *   }160 *161 *   // Asynchronous promise API:162 *   doAsynchronousWork().thenFinally(cleanUp);163 * </code></pre>164 *165 * <b>Note:</b> similar to the {@code finally} clause, if the registered166 * callback returns a rejected promise or throws an error, it will silently167 * replace the rejection error (if any) from this promise:168 * <pre><code>169 *   try {170 *     throw Error('one');171 *   } finally {172 *     throw Error('two');  // Hides Error: one173 *   }174 *175 *   webdriver.promise.rejected(Error('one'))176 *       .thenFinally(function() {177 *         throw Error('two');  // Hides Error: one178 *       });179 * </code></pre>180 *181 *182 * @param {function(): (R|webdriver.promise.Promise.<R>)} callback The function183 *     to call when this promise is resolved.184 * @return {!webdriver.promise.Promise.<R>} A promise that will be fulfilled185 *     with the callback result.186 * @template R187 */188webdriver.promise.Thenable.prototype.thenFinally = function(callback) {};189190191/**192 * Property used to flag constructor's as implementing the Thenable interface193 * for runtime type checking.194 * @private {string}195 * @const196 */197webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_ = '$webdriver_Thenable';198199200/**201 * Adds a property to a class prototype to allow runtime checks of whether202 * instances of that class implement the Thenable interface. This function will203 * also ensure the prototype's {@code then} function is exported from compiled204 * code.205 * @param {function(new: webdriver.promise.Thenable, ...[?])} ctor The206 *     constructor whose prototype to modify.207 */208webdriver.promise.Thenable.addImplementation = function(ctor) {209  // Based on goog.promise.Thenable.isImplementation.210  ctor.prototype['then'] = ctor.prototype.then;211  try {212    // Old IE7 does not support defineProperty; IE8 only supports it for213    // DOM elements.214    Object.defineProperty(215        ctor.prototype,216        webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_,217        {'value': true, 'enumerable': false});218  } catch (ex) {219    ctor.prototype[webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_] = true;220  }221};222223224/**225 * Checks if an object has been tagged for implementing the Thenable interface226 * as defined by {@link webdriver.promise.Thenable.addImplementation}.227 * @param {*} object The object to test.228 * @return {boolean} Whether the object is an implementation of the Thenable229 *     interface.230 */231webdriver.promise.Thenable.isImplementation = function(object) {232  // Based on goog.promise.Thenable.isImplementation.233  if (!object) {234    return false;235  }236  try {237    return !!object[webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_];238  } catch (e) {239    return false;  // Property access seems to be forbidden.240  }241};242243244245/**246 * Represents the eventual value of a completed operation. Each promise may be247 * in one of three states: pending, resolved, or rejected. Each promise starts248 * in the pending state and may make a single transition to either a249 * fulfilled or rejected state, at which point the promise is considered250 * resolved.251 *252 * @constructor253 * @implements {webdriver.promise.Thenable.<T>}254 * @template T255 * @see http://promises-aplus.github.io/promises-spec/256 */257webdriver.promise.Promise = function() {};258webdriver.promise.Thenable.addImplementation(webdriver.promise.Promise);259260261/** @override */262webdriver.promise.Promise.prototype.cancel = function(reason) {263  throw new TypeError('Unimplemented function: "cancel"');264};265266267/** @override */268webdriver.promise.Promise.prototype.isPending = function() {269  throw new TypeError('Unimplemented function: "isPending"');270};271272273/** @override */274webdriver.promise.Promise.prototype.then = function(275    opt_callback, opt_errback) {276  throw new TypeError('Unimplemented function: "then"');277};278279280/** @override */281webdriver.promise.Promise.prototype.thenCatch = function(errback) {282  return this.then(null, errback);283};284285286/** @override */287webdriver.promise.Promise.prototype.thenFinally = function(callback) {288  return this.then(callback, function(err) {289    var value = callback();290    if (webdriver.promise.isPromise(value)) {291      return value.then(function() {292        throw err;293      });294    }295    throw err;296  });297};298299300301/**302 * Represents a value that will be resolved at some point in the future. This303 * class represents the protected "producer" half of a Promise - each Deferred304 * has a {@code promise} property that may be returned to consumers for305 * registering callbacks, reserving the ability to resolve the deferred to the306 * producer.307 *308 * <p>If this Deferred is rejected and there are no listeners registered before309 * the next turn of the event loop, the rejection will be passed to the310 * {@link webdriver.promise.ControlFlow} as an unhandled failure.311 *312 * <p>If this Deferred is cancelled, the cancellation reason will be forward to313 * the Deferred's canceller function (if provided). The canceller may return a314 * truth-y value to override the reason provided for rejection.315 *316 * @param {webdriver.promise.ControlFlow=} opt_flow The control flow317 *     this instance was created under. This should only be provided during318 *     unit tests.319 * @constructor320 * @extends {webdriver.promise.Promise.<T>}321 * @template T322 */323webdriver.promise.Deferred = function(opt_flow) {324  /* NOTE: This class's implementation diverges from the prototypical style325   * used in the rest of the atoms library. This was done intentionally to326   * protect the internal Deferred state from consumers, as outlined by327   *     http://wiki.commonjs.org/wiki/Promises328   */329  goog.base(this);330331  var flow = opt_flow || webdriver.promise.controlFlow();332333  /**334   * The deferred this instance is chained from, if any.335   * @private {webdriver.promise.Deferred.<?>}336   */337  this.parent_ = null;338339  /**340   * The listeners registered with this Deferred. Each element in the list will341   * be a 3-tuple of the callback function, errback function, and the342   * corresponding deferred object.343   * @type {!Array.<!webdriver.promise.Deferred.Listener_>}344   */345  var listeners = [];346347  /**348   * Whether this Deferred's resolution was ever handled by a listener.349   * If the Deferred is rejected and its value is not handled by a listener350   * before the next turn of the event loop, the error will be passed to the351   * global error handler.352   * @type {boolean}353   */354  var handled = false;355356  /**357   * Key for the timeout used to delay reproting an unhandled rejection to the358   * parent {@link webdriver.promise.ControlFlow}.359   * @type {?number}360   */361  var pendingRejectionKey = null;362363  /**364   * This Deferred's current state.365   * @type {!webdriver.promise.Deferred.State_}366   */367  var state = webdriver.promise.Deferred.State_.PENDING;368369  /**370   * This Deferred's resolved value; set when the state transitions from371   * {@code webdriver.promise.Deferred.State_.PENDING}.372   * @type {*}373   */374  var value;375376  /** @return {boolean} Whether this promise's value is still pending. */377  function isPending() {378    return state == webdriver.promise.Deferred.State_.PENDING;379  }380381  /**382   * Removes all of the listeners previously registered on this deferred.383   * @throws {Error} If this deferred has already been resolved.384   */385  function removeAll() {386    listeners = [];387  }388389  /**390   * Resolves this deferred. If the new value is a promise, this function will391   * wait for it to be resolved before notifying the registered listeners.392   * @param {!webdriver.promise.Deferred.State_} newState The deferred's new393   *     state.394   * @param {*} newValue The deferred's new value.395   */396  function resolve(newState, newValue) {397    if (webdriver.promise.Deferred.State_.PENDING !== state) {398      return;399    }400401    if (newValue === self) {402      // See promise a+, 2.3.1403      // http://promises-aplus.github.io/promises-spec/#point-48404      throw TypeError('A promise may not resolve to itself');405    }406407    state = webdriver.promise.Deferred.State_.BLOCKED;408409    if (webdriver.promise.isPromise(newValue)) {410      var onFulfill = goog.partial(notifyAll, newState);411      var onReject = goog.partial(412          notifyAll, webdriver.promise.Deferred.State_.REJECTED);413      if (newValue instanceof webdriver.promise.Deferred) {414        newValue.then(onFulfill, onReject);415      } else {416        webdriver.promise.asap(newValue, onFulfill, onReject);417      }418419    } else {420      notifyAll(newState, newValue);421    }422  }423424  /**425   * Notifies all of the listeners registered with this Deferred that its state426   * has changed.427   * @param {!webdriver.promise.Deferred.State_} newState The deferred's new428   *     state.429   * @param {*} newValue The deferred's new value.430   */431  function notifyAll(newState, newValue) {432    if (newState === webdriver.promise.Deferred.State_.REJECTED &&433        // We cannot check instanceof Error since the object may have been434        // created in a different JS context.435        goog.isObject(newValue) && goog.isString(newValue.message)) {436      newValue = flow.annotateError(/** @type {!Error} */(newValue));437    }438439    state = newState;440    value = newValue;441    while (listeners.length) {442      notify(listeners.shift());443    }444445    if (!handled && state == webdriver.promise.Deferred.State_.REJECTED &&446        !(value instanceof webdriver.promise.CancellationError)) {447      flow.pendingRejections_ += 1;448      pendingRejectionKey = flow.timer.setTimeout(function() {449        pendingRejectionKey = null;450        flow.pendingRejections_ -= 1;451        flow.abortFrame_(value);452      }, 0);453    }454  }455456  /**457   * Notifies a single listener of this Deferred's change in state.458   * @param {!webdriver.promise.Deferred.Listener_} listener The listener to459   *     notify.460   */461  function notify(listener) {462    var func = state == webdriver.promise.Deferred.State_.RESOLVED ?463        listener.callback : listener.errback;464    if (func) {465      flow.runInNewFrame_(goog.partial(func, value),466          listener.fulfill, listener.reject);467    } else if (state == webdriver.promise.Deferred.State_.REJECTED) {468      listener.reject(value);469    } else {470      listener.fulfill(value);471    }472  }473474  /**475   * The consumer promise for this instance. Provides protected access to the476   * callback registering functions.477   * @type {!webdriver.promise.Promise.<T>}478   */479  var promise = new webdriver.promise.Promise();480481  var self = this;482483  /**484   * Registers a callback on this Deferred.485   *486   * @param {?(function(T): (R|webdriver.promise.Promise.<R>))=} opt_callback .487   * @param {?(function(*): (R|webdriver.promise.Promise.<R>))=} opt_errback .488   * @return {!webdriver.promise.Promise.<R>} A new promise representing the489   *     result of the callback.490   * @template R491   * @see webdriver.promise.Promise#then492   */493  function then(opt_callback, opt_errback) {494    // Avoid unnecessary allocations if we weren't given any callback functions.495    if (!opt_callback && !opt_errback) {496      return promise;497    }498499    // The moment a listener is registered, we consider this deferred to be500    // handled; the callback must handle any rejection errors.501    handled = true;502    if (pendingRejectionKey !== null) {503      flow.pendingRejections_ -= 1;504      flow.timer.clearTimeout(pendingRejectionKey);505      pendingRejectionKey = null;506    }507508    var deferred = new webdriver.promise.Deferred(flow);509    deferred.parent_ = self;510511    var listener = {512      callback: opt_callback,513      errback: opt_errback,514      fulfill: deferred.fulfill,515      reject: deferred.reject516    };517518    if (state == webdriver.promise.Deferred.State_.PENDING ||519        state == webdriver.promise.Deferred.State_.BLOCKED) {520      listeners.push(listener);521    } else {522      notify(listener);523    }524525    return deferred.promise;526  }527528  /**529   * Resolves this promise with the given value. If the value is itself a530   * promise and not a reference to this deferred, this instance will wait for531   * it before resolving.532   * @param {T=} opt_value The fulfilled value.533   */534  function fulfill(opt_value) {535    resolve(webdriver.promise.Deferred.State_.RESOLVED, opt_value);536  }537538  /**539   * Rejects this promise. If the error is itself a promise, this instance will540   * be chained to it and be rejected with the error's resolved value.541   * @param {*=} opt_error The rejection reason, typically either a542   *     {@code Error} or a {@code string}.543   */544  function reject(opt_error) {545    resolve(webdriver.promise.Deferred.State_.REJECTED, opt_error);546  }547548  /**549   * Attempts to cancel the computation of this instance's value. This attempt550   * will silently fail if this instance has already resolved.551   * @param {string=} opt_reason The reason for cancelling this promise.   */552  function cancel(opt_reason) {553    if (!isPending()) {554      return;555    }556557    if (self.parent_) {558      self.parent_.cancel(opt_reason);559    } else {560      reject(new webdriver.promise.CancellationError(opt_reason));561    }562  }563564  this.promise = promise;565  this.promise.then = this.then = then;566  this.promise.cancel = this.cancel = cancel;567  this.promise.isPending = this.isPending = isPending;568  this.fulfill = fulfill;569  this.reject = this.errback = reject;570571  // Only expose this function to our internal classes.572  // TODO: find a cleaner way of handling this.573  if (this instanceof webdriver.promise.Task_) {574    this.removeAll = removeAll;575  }576577  // Export symbols necessary for the contract on this object to work in578  // compiled mode.579  goog.exportProperty(this, 'then', this.then);580  goog.exportProperty(this, 'cancel', cancel);581  goog.exportProperty(this, 'fulfill', fulfill);582  goog.exportProperty(this, 'reject', reject);583  goog.exportProperty(this, 'isPending', isPending);584  goog.exportProperty(this, 'promise', this.promise);585  goog.exportProperty(this.promise, 'then', this.then);586  goog.exportProperty(this.promise, 'cancel', cancel);587  goog.exportProperty(this.promise, 'isPending', isPending);588};589goog.inherits(webdriver.promise.Deferred, webdriver.promise.Promise);590591592/**593 * Type definition for a listener registered on a Deferred object.594 * @typedef {{callback:(Function|undefined),595 *            errback:(Function|undefined),596 *            fulfill: function(*), reject: function(*)}}597 * @private598 */599webdriver.promise.Deferred.Listener_;600601602/**603 * The three states a {@link webdriver.promise.Deferred} object may be in.604 * @enum {number}605 * @private606 */607webdriver.promise.Deferred.State_ = {608  REJECTED: -1,609  PENDING: 0,610  BLOCKED: 1,611  RESOLVED: 2612};613614615/**616 * Tests if a value is an Error-like object. This is more than an straight617 * instanceof check since the value may originate from another context.618 * @param {*} value The value to test.619 * @return {boolean} Whether the value is an error.620 * @private621 */622webdriver.promise.isError_ = function(value) {623  return value instanceof Error ||624      goog.isObject(value) &&625      (Object.prototype.toString.call(value) === '[object Error]' ||626       // A special test for goog.testing.JsUnitException.627       value.isJsUnitException);628629};630631632/**633 * Determines whether a {@code value} should be treated as a promise.634 * Any object whose "then" property is a function will be considered a promise.635 *636 * @param {*} value The value to test.637 * @return {boolean} Whether the value is a promise.638 */639webdriver.promise.isPromise = function(value) {640  return !!value && goog.isObject(value) &&641      // Use array notation so the Closure compiler does not obfuscate away our642      // contract.643      goog.isFunction(value['then']);644};645646647/**648 * Creates a promise that will be resolved at a set time in the future.649 * @param {number} ms The amount of time, in milliseconds, to wait before650 *     resolving the promise.651 * @return {!webdriver.promise.Promise} The promise.652 */653webdriver.promise.delayed = function(ms) {654  var timer = webdriver.promise.controlFlow().timer;655  var deferred = new webdriver.promise.Deferred();656  var key = timer.setTimeout(deferred.fulfill, ms);657  return deferred.thenCatch(function(e) {658    timer.clearTimeout(key);659    throw e;660  });661};662663664/**665 * Creates a new deferred object.666 * @return {!webdriver.promise.Deferred.<T>} The new deferred object.667 * @template T668 */669webdriver.promise.defer = function() {670  return new webdriver.promise.Deferred();671};672673674/**675 * Creates a promise that has been resolved with the given value.676 * @param {T=} opt_value The resolved value.677 * @return {!webdriver.promise.Promise.<T>} The resolved promise.678 * @template T679 */680webdriver.promise.fulfilled = function(opt_value) {681  if (opt_value instanceof webdriver.promise.Promise) {682    return opt_value;683  }684  var deferred = new webdriver.promise.Deferred();685  deferred.fulfill(opt_value);686  return deferred.promise;687};688689690/**691 * Creates a promise that has been rejected with the given reason.692 * @param {*=} opt_reason The rejection reason; may be any value, but is693 *     usually an Error or a string.694 * @return {!webdriver.promise.Promise.<T>} The rejected promise.695 * @template T696 */697webdriver.promise.rejected = function(opt_reason) {698  var deferred = new webdriver.promise.Deferred();699  deferred.reject(opt_reason);700  return deferred.promise;701};702703704/**705 * Wraps a function that is assumed to be a node-style callback as its final706 * argument. This callback takes two arguments: an error value (which will be707 * null if the call succeeded), and the success value as the second argument.708 * If the call fails, the returned promise will be rejected, otherwise it will709 * be resolved with the result.710 * @param {!Function} fn The function to wrap.711 * @param {...?} var_args The arguments to apply to the function, excluding the712 *     final callback.713 * @return {!webdriver.promise.Promise} A promise that will be resolved with the714 *     result of the provided function's callback.715 */716webdriver.promise.checkedNodeCall = function(fn, var_args) {717  var deferred = new webdriver.promise.Deferred();718  try {719    var args = goog.array.slice(arguments, 1);720    args.push(function(error, value) {721      error ? deferred.reject(error) : deferred.fulfill(value);722    });723    fn.apply(null, args);724  } catch (ex) {725    deferred.reject(ex);726  }727  return deferred.promise;728};729730731/**732 * Registers an observer on a promised {@code value}, returning a new promise733 * that will be resolved when the value is. If {@code value} is not a promise,734 * then the return promise will be immediately resolved.735 * @param {*} value The value to observe.736 * @param {Function=} opt_callback The function to call when the value is737 *     resolved successfully.738 * @param {Function=} opt_errback The function to call when the value is739 *     rejected.740 * @return {!webdriver.promise.Promise} A new promise.741 */742webdriver.promise.when = function(value, opt_callback, opt_errback) {743  if (webdriver.promise.Thenable.isImplementation(value)) {744    return value.then(opt_callback, opt_errback);745  }746747  var deferred = new webdriver.promise.Deferred();748749  webdriver.promise.asap(value, deferred.fulfill, deferred.reject);750751  return deferred.then(opt_callback, opt_errback);752};753754755/**756 * Invokes the appropriate callback function as soon as a promised757 * {@code value} is resolved. This function is similar to758 * {@link webdriver.promise.when}, except it does not return a new promise.759 * @param {*} value The value to observe.760 * @param {Function} callback The function to call when the value is761 *     resolved successfully.762 * @param {Function=} opt_errback The function to call when the value is763 *     rejected.764 */765webdriver.promise.asap = function(value, callback, opt_errback) {766  if (webdriver.promise.isPromise(value)) {767    value.then(callback, opt_errback);768769  // Maybe a Dojo-like deferred object?770  } else if (!!value && goog.isObject(value) &&771      goog.isFunction(value.addCallbacks)) {772    value.addCallbacks(callback, opt_errback);773774  // A raw value, return a resolved promise.775  } else if (callback) {776    callback(value);777  }778};779780781/**782 * Given an array of promises, will return a promise that will be fulfilled783 * with the fulfillment values of the input array's values. If any of the784 * input array's promises are rejected, the returned promise will be rejected785 * with the same reason.786 *787 * @param {!Array.<(T|!webdriver.promise.Promise.<T>)>} arr An array of788 *     promises to wait on.789 * @return {!webdriver.promise.Promise.<!Array.<T>>} A promise that is790 *     fulfilled with an array containing the fulfilled values of the791 *     input array, or rejected with the same reason as the first792 *     rejected value.793 * @template T794 */795webdriver.promise.all = function(arr) {796  var n = arr.length;797  if (!n) {798    return webdriver.promise.fulfilled([]);799  }800801  var toFulfill = n;802  var result = webdriver.promise.defer();803  var values = [];804805  var onFulfill = function(index, value) {806    values[index] = value;807    toFulfill--;808    if (toFulfill == 0) {809      result.fulfill(values);810    }811  };812813  for (var i = 0; i < n; ++i) {814    webdriver.promise.asap(815        arr[i], goog.partial(onFulfill, i), result.reject);816  }817818  return result.promise;819};820821822/**823 * Calls a function for each element in an array and inserts the result into a824 * new array, which is used as the fulfillment value of the promise returned825 * by this function.826 *827 * <p>If the return value of the mapping function is a promise, this function828 * will wait for it to be fulfilled before inserting it into the new array.829 *830 * <p>If the mapping function throws or returns a rejected promise, the831 * promise returned by this function will be rejected with the same reason.832 * Only the first failure will be reported; all subsequent errors will be833 * silently ignored.834 *835 * @param {!(Array.<TYPE>|webdriver.promise.Promise.<!Array.<TYPE>>)} arr The836 *     array to iterator over, or a promise that will resolve to said array.837 * @param {function(this: SELF, TYPE, number, !Array.<TYPE>): ?} fn The838 *     function to call for each element in the array. This function should839 *     expect three arguments (the element, the index, and the array itself.840 * @param {SELF=} opt_self The object to be used as the value of 'this' within841 *     {@code fn}.842 * @template TYPE, SELF843 */844webdriver.promise.map = function(arr, fn, opt_self) {845  return webdriver.promise.when(arr, function(arr) {846    var result = goog.array.map(arr, fn, opt_self);847    return webdriver.promise.all(result);848  });849};850851852/**853 * Calls a function for each element in an array, and if the function returns854 * true adds the element to a new array.855 *856 * <p>If the return value of the filter function is a promise, this function857 * will wait for it to be fulfilled before determining whether to insert the858 * element into the new array.859 *860 * <p>If the filter function throws or returns a rejected promise, the promise861 * returned by this function will be rejected with the same reason. Only the862 * first failure will be reported; all subsequent errors will be silently863 * ignored.864 *865 * @param {!(Array.<TYPE>|webdriver.promise.Promise.<!Array.<TYPE>>)} arr The866 *     array to iterator over, or a promise that will resolve to said array.867 * @param {function(this: SELF, TYPE, number, !Array.<TYPE>): (868 *             boolean|webdriver.promise.Promise.<boolean>)} fn The function869 *     to call for each element in the array.870 * @param {SELF=} opt_self The object to be used as the value of 'this' within871 *     {@code fn}.872 * @template TYPE, SELF873 */874webdriver.promise.filter = function(arr, fn, opt_self) {875  return webdriver.promise.when(arr, function(arr) {876    var originalValues = goog.array.clone(arr);877    return webdriver.promise.map(arr, fn, opt_self).then(function(include) {878      return goog.array.filter(originalValues, function(value, index) {879        return include[index];880      });881    });882  });883};884885886/**887 * Returns a promise that will be resolved with the input value in a888 * fully-resolved state. If the value is an array, each element will be fully889 * resolved. Likewise, if the value is an object, all keys will be fully890 * resolved. In both cases, all nested arrays and objects will also be891 * fully resolved.  All fields are resolved in place; the returned promise will892 * resolve on {@code value} and not a copy.893 *894 * Warning: This function makes no checks against objects that contain895 * cyclical references:896 * <pre><code>897 *   var value = {};898 *   value['self'] = value;899 *   webdriver.promise.fullyResolved(value);  // Stack overflow.900 * </code></pre>901 *902 * @param {*} value The value to fully resolve.903 * @return {!webdriver.promise.Promise} A promise for a fully resolved version904 *     of the input value.905 */906webdriver.promise.fullyResolved = function(value) {907  if (webdriver.promise.isPromise(value)) {908    return webdriver.promise.when(value, webdriver.promise.fullyResolveValue_);909  }910  return webdriver.promise.fullyResolveValue_(value);911};912913914/**915 * @param {*} value The value to fully resolve. If a promise, assumed to916 *     already be resolved.917 * @return {!webdriver.promise.Promise} A promise for a fully resolved version918 *     of the input value.919 * @private920 */921webdriver.promise.fullyResolveValue_ = function(value) {922  switch (goog.typeOf(value)) {923    case 'array':924      return webdriver.promise.fullyResolveKeys_(925          /** @type {!Array} */ (value));926927    case 'object':928      if (webdriver.promise.isPromise(value)) {929        // We get here when the original input value is a promise that930        // resolves to itself. When the user provides us with such a promise,931        // trust that it counts as a "fully resolved" value and return it.932        // Of course, since it's already a promise, we can just return it933        // to the user instead of wrapping it in another promise.934        return /** @type {!webdriver.promise.Promise} */ (value);935      }936937      if (goog.isNumber(value.nodeType) &&938          goog.isObject(value.ownerDocument) &&939          goog.isNumber(value.ownerDocument.nodeType)) {940        // DOM node; return early to avoid infinite recursion. Should we941        // only support objects with a certain level of nesting?942        return webdriver.promise.fulfilled(value);943      }944945      return webdriver.promise.fullyResolveKeys_(946          /** @type {!Object} */ (value));947948    default:  // boolean, function, null, number, string, undefined949      return webdriver.promise.fulfilled(value);950  }951};952953954/**955 * @param {!(Array|Object)} obj the object to resolve.956 * @return {!webdriver.promise.Promise} A promise that will be resolved with the957 *     input object once all of its values have been fully resolved.958 * @private959 */960webdriver.promise.fullyResolveKeys_ = function(obj) {961  var isArray = goog.isArray(obj);962  var numKeys = isArray ? obj.length : goog.object.getCount(obj);963  if (!numKeys) {964    return webdriver.promise.fulfilled(obj);965  }966967  var numResolved = 0;968  var deferred = new webdriver.promise.Deferred();969970  // In pre-IE9, goog.array.forEach will not iterate properly over arrays971  // containing undefined values because "index in array" returns false972  // when array[index] === undefined (even for x = [undefined, 1]). To get973  // around this, we need to use our own forEach implementation.974  // DO NOT REMOVE THIS UNTIL WE NO LONGER SUPPORT IE8. This cannot be975  // reproduced in IE9 by changing the browser/document modes, it requires an976  // actual pre-IE9 browser.  Yay, IE!977  var forEachKey = !isArray ? goog.object.forEach : function(arr, fn) {978    var n = arr.length;979    for (var i = 0; i < n; ++i) {980      fn.call(null, arr[i], i, arr);981    }982  };983984  forEachKey(obj, function(partialValue, key) {985    var type = goog.typeOf(partialValue);986    if (type != 'array' && type != 'object') {987      maybeResolveValue();988      return;989    }990991    webdriver.promise.fullyResolved(partialValue).then(992        function(resolvedValue) {993          obj[key] = resolvedValue;994          maybeResolveValue();995        },996        deferred.reject);997  });998999  return deferred.promise;10001001  function maybeResolveValue() {1002    if (++numResolved == numKeys) {1003      deferred.fulfill(obj);1004    }1005  }1006};100710081009//////////////////////////////////////////////////////////////////////////////1010//1011//  webdriver.promise.ControlFlow1012//1013//////////////////////////////////////////////////////////////////////////////1014101510161017/**1018 * Handles the execution of scheduled tasks, each of which may be an1019 * asynchronous operation. The control flow will ensure tasks are executed in1020 * the ordered scheduled, starting each task only once those before it have1021 * completed.1022 *1023 * <p>Each task scheduled within this flow may return a1024 * {@link webdriver.promise.Promise} to indicate it is an asynchronous1025 * operation. The ControlFlow will wait for such promises to be resolved before1026 * marking the task as completed.1027 *1028 * <p>Tasks and each callback registered on a {@link webdriver.promise.Deferred}1029 * will be run in their own ControlFlow frame.  Any tasks scheduled within a1030 * frame will have priority over previously scheduled tasks. Furthermore, if1031 * any of the tasks in the frame fails, the remainder of the tasks in that frame1032 * will be discarded and the failure will be propagated to the user through the1033 * callback/task's promised result.1034 *1035 * <p>Each time a ControlFlow empties its task queue, it will fire an1036 * {@link webdriver.promise.ControlFlow.EventType.IDLE} event. Conversely,1037 * whenever the flow terminates due to an unhandled error, it will remove all1038 * remaining tasks in its queue and fire an1039 * {@link webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION} event. If1040 * there are no listeners registered with the flow, the error will be1041 * rethrown to the global error handler.1042 *1043 * @param {webdriver.promise.ControlFlow.Timer=} opt_timer The timer object1044 *     to use. Should only be set for testing.1045 * @constructor1046 * @extends {webdriver.EventEmitter}1047 */1048webdriver.promise.ControlFlow = function(opt_timer) {1049  webdriver.EventEmitter.call(this);10501051  /**1052   * The timer used by this instance.1053   * @type {webdriver.promise.ControlFlow.Timer}1054   */1055  this.timer = opt_timer || webdriver.promise.ControlFlow.defaultTimer;10561057  /**1058   * A list of recent tasks. Each time a new task is started, or a frame is1059   * completed, the previously recorded task is removed from this list. If1060   * there are multiple tasks, task N+1 is considered a sub-task of task1061   * N.1062   * @private {!Array.<!webdriver.promise.Task_>}1063   */1064  this.history_ = [];10651066  /**1067   * Tracks the active execution frame for this instance. Lazily initialized1068   * when the first task is scheduled.1069   * @private {webdriver.promise.Frame_}1070   */1071  this.activeFrame_ = null;10721073  /**1074   * A reference to the frame in which new tasks should be scheduled. If1075   * {@code null}, tasks will be scheduled within the active frame. When forcing1076   * a function to run in the context of a new frame, this pointer is used to1077   * ensure tasks are scheduled within the newly created frame, even though it1078   * won't be active yet.1079   * @private {webdriver.promise.Frame_}1080   * @see {#runInNewFrame_}1081   */1082  this.schedulingFrame_ = null;10831084  /**1085   * Timeout ID set when the flow is about to shutdown without any errors1086   * being detected. Upon shutting down, the flow will emit an1087   * {@link webdriver.promise.ControlFlow.EventType.IDLE} event. Idle events1088   * always follow a brief timeout in order to catch latent errors from the last1089   * completed task. If this task had a callback registered, but no errback, and1090   * the task fails, the unhandled failure would not be reported by the promise1091   * system until the next turn of the event loop:1092   *1093   *   // Schedule 1 task that fails.1094   *   var result = webriver.promise.controlFlow().schedule('example',1095   *       function() { return webdriver.promise.rejected('failed'); });1096   *   // Set a callback on the result. This delays reporting the unhandled1097   *   // failure for 1 turn of the event loop.1098   *   result.then(goog.nullFunction);1099   *1100   * @private {?number}1101   */1102  this.shutdownId_ = null;11031104  /**1105   * Interval ID for this instance's event loop.1106   * @private {?number}1107   */1108  this.eventLoopId_ = null;11091110  /**1111   * The number of "pending" promise rejections.1112   *1113   * <p>Each time a promise is rejected and is not handled by a listener, it1114   * will schedule a 0-based timeout to check if it is still unrejected in the1115   * next turn of the JS-event loop. This allows listeners to attach to, and1116   * handle, the rejected promise at any point in same turn of the event loop1117   * that the promise was rejected.1118   *1119   * <p>When this flow's own event loop triggers, it will not run if there1120   * are any outstanding promise rejections. This allows unhandled promises to1121   * be reported before a new task is started, ensuring the error is reported1122   * to the current task queue.1123   *1124   * @private {number}1125   */1126  this.pendingRejections_ = 0;11271128  /**1129   * The number of aborted frames since the last time a task was executed or a1130   * frame completed successfully.1131   * @private {number}1132   */1133  this.numAbortedFrames_ = 0;1134};1135goog.inherits(webdriver.promise.ControlFlow, webdriver.EventEmitter);113611371138/**1139 * @typedef {{clearInterval: function(number),1140 *            clearTimeout: function(number),1141 *            setInterval: function(!Function, number): number,1142 *            setTimeout: function(!Function, number): number}}1143 */1144webdriver.promise.ControlFlow.Timer;114511461147/**1148 * The default timer object, which uses the global timer functions.1149 * @type {webdriver.promise.ControlFlow.Timer}1150 */1151webdriver.promise.ControlFlow.defaultTimer = (function() {1152  // The default timer functions may be defined as free variables for the1153  // current context, so do not reference them using "window" or1154  // "goog.global".  Also, we must invoke them in a closure, and not using1155  // bind(), so we do not get "TypeError: Illegal invocation" (WebKit) or1156  // "Invalid calling object" (IE) errors.1157  return {1158    clearInterval: wrap(clearInterval),1159    clearTimeout: wrap(clearTimeout),1160    setInterval: wrap(setInterval),1161    setTimeout: wrap(setTimeout)1162  };11631164  function wrap(fn) {1165    return function() {1166      // Cannot use .call() or .apply() since we do not know which variable1167      // the function is bound to, and using the wrong one will generate1168      // an error.1169      return fn(arguments[0], arguments[1]);1170    };1171  }1172})();117311741175/**1176 * Events that may be emitted by an {@link webdriver.promise.ControlFlow}.1177 * @enum {string}1178 */1179webdriver.promise.ControlFlow.EventType = {11801181  /** Emitted when all tasks have been successfully executed. */1182  IDLE: 'idle',11831184  /** Emitted when a ControlFlow has been reset. */1185  RESET: 'reset',11861187  /** Emitted whenever a new task has been scheduled. */1188  SCHEDULE_TASK: 'scheduleTask',11891190  /**1191   * Emitted whenever a control flow aborts due to an unhandled promise1192   * rejection. This event will be emitted along with the offending rejection1193   * reason. Upon emitting this event, the control flow will empty its task1194   * queue and revert to its initial state.1195   */1196  UNCAUGHT_EXCEPTION: 'uncaughtException'1197};119811991200/**1201 * How often, in milliseconds, the event loop should run.1202 * @type {number}1203 * @const1204 */1205webdriver.promise.ControlFlow.EVENT_LOOP_FREQUENCY = 10;120612071208/**1209 * Resets this instance, clearing its queue and removing all event listeners.1210 */1211webdriver.promise.ControlFlow.prototype.reset = function() {1212  this.activeFrame_ = null;1213  this.clearHistory();1214  this.emit(webdriver.promise.ControlFlow.EventType.RESET);1215  this.removeAllListeners();1216  this.cancelShutdown_();1217  this.cancelEventLoop_();1218};121912201221/**1222 * Returns a summary of the recent task activity for this instance. This1223 * includes the most recently completed task, as well as any parent tasks. In1224 * the returned summary, the task at index N is considered a sub-task of the1225 * task at index N+1.1226 * @return {!Array.<string>} A summary of this instance's recent task1227 *     activity.1228 */1229webdriver.promise.ControlFlow.prototype.getHistory = function() {1230  var pendingTasks = [];1231  var currentFrame = this.activeFrame_;1232  while (currentFrame) {1233    var task = currentFrame.getPendingTask();1234    if (task) {1235      pendingTasks.push(task);1236    }1237    // A frame's parent node will always be another frame.1238    currentFrame =1239        /** @type {webdriver.promise.Frame_} */ (currentFrame.getParent());1240  }12411242  var fullHistory = goog.array.concat(this.history_, pendingTasks);1243  return goog.array.map(fullHistory, function(task) {1244    return task.toString();1245  });1246};124712481249/** Clears this instance's task history. */1250webdriver.promise.ControlFlow.prototype.clearHistory = function() {1251  this.history_ = [];1252};125312541255/**1256 * Removes a completed task from this instance's history record. If any1257 * tasks remain from aborted frames, those will be removed as well.1258 * @private1259 */1260webdriver.promise.ControlFlow.prototype.trimHistory_ = function() {1261  if (this.numAbortedFrames_) {1262    goog.array.splice(this.history_,1263        this.history_.length - this.numAbortedFrames_,1264        this.numAbortedFrames_);1265    this.numAbortedFrames_ = 0;1266  }1267  this.history_.pop();1268};126912701271/**1272 * Property used to track whether an error has been annotated by1273 * {@link webdriver.promise.ControlFlow#annotateError}.1274 * @private {string}1275 * @const1276 */1277webdriver.promise.ControlFlow.ANNOTATION_PROPERTY_ =1278    'webdriver_promise_error_';127912801281/**1282 * Appends a summary of this instance's recent task history to the given1283 * error's stack trace. This function will also ensure the error's stack trace1284 * is in canonical form.1285 * @param {!(Error|goog.testing.JsUnitException)} e The error to annotate.1286 * @return {!(Error|goog.testing.JsUnitException)} The annotated error.1287 */1288webdriver.promise.ControlFlow.prototype.annotateError = function(e) {1289  if (!!e[webdriver.promise.ControlFlow.ANNOTATION_PROPERTY_]) {1290    return e;1291  }12921293  var history = this.getHistory();1294  if (history.length) {1295    e = webdriver.stacktrace.format(e);12961297    /** @type {!Error} */(e).stack += [1298      '\n==== async task ====\n',1299      history.join('\n==== async task ====\n')1300    ].join('');13011302    e[webdriver.promise.ControlFlow.ANNOTATION_PROPERTY_] = true;1303  }13041305  return e;1306};130713081309/**1310 * @return {string} The scheduled tasks still pending with this instance.1311 */1312webdriver.promise.ControlFlow.prototype.getSchedule = function() {1313  return this.activeFrame_ ? this.activeFrame_.getRoot().toString() : '[]';1314};131513161317/**1318 * Schedules a task for execution. If there is nothing currently in the1319 * queue, the task will be executed in the next turn of the event loop. If1320 * the task function is a generator, the task will be executed using1321 * {@link webdriver.promise.consume}.1322 *1323 * @param {function(): (T|webdriver.promise.Promise.<T>)} fn The function to1324 *     call to start the task. If the function returns a1325 *     {@link webdriver.promise.Promise}, this instance will wait for it to be1326 *     resolved before starting the next task.1327 * @param {string=} opt_description A description of the task.1328 * @return {!webdriver.promise.Promise.<T>} A promise that will be resolved1329 *     with the result of the action.1330 * @template T1331 */1332webdriver.promise.ControlFlow.prototype.execute = function(1333    fn, opt_description) {1334  if (webdriver.promise.isGenerator(fn)) {1335    fn = goog.partial(webdriver.promise.consume, fn);1336  }13371338  this.cancelShutdown_();13391340  if (!this.activeFrame_) {1341    this.activeFrame_ = new webdriver.promise.Frame_(this);1342  }13431344  // Trim an extra frame off the generated stack trace for the call to this1345  // function.1346  var snapshot = new webdriver.stacktrace.Snapshot(1);1347  var task = new webdriver.promise.Task_(1348      this, fn, opt_description || '', snapshot);1349  var scheduleIn = this.schedulingFrame_ || this.activeFrame_;1350  scheduleIn.addChild(task);13511352  this.emit(webdriver.promise.ControlFlow.EventType.SCHEDULE_TASK, opt_description);13531354  this.scheduleEventLoopStart_();1355  return task.promise;1356};135713581359/**1360 * Inserts a {@code setTimeout} into the command queue. This is equivalent to1361 * a thread sleep in a synchronous programming language.1362 *1363 * @param {number} ms The timeout delay, in milliseconds.1364 * @param {string=} opt_description A description to accompany the timeout.1365 * @return {!webdriver.promise.Promise} A promise that will be resolved with1366 *     the result of the action.1367 */1368webdriver.promise.ControlFlow.prototype.timeout = function(1369    ms, opt_description) {1370  return this.execute(function() {1371    return webdriver.promise.delayed(ms);1372  }, opt_description);1373};137413751376/**1377 * Schedules a task that shall wait for a condition to hold. Each condition1378 * function may return any value, but it will always be evaluated as a boolean.1379 *1380 * <p>Condition functions may schedule sub-tasks with this instance, however,1381 * their execution time will be factored into whether a wait has timed out.1382 *1383 * <p>In the event a condition returns a Promise, the polling loop will wait for1384 * it to be resolved before evaluating whether the condition has been satisfied.1385 * The resolution time for a promise is factored into whether a wait has timed1386 * out.1387 *1388 * <p>If the condition function throws, or returns a rejected promise, the1389 * wait task will fail.1390 *1391 * @param {function(): T} condition The condition function to poll.1392 * @param {number} timeout How long to wait, in milliseconds, for the condition1393 *     to hold before timing out.1394 * @param {string=} opt_message An optional error message to include if the1395 *     wait times out; defaults to the empty string.1396 * @return {!webdriver.promise.Promise.<T>} A promise that will be fulfilled1397 *     when the condition has been satisified. The promise shall be rejected if1398 *     the wait times out waiting for the condition.1399 * @template T1400 */1401webdriver.promise.ControlFlow.prototype.wait = function(1402    condition, timeout, opt_message) {1403  var sleep = Math.min(timeout, 100);1404  var self = this;14051406  if (webdriver.promise.isGenerator(condition)) {1407    condition = goog.partial(webdriver.promise.consume, condition);1408  }14091410  return this.execute(function() {1411    var startTime = goog.now();1412    var waitResult = new webdriver.promise.Deferred();1413    var waitFrame = self.activeFrame_;1414    waitFrame.isWaiting = true;1415    pollCondition();1416    return waitResult.promise;14171418    function pollCondition() {1419      self.runInNewFrame_(condition, function(value) {1420        var elapsed = goog.now() - startTime;1421        if (!!value) {1422          waitFrame.isWaiting = false;1423          waitResult.fulfill(value);1424        } else if (elapsed >= timeout) {1425          waitResult.reject(new Error((opt_message ? opt_message + '\n' : '') +1426              'Wait timed out after ' + elapsed + 'ms'));1427        } else {1428          self.timer.setTimeout(pollCondition, sleep);1429        }1430      }, waitResult.reject, true);1431    }1432  }, opt_message);1433};143414351436/**1437 * Schedules a task that will wait for another promise to resolve.  The resolved1438 * promise's value will be returned as the task result.1439 * @param {!webdriver.promise.Promise} promise The promise to wait on.1440 * @return {!webdriver.promise.Promise} A promise that will resolve when the1441 *     task has completed.1442 */1443webdriver.promise.ControlFlow.prototype.await = function(promise) {1444  return this.execute(function() {1445    return promise;1446  });1447};144814491450/**1451 * Schedules the interval for this instance's event loop, if necessary.1452 * @private1453 */1454webdriver.promise.ControlFlow.prototype.scheduleEventLoopStart_ = function() {1455  if (!this.eventLoopId_) {1456    this.eventLoopId_ = this.timer.setInterval(1457        goog.bind(this.runEventLoop_, this),1458        webdriver.promise.ControlFlow.EVENT_LOOP_FREQUENCY);1459  }1460};146114621463/**1464 * Cancels the event loop, if necessary.1465 * @private1466 */1467webdriver.promise.ControlFlow.prototype.cancelEventLoop_ = function() {1468  if (this.eventLoopId_) {1469    this.timer.clearInterval(this.eventLoopId_);1470    this.eventLoopId_ = null;1471  }1472};147314741475/**1476 * Executes the next task for the current frame. If the current frame has no1477 * more tasks, the frame's result will be resolved, returning control to the1478 * frame's creator. This will terminate the flow if the completed frame was at1479 * the top of the stack.1480 * @private1481 */1482webdriver.promise.ControlFlow.prototype.runEventLoop_ = function() {1483  // If we get here and there are pending promise rejections, then those1484  // promises are queued up to run as soon as this (JS) event loop terminates.1485  // Short-circuit our loop to give those promises a chance to run. Otherwise,1486  // we might start a new task only to have it fail because of one of these1487  // pending rejections.1488  if (this.pendingRejections_) {1489    return;1490  }14911492  // If the flow aborts due to an unhandled exception after we've scheduled1493  // another turn of the execution loop, we can end up in here with no tasks1494  // left. This is OK, just quietly return.1495  if (!this.activeFrame_) {1496    this.commenceShutdown_();1497    return;1498  }14991500  var task;1501  if (this.activeFrame_.getPendingTask() || !(task = this.getNextTask_())) {1502    // Either the current frame is blocked on a pending task, or we don't have1503    // a task to finish because we've completed a frame. When completing a1504    // frame, we must abort the event loop to allow the frame's promise's1505    // callbacks to execute.1506    return;1507  }15081509  var activeFrame = this.activeFrame_;1510  activeFrame.setPendingTask(task);1511  var markTaskComplete = goog.bind(function() {1512    this.history_.push(/** @type {!webdriver.promise.Task_} */ (task));1513    activeFrame.setPendingTask(null);1514  }, this);15151516  this.trimHistory_();1517  var self = this;1518  this.runInNewFrame_(task.execute, function(result) {1519    markTaskComplete();1520    task.fulfill(result);1521  }, function(error) {1522    markTaskComplete();15231524    if (!webdriver.promise.isError_(error) &&1525        !webdriver.promise.isPromise(error)) {1526      error = Error(error);1527    }15281529    task.reject(self.annotateError(/** @type {!Error} */ (error)));1530  }, true);1531};153215331534/**1535 * @return {webdriver.promise.Task_} The next task to execute, or1536 *     {@code null} if a frame was resolved.1537 * @private1538 */1539webdriver.promise.ControlFlow.prototype.getNextTask_ = function() {1540  var frame = this.activeFrame_;1541  var firstChild = frame.getFirstChild();1542  if (!firstChild) {1543    if (!frame.isWaiting) {1544      this.resolveFrame_(frame);1545    }1546    return null;1547  }15481549  if (firstChild instanceof webdriver.promise.Frame_) {1550    this.activeFrame_ = firstChild;1551    return this.getNextTask_();1552  }15531554  frame.removeChild(firstChild);1555  return firstChild;1556};155715581559/**1560 * @param {!webdriver.promise.Frame_} frame The frame to resolve.1561 * @private1562 */1563webdriver.promise.ControlFlow.prototype.resolveFrame_ = function(frame) {1564  if (this.activeFrame_ === frame) {1565    // Frame parent is always another frame, but the compiler is not smart1566    // enough to recognize this.1567    this.activeFrame_ =1568        /** @type {webdriver.promise.Frame_} */ (frame.getParent());1569  }15701571  if (frame.getParent()) {1572    frame.getParent().removeChild(frame);1573  }1574  this.trimHistory_();1575  frame.close();15761577  if (!this.activeFrame_) {1578    this.commenceShutdown_();1579  }1580};158115821583/**1584 * Aborts the current frame. The frame, and all of the tasks scheduled within it1585 * will be discarded. If this instance does not have an active frame, it will1586 * immediately terminate all execution.1587 * @param {*} error The reason the frame is being aborted; typically either1588 *     an Error or string.1589 * @private1590 */1591webdriver.promise.ControlFlow.prototype.abortFrame_ = function(error) {1592  // Annotate the error value if it is Error-like.1593  if (webdriver.promise.isError_(error)) {1594    this.annotateError(/** @type {!Error} */ (error));1595  }1596  this.numAbortedFrames_++;15971598  if (!this.activeFrame_) {1599    this.abortNow_(error);1600    return;1601  }16021603  // Frame parent is always another frame, but the compiler is not smart1604  // enough to recognize this.1605  var parent = /** @type {webdriver.promise.Frame_} */ (1606      this.activeFrame_.getParent());1607  if (parent) {1608    parent.removeChild(this.activeFrame_);1609  }16101611  var frame = this.activeFrame_;1612  this.activeFrame_ = parent;1613  frame.abort(error);1614};161516161617/**1618 * Executes a function in a new frame. If the function does not schedule any new1619 * tasks, the frame will be discarded and the function's result returned1620 * immediately. Otherwise, a promise will be returned. This promise will be1621 * resolved with the function's result once all of the tasks scheduled within1622 * the function have been completed. If the function's frame is aborted, the1623 * returned promise will be rejected.1624 *1625 * @param {!Function} fn The function to execute.1626 * @param {function(*)} callback The function to call with a successful result.1627 * @param {function(*)} errback The function to call if there is an error.1628 * @param {boolean=} opt_activate Whether the active frame should be updated to1629 *     the newly created frame so tasks are treated as sub-tasks.1630 * @private1631 */1632webdriver.promise.ControlFlow.prototype.runInNewFrame_ = function(1633    fn, callback, errback, opt_activate) {1634  var newFrame = new webdriver.promise.Frame_(this),1635      self = this,1636      oldFrame = this.activeFrame_;16371638  try {1639    if (!this.activeFrame_) {1640      this.activeFrame_ = newFrame;1641    } else {1642      this.activeFrame_.addChild(newFrame);1643    }16441645    // Activate the new frame to force tasks to be treated as sub-tasks of1646    // the parent frame.1647    if (opt_activate) {1648      this.activeFrame_ = newFrame;1649    }16501651    try {1652      this.schedulingFrame_ = newFrame;1653      webdriver.promise.pushFlow_(this);1654      var result = fn();1655    } finally {1656      webdriver.promise.popFlow_();1657      this.schedulingFrame_ = null;1658    }1659    newFrame.isLocked_ = true;16601661    // If there was nothing scheduled in the new frame we can discard the1662    // frame and return immediately.1663    if (!newFrame.children_.length) {1664      removeNewFrame();1665      webdriver.promise.asap(result, callback, errback);1666      return;1667    }16681669    newFrame.onComplete = function() {1670      webdriver.promise.asap(result, callback, errback);1671    };16721673    newFrame.onAbort = function(e) {1674      if (webdriver.promise.Thenable.isImplementation(result) &&1675          result.isPending()) {1676        result.cancel(e);1677        e = result;1678      }1679      errback(e);1680    };1681  } catch (ex) {1682    removeNewFrame(ex);1683    errback(ex);1684  }16851686  /**1687   * @param {*=} opt_err If provided, the reason that the frame was removed.1688   */1689  function removeNewFrame(opt_err) {1690    var parent = newFrame.getParent();1691    if (parent) {1692      parent.removeChild(newFrame);1693    }16941695    if (opt_err) {1696      newFrame.cancelRemainingTasks(1697          'Tasks cancelled due to uncaught error: ' + opt_err);1698    }1699    self.activeFrame_ = oldFrame;1700  }1701};170217031704/**1705 * Commences the shutdown sequence for this instance. After one turn of the1706 * event loop, this object will emit the1707 * {@link webdriver.promise.ControlFlow.EventType.IDLE} event to signal1708 * listeners that it has completed. During this wait, if another task is1709 * scheduled, the shutdown will be aborted.1710 * @private1711 */1712webdriver.promise.ControlFlow.prototype.commenceShutdown_ = function() {1713  if (!this.shutdownId_) {1714    // Go ahead and stop the event loop now.  If we're in here, then there are1715    // no more frames with tasks to execute. If we waited to cancel the event1716    // loop in our timeout below, the event loop could trigger *before* the1717    // timeout, generating an error from there being no frames.1718    // If #execute is called before the timeout below fires, it will cancel1719    // the timeout and restart the event loop.1720    this.cancelEventLoop_();17211722    var self = this;1723    self.shutdownId_ = self.timer.setTimeout(function() {1724      self.shutdownId_ = null;1725      self.emit(webdriver.promise.ControlFlow.EventType.IDLE);1726    }, 0);1727  }1728};172917301731/**1732 * Cancels the shutdown sequence if it is currently scheduled.1733 * @private1734 */1735webdriver.promise.ControlFlow.prototype.cancelShutdown_ = function() {1736  if (this.shutdownId_) {1737    this.timer.clearTimeout(this.shutdownId_);1738    this.shutdownId_ = null;1739  }1740};174117421743/**1744 * Aborts this flow, abandoning all remaining tasks. If there are1745 * listeners registered, an {@code UNCAUGHT_EXCEPTION} will be emitted with the1746 * offending {@code error}, otherwise, the {@code error} will be rethrown to the1747 * global error handler.1748 * @param {*} error Object describing the error that caused the flow to1749 *     abort; usually either an Error or string value.1750 * @private1751 */1752webdriver.promise.ControlFlow.prototype.abortNow_ = function(error) {1753  this.activeFrame_ = null;1754  this.cancelShutdown_();1755  this.cancelEventLoop_();17561757  var listeners = this.listeners(1758      webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION);1759  if (!listeners.length) {1760    this.timer.setTimeout(function() {1761      throw error;1762    }, 0);1763  } else {1764    this.emit(webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION,1765        error);1766  }1767};1768176917701771/**1772 * An execution frame within a {@link webdriver.promise.ControlFlow}.  Each1773 * frame represents the execution context for either a1774 * {@link webdriver.promise.Task_} or a callback on a1775 * {@link webdriver.promise.Deferred}.1776 *1777 * <p>Each frame may contain sub-frames.  If child N is a sub-frame, then the1778 * items queued within it are given priority over child N+1.1779 *1780 * @param {!webdriver.promise.ControlFlow} flow The flow this instance belongs1781 *     to.1782 * @constructor1783 * @private1784 * @final1785 * @struct1786 */1787webdriver.promise.Frame_ = function(flow) {1788  /** @private {!webdriver.promise.ControlFlow} */1789  this.flow_ = flow;17901791  /** @private {webdriver.promise.Frame_} */1792  this.parent_ = null;17931794  /**1795   * @private {!Array.<!(webdriver.promise.Frame_|webdriver.promise.Task_)>}1796   */1797  this.children_ = [];179817991800  /** @private {(webdriver.promise.Frame_|webdriver.promise.Task_)} */1801  this.lastInsertedChild_ = null;18021803  /**1804   * The task currently being executed within this frame.1805   * @private {webdriver.promise.Task_}1806   */1807  this.pendingTask_ = null;18081809  /**1810   * Whether this frame is currently locked. A locked frame represents an1811   * executed function that has scheduled all of its tasks.1812   *1813   * <p>Once a frame becomes locked, any new frames which are added as children1814   * represent interrupts (such as a {@link webdriver.promise.Promise}1815   * callback) whose tasks must be given priority over those already scheduled1816   * within this frame. For example:1817   * <code><pre>1818   *   var flow = webdriver.promise.controlFlow();1819   *   flow.execute('start here', goog.nullFunction).then(function() {1820   *     flow.execute('this should execute 2nd', goog.nullFunction);1821   *   });1822   *   flow.execute('this should execute last', goog.nullFunction);1823   * </pre></code>1824   *1825   * @private {boolean}1826   */1827  this.isLocked_ = false;18281829  /** @type {boolean} */1830  this.isWaiting = false;18311832  /**1833   * The function to notify if this frame executes without error.1834   * @type {?function()}1835   */1836  this.onComplete = null;18371838  /**1839   * The function to notify if this frame is aborted with an error.1840   * @type {?function(*)}1841   */1842  this.onAbort = null;1843};184418451846/** @return {webdriver.promise.Frame_} This frame's parent, if any. */1847webdriver.promise.Frame_.prototype.getParent = function() {1848  return this.parent_;1849};185018511852/**1853 * @param {webdriver.promise.Frame_} parent This frame's new parent.1854 */1855webdriver.promise.Frame_.prototype.setParent = function(parent) {1856  this.parent_ = parent;1857};185818591860/**1861 * @return {!webdriver.promise.Frame_} The root of this frame's tree.1862 */1863webdriver.promise.Frame_.prototype.getRoot = function() {1864  var root = this;1865  while (root.parent_) {1866    root = root.parent_;1867  }1868  return root;1869};187018711872/**1873 * Aborts the execution of this frame, cancelling all outstanding tasks1874 * scheduled within this frame.1875 *1876 * @param {*} error The error that triggered this abortion.1877 */1878webdriver.promise.Frame_.prototype.abort = function(error) {1879  this.cancelRemainingTasks(1880      'Task discarded due to a previous task failure: ' + error);18811882  var frame = this;1883  frame.flow_.pendingRejections_ += 1;1884  this.flow_.timer.setTimeout(function() {1885    frame.flow_.pendingRejections_ -= 1;1886    if (frame.onAbort) {1887      frame.notify_(frame.onAbort, error);1888    } else {1889      frame.flow_.abortFrame_(error);1890    }1891  }, 0);1892};189318941895/**1896 * Signals that this frame has successfully finished executing.1897 */1898webdriver.promise.Frame_.prototype.close = function() {1899  var frame = this;1900  this.flow_.timer.setTimeout(function() {1901    frame.notify_(frame.onComplete);1902  }, 0);1903};190419051906/**1907 * @param {?(function(*)|function())} fn The function to notify.1908 * @param {*=} opt_error Value to pass to the notified function, if any.1909 * @private1910 */1911webdriver.promise.Frame_.prototype.notify_ = function(fn, opt_error) {1912  this.onAbort = this.onComplete = null;1913  if (fn) {1914    fn(opt_error);1915  }1916};191719181919/**1920 * Marks all of the tasks that are descendants of this frame in the execution1921 * tree as cancelled. This is necessary for callbacks scheduled asynchronous.1922 * For example:1923 *1924 *     var someResult;1925 *     webdriver.promise.createFlow(function(flow) {1926 *       someResult = flow.execute(function() {});1927 *       throw Error();1928 *     }).addErrback(function(err) {1929 *       console.log('flow failed: ' + err);1930 *       someResult.then(function() {1931 *         console.log('task succeeded!');1932 *       }, function(err) {1933 *         console.log('task failed! ' + err);1934 *       });1935 *     });1936 *     // flow failed: Error: boom1937 *     // task failed! CancelledTaskError: Task discarded due to a previous1938 *     // task failure: Error: boom1939 *1940 * @param {string} reason The cancellation reason.1941 */1942webdriver.promise.Frame_.prototype.cancelRemainingTasks = function(reason) {1943  goog.array.forEach(this.children_, function(child) {1944    if (child instanceof webdriver.promise.Frame_) {1945      child.cancelRemainingTasks(reason);1946    } else {1947      // None of the previously registered listeners should be notified that1948      // the task is being canceled, however, we need at least one errback1949      // to prevent the cancellation from bubbling up.1950      child.removeAll();1951      child.thenCatch(goog.nullFunction);1952      child.cancel(reason);1953    }1954  });1955};195619571958/**1959 * @return {webdriver.promise.Task_} The task currently executing1960 *     within this frame, if any.1961 */1962webdriver.promise.Frame_.prototype.getPendingTask = function() {1963  return this.pendingTask_;1964};196519661967/**1968 * @param {webdriver.promise.Task_} task The task currently1969 *     executing within this frame, if any.1970 */1971webdriver.promise.Frame_.prototype.setPendingTask = function(task) {1972  this.pendingTask_ = task;1973};197419751976/**1977 * Adds a new node to this frame.1978 * @param {!(webdriver.promise.Frame_|webdriver.promise.Task_)} node1979 *     The node to insert.1980 */1981webdriver.promise.Frame_.prototype.addChild = function(node) {1982  if (this.lastInsertedChild_ &&1983      this.lastInsertedChild_ instanceof webdriver.promise.Frame_ &&1984      !this.lastInsertedChild_.isLocked_) {1985    this.lastInsertedChild_.addChild(node);1986    return;1987  }19881989  if (node instanceof webdriver.promise.Frame_) {1990    node.setParent(this);1991  }19921993  if (this.isLocked_ && node instanceof webdriver.promise.Frame_) {1994    var index = 0;1995    if (this.lastInsertedChild_ instanceof1996        webdriver.promise.Frame_) {1997      index = goog.array.indexOf(this.children_, this.lastInsertedChild_) + 1;1998    }1999    goog.array.insertAt(this.children_, node, index);2000    this.lastInsertedChild_ = node;2001    return;2002  }20032004  this.lastInsertedChild_ = node;2005  this.children_.push(node);2006};200720082009/**2010 * @return {(webdriver.promise.Frame_|webdriver.promise.Task_)} This frame's2011 *     fist child.2012 */2013webdriver.promise.Frame_.prototype.getFirstChild = function() {2014  this.isLocked_ = true;2015  this.lastInsertedChild_ = null;2016  return this.children_[0];2017};201820192020/**2021 * Removes a child from this frame.2022 * @param {!(webdriver.promise.Frame_|webdriver.promise.Task_)} child2023 *     The child to remove.2024 */2025webdriver.promise.Frame_.prototype.removeChild = function(child) {2026  var index = goog.array.indexOf(this.children_, child);2027  if (child instanceof webdriver.promise.Frame_) {2028    child.setParent(null);2029  }2030  goog.array.removeAt(this.children_, index);2031  if (this.lastInsertedChild_ === child) {2032    this.lastInsertedChild_ = null;2033  }2034};203520362037/** @override */2038webdriver.promise.Frame_.prototype.toString = function() {2039  return '[' + goog.array.map(this.children_, function(child) {2040    return child.toString();2041  }).join(', ') + ']';2042};2043204420452046/**2047 * A task to be executed by a {@link webdriver.promise.ControlFlow}.2048 *2049 * @param {!webdriver.promise.ControlFlow} flow The flow this instances belongs2050 *     to.2051 * @param {function(): (T|!webdriver.promise.Promise.<T>)} fn The function to2052 *     call when the task executes. If it returns a2053 *     {@link webdriver.promise.Promise}, the flow will wait for it to be2054 *     resolved before starting the next task.2055 * @param {string} description A description of the task for debugging.2056 * @param {!webdriver.stacktrace.Snapshot} snapshot A snapshot of the stack2057 *     when this task was scheduled.2058 * @constructor2059 * @extends {webdriver.promise.Deferred.<T>}2060 * @template T2061 * @private2062 */2063webdriver.promise.Task_ = function(flow, fn, description, snapshot) {2064  webdriver.promise.Deferred.call(this, flow);20652066  /**2067   * @type {function(): (T|!webdriver.promise.Promise.<T>)}2068   */2069  this.execute = fn;20702071  /** @private {string} */2072  this.description_ = description;20732074  /** @private {!webdriver.stacktrace.Snapshot} */2075  this.snapshot_ = snapshot;2076};2077goog.inherits(webdriver.promise.Task_, webdriver.promise.Deferred);207820792080/** @return {string} This task's description. */2081webdriver.promise.Task_.prototype.getDescription = function() {2082  return this.description_;2083};208420852086/** @override */2087webdriver.promise.Task_.prototype.toString = function() {2088  var stack = this.snapshot_.getStacktrace();2089  var ret = this.description_;2090  if (stack.length) {2091    if (this.description_) {2092      ret += '\n';2093    }2094    ret += stack.join('\n');2095  }2096  return ret;2097};2098209921002101/**2102 * The default flow to use if no others are active.2103 * @private {!webdriver.promise.ControlFlow}2104 */2105webdriver.promise.defaultFlow_ = new webdriver.promise.ControlFlow();210621072108/**2109 * A stack of active control flows, with the top of the stack used to schedule2110 * commands. When there are multiple flows on the stack, the flow at index N2111 * represents a callback triggered within a task owned by the flow at index2112 * N-1.2113 * @private {!Array.<!webdriver.promise.ControlFlow>}2114 */2115webdriver.promise.activeFlows_ = [];211621172118/**2119 * Changes the default flow to use when no others are active.2120 * @param {!webdriver.promise.ControlFlow} flow The new default flow.2121 * @throws {Error} If the default flow is not currently active.2122 */2123webdriver.promise.setDefaultFlow = function(flow) {2124  if (webdriver.promise.activeFlows_.length) {2125    throw Error('You may only change the default flow while it is active');2126  }2127  webdriver.promise.defaultFlow_ = flow;2128};212921302131/**2132 * @return {!webdriver.promise.ControlFlow} The currently active control flow.2133 */2134webdriver.promise.controlFlow = function() {2135  return /** @type {!webdriver.promise.ControlFlow} */ (2136      goog.array.peek(webdriver.promise.activeFlows_) ||2137      webdriver.promise.defaultFlow_);2138};213921402141/**2142 * @param {!webdriver.promise.ControlFlow} flow The new flow.2143 * @private2144 */2145webdriver.promise.pushFlow_ = function(flow) {2146  webdriver.promise.activeFlows_.push(flow);2147};214821492150/** @private */2151webdriver.promise.popFlow_ = function() {2152  webdriver.promise.activeFlows_.pop();2153};215421552156/**2157 * Creates a new control flow. The provided callback will be invoked as the2158 * first task within the new flow, with the flow as its sole argument. Returns2159 * a promise that resolves to the callback result.2160 * @param {function(!webdriver.promise.ControlFlow)} callback The entry point2161 *     to the newly created flow.2162 * @return {!webdriver.promise.Promise} A promise that resolves to the callback2163 *     result.2164 */2165webdriver.promise.createFlow = function(callback) {2166  var flow = new webdriver.promise.ControlFlow(2167      webdriver.promise.defaultFlow_.timer);2168  return flow.execute(function() {2169    return callback(flow);2170  });2171};217221732174/**2175 * Tests is a function is a generator.2176 * @param {!Function} fn The function to test.2177 * @return {boolean} Whether the function is a generator.2178 */2179webdriver.promise.isGenerator = function(fn) {2180  return fn.constructor.name === 'GeneratorFunction';2181};218221832184/**2185 * Consumes a {@code GeneratorFunction}. Each time the generator yields a2186 * promise, this function will wait for it to be fulfilled before feeding the2187 * fulfilled value back into {@code next}. Likewise, if a yielded promise is2188 * rejected, the rejection error will be passed to {@code throw}.2189 *2190 * <p>Example 1: the Fibonacci Sequence.2191 * <pre><code>2192 * webdriver.promise.consume(function* fibonacci() {2193 *   var n1 = 1, n2 = 1;2194 *   for (var i = 0; i < 4; ++i) {2195 *     var tmp = yield n1 + n2;2196 *     n1 = n2;2197 *     n2 = tmp;2198 *   }2199 *   return n1 + n2;2200 * }).then(function(result) {2201 *   console.log(result);  // 132202 * });2203 * </code></pre>2204 *2205 * <p>Example 2: a generator that throws.2206 * <pre><code>2207 * webdriver.promise.consume(function* () {2208 *   yield webdriver.promise.delayed(250).then(function() {2209 *     throw Error('boom');2210 *   });2211 * }).thenCatch(function(e) {2212 *   console.log(e.toString());  // Error: boom2213 * });2214 * </code></pre>2215 *2216 * @param {!Function} generatorFn The generator function to execute.2217 * @param {Object=} opt_self The object to use as "this" when invoking the2218 *     initial generator.2219 * @param {...*} var_args Any arguments to pass to the initial generator.2220 * @return {!webdriver.promise.Promise.<?>} A promise that will resolve to the2221 *     generator's final result.2222 * @throws {TypeError} If the given function is not a generator.2223 */2224webdriver.promise.consume = function(generatorFn, opt_self, var_args) {2225  if (!webdriver.promise.isGenerator(generatorFn)) {2226    throw TypeError('Input is not a GeneratorFunction: ' +2227        generatorFn.constructor.name);2228  }22292230  var deferred = webdriver.promise.defer();2231  var generator = generatorFn.apply(opt_self, goog.array.slice(arguments, 2));2232  callNext();2233  return deferred.promise;22342235  /** @param {*=} opt_value . */2236  function callNext(opt_value) {2237    pump(generator.next, opt_value);2238  }22392240  /** @param {*=} opt_error . */2241  function callThrow(opt_error) {2242    // Dictionary lookup required because Closure compiler's built-in2243    // externs does not include GeneratorFunction.prototype.throw.2244    pump(generator['throw'], opt_error);2245  }22462247  function pump(fn, opt_arg) {2248    if (!deferred.isPending()) {2249      return;  // Defererd was cancelled; silently abort.2250    }22512252    try {2253      var result = fn.call(generator, opt_arg);2254    } catch (ex) {2255      deferred.reject(ex);2256      return;2257    }22582259    if (result.done) {2260      deferred.fulfill(result.value);2261      return;2262    }22632264    webdriver.promise.asap(result.value, callNext, callThrow);2265  }2266};