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 at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// 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 and
13// limitations under the License.
14
15/**
16 * @license Portions of this code are from the Dojo toolkit, received under the
17 * BSD License:
18 * Redistribution and use in source and binary forms, with or without
19 * 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 documentation
25 * and/or other materials provided with the distribution.
26 * * Neither the name of the Dojo Foundation nor the names of its contributors
27 * may be used to endorse or promote products derived from this software
28 * 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, THE
32 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
34 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
35 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
36 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
37 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
38 * 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 THE
40 * POSSIBILITY OF SUCH DAMAGE.
41 */
42
43/**
44 * @fileoverview A promise implementation based on the CommonJS promise/A and
45 * promise/B proposals. For more information, see
46 * http://wiki.commonjs.org/wiki/Promises.
47 */
48
49goog.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');
55
56goog.require('goog.array');
57goog.require('goog.debug.Error');
58goog.require('goog.object');
59goog.require('webdriver.EventEmitter');
60goog.require('webdriver.stacktrace.Snapshot');
61
62
63
64/**
65 * Error used when the computation of a promise is cancelled.
66 *
67 * @param {string=} opt_msg The cancellation message.
68 * @constructor
69 * @extends {goog.debug.Error}
70 * @final
71 */
72webdriver.promise.CancellationError = function(opt_msg) {
73 goog.debug.Error.call(this, opt_msg);
74
75 /** @override */
76 this.name = 'CancellationError';
77};
78goog.inherits(webdriver.promise.CancellationError, goog.debug.Error);
79
80
81
82/**
83 * Thenable is a promise-like object with a {@code then} method which may be
84 * used to schedule callbacks on a promised value.
85 *
86 * @interface
87 * @template T
88 */
89webdriver.promise.Thenable = function() {};
90
91
92/**
93 * Cancels the computation of this promise's value, rejecting the promise in the
94 * 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) {};
99
100
101/** @return {boolean} Whether this promise's value is still being computed. */
102webdriver.promise.Thenable.prototype.isPending = function() {};
103
104
105/**
106 * Registers listeners for when this instance is resolved.
107 *
108 * @param {?(function(T): (R|webdriver.promise.Promise.<R>))=} opt_callback The
109 * function to call if this promise is successfully resolved. The function
110 * should expect a single argument: the promise's resolved value.
111 * @param {?(function(*): (R|webdriver.promise.Promise.<R>))=} opt_errback The
112 * function to call if this promise is rejected. The function should expect
113 * a single argument: the rejection reason.
114 * @return {!webdriver.promise.Promise.<R>} A new promise which will be
115 * resolved with the result of the invoked callback.
116 * @template R
117 */
118webdriver.promise.Thenable.prototype.then = function(
119 opt_callback, opt_errback) {};
120
121
122/**
123 * Registers a listener for when this promise is rejected. This is synonymous
124 * 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 function
140 * to call if this promise is rejected. The function should expect a single
141 * argument: the rejection reason.
142 * @return {!webdriver.promise.Promise.<R>} A new promise which will be
143 * resolved with the result of the invoked callback.
144 * @template R
145 */
146webdriver.promise.Thenable.prototype.thenCatch = function(errback) {};
147
148
149/**
150 * Registers a listener to invoke when this promise is resolved, regardless
151 * of whether the promise's value was successfully computed. This function
152 * 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 registered
166 * callback returns a rejected promise or throws an error, it will silently
167 * 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: one
173 * }
174 *
175 * webdriver.promise.rejected(Error('one'))
176 * .thenFinally(function() {
177 * throw Error('two'); // Hides Error: one
178 * });
179 * </code></pre>
180 *
181 *
182 * @param {function(): (R|webdriver.promise.Promise.<R>)} callback The function
183 * to call when this promise is resolved.
184 * @return {!webdriver.promise.Promise.<R>} A promise that will be fulfilled
185 * with the callback result.
186 * @template R
187 */
188webdriver.promise.Thenable.prototype.thenFinally = function(callback) {};
189
190
191/**
192 * Property used to flag constructor's as implementing the Thenable interface
193 * for runtime type checking.
194 * @private {string}
195 * @const
196 */
197webdriver.promise.Thenable.IMPLEMENTED_BY_PROP_ = '$webdriver_Thenable';
198
199
200/**
201 * Adds a property to a class prototype to allow runtime checks of whether
202 * instances of that class implement the Thenable interface. This function will
203 * also ensure the prototype's {@code then} function is exported from compiled
204 * code.
205 * @param {function(new: webdriver.promise.Thenable, ...[?])} ctor The
206 * 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 for
213 // 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};
222
223
224/**
225 * Checks if an object has been tagged for implementing the Thenable interface
226 * 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 Thenable
229 * 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};
242
243
244
245/**
246 * Represents the eventual value of a completed operation. Each promise may be
247 * in one of three states: pending, resolved, or rejected. Each promise starts
248 * in the pending state and may make a single transition to either a
249 * fulfilled or rejected state, at which point the promise is considered
250 * resolved.
251 *
252 * @constructor
253 * @implements {webdriver.promise.Thenable.<T>}
254 * @template T
255 * @see http://promises-aplus.github.io/promises-spec/
256 */
257webdriver.promise.Promise = function() {};
258webdriver.promise.Thenable.addImplementation(webdriver.promise.Promise);
259
260
261/** @override */
262webdriver.promise.Promise.prototype.cancel = function(reason) {
263 throw new TypeError('Unimplemented function: "cancel"');
264};
265
266
267/** @override */
268webdriver.promise.Promise.prototype.isPending = function() {
269 throw new TypeError('Unimplemented function: "isPending"');
270};
271
272
273/** @override */
274webdriver.promise.Promise.prototype.then = function(
275 opt_callback, opt_errback) {
276 throw new TypeError('Unimplemented function: "then"');
277};
278
279
280/** @override */
281webdriver.promise.Promise.prototype.thenCatch = function(errback) {
282 return this.then(null, errback);
283};
284
285
286/** @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};
298
299
300
301/**
302 * Represents a value that will be resolved at some point in the future. This
303 * class represents the protected "producer" half of a Promise - each Deferred
304 * has a {@code promise} property that may be returned to consumers for
305 * registering callbacks, reserving the ability to resolve the deferred to the
306 * producer.
307 *
308 * <p>If this Deferred is rejected and there are no listeners registered before
309 * the next turn of the event loop, the rejection will be passed to the
310 * {@link webdriver.promise.ControlFlow} as an unhandled failure.
311 *
312 * <p>If this Deferred is cancelled, the cancellation reason will be forward to
313 * the Deferred's canceller function (if provided). The canceller may return a
314 * truth-y value to override the reason provided for rejection.
315 *
316 * @param {webdriver.promise.ControlFlow=} opt_flow The control flow
317 * this instance was created under. This should only be provided during
318 * unit tests.
319 * @constructor
320 * @extends {webdriver.promise.Promise.<T>}
321 * @template T
322 */
323webdriver.promise.Deferred = function(opt_flow) {
324 /* NOTE: This class's implementation diverges from the prototypical style
325 * used in the rest of the atoms library. This was done intentionally to
326 * protect the internal Deferred state from consumers, as outlined by
327 * http://wiki.commonjs.org/wiki/Promises
328 */
329 goog.base(this);
330
331 var flow = opt_flow || webdriver.promise.controlFlow();
332
333 /**
334 * The deferred this instance is chained from, if any.
335 * @private {webdriver.promise.Deferred.<?>}
336 */
337 this.parent_ = null;
338
339 /**
340 * The listeners registered with this Deferred. Each element in the list will
341 * be a 3-tuple of the callback function, errback function, and the
342 * corresponding deferred object.
343 * @type {!Array.<!webdriver.promise.Deferred.Listener_>}
344 */
345 var listeners = [];
346
347 /**
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 listener
350 * before the next turn of the event loop, the error will be passed to the
351 * global error handler.
352 * @type {boolean}
353 */
354 var handled = false;
355
356 /**
357 * Key for the timeout used to delay reproting an unhandled rejection to the
358 * parent {@link webdriver.promise.ControlFlow}.
359 * @type {?number}
360 */
361 var pendingRejectionKey = null;
362
363 /**
364 * This Deferred's current state.
365 * @type {!webdriver.promise.Deferred.State_}
366 */
367 var state = webdriver.promise.Deferred.State_.PENDING;
368
369 /**
370 * This Deferred's resolved value; set when the state transitions from
371 * {@code webdriver.promise.Deferred.State_.PENDING}.
372 * @type {*}
373 */
374 var value;
375
376 /** @return {boolean} Whether this promise's value is still pending. */
377 function isPending() {
378 return state == webdriver.promise.Deferred.State_.PENDING;
379 }
380
381 /**
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 }
388
389 /**
390 * Resolves this deferred. If the new value is a promise, this function will
391 * wait for it to be resolved before notifying the registered listeners.
392 * @param {!webdriver.promise.Deferred.State_} newState The deferred's new
393 * 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 }
400
401 if (newValue === self) {
402 // See promise a+, 2.3.1
403 // http://promises-aplus.github.io/promises-spec/#point-48
404 throw TypeError('A promise may not resolve to itself');
405 }
406
407 state = webdriver.promise.Deferred.State_.BLOCKED;
408
409 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 }
418
419 } else {
420 notifyAll(newState, newValue);
421 }
422 }
423
424 /**
425 * Notifies all of the listeners registered with this Deferred that its state
426 * has changed.
427 * @param {!webdriver.promise.Deferred.State_} newState The deferred's new
428 * 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 been
434 // created in a different JS context.
435 goog.isObject(newValue) && goog.isString(newValue.message)) {
436 newValue = flow.annotateError(/** @type {!Error} */(newValue));
437 }
438
439 state = newState;
440 value = newValue;
441 while (listeners.length) {
442 notify(listeners.shift());
443 }
444
445 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 }
455
456 /**
457 * Notifies a single listener of this Deferred's change in state.
458 * @param {!webdriver.promise.Deferred.Listener_} listener The listener to
459 * 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 }
473
474 /**
475 * The consumer promise for this instance. Provides protected access to the
476 * callback registering functions.
477 * @type {!webdriver.promise.Promise.<T>}
478 */
479 var promise = new webdriver.promise.Promise();
480
481 var self = this;
482
483 /**
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 the
489 * result of the callback.
490 * @template R
491 * @see webdriver.promise.Promise#then
492 */
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 }
498
499 // The moment a listener is registered, we consider this deferred to be
500 // 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 }
507
508 var deferred = new webdriver.promise.Deferred(flow);
509 deferred.parent_ = self;
510
511 var listener = {
512 callback: opt_callback,
513 errback: opt_errback,
514 fulfill: deferred.fulfill,
515 reject: deferred.reject
516 };
517
518 if (state == webdriver.promise.Deferred.State_.PENDING ||
519 state == webdriver.promise.Deferred.State_.BLOCKED) {
520 listeners.push(listener);
521 } else {
522 notify(listener);
523 }
524
525 return deferred.promise;
526 }
527
528 /**
529 * Resolves this promise with the given value. If the value is itself a
530 * promise and not a reference to this deferred, this instance will wait for
531 * 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 }
537
538 /**
539 * Rejects this promise. If the error is itself a promise, this instance will
540 * be chained to it and be rejected with the error's resolved value.
541 * @param {*=} opt_error The rejection reason, typically either a
542 * {@code Error} or a {@code string}.
543 */
544 function reject(opt_error) {
545 resolve(webdriver.promise.Deferred.State_.REJECTED, opt_error);
546 }
547
548 /**
549 * Attempts to cancel the computation of this instance's value. This attempt
550 * 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 }
556
557 if (self.parent_) {
558 self.parent_.cancel(opt_reason);
559 } else {
560 reject(new webdriver.promise.CancellationError(opt_reason));
561 }
562 }
563
564 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;
570
571 // 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 }
576
577 // Export symbols necessary for the contract on this object to work in
578 // 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);
590
591
592/**
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 * @private
598 */
599webdriver.promise.Deferred.Listener_;
600
601
602/**
603 * The three states a {@link webdriver.promise.Deferred} object may be in.
604 * @enum {number}
605 * @private
606 */
607webdriver.promise.Deferred.State_ = {
608 REJECTED: -1,
609 PENDING: 0,
610 BLOCKED: 1,
611 RESOLVED: 2
612};
613
614
615/**
616 * Tests if a value is an Error-like object. This is more than an straight
617 * 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 * @private
621 */
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);
628
629};
630
631
632/**
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 our
642 // contract.
643 goog.isFunction(value['then']);
644};
645
646
647/**
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 before
650 * 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};
662
663
664/**
665 * Creates a new deferred object.
666 * @return {!webdriver.promise.Deferred.<T>} The new deferred object.
667 * @template T
668 */
669webdriver.promise.defer = function() {
670 return new webdriver.promise.Deferred();
671};
672
673
674/**
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 T
679 */
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};
688
689
690/**
691 * Creates a promise that has been rejected with the given reason.
692 * @param {*=} opt_reason The rejection reason; may be any value, but is
693 * usually an Error or a string.
694 * @return {!webdriver.promise.Promise.<T>} The rejected promise.
695 * @template T
696 */
697webdriver.promise.rejected = function(opt_reason) {
698 var deferred = new webdriver.promise.Deferred();
699 deferred.reject(opt_reason);
700 return deferred.promise;
701};
702
703
704/**
705 * Wraps a function that is assumed to be a node-style callback as its final
706 * argument. This callback takes two arguments: an error value (which will be
707 * 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 will
709 * 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 the
712 * final callback.
713 * @return {!webdriver.promise.Promise} A promise that will be resolved with the
714 * 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};
729
730
731/**
732 * Registers an observer on a promised {@code value}, returning a new promise
733 * 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 is
737 * resolved successfully.
738 * @param {Function=} opt_errback The function to call when the value is
739 * 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 }
746
747 var deferred = new webdriver.promise.Deferred();
748
749 webdriver.promise.asap(value, deferred.fulfill, deferred.reject);
750
751 return deferred.then(opt_callback, opt_errback);
752};
753
754
755/**
756 * Invokes the appropriate callback function as soon as a promised
757 * {@code value} is resolved. This function is similar to
758 * {@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 is
761 * resolved successfully.
762 * @param {Function=} opt_errback The function to call when the value is
763 * rejected.
764 */
765webdriver.promise.asap = function(value, callback, opt_errback) {
766 if (webdriver.promise.isPromise(value)) {
767 value.then(callback, opt_errback);
768
769 // Maybe a Dojo-like deferred object?
770 } else if (!!value && goog.isObject(value) &&
771 goog.isFunction(value.addCallbacks)) {
772 value.addCallbacks(callback, opt_errback);
773
774 // A raw value, return a resolved promise.
775 } else if (callback) {
776 callback(value);
777 }
778};
779
780
781/**
782 * Given an array of promises, will return a promise that will be fulfilled
783 * with the fulfillment values of the input array's values. If any of the
784 * input array's promises are rejected, the returned promise will be rejected
785 * with the same reason.
786 *
787 * @param {!Array.<(T|!webdriver.promise.Promise.<T>)>} arr An array of
788 * promises to wait on.
789 * @return {!webdriver.promise.Promise.<!Array.<T>>} A promise that is
790 * fulfilled with an array containing the fulfilled values of the
791 * input array, or rejected with the same reason as the first
792 * rejected value.
793 * @template T
794 */
795webdriver.promise.all = function(arr) {
796 var n = arr.length;
797 if (!n) {
798 return webdriver.promise.fulfilled([]);
799 }
800
801 var toFulfill = n;
802 var result = webdriver.promise.defer();
803 var values = [];
804
805 var onFulfill = function(index, value) {
806 values[index] = value;
807 toFulfill--;
808 if (toFulfill == 0) {
809 result.fulfill(values);
810 }
811 };
812
813 for (var i = 0; i < n; ++i) {
814 webdriver.promise.asap(
815 arr[i], goog.partial(onFulfill, i), result.reject);
816 }
817
818 return result.promise;
819};
820
821
822/**
823 * Calls a function for each element in an array and inserts the result into a
824 * new array, which is used as the fulfillment value of the promise returned
825 * by this function.
826 *
827 * <p>If the return value of the mapping function is a promise, this function
828 * 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, the
831 * promise returned by this function will be rejected with the same reason.
832 * Only the first failure will be reported; all subsequent errors will be
833 * silently ignored.
834 *
835 * @param {!(Array.<TYPE>|webdriver.promise.Promise.<!Array.<TYPE>>)} arr The
836 * array to iterator over, or a promise that will resolve to said array.
837 * @param {function(this: SELF, TYPE, number, !Array.<TYPE>): ?} fn The
838 * function to call for each element in the array. This function should
839 * 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' within
841 * {@code fn}.
842 * @template TYPE, SELF
843 */
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};
850
851
852/**
853 * Calls a function for each element in an array, and if the function returns
854 * true adds the element to a new array.
855 *
856 * <p>If the return value of the filter function is a promise, this function
857 * will wait for it to be fulfilled before determining whether to insert the
858 * element into the new array.
859 *
860 * <p>If the filter function throws or returns a rejected promise, the promise
861 * returned by this function will be rejected with the same reason. Only the
862 * first failure will be reported; all subsequent errors will be silently
863 * ignored.
864 *
865 * @param {!(Array.<TYPE>|webdriver.promise.Promise.<!Array.<TYPE>>)} arr The
866 * 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 function
869 * to call for each element in the array.
870 * @param {SELF=} opt_self The object to be used as the value of 'this' within
871 * {@code fn}.
872 * @template TYPE, SELF
873 */
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};
884
885
886/**
887 * Returns a promise that will be resolved with the input value in a
888 * fully-resolved state. If the value is an array, each element will be fully
889 * resolved. Likewise, if the value is an object, all keys will be fully
890 * resolved. In both cases, all nested arrays and objects will also be
891 * fully resolved. All fields are resolved in place; the returned promise will
892 * resolve on {@code value} and not a copy.
893 *
894 * Warning: This function makes no checks against objects that contain
895 * 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 version
904 * 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};
912
913
914/**
915 * @param {*} value The value to fully resolve. If a promise, assumed to
916 * already be resolved.
917 * @return {!webdriver.promise.Promise} A promise for a fully resolved version
918 * of the input value.
919 * @private
920 */
921webdriver.promise.fullyResolveValue_ = function(value) {
922 switch (goog.typeOf(value)) {
923 case 'array':
924 return webdriver.promise.fullyResolveKeys_(
925 /** @type {!Array} */ (value));
926
927 case 'object':
928 if (webdriver.promise.isPromise(value)) {
929 // We get here when the original input value is a promise that
930 // 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 it
933 // to the user instead of wrapping it in another promise.
934 return /** @type {!webdriver.promise.Promise} */ (value);
935 }
936
937 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 we
941 // only support objects with a certain level of nesting?
942 return webdriver.promise.fulfilled(value);
943 }
944
945 return webdriver.promise.fullyResolveKeys_(
946 /** @type {!Object} */ (value));
947
948 default: // boolean, function, null, number, string, undefined
949 return webdriver.promise.fulfilled(value);
950 }
951};
952
953
954/**
955 * @param {!(Array|Object)} obj the object to resolve.
956 * @return {!webdriver.promise.Promise} A promise that will be resolved with the
957 * input object once all of its values have been fully resolved.
958 * @private
959 */
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 }
966
967 var numResolved = 0;
968 var deferred = new webdriver.promise.Deferred();
969
970 // In pre-IE9, goog.array.forEach will not iterate properly over arrays
971 // containing undefined values because "index in array" returns false
972 // when array[index] === undefined (even for x = [undefined, 1]). To get
973 // around this, we need to use our own forEach implementation.
974 // DO NOT REMOVE THIS UNTIL WE NO LONGER SUPPORT IE8. This cannot be
975 // reproduced in IE9 by changing the browser/document modes, it requires an
976 // 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 };
983
984 forEachKey(obj, function(partialValue, key) {
985 var type = goog.typeOf(partialValue);
986 if (type != 'array' && type != 'object') {
987 maybeResolveValue();
988 return;
989 }
990
991 webdriver.promise.fullyResolved(partialValue).then(
992 function(resolvedValue) {
993 obj[key] = resolvedValue;
994 maybeResolveValue();
995 },
996 deferred.reject);
997 });
998
999 return deferred.promise;
1000
1001 function maybeResolveValue() {
1002 if (++numResolved == numKeys) {
1003 deferred.fulfill(obj);
1004 }
1005 }
1006};
1007
1008
1009//////////////////////////////////////////////////////////////////////////////
1010//
1011// webdriver.promise.ControlFlow
1012//
1013//////////////////////////////////////////////////////////////////////////////
1014
1015
1016
1017/**
1018 * Handles the execution of scheduled tasks, each of which may be an
1019 * asynchronous operation. The control flow will ensure tasks are executed in
1020 * the ordered scheduled, starting each task only once those before it have
1021 * completed.
1022 *
1023 * <p>Each task scheduled within this flow may return a
1024 * {@link webdriver.promise.Promise} to indicate it is an asynchronous
1025 * operation. The ControlFlow will wait for such promises to be resolved before
1026 * 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 a
1030 * frame will have priority over previously scheduled tasks. Furthermore, if
1031 * any of the tasks in the frame fails, the remainder of the tasks in that frame
1032 * will be discarded and the failure will be propagated to the user through the
1033 * callback/task's promised result.
1034 *
1035 * <p>Each time a ControlFlow empties its task queue, it will fire an
1036 * {@link webdriver.promise.ControlFlow.EventType.IDLE} event. Conversely,
1037 * whenever the flow terminates due to an unhandled error, it will remove all
1038 * remaining tasks in its queue and fire an
1039 * {@link webdriver.promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION} event. If
1040 * there are no listeners registered with the flow, the error will be
1041 * rethrown to the global error handler.
1042 *
1043 * @param {webdriver.promise.ControlFlow.Timer=} opt_timer The timer object
1044 * to use. Should only be set for testing.
1045 * @constructor
1046 * @extends {webdriver.EventEmitter}
1047 */
1048webdriver.promise.ControlFlow = function(opt_timer) {
1049 webdriver.EventEmitter.call(this);
1050
1051 /**
1052 * The timer used by this instance.
1053 * @type {webdriver.promise.ControlFlow.Timer}
1054 */
1055 this.timer = opt_timer || webdriver.promise.ControlFlow.defaultTimer;
1056
1057 /**
1058 * A list of recent tasks. Each time a new task is started, or a frame is
1059 * completed, the previously recorded task is removed from this list. If
1060 * there are multiple tasks, task N+1 is considered a sub-task of task
1061 * N.
1062 * @private {!Array.<!webdriver.promise.Task_>}
1063 */
1064 this.history_ = [];
1065
1066 /**
1067 * Tracks the active execution frame for this instance. Lazily initialized
1068 * when the first task is scheduled.
1069 * @private {webdriver.promise.Frame_}
1070 */
1071 this.activeFrame_ = null;
1072
1073 /**
1074 * A reference to the frame in which new tasks should be scheduled. If
1075 * {@code null}, tasks will be scheduled within the active frame. When forcing
1076 * a function to run in the context of a new frame, this pointer is used to
1077 * ensure tasks are scheduled within the newly created frame, even though it
1078 * won't be active yet.
1079 * @private {webdriver.promise.Frame_}
1080 * @see {#runInNewFrame_}
1081 */
1082 this.schedulingFrame_ = null;
1083
1084 /**
1085 * Timeout ID set when the flow is about to shutdown without any errors
1086 * being detected. Upon shutting down, the flow will emit an
1087 * {@link webdriver.promise.ControlFlow.EventType.IDLE} event. Idle events
1088 * always follow a brief timeout in order to catch latent errors from the last
1089 * completed task. If this task had a callback registered, but no errback, and
1090 * the task fails, the unhandled failure would not be reported by the promise
1091 * 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 unhandled
1097 * // failure for 1 turn of the event loop.
1098 * result.then(goog.nullFunction);
1099 *
1100 * @private {?number}
1101 */
1102 this.shutdownId_ = null;
1103
1104 /**
1105 * Interval ID for this instance's event loop.
1106 * @private {?number}
1107 */
1108 this.eventLoopId_ = null;
1109
1110 /**
1111 * The number of "pending" promise rejections.
1112 *
1113 * <p>Each time a promise is rejected and is not handled by a listener, it
1114 * will schedule a 0-based timeout to check if it is still unrejected in the
1115 * next turn of the JS-event loop. This allows listeners to attach to, and
1116 * handle, the rejected promise at any point in same turn of the event loop
1117 * that the promise was rejected.
1118 *
1119 * <p>When this flow's own event loop triggers, it will not run if there
1120 * are any outstanding promise rejections. This allows unhandled promises to
1121 * be reported before a new task is started, ensuring the error is reported
1122 * to the current task queue.
1123 *
1124 * @private {number}
1125 */
1126 this.pendingRejections_ = 0;
1127
1128 /**
1129 * The number of aborted frames since the last time a task was executed or a
1130 * frame completed successfully.
1131 * @private {number}
1132 */
1133 this.numAbortedFrames_ = 0;
1134};
1135goog.inherits(webdriver.promise.ControlFlow, webdriver.EventEmitter);
1136
1137
1138/**
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;
1145
1146
1147/**
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 the
1153 // current context, so do not reference them using "window" or
1154 // "goog.global". Also, we must invoke them in a closure, and not using
1155 // bind(), so we do not get "TypeError: Illegal invocation" (WebKit) or
1156 // "Invalid calling object" (IE) errors.
1157 return {
1158 clearInterval: wrap(clearInterval),
1159 clearTimeout: wrap(clearTimeout),
1160 setInterval: wrap(setInterval),
1161 setTimeout: wrap(setTimeout)
1162 };
1163
1164 function wrap(fn) {
1165 return function() {
1166 // Cannot use .call() or .apply() since we do not know which variable
1167 // the function is bound to, and using the wrong one will generate
1168 // an error.
1169 return fn(arguments[0], arguments[1]);
1170 };
1171 }
1172})();
1173
1174
1175/**
1176 * Events that may be emitted by an {@link webdriver.promise.ControlFlow}.
1177 * @enum {string}
1178 */
1179webdriver.promise.ControlFlow.EventType = {
1180
1181 /** Emitted when all tasks have been successfully executed. */
1182 IDLE: 'idle',
1183
1184 /** Emitted when a ControlFlow has been reset. */
1185 RESET: 'reset',
1186
1187 /** Emitted whenever a new task has been scheduled. */
1188 SCHEDULE_TASK: 'scheduleTask',
1189
1190 /**
1191 * Emitted whenever a control flow aborts due to an unhandled promise
1192 * rejection. This event will be emitted along with the offending rejection
1193 * reason. Upon emitting this event, the control flow will empty its task
1194 * queue and revert to its initial state.
1195 */
1196 UNCAUGHT_EXCEPTION: 'uncaughtException'
1197};
1198
1199
1200/**
1201 * How often, in milliseconds, the event loop should run.
1202 * @type {number}
1203 * @const
1204 */
1205webdriver.promise.ControlFlow.EVENT_LOOP_FREQUENCY = 10;
1206
1207
1208/**
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};
1219
1220
1221/**
1222 * Returns a summary of the recent task activity for this instance. This
1223 * includes the most recently completed task, as well as any parent tasks. In
1224 * the returned summary, the task at index N is considered a sub-task of the
1225 * task at index N+1.
1226 * @return {!Array.<string>} A summary of this instance's recent task
1227 * 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 }
1241
1242 var fullHistory = goog.array.concat(this.history_, pendingTasks);
1243 return goog.array.map(fullHistory, function(task) {
1244 return task.toString();
1245 });
1246};
1247
1248
1249/** Clears this instance's task history. */
1250webdriver.promise.ControlFlow.prototype.clearHistory = function() {
1251 this.history_ = [];
1252};
1253
1254
1255/**
1256 * Removes a completed task from this instance's history record. If any
1257 * tasks remain from aborted frames, those will be removed as well.
1258 * @private
1259 */
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};
1269
1270
1271/**
1272 * Property used to track whether an error has been annotated by
1273 * {@link webdriver.promise.ControlFlow#annotateError}.
1274 * @private {string}
1275 * @const
1276 */
1277webdriver.promise.ControlFlow.ANNOTATION_PROPERTY_ =
1278 'webdriver_promise_error_';
1279
1280
1281/**
1282 * Appends a summary of this instance's recent task history to the given
1283 * error's stack trace. This function will also ensure the error's stack trace
1284 * 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 }
1292
1293 var history = this.getHistory();
1294 if (history.length) {
1295 e = webdriver.stacktrace.format(e);
1296
1297 /** @type {!Error} */(e).stack += [
1298 '\n==== async task ====\n',
1299 history.join('\n==== async task ====\n')
1300 ].join('');
1301
1302 e[webdriver.promise.ControlFlow.ANNOTATION_PROPERTY_] = true;
1303 }
1304
1305 return e;
1306};
1307
1308
1309/**
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};
1315
1316
1317/**
1318 * Schedules a task for execution. If there is nothing currently in the
1319 * queue, the task will be executed in the next turn of the event loop. If
1320 * the task function is a generator, the task will be executed using
1321 * {@link webdriver.promise.consume}.
1322 *
1323 * @param {function(): (T|webdriver.promise.Promise.<T>)} fn The function to
1324 * call to start the task. If the function returns a
1325 * {@link webdriver.promise.Promise}, this instance will wait for it to be
1326 * 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 resolved
1329 * with the result of the action.
1330 * @template T
1331 */
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 }
1337
1338 this.cancelShutdown_();
1339
1340 if (!this.activeFrame_) {
1341 this.activeFrame_ = new webdriver.promise.Frame_(this);
1342 }
1343
1344 // Trim an extra frame off the generated stack trace for the call to this
1345 // 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);
1351
1352 this.emit(webdriver.promise.ControlFlow.EventType.SCHEDULE_TASK, opt_description);
1353
1354 this.scheduleEventLoopStart_();
1355 return task.promise;
1356};
1357
1358
1359/**
1360 * Inserts a {@code setTimeout} into the command queue. This is equivalent to
1361 * 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 with
1366 * 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};
1374
1375
1376/**
1377 * Schedules a task that shall wait for a condition to hold. Each condition
1378 * 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 for
1384 * 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 timed
1386 * out.
1387 *
1388 * <p>If the condition function throws, or returns a rejected promise, the
1389 * 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 condition
1393 * to hold before timing out.
1394 * @param {string=} opt_message An optional error message to include if the
1395 * wait times out; defaults to the empty string.
1396 * @return {!webdriver.promise.Promise.<T>} A promise that will be fulfilled
1397 * when the condition has been satisified. The promise shall be rejected if
1398 * the wait times out waiting for the condition.
1399 * @template T
1400 */
1401webdriver.promise.ControlFlow.prototype.wait = function(
1402 condition, timeout, opt_message) {
1403 var sleep = Math.min(timeout, 100);
1404 var self = this;
1405
1406 if (webdriver.promise.isGenerator(condition)) {
1407 condition = goog.partial(webdriver.promise.consume, condition);
1408 }
1409
1410 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;
1417
1418 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};
1434
1435
1436/**
1437 * Schedules a task that will wait for another promise to resolve. The resolved
1438 * 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 the
1441 * task has completed.
1442 */
1443webdriver.promise.ControlFlow.prototype.await = function(promise) {
1444 return this.execute(function() {
1445 return promise;
1446 });
1447};
1448
1449
1450/**
1451 * Schedules the interval for this instance's event loop, if necessary.
1452 * @private
1453 */
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};
1461
1462
1463/**
1464 * Cancels the event loop, if necessary.
1465 * @private
1466 */
1467webdriver.promise.ControlFlow.prototype.cancelEventLoop_ = function() {
1468 if (this.eventLoopId_) {
1469 this.timer.clearInterval(this.eventLoopId_);
1470 this.eventLoopId_ = null;
1471 }
1472};
1473
1474
1475/**
1476 * Executes the next task for the current frame. If the current frame has no
1477 * more tasks, the frame's result will be resolved, returning control to the
1478 * frame's creator. This will terminate the flow if the completed frame was at
1479 * the top of the stack.
1480 * @private
1481 */
1482webdriver.promise.ControlFlow.prototype.runEventLoop_ = function() {
1483 // If we get here and there are pending promise rejections, then those
1484 // 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 these
1487 // pending rejections.
1488 if (this.pendingRejections_) {
1489 return;
1490 }
1491
1492 // If the flow aborts due to an unhandled exception after we've scheduled
1493 // another turn of the execution loop, we can end up in here with no tasks
1494 // left. This is OK, just quietly return.
1495 if (!this.activeFrame_) {
1496 this.commenceShutdown_();
1497 return;
1498 }
1499
1500 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 have
1503 // a task to finish because we've completed a frame. When completing a
1504 // frame, we must abort the event loop to allow the frame's promise's
1505 // callbacks to execute.
1506 return;
1507 }
1508
1509 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);
1515
1516 this.trimHistory_();
1517 var self = this;
1518 this.runInNewFrame_(task.execute, function(result) {
1519 markTaskComplete();
1520 task.fulfill(result);
1521 }, function(error) {
1522 markTaskComplete();
1523
1524 if (!webdriver.promise.isError_(error) &&
1525 !webdriver.promise.isPromise(error)) {
1526 error = Error(error);
1527 }
1528
1529 task.reject(self.annotateError(/** @type {!Error} */ (error)));
1530 }, true);
1531};
1532
1533
1534/**
1535 * @return {webdriver.promise.Task_} The next task to execute, or
1536 * {@code null} if a frame was resolved.
1537 * @private
1538 */
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 }
1548
1549 if (firstChild instanceof webdriver.promise.Frame_) {
1550 this.activeFrame_ = firstChild;
1551 return this.getNextTask_();
1552 }
1553
1554 frame.removeChild(firstChild);
1555 return firstChild;
1556};
1557
1558
1559/**
1560 * @param {!webdriver.promise.Frame_} frame The frame to resolve.
1561 * @private
1562 */
1563webdriver.promise.ControlFlow.prototype.resolveFrame_ = function(frame) {
1564 if (this.activeFrame_ === frame) {
1565 // Frame parent is always another frame, but the compiler is not smart
1566 // enough to recognize this.
1567 this.activeFrame_ =
1568 /** @type {webdriver.promise.Frame_} */ (frame.getParent());
1569 }
1570
1571 if (frame.getParent()) {
1572 frame.getParent().removeChild(frame);
1573 }
1574 this.trimHistory_();
1575 frame.close();
1576
1577 if (!this.activeFrame_) {
1578 this.commenceShutdown_();
1579 }
1580};
1581
1582
1583/**
1584 * Aborts the current frame. The frame, and all of the tasks scheduled within it
1585 * will be discarded. If this instance does not have an active frame, it will
1586 * immediately terminate all execution.
1587 * @param {*} error The reason the frame is being aborted; typically either
1588 * an Error or string.
1589 * @private
1590 */
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_++;
1597
1598 if (!this.activeFrame_) {
1599 this.abortNow_(error);
1600 return;
1601 }
1602
1603 // Frame parent is always another frame, but the compiler is not smart
1604 // enough to recognize this.
1605 var parent = /** @type {webdriver.promise.Frame_} */ (
1606 this.activeFrame_.getParent());
1607 if (parent) {
1608 parent.removeChild(this.activeFrame_);
1609 }
1610
1611 var frame = this.activeFrame_;
1612 this.activeFrame_ = parent;
1613 frame.abort(error);
1614};
1615
1616
1617/**
1618 * Executes a function in a new frame. If the function does not schedule any new
1619 * tasks, the frame will be discarded and the function's result returned
1620 * immediately. Otherwise, a promise will be returned. This promise will be
1621 * resolved with the function's result once all of the tasks scheduled within
1622 * the function have been completed. If the function's frame is aborted, the
1623 * 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 to
1629 * the newly created frame so tasks are treated as sub-tasks.
1630 * @private
1631 */
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_;
1637
1638 try {
1639 if (!this.activeFrame_) {
1640 this.activeFrame_ = newFrame;
1641 } else {
1642 this.activeFrame_.addChild(newFrame);
1643 }
1644
1645 // Activate the new frame to force tasks to be treated as sub-tasks of
1646 // the parent frame.
1647 if (opt_activate) {
1648 this.activeFrame_ = newFrame;
1649 }
1650
1651 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;
1660
1661 // If there was nothing scheduled in the new frame we can discard the
1662 // frame and return immediately.
1663 if (!newFrame.children_.length) {
1664 removeNewFrame();
1665 webdriver.promise.asap(result, callback, errback);
1666 return;
1667 }
1668
1669 newFrame.onComplete = function() {
1670 webdriver.promise.asap(result, callback, errback);
1671 };
1672
1673 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 }
1685
1686 /**
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 }
1694
1695 if (opt_err) {
1696 newFrame.cancelRemainingTasks(
1697 'Tasks cancelled due to uncaught error: ' + opt_err);
1698 }
1699 self.activeFrame_ = oldFrame;
1700 }
1701};
1702
1703
1704/**
1705 * Commences the shutdown sequence for this instance. After one turn of the
1706 * event loop, this object will emit the
1707 * {@link webdriver.promise.ControlFlow.EventType.IDLE} event to signal
1708 * listeners that it has completed. During this wait, if another task is
1709 * scheduled, the shutdown will be aborted.
1710 * @private
1711 */
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 are
1715 // no more frames with tasks to execute. If we waited to cancel the event
1716 // loop in our timeout below, the event loop could trigger *before* the
1717 // timeout, generating an error from there being no frames.
1718 // If #execute is called before the timeout below fires, it will cancel
1719 // the timeout and restart the event loop.
1720 this.cancelEventLoop_();
1721
1722 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};
1729
1730
1731/**
1732 * Cancels the shutdown sequence if it is currently scheduled.
1733 * @private
1734 */
1735webdriver.promise.ControlFlow.prototype.cancelShutdown_ = function() {
1736 if (this.shutdownId_) {
1737 this.timer.clearTimeout(this.shutdownId_);
1738 this.shutdownId_ = null;
1739 }
1740};
1741
1742
1743/**
1744 * Aborts this flow, abandoning all remaining tasks. If there are
1745 * listeners registered, an {@code UNCAUGHT_EXCEPTION} will be emitted with the
1746 * offending {@code error}, otherwise, the {@code error} will be rethrown to the
1747 * global error handler.
1748 * @param {*} error Object describing the error that caused the flow to
1749 * abort; usually either an Error or string value.
1750 * @private
1751 */
1752webdriver.promise.ControlFlow.prototype.abortNow_ = function(error) {
1753 this.activeFrame_ = null;
1754 this.cancelShutdown_();
1755 this.cancelEventLoop_();
1756
1757 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};
1768
1769
1770
1771/**
1772 * An execution frame within a {@link webdriver.promise.ControlFlow}. Each
1773 * frame represents the execution context for either a
1774 * {@link webdriver.promise.Task_} or a callback on a
1775 * {@link webdriver.promise.Deferred}.
1776 *
1777 * <p>Each frame may contain sub-frames. If child N is a sub-frame, then the
1778 * items queued within it are given priority over child N+1.
1779 *
1780 * @param {!webdriver.promise.ControlFlow} flow The flow this instance belongs
1781 * to.
1782 * @constructor
1783 * @private
1784 * @final
1785 * @struct
1786 */
1787webdriver.promise.Frame_ = function(flow) {
1788 /** @private {!webdriver.promise.ControlFlow} */
1789 this.flow_ = flow;
1790
1791 /** @private {webdriver.promise.Frame_} */
1792 this.parent_ = null;
1793
1794 /**
1795 * @private {!Array.<!(webdriver.promise.Frame_|webdriver.promise.Task_)>}
1796 */
1797 this.children_ = [];
1798
1799
1800 /** @private {(webdriver.promise.Frame_|webdriver.promise.Task_)} */
1801 this.lastInsertedChild_ = null;
1802
1803 /**
1804 * The task currently being executed within this frame.
1805 * @private {webdriver.promise.Task_}
1806 */
1807 this.pendingTask_ = null;
1808
1809 /**
1810 * Whether this frame is currently locked. A locked frame represents an
1811 * executed function that has scheduled all of its tasks.
1812 *
1813 * <p>Once a frame becomes locked, any new frames which are added as children
1814 * represent interrupts (such as a {@link webdriver.promise.Promise}
1815 * callback) whose tasks must be given priority over those already scheduled
1816 * 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;
1828
1829 /** @type {boolean} */
1830 this.isWaiting = false;
1831
1832 /**
1833 * The function to notify if this frame executes without error.
1834 * @type {?function()}
1835 */
1836 this.onComplete = null;
1837
1838 /**
1839 * The function to notify if this frame is aborted with an error.
1840 * @type {?function(*)}
1841 */
1842 this.onAbort = null;
1843};
1844
1845
1846/** @return {webdriver.promise.Frame_} This frame's parent, if any. */
1847webdriver.promise.Frame_.prototype.getParent = function() {
1848 return this.parent_;
1849};
1850
1851
1852/**
1853 * @param {webdriver.promise.Frame_} parent This frame's new parent.
1854 */
1855webdriver.promise.Frame_.prototype.setParent = function(parent) {
1856 this.parent_ = parent;
1857};
1858
1859
1860/**
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};
1870
1871
1872/**
1873 * Aborts the execution of this frame, cancelling all outstanding tasks
1874 * 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);
1881
1882 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};
1893
1894
1895/**
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};
1904
1905
1906/**
1907 * @param {?(function(*)|function())} fn The function to notify.
1908 * @param {*=} opt_error Value to pass to the notified function, if any.
1909 * @private
1910 */
1911webdriver.promise.Frame_.prototype.notify_ = function(fn, opt_error) {
1912 this.onAbort = this.onComplete = null;
1913 if (fn) {
1914 fn(opt_error);
1915 }
1916};
1917
1918
1919/**
1920 * Marks all of the tasks that are descendants of this frame in the execution
1921 * 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: boom
1937 * // task failed! CancelledTaskError: Task discarded due to a previous
1938 * // task failure: Error: boom
1939 *
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 that
1948 // the task is being canceled, however, we need at least one errback
1949 // to prevent the cancellation from bubbling up.
1950 child.removeAll();
1951 child.thenCatch(goog.nullFunction);
1952 child.cancel(reason);
1953 }
1954 });
1955};
1956
1957
1958/**
1959 * @return {webdriver.promise.Task_} The task currently executing
1960 * within this frame, if any.
1961 */
1962webdriver.promise.Frame_.prototype.getPendingTask = function() {
1963 return this.pendingTask_;
1964};
1965
1966
1967/**
1968 * @param {webdriver.promise.Task_} task The task currently
1969 * executing within this frame, if any.
1970 */
1971webdriver.promise.Frame_.prototype.setPendingTask = function(task) {
1972 this.pendingTask_ = task;
1973};
1974
1975
1976/**
1977 * Adds a new node to this frame.
1978 * @param {!(webdriver.promise.Frame_|webdriver.promise.Task_)} node
1979 * 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 }
1988
1989 if (node instanceof webdriver.promise.Frame_) {
1990 node.setParent(this);
1991 }
1992
1993 if (this.isLocked_ && node instanceof webdriver.promise.Frame_) {
1994 var index = 0;
1995 if (this.lastInsertedChild_ instanceof
1996 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 }
2003
2004 this.lastInsertedChild_ = node;
2005 this.children_.push(node);
2006};
2007
2008
2009/**
2010 * @return {(webdriver.promise.Frame_|webdriver.promise.Task_)} This frame's
2011 * fist child.
2012 */
2013webdriver.promise.Frame_.prototype.getFirstChild = function() {
2014 this.isLocked_ = true;
2015 this.lastInsertedChild_ = null;
2016 return this.children_[0];
2017};
2018
2019
2020/**
2021 * Removes a child from this frame.
2022 * @param {!(webdriver.promise.Frame_|webdriver.promise.Task_)} child
2023 * 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};
2035
2036
2037/** @override */
2038webdriver.promise.Frame_.prototype.toString = function() {
2039 return '[' + goog.array.map(this.children_, function(child) {
2040 return child.toString();
2041 }).join(', ') + ']';
2042};
2043
2044
2045
2046/**
2047 * A task to be executed by a {@link webdriver.promise.ControlFlow}.
2048 *
2049 * @param {!webdriver.promise.ControlFlow} flow The flow this instances belongs
2050 * to.
2051 * @param {function(): (T|!webdriver.promise.Promise.<T>)} fn The function to
2052 * call when the task executes. If it returns a
2053 * {@link webdriver.promise.Promise}, the flow will wait for it to be
2054 * 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 stack
2057 * when this task was scheduled.
2058 * @constructor
2059 * @extends {webdriver.promise.Deferred.<T>}
2060 * @template T
2061 * @private
2062 */
2063webdriver.promise.Task_ = function(flow, fn, description, snapshot) {
2064 webdriver.promise.Deferred.call(this, flow);
2065
2066 /**
2067 * @type {function(): (T|!webdriver.promise.Promise.<T>)}
2068 */
2069 this.execute = fn;
2070
2071 /** @private {string} */
2072 this.description_ = description;
2073
2074 /** @private {!webdriver.stacktrace.Snapshot} */
2075 this.snapshot_ = snapshot;
2076};
2077goog.inherits(webdriver.promise.Task_, webdriver.promise.Deferred);
2078
2079
2080/** @return {string} This task's description. */
2081webdriver.promise.Task_.prototype.getDescription = function() {
2082 return this.description_;
2083};
2084
2085
2086/** @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};
2098
2099
2100
2101/**
2102 * The default flow to use if no others are active.
2103 * @private {!webdriver.promise.ControlFlow}
2104 */
2105webdriver.promise.defaultFlow_ = new webdriver.promise.ControlFlow();
2106
2107
2108/**
2109 * A stack of active control flows, with the top of the stack used to schedule
2110 * commands. When there are multiple flows on the stack, the flow at index N
2111 * represents a callback triggered within a task owned by the flow at index
2112 * N-1.
2113 * @private {!Array.<!webdriver.promise.ControlFlow>}
2114 */
2115webdriver.promise.activeFlows_ = [];
2116
2117
2118/**
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};
2129
2130
2131/**
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};
2139
2140
2141/**
2142 * @param {!webdriver.promise.ControlFlow} flow The new flow.
2143 * @private
2144 */
2145webdriver.promise.pushFlow_ = function(flow) {
2146 webdriver.promise.activeFlows_.push(flow);
2147};
2148
2149
2150/** @private */
2151webdriver.promise.popFlow_ = function() {
2152 webdriver.promise.activeFlows_.pop();
2153};
2154
2155
2156/**
2157 * Creates a new control flow. The provided callback will be invoked as the
2158 * first task within the new flow, with the flow as its sole argument. Returns
2159 * a promise that resolves to the callback result.
2160 * @param {function(!webdriver.promise.ControlFlow)} callback The entry point
2161 * to the newly created flow.
2162 * @return {!webdriver.promise.Promise} A promise that resolves to the callback
2163 * 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};
2172
2173
2174/**
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};
2182
2183
2184/**
2185 * Consumes a {@code GeneratorFunction}. Each time the generator yields a
2186 * promise, this function will wait for it to be fulfilled before feeding the
2187 * fulfilled value back into {@code next}. Likewise, if a yielded promise is
2188 * 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); // 13
2202 * });
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: boom
2213 * });
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 the
2218 * 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 the
2221 * 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 }
2229
2230 var deferred = webdriver.promise.defer();
2231 var generator = generatorFn.apply(opt_self, goog.array.slice(arguments, 2));
2232 callNext();
2233 return deferred.promise;
2234
2235 /** @param {*=} opt_value . */
2236 function callNext(opt_value) {
2237 pump(generator.next, opt_value);
2238 }
2239
2240 /** @param {*=} opt_error . */
2241 function callThrow(opt_error) {
2242 // Dictionary lookup required because Closure compiler's built-in
2243 // externs does not include GeneratorFunction.prototype.throw.
2244 pump(generator['throw'], opt_error);
2245 }
2246
2247 function pump(fn, opt_arg) {
2248 if (!deferred.isPending()) {
2249 return; // Defererd was cancelled; silently abort.
2250 }
2251
2252 try {
2253 var result = fn.call(generator, opt_arg);
2254 } catch (ex) {
2255 deferred.reject(ex);
2256 return;
2257 }
2258
2259 if (result.done) {
2260 deferred.fulfill(result.value);
2261 return;
2262 }
2263
2264 webdriver.promise.asap(result.value, callNext, callThrow);
2265 }
2266};