aboutsummaryrefslogtreecommitdiff
path: root/node_modules/selenium-webdriver/lib
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/selenium-webdriver/lib')
-rw-r--r--node_modules/selenium-webdriver/lib/actions.js28
-rw-r--r--node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.sobin41262 -> 41623 bytes
-rw-r--r--node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.sobin30887 -> 36934 bytes
-rw-r--r--node_modules/selenium-webdriver/lib/firefox/webdriver.xpibin706947 -> 707042 bytes
-rw-r--r--node_modules/selenium-webdriver/lib/http.js201
-rw-r--r--node_modules/selenium-webdriver/lib/logging.js8
-rw-r--r--node_modules/selenium-webdriver/lib/promise.js927
-rw-r--r--node_modules/selenium-webdriver/lib/safari/client.js56
-rw-r--r--node_modules/selenium-webdriver/lib/session.js2
-rw-r--r--node_modules/selenium-webdriver/lib/test/build.js54
-rw-r--r--node_modules/selenium-webdriver/lib/test/data/formPage.html1
-rw-r--r--node_modules/selenium-webdriver/lib/test/fileserver.js22
-rw-r--r--node_modules/selenium-webdriver/lib/test/httpserver.js10
-rw-r--r--node_modules/selenium-webdriver/lib/test/index.js43
-rw-r--r--node_modules/selenium-webdriver/lib/webdriver.js1005
15 files changed, 1387 insertions, 970 deletions
diff --git a/node_modules/selenium-webdriver/lib/actions.js b/node_modules/selenium-webdriver/lib/actions.js
index 7200b08d6..1b059bbbf 100644
--- a/node_modules/selenium-webdriver/lib/actions.js
+++ b/node_modules/selenium-webdriver/lib/actions.js
@@ -65,9 +65,12 @@ function checkModifierKey(key) {
* Class for defining sequences of complex user interactions. Each sequence
* will not be executed until {@link #perform} is called.
*
- * Example:
+ * This class should not be instantiated directly. Instead, obtain an instance
+ * using {@link ./webdriver.WebDriver#actions() WebDriver.actions()}.
*
- * new ActionSequence(driver).
+ * Sample usage:
+ *
+ * driver.actions().
* keyDown(Key.SHIFT).
* click(element1).
* click(element2).
@@ -107,7 +110,7 @@ class ActionSequence {
/**
* Executes this action sequence.
*
- * @return {!./promise.Promise} A promise that will be resolved once
+ * @return {!./promise.Thenable} A promise that will be resolved once
* this sequence has completed.
*/
perform() {
@@ -117,9 +120,10 @@ class ActionSequence {
let actions = this.actions_.concat();
let driver = this.driver_;
return driver.controlFlow().execute(function() {
- actions.forEach(function(action) {
- driver.schedule(action.command, action.description);
+ let results = actions.map(action => {
+ return driver.schedule(action.command, action.description);
});
+ return Promise.all(results);
}, 'ActionSequence.perform');
}
@@ -377,9 +381,12 @@ class ActionSequence {
* Class for defining sequences of user touch interactions. Each sequence
* will not be executed until {@link #perform} is called.
*
- * Example:
+ * This class should not be instantiated directly. Instead, obtain an instance
+ * using {@link ./webdriver.WebDriver#touchActions() WebDriver.touchActions()}.
+ *
+ * Sample usage:
*
- * new TouchSequence(driver).
+ * driver.touchActions().
* tapAndHold({x: 0, y: 0}).
* move({x: 3, y: 4}).
* release({x: 10, y: 10}).
@@ -415,7 +422,7 @@ class TouchSequence {
/**
* Executes this action sequence.
- * @return {!./promise.Promise} A promise that will be resolved once
+ * @return {!./promise.Thenable} A promise that will be resolved once
* this sequence has completed.
*/
perform() {
@@ -425,9 +432,10 @@ class TouchSequence {
let actions = this.actions_.concat();
let driver = this.driver_;
return driver.controlFlow().execute(function() {
- actions.forEach(function(action) {
- driver.schedule(action.command, action.description);
+ let results = actions.map(action => {
+ return driver.schedule(action.command, action.description);
});
+ return Promise.all(results);
}, 'TouchSequence.perform');
}
diff --git a/node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.so b/node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.so
index 916e530f3..248c32db5 100644
--- a/node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.so
+++ b/node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.so
Binary files differ
diff --git a/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so b/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so
index 8e7db8de3..004062c7b 100644
--- a/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so
+++ b/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so
Binary files differ
diff --git a/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi b/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi
index 39aca6b62..f9a51cf4f 100644
--- a/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi
+++ b/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi
Binary files differ
diff --git a/node_modules/selenium-webdriver/lib/http.js b/node_modules/selenium-webdriver/lib/http.js
index a5675f81f..68bc43213 100644
--- a/node_modules/selenium-webdriver/lib/http.js
+++ b/node_modules/selenium-webdriver/lib/http.js
@@ -25,7 +25,11 @@
'use strict';
+const fs = require('fs');
+const path = require('path');
+
const cmd = require('./command');
+const devmode = require('./devmode');
const error = require('./error');
const logging = require('./logging');
const promise = require('./promise');
@@ -110,13 +114,77 @@ class Response {
}
+const DEV_ROOT = '../../../../buck-out/gen/javascript/';
+
+/** @enum {string} */
+const Atom = {
+ GET_ATTRIBUTE: devmode
+ ? path.join(__dirname, DEV_ROOT, 'webdriver/atoms/getAttribute.js')
+ : path.join(__dirname, 'atoms/getAttribute.js'),
+ IS_DISPLAYED: devmode
+ ? path.join(__dirname, DEV_ROOT, 'atoms/fragments/is-displayed.js')
+ : path.join(__dirname, 'atoms/isDisplayed.js'),
+};
+
+
+const ATOMS = /** !Map<string, !Promise<string>> */new Map();
+const LOG = logging.getLogger('webdriver.http');
+
+/**
+ * @param {Atom} file The atom file to load.
+ * @return {!Promise<string>} A promise that will resolve to the contents of the
+ * file.
+ */
+function loadAtom(file) {
+ if (ATOMS.has(file)) {
+ return ATOMS.get(file);
+ }
+ let contents = /** !Promise<string> */new Promise((resolve, reject) => {
+ LOG.finest(() => `Loading atom ${file}`);
+ fs.readFile(file, 'utf8', function(err, data) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ ATOMS.set(file, contents);
+ return contents;
+}
+
+
function post(path) { return resource('POST', path); }
function del(path) { return resource('DELETE', path); }
function get(path) { return resource('GET', path); }
function resource(method, path) { return {method: method, path: path}; }
-/** @const {!Map<string, {method: string, path: string}>} */
+/** @typedef {{method: string, path: string}} */
+var CommandSpec;
+
+
+/** @typedef {function(!cmd.Command): !Promise<!cmd.Command>} */
+var CommandTransformer;
+
+
+/**
+ * @param {!cmd.Command} command The initial command.
+ * @param {Atom} atom The name of the atom to execute.
+ * @return {!Promise<!cmd.Command>} The transformed command to execute.
+ */
+function toExecuteAtomCommand(command, atom, ...params) {
+ return loadAtom(atom).then(atom => {
+ return new cmd.Command(cmd.Name.EXECUTE_SCRIPT)
+ .setParameter('sessionId', command.getParameter('sessionId'))
+ .setParameter('script', `return (${atom}).apply(null, arguments)`)
+ .setParameter('args', params.map(param => command.getParameter(param)));
+ });
+}
+
+
+
+/** @const {!Map<string, CommandSpec>} */
const COMMAND_MAP = new Map([
[cmd.Name.GET_SERVER_STATUS, get('/status')],
[cmd.Name.NEW_SESSION, post('/session')],
@@ -195,8 +263,15 @@ const COMMAND_MAP = new Map([
]);
-/** @const {!Map<string, {method: string, path: string}>} */
+/** @const {!Map<string, (CommandSpec|CommandTransformer)>} */
const W3C_COMMAND_MAP = new Map([
+ [cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')],
+ [cmd.Name.GET_ELEMENT_ATTRIBUTE, (cmd) => {
+ return toExecuteAtomCommand(cmd, Atom.GET_ATTRIBUTE, 'id', 'name');
+ }],
+ [cmd.Name.IS_ELEMENT_DISPLAYED, (cmd) => {
+ return toExecuteAtomCommand(cmd, Atom.IS_DISPLAYED, 'id');
+ }],
[cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
[cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/position')],
[cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/position')],
@@ -249,6 +324,53 @@ function doSend(executor, request) {
/**
+ * @param {Map<string, CommandSpec>} customCommands
+ * A map of custom command definitions.
+ * @param {boolean} w3c Whether to use W3C command mappings.
+ * @param {!cmd.Command} command The command to resolve.
+ * @return {!Promise<!Request>} A promise that will resolve with the
+ * command to execute.
+ */
+function buildRequest(customCommands, w3c, command) {
+ LOG.finest(() => `Translating command: ${command.getName()}`);
+ let spec = customCommands && customCommands.get(command.getName());
+ if (spec) {
+ return toHttpRequest(spec);
+ }
+
+ if (w3c) {
+ spec = W3C_COMMAND_MAP.get(command.getName());
+ if (typeof spec === 'function') {
+ LOG.finest(() => `Transforming command for W3C: ${command.getName()}`);
+ return spec(command)
+ .then(newCommand => buildRequest(customCommands, w3c, newCommand));
+ } else if (spec) {
+ return toHttpRequest(spec);
+ }
+ }
+
+ spec = COMMAND_MAP.get(command.getName());
+ if (spec) {
+ return toHttpRequest(spec);
+ }
+ return Promise.reject(
+ new error.UnknownCommandError(
+ 'Unrecognized command: ' + command.getName()));
+
+ /**
+ * @param {CommandSpec} resource
+ * @return {!Promise<!Request>}
+ */
+ function toHttpRequest(resource) {
+ LOG.finest(() => `Building HTTP request: ${JSON.stringify(resource)}`);
+ let parameters = command.getParameters();
+ let path = buildPath(resource.path, parameters);
+ return Promise.resolve(new Request(resource.method, path, parameters));
+ }
+}
+
+
+/**
* A command executor that communicates with the server using JSON over HTTP.
*
* By default, each instance of this class will use the legacy wire protocol
@@ -279,7 +401,7 @@ class Executor {
*/
this.w3c = false;
- /** @private {Map<string, {method: string, path: string}>} */
+ /** @private {Map<string, CommandSpec>} */
this.customCommands_ = null;
/** @private {!logging.Logger} */
@@ -308,51 +430,40 @@ class Executor {
/** @override */
execute(command) {
- let resource =
- (this.customCommands_ && this.customCommands_.get(command.getName()))
- || (this.w3c && W3C_COMMAND_MAP.get(command.getName()))
- || COMMAND_MAP.get(command.getName());
- if (!resource) {
- throw new error.UnknownCommandError(
- 'Unrecognized command: ' + command.getName());
- }
-
- let parameters = command.getParameters();
- let path = buildPath(resource.path, parameters);
- let request = new Request(resource.method, path, parameters);
-
- let log = this.log_;
- log.finer(() => '>>>\n' + request);
- return doSend(this, request).then(response => {
- log.finer(() => '<<<\n' + response);
-
- let parsed =
- parseHttpResponse(/** @type {!Response} */ (response), this.w3c);
-
- if (command.getName() === cmd.Name.NEW_SESSION
- || command.getName() === cmd.Name.DESCRIBE_SESSION) {
- if (!parsed || !parsed['sessionId']) {
- throw new error.WebDriverError(
- 'Unable to parse new session response: ' + response.body);
+ let request = buildRequest(this.customCommands_, this.w3c, command);
+ return request.then(request => {
+ this.log_.finer(() => `>>> ${request.method} ${request.path}`);
+ return doSend(this, request).then(response => {
+ this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`);
+
+ let parsed =
+ parseHttpResponse(/** @type {!Response} */ (response), this.w3c);
+
+ if (command.getName() === cmd.Name.NEW_SESSION
+ || command.getName() === cmd.Name.DESCRIBE_SESSION) {
+ if (!parsed || !parsed['sessionId']) {
+ throw new error.WebDriverError(
+ 'Unable to parse new session response: ' + response.body);
+ }
+
+ // The remote end is a W3C compliant server if there is no `status`
+ // field in the response. This is not appliable for the DESCRIBE_SESSION
+ // command, which is not defined in the W3C spec.
+ if (command.getName() === cmd.Name.NEW_SESSION) {
+ this.w3c = this.w3c || !('status' in parsed);
+ }
+
+ return new Session(parsed['sessionId'], parsed['value']);
}
- // The remote end is a W3C compliant server if there is no `status`
- // field in the response. This is not appliable for the DESCRIBE_SESSION
- // command, which is not defined in the W3C spec.
- if (command.getName() === cmd.Name.NEW_SESSION) {
- this.w3c = this.w3c || !('status' in parsed);
+ if (parsed
+ && typeof parsed === 'object'
+ && 'value' in parsed) {
+ let value = parsed['value'];
+ return typeof value === 'undefined' ? null : value;
}
-
- return new Session(parsed['sessionId'], parsed['value']);
- }
-
- if (parsed
- && typeof parsed === 'object'
- && 'value' in parsed) {
- let value = parsed['value'];
- return typeof value === 'undefined' ? null : value;
- }
- return parsed;
+ return parsed;
+ });
});
}
}
diff --git a/node_modules/selenium-webdriver/lib/logging.js b/node_modules/selenium-webdriver/lib/logging.js
index 97f54924c..634a5cbc8 100644
--- a/node_modules/selenium-webdriver/lib/logging.js
+++ b/node_modules/selenium-webdriver/lib/logging.js
@@ -521,8 +521,14 @@ function getLogger(name) {
}
+/**
+ * Pads a number to ensure it has a minimum of two digits.
+ *
+ * @param {number} n the number to be padded.
+ * @return {string} the padded number.
+ */
function pad(n) {
- if (n > 10) {
+ if (n >= 10) {
return '' + n;
} else {
return '0' + n;
diff --git a/node_modules/selenium-webdriver/lib/promise.js b/node_modules/selenium-webdriver/lib/promise.js
index b98e7cf2e..32d0c98e6 100644
--- a/node_modules/selenium-webdriver/lib/promise.js
+++ b/node_modules/selenium-webdriver/lib/promise.js
@@ -17,6 +17,42 @@
/**
* @fileoverview
+ *
+ * > ### IMPORTANT NOTICE
+ * >
+ * > The promise manager contained in this module is in the process of being
+ * > phased out in favor of native JavaScript promises. This will be a long
+ * > process and will not be completed until there have been two major LTS Node
+ * > releases (approx. Node v10.0) that support
+ * > [async functions](https://tc39.github.io/ecmascript-asyncawait/).
+ * >
+ * > At this time, the promise manager can be disabled by setting an environment
+ * > variable, `SELENIUM_PROMISE_MANAGER=0`. In the absence of async functions,
+ * > users may use generators with the
+ * > {@link ./promise.consume promise.consume()} function to write "synchronous"
+ * > style tests:
+ * >
+ * > ```js
+ * > const {Builder, By, promise, until} = require('selenium-webdriver');
+ * >
+ * > let result = promise.consume(function* doGoogleSearch() {
+ * > let driver = new Builder().forBrowser('firefox').build();
+ * > yield driver.get('http://www.google.com/ncr');
+ * > yield driver.findElement(By.name('q')).sendKeys('webdriver');
+ * > yield driver.findElement(By.name('btnG')).click();
+ * > yield driver.wait(until.titleIs('webdriver - Google Search'), 1000);
+ * > yield driver.quit();
+ * > });
+ * >
+ * > result.then(_ => console.log('SUCCESS!'),
+ * > e => console.error('FAILURE: ' + e));
+ * > ```
+ * >
+ * > The motiviation behind this change and full deprecation plan are documented
+ * > in [issue 2969](https://github.com/SeleniumHQ/selenium/issues/2969).
+ * >
+ * >
+ *
* The promise module is centered around the {@linkplain 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
@@ -592,6 +628,7 @@
'use strict';
+const error = require('./error');
const events = require('./events');
const logging = require('./logging');
@@ -643,7 +680,6 @@ function asyncRun(fn) {
});
}
-
/**
* @param {number} level What level of verbosity to log with.
* @param {(string|function(this: T): string)} loggable The message to log.
@@ -799,32 +835,56 @@ class MultipleUnhandledRejectionError extends Error {
* @const
*/
const IMPLEMENTED_BY_SYMBOL = Symbol('promise.Thenable');
+const CANCELLABLE_SYMBOL = Symbol('promise.CancellableThenable');
+
+
+/**
+ * @param {function(new: ?)} ctor
+ * @param {!Object} symbol
+ */
+function addMarkerSymbol(ctor, symbol) {
+ try {
+ ctor.prototype[symbol] = true;
+ } catch (ignored) {
+ // Property access denied?
+ }
+}
+
+
+/**
+ * @param {*} object
+ * @param {!Object} symbol
+ * @return {boolean}
+ */
+function hasMarkerSymbol(object, symbol) {
+ if (!object) {
+ return false;
+ }
+ try {
+ return !!object[symbol];
+ } catch (e) {
+ return false; // Property access seems to be forbidden.
+ }
+}
/**
* Thenable is a promise-like object with a {@code then} method which may be
* used to schedule callbacks on a promised value.
*
- * @interface
+ * @record
* @extends {IThenable<T>}
* @template T
*/
class Thenable {
/**
* Adds a property to a class prototype to allow runtime checks of whether
- * instances of that class implement the Thenable interface. This function
- * will also ensure the prototype's {@code then} function is exported from
- * compiled code.
+ * instances of that class implement the Thenable interface.
* @param {function(new: Thenable, ...?)} ctor The
* constructor whose prototype to modify.
*/
static addImplementation(ctor) {
- ctor.prototype['then'] = ctor.prototype.then;
- try {
- ctor.prototype[IMPLEMENTED_BY_SYMBOL] = true;
- } catch (ignored) {
- // Property access denied?
- }
+ addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL);
}
/**
@@ -835,30 +895,10 @@ class Thenable {
* interface.
*/
static isImplementation(object) {
- if (!object) {
- return false;
- }
- try {
- return !!object[IMPLEMENTED_BY_SYMBOL];
- } catch (e) {
- return false; // Property access seems to be forbidden.
- }
+ return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL);
}
/**
- * 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|Error)=} opt_reason The reason this promise is being
- * cancelled. This value will be wrapped in a {@link CancellationError}.
- */
- cancel(opt_reason) {}
-
- /** @return {boolean} Whether this promise's value is still being computed. */
- isPending() {}
-
- /**
* Registers listeners for when this instance is resolved.
*
* @param {?(function(T): (R|IThenable<R>))=} opt_callback The
@@ -867,8 +907,8 @@ class Thenable {
* @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 {!ManagedPromise<R>} A new promise which will be
- * resolved with the result of the invoked callback.
+ * @return {!Thenable<R>} A new promise which will be resolved with the result
+ * of the invoked callback.
* @template R
*/
then(opt_callback, opt_errback) {}
@@ -892,49 +932,53 @@ class Thenable {
* @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 {!ManagedPromise<R>} A new promise which will be
- * resolved with the result of the invoked callback.
+ * @return {!Thenable<R>} A new promise which will be resolved with the result
+ * of the invoked callback.
* @template R
*/
catch(errback) {}
+}
+
+/**
+ * Marker interface for objects that allow consumers to request the cancellation
+ * of a promies-based operation. A cancelled promise will be rejected with a
+ * {@link CancellationError}.
+ *
+ * This interface is considered package-private and should not be used outside
+ * of selenium-webdriver.
+ *
+ * @interface
+ * @extends {Thenable<T>}
+ * @template T
+ * @package
+ */
+class CancellableThenable {
/**
- * 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().finally(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'))
- * .finally(function() {
- * throw Error('two'); // Hides Error: one
- * });
+ * @param {function(new: CancellableThenable, ...?)} ctor
+ */
+ static addImplementation(ctor) {
+ Thenable.addImplementation(ctor);
+ addMarkerSymbol(ctor, CANCELLABLE_SYMBOL);
+ }
+
+ /**
+ * @param {*} object
+ * @return {boolean}
+ */
+ static isImplementation(object) {
+ return hasMarkerSymbol(object, CANCELLABLE_SYMBOL);
+ }
+
+ /**
+ * Requests the cancellation of 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 {function(): (R|IThenable<R>)} callback The function to call when
- * this promise is resolved.
- * @return {!ManagedPromise<R>} A promise that will be fulfilled
- * with the callback result.
- * @template R
+ * @param {(string|Error)=} opt_reason The reason this promise is being
+ * cancelled. This value will be wrapped in a {@link CancellationError}.
*/
- finally(callback) {}
+ cancel(opt_reason) {}
}
@@ -967,7 +1011,7 @@ const ON_CANCEL_HANDLER = new WeakMap;
* fulfilled or rejected state, at which point the promise is considered
* resolved.
*
- * @implements {Thenable<T>}
+ * @implements {CancellableThenable<T>}
* @template T
* @see http://promises-aplus.github.io/promises-spec/
*/
@@ -983,6 +1027,12 @@ class ManagedPromise {
* this instance was created under. Defaults to the currently active flow.
*/
constructor(resolver, opt_flow) {
+ if (!usePromiseManager()) {
+ throw TypeError(
+ 'Unable to create a managed promise instance: the promise manager has'
+ + ' been disabled by the SELENIUM_PROMISE_MANAGER environment'
+ + ' variable: ' + process.env['SELENIUM_PROMISE_MANAGER']);
+ }
getUid(this);
/** @private {!ControlFlow} */
@@ -1024,6 +1074,30 @@ class ManagedPromise {
}
}
+ /**
+ * Creates a promise that is immediately resolved with the given value.
+ *
+ * @param {T=} opt_value The value to resolve.
+ * @return {!ManagedPromise<T>} A promise resolved with the given value.
+ * @template T
+ */
+ static resolve(opt_value) {
+ if (opt_value instanceof ManagedPromise) {
+ return opt_value;
+ }
+ return new ManagedPromise(resolve => resolve(opt_value));
+ }
+
+ /**
+ * Creates a promise that is immediately rejected with the given reason.
+ *
+ * @param {*=} opt_reason The rejection reason.
+ * @return {!ManagedPromise<?>} A new rejected promise.
+ */
+ static reject(opt_reason) {
+ return new ManagedPromise((_, reject) => reject(opt_reason));
+ }
+
/** @override */
toString() {
return 'ManagedPromise::' + getUid(this) +
@@ -1176,7 +1250,7 @@ class ManagedPromise {
}
if (this.parent_ && canCancel(this.parent_)) {
- this.parent_.cancel(opt_reason);
+ /** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason);
} else {
var reason = CancellationError.wrap(opt_reason);
let onCancel = ON_CANCEL_HANDLER.get(this);
@@ -1194,7 +1268,7 @@ class ManagedPromise {
function canCancel(promise) {
if (!(promise instanceof ManagedPromise)) {
- return Thenable.isImplementation(promise);
+ return CancellableThenable.isImplementation(promise);
}
return promise.state_ === PromiseState.PENDING
|| promise.state_ === PromiseState.BLOCKED;
@@ -1202,11 +1276,6 @@ class ManagedPromise {
}
/** @override */
- isPending() {
- return this.state_ === PromiseState.PENDING;
- }
-
- /** @override */
then(opt_callback, opt_errback) {
return this.addCallback_(
opt_callback, opt_errback, 'then', ManagedPromise.prototype.then);
@@ -1218,21 +1287,15 @@ class ManagedPromise {
null, errback, 'catch', ManagedPromise.prototype.catch);
}
- /** @override */
+ /**
+ * @param {function(): (R|IThenable<R>)} callback
+ * @return {!ManagedPromise<R>}
+ * @template R
+ * @see ./promise.finally()
+ */
finally(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;
- }
- });
+ let result = thenFinally(this, callback);
+ return /** @type {!ManagedPromise} */(result);
}
/**
@@ -1308,7 +1371,16 @@ class ManagedPromise {
}
}
}
-Thenable.addImplementation(ManagedPromise);
+CancellableThenable.addImplementation(ManagedPromise);
+
+
+/**
+ * @param {!ManagedPromise} promise
+ * @return {boolean}
+ */
+function isPending(promise) {
+ return promise.state_ === PromiseState.PENDING;
+}
/**
@@ -1405,19 +1477,11 @@ function isPromise(value) {
* 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 {!ManagedPromise} The promise.
+ * @return {!Thenable} The promise.
*/
function delayed(ms) {
- var key;
- return new ManagedPromise(function(fulfill) {
- key = setTimeout(function() {
- key = null;
- fulfill();
- }, ms);
- }).catch(function(e) {
- clearTimeout(key);
- key = null;
- throw e;
+ return createPromise(resolve => {
+ setTimeout(() => resolve(), ms);
});
}
@@ -1436,15 +1500,11 @@ function defer() {
* Creates a promise that has been resolved with the given value.
* @param {T=} opt_value The resolved value.
* @return {!ManagedPromise<T>} The resolved promise.
+ * @deprecated Use {@link ManagedPromise#resolve Promise.resolve(value)}.
* @template T
*/
function fulfilled(opt_value) {
- if (opt_value instanceof ManagedPromise) {
- return opt_value;
- }
- return new ManagedPromise(function(fulfill) {
- fulfill(opt_value);
- });
+ return ManagedPromise.resolve(opt_value);
}
@@ -1452,16 +1512,11 @@ function fulfilled(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 {!ManagedPromise<T>} The rejected promise.
- * @template T
+ * @return {!ManagedPromise<?>} The rejected promise.
+ * @deprecated Use {@link ManagedPromise#reject Promise.reject(reason)}.
*/
function rejected(opt_reason) {
- if (opt_reason instanceof ManagedPromise) {
- return opt_reason;
- }
- return new ManagedPromise(function(_, reject) {
- reject(opt_reason);
- });
+ return ManagedPromise.reject(opt_reason);
}
@@ -1474,12 +1529,12 @@ function rejected(opt_reason) {
* @param {!Function} fn The function to wrap.
* @param {...?} var_args The arguments to apply to the function, excluding the
* final callback.
- * @return {!ManagedPromise} A promise that will be resolved with the
+ * @return {!Thenable} A promise that will be resolved with the
* result of the provided function's callback.
*/
function checkedNodeCall(fn, var_args) {
let args = Array.prototype.slice.call(arguments, 1);
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
try {
args.push(function(error, value) {
error ? reject(error) : fulfill(value);
@@ -1491,6 +1546,59 @@ function checkedNodeCall(fn, var_args) {
});
}
+/**
+ * Registers a listener to invoke when a 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().finally(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
+ * }
+ *
+ * let p = Promise.reject(Error('one'));
+ * promise.finally(p, function() {
+ * throw Error('two'); // Hides Error: one
+ * });
+ *
+ * @param {!IThenable<?>} promise The promise to add the listener to.
+ * @param {function(): (R|IThenable<R>)} callback The function to call when
+ * the promise is resolved.
+ * @return {!IThenable<R>} A promise that will be resolved with the callback
+ * result.
+ * @template R
+ */
+function thenFinally(promise, callback) {
+ let error;
+ let mustThrow = false;
+ return promise.then(function() {
+ return callback();
+ }, function(err) {
+ error = err;
+ mustThrow = true;
+ return callback();
+ }).then(function() {
+ if (mustThrow) {
+ throw error;
+ }
+ });
+}
+
/**
* Registers an observer on a promised {@code value}, returning a new promise
@@ -1501,16 +1609,15 @@ function checkedNodeCall(fn, var_args) {
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
- * @return {!ManagedPromise} A new promise.
+ * @return {!Thenable} A new promise.
*/
function when(value, opt_callback, opt_errback) {
if (Thenable.isImplementation(value)) {
return value.then(opt_callback, opt_errback);
}
- return new ManagedPromise(function(fulfill) {
- fulfill(value);
- }).then(opt_callback, opt_errback);
+ return createPromise(resolve => resolve(value))
+ .then(opt_callback, opt_errback);
}
@@ -1542,14 +1649,14 @@ function asap(value, callback, opt_errback) {
*
* @param {!Array<(T|!ManagedPromise<T>)>} arr An array of
* promises to wait on.
- * @return {!ManagedPromise<!Array<T>>} A promise that is
+ * @return {!Thenable<!Array<T>>} A promise that is
* fulfilled with an array containing the fulfilled values of the
* input array, or rejected with the same reason as the first
* rejected value.
* @template T
*/
function all(arr) {
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = [];
@@ -1603,12 +1710,12 @@ function all(arr) {
* @template TYPE, SELF
*/
function map(arr, fn, opt_self) {
- return fulfilled(arr).then(function(v) {
+ return createPromise(resolve => resolve(arr)).then(v => {
if (!Array.isArray(v)) {
throw TypeError('not an array');
}
var arr = /** @type {!Array} */(v);
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = new Array(n);
(function processNext(i) {
@@ -1661,12 +1768,12 @@ function map(arr, fn, opt_self) {
* @template TYPE, SELF
*/
function filter(arr, fn, opt_self) {
- return fulfilled(arr).then(function(v) {
+ return createPromise(resolve => resolve(arr)).then(v => {
if (!Array.isArray(v)) {
throw TypeError('not an array');
}
var arr = /** @type {!Array} */(v);
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = [];
var valuesLength = 0;
@@ -1714,7 +1821,7 @@ function filter(arr, fn, opt_self) {
* promise.fullyResolved(value); // Stack overflow.
*
* @param {*} value The value to fully resolve.
- * @return {!ManagedPromise} A promise for a fully resolved version
+ * @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
function fullyResolved(value) {
@@ -1728,7 +1835,7 @@ function fullyResolved(value) {
/**
* @param {*} value The value to fully resolve. If a promise, assumed to
* already be resolved.
- * @return {!ManagedPromise} A promise for a fully resolved version
+ * @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
function fullyResolveValue(value) {
@@ -1755,13 +1862,13 @@ function fullyResolveValue(value) {
return fullyResolveKeys(/** @type {!Object} */ (value));
}
- return fulfilled(value);
+ return createPromise(resolve => resolve(value));
}
/**
* @param {!(Array|Object)} obj the object to resolve.
- * @return {!ManagedPromise} A promise that will be resolved with the
+ * @return {!Thenable} A promise that will be resolved with the
* input object once all of its values have been fully resolved.
*/
function fullyResolveKeys(obj) {
@@ -1773,8 +1880,9 @@ function fullyResolveKeys(obj) {
}
return n;
})();
+
if (!numKeys) {
- return fulfilled(obj);
+ return createPromise(resolve => resolve(obj));
}
function forEachProperty(obj, fn) {
@@ -1788,7 +1896,7 @@ function fullyResolveKeys(obj) {
}
var numResolved = 0;
- return new ManagedPromise(function(fulfill, reject) {
+ return createPromise(function(fulfill, reject) {
var forEachKey = isArray ? forEachElement: forEachProperty;
forEachKey(obj, function(partialValue, key) {
@@ -1822,6 +1930,236 @@ function fullyResolveKeys(obj) {
//////////////////////////////////////////////////////////////////////////////
+/**
+ * Defines methods for coordinating the execution of asynchronous tasks.
+ * @record
+ */
+class Scheduler {
+ /**
+ * Schedules a task for execution. If the task function is a generator, the
+ * task will be executed using {@link ./promise.consume consume()}.
+ *
+ * @param {function(): (T|IThenable<T>)} fn The function to call to start the
+ * task.
+ * @param {string=} opt_description A description of the task for debugging
+ * purposes.
+ * @return {!Thenable<T>} A promise that will be resolved with the task
+ * result.
+ * @template T
+ */
+ execute(fn, opt_description) {}
+
+ /**
+ * Creates a new promise using the given resolver function.
+ *
+ * @param {function(
+ * function((T|IThenable<T>|Thenable|null)=),
+ * function(*=))} resolver
+ * @return {!Thenable<T>}
+ * @template T
+ */
+ promise(resolver) {}
+
+ /**
+ * Schedules a `setTimeout` call.
+ *
+ * @param {number} ms The timeout delay, in milliseconds.
+ * @param {string=} opt_description A description to accompany the timeout.
+ * @return {!Thenable<void>} A promise that will be resolved when the timeout
+ * fires.
+ */
+ timeout(ms, opt_description) {}
+
+ /**
+ * Schedules a task to wait for a condition to hold.
+ *
+ * If the condition is defined as a function, it may return any value. Promies
+ * will be resolved before testing if the condition holds (resolution time
+ * counts towards the timeout). Once resolved, values are always evaluated as
+ * booleans.
+ *
+ * If the condition function throws, or returns a rejected promise, the
+ * wait task will fail.
+ *
+ * If the condition is defined as a promise, the scheduler will wait for it to
+ * settle. If the timeout expires before the promise settles, the promise
+ * returned by this function will be rejected.
+ *
+ * If this function is invoked with `timeout === 0`, or the timeout is
+ * omitted, this scheduler will wait indefinitely for the condition to be
+ * satisfied.
+ *
+ * @param {(!IThenable<T>|function())} condition The condition to poll,
+ * or a promise to wait on.
+ * @param {number=} opt_timeout How long to wait, in milliseconds, for the
+ * condition to hold before timing out. If omitted, the flow will wait
+ * indefinitely.
+ * @param {string=} opt_message An optional error message to include if the
+ * wait times out; defaults to the empty string.
+ * @return {!Thenable<T>} A promise that will be fulfilled
+ * when the condition has been satisified. The promise shall be rejected
+ * if the wait times out waiting for the condition.
+ * @throws {TypeError} If condition is not a function or promise or if timeout
+ * is not a number >= 0.
+ * @template T
+ */
+ wait(condition, opt_timeout, opt_message) {}
+}
+
+
+let USE_PROMISE_MANAGER;
+function usePromiseManager() {
+ if (typeof USE_PROMISE_MANAGER !== 'undefined') {
+ return !!USE_PROMISE_MANAGER;
+ }
+ return process.env['SELENIUM_PROMISE_MANAGER'] === undefined
+ || !/^0|false$/i.test(process.env['SELENIUM_PROMISE_MANAGER']);
+}
+
+
+/**
+ * @param {function(
+ * function((T|IThenable<T>|Thenable|null)=),
+ * function(*=))} resolver
+ * @return {!Thenable<T>}
+ * @template T
+ */
+function createPromise(resolver) {
+ let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
+ return new ctor(resolver);
+}
+
+
+/**
+ * @param {!Scheduler} scheduler The scheduler to use.
+ * @param {(!IThenable<T>|function())} condition The condition to poll,
+ * or a promise to wait on.
+ * @param {number=} opt_timeout How long to wait, in milliseconds, for the
+ * condition to hold before timing out. If omitted, the flow will wait
+ * indefinitely.
+ * @param {string=} opt_message An optional error message to include if the
+ * wait times out; defaults to the empty string.
+ * @return {!Thenable<T>} A promise that will be fulfilled
+ * when the condition has been satisified. The promise shall be rejected
+ * if the wait times out waiting for the condition.
+ * @throws {TypeError} If condition is not a function or promise or if timeout
+ * is not a number >= 0.
+ * @template T
+ */
+function scheduleWait(scheduler, condition, opt_timeout, opt_message) {
+ let timeout = opt_timeout || 0;
+ if (typeof timeout !== 'number' || timeout < 0) {
+ throw TypeError('timeout must be a number >= 0: ' + timeout);
+ }
+
+ if (isPromise(condition)) {
+ return scheduler.execute(function() {
+ if (!timeout) {
+ return condition;
+ }
+ return scheduler.promise(function(fulfill, reject) {
+ let start = Date.now();
+ let timer = setTimeout(function() {
+ timer = null;
+ reject(
+ new error.TimeoutError(
+ (opt_message ? opt_message + '\n' : '')
+ + 'Timed out waiting for promise to resolve after '
+ + (Date.now() - start) + 'ms'));
+ }, timeout);
+
+ /** @type {Thenable} */(condition).then(
+ function(value) {
+ timer && clearTimeout(timer);
+ fulfill(value);
+ },
+ function(error) {
+ timer && clearTimeout(timer);
+ reject(error);
+ });
+ });
+ }, opt_message || '<anonymous wait: promise resolution>');
+ }
+
+ if (typeof condition !== 'function') {
+ throw TypeError('Invalid condition; must be a function or promise: ' +
+ typeof condition);
+ }
+
+ if (isGenerator(condition)) {
+ let original = condition;
+ condition = () => consume(original);
+ }
+
+ return scheduler.execute(function() {
+ var startTime = Date.now();
+ return scheduler.promise(function(fulfill, reject) {
+ pollCondition();
+
+ function pollCondition() {
+ var conditionFn = /** @type {function()} */(condition);
+ scheduler.execute(conditionFn).then(function(value) {
+ var elapsed = Date.now() - startTime;
+ if (!!value) {
+ fulfill(value);
+ } else if (timeout && elapsed >= timeout) {
+ reject(
+ new error.TimeoutError(
+ (opt_message ? opt_message + '\n' : '')
+ + `Wait timed out after ${elapsed}ms`));
+ } else {
+ // Do not use asyncRun here because we need a non-micro yield
+ // here so the UI thread is given a chance when running in a
+ // browser.
+ setTimeout(pollCondition, 0);
+ }
+ }, reject);
+ }
+ });
+ }, opt_message || '<anonymous wait>');
+}
+
+
+/**
+ * A scheduler that executes all tasks immediately, with no coordination. This
+ * class is an event emitter for API compatibility with the {@link ControlFlow},
+ * however, it emits no events.
+ *
+ * @implements {Scheduler}
+ */
+class SimpleScheduler extends events.EventEmitter {
+ /** @override */
+ execute(fn) {
+ return this.promise((resolve, reject) => {
+ try {
+ if (isGenerator(fn)) {
+ consume(fn).then(resolve, reject);
+ } else {
+ resolve(fn.call(undefined));
+ }
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
+ /** @override */
+ promise(resolver) {
+ return new NativePromise(resolver);
+ }
+
+ /** @override */
+ timeout(ms) {
+ return this.promise(resolve => setTimeout(_ => resolve(), ms));
+ }
+
+ /** @override */
+ wait(condition, opt_timeout, opt_message) {
+ return scheduleWait(this, condition, opt_timeout, opt_message);
+ }
+}
+const SIMPLE_SCHEDULER = new SimpleScheduler;
+
/**
* Handles the execution of scheduled tasks, each of which may be an
@@ -1848,13 +2186,20 @@ function fullyResolveKeys(obj) {
* If there are no listeners registered with the flow, the error will be
* rethrown to the global error handler.
*
- * Refer to the {@link ./promise} module documentation fora detailed
+ * Refer to the {@link ./promise} module documentation for a detailed
* explanation of how the ControlFlow coordinates task execution.
*
+ * @implements {Scheduler}
* @final
*/
class ControlFlow extends events.EventEmitter {
constructor() {
+ if (!usePromiseManager()) {
+ throw TypeError(
+ 'Cannot instantiate control flow when the promise manager has'
+ + ' been disabled');
+ }
+
super();
/** @private {boolean} */
@@ -2024,21 +2369,7 @@ class ControlFlow extends events.EventEmitter {
return this.activeQueue_;
}
- /**
- * Schedules a task for execution. If there is nothing currently in the
- * queue, the task will be executed in the next turn of the event loop. If
- * the task function is a generator, the task will be executed using
- * {@link ./promise.consume consume()}.
- *
- * @param {function(): (T|ManagedPromise<T>)} fn The function to
- * call to start the task. If the function returns a
- * {@link ManagedPromise}, this instance will wait for it to be
- * resolved before starting the next task.
- * @param {string=} opt_description A description of the task.
- * @return {!ManagedPromise<T>} A promise that will be resolved
- * with the result of the action.
- * @template T
- */
+ /** @override */
execute(fn, opt_description) {
if (isGenerator(fn)) {
let original = fn;
@@ -2060,126 +2391,21 @@ class ControlFlow extends events.EventEmitter {
return task.promise;
}
- /**
- * Inserts a {@code setTimeout} into the command queue. This is equivalent to
- * a thread sleep in a synchronous programming language.
- *
- * @param {number} ms The timeout delay, in milliseconds.
- * @param {string=} opt_description A description to accompany the timeout.
- * @return {!ManagedPromise} A promise that will be resolved with
- * the result of the action.
- */
+ /** @override */
+ promise(resolver) {
+ return new ManagedPromise(resolver, this);
+ }
+
+ /** @override */
timeout(ms, opt_description) {
- return this.execute(function() {
- return delayed(ms);
+ return this.execute(() => {
+ return this.promise(resolve => setTimeout(() => resolve(), ms));
}, opt_description);
}
- /**
- * Schedules a task that shall wait for a condition to hold. Each condition
- * function may return any value, but it will always be evaluated as a
- * boolean.
- *
- * Condition functions may schedule sub-tasks with this instance, however,
- * their execution time will be factored into whether a wait has timed out.
- *
- * In the event a condition returns a ManagedPromise, the polling loop will wait for
- * it to be resolved before evaluating whether the condition has been
- * satisfied. The resolution time for a promise is factored into whether a
- * wait has timed out.
- *
- * If the condition function throws, or returns a rejected promise, the
- * wait task will fail.
- *
- * If the condition is defined as a promise, the flow will wait for it to
- * settle. If the timeout expires before the promise settles, the promise
- * returned by this function will be rejected.
- *
- * If this function is invoked with `timeout === 0`, or the timeout is
- * omitted, the flow will wait indefinitely for the condition to be satisfied.
- *
- * @param {(!ManagedPromise<T>|function())} condition The condition to poll,
- * or a promise to wait on.
- * @param {number=} opt_timeout How long to wait, in milliseconds, for the
- * condition to hold before timing out. If omitted, the flow will wait
- * indefinitely.
- * @param {string=} opt_message An optional error message to include if the
- * wait times out; defaults to the empty string.
- * @return {!ManagedPromise<T>} A promise that will be fulfilled
- * when the condition has been satisified. The promise shall be rejected
- * if the wait times out waiting for the condition.
- * @throws {TypeError} If condition is not a function or promise or if timeout
- * is not a number >= 0.
- * @template T
- */
+ /** @override */
wait(condition, opt_timeout, opt_message) {
- var timeout = opt_timeout || 0;
- if (typeof timeout !== 'number' || timeout < 0) {
- throw TypeError('timeout must be a number >= 0: ' + timeout);
- }
-
- if (isPromise(condition)) {
- return this.execute(function() {
- if (!timeout) {
- return condition;
- }
- return new ManagedPromise(function(fulfill, reject) {
- var start = Date.now();
- var timer = setTimeout(function() {
- timer = null;
- reject(Error((opt_message ? opt_message + '\n' : '') +
- 'Timed out waiting for promise to resolve after ' +
- (Date.now() - start) + 'ms'));
- }, timeout);
-
- /** @type {Thenable} */(condition).then(
- function(value) {
- timer && clearTimeout(timer);
- fulfill(value);
- },
- function(error) {
- timer && clearTimeout(timer);
- reject(error);
- });
- });
- }, opt_message || '<anonymous wait: promise resolution>');
- }
-
- if (typeof condition !== 'function') {
- throw TypeError('Invalid condition; must be a function or promise: ' +
- typeof condition);
- }
-
- if (isGenerator(condition)) {
- let original = condition;
- condition = () => consume(original);
- }
-
- var self = this;
- return this.execute(function() {
- var startTime = Date.now();
- return new ManagedPromise(function(fulfill, reject) {
- pollCondition();
-
- function pollCondition() {
- var conditionFn = /** @type {function()} */(condition);
- self.execute(conditionFn).then(function(value) {
- var elapsed = Date.now() - startTime;
- if (!!value) {
- fulfill(value);
- } else if (timeout && elapsed >= timeout) {
- reject(new Error((opt_message ? opt_message + '\n' : '') +
- 'Wait timed out after ' + elapsed + 'ms'));
- } else {
- // Do not use asyncRun here because we need a non-micro yield
- // here so the UI thread is given a chance when running in a
- // browser.
- setTimeout(pollCondition, 0);
- }
- }, reject);
- }
- });
- }, opt_message || '<anonymous wait>');
+ return scheduleWait(this, condition, opt_timeout, opt_message);
}
/**
@@ -2510,6 +2736,9 @@ class TaskQueue extends events.EventEmitter {
/** @private {({task: !Task, q: !TaskQueue}|null)} */
this.pending_ = null;
+ /** @private {TaskQueue} */
+ this.subQ_ = null;
+
/** @private {TaskQueueState} */
this.state_ = TaskQueueState.NEW;
@@ -2690,7 +2919,7 @@ class TaskQueue extends events.EventEmitter {
var task;
do {
task = this.getNextTask_();
- } while (task && !task.promise.isPending());
+ } while (task && !isPending(task.promise));
if (!task) {
this.state_ = TaskQueueState.FINISHED;
@@ -2701,20 +2930,30 @@ class TaskQueue extends events.EventEmitter {
return;
}
- var self = this;
- var subQ = new TaskQueue(this.flow_);
- subQ.once('end', () => self.onTaskComplete_(result))
- .once('error', (e) => self.onTaskFailure_(result, e));
- vlog(2, () => self + ' created ' + subQ + ' for ' + task);
+ let result = undefined;
+ this.subQ_ = new TaskQueue(this.flow_);
+
+ this.subQ_.once('end', () => { // On task completion.
+ this.subQ_ = null;
+ this.pending_ && this.pending_.task.fulfill(result);
+ });
+
+ this.subQ_.once('error', e => { // On task failure.
+ this.subQ_ = null;
+ if (Thenable.isImplementation(result)) {
+ result.cancel(CancellationError.wrap(e));
+ }
+ this.pending_ && this.pending_.task.reject(e);
+ });
+ vlog(2, () => `${this} created ${this.subQ_} for ${task}`);
- var result = undefined;
try {
- this.pending_ = {task: task, q: subQ};
+ this.pending_ = {task: task, q: this.subQ_};
task.promise.queue_ = this;
- result = subQ.execute_(task.execute);
- subQ.start();
+ result = this.subQ_.execute_(task.execute);
+ this.subQ_.start();
} catch (ex) {
- subQ.abort_(ex);
+ this.subQ_.abort_(ex);
}
}
@@ -2805,28 +3044,6 @@ class TaskQueue extends events.EventEmitter {
}
/**
- * @param {*} value the value originally returned by the task function.
- * @private
- */
- onTaskComplete_(value) {
- if (this.pending_) {
- this.pending_.task.fulfill(value);
- }
- }
-
- /**
- * @param {*} taskFnResult the value originally returned by the task function.
- * @param {*} error the error that caused the task function to terminate.
- * @private
- */
- onTaskFailure_(taskFnResult, error) {
- if (Thenable.isImplementation(taskFnResult)) {
- taskFnResult.cancel(CancellationError.wrap(error));
- }
- this.pending_.task.reject(error);
- }
-
- /**
* @return {(Task|undefined)} the next task scheduled within this queue,
* if any.
* @private
@@ -2857,9 +3074,9 @@ class TaskQueue extends events.EventEmitter {
/**
* The default flow to use if no others are active.
- * @type {!ControlFlow}
+ * @type {ControlFlow}
*/
-var defaultFlow = new ControlFlow();
+var defaultFlow;
/**
@@ -2878,6 +3095,11 @@ var activeFlows = [];
* @throws {Error} If the default flow is not currently active.
*/
function setDefaultFlow(flow) {
+ if (!usePromiseManager()) {
+ throw Error(
+ 'You may not change set the control flow when the promise'
+ +' manager is disabled');
+ }
if (activeFlows.length) {
throw Error('You may only change the default flow while it is active');
}
@@ -2887,10 +3109,21 @@ function setDefaultFlow(flow) {
/**
* @return {!ControlFlow} The currently active control flow.
+ * @suppress {checkTypes}
*/
function controlFlow() {
- return /** @type {!ControlFlow} */ (
- activeFlows.length ? activeFlows[activeFlows.length - 1] : defaultFlow);
+ if (!usePromiseManager()) {
+ return SIMPLE_SCHEDULER;
+ }
+
+ if (activeFlows.length) {
+ return activeFlows[activeFlows.length - 1];
+ }
+
+ if (!defaultFlow) {
+ defaultFlow = new ControlFlow;
+ }
+ return defaultFlow;
}
@@ -2900,8 +3133,7 @@ function controlFlow() {
* a promise that resolves to the callback result.
* @param {function(!ControlFlow)} callback The entry point
* to the newly created flow.
- * @return {!ManagedPromise} A promise that resolves to the callback
- * result.
+ * @return {!Thenable} A promise that resolves to the callback result.
*/
function createFlow(callback) {
var flow = new ControlFlow;
@@ -2955,53 +3187,51 @@ function isGenerator(fn) {
* @param {Object=} opt_self The object to use as "this" when invoking the
* initial generator.
* @param {...*} var_args Any arguments to pass to the initial generator.
- * @return {!ManagedPromise<?>} A promise that will resolve to the
+ * @return {!Thenable<?>} A promise that will resolve to the
* generator's final result.
* @throws {TypeError} If the given function is not a generator.
*/
-function consume(generatorFn, opt_self, var_args) {
+function consume(generatorFn, opt_self, ...var_args) {
if (!isGenerator(generatorFn)) {
throw new TypeError('Input is not a GeneratorFunction: ' +
generatorFn.constructor.name);
}
- var deferred = defer();
- var generator = generatorFn.apply(
- opt_self, Array.prototype.slice.call(arguments, 2));
- callNext();
- return deferred.promise;
-
- /** @param {*=} opt_value . */
- function callNext(opt_value) {
- pump(generator.next, opt_value);
- }
-
- /** @param {*=} opt_error . */
- function callThrow(opt_error) {
- // Dictionary lookup required because Closure compiler's built-in
- // externs does not include GeneratorFunction.prototype.throw.
- pump(generator['throw'], opt_error);
- }
+ let ret;
+ return ret = createPromise((resolve, reject) => {
+ let generator = generatorFn.apply(opt_self, var_args);
+ callNext();
- function pump(fn, opt_arg) {
- if (!deferred.promise.isPending()) {
- return; // Defererd was cancelled; silently abort.
+ /** @param {*=} opt_value . */
+ function callNext(opt_value) {
+ pump(generator.next, opt_value);
}
- try {
- var result = fn.call(generator, opt_arg);
- } catch (ex) {
- deferred.reject(ex);
- return;
+ /** @param {*=} opt_error . */
+ function callThrow(opt_error) {
+ pump(generator.throw, opt_error);
}
- if (result.done) {
- deferred.fulfill(result.value);
- return;
- }
+ function pump(fn, opt_arg) {
+ if (ret instanceof ManagedPromise && !isPending(ret)) {
+ return; // Defererd was cancelled; silently abort.
+ }
- asap(result.value, callNext, callThrow);
- }
+ try {
+ var result = fn.call(generator, opt_arg);
+ } catch (ex) {
+ reject(ex);
+ return;
+ }
+
+ if (result.done) {
+ resolve(result.value);
+ return;
+ }
+
+ asap(result.value, callNext, callThrow);
+ }
+ });
}
@@ -3009,12 +3239,14 @@ function consume(generatorFn, opt_self, var_args) {
module.exports = {
+ CancellableThenable: CancellableThenable,
CancellationError: CancellationError,
ControlFlow: ControlFlow,
Deferred: Deferred,
MultipleUnhandledRejectionError: MultipleUnhandledRejectionError,
Thenable: Thenable,
Promise: ManagedPromise,
+ Scheduler: Scheduler,
all: all,
asap: asap,
captureStackTrace: captureStackTrace,
@@ -3025,6 +3257,7 @@ module.exports = {
defer: defer,
delayed: delayed,
filter: filter,
+ finally: thenFinally,
fulfilled: fulfilled,
fullyResolved: fullyResolved,
isGenerator: isGenerator,
@@ -3034,6 +3267,22 @@ module.exports = {
setDefaultFlow: setDefaultFlow,
when: when,
+ /**
+ * Indicates whether the promise manager is currently enabled. When disabled,
+ * attempting to use the {@link ControlFlow} or {@link ManagedPromise Promise}
+ * classes will generate an error.
+ *
+ * The promise manager is currently enabled by default, but may be disabled
+ * by setting the environment variable `SELENIUM_PROMISE_MANAGER=0` or by
+ * setting this property to false. Setting this property will always take
+ * precedence ove the use of the environment variable.
+ *
+ * @return {boolean} Whether the promise manager is enabled.
+ * @see <https://github.com/SeleniumHQ/selenium/issues/2969>
+ */
+ get USE_PROMISE_MANAGER() { return usePromiseManager(); },
+ set USE_PROMISE_MANAGER(/** boolean */value) { USE_PROMISE_MANAGER = value; },
+
get LONG_STACK_TRACES() { return LONG_STACK_TRACES; },
set LONG_STACK_TRACES(v) { LONG_STACK_TRACES = v; },
};
diff --git a/node_modules/selenium-webdriver/lib/safari/client.js b/node_modules/selenium-webdriver/lib/safari/client.js
deleted file mode 100644
index 482c820fc..000000000
--- a/node_modules/selenium-webdriver/lib/safari/client.js
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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.
-
-'use strict';var n=this;
-function q(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";else if("function"==
-b&&"undefined"==typeof a.call)return"object";return b}function aa(a){var b=q(a);return"array"==b||"object"==b&&"number"==typeof a.length}function r(a){return"string"==typeof a}function ba(a){var b=typeof a;return"object"==b&&null!=a||"function"==b}function ca(a,b,c){return a.call.apply(a.bind,arguments)}
-function da(a,b,c){if(!a)throw Error();if(2<arguments.length){var d=Array.prototype.slice.call(arguments,2);return function(){var c=Array.prototype.slice.call(arguments);Array.prototype.unshift.apply(c,d);return a.apply(b,c)}}return function(){return a.apply(b,arguments)}}function ea(a,b,c){ea=Function.prototype.bind&&-1!=Function.prototype.bind.toString().indexOf("native code")?ca:da;return ea.apply(null,arguments)}var fa=Date.now||function(){return+new Date};
-function ga(a,b){function c(){}c.prototype=b.prototype;a.B=b.prototype;a.prototype=new c;a.prototype.constructor=a;a.w=function(a,c,g){for(var e=Array(arguments.length-2),l=2;l<arguments.length;l++)e[l-2]=arguments[l];return b.prototype[c].apply(a,e)}};function t(a){if(Error.captureStackTrace)Error.captureStackTrace(this,t);else{var b=Error().stack;b&&(this.stack=b)}a&&(this.message=String(a))}ga(t,Error);t.prototype.name="CustomError";var ia;function ja(a,b){for(var c=a.split("%s"),d="",f=Array.prototype.slice.call(arguments,1);f.length&&1<c.length;)d+=c.shift()+f.shift();return d+c.join("%s")}var ka=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")};
-function la(a){if(!ma.test(a))return a;-1!=a.indexOf("&")&&(a=a.replace(na,"&amp;"));-1!=a.indexOf("<")&&(a=a.replace(oa,"&lt;"));-1!=a.indexOf(">")&&(a=a.replace(pa,"&gt;"));-1!=a.indexOf('"')&&(a=a.replace(qa,"&quot;"));-1!=a.indexOf("'")&&(a=a.replace(ra,"&#39;"));-1!=a.indexOf("\x00")&&(a=a.replace(sa,"&#0;"));return a}var na=/&/g,oa=/</g,pa=/>/g,qa=/"/g,ra=/'/g,sa=/\x00/g,ma=/[\x00&<>"']/;function ta(a,b){return a<b?-1:a>b?1:0};function ua(a,b){b.unshift(a);t.call(this,ja.apply(null,b));b.shift()}ga(ua,t);ua.prototype.name="AssertionError";function u(a,b){throw new ua("Failure"+(a?": "+a:""),Array.prototype.slice.call(arguments,1));};var va=Array.prototype.indexOf?function(a,b,c){return Array.prototype.indexOf.call(a,b,c)}:function(a,b,c){c=null==c?0:0>c?Math.max(0,a.length+c):c;if(r(a))return r(b)&&1==b.length?a.indexOf(b,c):-1;for(;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},wa=Array.prototype.forEach?function(a,b,c){Array.prototype.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,f=r(a)?a.split(""):a,g=0;g<d;g++)g in f&&b.call(c,f[g],g,a)};
-function xa(a){return Array.prototype.concat.apply(Array.prototype,arguments)}function Ba(a){var b=a.length;if(0<b){for(var c=Array(b),d=0;d<b;d++)c[d]=a[d];return c}return[]}function Ca(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};function Da(a,b){for(var c in a)b.call(void 0,a[c],c,a)};function v(a,b){this.b={};this.a=[];this.f=this.c=0;var c=arguments.length;if(1<c){if(c%2)throw Error("Uneven number of arguments");for(var d=0;d<c;d+=2)Ea(this,arguments[d],arguments[d+1])}else if(a){var f;if(a instanceof v)f=a.o(),d=a.l();else{var c=[],g=0;for(f in a)c[g++]=f;f=c;c=[];g=0;for(d in a)c[g++]=a[d];d=c}for(c=0;c<f.length;c++)Ea(this,f[c],d[c])}}v.prototype.l=function(){Fa(this);for(var a=[],b=0;b<this.a.length;b++)a.push(this.b[this.a[b]]);return a};
-v.prototype.o=function(){Fa(this);return this.a.concat()};v.prototype.clear=function(){this.b={};this.f=this.c=this.a.length=0};function Fa(a){if(a.c!=a.a.length){for(var b=0,c=0;b<a.a.length;){var d=a.a[b];y(a.b,d)&&(a.a[c++]=d);b++}a.a.length=c}if(a.c!=a.a.length){for(var f={},c=b=0;b<a.a.length;)d=a.a[b],y(f,d)||(a.a[c++]=d,f[d]=1),b++;a.a.length=c}}function Ga(a,b){return y(a.b,b)?a.b[b]:void 0}function Ea(a,b,c){y(a.b,b)||(a.c++,a.a.push(b),a.f++);a.b[b]=c}
-v.prototype.forEach=function(a,b){for(var c=this.o(),d=0;d<c.length;d++){var f=c[d];a.call(b,Ga(this,f),f,this)}};v.prototype.clone=function(){return new v(this)};function y(a,b){return Object.prototype.hasOwnProperty.call(a,b)};var Ha=/^(?:([^:/?#.]+):)?(?:\/\/(?:([^/?#]*)@)?([^/#?]*?)(?::([0-9]+))?(?=[/#?]|$))?([^?#]+)?(?:\?([^#]*))?(?:#(.*))?$/;function Ia(a,b){if(a)for(var c=a.split("&"),d=0;d<c.length;d++){var f=c[d].indexOf("="),g=null,e=null;0<=f?(g=c[d].substring(0,f),e=c[d].substring(f+1)):g=c[d];b(g,e?decodeURIComponent(e.replace(/\+/g," ")):"")}};function z(a,b){this.f=this.u=this.c="";this.s=null;this.g=this.m="";this.a=!1;var c;a instanceof z?(this.a=void 0!==b?b:a.a,Ja(this,a.c),this.u=a.u,this.f=a.f,Ka(this,a.s),this.m=a.m,La(this,a.b.clone()),this.g=a.g):a&&(c=String(a).match(Ha))?(this.a=!!b,Ja(this,c[1]||"",!0),this.u=A(c[2]||""),this.f=A(c[3]||"",!0),Ka(this,c[4]),this.m=A(c[5]||"",!0),La(this,c[6]||"",!0),this.g=A(c[7]||"")):(this.a=!!b,this.b=new B(null,0,this.a))}
-z.prototype.toString=function(){var a=[],b=this.c;b&&a.push(C(b,Ma,!0),":");var c=this.f;if(c||"file"==b)a.push("//"),(b=this.u)&&a.push(C(b,Ma,!0),"@"),a.push(encodeURIComponent(String(c)).replace(/%25([0-9a-fA-F]{2})/g,"%$1")),c=this.s,null!=c&&a.push(":",String(c));if(c=this.m)this.f&&"/"!=c.charAt(0)&&a.push("/"),a.push(C(c,"/"==c.charAt(0)?Na:Oa,!0));(c=this.b.toString())&&a.push("?",c);(c=this.g)&&a.push("#",C(c,Pa));return a.join("")};z.prototype.clone=function(){return new z(this)};
-function Ja(a,b,c){a.c=c?A(b,!0):b;a.c&&(a.c=a.c.replace(/:$/,""))}function Ka(a,b){if(b){b=Number(b);if(isNaN(b)||0>b)throw Error("Bad port number "+b);a.s=b}else a.s=null}function La(a,b,c){b instanceof B?(a.b=b,Qa(a.b,a.a)):(c||(b=C(b,Ra)),a.b=new B(b,0,a.a))}function A(a,b){return a?b?decodeURI(a.replace(/%25/g,"%2525")):decodeURIComponent(a):""}function C(a,b,c){return r(a)?(a=encodeURI(a).replace(b,Sa),c&&(a=a.replace(/%25([0-9a-fA-F]{2})/g,"%$1")),a):null}
-function Sa(a){a=a.charCodeAt(0);return"%"+(a>>4&15).toString(16)+(a&15).toString(16)}var Ma=/[#\/\?@]/g,Oa=/[\#\?:]/g,Na=/[\#\?]/g,Ra=/[\#\?@]/g,Pa=/#/g;function B(a,b,c){this.c=this.a=null;this.b=a||null;this.f=!!c}function D(a){a.a||(a.a=new v,a.c=0,a.b&&Ia(a.b,function(b,c){var d=decodeURIComponent(b.replace(/\+/g," "));D(a);a.b=null;var d=E(a,d),f=Ga(a.a,d);f||Ea(a.a,d,f=[]);f.push(c);a.c=a.c+1}))}
-function Ta(a,b){D(a);b=E(a,b);if(y(a.a.b,b)){a.b=null;a.c=a.c-Ga(a.a,b).length;var c=a.a;y(c.b,b)&&(delete c.b[b],c.c--,c.f++,c.a.length>2*c.c&&Fa(c))}}B.prototype.clear=function(){this.a=this.b=null;this.c=0};B.prototype.o=function(){D(this);for(var a=this.a.l(),b=this.a.o(),c=[],d=0;d<b.length;d++)for(var f=a[d],g=0;g<f.length;g++)c.push(b[d]);return c};
-B.prototype.l=function(a){D(this);var b=[];if(r(a)){var c=a;D(this);c=E(this,c);y(this.a.b,c)&&(b=xa(b,Ga(this.a,E(this,a))))}else for(a=this.a.l(),c=0;c<a.length;c++)b=xa(b,a[c]);return b};function Ua(){var a=(new z(window.location)).b.l("url");return 0<a.length?String(a[0]):void 0}
-B.prototype.toString=function(){if(this.b)return this.b;if(!this.a)return"";for(var a=[],b=this.a.o(),c=0;c<b.length;c++)for(var d=b[c],f=encodeURIComponent(String(d)),d=this.l(d),g=0;g<d.length;g++){var e=f;""!==d[g]&&(e+="="+encodeURIComponent(String(d[g])));a.push(e)}return this.b=a.join("&")};B.prototype.clone=function(){var a=new B;a.b=this.b;this.a&&(a.a=this.a.clone(),a.c=this.c);return a};function E(a,b){var c=String(b);a.f&&(c=c.toLowerCase());return c}
-function Qa(a,b){b&&!a.f&&(D(a),a.b=null,a.a.forEach(function(a,b){var f=b.toLowerCase();b!=f&&(Ta(this,b),Ta(this,f),0<a.length&&(this.b=null,Ea(this.a,E(this,f),Ba(a)),this.c=this.c+a.length))},a));a.f=b};var Va={area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0};function F(){this.a="";this.b=Wa}F.prototype.i=!0;F.prototype.h=function(){return this.a};F.prototype.toString=function(){return"Const{"+this.a+"}"};function Xa(a){if(a instanceof F&&a.constructor===F&&a.b===Wa)return a.a;u("expected object of type Const, got '"+a+"'");return"type_error:Const"}var Wa={};function Ya(a){var b=new F;b.a=a;return b};function G(){this.a="";this.b=Za}G.prototype.i=!0;var Za={};G.prototype.h=function(){return this.a};G.prototype.toString=function(){return"SafeStyle{"+this.a+"}"};function $a(a){var b=new G;b.a=a;return b}var ab=$a(""),bb=/^([-,."'%_!# a-zA-Z0-9]+|(?:rgb|hsl)a?\([0-9.%, ]+\))$/;function H(){this.a="";this.b=cb}H.prototype.i=!0;H.prototype.h=function(){return this.a};H.prototype.v=!0;H.prototype.j=function(){return 1};H.prototype.toString=function(){return"SafeUrl{"+this.a+"}"};function db(a){if(a instanceof H&&a.constructor===H&&a.b===cb)return a.a;u("expected object of type SafeUrl, got '"+a+"' of type "+q(a));return"type_error:SafeUrl"}var eb=/^(?:(?:https?|mailto|ftp):|[^&:/?#]*(?:[/?#]|$))/i;
-function fb(a){if(a instanceof H)return a;a=a.i?a.h():String(a);eb.test(a)||(a="about:invalid#zClosurez");return gb(a)}var cb={};function gb(a){var b=new H;b.a=a;return b}gb("about:blank");function I(){this.a=hb}I.prototype.i=!0;I.prototype.h=function(){return""};I.prototype.v=!0;I.prototype.j=function(){return 1};I.prototype.toString=function(){return"TrustedResourceUrl{}"};var hb={};var J;a:{var ib=n.navigator;if(ib){var jb=ib.userAgent;if(jb){J=jb;break a}}J=""};function L(){this.a="";this.c=kb;this.b=null}L.prototype.v=!0;L.prototype.j=function(){return this.b};L.prototype.i=!0;L.prototype.h=function(){return this.a};L.prototype.toString=function(){return"SafeHtml{"+this.a+"}"};function M(a){if(a instanceof L&&a.constructor===L&&a.c===kb)return a.a;u("expected object of type SafeHtml, got '"+a+"' of type "+q(a));return"type_error:SafeHtml"}function lb(a){if(a instanceof L)return a;var b=null;a.v&&(b=a.j());a=la(a.i?a.h():String(a));return N(a,b)}
-function O(a){if(a instanceof L)return a;a=lb(a);var b;b=M(a).replace(/ /g," &#160;").replace(/(\r\n|\r|\n)/g,"<br>");return N(b,a.j())}var mb=/^[a-zA-Z0-9-]+$/,nb={action:!0,cite:!0,data:!0,formaction:!0,href:!0,manifest:!0,poster:!0,src:!0},ob={APPLET:!0,BASE:!0,EMBED:!0,IFRAME:!0,LINK:!0,MATH:!0,META:!0,OBJECT:!0,SCRIPT:!0,STYLE:!0,SVG:!0,TEMPLATE:!0};
-function pb(a,b,c){if(!mb.test(a))throw Error("Invalid tag name <"+a+">.");if(a.toUpperCase()in ob)throw Error("Tag name <"+a+"> is not allowed for SafeHtml.");var d=null,f="<"+a;if(b)for(var g in b){if(!mb.test(g))throw Error('Invalid attribute name "'+g+'".');var e=b[g];if(null!=e){var l,m=a;l=g;if(e instanceof F)e=Xa(e);else if("style"==l.toLowerCase()){if(!ba(e))throw Error('The "style" attribute requires goog.html.SafeStyle or map of style properties, '+typeof e+" given: "+e);if(!(e instanceof
-G)){var m="",w=void 0;for(w in e){if(!/^[-_a-zA-Z0-9]+$/.test(w))throw Error("Name allows only [-_a-zA-Z0-9], got: "+w);var k=e[w];if(null!=k){if(k instanceof F)k=Xa(k);else if(bb.test(k)){for(var h=!0,x=!0,p=0;p<k.length;p++){var K=k.charAt(p);"'"==K&&x?h=!h:'"'==K&&h&&(x=!x)}h&&x||(u("String value requires balanced quotes, got: "+k),k="zClosurez")}else u("String value allows only [-,.\"'%_!# a-zA-Z0-9], rgb() and rgba(), got: "+k),k="zClosurez";m+=w+":"+k+";"}}e=m?$a(m):ab}m=void 0;e instanceof
-G&&e.constructor===G&&e.b===Za?m=e.a:(u("expected object of type SafeStyle, got '"+e+"' of type "+q(e)),m="type_error:SafeStyle");e=m}else{if(/^on/i.test(l))throw Error('Attribute "'+l+'" requires goog.string.Const value, "'+e+'" given.');if(l.toLowerCase()in nb)if(e instanceof I)e instanceof I&&e.constructor===I&&e.a===hb?e="":(u("expected object of type TrustedResourceUrl, got '"+e+"' of type "+q(e)),e="type_error:TrustedResourceUrl");else if(e instanceof H)e=db(e);else if(r(e))e=fb(e).h();else throw Error('Attribute "'+
-l+'" on tag "'+m+'" requires goog.html.SafeUrl, goog.string.Const, or string, value "'+e+'" given.');}e.i&&(e=e.h());l=l+'="'+la(String(e))+'"';f=f+(" "+l)}}null!=c?"array"==q(c)||(c=[c]):c=[];!0===Va[a.toLowerCase()]?f+=">":(d=P(c),f+=">"+M(d)+"</"+a+">",d=d.j());(a=b&&b.dir)&&(/^(ltr|rtl|auto)$/i.test(a)?d=0:d=null);return N(f,d)}function P(a){function b(a){"array"==q(a)?wa(a,b):(a=lb(a),d+=M(a),a=a.j(),0==c?c=a:0!=a&&c!=a&&(c=null))}var c=0,d="";wa(arguments,b);return N(d,c)}var kb={};
-function N(a,b){var c=new L;c.a=a;c.b=b;return c}N("<!DOCTYPE html>",0);var qb=N("",0),rb=N("<br>",0);function sb(a){var b;b=Error();if(Error.captureStackTrace)Error.captureStackTrace(b,a||sb),b=String(b.stack);else{try{throw b;}catch(c){b=c}b=(b=b.stack)?String(b):null}b||(b=tb(a||arguments.callee.caller,[]));return b}
-function tb(a,b){var c=[];if(0<=va(b,a))c.push("[...circular reference...]");else if(a&&50>b.length){c.push(ub(a)+"(");for(var d=a.arguments,f=0;d&&f<d.length;f++){0<f&&c.push(", ");var g;g=d[f];switch(typeof g){case "object":g=g?"object":"null";break;case "string":break;case "number":g=String(g);break;case "boolean":g=g?"true":"false";break;case "function":g=(g=ub(g))?g:"[fn]";break;default:g=typeof g}40<g.length&&(g=g.substr(0,40)+"...");c.push(g)}b.push(a);c.push(")\n");try{c.push(tb(a.caller,
-b))}catch(e){c.push("[exception trying to get caller]\n")}}else a?c.push("[...long stack...]"):c.push("[end]");return c.join("")}function ub(a){if(Q[a])return Q[a];a=String(a);if(!Q[a]){var b=/function ([^\(]+)/.exec(a);Q[a]=b?b[1]:"[Anonymous]"}return Q[a]}var Q={};function vb(a,b,c,d,f){"number"==typeof f||wb++;this.c=d||fa();this.f=a;this.b=b;this.g=c;delete this.a}vb.prototype.a=null;var wb=0;function xb(a){this.g=a;this.a=this.c=this.f=this.b=null}function R(a,b){this.name=a;this.value=b}R.prototype.toString=function(){return this.name};var yb=new R("SHOUT",1200),zb=new R("SEVERE",1E3),Ab=new R("WARNING",900),Bb=new R("INFO",800),Cb=new R("CONFIG",700);function Db(a){if(a.f)return a.f;if(a.b)return Db(a.b);u("Root logger has no level set.");return null}
-xb.prototype.log=function(a,b,c){if(a.value>=Db(this).value)for("function"==q(b)&&(b=b()),a=new vb(a,String(b),this.g),c&&(a.a=c),c="log:"+a.b,n.console&&(n.console.timeStamp?n.console.timeStamp(c):n.console.markTimeline&&n.console.markTimeline(c)),n.msWriteProfilerMark&&n.msWriteProfilerMark(c),c=this;c;){b=c;var d=a;if(b.a)for(var f=0,g=void 0;g=b.a[f];f++)g(d);c=c.b}};var Eb={},S=null;function Fb(){S||(S=new xb(""),Eb[""]=S,S.f=Cb)}
-function Gb(a){Fb();var b;if(!(b=Eb[a])){b=new xb(a);var c=a.lastIndexOf("."),d=a.substr(c+1),c=Gb(a.substr(0,c));c.c||(c.c={});c.c[d]=b;b.b=c;Eb[a]=b}return b};var Hb=new function(){this.a=fa()};function Ib(a){this.c=a||"";this.f=Hb}Ib.prototype.a=!0;Ib.prototype.b=!1;function T(a){return 10>a?"0"+a:String(a)}function Jb(a){Ib.call(this,a)}ga(Jb,Ib);Jb.prototype.b=!0;function Kb(a,b){Da(b,function(b,d){"style"==d?a.style.cssText=b:"class"==d?a.className=b:"for"==d?a.htmlFor=b:Lb.hasOwnProperty(d)?a.setAttribute(Lb[d],b):0==d.lastIndexOf("aria-",0)||0==d.lastIndexOf("data-",0)?a.setAttribute(d,b):a[d]=b})}var Lb={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",frameborder:"frameBorder",height:"height",maxlength:"maxLength",role:"role",rowspan:"rowSpan",type:"type",usemap:"useMap",valign:"vAlign",width:"width"};
-function Mb(a,b,c){function d(c){c&&b.appendChild(r(c)?a.createTextNode(c):c)}for(var f=2;f<c.length;f++){var g=c[f];!aa(g)||ba(g)&&0<g.nodeType?d(g):wa(Nb(g)?Ba(g):g,d)}}function Nb(a){if(a&&"number"==typeof a.length){if(ba(a))return"function"==typeof a.item||"string"==typeof a.item;if("function"==q(a))return"function"==typeof a.item}return!1}function Ob(a){this.b=a||n.document||document}
-function Pb(a,b){var c;c=a.b;var d=b&&"*"!=b?b.toUpperCase():"";c.querySelectorAll&&c.querySelector&&d?c=c.querySelectorAll(d+""):c=c.getElementsByTagName(d||"*");return c}Ob.prototype.a=function(a,b,c){var d=this.b,f=arguments,g=f[1],e=d.createElement(f[0]);g&&(r(g)?e.className=g:"array"==q(g)?e.className=g.join(" "):Kb(e,g));2<f.length&&Mb(d,e,f);return e};
-Ob.prototype.contains=function(a,b){if(!a||!b)return!1;if(a.contains&&1==b.nodeType)return a==b||a.contains(b);if("undefined"!=typeof a.compareDocumentPosition)return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a};function Qb(a){this.m=ea(this.f,this);this.b=new Jb;this.c=this.b.a=!1;this.a=a;this.g=this.a.ownerDocument||this.a.document;a=(a=this.a)?new Ob(9==a.nodeType?a:a.ownerDocument||a.document):ia||(ia=new Ob);var b=null,c=Pb(a,"HEAD")[0];c||(b=Pb(a,"BODY")[0],c=a.a("HEAD"),b.parentNode.insertBefore(c,b));b=a.a("STYLE");b.innerHTML=".dbg-sev{color:#F00}.dbg-w{color:#C40}.dbg-sh{font-weight:bold;color:#000}.dbg-i{color:#444}.dbg-f{color:#999}.dbg-ev{color:#0A0}.dbg-m{color:#990}.logmsg{border-bottom:1px solid #CCC;padding:2px}.logsep{background-color: #8C8;}.logdiv{border:1px solid #CCC;background-color:#FCFCFC;font:medium monospace}";
-c.appendChild(b);this.a.className+=" logdiv"}function Rb(a){if(1!=a.c){var b;Fb();b=S;var c=a.m;b.a||(b.a=[]);b.a.push(c);a.c=!0}}
-Qb.prototype.f=function(a){if(a){var b=100>=this.a.scrollHeight-this.a.scrollTop-this.a.clientHeight,c=this.g.createElement("DIV");c.className="logmsg";var d;var f=this.b;if(a){switch(a.f.value){case yb.value:d="dbg-sh";break;case zb.value:d="dbg-sev";break;case Ab.value:d="dbg-w";break;case Bb.value:d="dbg-i";break;default:d="dbg-f"}var g=[];g.push(f.c," ");if(f.a){var e=new Date(a.c);g.push("[",T(e.getFullYear()-2E3)+T(e.getMonth()+1)+T(e.getDate())+" "+T(e.getHours())+":"+T(e.getMinutes())+":"+
-T(e.getSeconds())+"."+T(Math.floor(e.getMilliseconds()/10)),"] ")}var e=(a.c-f.f.a)/1E3,l=e.toFixed(3),m=0;if(1>e)m=2;else for(;100>e;)m++,e*=10;for(;0<m--;)l=" "+l;g.push("[",l,"s] ");g.push("[",a.g,"] ");g=O(g.join(""));e=qb;if(f.b&&a.a){var w;try{var k;var h=a.a,x;d:{for(var f=["window","location","href"],e=n,p;p=f.shift();)if(null!=e[p])e=e[p];else{x=null;break d}x=e}if(r(h))k={message:h,name:"Unknown error",lineNumber:"Not available",fileName:x,stack:"Not available"};else{var K,ya;p=!1;try{K=
-h.lineNumber||h.A||"Not available"}catch(za){K="Not available",p=!0}try{ya=h.fileName||h.filename||h.sourceURL||n.$googDebugFname||x}catch(za){ya="Not available",p=!0}k=!p&&h.lineNumber&&h.fileName&&h.stack&&h.message&&h.name?h:{message:h.message||"Not available",name:h.name||"UnknownError",lineNumber:K,fileName:ya,stack:h.stack||"Not available"}}var Aa;var ha=k.fileName;null!=ha||(ha="");if(/^https?:\/\//i.test(ha)){var cc=fb(ha);Ya("view-source scheme plus HTTP/HTTPS URL");var dc="view-source:"+
-db(cc);Aa=gb(dc)}else{var ec=Ya("sanitizedviewsrc");Aa=gb(Xa(ec))}w=P(O("Message: "+k.message+"\nUrl: "),pb("a",{href:Aa,target:"_new"},k.fileName),O("\nLine: "+k.lineNumber+"\n\nBrowser stack:\n"+k.stack+"-> [end]\n\nJS stack traversal:\n"+sb(void 0)+"-> "))}catch(za){w=O("Exception trying to expose exception! You win, we lose. "+za)}e=P(rb,w)}a=O(a.b);d=pb("span",{"class":d},P(a,e));d=P(g,d,rb)}else d=qb;c.innerHTML=M(d);this.a.appendChild(c);b&&(this.a.scrollTop=this.a.scrollHeight)}};
-Qb.prototype.clear=function(){this.a&&(this.a.innerHTML=M(qb))};function U(a,b){a&&a.log(Bb,b,void 0)};var Sb;if(-1!=J.indexOf("iPhone")&&-1==J.indexOf("iPod")&&-1==J.indexOf("iPad")||-1!=J.indexOf("iPad")||-1!=J.indexOf("iPod"))Sb="";else{var Tb=/Version\/([0-9.]+)/.exec(J);Sb=Tb?Tb[1]:""};for(var Ub=0,Vb=ka(String(Sb)).split("."),Wb=ka("6").split("."),Xb=Math.max(Vb.length,Wb.length),Yb=0;0==Ub&&Yb<Xb;Yb++){var Zb=Vb[Yb]||"",$b=Wb[Yb]||"",ac=/(\d*)(\D*)/g,bc=/(\d*)(\D*)/g;do{var V=ac.exec(Zb)||["","",""],W=bc.exec($b)||["","",""];if(0==V[0].length&&0==W[0].length)break;Ub=ta(0==V[1].length?0:parseInt(V[1],10),0==W[1].length?0:parseInt(W[1],10))||ta(0==V[2].length,0==W[2].length)||ta(V[2],W[2])}while(0==Ub)};var fc=JSON.stringify;function X(a,b,c){var d=a.constructor;if(a.window===a)try{var f=a.constructor;delete a.constructor;d=a.constructor;a.constructor=f}catch(g){}return d.prototype[b].apply(a,Ca(arguments,2))};Gb("safaridriver.message");function gc(a){this.a={};this.a[hc]="webdriver";this.a[ic]=a}var hc="origin",ic="type";function jc(a){var b=window;a.a[hc]="webdriver";if(b.postMessage){var c,b=function(a){c=a.data};X(window,"addEventListener","safaridriver.message.response",b,!1);kc(a.a);X(window,"removeEventListener","safaridriver.message.response",b,!1);return c}var d=X(document,"createEvent","Events");d.initEvent("beforeload",!1,!1);return b.canLoad(d,a.a)}
-function kc(a){var b=X(document,"createEvent","MessageEvent");b.initMessageEvent("safaridriver.message",!1,!1,a,window.location.origin,"0",window,null);X(window,"dispatchEvent",b)}gc.prototype.toJSON=function(){return this.a};gc.prototype.toString=function(){return fc(this.toJSON())};function lc(a){gc.call(this,"connect");this.a.url=a}ga(lc,gc);function mc(){function a(){f+=1;jc(g)?(U(c,"Connected to extension"),U(c,"Requesting extension connect to client at "+d)):5>f?setTimeout(a,250*f):c&&c.log(zb,"Unable to establish a connection with the SafariDriver extension",void 0)}var b=document.createElement("h2");b.innerHTML="SafariDriver Launcher";document.body.appendChild(b);b=document.createElement("div");document.body.appendChild(b);Rb(new Qb(b));var c=Gb("safaridriver.client"),d=Ua();if(d){d=new z(d);U(c,"Connecting to SafariDriver browser extension...");
-U(c,"This will fail if you have not installed the latest SafariDriver extension from\nhttp://selenium-release.storage.googleapis.com/index.html");U(c,"Extension logs may be viewed by clicking the Selenium [\u2713] button on the Safari toolbar");var f=0,g=new lc(d.toString());a()}else c&&c.log(zb,"No url specified. Please reload this page with the url parameter set",void 0)}var Y=["init"],Z=n;Y[0]in Z||!Z.execScript||Z.execScript("var "+Y[0]);
-for(var nc;Y.length&&(nc=Y.shift());)Y.length||void 0===mc?Z[nc]?Z=Z[nc]:Z=Z[nc]={}:Z[nc]=mc;;window.onload = init;
diff --git a/node_modules/selenium-webdriver/lib/session.js b/node_modules/selenium-webdriver/lib/session.js
index 296291d4c..64ff6be76 100644
--- a/node_modules/selenium-webdriver/lib/session.js
+++ b/node_modules/selenium-webdriver/lib/session.js
@@ -17,7 +17,7 @@
'use strict';
-const Capabilities = require('./capabilities').Capabilities;
+const {Capabilities} = require('./capabilities');
/**
diff --git a/node_modules/selenium-webdriver/lib/test/build.js b/node_modules/selenium-webdriver/lib/test/build.js
index 59f0b1357..53b284ffb 100644
--- a/node_modules/selenium-webdriver/lib/test/build.js
+++ b/node_modules/selenium-webdriver/lib/test/build.js
@@ -21,8 +21,7 @@ const spawn = require('child_process').spawn,
fs = require('fs'),
path = require('path');
-const isDevMode = require('../devmode'),
- promise = require('../promise');
+const isDevMode = require('../devmode');
var projectRoot = path.normalize(path.join(__dirname, '../../../../..'));
@@ -69,7 +68,7 @@ Build.prototype.onlyOnce = function() {
/**
* Executes the build.
- * @return {!webdriver.promise.Promise} A promise that will be resolved when
+ * @return {!Promise} A promise that will be resolved when
* the build has completed.
* @throws {Error} If no targets were specified.
*/
@@ -86,7 +85,7 @@ Build.prototype.go = function() {
});
if (!targets.length) {
- return promise.fulfilled();
+ return Promise.resolve();
}
}
@@ -100,31 +99,30 @@ Build.prototype.go = function() {
cmd = path.join(projectRoot, 'go');
}
- var result = promise.defer();
- spawn(cmd, args, {
- cwd: projectRoot,
- env: process.env,
- stdio: ['ignore', process.stdout, process.stderr]
- }).on('exit', function(code, signal) {
- if (code === 0) {
- targets.forEach(function(target) {
- builtTargets[target] = 1;
- });
- return result.fulfill();
- }
-
- var msg = 'Unable to build artifacts';
- if (code) { // May be null.
- msg += '; code=' + code;
- }
- if (signal) {
- msg += '; signal=' + signal;
- }
-
- result.reject(Error(msg));
+ return new Promise((resolve, reject) => {
+ spawn(cmd, args, {
+ cwd: projectRoot,
+ env: process.env,
+ stdio: ['ignore', process.stdout, process.stderr]
+ }).on('exit', function(code, signal) {
+ if (code === 0) {
+ targets.forEach(function(target) {
+ builtTargets[target] = 1;
+ });
+ return resolve();
+ }
+
+ var msg = 'Unable to build artifacts';
+ if (code) { // May be null.
+ msg += '; code=' + code;
+ }
+ if (signal) {
+ msg += '; signal=' + signal;
+ }
+
+ reject(Error(msg));
+ });
});
-
- return result.promise;
};
diff --git a/node_modules/selenium-webdriver/lib/test/data/formPage.html b/node_modules/selenium-webdriver/lib/test/data/formPage.html
index 7bcfea00f..45ae2b7d6 100644
--- a/node_modules/selenium-webdriver/lib/test/data/formPage.html
+++ b/node_modules/selenium-webdriver/lib/test/data/formPage.html
@@ -45,6 +45,7 @@ There should be a form here:
<select name="no-select" disabled="disabled">
<option value="foo">Foo</option>
+ <option value="bar">Bar</option>
</select>
<select name="select_empty_multiple" multiple>
diff --git a/node_modules/selenium-webdriver/lib/test/fileserver.js b/node_modules/selenium-webdriver/lib/test/fileserver.js
index 448b0c9a2..8194778ea 100644
--- a/node_modules/selenium-webdriver/lib/test/fileserver.js
+++ b/node_modules/selenium-webdriver/lib/test/fileserver.js
@@ -28,8 +28,7 @@ var serveIndex = require('serve-index');
var Server = require('./httpserver').Server,
resources = require('./resources'),
- isDevMode = require('../devmode'),
- promise = require('../promise');
+ isDevMode = require('../devmode');
var WEB_ROOT = '/common';
var JS_ROOT = '/javascript';
@@ -194,15 +193,18 @@ function sendDelayedResponse(request, response) {
}
-function handleUpload(request, response, next) {
- multer({
- inMemory: true,
- onFileUploadComplete: function(file) {
+function handleUpload(request, response) {
+ let upload = multer({storage: multer.memoryStorage()}).any();
+ upload(request, response, function(err) {
+ if (err) {
+ response.writeHead(500);
+ response.end(err + '');
+ } else {
response.writeHead(200);
- response.write(file.buffer);
+ response.write(request.files[0].buffer);
response.end('<script>window.top.window.onUploadDone();</script>');
}
- })(request, response, function() {});
+ });
}
@@ -269,7 +271,7 @@ function sendIndex(request, response) {
/**
* Starts the server on the specified port.
* @param {number=} opt_port The port to use, or 0 for any free port.
- * @return {!webdriver.promise.Promise.<Host>} A promise that will resolve
+ * @return {!Promise<Host>} A promise that will resolve
* with the server host when it has fully started.
*/
exports.start = server.start.bind(server);
@@ -277,7 +279,7 @@ exports.start = server.start.bind(server);
/**
* Stops the server.
- * @return {!webdriver.promise.Promise} A promise that will resolve when the
+ * @return {!Promise} A promise that will resolve when the
* server has closed all connections.
*/
exports.stop = server.stop.bind(server);
diff --git a/node_modules/selenium-webdriver/lib/test/httpserver.js b/node_modules/selenium-webdriver/lib/test/httpserver.js
index 55b12551f..f7847f734 100644
--- a/node_modules/selenium-webdriver/lib/test/httpserver.js
+++ b/node_modules/selenium-webdriver/lib/test/httpserver.js
@@ -50,14 +50,14 @@ var Server = function(requestHandler) {
* Starts the server on the given port. If no port, or 0, is provided,
* the server will be started on a random port.
* @param {number=} opt_port The port to start on.
- * @return {!webdriver.promise.Promise.<Host>} A promise that will resolve
+ * @return {!Promise<Host>} A promise that will resolve
* with the server host when it has fully started.
*/
this.start = function(opt_port) {
assert(typeof opt_port !== 'function',
"start invoked with function, not port (mocha callback)?");
var port = opt_port || portprober.findFreePort('localhost');
- return promise.when(port, function(port) {
+ return Promise.resolve(port).then(port => {
return promise.checkedNodeCall(
server.listen.bind(server, port, 'localhost'));
}).then(function() {
@@ -67,13 +67,11 @@ var Server = function(requestHandler) {
/**
* Stops the server.
- * @return {!webdriver.promise.Promise} A promise that will resolve when the
+ * @return {!Promise} A promise that will resolve when the
* server has closed all connections.
*/
this.stop = function() {
- var d = promise.defer();
- server.close(d.fulfill);
- return d.promise;
+ return new Promise(resolve => server.close(resolve));
};
/**
diff --git a/node_modules/selenium-webdriver/lib/test/index.js b/node_modules/selenium-webdriver/lib/test/index.js
index d702c5fb2..ba34ddab4 100644
--- a/node_modules/selenium-webdriver/lib/test/index.js
+++ b/node_modules/selenium-webdriver/lib/test/index.js
@@ -31,7 +31,6 @@ var build = require('./build'),
const LEGACY_FIREFOX = 'legacy-' + webdriver.Browser.FIREFOX;
-const LEGACY_SAFARI = 'legacy-' + webdriver.Browser.SAFARI;
/**
@@ -46,8 +45,7 @@ var NATIVE_BROWSERS = [
webdriver.Browser.IE,
webdriver.Browser.OPERA,
webdriver.Browser.PHANTOM_JS,
- webdriver.Browser.SAFARI,
- LEGACY_SAFARI
+ webdriver.Browser.SAFARI
];
@@ -83,8 +81,7 @@ var browsersToTest = (function() {
parts[0] = webdriver.Browser.IE;
}
- if (parts[0] === LEGACY_FIREFOX ||
- parts[0] === LEGACY_SAFARI) {
+ if (parts[0] === LEGACY_FIREFOX) {
return;
}
@@ -117,6 +114,8 @@ var browsersToTest = (function() {
console.log('Running tests using loopback address')
}
}
+ console.log(
+ 'Promise manager is enabled? ' + webdriver.promise.USE_PROMISE_MANAGER);
return browsers;
})();
@@ -175,14 +174,6 @@ function TestEnvironment(browserName, server) {
parts[0] = webdriver.Browser.FIREFOX;
}
- if (parts[0] === LEGACY_SAFARI) {
- var options = builder.getSafariOptions() || new safari.Options();
- options.useLegacyDriver(true);
- builder.setSafariOptions(options);
-
- parts[0] = webdriver.Browser.SAFARI;
- }
-
builder.forBrowser(parts[0], parts[1], parts[2]);
if (server) {
builder.usingServer(server.address());
@@ -227,27 +218,31 @@ function suite(fn, opt_options) {
try {
+ before(function() {
+ if (isDevMode) {
+ return build.of(
+ '//javascript/atoms/fragments:is-displayed',
+ '//javascript/webdriver/atoms:getAttribute')
+ .onlyOnce().go();
+ }
+ });
+
// Server is only started if required for a specific config.
- testing.after(function() {
+ after(function() {
if (seleniumServer) {
return seleniumServer.stop();
}
});
browsers.forEach(function(browser) {
- testing.describe('[' + browser + ']', function() {
+ describe('[' + browser + ']', function() {
if (isDevMode && nativeRun) {
if (browser === LEGACY_FIREFOX) {
- testing.before(function() {
+ before(function() {
return build.of('//javascript/firefox-driver:webdriver')
.onlyOnce().go();
});
- } else if (browser === LEGACY_SAFARI) {
- testing.before(function() {
- return build.of('//javascript/safari-driver:client')
- .onlyOnce().go();
- });
}
}
@@ -259,7 +254,7 @@ function suite(fn, opt_options) {
serverJar, {loopback: useLoopback});
}
- testing.before(function() {
+ before(function() {
this.timeout(0);
return seleniumServer.start(60 * 1000);
});
@@ -275,14 +270,14 @@ function suite(fn, opt_options) {
// GLOBAL TEST SETUP
-testing.before(function() {
+before(function() {
// Do not pass register fileserver.start directly with testing.before,
// as start takes an optional port, which before assumes is an async
// callback.
return fileserver.start();
});
-testing.after(function() {
+after(function() {
return fileserver.stop();
});
diff --git a/node_modules/selenium-webdriver/lib/webdriver.js b/node_modules/selenium-webdriver/lib/webdriver.js
index 13077b54e..081d77bda 100644
--- a/node_modules/selenium-webdriver/lib/webdriver.js
+++ b/node_modules/selenium-webdriver/lib/webdriver.js
@@ -28,7 +28,7 @@ const command = require('./command');
const error = require('./error');
const input = require('./input');
const logging = require('./logging');
-const Session = require('./session').Session;
+const {Session} = require('./session');
const Symbols = require('./symbols');
const promise = require('./promise');
@@ -64,13 +64,13 @@ class Condition {
/**
* Defines a condition that will result in a {@link WebElement}.
*
- * @extends {Condition<!(WebElement|promise.Promise<!WebElement>)>}
+ * @extends {Condition<!(WebElement|IThenable<!WebElement>)>}
*/
class WebElementCondition extends Condition {
/**
* @param {string} message A descriptive error message. Should complete the
* sentence "Waiting [...]"
- * @param {function(!WebDriver): !(WebElement|promise.Promise<!WebElement>)}
+ * @param {function(!WebDriver): !(WebElement|IThenable<!WebElement>)}
* fn The condition function to evaluate on each iteration of the wait
* loop.
*/
@@ -237,145 +237,14 @@ function fromWireValue(driver, value) {
/**
- * Creates a new WebDriver client, which provides control over a browser.
- *
- * Every command.Command returns a {@link promise.Promise} that
- * represents the result of that command. Callbacks may be registered on this
- * object to manipulate the command result or catch an expected error. Any
- * commands scheduled with a callback are considered sub-commands and will
- * execute before the next command in the current frame. For example:
- *
- * var message = [];
- * driver.call(message.push, message, 'a').then(function() {
- * driver.call(message.push, message, 'b');
- * });
- * driver.call(message.push, message, 'c');
- * driver.call(function() {
- * alert('message is abc? ' + (message.join('') == 'abc'));
- * });
+ * Structural interface for a WebDriver client.
*
+ * @record
*/
-class WebDriver {
- /**
- * @param {!(Session|promise.Promise<!Session>)} session Either a
- * known session or a promise that will be resolved to a session.
- * @param {!command.Executor} executor The executor to use when sending
- * commands to the browser.
- * @param {promise.ControlFlow=} opt_flow The flow to
- * schedule commands through. Defaults to the active flow object.
- */
- constructor(session, executor, opt_flow) {
- /** @private {!promise.Promise<!Session>} */
- this.session_ = promise.fulfilled(session);
-
- /** @private {!command.Executor} */
- this.executor_ = executor;
-
- /** @private {!promise.ControlFlow} */
- this.flow_ = opt_flow || promise.controlFlow();
-
- /** @private {input.FileDetector} */
- this.fileDetector_ = null;
- }
-
- /**
- * Creates a new WebDriver client for an existing session.
- * @param {!command.Executor} executor Command executor to use when querying
- * for session details.
- * @param {string} sessionId ID of the session to attach to.
- * @param {promise.ControlFlow=} opt_flow The control flow all
- * driver commands should execute under. Defaults to the
- * {@link promise.controlFlow() currently active} control flow.
- * @return {!WebDriver} A new client for the specified session.
- */
- static attachToSession(executor, sessionId, opt_flow) {
- let flow = opt_flow || promise.controlFlow();
- let cmd = new command.Command(command.Name.DESCRIBE_SESSION)
- .setParameter('sessionId', sessionId);
- let session = flow.execute(
- () => executeCommand(executor, cmd).catch(err => {
- // The DESCRIBE_SESSION command is not supported by the W3C spec, so
- // if we get back an unknown command, just return a session with
- // unknown capabilities.
- if (err instanceof error.UnknownCommandError) {
- return new Session(sessionId, new Capabilities);
- }
- throw err;
- }),
- 'WebDriver.attachToSession()');
- return new WebDriver(session, executor, flow);
- }
-
- /**
- * Creates a new WebDriver session.
- *
- * By default, the requested session `capabilities` are merely "desired" and
- * the remote end will still create a new session even if it cannot satisfy
- * all of the requested capabilities. You can query which capabilities a
- * session actually has using the
- * {@linkplain #getCapabilities() getCapabilities()} method on the returned
- * WebDriver instance.
- *
- * To define _required capabilities_, provide the `capabilities` as an object
- * literal with `required` and `desired` keys. The `desired` key may be
- * omitted if all capabilities are required, and vice versa. If the server
- * cannot create a session with all of the required capabilities, it will
- * return an {@linkplain error.SessionNotCreatedError}.
- *
- * let required = new Capabilities().set('browserName', 'firefox');
- * let desired = new Capabilities().set('version', '45');
- * let driver = WebDriver.createSession(executor, {required, desired});
- *
- * This function will always return a WebDriver instance. If there is an error
- * creating the session, such as the aforementioned SessionNotCreatedError,
- * the driver will have a rejected {@linkplain #getSession session} promise.
- * It is recommended that this promise is left _unhandled_ so it will
- * propagate through the {@linkplain promise.ControlFlow control flow} and
- * cause subsequent commands to fail.
- *
- * let required = Capabilities.firefox();
- * let driver = WebDriver.createSession(executor, {required});
- *
- * // If the createSession operation failed, then this command will also
- * // also fail, propagating the creation failure.
- * driver.get('http://www.google.com').catch(e => console.log(e));
- *
- * @param {!command.Executor} executor The executor to create the new session
- * with.
- * @param {(!Capabilities|
- * {desired: (Capabilities|undefined),
- * required: (Capabilities|undefined)})} capabilities The desired
- * capabilities for the new session.
- * @param {promise.ControlFlow=} opt_flow The control flow all driver
- * commands should execute under, including the initial session creation.
- * Defaults to the {@link promise.controlFlow() currently active}
- * control flow.
- * @return {!WebDriver} The driver for the newly created session.
- */
- static createSession(executor, capabilities, opt_flow) {
- let flow = opt_flow || promise.controlFlow();
- let cmd = new command.Command(command.Name.NEW_SESSION);
-
- if (capabilities && (capabilities.desired || capabilities.required)) {
- cmd.setParameter('desiredCapabilities', capabilities.desired);
- cmd.setParameter('requiredCapabilities', capabilities.required);
- } else {
- cmd.setParameter('desiredCapabilities', capabilities);
- }
+class IWebDriver {
- let session = flow.execute(
- () => executeCommand(executor, cmd),
- 'WebDriver.createSession()');
- return new WebDriver(session, executor, flow);
- }
-
- /**
- * @return {!promise.ControlFlow} The control flow used by this
- * instance.
- */
- controlFlow() {
- return this.flow_;
- }
+ /** @return {!promise.ControlFlow} The control flow used by this instance. */
+ controlFlow() {}
/**
* Schedules a {@link command.Command} to be executed by this driver's
@@ -383,107 +252,44 @@ class WebDriver {
*
* @param {!command.Command} command The command to schedule.
* @param {string} description A description of the command for debugging.
- * @return {!promise.Promise<T>} A promise that will be resolved
+ * @return {!promise.Thenable<T>} A promise that will be resolved
* with the command result.
* @template T
*/
- schedule(command, description) {
- var self = this;
-
- checkHasNotQuit();
- command.setParameter('sessionId', this.session_);
-
- // If any of the command parameters are rejected promises, those
- // rejections may be reported as unhandled before the control flow
- // attempts to execute the command. To ensure parameters errors
- // propagate through the command itself, we resolve all of the
- // command parameters now, but suppress any errors until the ControlFlow
- // actually executes the command. This addresses scenarios like catching
- // an element not found error in:
- //
- // driver.findElement(By.id('foo')).click().catch(function(e) {
- // if (e instanceof NoSuchElementError) {
- // // Do something.
- // }
- // });
- var prepCommand = toWireValue(command.getParameters());
- prepCommand.catch(function() {});
-
- var flow = this.flow_;
- var executor = this.executor_;
- return flow.execute(function() {
- // A call to WebDriver.quit() may have been scheduled in the same event
- // loop as this |command|, which would prevent us from detecting that the
- // driver has quit above. Therefore, we need to make another quick check.
- // We still check above so we can fail as early as possible.
- checkHasNotQuit();
-
- // Retrieve resolved command parameters; any previously suppressed errors
- // will now propagate up through the control flow as part of the command
- // execution.
- return prepCommand.then(function(parameters) {
- command.setParameters(parameters);
- return executor.execute(command);
- }).then(value => fromWireValue(self, value));
- }, description);
-
- function checkHasNotQuit() {
- if (!self.session_) {
- throw new error.NoSuchSessionError(
- 'This driver instance does not have a valid session ID ' +
- '(did you call WebDriver.quit()?) and may no longer be ' +
- 'used.');
- }
- }
- }
+ schedule(command, description) {}
/**
* Sets the {@linkplain input.FileDetector file detector} that should be
* used with this instance.
* @param {input.FileDetector} detector The detector to use or {@code null}.
*/
- setFileDetector(detector) {
- this.fileDetector_ = detector;
- }
+ setFileDetector(detector) {}
/**
* @return {!command.Executor} The command executor used by this instance.
*/
- getExecutor() {
- return this.executor_;
- }
+ getExecutor() {}
/**
- * @return {!promise.Promise<!Session>} A promise for this client's
- * session.
+ * @return {!promise.Thenable<!Session>} A promise for this client's session.
*/
- getSession() {
- return this.session_;
- }
+ getSession() {}
/**
- * @return {!promise.Promise<!Capabilities>} A promise
- * that will resolve with the this instance's capabilities.
+ * @return {!promise.Thenable<!Capabilities>} A promise that will resolve with
+ * the this instance's capabilities.
*/
- getCapabilities() {
- return this.session_.then(session => session.getCapabilities());
- }
+ getCapabilities() {}
/**
- * Schedules a command to quit the current session. After calling quit, this
- * instance will be invalidated and may no longer be used to issue commands
- * against the browser.
- * @return {!promise.Promise<void>} A promise that will be resolved
- * when the command has completed.
+ * Terminates the browser session. After calling quit, this instance will be
+ * invalidated and may no longer be used to issue commands against the
+ * browser.
+ *
+ * @return {!promise.Thenable<void>} A promise that will be resolved when the
+ * command has completed.
*/
- quit() {
- var result = this.schedule(
- new command.Command(command.Name.QUIT),
- 'WebDriver.quit()');
- // Delete our session ID when the quit command finishes; this will allow us
- // to throw an error when attemnpting to use a driver post-quit.
- return result.finally(() => delete this.session_);
- }
+ quit() {}
/**
* Creates a new action sequence using this driver. The sequence will not be
@@ -498,9 +304,7 @@ class WebDriver {
*
* @return {!actions.ActionSequence} A new action sequence for this instance.
*/
- actions() {
- return new actions.ActionSequence(this);
- }
+ actions() {}
/**
* Creates a new touch sequence using this driver. The sequence will not be
@@ -514,9 +318,7 @@ class WebDriver {
*
* @return {!actions.TouchSequence} A new touch sequence for this instance.
*/
- touchActions() {
- return new actions.TouchSequence(this);
- }
+ touchActions() {}
/**
* Schedules a command to execute JavaScript in the context of the currently
@@ -550,22 +352,11 @@ class WebDriver {
*
* @param {!(string|Function)} script The script to execute.
* @param {...*} var_args The arguments to pass to the script.
- * @return {!promise.Promise<T>} A promise that will resolve to the
+ * @return {!promise.Thenable<T>} A promise that will resolve to the
* scripts return value.
* @template T
*/
- executeScript(script, var_args) {
- if (typeof script === 'function') {
- script = 'return (' + script + ').apply(null, arguments);';
- }
- let args =
- arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : [];
- return this.schedule(
- new command.Command(command.Name.EXECUTE_SCRIPT).
- setParameter('script', script).
- setParameter('args', args),
- 'WebDriver.executeScript()');
- }
+ executeScript(script, var_args) {}
/**
* Schedules a command to execute asynchronous JavaScript in the context of the
@@ -639,45 +430,22 @@ class WebDriver {
*
* @param {!(string|Function)} script The script to execute.
* @param {...*} var_args The arguments to pass to the script.
- * @return {!promise.Promise<T>} A promise that will resolve to the
+ * @return {!promise.Thenable<T>} A promise that will resolve to the
* scripts return value.
* @template T
*/
- executeAsyncScript(script, var_args) {
- if (typeof script === 'function') {
- script = 'return (' + script + ').apply(null, arguments);';
- }
- let args = Array.prototype.slice.call(arguments, 1);
- return this.schedule(
- new command.Command(command.Name.EXECUTE_ASYNC_SCRIPT).
- setParameter('script', script).
- setParameter('args', args),
- 'WebDriver.executeScript()');
- }
+ executeAsyncScript(script, var_args) {}
/**
* Schedules a command to execute a custom function.
- * @param {function(...): (T|promise.Promise<T>)} fn The function to
- * execute.
+ * @param {function(...): (T|IThenable<T>)} fn The function to execute.
* @param {Object=} opt_scope The object in whose scope to execute the function.
* @param {...*} var_args Any arguments to pass to the function.
- * @return {!promise.Promise<T>} A promise that will be resolved'
+ * @return {!promise.Thenable<T>} A promise that will be resolved'
* with the function's result.
* @template T
*/
- call(fn, opt_scope, var_args) {
- let args = Array.prototype.slice.call(arguments, 2);
- let flow = this.flow_;
- return flow.execute(function() {
- return promise.fullyResolved(args).then(function(args) {
- if (promise.isGenerator(fn)) {
- args.unshift(fn, opt_scope);
- return promise.consume.apply(null, args);
- }
- return fn.apply(opt_scope, args);
- });
- }, 'WebDriver.call(' + (fn.name || 'function') + ')');
- }
+ call(fn, opt_scope, var_args) {}
/**
* Schedules a command to wait for a condition to hold. The condition may be
@@ -716,7 +484,7 @@ class WebDriver {
* driver.wait(started, 5 * 1000, 'Server should start within 5 seconds');
* driver.get(getServerUrl());
*
- * @param {!(promise.Promise<T>|
+ * @param {!(IThenable<T>|
* Condition<T>|
* function(!WebDriver): T)} condition The condition to
* wait on, defined as a promise, condition object, or a function to
@@ -724,134 +492,76 @@ class WebDriver {
* @param {number=} opt_timeout How long to wait for the condition to be true.
* @param {string=} opt_message An optional message to use if the wait times
* out.
- * @return {!(promise.Promise<T>|WebElementPromise)} A promise that will be
+ * @return {!(promise.Thenable<T>|WebElementPromise)} A promise that will be
* resolved with the first truthy value returned by the condition
* function, or rejected if the condition times out. If the input
* input condition is an instance of a {@link WebElementCondition},
* the returned value will be a {@link WebElementPromise}.
+ * @throws {TypeError} if the provided `condition` is not a valid type.
* @template T
*/
- wait(condition, opt_timeout, opt_message) {
- if (promise.isPromise(condition)) {
- return this.flow_.wait(
- /** @type {!promise.Promise} */(condition),
- opt_timeout, opt_message);
- }
-
- var message = opt_message;
- var fn = /** @type {!Function} */(condition);
- if (condition instanceof Condition) {
- message = message || condition.description();
- fn = condition.fn;
- }
-
- var driver = this;
- var result = this.flow_.wait(function() {
- if (promise.isGenerator(fn)) {
- return promise.consume(fn, null, [driver]);
- }
- return fn(driver);
- }, opt_timeout, message);
-
- if (condition instanceof WebElementCondition) {
- result = new WebElementPromise(this, result.then(function(value) {
- if (!(value instanceof WebElement)) {
- throw TypeError(
- 'WebElementCondition did not resolve to a WebElement: '
- + Object.prototype.toString.call(value));
- }
- return value;
- }));
- }
- return result;
- }
+ wait(condition, opt_timeout, opt_message) {}
/**
* Schedules a command to make the driver sleep for the given amount of time.
* @param {number} ms The amount of time, in milliseconds, to sleep.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the sleep has finished.
*/
- sleep(ms) {
- return this.flow_.timeout(ms, 'WebDriver.sleep(' + ms + ')');
- }
+ sleep(ms) {}
/**
* Schedules a command to retrieve the current window handle.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the current window handle.
*/
- getWindowHandle() {
- return this.schedule(
- new command.Command(command.Name.GET_CURRENT_WINDOW_HANDLE),
- 'WebDriver.getWindowHandle()');
- }
+ getWindowHandle() {}
/**
* Schedules a command to retrieve the current list of available window handles.
- * @return {!promise.Promise.<!Array<string>>} A promise that will
+ * @return {!promise.Thenable<!Array<string>>} A promise that will
* be resolved with an array of window handles.
*/
- getAllWindowHandles() {
- return this.schedule(
- new command.Command(command.Name.GET_WINDOW_HANDLES),
- 'WebDriver.getAllWindowHandles()');
- }
+ getAllWindowHandles() {}
/**
* Schedules a command to retrieve the current page's source. The page source
* returned is a representation of the underlying DOM: do not expect it to be
* formatted or escaped in the same way as the response sent from the web
* server.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the current page source.
*/
- getPageSource() {
- return this.schedule(
- new command.Command(command.Name.GET_PAGE_SOURCE),
- 'WebDriver.getPageSource()');
- }
+ getPageSource() {}
/**
* Schedules a command to close the current window.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when this command has completed.
*/
- close() {
- return this.schedule(new command.Command(command.Name.CLOSE),
- 'WebDriver.close()');
- }
+ close() {}
/**
* Schedules a command to navigate to the given URL.
* @param {string} url The fully qualified URL to open.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the document has finished loading.
*/
- get(url) {
- return this.navigate().to(url);
- }
+ get(url) {}
/**
* Schedules a command to retrieve the URL of the current page.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the current URL.
*/
- getCurrentUrl() {
- return this.schedule(
- new command.Command(command.Name.GET_CURRENT_URL),
- 'WebDriver.getCurrentUrl()');
- }
+ getCurrentUrl() {}
/**
* Schedules a command to retrieve the current page's title.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the current page's title.
*/
- getTitle() {
- return this.schedule(new command.Command(command.Name.GET_TITLE),
- 'WebDriver.getTitle()');
- }
+ getTitle() {}
/**
* Schedule a command to find an element on the page. If the element cannot be
@@ -859,7 +569,10 @@ class WebDriver {
* by the driver. Unlike other commands, this error cannot be suppressed. In
* other words, scheduling a command to find an element doubles as an assert
* that the element is present on the page. To test whether an element is
- * present on the page, use {@link #isElementPresent} instead.
+ * present on the page, use {@link #findElements}:
+ *
+ * driver.findElements(By.id('foo'))
+ * .then(found => console.log('Element found? %s', !!found.length));
*
* The search criteria for an element may be defined using one of the
* factories in the {@link webdriver.By} namespace, or as a short-hand
@@ -889,6 +602,415 @@ class WebDriver {
* commands against the located element. If the element is not found, the
* element will be invalidated and all scheduled commands aborted.
*/
+ findElement(locator) {}
+
+ /**
+ * Schedule a command to search for multiple elements on the page.
+ *
+ * @param {!(by.By|Function)} locator The locator to use.
+ * @return {!promise.Thenable<!Array<!WebElement>>} A
+ * promise that will resolve to an array of WebElements.
+ */
+ findElements(locator) {}
+
+ /**
+ * Schedule a command to take a screenshot. The driver makes a best effort to
+ * return a screenshot of the following, in order of preference:
+ *
+ * 1. Entire page
+ * 2. Current window
+ * 3. Visible portion of the current frame
+ * 4. The entire display containing the browser
+ *
+ * @return {!promise.Thenable<string>} A promise that will be
+ * resolved to the screenshot as a base-64 encoded PNG.
+ */
+ takeScreenshot() {}
+
+ /**
+ * @return {!Options} The options interface for this instance.
+ */
+ manage() {}
+
+ /**
+ * @return {!Navigation} The navigation interface for this instance.
+ */
+ navigate() {}
+
+ /**
+ * @return {!TargetLocator} The target locator interface for this
+ * instance.
+ */
+ switchTo() {}
+}
+
+
+/**
+ * Each WebDriver instance provides automated control over a browser session.
+ *
+ * @implements {IWebDriver}
+ */
+class WebDriver {
+ /**
+ * @param {!(Session|IThenable<!Session>)} session Either a known session or a
+ * promise that will be resolved to a session.
+ * @param {!command.Executor} executor The executor to use when sending
+ * commands to the browser.
+ * @param {promise.ControlFlow=} opt_flow The flow to
+ * schedule commands through. Defaults to the active flow object.
+ * @param {(function(this: void): ?)=} opt_onQuit A function to call, if any,
+ * when the session is terminated.
+ */
+ constructor(session, executor, opt_flow, opt_onQuit) {
+ /** @private {!promise.ControlFlow} */
+ this.flow_ = opt_flow || promise.controlFlow();
+
+ /** @private {!promise.Thenable<!Session>} */
+ this.session_ = this.flow_.promise(resolve => resolve(session));
+
+ /** @private {!command.Executor} */
+ this.executor_ = executor;
+
+ /** @private {input.FileDetector} */
+ this.fileDetector_ = null;
+
+ /** @private @const {(function(this: void): ?|undefined)} */
+ this.onQuit_ = opt_onQuit;
+ }
+
+ /**
+ * Creates a new WebDriver client for an existing session.
+ * @param {!command.Executor} executor Command executor to use when querying
+ * for session details.
+ * @param {string} sessionId ID of the session to attach to.
+ * @param {promise.ControlFlow=} opt_flow The control flow all
+ * driver commands should execute under. Defaults to the
+ * {@link promise.controlFlow() currently active} control flow.
+ * @return {!WebDriver} A new client for the specified session.
+ */
+ static attachToSession(executor, sessionId, opt_flow) {
+ let flow = opt_flow || promise.controlFlow();
+ let cmd = new command.Command(command.Name.DESCRIBE_SESSION)
+ .setParameter('sessionId', sessionId);
+ let session = flow.execute(
+ () => executeCommand(executor, cmd).catch(err => {
+ // The DESCRIBE_SESSION command is not supported by the W3C spec, so
+ // if we get back an unknown command, just return a session with
+ // unknown capabilities.
+ if (err instanceof error.UnknownCommandError) {
+ return new Session(sessionId, new Capabilities);
+ }
+ throw err;
+ }),
+ 'WebDriver.attachToSession()');
+ return new WebDriver(session, executor, flow);
+ }
+
+ /**
+ * Creates a new WebDriver session.
+ *
+ * By default, the requested session `capabilities` are merely "desired" and
+ * the remote end will still create a new session even if it cannot satisfy
+ * all of the requested capabilities. You can query which capabilities a
+ * session actually has using the
+ * {@linkplain #getCapabilities() getCapabilities()} method on the returned
+ * WebDriver instance.
+ *
+ * To define _required capabilities_, provide the `capabilities` as an object
+ * literal with `required` and `desired` keys. The `desired` key may be
+ * omitted if all capabilities are required, and vice versa. If the server
+ * cannot create a session with all of the required capabilities, it will
+ * return an {@linkplain error.SessionNotCreatedError}.
+ *
+ * let required = new Capabilities().set('browserName', 'firefox');
+ * let desired = new Capabilities().set('version', '45');
+ * let driver = WebDriver.createSession(executor, {required, desired});
+ *
+ * This function will always return a WebDriver instance. If there is an error
+ * creating the session, such as the aforementioned SessionNotCreatedError,
+ * the driver will have a rejected {@linkplain #getSession session} promise.
+ * It is recommended that this promise is left _unhandled_ so it will
+ * propagate through the {@linkplain promise.ControlFlow control flow} and
+ * cause subsequent commands to fail.
+ *
+ * let required = Capabilities.firefox();
+ * let driver = WebDriver.createSession(executor, {required});
+ *
+ * // If the createSession operation failed, then this command will also
+ * // also fail, propagating the creation failure.
+ * driver.get('http://www.google.com').catch(e => console.log(e));
+ *
+ * @param {!command.Executor} executor The executor to create the new session
+ * with.
+ * @param {(!Capabilities|
+ * {desired: (Capabilities|undefined),
+ * required: (Capabilities|undefined)})} capabilities The desired
+ * capabilities for the new session.
+ * @param {promise.ControlFlow=} opt_flow The control flow all driver
+ * commands should execute under, including the initial session creation.
+ * Defaults to the {@link promise.controlFlow() currently active}
+ * control flow.
+ * @param {(function(new: WebDriver,
+ * !IThenable<!Session>,
+ * !command.Executor,
+ * promise.ControlFlow=))=} opt_ctor
+ * A reference to the constructor of the specific type of WebDriver client
+ * to instantiate. Will create a vanilla {@linkplain WebDriver} instance
+ * if a constructor is not provided.
+ * @param {(function(this: void): ?)=} opt_onQuit A callback to invoke when
+ * the newly created session is terminated. This should be used to clean
+ * up any resources associated with the session.
+ * @return {!WebDriver} The driver for the newly created session.
+ */
+ static createSession(
+ executor, capabilities, opt_flow, opt_ctor, opt_onQuit) {
+ let flow = opt_flow || promise.controlFlow();
+ let cmd = new command.Command(command.Name.NEW_SESSION);
+
+ if (capabilities && (capabilities.desired || capabilities.required)) {
+ cmd.setParameter('desiredCapabilities', capabilities.desired);
+ cmd.setParameter('requiredCapabilities', capabilities.required);
+ } else {
+ cmd.setParameter('desiredCapabilities', capabilities);
+ }
+
+ let session = flow.execute(
+ () => executeCommand(executor, cmd),
+ 'WebDriver.createSession()');
+ if (typeof opt_onQuit === 'function') {
+ session = session.catch(err => {
+ return Promise.resolve(opt_onQuit.call(void 0)).then(_ => {throw err});
+ });
+ }
+ const ctor = opt_ctor || WebDriver;
+ return new ctor(session, executor, flow, opt_onQuit);
+ }
+
+ /** @override */
+ controlFlow() {
+ return this.flow_;
+ }
+
+ /** @override */
+ schedule(command, description) {
+ command.setParameter('sessionId', this.session_);
+
+ // If any of the command parameters are rejected promises, those
+ // rejections may be reported as unhandled before the control flow
+ // attempts to execute the command. To ensure parameters errors
+ // propagate through the command itself, we resolve all of the
+ // command parameters now, but suppress any errors until the ControlFlow
+ // actually executes the command. This addresses scenarios like catching
+ // an element not found error in:
+ //
+ // driver.findElement(By.id('foo')).click().catch(function(e) {
+ // if (e instanceof NoSuchElementError) {
+ // // Do something.
+ // }
+ // });
+ var prepCommand = toWireValue(command.getParameters());
+ prepCommand.catch(function() {});
+
+ var flow = this.flow_;
+ var executor = this.executor_;
+ return flow.execute(() => {
+ // Retrieve resolved command parameters; any previously suppressed errors
+ // will now propagate up through the control flow as part of the command
+ // execution.
+ return prepCommand.then(function(parameters) {
+ command.setParameters(parameters);
+ return executor.execute(command);
+ }).then(value => fromWireValue(this, value));
+ }, description);
+ }
+
+ /** @override */
+ setFileDetector(detector) {
+ this.fileDetector_ = detector;
+ }
+
+ /** @override */
+ getExecutor() {
+ return this.executor_;
+ }
+
+ /** @override */
+ getSession() {
+ return this.session_;
+ }
+
+ /** @override */
+ getCapabilities() {
+ return this.session_.then(s => s.getCapabilities());
+ }
+
+ /** @override */
+ quit() {
+ var result = this.schedule(
+ new command.Command(command.Name.QUIT),
+ 'WebDriver.quit()');
+ // Delete our session ID when the quit command finishes; this will allow us
+ // to throw an error when attemnpting to use a driver post-quit.
+ return /** @type {!promise.Thenable} */(promise.finally(result, () => {
+ this.session_ = this.flow_.promise((_, reject) => {
+ reject(new error.NoSuchSessionError(
+ 'This driver instance does not have a valid session ID ' +
+ '(did you call WebDriver.quit()?) and may no longer be used.'));
+ });
+
+ // Only want the session rejection to bubble if accessed.
+ this.session_.catch(function() {});
+
+ if (this.onQuit_) {
+ return this.onQuit_.call(void 0);
+ }
+ }));
+ }
+
+ /** @override */
+ actions() {
+ return new actions.ActionSequence(this);
+ }
+
+ /** @override */
+ touchActions() {
+ return new actions.TouchSequence(this);
+ }
+
+ /** @override */
+ executeScript(script, var_args) {
+ if (typeof script === 'function') {
+ script = 'return (' + script + ').apply(null, arguments);';
+ }
+ let args =
+ arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : [];
+ return this.schedule(
+ new command.Command(command.Name.EXECUTE_SCRIPT).
+ setParameter('script', script).
+ setParameter('args', args),
+ 'WebDriver.executeScript()');
+ }
+
+ /** @override */
+ executeAsyncScript(script, var_args) {
+ if (typeof script === 'function') {
+ script = 'return (' + script + ').apply(null, arguments);';
+ }
+ let args = Array.prototype.slice.call(arguments, 1);
+ return this.schedule(
+ new command.Command(command.Name.EXECUTE_ASYNC_SCRIPT).
+ setParameter('script', script).
+ setParameter('args', args),
+ 'WebDriver.executeScript()');
+ }
+
+ /** @override */
+ call(fn, opt_scope, var_args) {
+ let args = Array.prototype.slice.call(arguments, 2);
+ return this.flow_.execute(function() {
+ return promise.fullyResolved(args).then(function(args) {
+ if (promise.isGenerator(fn)) {
+ args.unshift(fn, opt_scope);
+ return promise.consume.apply(null, args);
+ }
+ return fn.apply(opt_scope, args);
+ });
+ }, 'WebDriver.call(' + (fn.name || 'function') + ')');
+ }
+
+ /** @override */
+ wait(condition, opt_timeout, opt_message) {
+ if (promise.isPromise(condition)) {
+ return this.flow_.wait(
+ /** @type {!IThenable} */(condition),
+ opt_timeout, opt_message);
+ }
+
+ var message = opt_message;
+ var fn = /** @type {!Function} */(condition);
+ if (condition instanceof Condition) {
+ message = message || condition.description();
+ fn = condition.fn;
+ }
+
+ if (typeof fn !== 'function') {
+ throw TypeError(
+ 'Wait condition must be a promise-like object, function, or a '
+ + 'Condition object');
+ }
+
+ var driver = this;
+ var result = this.flow_.wait(function() {
+ if (promise.isGenerator(fn)) {
+ return promise.consume(fn, null, [driver]);
+ }
+ return fn(driver);
+ }, opt_timeout, message);
+
+ if (condition instanceof WebElementCondition) {
+ result = new WebElementPromise(this, result.then(function(value) {
+ if (!(value instanceof WebElement)) {
+ throw TypeError(
+ 'WebElementCondition did not resolve to a WebElement: '
+ + Object.prototype.toString.call(value));
+ }
+ return value;
+ }));
+ }
+ return result;
+ }
+
+ /** @override */
+ sleep(ms) {
+ return this.flow_.timeout(ms, 'WebDriver.sleep(' + ms + ')');
+ }
+
+ /** @override */
+ getWindowHandle() {
+ return this.schedule(
+ new command.Command(command.Name.GET_CURRENT_WINDOW_HANDLE),
+ 'WebDriver.getWindowHandle()');
+ }
+
+ /** @override */
+ getAllWindowHandles() {
+ return this.schedule(
+ new command.Command(command.Name.GET_WINDOW_HANDLES),
+ 'WebDriver.getAllWindowHandles()');
+ }
+
+ /** @override */
+ getPageSource() {
+ return this.schedule(
+ new command.Command(command.Name.GET_PAGE_SOURCE),
+ 'WebDriver.getPageSource()');
+ }
+
+ /** @override */
+ close() {
+ return this.schedule(new command.Command(command.Name.CLOSE),
+ 'WebDriver.close()');
+ }
+
+ /** @override */
+ get(url) {
+ return this.navigate().to(url);
+ }
+
+ /** @override */
+ getCurrentUrl() {
+ return this.schedule(
+ new command.Command(command.Name.GET_CURRENT_URL),
+ 'WebDriver.getCurrentUrl()');
+ }
+
+ /** @override */
+ getTitle() {
+ return this.schedule(new command.Command(command.Name.GET_TITLE),
+ 'WebDriver.getTitle()');
+ }
+
+ /** @override */
findElement(locator) {
let id;
locator = by.checkedLocator(locator);
@@ -907,7 +1029,7 @@ class WebDriver {
* @param {!Function} locatorFn The locator function to use.
* @param {!(WebDriver|WebElement)} context The search
* context.
- * @return {!promise.Promise.<!WebElement>} A
+ * @return {!promise.Thenable<!WebElement>} A
* promise that will resolve to a list of WebElements.
* @private
*/
@@ -923,13 +1045,7 @@ class WebDriver {
});
}
- /**
- * Schedule a command to search for multiple elements on the page.
- *
- * @param {!(by.By|Function)} locator The locator to use.
- * @return {!promise.Promise.<!Array.<!WebElement>>} A
- * promise that will resolve to an array of WebElements.
- */
+ /** @override */
findElements(locator) {
locator = by.checkedLocator(locator);
if (typeof locator === 'function') {
@@ -951,7 +1067,7 @@ class WebDriver {
/**
* @param {!Function} locatorFn The locator function to use.
* @param {!(WebDriver|WebElement)} context The search context.
- * @return {!promise.Promise<!Array<!WebElement>>} A promise that
+ * @return {!promise.Thenable<!Array<!WebElement>>} A promise that
* will resolve to an array of WebElements.
* @private
*/
@@ -971,41 +1087,23 @@ class WebDriver {
});
}
- /**
- * Schedule a command to take a screenshot. The driver makes a best effort to
- * return a screenshot of the following, in order of preference:
- *
- * 1. Entire page
- * 2. Current window
- * 3. Visible portion of the current frame
- * 4. The entire display containing the browser
- *
- * @return {!promise.Promise<string>} A promise that will be
- * resolved to the screenshot as a base-64 encoded PNG.
- */
+ /** @override */
takeScreenshot() {
return this.schedule(new command.Command(command.Name.SCREENSHOT),
'WebDriver.takeScreenshot()');
}
- /**
- * @return {!Options} The options interface for this instance.
- */
+ /** @override */
manage() {
return new Options(this);
}
- /**
- * @return {!Navigation} The navigation interface for this instance.
- */
+ /** @override */
navigate() {
return new Navigation(this);
}
- /**
- * @return {!TargetLocator} The target locator interface for this
- * instance.
- */
+ /** @override */
switchTo() {
return new TargetLocator(this);
}
@@ -1015,7 +1113,7 @@ class WebDriver {
/**
* Interface for navigating back and forth in the browser history.
*
- * This class should never be instantiated directly. Insead, obtain an instance
+ * This class should never be instantiated directly. Instead, obtain an instance
* with
*
* webdriver.navigate()
@@ -1035,7 +1133,7 @@ class Navigation {
/**
* Schedules a command to navigate to a new URL.
* @param {string} url The URL to navigate to.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the URL has been loaded.
*/
to(url) {
@@ -1047,7 +1145,7 @@ class Navigation {
/**
* Schedules a command to move backwards in the browser history.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the navigation event has completed.
*/
back() {
@@ -1058,7 +1156,7 @@ class Navigation {
/**
* Schedules a command to move forwards in the browser history.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the navigation event has completed.
*/
forward() {
@@ -1069,7 +1167,7 @@ class Navigation {
/**
* Schedules a command to refresh the current page.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the navigation event has completed.
*/
refresh() {
@@ -1116,7 +1214,7 @@ class Options {
* });
*
* @param {!Options.Cookie} spec Defines the cookie to add.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the cookie has been added to the page.
* @throws {error.InvalidArgumentError} if any of the cookie parameters are
* invalid.
@@ -1171,7 +1269,7 @@ class Options {
/**
* Schedules a command to delete all cookies visible to the current page.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when all cookies have been deleted.
*/
deleteAllCookies() {
@@ -1185,7 +1283,7 @@ class Options {
* is a no-op if there is no cookie with the given name visible to the current
* page.
* @param {string} name The name of the cookie to delete.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the cookie has been deleted.
*/
deleteCookie(name) {
@@ -1199,7 +1297,7 @@ class Options {
* Schedules a command to retrieve all cookies visible to the current page.
* Each cookie will be returned as a JSON object as described by the WebDriver
* wire protocol.
- * @return {!promise.Promise<!Array<!Options.Cookie>>} A promise that will be
+ * @return {!promise.Thenable<!Array<!Options.Cookie>>} A promise that will be
* resolved with the cookies visible to the current browsing context.
*/
getCookies() {
@@ -1214,7 +1312,7 @@ class Options {
* described by the WebDriver wire protocol.
*
* @param {string} name The name of the cookie to retrieve.
- * @return {!promise.Promise<?Options.Cookie>} A promise that will be resolved
+ * @return {!promise.Thenable<?Options.Cookie>} A promise that will be resolved
* with the named cookie, or `null` if there is no such cookie.
*/
getCookie(name) {
@@ -1365,7 +1463,7 @@ class Timeouts {
* slower location strategies like XPath.
*
* @param {number} ms The amount of time to wait, in milliseconds.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the implicit wait timeout has been set.
*/
implicitlyWait(ms) {
@@ -1378,7 +1476,7 @@ class Timeouts {
* less than or equal to 0, the script will be allowed to run indefinitely.
*
* @param {number} ms The amount of time to wait, in milliseconds.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the script timeout has been set.
*/
setScriptTimeout(ms) {
@@ -1391,7 +1489,7 @@ class Timeouts {
* indefinite.
*
* @param {number} ms The amount of time to wait, in milliseconds.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the timeout has been set.
*/
pageLoadTimeout(ms) {
@@ -1411,7 +1509,7 @@ class Timeouts {
/**
* An interface for managing the current window.
*
- * This class should never be instantiated directly. Insead, obtain an instance
+ * This class should never be instantiated directly. Instead, obtain an instance
* with
*
* webdriver.manage().window()
@@ -1432,7 +1530,7 @@ class Window {
/**
* Retrieves the window's current position, relative to the top left corner of
* the screen.
- * @return {!promise.Promise.<{x: number, y: number}>} A promise
+ * @return {!promise.Thenable<{x: number, y: number}>} A promise
* that will be resolved with the window's position in the form of a
* {x:number, y:number} object literal.
*/
@@ -1449,7 +1547,7 @@ class Window {
* side of the screen.
* @param {number} y The desired vertical position, relative to the top of the
* of the screen.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the command has completed.
*/
setPosition(x, y) {
@@ -1463,7 +1561,7 @@ class Window {
/**
* Retrieves the window's current size.
- * @return {!promise.Promise<{width: number, height: number}>} A
+ * @return {!promise.Thenable<{width: number, height: number}>} A
* promise that will be resolved with the window's size in the form of a
* {width:number, height:number} object literal.
*/
@@ -1478,7 +1576,7 @@ class Window {
* Resizes the current window.
* @param {number} width The desired window width.
* @param {number} height The desired window height.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the command has completed.
*/
setSize(width, height) {
@@ -1492,7 +1590,7 @@ class Window {
/**
* Maximizes the current window.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the command has completed.
*/
maximize() {
@@ -1534,7 +1632,7 @@ class Logs {
* entries since the last call, or from the start of the session.
*
* @param {!logging.Type} type The desired log type.
- * @return {!promise.Promise.<!Array.<!logging.Entry>>} A
+ * @return {!promise.Thenable<!Array.<!logging.Entry>>} A
* promise that will resolve to a list of log entries for the specified
* type.
*/
@@ -1557,7 +1655,7 @@ class Logs {
/**
* Retrieves the log types available to this driver.
- * @return {!promise.Promise<!Array<!logging.Type>>} A
+ * @return {!promise.Thenable<!Array<!logging.Type>>} A
* promise that will resolve to a list of available log types.
*/
getAvailableLogTypes() {
@@ -1604,7 +1702,7 @@ class TargetLocator {
/**
* Schedules a command to switch focus of all future commands to the topmost
* frame on the page.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the driver has changed focus to the default content.
*/
defaultContent() {
@@ -1630,7 +1728,7 @@ class TargetLocator {
* rejected with a {@linkplain error.NoSuchFrameError}.
*
* @param {(number|WebElement|null)} id The frame locator.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the driver has changed focus to the specified frame.
*/
frame(id) {
@@ -1650,7 +1748,7 @@ class TargetLocator {
*
* @param {string} nameOrHandle The name or window handle of the window to
* switch focus to.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the driver has changed focus to the specified window.
*/
window(nameOrHandle) {
@@ -1714,8 +1812,8 @@ class WebElement {
/** @private {!WebDriver} */
this.driver_ = driver;
- /** @private {!promise.Promise<string>} */
- this.id_ = promise.fulfilled(id);
+ /** @private {!promise.Thenable<string>} */
+ this.id_ = driver.controlFlow().promise(resolve => resolve(id));
}
/**
@@ -1762,12 +1860,12 @@ class WebElement {
*
* @param {!WebElement} a A WebElement.
* @param {!WebElement} b A WebElement.
- * @return {!promise.Promise<boolean>} A promise that will be
+ * @return {!promise.Thenable<boolean>} A promise that will be
* resolved to whether the two WebElements are equal.
*/
static equals(a, b) {
if (a === b) {
- return promise.fulfilled(true);
+ return a.driver_.controlFlow().promise(resolve => resolve(true));
}
let ids = [a.getId(), b.getId()];
return promise.all(ids).then(function(ids) {
@@ -1791,7 +1889,7 @@ class WebElement {
}
/**
- * @return {!promise.Promise<string>} A promise that resolves to
+ * @return {!promise.Thenable<string>} A promise that resolves to
* the server-assigned opaque ID assigned to this element.
*/
getId() {
@@ -1812,14 +1910,14 @@ class WebElement {
*
* @param {!command.Command} command The command to schedule.
* @param {string} description A description of the command for debugging.
- * @return {!promise.Promise<T>} A promise that will be resolved
+ * @return {!promise.Thenable<T>} A promise that will be resolved
* with the command result.
* @template T
* @see WebDriver#schedule
* @private
*/
schedule_(command, description) {
- command.setParameter('id', this.getId());
+ command.setParameter('id', this);
return this.driver_.schedule(command, description);
}
@@ -1878,7 +1976,7 @@ class WebElement {
*
* @param {!(by.By|Function)} locator The locator strategy to use when
* searching for the element.
- * @return {!promise.Promise<!Array<!WebElement>>} A
+ * @return {!promise.Thenable<!Array<!WebElement>>} A
* promise that will resolve to an array of WebElements.
*/
findElements(locator) {
@@ -1897,7 +1995,7 @@ class WebElement {
/**
* Schedules a command to click on this element.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the click command has completed.
*/
click() {
@@ -1959,7 +2057,7 @@ class WebElement {
* sequence of keys to type. Number keys may be referenced numerically or
* by string (1 or '1'). All arguments will be joined into a single
* sequence.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when all keys have been typed.
*/
sendKeys(var_args) {
@@ -1993,7 +2091,7 @@ class WebElement {
keys.catch(function() {});
var element = this;
- return this.driver_.flow_.execute(function() {
+ return this.getDriver().controlFlow().execute(function() {
return keys.then(function(keys) {
return element.driver_.fileDetector_
.handleFile(element.driver_, keys.join(''));
@@ -2008,7 +2106,7 @@ class WebElement {
/**
* Schedules a command to query for the tag/node name of this element.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the element's tag name.
*/
getTagName() {
@@ -2029,7 +2127,7 @@ class WebElement {
*
* @param {string} cssStyleProperty The name of the CSS style property to look
* up.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the requested CSS value.
*/
getCssValue(cssStyleProperty) {
@@ -2065,7 +2163,7 @@ class WebElement {
* - "readonly"
*
* @param {string} attributeName The name of the attribute to query.
- * @return {!promise.Promise<?string>} A promise that will be
+ * @return {!promise.Thenable<?string>} A promise that will be
* resolved with the attribute's value. The returned value will always be
* either a string or null.
*/
@@ -2080,7 +2178,7 @@ class WebElement {
* Get the visible (i.e. not hidden by CSS) innerText of this element,
* including sub-elements, without any leading or trailing whitespace.
*
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved with the element's visible text.
*/
getText() {
@@ -2092,7 +2190,7 @@ class WebElement {
/**
* Schedules a command to compute the size of this element's bounding box, in
* pixels.
- * @return {!promise.Promise.<{width: number, height: number}>} A
+ * @return {!promise.Thenable<{width: number, height: number}>} A
* promise that will be resolved with the element's size as a
* {@code {width:number, height:number}} object.
*/
@@ -2104,7 +2202,7 @@ class WebElement {
/**
* Schedules a command to compute the location of this element in page space.
- * @return {!promise.Promise.<{x: number, y: number}>} A promise that
+ * @return {!promise.Thenable<{x: number, y: number}>} A promise that
* will be resolved to the element's location as a
* {@code {x:number, y:number}} object.
*/
@@ -2117,7 +2215,7 @@ class WebElement {
/**
* Schedules a command to query whether the DOM element represented by this
* instance is enabled, as dicted by the {@code disabled} attribute.
- * @return {!promise.Promise<boolean>} A promise that will be
+ * @return {!promise.Thenable<boolean>} A promise that will be
* resolved with whether this element is currently enabled.
*/
isEnabled() {
@@ -2128,7 +2226,7 @@ class WebElement {
/**
* Schedules a command to query whether this element is selected.
- * @return {!promise.Promise<boolean>} A promise that will be
+ * @return {!promise.Thenable<boolean>} A promise that will be
* resolved with whether this element is currently selected.
*/
isSelected() {
@@ -2141,7 +2239,7 @@ class WebElement {
* Schedules a command to submit the form containing this element (or this
* element if it is a FORM element). This command is a no-op if the element is
* not contained in a form.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the form has been submitted.
*/
submit() {
@@ -2154,7 +2252,7 @@ class WebElement {
* Schedules a command to clear the `value` of this element. This command has
* no effect if the underlying DOM element is neither a text INPUT element
* nor a TEXTAREA element.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when the element has been cleared.
*/
clear() {
@@ -2165,7 +2263,7 @@ class WebElement {
/**
* Schedules a command to test whether this element is currently displayed.
- * @return {!promise.Promise<boolean>} A promise that will be
+ * @return {!promise.Thenable<boolean>} A promise that will be
* resolved with whether this element is currently visible on the page.
*/
isDisplayed() {
@@ -2181,7 +2279,7 @@ class WebElement {
* @param {boolean=} opt_scroll Optional argument that indicates whether the
* element should be scrolled into view before taking a screenshot.
* Defaults to false.
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved to the screenshot as a base-64 encoded PNG.
*/
takeScreenshot(opt_scroll) {
@@ -2206,24 +2304,30 @@ class WebElement {
* return el.click();
* });
*
- * @implements {promise.Thenable<!WebElement>}
+ * @implements {promise.CancellableThenable<!WebElement>}
* @final
*/
class WebElementPromise extends WebElement {
/**
* @param {!WebDriver} driver The parent WebDriver instance for this
* element.
- * @param {!promise.Promise<!WebElement>} el A promise
+ * @param {!promise.Thenable<!WebElement>} el A promise
* that will resolve to the promised element.
*/
constructor(driver, el) {
super(driver, 'unused');
- /** @override */
- this.cancel = el.cancel.bind(el);
-
- /** @override */
- this.isPending = el.isPending.bind(el);
+ /**
+ * Cancel operation is only supported if the wrapped thenable is also
+ * cancellable.
+ * @param {(string|Error)=} opt_reason
+ * @override
+ */
+ this.cancel = function(opt_reason) {
+ if (promise.CancellableThenable.isImplementation(el)) {
+ /** @type {!promise.CancellableThenable} */(el).cancel(opt_reason);
+ }
+ }
/** @override */
this.then = el.then.bind(el);
@@ -2231,9 +2335,6 @@ class WebElementPromise extends WebElement {
/** @override */
this.catch = el.catch.bind(el);
- /** @override */
- this.finally = el.finally.bind(el);
-
/**
* Defers returning the element ID until the wrapped WebElement has been
* resolved.
@@ -2246,7 +2347,7 @@ class WebElementPromise extends WebElement {
};
}
}
-promise.Thenable.addImplementation(WebElementPromise);
+promise.CancellableThenable.addImplementation(WebElementPromise);
//////////////////////////////////////////////////////////////////////////////
@@ -2272,15 +2373,15 @@ class Alert {
/** @private {!WebDriver} */
this.driver_ = driver;
- /** @private {!promise.Promise<string>} */
- this.text_ = promise.fulfilled(text);
+ /** @private {!promise.Thenable<string>} */
+ this.text_ = driver.controlFlow().promise(resolve => resolve(text));
}
/**
* Retrieves the message text displayed with this alert. For instance, if the
* alert were opened with alert("hello"), then this would return "hello".
*
- * @return {!promise.Promise<string>} A promise that will be
+ * @return {!promise.Thenable<string>} A promise that will be
* resolved to the text displayed with this alert.
*/
getText() {
@@ -2294,7 +2395,7 @@ class Alert {
*
* @param {string} username The username to send.
* @param {string} password The password to send.
- * @return {!promise.Promise<void>} A promise that will be resolved when this
+ * @return {!promise.Thenable<void>} A promise that will be resolved when this
* command has completed.
*/
authenticateAs(username, password) {
@@ -2307,7 +2408,7 @@ class Alert {
/**
* Accepts this alert.
*
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when this command has completed.
*/
accept() {
@@ -2319,7 +2420,7 @@ class Alert {
/**
* Dismisses this alert.
*
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when this command has completed.
*/
dismiss() {
@@ -2334,7 +2435,7 @@ class Alert {
* window.confirm).
*
* @param {string} text The text to set.
- * @return {!promise.Promise<void>} A promise that will be resolved
+ * @return {!promise.Thenable<void>} A promise that will be resolved
* when this command has completed.
*/
sendKeys(text) {
@@ -2357,7 +2458,7 @@ class Alert {
* return alert.dismiss();
* });
*
- * @implements {promise.Thenable.<!webdriver.Alert>}
+ * @implements {promise.CancellableThenable<!webdriver.Alert>}
* @final
*/
class AlertPromise extends Alert {
@@ -2370,11 +2471,17 @@ class AlertPromise extends Alert {
constructor(driver, alert) {
super(driver, 'unused');
- /** @override */
- this.cancel = alert.cancel.bind(alert);
-
- /** @override */
- this.isPending = alert.isPending.bind(alert);
+ /**
+ * Cancel operation is only supported if the wrapped thenable is also
+ * cancellable.
+ * @param {(string|Error)=} opt_reason
+ * @override
+ */
+ this.cancel = function(opt_reason) {
+ if (promise.CancellableThenable.isImplementation(alert)) {
+ /** @type {!promise.CancellableThenable} */(alert).cancel(opt_reason);
+ }
+ };
/** @override */
this.then = alert.then.bind(alert);
@@ -2382,9 +2489,6 @@ class AlertPromise extends Alert {
/** @override */
this.catch = alert.catch.bind(alert);
- /** @override */
- this.finally = alert.finally.bind(alert);
-
/**
* Defer returning text until the promised alert has been resolved.
* @override
@@ -2436,7 +2540,7 @@ class AlertPromise extends Alert {
};
}
}
-promise.Thenable.addImplementation(AlertPromise);
+promise.CancellableThenable.addImplementation(AlertPromise);
// PUBLIC API
@@ -2451,6 +2555,7 @@ module.exports = {
Options: Options,
TargetLocator: TargetLocator,
Timeouts: Timeouts,
+ IWebDriver: IWebDriver,
WebDriver: WebDriver,
WebElement: WebElement,
WebElementCondition: WebElementCondition,