| // 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'; |
| |
| const command = require('./command'); |
| const error = require('./error'); |
| const input = require('./input'); |
| |
| |
| /** |
| * @param {!IArrayLike} args . |
| * @return {!Array} . |
| */ |
| function flatten(args) { |
| let result = []; |
| for (let i = 0; i < args.length; i++) { |
| let element = args[i]; |
| if (Array.isArray(element)) { |
| result.push.apply(result, flatten(element)); |
| } else { |
| result.push(element); |
| } |
| } |
| return result; |
| } |
| |
| |
| const MODIFIER_KEYS = new Set([ |
| input.Key.ALT, |
| input.Key.CONTROL, |
| input.Key.SHIFT, |
| input.Key.COMMAND |
| ]); |
| |
| |
| /** |
| * Checks that a key is a modifier key. |
| * @param {!input.Key} key The key to check. |
| * @throws {error.InvalidArgumentError} If the key is not a modifier key. |
| * @private |
| */ |
| function checkModifierKey(key) { |
| if (!MODIFIER_KEYS.has(key)) { |
| throw new error.InvalidArgumentError('Not a modifier key'); |
| } |
| } |
| |
| |
| /** |
| * Class for defining sequences of complex user interactions. Each sequence |
| * will not be executed until {@link #perform} is called. |
| * |
| * This class should not be instantiated directly. Instead, obtain an instance |
| * using {@link ./webdriver.WebDriver#actions() WebDriver.actions()}. |
| * |
| * Sample usage: |
| * |
| * driver.actions(). |
| * keyDown(Key.SHIFT). |
| * click(element1). |
| * click(element2). |
| * dragAndDrop(element3, element4). |
| * keyUp(Key.SHIFT). |
| * perform(); |
| * |
| */ |
| class ActionSequence { |
| /** |
| * @param {!./webdriver.WebDriver} driver The driver that should be used to |
| * perform this action sequence. |
| */ |
| constructor(driver) { |
| /** @private {!./webdriver.WebDriver} */ |
| this.driver_ = driver; |
| |
| /** @private {!Array<{description: string, command: !command.Command}>} */ |
| this.actions_ = []; |
| } |
| |
| /** |
| * Schedules an action to be executed each time {@link #perform} is called on |
| * this instance. |
| * |
| * @param {string} description A description of the command. |
| * @param {!command.Command} command The command. |
| * @private |
| */ |
| schedule_(description, command) { |
| this.actions_.push({ |
| description: description, |
| command: command |
| }); |
| } |
| |
| /** |
| * Executes this action sequence. |
| * |
| * @return {!./promise.Thenable} A promise that will be resolved once |
| * this sequence has completed. |
| */ |
| perform() { |
| // Make a protected copy of the scheduled actions. This will protect against |
| // users defining additional commands before this sequence is actually |
| // executed. |
| let actions = this.actions_.concat(); |
| let driver = this.driver_; |
| return driver.controlFlow().execute(function() { |
| let results = actions.map(action => { |
| return driver.schedule(action.command, action.description); |
| }); |
| return Promise.all(results); |
| }, 'ActionSequence.perform'); |
| } |
| |
| /** |
| * Moves the mouse. The location to move to may be specified in terms of the |
| * mouse's current location, an offset relative to the top-left corner of an |
| * element, or an element (in which case the middle of the element is used). |
| * |
| * @param {(!./webdriver.WebElement|{x: number, y: number})} location The |
| * location to drag to, as either another WebElement or an offset in |
| * pixels. |
| * @param {{x: number, y: number}=} opt_offset If the target {@code location} |
| * is defined as a {@link ./webdriver.WebElement}, this parameter defines |
| * an offset within that element. The offset should be specified in pixels |
| * relative to the top-left corner of the element's bounding box. If |
| * omitted, the element's center will be used as the target offset. |
| * @return {!ActionSequence} A self reference. |
| */ |
| mouseMove(location, opt_offset) { |
| let cmd = new command.Command(command.Name.MOVE_TO); |
| |
| if (typeof location.x === 'number') { |
| setOffset(/** @type {{x: number, y: number}} */(location)); |
| } else { |
| cmd.setParameter('element', location.getId()); |
| if (opt_offset) { |
| setOffset(opt_offset); |
| } |
| } |
| |
| this.schedule_('mouseMove', cmd); |
| return this; |
| |
| /** @param {{x: number, y: number}} offset The offset to use. */ |
| function setOffset(offset) { |
| cmd.setParameter('xoffset', offset.x || 0); |
| cmd.setParameter('yoffset', offset.y || 0); |
| } |
| } |
| |
| /** |
| * Schedules a mouse action. |
| * @param {string} description A simple descriptive label for the scheduled |
| * action. |
| * @param {!command.Name} commandName The name of the command. |
| * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either |
| * the element to interact with or the button to click with. |
| * Defaults to {@link input.Button.LEFT} if neither an element nor |
| * button is specified. |
| * @param {input.Button=} opt_button The button to use. Defaults to |
| * {@link input.Button.LEFT}. Ignored if the previous argument is |
| * provided as a button. |
| * @return {!ActionSequence} A self reference. |
| * @private |
| */ |
| scheduleMouseAction_( |
| description, commandName, opt_elementOrButton, opt_button) { |
| let button; |
| if (typeof opt_elementOrButton === 'number') { |
| button = opt_elementOrButton; |
| } else { |
| if (opt_elementOrButton) { |
| this.mouseMove( |
| /** @type {!./webdriver.WebElement} */ (opt_elementOrButton)); |
| } |
| button = opt_button !== void(0) ? opt_button : input.Button.LEFT; |
| } |
| |
| let cmd = new command.Command(commandName). |
| setParameter('button', button); |
| this.schedule_(description, cmd); |
| return this; |
| } |
| |
| /** |
| * Presses a mouse button. The mouse button will not be released until |
| * {@link #mouseUp} is called, regardless of whether that call is made in this |
| * sequence or another. The behavior for out-of-order events (e.g. mouseDown, |
| * click) is undefined. |
| * |
| * If an element is provided, the mouse will first be moved to the center |
| * of that element. This is equivalent to: |
| * |
| * sequence.mouseMove(element).mouseDown() |
| * |
| * Warning: this method currently only supports the left mouse button. See |
| * [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047). |
| * |
| * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either |
| * the element to interact with or the button to click with. |
| * Defaults to {@link input.Button.LEFT} if neither an element nor |
| * button is specified. |
| * @param {input.Button=} opt_button The button to use. Defaults to |
| * {@link input.Button.LEFT}. Ignored if a button is provided as the |
| * first argument. |
| * @return {!ActionSequence} A self reference. |
| */ |
| mouseDown(opt_elementOrButton, opt_button) { |
| return this.scheduleMouseAction_('mouseDown', |
| command.Name.MOUSE_DOWN, opt_elementOrButton, opt_button); |
| } |
| |
| /** |
| * Releases a mouse button. Behavior is undefined for calling this function |
| * without a previous call to {@link #mouseDown}. |
| * |
| * If an element is provided, the mouse will first be moved to the center |
| * of that element. This is equivalent to: |
| * |
| * sequence.mouseMove(element).mouseUp() |
| * |
| * Warning: this method currently only supports the left mouse button. See |
| * [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047). |
| * |
| * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either |
| * the element to interact with or the button to click with. |
| * Defaults to {@link input.Button.LEFT} if neither an element nor |
| * button is specified. |
| * @param {input.Button=} opt_button The button to use. Defaults to |
| * {@link input.Button.LEFT}. Ignored if a button is provided as the |
| * first argument. |
| * @return {!ActionSequence} A self reference. |
| */ |
| mouseUp(opt_elementOrButton, opt_button) { |
| return this.scheduleMouseAction_('mouseUp', |
| command.Name.MOUSE_UP, opt_elementOrButton, opt_button); |
| } |
| |
| /** |
| * Convenience function for performing a "drag and drop" manuever. The target |
| * element may be moved to the location of another element, or by an offset (in |
| * pixels). |
| * |
| * @param {!./webdriver.WebElement} element The element to drag. |
| * @param {(!./webdriver.WebElement|{x: number, y: number})} location The |
| * location to drag to, either as another WebElement or an offset in |
| * pixels. |
| * @return {!ActionSequence} A self reference. |
| */ |
| dragAndDrop(element, location) { |
| return this.mouseDown(element).mouseMove(location).mouseUp(); |
| } |
| |
| /** |
| * Clicks a mouse button. |
| * |
| * If an element is provided, the mouse will first be moved to the center |
| * of that element. This is equivalent to: |
| * |
| * sequence.mouseMove(element).click() |
| * |
| * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either |
| * the element to interact with or the button to click with. |
| * Defaults to {@link input.Button.LEFT} if neither an element nor |
| * button is specified. |
| * @param {input.Button=} opt_button The button to use. Defaults to |
| * {@link input.Button.LEFT}. Ignored if a button is provided as the |
| * first argument. |
| * @return {!ActionSequence} A self reference. |
| */ |
| click(opt_elementOrButton, opt_button) { |
| return this.scheduleMouseAction_('click', |
| command.Name.CLICK, opt_elementOrButton, opt_button); |
| } |
| |
| /** |
| * Double-clicks a mouse button. |
| * |
| * If an element is provided, the mouse will first be moved to the center of |
| * that element. This is equivalent to: |
| * |
| * sequence.mouseMove(element).doubleClick() |
| * |
| * Warning: this method currently only supports the left mouse button. See |
| * [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047). |
| * |
| * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either |
| * the element to interact with or the button to click with. |
| * Defaults to {@link input.Button.LEFT} if neither an element nor |
| * button is specified. |
| * @param {input.Button=} opt_button The button to use. Defaults to |
| * {@link input.Button.LEFT}. Ignored if a button is provided as the |
| * first argument. |
| * @return {!ActionSequence} A self reference. |
| */ |
| doubleClick(opt_elementOrButton, opt_button) { |
| return this.scheduleMouseAction_('doubleClick', |
| command.Name.DOUBLE_CLICK, opt_elementOrButton, opt_button); |
| } |
| |
| /** |
| * Schedules a keyboard action. |
| * |
| * @param {string} description A simple descriptive label for the scheduled |
| * action. |
| * @param {!Array<(string|!input.Key)>} keys The keys to send. |
| * @return {!ActionSequence} A self reference. |
| * @private |
| */ |
| scheduleKeyboardAction_(description, keys) { |
| let cmd = new command.Command(command.Name.SEND_KEYS_TO_ACTIVE_ELEMENT) |
| .setParameter('value', keys); |
| this.schedule_(description, cmd); |
| return this; |
| } |
| |
| /** |
| * Performs a modifier key press. The modifier key is <em>not released</em> |
| * until {@link #keyUp} or {@link #sendKeys} is called. The key press will be |
| * targeted at the currently focused element. |
| * |
| * @param {!input.Key} key The modifier key to push. Must be one of |
| * {ALT, CONTROL, SHIFT, COMMAND, META}. |
| * @return {!ActionSequence} A self reference. |
| * @throws {error.InvalidArgumentError} If the key is not a valid modifier |
| * key. |
| */ |
| keyDown(key) { |
| checkModifierKey(key); |
| return this.scheduleKeyboardAction_('keyDown', [key]); |
| } |
| |
| /** |
| * Performs a modifier key release. The release is targeted at the currently |
| * focused element. |
| * @param {!input.Key} key The modifier key to release. Must be one of |
| * {ALT, CONTROL, SHIFT, COMMAND, META}. |
| * @return {!ActionSequence} A self reference. |
| * @throws {error.InvalidArgumentError} If the key is not a valid modifier |
| * key. |
| */ |
| keyUp(key) { |
| checkModifierKey(key); |
| return this.scheduleKeyboardAction_('keyUp', [key]); |
| } |
| |
| /** |
| * Simulates typing multiple keys. Each modifier key encountered in the |
| * sequence will not be released until it is encountered again. All key events |
| * will be targeted at the currently focused element. |
| * |
| * @param {...(string|!input.Key|!Array<(string|!input.Key)>)} var_args |
| * The keys to type. |
| * @return {!ActionSequence} A self reference. |
| * @throws {Error} If the key is not a valid modifier key. |
| */ |
| sendKeys(var_args) { |
| let keys = flatten(arguments); |
| return this.scheduleKeyboardAction_('sendKeys', keys); |
| } |
| } |
| |
| |
| /** |
| * Class for defining sequences of user touch interactions. Each sequence |
| * will not be executed until {@link #perform} is called. |
| * |
| * This class should not be instantiated directly. Instead, obtain an instance |
| * using {@link ./webdriver.WebDriver#touchActions() WebDriver.touchActions()}. |
| * |
| * Sample usage: |
| * |
| * driver.touchActions(). |
| * tapAndHold({x: 0, y: 0}). |
| * move({x: 3, y: 4}). |
| * release({x: 10, y: 10}). |
| * perform(); |
| * |
| */ |
| class TouchSequence { |
| /** |
| * @param {!./webdriver.WebDriver} driver The driver that should be used to |
| * perform this action sequence. |
| */ |
| constructor(driver) { |
| /** @private {!./webdriver.WebDriver} */ |
| this.driver_ = driver; |
| |
| /** @private {!Array<{description: string, command: !command.Command}>} */ |
| this.actions_ = []; |
| } |
| |
| /** |
| * Schedules an action to be executed each time {@link #perform} is called on |
| * this instance. |
| * @param {string} description A description of the command. |
| * @param {!command.Command} command The command. |
| * @private |
| */ |
| schedule_(description, command) { |
| this.actions_.push({ |
| description: description, |
| command: command |
| }); |
| } |
| |
| /** |
| * Executes this action sequence. |
| * @return {!./promise.Thenable} A promise that will be resolved once |
| * this sequence has completed. |
| */ |
| perform() { |
| // Make a protected copy of the scheduled actions. This will protect against |
| // users defining additional commands before this sequence is actually |
| // executed. |
| let actions = this.actions_.concat(); |
| let driver = this.driver_; |
| return driver.controlFlow().execute(function() { |
| let results = actions.map(action => { |
| return driver.schedule(action.command, action.description); |
| }); |
| return Promise.all(results); |
| }, 'TouchSequence.perform'); |
| } |
| |
| /** |
| * Taps an element. |
| * |
| * @param {!./webdriver.WebElement} elem The element to tap. |
| * @return {!TouchSequence} A self reference. |
| */ |
| tap(elem) { |
| let cmd = new command.Command(command.Name.TOUCH_SINGLE_TAP). |
| setParameter('element', elem.getId()); |
| |
| this.schedule_('tap', cmd); |
| return this; |
| } |
| |
| /** |
| * Double taps an element. |
| * |
| * @param {!./webdriver.WebElement} elem The element to double tap. |
| * @return {!TouchSequence} A self reference. |
| */ |
| doubleTap(elem) { |
| let cmd = new command.Command(command.Name.TOUCH_DOUBLE_TAP). |
| setParameter('element', elem.getId()); |
| |
| this.schedule_('doubleTap', cmd); |
| return this; |
| } |
| |
| /** |
| * Long press on an element. |
| * |
| * @param {!./webdriver.WebElement} elem The element to long press. |
| * @return {!TouchSequence} A self reference. |
| */ |
| longPress(elem) { |
| let cmd = new command.Command(command.Name.TOUCH_LONG_PRESS). |
| setParameter('element', elem.getId()); |
| |
| this.schedule_('longPress', cmd); |
| return this; |
| } |
| |
| /** |
| * Touch down at the given location. |
| * |
| * @param {{x: number, y: number}} location The location to touch down at. |
| * @return {!TouchSequence} A self reference. |
| */ |
| tapAndHold(location) { |
| let cmd = new command.Command(command.Name.TOUCH_DOWN). |
| setParameter('x', location.x). |
| setParameter('y', location.y); |
| |
| this.schedule_('tapAndHold', cmd); |
| return this; |
| } |
| |
| /** |
| * Move a held {@linkplain #tapAndHold touch} to the specified location. |
| * |
| * @param {{x: number, y: number}} location The location to move to. |
| * @return {!TouchSequence} A self reference. |
| */ |
| move(location) { |
| let cmd = new command.Command(command.Name.TOUCH_MOVE). |
| setParameter('x', location.x). |
| setParameter('y', location.y); |
| |
| this.schedule_('move', cmd); |
| return this; |
| } |
| |
| /** |
| * Release a held {@linkplain #tapAndHold touch} at the specified location. |
| * |
| * @param {{x: number, y: number}} location The location to release at. |
| * @return {!TouchSequence} A self reference. |
| */ |
| release(location) { |
| let cmd = new command.Command(command.Name.TOUCH_UP). |
| setParameter('x', location.x). |
| setParameter('y', location.y); |
| |
| this.schedule_('release', cmd); |
| return this; |
| } |
| |
| /** |
| * Scrolls the touch screen by the given offset. |
| * |
| * @param {{x: number, y: number}} offset The offset to scroll to. |
| * @return {!TouchSequence} A self reference. |
| */ |
| scroll(offset) { |
| let cmd = new command.Command(command.Name.TOUCH_SCROLL). |
| setParameter('xoffset', offset.x). |
| setParameter('yoffset', offset.y); |
| |
| this.schedule_('scroll', cmd); |
| return this; |
| } |
| |
| /** |
| * Scrolls the touch screen, starting on `elem` and moving by the specified |
| * offset. |
| * |
| * @param {!./webdriver.WebElement} elem The element where scroll starts. |
| * @param {{x: number, y: number}} offset The offset to scroll to. |
| * @return {!TouchSequence} A self reference. |
| */ |
| scrollFromElement(elem, offset) { |
| let cmd = new command.Command(command.Name.TOUCH_SCROLL). |
| setParameter('element', elem.getId()). |
| setParameter('xoffset', offset.x). |
| setParameter('yoffset', offset.y); |
| |
| this.schedule_('scrollFromElement', cmd); |
| return this; |
| } |
| |
| /** |
| * Flick, starting anywhere on the screen, at speed xspeed and yspeed. |
| * |
| * @param {{xspeed: number, yspeed: number}} speed The speed to flick in each |
| direction, in pixels per second. |
| * @return {!TouchSequence} A self reference. |
| */ |
| flick(speed) { |
| let cmd = new command.Command(command.Name.TOUCH_FLICK). |
| setParameter('xspeed', speed.xspeed). |
| setParameter('yspeed', speed.yspeed); |
| |
| this.schedule_('flick', cmd); |
| return this; |
| } |
| |
| /** |
| * Flick starting at elem and moving by x and y at specified speed. |
| * |
| * @param {!./webdriver.WebElement} elem The element where flick starts. |
| * @param {{x: number, y: number}} offset The offset to flick to. |
| * @param {number} speed The speed to flick at in pixels per second. |
| * @return {!TouchSequence} A self reference. |
| */ |
| flickElement(elem, offset, speed) { |
| let cmd = new command.Command(command.Name.TOUCH_FLICK). |
| setParameter('element', elem.getId()). |
| setParameter('xoffset', offset.x). |
| setParameter('yoffset', offset.y). |
| setParameter('speed', speed); |
| |
| this.schedule_('flickElement', cmd); |
| return this; |
| } |
| } |
| |
| |
| // PUBLIC API |
| |
| module.exports = { |
| ActionSequence: ActionSequence, |
| TouchSequence: TouchSequence, |
| }; |