Path: blob/trunk/javascript/selenium-webdriver/lib/input.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617'use strict'1819/**20* @fileoverview Defines types related to user input with the WebDriver API.21*/22const { Command, Name } = require('./command')23const { InvalidArgumentError } = require('./error')2425/**26* Enumeration of the buttons used in the advanced interactions API.27* @enum {number}28*/29const Button = {30LEFT: 0,31MIDDLE: 1,32RIGHT: 2,33BACK: 3,34FORWARD: 4,35}3637/**38* Representations of pressable keys that aren't text. These are stored in39* the Unicode PUA (Private Use Area) code points, 0xE000-0xF8FF. Refer to40* http://www.google.com.au/search?&q=unicode+pua&btnK=Search41*42* @enum {string}43* @see <https://www.w3.org/TR/webdriver/#keyboard-actions>44*/45const Key = {46NULL: '\uE000',47CANCEL: '\uE001', // ^break48HELP: '\uE002',49BACK_SPACE: '\uE003',50TAB: '\uE004',51CLEAR: '\uE005',52RETURN: '\uE006',53ENTER: '\uE007',54SHIFT: '\uE008',55CONTROL: '\uE009',56ALT: '\uE00A',57PAUSE: '\uE00B',58ESCAPE: '\uE00C',59SPACE: '\uE00D',60PAGE_UP: '\uE00E',61PAGE_DOWN: '\uE00F',62END: '\uE010',63HOME: '\uE011',64ARROW_LEFT: '\uE012',65LEFT: '\uE012',66ARROW_UP: '\uE013',67UP: '\uE013',68ARROW_RIGHT: '\uE014',69RIGHT: '\uE014',70ARROW_DOWN: '\uE015',71DOWN: '\uE015',72INSERT: '\uE016',73DELETE: '\uE017',74SEMICOLON: '\uE018',75EQUALS: '\uE019',7677NUMPAD0: '\uE01A', // number pad keys78NUMPAD1: '\uE01B',79NUMPAD2: '\uE01C',80NUMPAD3: '\uE01D',81NUMPAD4: '\uE01E',82NUMPAD5: '\uE01F',83NUMPAD6: '\uE020',84NUMPAD7: '\uE021',85NUMPAD8: '\uE022',86NUMPAD9: '\uE023',87MULTIPLY: '\uE024',88ADD: '\uE025',89SEPARATOR: '\uE026',90SUBTRACT: '\uE027',91DECIMAL: '\uE028',92DIVIDE: '\uE029',9394F1: '\uE031', // function keys95F2: '\uE032',96F3: '\uE033',97F4: '\uE034',98F5: '\uE035',99F6: '\uE036',100F7: '\uE037',101F8: '\uE038',102F9: '\uE039',103F10: '\uE03A',104F11: '\uE03B',105F12: '\uE03C',106107COMMAND: '\uE03D', // Apple command key108META: '\uE03D', // alias for Windows key109110/**111* Japanese modifier key for switching between full- and half-width112* characters.113* @see <https://en.wikipedia.org/wiki/Language_input_keys>114*/115ZENKAKU_HANKAKU: '\uE040',116}117118/**119* Simulate pressing many keys at once in a "chord". Takes a sequence of120* {@linkplain Key keys} or strings, appends each of the values to a string,121* adds the chord termination key ({@link Key.NULL}) and returns the resulting122* string.123*124* Note: when the low-level webdriver key handlers see Keys.NULL, active125* modifier keys (CTRL/ALT/SHIFT/etc) release via a keyup event.126*127* @param {...string} keys The key sequence to concatenate.128* @return {string} The null-terminated key sequence.129*/130Key.chord = function (...keys) {131return keys.join('') + Key.NULL132}133134/**135* Used with {@link ./webelement.WebElement#sendKeys WebElement#sendKeys} on136* file input elements (`<input type="file">`) to detect when the entered key137* sequence defines the path to a file.138*139* By default, {@linkplain ./webelement.WebElement WebElement's} will enter all140* key sequences exactly as entered. You may set a141* {@linkplain ./webdriver.WebDriver#setFileDetector file detector} on the142* parent WebDriver instance to define custom behavior for handling file143* elements. Of particular note is the144* {@link selenium-webdriver/remote.FileDetector}, which should be used when145* running against a remote146* [Selenium Server](https://selenium.dev/downloads/).147*/148class FileDetector {149/**150* Handles the file specified by the given path, preparing it for use with151* the current browser. If the path does not refer to a valid file, it will152* be returned unchanged, otherwise a path suitable for use with the current153* browser will be returned.154*155* This default implementation is a no-op. Subtypes may override this function156* for custom tailored file handling.157*158* @param {!./webdriver.WebDriver} driver The driver for the current browser.159* @param {string} path The path to process.160* @return {!Promise<string>} A promise for the processed file path.161* @package162*/163handleFile(_driver, path) {164return Promise.resolve(path)165}166}167168/**169* Generic description of a single action to send to the remote end.170*171* @record172* @package173*/174class Action {175constructor() {176/** @type {!Action.Type} */177this.type178/** @type {(number|undefined)} */179this.duration180/** @type {(string|undefined)} */181this.value182/** @type {(Button|undefined)} */183this.button184/** @type {(number|undefined)} */185this.x186/** @type {(number|undefined)} */187this.y188}189}190191/**192* @enum {string}193* @package194* @see <https://w3c.github.io/webdriver/webdriver-spec.html#terminology-0>195*/196Action.Type = {197KEY_DOWN: 'keyDown',198KEY_UP: 'keyUp',199PAUSE: 'pause',200POINTER_DOWN: 'pointerDown',201POINTER_UP: 'pointerUp',202POINTER_MOVE: 'pointerMove',203POINTER_CANCEL: 'pointerCancel',204SCROLL: 'scroll',205}206207/**208* Represents a user input device.209*210* @abstract211*/212class Device {213/**214* @param {Device.Type} type the input type.215* @param {string} id a unique ID for this device.216*/217constructor(type, id) {218/** @private @const */ this.type_ = type219/** @private @const */ this.id_ = id220}221222/** @return {!Object} the JSON encoding for this device. */223toJSON() {224return { type: this.type_, id: this.id_ }225}226}227228/**229* Device types supported by the WebDriver protocol.230*231* @enum {string}232* @see <https://w3c.github.io/webdriver/webdriver-spec.html#input-source-state>233*/234Device.Type = {235KEY: 'key',236NONE: 'none',237POINTER: 'pointer',238WHEEL: 'wheel',239}240241/**242* @param {(string|Key|number)} key243* @return {string}244* @throws {!(InvalidArgumentError|RangeError)}245*/246function checkCodePoint(key) {247if (typeof key === 'number') {248return String.fromCodePoint(key)249}250251if (typeof key !== 'string') {252throw new InvalidArgumentError(`key is not a string: ${key}`)253}254255key = key.normalize()256if (Array.from(key).length !== 1) {257throw new InvalidArgumentError(`key input is not a single code point: ${key}`)258}259return key260}261262/**263* Keyboard input device.264*265* @final266* @see <https://www.w3.org/TR/webdriver/#dfn-key-input-source>267*/268class Keyboard extends Device {269/** @param {string} id the device ID. */270constructor(id) {271super(Device.Type.KEY, id)272}273274/**275* Generates a key down action.276*277* @param {(Key|string|number)} key the key to press. This key may be278* specified as a {@link Key} value, a specific unicode code point,279* or a string containing a single unicode code point.280* @return {!Action} a new key down action.281* @package282*/283keyDown(key) {284return { type: Action.Type.KEY_DOWN, value: checkCodePoint(key) }285}286287/**288* Generates a key up action.289*290* @param {(Key|string|number)} key the key to press. This key may be291* specified as a {@link Key} value, a specific unicode code point,292* or a string containing a single unicode code point.293* @return {!Action} a new key up action.294* @package295*/296keyUp(key) {297return { type: Action.Type.KEY_UP, value: checkCodePoint(key) }298}299}300301/**302* Defines the reference point from which to compute offsets for303* {@linkplain ./input.Pointer#move pointer move} actions.304*305* @enum {string}306*/307const Origin = {308/** Compute offsets relative to the pointer's current position. */309POINTER: 'pointer',310/** Compute offsets relative to the viewport. */311VIEWPORT: 'viewport',312}313314/**315* Pointer input device.316*317* @final318* @see <https://www.w3.org/TR/webdriver/#dfn-pointer-input-source>319*/320class Pointer extends Device {321/**322* @param {string} id the device ID.323* @param {Pointer.Type} type the pointer type.324*/325constructor(id, type) {326super(Device.Type.POINTER, id)327/** @private @const */ this.pointerType_ = type328}329330/** @override */331toJSON() {332return Object.assign({ parameters: { pointerType: this.pointerType_ } }, super.toJSON())333}334335/**336* @return {!Action} An action that cancels this pointer's current input.337* @package338*/339cancel() {340return { type: Action.Type.POINTER_CANCEL }341}342343/**344* @param {!Button=} button The button to press.345* @param width346* @param height347* @param pressure348* @param tangentialPressure349* @param tiltX350* @param tiltY351* @param twist352* @param altitudeAngle353* @param azimuthAngle354* @return {!Action} An action to press the specified button with this device.355* @package356*/357press(358button = Button.LEFT,359width = 0,360height = 0,361pressure = 0,362tangentialPressure = 0,363tiltX = 0,364tiltY = 0,365twist = 0,366altitudeAngle = 0,367azimuthAngle = 0,368) {369return {370type: Action.Type.POINTER_DOWN,371button,372width,373height,374pressure,375tangentialPressure,376tiltX,377tiltY,378twist,379altitudeAngle,380azimuthAngle,381}382}383384/**385* @param {!Button=} button The button to release.386* @return {!Action} An action to release the specified button with this387* device.388* @package389*/390release(button = Button.LEFT) {391return { type: Action.Type.POINTER_UP, button }392}393394/**395* Creates an action for moving the pointer `x` and `y` pixels from the396* specified `origin`. The `origin` may be defined as the pointer's397* {@linkplain Origin.POINTER current position}, the398* {@linkplain Origin.VIEWPORT viewport}, or the center of a specific399* {@linkplain ./webdriver.WebElement WebElement}.400*401* @param {{402* x: (number|undefined),403* y: (number|undefined),404* duration: (number|undefined),405* origin: (!Origin|!./webdriver.WebElement|undefined),406* }=} options the move options.407* @return {!Action} The new action.408* @package409*/410move({411x = 0,412y = 0,413duration = 100,414origin = Origin.VIEWPORT,415width = 0,416height = 0,417pressure = 0,418tangentialPressure = 0,419tiltX = 0,420tiltY = 0,421twist = 0,422altitudeAngle = 0,423azimuthAngle = 0,424}) {425return {426type: Action.Type.POINTER_MOVE,427origin,428duration,429x,430y,431width,432height,433pressure,434tangentialPressure,435tiltX,436tiltY,437twist,438altitudeAngle,439azimuthAngle,440}441}442}443444/**445* The supported types of pointers.446* @enum {string}447*/448Pointer.Type = {449MOUSE: 'mouse',450PEN: 'pen',451TOUCH: 'touch',452}453454class Wheel extends Device {455/**456* @param {string} id the device ID..457*/458constructor(id) {459super(Device.Type.WHEEL, id)460}461462/**463* Scrolls a page via the coordinates given464* @param {number} x starting x coordinate465* @param {number} y starting y coordinate466* @param {number} deltaX Delta X to scroll to target467* @param {number} deltaY Delta Y to scroll to target468* @param {WebElement} origin element origin469* @param {number} duration duration ratio be the ratio of time delta and duration470* @returns {!Action} An action to scroll with this device.471*/472scroll(x, y, deltaX, deltaY, origin, duration) {473return {474type: Action.Type.SCROLL,475duration: duration,476x: x,477y: y,478deltaX: deltaX,479deltaY: deltaY,480origin: origin,481}482}483}484485/**486* User facing API for generating complex user gestures. This class should not487* be instantiated directly. Instead, users should create new instances by488* calling {@link ./webdriver.WebDriver#actions WebDriver.actions()}.489*490* ### Action Ticks491*492* Action sequences are divided into a series of "ticks". At each tick, the493* WebDriver remote end will perform a single action for each device included494* in the action sequence. At tick 0, the driver will perform the first action495* defined for each device, at tick 1 the second action for each device, and496* so on until all actions have been executed. If an individual device does497* not have an action defined at a particular tick, it will automatically498* pause.499*500* By default, action sequences will be synchronized so only one device has a501* define action in each tick. Consider the following code sample:502*503* const actions = driver.actions();504*505* await actions506* .keyDown(SHIFT)507* .move({origin: el})508* .press()509* .release()510* .keyUp(SHIFT)511* .perform();512*513* This sample produces the following sequence of ticks:514*515* | Device | Tick 1 | Tick 2 | Tick 3 | Tick 4 | Tick 5 |516* | -------- | -------------- | ------------------ | ------- | --------- | ------------ |517* | Keyboard | keyDown(SHIFT) | pause() | pause() | pause() | keyUp(SHIFT) |518* | Mouse | pause() | move({origin: el}) | press() | release() | pause() |519*520* If you'd like the remote end to execute actions with multiple devices521* simultaneously, you may pass `{async: true}` when creating the actions522* builder. With synchronization disabled (`{async: true}`), the ticks from our523* previous example become:524*525* | Device | Tick 1 | Tick 2 | Tick 3 |526* | -------- | ------------------ | ------------ | --------- |527* | Keyboard | keyDown(SHIFT) | keyUp(SHIFT) | |528* | Mouse | move({origin: el}) | press() | release() |529*530* When synchronization is disabled, it is your responsibility to insert531* {@linkplain #pause() pauses} for each device, as needed:532*533* const actions = driver.actions({async: true});534* const kb = actions.keyboard();535* const mouse = actions.mouse();536*537* actions.keyDown(SHIFT).pause(kb).pause(kb).key(SHIFT);538* actions.pause(mouse).move({origin: el}).press().release();539* actions.perform();540*541* With pauses insert for individual devices, we're back to:542*543* | Device | Tick 1 | Tick 2 | Tick 3 | Tick 4 |544* | -------- | -------------- | ------------------ | ------- | ------------ |545* | Keyboard | keyDown(SHIFT) | pause() | pause() | keyUp(SHIFT) |546* | Mouse | pause() | move({origin: el}) | press() | release() |547*548* #### Tick Durations549*550* The length of each action tick is however long it takes the remote end to551* execute the actions for every device in that tick. Most actions are552* "instantaneous", however, {@linkplain #pause pause} and553* {@linkplain #move pointer move} actions allow you to specify a duration for554* how long that action should take. The remote end will always wait for all555* actions within a tick to finish before starting the next tick, so a device556* may implicitly pause while waiting for other devices to finish.557*558* | Device | Tick 1 | Tick 2 |559* | --------- | --------------------- | ------- |560* | Pointer 1 | move({duration: 200}) | press() |561* | Pointer 2 | move({duration: 300}) | press() |562*563* In table above, the move for Pointer 1 should only take 200 ms, but the564* remote end will wait for the move for Pointer 2 to finish565* (an additional 100 ms) before proceeding to Tick 2.566*567* This implicit waiting also applies to pauses. In the table below, even though568* the keyboard only defines a pause of 100 ms, the remote end will wait an569* additional 200 ms for the mouse move to finish before moving to Tick 2.570*571* | Device | Tick 1 | Tick 2 |572* | -------- | --------------------- | -------------- |573* | Keyboard | pause(100) | keyDown(SHIFT) |574* | Mouse | move({duration: 300}) | |575*576* [client rect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getClientRects577* [bounding client rect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect578*579* @final580* @see <https://www.w3.org/TR/webdriver/#actions>581*/582class Actions {583/**584* @param {!Executor} executor The object to execute the configured585* actions with.586* @param {{async: (boolean|undefined)}} options Options for this action587* sequence (see class description for details).588*/589constructor(executor, { async = false } = {}) {590/** @private @const */591this.executor_ = executor592593/** @private @const */594this.sync_ = !async595596/** @private @const */597this.keyboard_ = new Keyboard('default keyboard')598599/** @private @const */600this.mouse_ = new Pointer('default mouse', Pointer.Type.MOUSE)601602/** @private @const */603this.wheel_ = new Wheel('default wheel')604605/** @private @const {!Map<!Device, !Array<!Action>>} */606this.sequences_ = new Map([607[this.keyboard_, []],608[this.mouse_, []],609[this.wheel_, []],610])611}612613/** @return {!Keyboard} the keyboard device handle. */614keyboard() {615return this.keyboard_616}617618/** @return {!Pointer} the mouse pointer device handle. */619mouse() {620return this.mouse_621}622623/** @return {!Wheel} the wheel device handle. */624wheel() {625return this.wheel_626}627628/**629* @param {!Device} device630* @return {!Array<!Action>}631* @private632*/633sequence_(device) {634let sequence = this.sequences_.get(device)635if (!sequence) {636sequence = []637this.sequences_.set(device, sequence)638}639return sequence640}641642/**643* Appends `actions` to the end of the current sequence for the given644* `device`. If device synchronization is enabled, after inserting the645* actions, pauses will be inserted for all other devices to ensure all action646* sequences are the same length.647*648* @param {!Device} device the device to update.649* @param {...!Action} actions the actions to insert.650* @return {!Actions} a self reference.651*/652insert(device, ...actions) {653this.sequence_(device).push(...actions)654return this.sync_ ? this.synchronize() : this655}656657/**658* Ensures the action sequence for every device referenced in this action659* sequence is the same length. For devices whose sequence is too short,660* this will insert {@linkplain #pause pauses} so that every device has an661* explicit action defined at each tick.662*663* @param {...!Device} devices The specific devices to synchronize.664* If unspecified, the action sequences for every device will be665* synchronized.666* @return {!Actions} a self reference.667*/668synchronize(...devices) {669let sequences670let max = 0671if (devices.length === 0) {672for (const s of this.sequences_.values()) {673max = Math.max(max, s.length)674}675sequences = this.sequences_.values()676} else {677sequences = []678for (const device of devices) {679const seq = this.sequence_(device)680max = Math.max(max, seq.length)681sequences.push(seq)682}683}684685const pause = { type: Action.Type.PAUSE, duration: 0 }686for (const seq of sequences) {687while (seq.length < max) {688seq.push(pause)689}690}691692return this693}694695/**696* Inserts a pause action for the specified devices, ensuring each device is697* idle for a tick. The length of the pause (in milliseconds) may be specified698* as the first parameter to this method (defaults to 0). Otherwise, you may699* just specify the individual devices that should pause.700*701* If no devices are specified, a pause action will be created (using the same702* duration) for every device.703*704* When device synchronization is enabled (the default for new {@link Actions}705* objects), there is no need to specify devices as pausing one automatically706* pauses the others for the same duration. In other words, the following are707* all equivalent:708*709* let a1 = driver.actions();710* a1.pause(100).perform();711*712* let a2 = driver.actions();713* a2.pause(100, a2.keyboard()).perform();714* // Synchronization ensures a2.mouse() is automatically paused too.715*716* let a3 = driver.actions();717* a3.pause(100, a3.keyboard(), a3.mouse()).perform();718*719* When device synchronization is _disabled_, you can cause individual devices720* to pause during a tick. For example, to hold the SHIFT key down while721* moving the mouse:722*723* let actions = driver.actions({async: true});724*725* actions.keyDown(Key.SHIFT);726* actions.pause(actions.mouse()) // Pause for shift down727* .press(Button.LEFT)728* .move({x: 10, y: 10})729* .release(Button.LEFT);730* actions731* .pause(732* actions.keyboard(), // Pause for press left733* actions.keyboard(), // Pause for move734* actions.keyboard()) // Pause for release left735* .keyUp(Key.SHIFT);736* await actions.perform();737*738* @param {(number|!Device)=} duration The length of the pause to insert, in739* milliseconds. Alternatively, the duration may be omitted (yielding a740* default 0 ms pause), and the first device to pause may be specified.741* @param {...!Device} devices The devices to insert the pause for. If no742* devices are specified, the pause will be inserted for _all_ devices.743* @return {!Actions} a self reference.744*/745pause(duration, ...devices) {746if (duration instanceof Device) {747devices.push(duration)748duration = 0749} else if (!duration) {750duration = 0751}752753const action = { type: Action.Type.PAUSE, duration }754755// NB: need a properly typed variable for type checking.756/** @type {!Iterable<!Device>} */757const iterable = devices.length === 0 ? this.sequences_.keys() : devices758for (const device of iterable) {759this.sequence_(device).push(action)760}761return this.sync_ ? this.synchronize() : this762}763764/**765* Inserts an action to press a single key.766*767* @param {(Key|string|number)} key the key to press. This key may be768* specified as a {@link Key} value, a specific unicode code point,769* or a string containing a single unicode code point.770* @return {!Actions} a self reference.771*/772keyDown(key) {773return this.insert(this.keyboard_, this.keyboard_.keyDown(key))774}775776/**777* Inserts an action to release a single key.778*779* @param {(Key|string|number)} key the key to release. This key may be780* specified as a {@link Key} value, a specific unicode code point,781* or a string containing a single unicode code point.782* @return {!Actions} a self reference.783*/784keyUp(key) {785return this.insert(this.keyboard_, this.keyboard_.keyUp(key))786}787788/**789* Inserts a sequence of actions to type the provided key sequence.790* For each key, this will record a pair of {@linkplain #keyDown keyDown}791* and {@linkplain #keyUp keyUp} actions. An implication of this pairing792* is that modifier keys (e.g. {@link ./input.Key.SHIFT Key.SHIFT}) will793* always be immediately released. In other words, `sendKeys(Key.SHIFT, 'a')`794* is the same as typing `sendKeys('a')`, _not_ `sendKeys('A')`.795*796* @param {...(Key|string|number)} keys the keys to type.797* @return {!Actions} a self reference.798*/799sendKeys(...keys) {800const { WebElement } = require('./webdriver')801802const actions = []803if (keys.length > 1 && keys[0] instanceof WebElement) {804this.click(keys[0])805keys.shift()806}807for (const key of keys) {808if (typeof key === 'string') {809for (const symbol of key) {810actions.push(this.keyboard_.keyDown(symbol), this.keyboard_.keyUp(symbol))811}812} else {813actions.push(this.keyboard_.keyDown(key), this.keyboard_.keyUp(key))814}815}816return this.insert(this.keyboard_, ...actions)817}818819/**820* Inserts an action to press a mouse button at the mouse's current location.821*822* @param {!Button=} button The button to press; defaults to `LEFT`.823* @return {!Actions} a self reference.824*/825press(button = Button.LEFT) {826return this.insert(this.mouse_, this.mouse_.press(button))827}828829/**830* Inserts an action to release a mouse button at the mouse's current831* location.832*833* @param {!Button=} button The button to release; defaults to `LEFT`.834* @return {!Actions} a self reference.835*/836release(button = Button.LEFT) {837return this.insert(this.mouse_, this.mouse_.release(button))838}839840/**841* scrolls a page via the coordinates given842* @param {number} x starting x coordinate843* @param {number} y starting y coordinate844* @param {number} deltax delta x to scroll to target845* @param {number} deltay delta y to scroll to target846* @param {number} duration duration ratio be the ratio of time delta and duration847* @returns {!Actions} An action to scroll with this device.848*/849scroll(x, y, targetDeltaX, targetDeltaY, origin, duration) {850return this.insert(this.wheel_, this.wheel_.scroll(x, y, targetDeltaX, targetDeltaY, origin, duration))851}852853/**854* Inserts an action for moving the mouse `x` and `y` pixels relative to the855* specified `origin`. The `origin` may be defined as the mouse's856* {@linkplain ./input.Origin.POINTER current position}, the top-left corner of the857* {@linkplain ./input.Origin.VIEWPORT viewport}, or the center of a specific858* {@linkplain ./webdriver.WebElement WebElement}. Default is top left corner of the view-port if origin is not specified859*860* You may adjust how long the remote end should take, in milliseconds, to861* perform the move using the `duration` parameter (defaults to 100 ms).862* The number of incremental move events generated over this duration is an863* implementation detail for the remote end.864*865* @param {{866* x: (number|undefined),867* y: (number|undefined),868* duration: (number|undefined),869* origin: (!Origin|!./webdriver.WebElement|undefined),870* }=} options The move options. Defaults to moving the mouse to the top-left871* corner of the viewport over 100ms.872* @return {!Actions} a self reference.873*/874move({ x = 0, y = 0, duration = 100, origin = Origin.VIEWPORT } = {}) {875return this.insert(this.mouse_, this.mouse_.move({ x, y, duration, origin }))876}877878/**879* Short-hand for performing a simple left-click (down/up) with the mouse.880*881* @param {./webdriver.WebElement=} element If specified, the mouse will882* first be moved to the center of the element before performing the883* click.884* @return {!Actions} a self reference.885*/886click(element) {887if (element) {888this.move({ origin: element })889}890return this.press().release()891}892893/**894* Short-hand for performing a simple right-click (down/up) with the mouse.895*896* @param {./webdriver.WebElement=} element If specified, the mouse will897* first be moved to the center of the element before performing the898* click.899* @return {!Actions} a self reference.900*/901contextClick(element) {902if (element) {903this.move({ origin: element })904}905return this.press(Button.RIGHT).release(Button.RIGHT)906}907908/**909* Short-hand for performing a double left-click with the mouse.910*911* @param {./webdriver.WebElement=} element If specified, the mouse will912* first be moved to the center of the element before performing the913* click.914* @return {!Actions} a self reference.915*/916doubleClick(element) {917return this.click(element).press().release()918}919920/**921* Configures a drag-and-drop action consisting of the following steps:922*923* 1. Move to the center of the `from` element (element to be dragged).924* 2. Press the left mouse button.925* 3. If the `to` target is a {@linkplain ./webdriver.WebElement WebElement},926* move the mouse to its center. Otherwise, move the mouse by the927* specified offset.928* 4. Release the left mouse button.929*930* @param {!./webdriver.WebElement} from The element to press the left mouse931* button on to start the drag.932* @param {(!./webdriver.WebElement|{x: number, y: number})} to Either another933* element to drag to (will drag to the center of the element), or an934* object specifying the offset to drag by, in pixels.935* @return {!Actions} a self reference.936*/937dragAndDrop(from, to) {938// Do not require up top to avoid a cycle that breaks static analysis.939const { WebElement } = require('./webdriver')940if (!(to instanceof WebElement) && (!to || typeof to.x !== 'number' || typeof to.y !== 'number')) {941throw new InvalidArgumentError('Invalid drag target; must specify a WebElement or {x, y} offset')942}943944this.move({ origin: from }).press()945if (to instanceof WebElement) {946this.move({ origin: to })947} else {948this.move({ x: to.x, y: to.y, origin: Origin.POINTER })949}950return this.release()951}952953/**954* Releases all keys, pointers, and clears internal state.955*956* @return {!Promise<void>} a promise that will resolve when finished957* clearing all action state.958*/959clear() {960for (const s of this.sequences_.values()) {961s.length = 0962}963return this.executor_.execute(new Command(Name.CLEAR_ACTIONS))964}965966/**967* Performs the configured action sequence.968*969* @return {!Promise<void>} a promise that will resolve when all actions have970* been completed.971*/972async perform() {973const _actions = []974this.sequences_.forEach((actions, device) => {975if (!isIdle(actions)) {976actions = actions.concat() // Defensive copy.977_actions.push(Object.assign({ actions }, device.toJSON()))978}979})980981if (_actions.length === 0) {982return Promise.resolve()983}984985await this.executor_.execute(new Command(Name.ACTIONS).setParameter('actions', _actions))986}987988getSequences() {989const _actions = []990this.sequences_.forEach((actions, device) => {991if (!isIdle(actions)) {992actions = actions.concat()993_actions.push(Object.assign({ actions }, device.toJSON()))994}995})996997return _actions998}999}10001001/**1002* @param {!Array<!Action>} actions1003* @return {boolean}1004*/1005function isIdle(actions) {1006return actions.length === 0 || actions.every((a) => a.type === Action.Type.PAUSE && !a.duration)1007}10081009/**1010* Script used to compute the offset from the center of a DOM element's first1011* client rect from the top-left corner of the element's bounding client rect.1012* The element's center point is computed using the algorithm defined here:1013* <https://w3c.github.io/webdriver/webdriver-spec.html#dfn-center-point>.1014*1015* __This is only exported for use in internal unit tests. DO NOT USE.__1016*1017* @package1018*/1019const INTERNAL_COMPUTE_OFFSET_SCRIPT = `1020function computeOffset(el) {1021var rect = el.getClientRects()[0];1022var left = Math.max(0, Math.min(rect.x, rect.x + rect.width));1023var right =1024Math.min(window.innerWidth, Math.max(rect.x, rect.x + rect.width));1025var top = Math.max(0, Math.min(rect.y, rect.y + rect.height));1026var bot =1027Math.min(window.innerHeight, Math.max(rect.y, rect.y + rect.height));1028var x = Math.floor(0.5 * (left + right));1029var y = Math.floor(0.5 * (top + bot));10301031var bbox = el.getBoundingClientRect();1032return [x - bbox.left, y - bbox.top];1033}1034return computeOffset(arguments[0]);`10351036// PUBLIC API10371038module.exports = {1039Action, // For documentation only.1040Actions,1041Button,1042Device,1043Key,1044Keyboard,1045FileDetector,1046Origin,1047Pointer,1048INTERNAL_COMPUTE_OFFSET_SCRIPT,1049}105010511052