| // 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' |
| |
| /** |
| * @fileoverview Defines types related to user input with the WebDriver API. |
| */ |
| const { Command, Name } = require('./command') |
| const { InvalidArgumentError } = require('./error') |
| |
| /** |
| * Enumeration of the buttons used in the advanced interactions API. |
| * @enum {number} |
| */ |
| const Button = { |
| LEFT: 0, |
| MIDDLE: 1, |
| RIGHT: 2, |
| BACK: 3, |
| FORWARD: 4, |
| } |
| |
| /** |
| * Representations of pressable keys that aren't text. These are stored in |
| * the Unicode PUA (Private Use Area) code points, 0xE000-0xF8FF. Refer to |
| * http://www.google.com.au/search?&q=unicode+pua&btnK=Search |
| * |
| * @enum {string} |
| * @see <https://www.w3.org/TR/webdriver/#keyboard-actions> |
| */ |
| const Key = { |
| NULL: '\uE000', |
| CANCEL: '\uE001', // ^break |
| HELP: '\uE002', |
| BACK_SPACE: '\uE003', |
| TAB: '\uE004', |
| CLEAR: '\uE005', |
| RETURN: '\uE006', |
| ENTER: '\uE007', |
| SHIFT: '\uE008', |
| CONTROL: '\uE009', |
| ALT: '\uE00A', |
| PAUSE: '\uE00B', |
| ESCAPE: '\uE00C', |
| SPACE: '\uE00D', |
| PAGE_UP: '\uE00E', |
| PAGE_DOWN: '\uE00F', |
| END: '\uE010', |
| HOME: '\uE011', |
| ARROW_LEFT: '\uE012', |
| LEFT: '\uE012', |
| ARROW_UP: '\uE013', |
| UP: '\uE013', |
| ARROW_RIGHT: '\uE014', |
| RIGHT: '\uE014', |
| ARROW_DOWN: '\uE015', |
| DOWN: '\uE015', |
| INSERT: '\uE016', |
| DELETE: '\uE017', |
| SEMICOLON: '\uE018', |
| EQUALS: '\uE019', |
| |
| NUMPAD0: '\uE01A', // number pad keys |
| NUMPAD1: '\uE01B', |
| NUMPAD2: '\uE01C', |
| NUMPAD3: '\uE01D', |
| NUMPAD4: '\uE01E', |
| NUMPAD5: '\uE01F', |
| NUMPAD6: '\uE020', |
| NUMPAD7: '\uE021', |
| NUMPAD8: '\uE022', |
| NUMPAD9: '\uE023', |
| MULTIPLY: '\uE024', |
| ADD: '\uE025', |
| SEPARATOR: '\uE026', |
| SUBTRACT: '\uE027', |
| DECIMAL: '\uE028', |
| DIVIDE: '\uE029', |
| |
| F1: '\uE031', // function keys |
| F2: '\uE032', |
| F3: '\uE033', |
| F4: '\uE034', |
| F5: '\uE035', |
| F6: '\uE036', |
| F7: '\uE037', |
| F8: '\uE038', |
| F9: '\uE039', |
| F10: '\uE03A', |
| F11: '\uE03B', |
| F12: '\uE03C', |
| |
| COMMAND: '\uE03D', // Apple command key |
| META: '\uE03D', // alias for Windows key |
| |
| /** |
| * Japanese modifier key for switching between full- and half-width |
| * characters. |
| * @see <https://en.wikipedia.org/wiki/Language_input_keys> |
| */ |
| ZENKAKU_HANKAKU: '\uE040', |
| } |
| |
| /** |
| * Simulate pressing many keys at once in a "chord". Takes a sequence of |
| * {@linkplain Key keys} or strings, appends each of the values to a string, |
| * adds the chord termination key ({@link Key.NULL}) and returns the resulting |
| * string. |
| * |
| * Note: when the low-level webdriver key handlers see Keys.NULL, active |
| * modifier keys (CTRL/ALT/SHIFT/etc) release via a keyup event. |
| * |
| * @param {...string} keys The key sequence to concatenate. |
| * @return {string} The null-terminated key sequence. |
| */ |
| Key.chord = function (...keys) { |
| return keys.join('') + Key.NULL |
| } |
| |
| /** |
| * Used with {@link ./webelement.WebElement#sendKeys WebElement#sendKeys} on |
| * file input elements (`<input type="file">`) to detect when the entered key |
| * sequence defines the path to a file. |
| * |
| * By default, {@linkplain ./webelement.WebElement WebElement's} will enter all |
| * key sequences exactly as entered. You may set a |
| * {@linkplain ./webdriver.WebDriver#setFileDetector file detector} on the |
| * parent WebDriver instance to define custom behavior for handling file |
| * elements. Of particular note is the |
| * {@link selenium-webdriver/remote.FileDetector}, which should be used when |
| * running against a remote |
| * [Selenium Server](https://selenium.dev/downloads/). |
| */ |
| class FileDetector { |
| /** |
| * Handles the file specified by the given path, preparing it for use with |
| * the current browser. If the path does not refer to a valid file, it will |
| * be returned unchanged, otherwise a path suitable for use with the current |
| * browser will be returned. |
| * |
| * This default implementation is a no-op. Subtypes may override this function |
| * for custom tailored file handling. |
| * |
| * @param {!./webdriver.WebDriver} driver The driver for the current browser. |
| * @param {string} path The path to process. |
| * @return {!Promise<string>} A promise for the processed file path. |
| * @package |
| */ |
| handleFile(_driver, path) { |
| // eslint-disable-line |
| return Promise.resolve(path) |
| } |
| } |
| |
| /** |
| * Generic description of a single action to send to the remote end. |
| * |
| * @record |
| * @package |
| */ |
| class Action { |
| constructor() { |
| /** @type {!Action.Type} */ |
| this.type |
| /** @type {(number|undefined)} */ |
| this.duration |
| /** @type {(string|undefined)} */ |
| this.value |
| /** @type {(Button|undefined)} */ |
| this.button |
| /** @type {(number|undefined)} */ |
| this.x |
| /** @type {(number|undefined)} */ |
| this.y |
| } |
| } |
| |
| /** |
| * @enum {string} |
| * @package |
| * @see <https://w3c.github.io/webdriver/webdriver-spec.html#terminology-0> |
| */ |
| Action.Type = { |
| KEY_DOWN: 'keyDown', |
| KEY_UP: 'keyUp', |
| PAUSE: 'pause', |
| POINTER_DOWN: 'pointerDown', |
| POINTER_UP: 'pointerUp', |
| POINTER_MOVE: 'pointerMove', |
| POINTER_CANCEL: 'pointerCancel', |
| SCROLL: 'scroll', |
| } |
| |
| /** |
| * Represents a user input device. |
| * |
| * @abstract |
| */ |
| class Device { |
| /** |
| * @param {Device.Type} type the input type. |
| * @param {string} id a unique ID for this device. |
| */ |
| constructor(type, id) { |
| /** @private @const */ this.type_ = type |
| /** @private @const */ this.id_ = id |
| } |
| |
| /** @return {!Object} the JSON encoding for this device. */ |
| toJSON() { |
| return { type: this.type_, id: this.id_ } |
| } |
| } |
| |
| /** |
| * Device types supported by the WebDriver protocol. |
| * |
| * @enum {string} |
| * @see <https://w3c.github.io/webdriver/webdriver-spec.html#input-source-state> |
| */ |
| Device.Type = { |
| KEY: 'key', |
| NONE: 'none', |
| POINTER: 'pointer', |
| WHEEL: 'wheel', |
| } |
| |
| /** |
| * @param {(string|Key|number)} key |
| * @return {string} |
| * @throws {!(InvalidArgumentError|RangeError)} |
| */ |
| function checkCodePoint(key) { |
| if (typeof key === 'number') { |
| return String.fromCodePoint(key) |
| } |
| |
| if (typeof key !== 'string') { |
| throw new InvalidArgumentError(`key is not a string: ${key}`) |
| } |
| |
| key = key.normalize() |
| if (Array.from(key).length !== 1) { |
| throw new InvalidArgumentError(`key input is not a single code point: ${key}`) |
| } |
| return key |
| } |
| |
| /** |
| * Keyboard input device. |
| * |
| * @final |
| * @see <https://www.w3.org/TR/webdriver/#dfn-key-input-source> |
| */ |
| class Keyboard extends Device { |
| /** @param {string} id the device ID. */ |
| constructor(id) { |
| super(Device.Type.KEY, id) |
| } |
| |
| /** |
| * Generates a key down action. |
| * |
| * @param {(Key|string|number)} key the key to press. This key may be |
| * specified as a {@link Key} value, a specific unicode code point, |
| * or a string containing a single unicode code point. |
| * @return {!Action} a new key down action. |
| * @package |
| */ |
| keyDown(key) { |
| return { type: Action.Type.KEY_DOWN, value: checkCodePoint(key) } |
| } |
| |
| /** |
| * Generates a key up action. |
| * |
| * @param {(Key|string|number)} key the key to press. This key may be |
| * specified as a {@link Key} value, a specific unicode code point, |
| * or a string containing a single unicode code point. |
| * @return {!Action} a new key up action. |
| * @package |
| */ |
| keyUp(key) { |
| return { type: Action.Type.KEY_UP, value: checkCodePoint(key) } |
| } |
| } |
| |
| /** |
| * Defines the reference point from which to compute offsets for |
| * {@linkplain ./input.Pointer#move pointer move} actions. |
| * |
| * @enum {string} |
| */ |
| const Origin = { |
| /** Compute offsets relative to the pointer's current position. */ |
| POINTER: 'pointer', |
| /** Compute offsets relative to the viewport. */ |
| VIEWPORT: 'viewport', |
| } |
| |
| /** |
| * Pointer input device. |
| * |
| * @final |
| * @see <https://www.w3.org/TR/webdriver/#dfn-pointer-input-source> |
| */ |
| class Pointer extends Device { |
| /** |
| * @param {string} id the device ID. |
| * @param {Pointer.Type} type the pointer type. |
| */ |
| constructor(id, type) { |
| super(Device.Type.POINTER, id) |
| /** @private @const */ this.pointerType_ = type |
| } |
| |
| /** @override */ |
| toJSON() { |
| return Object.assign({ parameters: { pointerType: this.pointerType_ } }, super.toJSON()) |
| } |
| |
| /** |
| * @return {!Action} An action that cancels this pointer's current input. |
| * @package |
| */ |
| cancel() { |
| return { type: Action.Type.POINTER_CANCEL } |
| } |
| |
| /** |
| * @param {!Button=} button The button to press. |
| * @param width |
| * @param height |
| * @param pressure |
| * @param tangentialPressure |
| * @param tiltX |
| * @param tiltY |
| * @param twist |
| * @param altitudeAngle |
| * @param azimuthAngle |
| * @return {!Action} An action to press the specified button with this device. |
| * @package |
| */ |
| press( |
| button = Button.LEFT, |
| width = 0, |
| height = 0, |
| pressure = 0, |
| tangentialPressure = 0, |
| tiltX = 0, |
| tiltY = 0, |
| twist = 0, |
| altitudeAngle = 0, |
| azimuthAngle = 0, |
| ) { |
| return { |
| type: Action.Type.POINTER_DOWN, |
| button, |
| width, |
| height, |
| pressure, |
| tangentialPressure, |
| tiltX, |
| tiltY, |
| twist, |
| altitudeAngle, |
| azimuthAngle, |
| } |
| } |
| |
| /** |
| * @param {!Button=} button The button to release. |
| * @return {!Action} An action to release the specified button with this |
| * device. |
| * @package |
| */ |
| release(button = Button.LEFT) { |
| return { type: Action.Type.POINTER_UP, button } |
| } |
| |
| /** |
| * Creates an action for moving the pointer `x` and `y` pixels from the |
| * specified `origin`. The `origin` may be defined as the pointer's |
| * {@linkplain Origin.POINTER current position}, the |
| * {@linkplain Origin.VIEWPORT viewport}, or the center of a specific |
| * {@linkplain ./webdriver.WebElement WebElement}. |
| * |
| * @param {{ |
| * x: (number|undefined), |
| * y: (number|undefined), |
| * duration: (number|undefined), |
| * origin: (!Origin|!./webdriver.WebElement|undefined), |
| * }=} options the move options. |
| * @return {!Action} The new action. |
| * @package |
| */ |
| move({ |
| x = 0, |
| y = 0, |
| duration = 100, |
| origin = Origin.VIEWPORT, |
| width = 0, |
| height = 0, |
| pressure = 0, |
| tangentialPressure = 0, |
| tiltX = 0, |
| tiltY = 0, |
| twist = 0, |
| altitudeAngle = 0, |
| azimuthAngle = 0, |
| }) { |
| return { |
| type: Action.Type.POINTER_MOVE, |
| origin, |
| duration, |
| x, |
| y, |
| width, |
| height, |
| pressure, |
| tangentialPressure, |
| tiltX, |
| tiltY, |
| twist, |
| altitudeAngle, |
| azimuthAngle, |
| } |
| } |
| } |
| |
| /** |
| * The supported types of pointers. |
| * @enum {string} |
| */ |
| Pointer.Type = { |
| MOUSE: 'mouse', |
| PEN: 'pen', |
| TOUCH: 'touch', |
| } |
| |
| class Wheel extends Device { |
| /** |
| * @param {string} id the device ID.. |
| */ |
| constructor(id) { |
| super(Device.Type.WHEEL, id) |
| } |
| |
| /** |
| * Scrolls a page via the coordinates given |
| * @param {number} x starting x coordinate |
| * @param {number} y starting y coordinate |
| * @param {number} deltaX Delta X to scroll to target |
| * @param {number} deltaY Delta Y to scroll to target |
| * @param {WebElement} origin element origin |
| * @param {number} duration duration ratio be the ratio of time delta and duration |
| * @returns {!Action} An action to scroll with this device. |
| */ |
| scroll(x, y, deltaX, deltaY, origin, duration) { |
| return { |
| type: Action.Type.SCROLL, |
| duration: duration, |
| x: x, |
| y: y, |
| deltaX: deltaX, |
| deltaY: deltaY, |
| origin: origin, |
| } |
| } |
| } |
| |
| /** |
| * User facing API for generating complex user gestures. This class should not |
| * be instantiated directly. Instead, users should create new instances by |
| * calling {@link ./webdriver.WebDriver#actions WebDriver.actions()}. |
| * |
| * ### Action Ticks |
| * |
| * Action sequences are divided into a series of "ticks". At each tick, the |
| * WebDriver remote end will perform a single action for each device included |
| * in the action sequence. At tick 0, the driver will perform the first action |
| * defined for each device, at tick 1 the second action for each device, and |
| * so on until all actions have been executed. If an individual device does |
| * not have an action defined at a particular tick, it will automatically |
| * pause. |
| * |
| * By default, action sequences will be synchronized so only one device has a |
| * define action in each tick. Consider the following code sample: |
| * |
| * const actions = driver.actions(); |
| * |
| * await actions |
| * .keyDown(SHIFT) |
| * .move({origin: el}) |
| * .press() |
| * .release() |
| * .keyUp(SHIFT) |
| * .perform(); |
| * |
| * This sample produces the following sequence of ticks: |
| * |
| * | Device | Tick 1 | Tick 2 | Tick 3 | Tick 4 | Tick 5 | |
| * | -------- | -------------- | ------------------ | ------- | --------- | ------------ | |
| * | Keyboard | keyDown(SHIFT) | pause() | pause() | pause() | keyUp(SHIFT) | |
| * | Mouse | pause() | move({origin: el}) | press() | release() | pause() | |
| * |
| * If you'd like the remote end to execute actions with multiple devices |
| * simultaneously, you may pass `{async: true}` when creating the actions |
| * builder. With synchronization disabled (`{async: true}`), the ticks from our |
| * previous example become: |
| * |
| * | Device | Tick 1 | Tick 2 | Tick 3 | |
| * | -------- | ------------------ | ------------ | --------- | |
| * | Keyboard | keyDown(SHIFT) | keyUp(SHIFT) | | |
| * | Mouse | move({origin: el}) | press() | release() | |
| * |
| * When synchronization is disabled, it is your responsibility to insert |
| * {@linkplain #pause() pauses} for each device, as needed: |
| * |
| * const actions = driver.actions({async: true}); |
| * const kb = actions.keyboard(); |
| * const mouse = actions.mouse(); |
| * |
| * actions.keyDown(SHIFT).pause(kb).pause(kb).key(SHIFT); |
| * actions.pause(mouse).move({origin: el}).press().release(); |
| * actions.perform(); |
| * |
| * With pauses insert for individual devices, we're back to: |
| * |
| * | Device | Tick 1 | Tick 2 | Tick 3 | Tick 4 | |
| * | -------- | -------------- | ------------------ | ------- | ------------ | |
| * | Keyboard | keyDown(SHIFT) | pause() | pause() | keyUp(SHIFT) | |
| * | Mouse | pause() | move({origin: el}) | press() | release() | |
| * |
| * #### Tick Durations |
| * |
| * The length of each action tick is however long it takes the remote end to |
| * execute the actions for every device in that tick. Most actions are |
| * "instantaneous", however, {@linkplain #pause pause} and |
| * {@linkplain #move pointer move} actions allow you to specify a duration for |
| * how long that action should take. The remote end will always wait for all |
| * actions within a tick to finish before starting the next tick, so a device |
| * may implicitly pause while waiting for other devices to finish. |
| * |
| * | Device | Tick 1 | Tick 2 | |
| * | --------- | --------------------- | ------- | |
| * | Pointer 1 | move({duration: 200}) | press() | |
| * | Pointer 2 | move({duration: 300}) | press() | |
| * |
| * In table above, the move for Pointer 1 should only take 200 ms, but the |
| * remote end will wait for the move for Pointer 2 to finish |
| * (an additional 100 ms) before proceeding to Tick 2. |
| * |
| * This implicit waiting also applies to pauses. In the table below, even though |
| * the keyboard only defines a pause of 100 ms, the remote end will wait an |
| * additional 200 ms for the mouse move to finish before moving to Tick 2. |
| * |
| * | Device | Tick 1 | Tick 2 | |
| * | -------- | --------------------- | -------------- | |
| * | Keyboard | pause(100) | keyDown(SHIFT) | |
| * | Mouse | move({duration: 300}) | | |
| * |
| * [client rect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getClientRects |
| * [bounding client rect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect |
| * |
| * @final |
| * @see <https://www.w3.org/TR/webdriver/#actions> |
| */ |
| class Actions { |
| /** |
| * @param {!Executor} executor The object to execute the configured |
| * actions with. |
| * @param {{async: (boolean|undefined)}} options Options for this action |
| * sequence (see class description for details). |
| */ |
| constructor(executor, { async = false } = {}) { |
| /** @private @const */ |
| this.executor_ = executor |
| |
| /** @private @const */ |
| this.sync_ = !async |
| |
| /** @private @const */ |
| this.keyboard_ = new Keyboard('default keyboard') |
| |
| /** @private @const */ |
| this.mouse_ = new Pointer('default mouse', Pointer.Type.MOUSE) |
| |
| /** @private @const */ |
| this.wheel_ = new Wheel('default wheel') |
| |
| /** @private @const {!Map<!Device, !Array<!Action>>} */ |
| this.sequences_ = new Map([ |
| [this.keyboard_, []], |
| [this.mouse_, []], |
| [this.wheel_, []], |
| ]) |
| } |
| |
| /** @return {!Keyboard} the keyboard device handle. */ |
| keyboard() { |
| return this.keyboard_ |
| } |
| |
| /** @return {!Pointer} the mouse pointer device handle. */ |
| mouse() { |
| return this.mouse_ |
| } |
| |
| /** @return {!Wheel} the wheel device handle. */ |
| wheel() { |
| return this.wheel_ |
| } |
| |
| /** |
| * @param {!Device} device |
| * @return {!Array<!Action>} |
| * @private |
| */ |
| sequence_(device) { |
| let sequence = this.sequences_.get(device) |
| if (!sequence) { |
| sequence = [] |
| this.sequences_.set(device, sequence) |
| } |
| return sequence |
| } |
| |
| /** |
| * Appends `actions` to the end of the current sequence for the given |
| * `device`. If device synchronization is enabled, after inserting the |
| * actions, pauses will be inserted for all other devices to ensure all action |
| * sequences are the same length. |
| * |
| * @param {!Device} device the device to update. |
| * @param {...!Action} actions the actions to insert. |
| * @return {!Actions} a self reference. |
| */ |
| insert(device, ...actions) { |
| this.sequence_(device).push(...actions) |
| return this.sync_ ? this.synchronize() : this |
| } |
| |
| /** |
| * Ensures the action sequence for every device referenced in this action |
| * sequence is the same length. For devices whose sequence is too short, |
| * this will insert {@linkplain #pause pauses} so that every device has an |
| * explicit action defined at each tick. |
| * |
| * @param {...!Device} devices The specific devices to synchronize. |
| * If unspecified, the action sequences for every device will be |
| * synchronized. |
| * @return {!Actions} a self reference. |
| */ |
| synchronize(...devices) { |
| let sequences |
| let max = 0 |
| if (devices.length === 0) { |
| for (const s of this.sequences_.values()) { |
| max = Math.max(max, s.length) |
| } |
| sequences = this.sequences_.values() |
| } else { |
| sequences = [] |
| for (const device of devices) { |
| const seq = this.sequence_(device) |
| max = Math.max(max, seq.length) |
| sequences.push(seq) |
| } |
| } |
| |
| const pause = { type: Action.Type.PAUSE, duration: 0 } |
| for (const seq of sequences) { |
| while (seq.length < max) { |
| seq.push(pause) |
| } |
| } |
| |
| return this |
| } |
| |
| /** |
| * Inserts a pause action for the specified devices, ensuring each device is |
| * idle for a tick. The length of the pause (in milliseconds) may be specified |
| * as the first parameter to this method (defaults to 0). Otherwise, you may |
| * just specify the individual devices that should pause. |
| * |
| * If no devices are specified, a pause action will be created (using the same |
| * duration) for every device. |
| * |
| * When device synchronization is enabled (the default for new {@link Actions} |
| * objects), there is no need to specify devices as pausing one automatically |
| * pauses the others for the same duration. In other words, the following are |
| * all equivalent: |
| * |
| * let a1 = driver.actions(); |
| * a1.pause(100).perform(); |
| * |
| * let a2 = driver.actions(); |
| * a2.pause(100, a2.keyboard()).perform(); |
| * // Synchronization ensures a2.mouse() is automatically paused too. |
| * |
| * let a3 = driver.actions(); |
| * a3.pause(100, a3.keyboard(), a3.mouse()).perform(); |
| * |
| * When device synchronization is _disabled_, you can cause individual devices |
| * to pause during a tick. For example, to hold the SHIFT key down while |
| * moving the mouse: |
| * |
| * let actions = driver.actions({async: true}); |
| * |
| * actions.keyDown(Key.SHIFT); |
| * actions.pause(actions.mouse()) // Pause for shift down |
| * .press(Button.LEFT) |
| * .move({x: 10, y: 10}) |
| * .release(Button.LEFT); |
| * actions |
| * .pause( |
| * actions.keyboard(), // Pause for press left |
| * actions.keyboard(), // Pause for move |
| * actions.keyboard()) // Pause for release left |
| * .keyUp(Key.SHIFT); |
| * await actions.perform(); |
| * |
| * @param {(number|!Device)=} duration The length of the pause to insert, in |
| * milliseconds. Alternatively, the duration may be omitted (yielding a |
| * default 0 ms pause), and the first device to pause may be specified. |
| * @param {...!Device} devices The devices to insert the pause for. If no |
| * devices are specified, the pause will be inserted for _all_ devices. |
| * @return {!Actions} a self reference. |
| */ |
| pause(duration, ...devices) { |
| if (duration instanceof Device) { |
| devices.push(duration) |
| duration = 0 |
| } else if (!duration) { |
| duration = 0 |
| } |
| |
| const action = { type: Action.Type.PAUSE, duration } |
| |
| // NB: need a properly typed variable for type checking. |
| /** @type {!Iterable<!Device>} */ |
| const iterable = devices.length === 0 ? this.sequences_.keys() : devices |
| for (const device of iterable) { |
| this.sequence_(device).push(action) |
| } |
| return this.sync_ ? this.synchronize() : this |
| } |
| |
| /** |
| * Inserts an action to press a single key. |
| * |
| * @param {(Key|string|number)} key the key to press. This key may be |
| * specified as a {@link Key} value, a specific unicode code point, |
| * or a string containing a single unicode code point. |
| * @return {!Actions} a self reference. |
| */ |
| keyDown(key) { |
| return this.insert(this.keyboard_, this.keyboard_.keyDown(key)) |
| } |
| |
| /** |
| * Inserts an action to release a single key. |
| * |
| * @param {(Key|string|number)} key the key to release. This key may be |
| * specified as a {@link Key} value, a specific unicode code point, |
| * or a string containing a single unicode code point. |
| * @return {!Actions} a self reference. |
| */ |
| keyUp(key) { |
| return this.insert(this.keyboard_, this.keyboard_.keyUp(key)) |
| } |
| |
| /** |
| * Inserts a sequence of actions to type the provided key sequence. |
| * For each key, this will record a pair of {@linkplain #keyDown keyDown} |
| * and {@linkplain #keyUp keyUp} actions. An implication of this pairing |
| * is that modifier keys (e.g. {@link ./input.Key.SHIFT Key.SHIFT}) will |
| * always be immediately released. In other words, `sendKeys(Key.SHIFT, 'a')` |
| * is the same as typing `sendKeys('a')`, _not_ `sendKeys('A')`. |
| * |
| * @param {...(Key|string|number)} keys the keys to type. |
| * @return {!Actions} a self reference. |
| */ |
| sendKeys(...keys) { |
| const { WebElement } = require('./webdriver') |
| |
| const actions = [] |
| if (keys.length > 1 && keys[0] instanceof WebElement) { |
| this.click(keys[0]) |
| keys.shift() |
| } |
| for (const key of keys) { |
| if (typeof key === 'string') { |
| for (const symbol of key) { |
| actions.push(this.keyboard_.keyDown(symbol), this.keyboard_.keyUp(symbol)) |
| } |
| } else { |
| actions.push(this.keyboard_.keyDown(key), this.keyboard_.keyUp(key)) |
| } |
| } |
| return this.insert(this.keyboard_, ...actions) |
| } |
| |
| /** |
| * Inserts an action to press a mouse button at the mouse's current location. |
| * |
| * @param {!Button=} button The button to press; defaults to `LEFT`. |
| * @return {!Actions} a self reference. |
| */ |
| press(button = Button.LEFT) { |
| return this.insert(this.mouse_, this.mouse_.press(button)) |
| } |
| |
| /** |
| * Inserts an action to release a mouse button at the mouse's current |
| * location. |
| * |
| * @param {!Button=} button The button to release; defaults to `LEFT`. |
| * @return {!Actions} a self reference. |
| */ |
| release(button = Button.LEFT) { |
| return this.insert(this.mouse_, this.mouse_.release(button)) |
| } |
| |
| /** |
| * scrolls a page via the coordinates given |
| * @param {number} x starting x coordinate |
| * @param {number} y starting y coordinate |
| * @param {number} deltax delta x to scroll to target |
| * @param {number} deltay delta y to scroll to target |
| * @param {number} duration duration ratio be the ratio of time delta and duration |
| * @returns {!Actions} An action to scroll with this device. |
| */ |
| scroll(x, y, targetDeltaX, targetDeltaY, origin, duration) { |
| return this.insert(this.wheel_, this.wheel_.scroll(x, y, targetDeltaX, targetDeltaY, origin, duration)) |
| } |
| |
| /** |
| * Inserts an action for moving the mouse `x` and `y` pixels relative to the |
| * specified `origin`. The `origin` may be defined as the mouse's |
| * {@linkplain ./input.Origin.POINTER current position}, the top-left corner of the |
| * {@linkplain ./input.Origin.VIEWPORT viewport}, or the center of a specific |
| * {@linkplain ./webdriver.WebElement WebElement}. Default is top left corner of the view-port if origin is not specified |
| * |
| * You may adjust how long the remote end should take, in milliseconds, to |
| * perform the move using the `duration` parameter (defaults to 100 ms). |
| * The number of incremental move events generated over this duration is an |
| * implementation detail for the remote end. |
| * |
| * @param {{ |
| * x: (number|undefined), |
| * y: (number|undefined), |
| * duration: (number|undefined), |
| * origin: (!Origin|!./webdriver.WebElement|undefined), |
| * }=} options The move options. Defaults to moving the mouse to the top-left |
| * corner of the viewport over 100ms. |
| * @return {!Actions} a self reference. |
| */ |
| move({ x = 0, y = 0, duration = 100, origin = Origin.VIEWPORT } = {}) { |
| return this.insert(this.mouse_, this.mouse_.move({ x, y, duration, origin })) |
| } |
| |
| /** |
| * Short-hand for performing a simple left-click (down/up) with the mouse. |
| * |
| * @param {./webdriver.WebElement=} element If specified, the mouse will |
| * first be moved to the center of the element before performing the |
| * click. |
| * @return {!Actions} a self reference. |
| */ |
| click(element) { |
| if (element) { |
| this.move({ origin: element }) |
| } |
| return this.press().release() |
| } |
| |
| /** |
| * Short-hand for performing a simple right-click (down/up) with the mouse. |
| * |
| * @param {./webdriver.WebElement=} element If specified, the mouse will |
| * first be moved to the center of the element before performing the |
| * click. |
| * @return {!Actions} a self reference. |
| */ |
| contextClick(element) { |
| if (element) { |
| this.move({ origin: element }) |
| } |
| return this.press(Button.RIGHT).release(Button.RIGHT) |
| } |
| |
| /** |
| * Short-hand for performing a double left-click with the mouse. |
| * |
| * @param {./webdriver.WebElement=} element If specified, the mouse will |
| * first be moved to the center of the element before performing the |
| * click. |
| * @return {!Actions} a self reference. |
| */ |
| doubleClick(element) { |
| return this.click(element).press().release() |
| } |
| |
| /** |
| * Configures a drag-and-drop action consisting of the following steps: |
| * |
| * 1. Move to the center of the `from` element (element to be dragged). |
| * 2. Press the left mouse button. |
| * 3. If the `to` target is a {@linkplain ./webdriver.WebElement WebElement}, |
| * move the mouse to its center. Otherwise, move the mouse by the |
| * specified offset. |
| * 4. Release the left mouse button. |
| * |
| * @param {!./webdriver.WebElement} from The element to press the left mouse |
| * button on to start the drag. |
| * @param {(!./webdriver.WebElement|{x: number, y: number})} to Either another |
| * element to drag to (will drag to the center of the element), or an |
| * object specifying the offset to drag by, in pixels. |
| * @return {!Actions} a self reference. |
| */ |
| dragAndDrop(from, to) { |
| // Do not require up top to avoid a cycle that breaks static analysis. |
| const { WebElement } = require('./webdriver') |
| if (!(to instanceof WebElement) && (!to || typeof to.x !== 'number' || typeof to.y !== 'number')) { |
| throw new InvalidArgumentError('Invalid drag target; must specify a WebElement or {x, y} offset') |
| } |
| |
| this.move({ origin: from }).press() |
| if (to instanceof WebElement) { |
| this.move({ origin: to }) |
| } else { |
| this.move({ x: to.x, y: to.y, origin: Origin.POINTER }) |
| } |
| return this.release() |
| } |
| |
| /** |
| * Releases all keys, pointers, and clears internal state. |
| * |
| * @return {!Promise<void>} a promise that will resolve when finished |
| * clearing all action state. |
| */ |
| clear() { |
| for (const s of this.sequences_.values()) { |
| s.length = 0 |
| } |
| return this.executor_.execute(new Command(Name.CLEAR_ACTIONS)) |
| } |
| |
| /** |
| * Performs the configured action sequence. |
| * |
| * @return {!Promise<void>} a promise that will resolve when all actions have |
| * been completed. |
| */ |
| async perform() { |
| const _actions = [] |
| this.sequences_.forEach((actions, device) => { |
| if (!isIdle(actions)) { |
| actions = actions.concat() // Defensive copy. |
| _actions.push(Object.assign({ actions }, device.toJSON())) |
| } |
| }) |
| |
| if (_actions.length === 0) { |
| return Promise.resolve() |
| } |
| |
| await this.executor_.execute(new Command(Name.ACTIONS).setParameter('actions', _actions)) |
| } |
| |
| getSequences() { |
| const _actions = [] |
| this.sequences_.forEach((actions, device) => { |
| if (!isIdle(actions)) { |
| actions = actions.concat() |
| _actions.push(Object.assign({ actions }, device.toJSON())) |
| } |
| }) |
| |
| return _actions |
| } |
| } |
| |
| /** |
| * @param {!Array<!Action>} actions |
| * @return {boolean} |
| */ |
| function isIdle(actions) { |
| return actions.length === 0 || actions.every((a) => a.type === Action.Type.PAUSE && !a.duration) |
| } |
| |
| /** |
| * Script used to compute the offset from the center of a DOM element's first |
| * client rect from the top-left corner of the element's bounding client rect. |
| * The element's center point is computed using the algorithm defined here: |
| * <https://w3c.github.io/webdriver/webdriver-spec.html#dfn-center-point>. |
| * |
| * __This is only exported for use in internal unit tests. DO NOT USE.__ |
| * |
| * @package |
| */ |
| const INTERNAL_COMPUTE_OFFSET_SCRIPT = ` |
| function computeOffset(el) { |
| var rect = el.getClientRects()[0]; |
| var left = Math.max(0, Math.min(rect.x, rect.x + rect.width)); |
| var right = |
| Math.min(window.innerWidth, Math.max(rect.x, rect.x + rect.width)); |
| var top = Math.max(0, Math.min(rect.y, rect.y + rect.height)); |
| var bot = |
| Math.min(window.innerHeight, Math.max(rect.y, rect.y + rect.height)); |
| var x = Math.floor(0.5 * (left + right)); |
| var y = Math.floor(0.5 * (top + bot)); |
| |
| var bbox = el.getBoundingClientRect(); |
| return [x - bbox.left, y - bbox.top]; |
| } |
| return computeOffset(arguments[0]);` |
| |
| // PUBLIC API |
| |
| module.exports = { |
| Action, // For documentation only. |
| Actions, |
| Button, |
| Device, |
| Key, |
| Keyboard, |
| FileDetector, |
| Origin, |
| Pointer, |
| INTERNAL_COMPUTE_OFFSET_SCRIPT, |
| } |