selenium-webdriver
Version:
The official WebDriver JavaScript bindings from the Selenium project
1,578 lines (1,452 loc) • 96 kB
JavaScript
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @license Portions of this code are from the Dojo toolkit, received under the
* BSD License:
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of the Dojo Foundation nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @fileoverview
* The promise module is centered around the
* {@linkplain webdriver.promise.ControlFlow ControlFlow}, a class that
* coordinates the execution of asynchronous tasks. The ControlFlow allows users
* to focus on the imperative commands for their script without worrying about
* chaining together every single asynchronous action, which can be tedious and
* verbose. APIs may be layered on top of the control flow to read as if they
* were synchronous. For instance, the core
* {@linkplain webdriver.WebDriver WebDriver} API is built on top of the
* control flow, allowing users to write
*
* driver.get('http://www.google.com/ncr');
* driver.findElement({name: 'q'}).sendKeys('webdriver');
* driver.findElement({name: 'btnGn'}).click();
*
* instead of
*
* driver.get('http://www.google.com/ncr')
* .then(function() {
* return driver.findElement({name: 'q'});
* })
* .then(function(q) {
* return q.sendKeys('webdriver');
* })
* .then(function() {
* return driver.findElement({name: 'btnG'});
* })
* .then(function(btnG) {
* return btnG.click();
* });
*
* ## Tasks and Task Queues
*
* The control flow is based on the concept of tasks and task queues. Tasks are
* functions that define the basic unit of work for the control flow to execute.
* Each task is scheduled via
* {@link webdriver.promise.ControlFlow#execute() ControlFlow#execute()}, which
* will return a {@link webdriver.promise.Promise Promise} that will be resolved
* with the task's result.
*
* A task queue contains all of the tasks scheduled within a single turn of the
* [JavaScript event loop][JSEL]. The control flow will create a new task queue
* the first time a task is scheduled within an event loop.
*
* var flow = promise.controlFlow();
* flow.execute(foo); // Creates a new task queue and inserts foo.
* flow.execute(bar); // Inserts bar into the same queue as foo.
* setTimeout(function() {
* flow.execute(baz); // Creates a new task queue and inserts baz.
* }, 0);
*
* Whenever the control flow creates a new task queue, it will automatically
* begin executing tasks in the next available turn of the event loop. This
* execution is scheduled using a "micro-task" timer, such as a (native)
* `Promise.then()` callback.
*
* setTimeout(() => console.log('a'));
* Promise.resolve().then(() => console.log('b')); // A native promise.
* flow.execute(() => console.log('c'));
* Promise.resolve().then(() => console.log('d'));
* setTimeout(() => console.log('fin'));
* // b
* // c
* // d
* // a
* // fin
*
* In the example above, b/c/d is logged before a/fin because native promises
* and this module use "micro-task" timers, which have a higher priority than
* "macro-tasks" like `setTimeout`.
*
* ## Task Execution
*
* Upon creating a task queue, and whenever an exisiting queue completes a task,
* the control flow will schedule a micro-task timer to process any scheduled
* tasks. This ensures no task is ever started within the same turn of the
* JavaScript event loop in which it was scheduled, nor is a task ever started
* within the same turn that another finishes.
*
* When the execution timer fires, a single task will be dequeued and executed.
* There are several important events that may occur while executing a task
* function:
*
* 1. A new task queue is created by a call to
* {@link webdriver.promise.ControlFlow#execute ControlFlow#execute()}. Any
* tasks scheduled within this task queue are considered subtasks of the
* current task.
* 2. The task function throws an error. Any scheduled tasks are immediately
* discarded and the task's promised result (previously returned by
* {@link webdriver.promise.ControlFlow#execute ControlFlow#execute()}) is
* immediately rejected with the thrown error.
* 3. The task function returns sucessfully.
*
* If a task function created a new task queue, the control flow will wait for
* that queue to complete before processing the task result. If the queue
* completes without error, the flow will settle the task's promise with the
* value originaly returned by the task function. On the other hand, if the task
* queue termintes with an error, the task's promise will be rejected with that
* error.
*
* flow.execute(function() {
* flow.execute(() => console.log('a'));
* flow.execute(() => console.log('b'));
* });
* flow.execute(() => console.log('c'));
* // a
* // b
* // c
*
* ## Promise Integration
*
* In addition to the {@link webdriver.promise.ControlFlow ControlFlow} class,
* the promise module also exports a [Promise/A+]
* {@linkplain webdriver.promise.Promise implementation} that is deeply
* integrated with the ControlFlow. First and foremost, each promise
* {@linkplain webdriver.promise.Promise#then() callback} is scheduled with the
* control flow as a task. As a result, each callback is invoked in its own turn
* of the JavaScript event loop with its own task queue. If any tasks are
* scheduled within a callback, the callback's promised result will not be
* settled until the task queue has completed.
*
* promise.fulfilled().then(function() {
* flow.execute(function() {
* console.log('b');
* });
* }).then(() => console.log('a'));
* // b
* // a
*
* ### Scheduling Promise Callbacks <a id="scheduling_callbacks"></a>
*
* How callbacks are scheduled in the control flow depends on when they are
* attached to the promise. Callbacks attached to a _previously_ resolved
* promise are immediately enqueued as subtasks of the currently running task.
*
* var p = promise.fulfilled();
* flow.execute(function() {
* flow.execute(() => console.log('A'));
* p.then( () => console.log('B'));
* flow.execute(() => console.log('C'));
* p.then( () => console.log('D'));
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* When a promise is resolved while a task function is on the call stack, any
* callbacks also registered in that stack frame are scheduled as if the promise
* were already resolved:
*
* var d = promise.defer();
* flow.execute(function() {
* flow.execute( () => console.log('A'));
* d.promise.then(() => console.log('B'));
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* If a promise is resolved while a task function is on the call stack, any
* previously registered callbacks (i.e. attached while the task was _not_ on
* the call stack), act as _interrupts_ and are inserted at the front of the
* task queue. If multiple promises are fulfilled, their interrupts are enqueued
* in the order the promises are resolved.
*
* var d1 = promise.defer();
* d1.promise.then(() => console.log('A'));
*
* var d2 = promise.defer();
* d2.promise.then(() => console.log('B'));
*
* flow.execute(function() {
* flow.execute(() => console.log('C'));
* flow.execute(() => console.log('D'));
* d1.fulfill();
* d2.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* Within a task function (or callback), each step of a promise chain acts as
* an interrupt on the task queue:
*
* var d = promise.defer();
* flow.execute(function() {
* d.promise.
* then(() => console.log('A')).
* then(() => console.log('B')).
* then(() => console.log('C')).
* then(() => console.log('D'));
*
* flow.execute(() => console.log('E'));
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // fin
*
* If there are multiple promise chains derived from a single promise, they are
* processed in the order created:
*
* var d = promise.defer();
* flow.execute(function() {
* var chain = d.promise.then(() => console.log('A'));
*
* chain.then(() => console.log('B')).
* then(() => console.log('C'));
*
* chain.then(() => console.log('D')).
* then(() => console.log('E'));
*
* flow.execute(() => console.log('F'));
*
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // F
* // fin
*
* Even though a subtask's promised result will never resolve while the task
* function is on the stack, it will be treated as a promise resolved within the
* task. In all other scenarios, a task's promise behaves just like a normal
* promise. In the sample below, `C/D` is loggged before `B` because the
* resolution of `subtask1` interrupts the flow of the enclosing task. Within
* the final subtask, `E/F` is logged in order because `subtask1` is a resolved
* promise when that task runs.
*
* flow.execute(function() {
* var subtask1 = flow.execute(() => console.log('A'));
* var subtask2 = flow.execute(() => console.log('B'));
*
* subtask1.then(() => console.log('C'));
* subtask1.then(() => console.log('D'));
*
* flow.execute(function() {
* flow.execute(() => console.log('E'));
* subtask1.then(() => console.log('F'));
* });
* }).then(function() {
* console.log('fin');
* });
* // A
* // C
* // D
* // B
* // E
* // F
* // fin
*
* __Note__: while the ControlFlow will wait for
* {@linkplain webdriver.promise.ControlFlow#execute tasks} and
* {@linkplain webdriver.promise.Promise#then callbacks} to complete, it
* _will not_ wait for unresolved promises created within a task:
*
* flow.execute(function() {
* var p = new promise.Promise(function(fulfill) {
* setTimeout(fulfill, 100);
* });
*
* p.then(() => console.log('promise resolved!'));
*
* }).then(function() {
* console.log('task complete!');
* });
* // task complete!
* // promise resolved!
*
* Finally, consider the following:
*
* var d = promise.defer();
* d.promise.then(() => console.log('A'));
* d.promise.then(() => console.log('B'));
*
* flow.execute(function() {
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* flow.execute( () => console.log('E'));
* d.promise.then(() => console.log('F'));
*
* d.fulfill();
*
* flow.execute( () => console.log('G'));
* d.promise.then(() => console.log('H'));
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // F
* // G
* // H
* // fin
*
* In this example, callbacks are registered on `d.promise` both before and
* during the invocation of the task function. When `d.fulfill()` is called,
* the callbacks registered before the task (`A` & `B`) are registered as
* interrupts. The remaining callbacks were all attached within the task and
* are scheduled in the flow as standard tasks.
*
* ## Generator Support
*
* [Generators][GF] may be scheduled as tasks within a control flow or attached
* as callbacks to a promise. Each time the generator yields a promise, the
* control flow will wait for that promise to settle before executing the next
* iteration of the generator. The yielded promise's fulfilled value will be
* passed back into the generator:
*
* flow.execute(function* () {
* var d = promise.defer();
*
* setTimeout(() => console.log('...waiting...'), 25);
* setTimeout(() => d.fulfill(123), 50);
*
* console.log('start: ' + Date.now());
*
* var value = yield d.promise;
* console.log('mid: %d; value = %d', Date.now(), value);
*
* yield promise.delayed(10);
* console.log('end: ' + Date.now());
* }).then(function() {
* console.log('fin');
* });
* // start: 0
* // ...waiting...
* // mid: 50; value = 123
* // end: 60
* // fin
*
* Yielding the result of a promise chain will wait for the entire chain to
* complete:
*
* promise.fulfilled().then(function* () {
* console.log('start: ' + Date.now());
*
* var value = yield flow.
* execute(() => console.log('A')).
* then( () => console.log('B')).
* then( () => 123);
*
* console.log('mid: %s; value = %d', Date.now(), value);
*
* yield flow.execute(() => console.log('C'));
* }).then(function() {
* console.log('fin');
* });
* // start: 0
* // A
* // B
* // mid: 2; value = 123
* // C
* // fin
*
* Yielding a _rejected_ promise will cause the rejected value to be thrown
* within the generator function:
*
* flow.execute(function* () {
* console.log('start: ' + Date.now());
* try {
* yield promise.delayed(10).then(function() {
* throw Error('boom');
* });
* } catch (ex) {
* console.log('caught time: ' + Date.now());
* console.log(ex.message);
* }
* });
* // start: 0
* // caught time: 10
* // boom
*
* # Error Handling
*
* ES6 promises do not require users to handle a promise rejections. This can
* result in subtle bugs as the rejections are silently "swallowed" by the
* Promise class.
*
* Promise.reject(Error('boom'));
* // ... *crickets* ...
*
* Selenium's {@link webdriver.promise promise} module, on the other hand,
* requires that every rejection be explicitly handled. When a
* {@linkplain webdriver.promise.Promise Promise} is rejected and no callbacks
* are defined on that promise, it is considered an _unhandled rejection_ and
* reproted to the active task queue. If the rejection remains unhandled after
* a single turn of the [event loop][JSEL] (scheduled with a micro-task), it
* will propagate up the stack.
*
* ## Error Propagation
*
* If an unhandled rejection occurs within a task function, that task's promised
* result is rejected and all remaining subtasks are discarded:
*
* flow.execute(function() {
* // No callbacks registered on promise -> unhandled rejection
* promise.rejected(Error('boom'));
* flow.execute(function() { console.log('this will never run'); });
* }).thenCatch(function(e) {
* console.log(e.message);
* });
* // boom
*
* The promised results for discarded tasks are silently rejected with a
* cancellation error and existing callback chains will never fire.
*
* flow.execute(function() {
* promise.rejected(Error('boom'));
* flow.execute(function() { console.log('a'); }).
* then(function() { console.log('b'); });
* }).thenCatch(function(e) {
* console.log(e.message);
* });
* // boom
*
* An unhandled rejection takes precedence over a task function's returned
* result, even if that value is another promise:
*
* flow.execute(function() {
* promise.rejected(Error('boom'));
* return flow.execute(someOtherTask);
* }).thenCatch(function(e) {
* console.log(e.message);
* });
* // boom
*
* If there are multiple unhandled rejections within a task, they are packaged
* in a {@link webdriver.promise.MultipleUnhandledRejectionError
* MultipleUnhandledRejectionError}, which has an `errors` property that is a
* `Set` of the recorded unhandled rejections:
*
* flow.execute(function() {
* promise.rejected(Error('boom1'));
* promise.rejected(Error('boom2'));
* }).thenCatch(function(ex) {
* console.log(ex instanceof promise.MultipleUnhandledRejectionError);
* for (var e of ex.errors) {
* console.log(e.message);
* }
* });
* // boom1
* // boom2
*
* When a subtask is discarded due to an unreported rejection in its parent
* frame, the existing callbacks on that task will never settle and the
* callbacks will not be invoked. If a new callback is attached ot the subtask
* _after_ it has been discarded, it is handled the same as adding a callback
* to a cancelled promise: the error-callback path is invoked. This behavior is
* intended to handle cases where the user saves a reference to a task promise,
* as illustrated below.
*
* var subTask;
* flow.execute(function() {
* promise.rejected(Error('boom'));
* subTask = flow.execute(function() {});
* }).thenCatch(function(e) {
* console.log(e.message);
* }).then(function() {
* return subTask.then(
* () => console.log('subtask success!'),
* (e) => console.log('subtask failed:\n' + e));
* });
* // boom
* // subtask failed:
* // DiscardedTaskError: Task was discarded due to a previous failure: boom
*
* When a subtask fails, its promised result is treated the same as any other
* promise: it must be handled within one turn of the rejection or the unhandled
* rejection is propagated to the parent task. This means users can catch errors
* from complex flows from the top level task:
*
* flow.execute(function() {
* flow.execute(function() {
* flow.execute(function() {
* throw Error('fail!');
* });
* });
* }).thenCatch(function(e) {
* console.log(e.message);
* });
* // fail!
*
* ## Unhandled Rejection Events
*
* When an unhandled rejection propagates to the root of the control flow, the
* flow will emit an __uncaughtException__ event. If no listeners are registered
* on the flow, the error will be rethrown to the global error handler: an
* __uncaughtException__ event from the
* [`process`](https://nodejs.org/api/process.html) object in node, or
* `window.onerror` when running in a browser.
*
* Bottom line: you __*must*__ handle rejected promises.
*
* # Promise/A+ Compatibility
*
* This `promise` module is compliant with the [Promise/A+][] specification
* except for sections `2.2.6.1` and `2.2.6.2`:
*
* >
* > - `then` may be called multiple times on the same promise.
* > - If/when `promise` is fulfilled, all respective `onFulfilled` callbacks
* > must execute in the order of their originating calls to `then`.
* > - If/when `promise` is rejected, all respective `onRejected` callbacks
* > must execute in the order of their originating calls to `then`.
* >
*
* Specifically, the conformance tests contains the following scenario (for
* brevity, only the fulfillment version is shown):
*
* var p1 = Promise.resolve();
* p1.then(function() {
* console.log('A');
* p1.then(() => console.log('B'));
* });
* p1.then(() => console.log('C'));
* // A
* // C
* // B
*
* Since the [ControlFlow](#scheduling_callbacks) executes promise callbacks as
* tasks, with this module, the result would be
*
* var p2 = promise.fulfilled();
* p2.then(function() {
* console.log('A');
* p2.then(() => console.log('B');
* });
* p2.then(() => console.log('C'));
* // A
* // B
* // C
*
* [JSEL]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
* [GF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
* [Promise/A+]: https://promisesaplus.com/
*/
goog.module('webdriver.promise');
goog.module.declareLegacyNamespace();
var Arrays = goog.require('goog.array');
var asserts = goog.require('goog.asserts');
var asyncRun = goog.require('goog.async.run');
var throwException = goog.require('goog.async.throwException');
var DebugError = goog.require('goog.debug.Error');
var log = goog.require('goog.log');
var Objects = goog.require('goog.object');
var EventEmitter = goog.require('webdriver.EventEmitter');
var stacktrace = goog.require('webdriver.stacktrace');
/**
* @define {boolean} Whether to append traces of {@code then} to rejection
* errors.
*/
goog.define('webdriver.promise.LONG_STACK_TRACES', false);
/** @const */
var promise = exports;
/** @const */
var LOG = log.getLogger('webdriver.promise');
/**
* @param {number} level What level of verbosity to log with.
* @param {(string|function(this: T): string)} loggable The message to log.
* @param {T=} opt_self The object in whose context to run the loggable
* function.
* @template T
*/
function vlog(level, loggable, opt_self) {
var logLevel = log.Level.FINE;
if (level > 1) {
logLevel = log.Level.FINEST;
} else if (level > 0) {
logLevel = log.Level.FINER;
}
if (typeof loggable === 'function') {
loggable = loggable.bind(opt_self);
}
log.log(LOG, logLevel, loggable);
}
/**
* Generates an error to capture the current stack trace.
* @param {string} name Error name for this stack trace.
* @param {string} msg Message to record.
* @param {!Function} topFn The function that should appear at the top of the
* stack; only applicable in V8.
* @return {!Error} The generated error.
*/
promise.captureStackTrace = function(name, msg, topFn) {
var e = Error(msg);
e.name = name;
if (Error.captureStackTrace) {
Error.captureStackTrace(e, topFn);
} else {
var stack = stacktrace.getStack(e);
e.stack = e.toString();
if (stack) {
e.stack += '\n' + stack;
}
}
return e;
};
/**
* Error used when the computation of a promise is cancelled.
*
* @unrestricted
*/
promise.CancellationError = goog.defineClass(DebugError, {
/**
* @param {string=} opt_msg The cancellation message.
*/
constructor: function(opt_msg) {
promise.CancellationError.base(this, 'constructor', opt_msg);
/** @override */
this.name = 'CancellationError';
/** @private {boolean} */
this.silent_ = false;
},
statics: {
/**
* Wraps the given error in a CancellationError.
*
* @param {*} error The error to wrap.
* @param {string=} opt_msg The prefix message to use.
* @return {!promise.CancellationError} A cancellation error.
*/
wrap: function(error, opt_msg) {
var message;
if (error instanceof promise.CancellationError) {
return new promise.CancellationError(
opt_msg ? (opt_msg + ': ' + error.message) : error.message);
} else if (opt_msg) {
message = opt_msg;
if (error) {
message += ': ' + error;
}
return new promise.CancellationError(message);
}
if (error) {
message = error + '';
}
return new promise.CancellationError(message);
}
}
});
/**
* Error used to cancel tasks when a control flow is reset.
* @unrestricted
* @final
*/
var FlowResetError = goog.defineClass(promise.CancellationError, {
constructor: function() {
FlowResetError.base(this, 'constructor', 'ControlFlow was reset');
/** @override */
this.name = 'FlowResetError';
this.silent_ = true;
}
});
/**
* Error used to cancel tasks that have been discarded due to an uncaught error
* reported earlier in the control flow.
* @unrestricted
* @final
*/
var DiscardedTaskError = goog.defineClass(promise.CancellationError, {
/** @param {*} error The original error. */
constructor: function(error) {
if (error instanceof DiscardedTaskError) {
return /** @type {!DiscardedTaskError} */(error);
}
var msg = '';
if (error) {
msg = ': ' + (typeof error.message === 'string' ? error.message : error);
}
DiscardedTaskError.base(this, 'constructor',
'Task was discarded due to a previous failure' + msg);
/** @override */
this.name = 'DiscardedTaskError';
this.silent_ = true;
}
});
/**
* Error used when there are multiple unhandled promise rejections detected
* within a task or callback.
*
* @unrestricted
* @final
*/
promise.MultipleUnhandledRejectionError = goog.defineClass(DebugError, {
/**
* @param {!(Set<*>)} errors The errors to report.
*/
constructor: function(errors) {
promise.MultipleUnhandledRejectionError.base(
this, 'constructor', 'Multiple unhandled promise rejections reported');
/** @override */
this.name = 'MultipleUnhandledRejectionError';
/** @type {!Set<*>} */
this.errors = errors;
}
});
/**
* Property used to flag constructor's as implementing the Thenable interface
* for runtime type checking.
* @type {string}
* @const
*/
var IMPLEMENTED_BY_PROP = '$webdriver_Thenable';
/**
* Thenable is a promise-like object with a {@code then} method which may be
* used to schedule callbacks on a promised value.
*
* @interface
* @extends {IThenable<T>}
* @template T
*/
promise.Thenable = goog.defineClass(null, {
statics: {
/**
* Adds a property to a class prototype to allow runtime checks of whether
* instances of that class implement the Thenable interface. This function
* will also ensure the prototype's {@code then} function is exported from
* compiled code.
* @param {function(new: promise.Thenable, ...?)} ctor The
* constructor whose prototype to modify.
*/
addImplementation: function(ctor) {
// Based on goog.promise.Thenable.isImplementation.
ctor.prototype['then'] = ctor.prototype.then;
try {
// Old IE7 does not support defineProperty; IE8 only supports it for
// DOM elements.
Object.defineProperty(
ctor.prototype,
IMPLEMENTED_BY_PROP,
{'value': true, 'enumerable': false});
} catch (ex) {
ctor.prototype[IMPLEMENTED_BY_PROP] = true;
}
},
/**
* Checks if an object has been tagged for implementing the Thenable
* interface as defined by
* {@link webdriver.promise.Thenable.addImplementation}.
* @param {*} object The object to test.
* @return {boolean} Whether the object is an implementation of the Thenable
* interface.
*/
isImplementation: function(object) {
// Based on goog.promise.Thenable.isImplementation.
if (!object) {
return false;
}
try {
return !!object[IMPLEMENTED_BY_PROP];
} catch (e) {
return false; // Property access seems to be forbidden.
}
}
},
/**
* Cancels the computation of this promise's value, rejecting the promise in
* the process. This method is a no-op if the promise has already been
* resolved.
*
* @param {(string|promise.CancellationError)=} opt_reason The reason this
* promise is being cancelled.
*/
cancel: function(opt_reason) {},
/** @return {boolean} Whether this promise's value is still being computed. */
isPending: function() {},
/**
* Registers listeners for when this instance is resolved.
*
* @param {?(function(T): (R|IThenable<R>))=} opt_callback The
* function to call if this promise is successfully resolved. The function
* should expect a single argument: the promise's resolved value.
* @param {?(function(*): (R|IThenable<R>))=} opt_errback
* The function to call if this promise is rejected. The function should
* expect a single argument: the rejection reason.
* @return {!promise.Promise<R>} A new promise which will be
* resolved with the result of the invoked callback.
* @template R
*/
then: function(opt_callback, opt_errback) {},
/**
* Registers a listener for when this promise is rejected. This is synonymous
* with the {@code catch} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } catch (ex) {
* console.error(ex);
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().thenCatch(function(ex) {
* console.error(ex);
* });
*
* @param {function(*): (R|IThenable<R>)} errback The
* function to call if this promise is rejected. The function should
* expect a single argument: the rejection reason.
* @return {!promise.Promise<R>} A new promise which will be
* resolved with the result of the invoked callback.
* @template R
*/
thenCatch: function(errback) {},
/**
* Registers a listener to invoke when this promise is resolved, regardless
* of whether the promise's value was successfully computed. This function
* is synonymous with the {@code finally} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } finally {
* cleanUp();
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().thenFinally(cleanUp);
*
* __Note:__ similar to the {@code finally} clause, if the registered
* callback returns a rejected promise or throws an error, it will silently
* replace the rejection error (if any) from this promise:
*
* try {
* throw Error('one');
* } finally {
* throw Error('two'); // Hides Error: one
* }
*
* promise.rejected(Error('one'))
* .thenFinally(function() {
* throw Error('two'); // Hides Error: one
* });
*
* @param {function(): (R|IThenable<R>)} callback The function
* to call when this promise is resolved.
* @return {!promise.Promise<R>} A promise that will be fulfilled
* with the callback result.
* @template R
*/
thenFinally: function(callback) {}
});
/**
* @enum {string}
*/
var PromiseState = {
PENDING: 'pending',
BLOCKED: 'blocked',
REJECTED: 'rejected',
FULFILLED: 'fulfilled'
};
/**
* Internal symbol used to store a cancellation handler for
* {@link promise.Promise} objects. This is an internal implementation detail
* used by the {@link TaskQueue} class to monitor for when a promise is
* cancelled without generating an extra promise via then().
*/
var CANCEL_HANDLER_SYMBOL = Symbol('on cancel');
/**
* Represents the eventual value of a completed operation. Each promise may be
* in one of three states: pending, fulfilled, or rejected. Each promise starts
* in the pending state and may make a single transition to either a
* fulfilled or rejected state, at which point the promise is considered
* resolved.
*
* @implements {promise.Thenable<T>}
* @template T
* @see http://promises-aplus.github.io/promises-spec/
* @unrestricted // For using CANCEL_HANDLER_SYMBOL.
*/
promise.Promise = goog.defineClass(null, {
/**
* @param {function(
* function((T|IThenable<T>|Thenable)=),
* function(*=))} resolver
* Function that is invoked immediately to begin computation of this
* promise's value. The function should accept a pair of callback
* functions, one for fulfilling the promise and another for rejecting it.
* @param {promise.ControlFlow=} opt_flow The control flow
* this instance was created under. Defaults to the currently active flow.
*/
constructor: function(resolver, opt_flow) {
goog.getUid(this);
/** @private {!promise.ControlFlow} */
this.flow_ = opt_flow || promise.controlFlow();
/** @private {Error} */
this.stack_ = null;
if (promise.LONG_STACK_TRACES) {
this.stack_ = promise.captureStackTrace(
'Promise', 'new', promise.Promise);
}
/** @private {promise.Promise<?>} */
this.parent_ = null;
/** @private {Array<!Task>} */
this.callbacks_ = null;
/** @private {PromiseState} */
this.state_ = PromiseState.PENDING;
/** @private {boolean} */
this.handled_ = false;
/** @private {*} */
this.value_ = undefined;
/** @private {TaskQueue} */
this.queue_ = null;
/** @private {(function(promise.CancellationError)|null)} */
this[CANCEL_HANDLER_SYMBOL] = null;
try {
var self = this;
resolver(function(value) {
self.resolve_(PromiseState.FULFILLED, value);
}, function(reason) {
self.resolve_(PromiseState.REJECTED, reason);
});
} catch (ex) {
this.resolve_(PromiseState.REJECTED, ex);
}
},
/** @override */
toString: function() {
return 'Promise::' + goog.getUid(this) +
' {[[PromiseStatus]]: "' + this.state_ + '"}';
},
/**
* Resolves this promise. If the new value is itself a promise, this function
* will wait for it to be resolved before notifying the registered listeners.
* @param {PromiseState} newState The promise's new state.
* @param {*} newValue The promise's new value.
* @throws {TypeError} If {@code newValue === this}.
* @private
*/
resolve_: function(newState, newValue) {
if (PromiseState.PENDING !== this.state_) {
return;
}
if (newValue === this) {
// See promise a+, 2.3.1
// http://promises-aplus.github.io/promises-spec/#point-48
newValue = new TypeError('A promise may not resolve to itself');
newState = PromiseState.REJECTED;
}
this.parent_ = null;
this.state_ = PromiseState.BLOCKED;
if (newState !== PromiseState.REJECTED) {
if (promise.Thenable.isImplementation(newValue)) {
// 2.3.2
newValue = /** @type {!promise.Thenable} */(newValue);
newValue.then(
this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
return;
} else if (goog.isObject(newValue)) {
// 2.3.3
try {
// 2.3.3.1
var then = newValue['then'];
} catch (e) {
// 2.3.3.2
this.state_ = PromiseState.REJECTED;
this.value_ = e;
this.scheduleNotifications_();
return;
}
// NB: goog.isFunction is loose and will accept instanceof Function.
if (typeof then === 'function') {
// 2.3.3.3
this.invokeThen_(newValue, then);
return;
}
}
}
if (newState === PromiseState.REJECTED &&
isError(newValue) && newValue.stack && this.stack_) {
newValue.stack += '\nFrom: ' + (this.stack_.stack || this.stack_);
}
// 2.3.3.4 and 2.3.4
this.state_ = newState;
this.value_ = newValue;
this.scheduleNotifications_();
},
/**
* Invokes a thenable's "then" method according to 2.3.3.3 of the promise
* A+ spec.
* @param {!Object} x The thenable object.
* @param {!Function} then The "then" function to invoke.
* @private
*/
invokeThen_: function(x, then) {
var called = false;
var self = this;
var resolvePromise = function(value) {
if (!called) { // 2.3.3.3.3
called = true;
// 2.3.3.3.1
self.unblockAndResolve_(PromiseState.FULFILLED, value);
}
};
var rejectPromise = function(reason) {
if (!called) { // 2.3.3.3.3
called = true;
// 2.3.3.3.2
self.unblockAndResolve_(PromiseState.REJECTED, reason);
}
};
try {
// 2.3.3.3
then.call(x, resolvePromise, rejectPromise);
} catch (e) {
// 2.3.3.3.4.2
rejectPromise(e);
}
},
/**
* @param {PromiseState} newState The promise's new state.
* @param {*} newValue The promise's new value.
* @private
*/
unblockAndResolve_: function(newState, newValue) {
if (this.state_ === PromiseState.BLOCKED) {
this.state_ = PromiseState.PENDING;
this.resolve_(newState, newValue);
}
},
/**
* @private
*/
scheduleNotifications_: function() {
vlog(2, () => this + ' scheduling notifications', this);
this[CANCEL_HANDLER_SYMBOL] = null;
if (this.value_ instanceof promise.CancellationError
&& this.value_.silent_) {
this.callbacks_ = null;
}
if (!this.queue_) {
this.queue_ = this.flow_.getActiveQueue_();
}
if (!this.handled_ &&
this.state_ === PromiseState.REJECTED &&
!(this.value_ instanceof promise.CancellationError)) {
this.queue_.addUnhandledRejection(this);
}
this.queue_.scheduleCallbacks(this);
},
/** @override */
cancel: function(opt_reason) {
if (!canCancel(this)) {
return;
}
if (this.parent_ && canCancel(this.parent_)) {
this.parent_.cancel(opt_reason);
} else {
var reason = promise.CancellationError.wrap(opt_reason);
if (this[CANCEL_HANDLER_SYMBOL]) {
this[CANCEL_HANDLER_SYMBOL](reason);
this[CANCEL_HANDLER_SYMBOL] = null;
}
if (this.state_ === PromiseState.BLOCKED) {
this.unblockAndResolve_(PromiseState.REJECTED, reason);
} else {
this.resolve_(PromiseState.REJECTED, reason);
}
}
function canCancel(promise) {
return promise.state_ === PromiseState.PENDING
|| promise.state_ === PromiseState.BLOCKED;
}
},
/** @override */
isPending: function() {
return this.state_ === PromiseState.PENDING;
},
/** @override */
then: function(opt_callback, opt_errback) {
return this.addCallback_(
opt_callback, opt_errback, 'then', promise.Promise.prototype.then);
},
/** @override */
thenCatch: function(errback) {
return this.addCallback_(
null, errback, 'thenCatch', promise.Promise.prototype.thenCatch);
},
/** @override */
thenFinally: function(callback) {
var error;
var mustThrow = false;
return this.then(function() {
return callback();
}, function(err) {
error = err;
mustThrow = true;
return callback();
}).then(function() {
if (mustThrow) {
throw error;
}
});
},
/**
* Registers a new callback with this promise
* @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
* fulfillment callback.
* @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
* rejection callback.
* @param {string} name The callback name.
* @param {!Function} fn The function to use as the top of the stack when
* recording the callback's creation point.
* @return {!promise.Promise<R>} A new promise which will be resolved with the
* esult of the invoked callback.
* @template R
* @private
*/
addCallback_: function(callback, errback, name, fn) {
if (!goog.isFunction(callback) && !goog.isFunction(errback)) {
return this;
}
this.handled_ = true;
if (this.queue_) {
this.queue_.clearUnhandledRejection(this);
}
var cb = new Task(
this.flow_,
this.invokeCallback_.bind(this, callback, errback),
name,
promise.LONG_STACK_TRACES ? {name: 'Promise', top: fn} : undefined);
cb.promise.parent_ = this;
if (this.state_ !== PromiseState.PENDING &&
this.state_ !== PromiseState.BLOCKED) {
this.flow_.getActiveQueue_().enqueue(cb);
} else {
if (!this.callbacks_) {
this.callbacks_ = [];
}
this.callbacks_.push(cb);
cb.isVolatile = true;
this.flow_.getActiveQueue_().enqueue(cb);
}
return cb.promise;
},
/**
* Invokes a callback function attached to this promise.
* @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
* fulfillment callback.
* @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
* rejection callback.
* @template R
* @private
*/
invokeCallback_: function(callback, errback) {
var callbackFn = callback;
if (this.state_ === PromiseState.REJECTED) {
callbackFn = errback;
}
if (goog.isFunction(callbackFn)) {
if (promise.isGenerator(callbackFn)) {
return promise.consume(callbackFn, null, this.value_);
}
return callbackFn(this.value_);
} else if (this.state_ === PromiseState.REJECTED) {
throw this.value_;
} else {
return this.value_;
}
}
});
promise.Thenable.addImplementation(promise.Promise);
/**
* Represents a value that will be resolved at some point in the future. This
* class represents the protected "producer" half of a Promise - each Deferred
* has a {@code promise} property that may be returned to consumers for
* registering callbacks, reserving the ability to resolve the deferred to the
* producer.
*
* If this Deferred is rejected and there are no listeners registered before
* the next turn of the event loop, the rejection will be passed to the
* {@link webdriver.promise.ControlFlow} as an unhandled failure.
*
* @implements {promise.Thenable<T>}
* @template T
*/
promise.Deferred = goog.defineClass(null, {
/**
* @param {promise.ControlFlow=} opt_flow The control flow this instance was
* created under. This should only be provided during unit tests.
*/
constructor: function(opt_flow) {
var fulfill, reject;
/** @type {!promise.Promise<T>} */
this.promise = new promise.Promise(function(f, r) {
fulfill = f;
reject = r;
}, opt_flow);
var self = this;
var checkNotSelf = function(value) {
if (value === self) {
throw new TypeError('May not resolve a Deferred with itself');
}
};
/**
* Resolves this deferred with the given value. It is safe to call this as a
* normal function (with no bound "this").
* @param {(T|IThenable<T>|Thenable)=} opt_value The fulfilled value.
*/
this.fulfill = function(opt_value) {
checkNotSelf(opt_value);
fulfill(opt_value);
};
/**
* Rejects this promise with the given reason. It is safe to call this as a
* normal function (with no bound "this").
* @param {*=} opt_reason The rejection reason.
*/
this.reject = function(opt_reason) {
checkNotSelf(opt_reason);
reject(opt_reason);
};
},
/** @override */
isPending: function() {
return this.promise.isPending();
},
/** @override */
cancel: function(opt_reason) {
this.promise.cancel(opt_reason);
},
/**
* @override
* @deprecated Use {@code then} from the promise property directly.
*/
then: function(opt_cb, opt_eb) {
return this.promise.then(opt_cb, opt_eb);
},
/**
* @override
* @deprecated Use {@code thenCatch} from the promise property directly.
*/
thenCatch: function(opt_eb) {
return this.promise.thenCatch(opt_eb);
},
/**
* @override
* @deprecated Use {@code thenFinally} from the promise property directly.
*/
thenFinally: function(opt_cb) {
return this.promise.thenFinally(opt_cb);
}
});
promise.Thenable.addImplementation(promise.Deferred);
/**
* Tests if a value is an Error-like object. This is more than an straight
* instanceof check since the value may originate from another context.
* @param {*} value The value to test.
* @return {boolean} Whether the value is an error.
*/
function isError(value) {
return value instanceof Error ||
goog.isObject(value) &&
(goog.isString(value.message) ||
// A special test for goog.testing.JsUnitException.
value.isJsUnitException);
}
/**
* Determines whether a {@code value} should be treated as a promise.
* Any object whose "then" property is a function will be considered a promise.
*
* @param {*} value The value to test.
* @return {boolean} Whether the value is a promise.
*/
promise.isPromise = function(value) {
return !!value && goog.isObject(value) &&
// Use array notation so the Closure compiler does not obfuscate away our
// contract. Use typeof rather than goog.isFunction because
// goog.isFunction accepts instanceof Function, which the promise spec
// does not.
typeof value['then'] === 'function';
};
/**
* Creates a promise that will be resolved at a set time in the future.
* @param {number} ms The amount of time, in milliseconds, to wait before
* resolving the promise.
* @return {!promise.Promise} The promise.
*/
promise.delayed = function(ms) {
var key;
return new promise.Promise(function(fulfill) {
key = setTimeout(function() {
key = null;
fulfill();
}, ms);
}).thenCatch(function(e) {
clearTimeout(key);
key = null;
throw e;
});
};
/**
* Creates a new deferred object.
* @return {!promise.Deferred<T>} The new deferred object.
* @template T
*/
promise.defer = function() {
return new promise.Deferred();
};
/**
* Creates a promise that has been resolved with the given value.
* @param {T=} opt_value The resolved value.
* @return {!promise.Promise<T>} The resolved promise.
* @template T
*/
promise.fulfilled = function(opt_value) {
if (opt_value instanceof promise.Promise) {
return opt_value;
}
return new promise.Promise(function(fulfill) {
fulfill(opt_value);
});
};
/**
* Creates a promise that has been rejected with the given reason.
* @param {*=} opt_reason The rejection reason; may be any value, but is
* usually an Error or a string.
* @return {!promise.Promise<T>} The rejected promise.
* @template T
*/
promise.rejected = function(opt_reason) {
if (opt_reason instanceof promise.Promise) {
return opt_reason;
}
return new promise.Promise(function(_, reject) {
reject(opt_reason);
});
};
/**
* Wraps a function that expects a node-style callback as its final
* argument. This callback expects two arguments: an error value (which will be
* null if the call succeeded), and the success value as the second argument.
* The callback will the resolve or reject the returned promise, based on its arguments.
* @param {!Function} fn The function to wrap.
* @param {...?} var_args The arguments to apply to the function, excluding the
* final callback.
* @return {!promise.Promise} A promise that will be resolved with the
* result of the provided function's callback.
*/
promise.checkedNodeCall = function(fn, var_args) {
var args = Arrays.slice(arguments, 1);
return new promise.Promise(function(fulfill, reject) {
try {
args.push(function(error, value) {
error ? reject(error) : fulfill(value);
});
fn.apply(undefined, args);
} catch (ex) {
reject(ex);
}
});
};
/**
* Registers an observer on a promised {@code value}, returning a new promise
* that will be resolved when the value is. If {@code value} is not a promise,
* then the return promise will be immediately resolved.
* @param {*} value The value to observe.
* @param {Function=} opt_callback The function to call when the value is
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
* @return {!promise.Promise} A new promise.
*/
promise.when = function(value, opt_callback, opt_errback) {
if (promise.Thenable.isImplementation(value)) {
return value.then(opt_callback, opt_errback);
}
return new promise.Promise(function(fulfill, reject) {
promise.asap(value, fulfill, reject);
}).then(opt_callback, opt_errback);
};
/**
* Invokes the appropriate callback function as soon as a promised
* {@code value} is resolved. This function is similar to
* {@link webdriver.promise.when}, except it does not return a new promise.
* @param {*} value The value