diff options
Diffstat (limited to 'node_modules/selenium-webdriver/lib/webdriver.js')
-rw-r--r-- | node_modules/selenium-webdriver/lib/webdriver.js | 1005 |
1 files changed, 555 insertions, 450 deletions
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, |