diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-11-16 01:59:39 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-11-16 02:00:31 +0100 |
commit | bd65bb67e25a79b019d745b7262b2008ce2adb15 (patch) | |
tree | 89e1b032103a63737f1a703e6a943832ef261704 /node_modules/selenium-webdriver/lib | |
parent | f91466595b651721690133f58ab37f977539e95b (diff) |
incrementally verify denoms
The denominations are not stored in a separate object store.
Diffstat (limited to 'node_modules/selenium-webdriver/lib')
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 Binary files differindex 916e530f3..248c32db5 100644 --- a/node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.so +++ b/node_modules/selenium-webdriver/lib/firefox/amd64/libnoblur64.so diff --git a/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so b/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so Binary files differindex 8e7db8de3..004062c7b 100644 --- a/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so +++ b/node_modules/selenium-webdriver/lib/firefox/i386/libnoblur.so diff --git a/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi b/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi Binary files differindex 39aca6b62..f9a51cf4f 100644 --- a/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi +++ b/node_modules/selenium-webdriver/lib/firefox/webdriver.xpi 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,"&"));-1!=a.indexOf("<")&&(a=a.replace(oa,"<"));-1!=a.indexOf(">")&&(a=a.replace(pa,">"));-1!=a.indexOf('"')&&(a=a.replace(qa,"""));-1!=a.indexOf("'")&&(a=a.replace(ra,"'"));-1!=a.indexOf("\x00")&&(a=a.replace(sa,"�"));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,"  ").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, |