UNPKG

29.1 kBJavaScriptView Raw
1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18'use strict'
19
20/**
21 * @fileoverview Defines types related to user input with the WebDriver API.
22 */
23const { Command, Name } = require('./command')
24const { InvalidArgumentError } = require('./error')
25
26/**
27 * Enumeration of the buttons used in the advanced interactions API.
28 * @enum {number}
29 */
30const Button = {
31 LEFT: 0,
32 MIDDLE: 1,
33 RIGHT: 2,
34}
35
36/**
37 * Representations of pressable keys that aren't text. These are stored in
38 * the Unicode PUA (Private Use Area) code points, 0xE000-0xF8FF. Refer to
39 * http://www.google.com.au/search?&q=unicode+pua&btnK=Search
40 *
41 * @enum {string}
42 * @see <https://www.w3.org/TR/webdriver/#keyboard-actions>
43 */
44const Key = {
45 NULL: '\uE000',
46 CANCEL: '\uE001', // ^break
47 HELP: '\uE002',
48 BACK_SPACE: '\uE003',
49 TAB: '\uE004',
50 CLEAR: '\uE005',
51 RETURN: '\uE006',
52 ENTER: '\uE007',
53 SHIFT: '\uE008',
54 CONTROL: '\uE009',
55 ALT: '\uE00A',
56 PAUSE: '\uE00B',
57 ESCAPE: '\uE00C',
58 SPACE: '\uE00D',
59 PAGE_UP: '\uE00E',
60 PAGE_DOWN: '\uE00F',
61 END: '\uE010',
62 HOME: '\uE011',
63 ARROW_LEFT: '\uE012',
64 LEFT: '\uE012',
65 ARROW_UP: '\uE013',
66 UP: '\uE013',
67 ARROW_RIGHT: '\uE014',
68 RIGHT: '\uE014',
69 ARROW_DOWN: '\uE015',
70 DOWN: '\uE015',
71 INSERT: '\uE016',
72 DELETE: '\uE017',
73 SEMICOLON: '\uE018',
74 EQUALS: '\uE019',
75
76 NUMPAD0: '\uE01A', // number pad keys
77 NUMPAD1: '\uE01B',
78 NUMPAD2: '\uE01C',
79 NUMPAD3: '\uE01D',
80 NUMPAD4: '\uE01E',
81 NUMPAD5: '\uE01F',
82 NUMPAD6: '\uE020',
83 NUMPAD7: '\uE021',
84 NUMPAD8: '\uE022',
85 NUMPAD9: '\uE023',
86 MULTIPLY: '\uE024',
87 ADD: '\uE025',
88 SEPARATOR: '\uE026',
89 SUBTRACT: '\uE027',
90 DECIMAL: '\uE028',
91 DIVIDE: '\uE029',
92
93 F1: '\uE031', // function keys
94 F2: '\uE032',
95 F3: '\uE033',
96 F4: '\uE034',
97 F5: '\uE035',
98 F6: '\uE036',
99 F7: '\uE037',
100 F8: '\uE038',
101 F9: '\uE039',
102 F10: '\uE03A',
103 F11: '\uE03B',
104 F12: '\uE03C',
105
106 COMMAND: '\uE03D', // Apple command key
107 META: '\uE03D', // alias for Windows key
108
109 /**
110 * Japanese modifier key for switching between full- and half-width
111 * characters.
112 * @see <https://en.wikipedia.org/wiki/Language_input_keys>
113 */
114 ZENKAKU_HANKAKU: '\uE040',
115}
116
117/**
118 * Simulate pressing many keys at once in a "chord". Takes a sequence of
119 * {@linkplain Key keys} or strings, appends each of the values to a string,
120 * adds the chord termination key ({@link Key.NULL}) and returns the resulting
121 * string.
122 *
123 * Note: when the low-level webdriver key handlers see Keys.NULL, active
124 * modifier keys (CTRL/ALT/SHIFT/etc) release via a keyup event.
125 *
126 * @param {...string} keys The key sequence to concatenate.
127 * @return {string} The null-terminated key sequence.
128 */
129Key.chord = function (...keys) {
130 return keys.join('') + Key.NULL
131}
132
133/**
134 * Used with {@link ./webelement.WebElement#sendKeys WebElement#sendKeys} on
135 * file input elements (`<input type="file">`) to detect when the entered key
136 * sequence defines the path to a file.
137 *
138 * By default, {@linkplain ./webelement.WebElement WebElement's} will enter all
139 * key sequences exactly as entered. You may set a
140 * {@linkplain ./webdriver.WebDriver#setFileDetector file detector} on the
141 * parent WebDriver instance to define custom behavior for handling file
142 * elements. Of particular note is the
143 * {@link selenium-webdriver/remote.FileDetector}, which should be used when
144 * running against a remote
145 * [Selenium Server](https://selenium.dev/downloads/).
146 */
147class FileDetector {
148 /**
149 * Handles the file specified by the given path, preparing it for use with
150 * the current browser. If the path does not refer to a valid file, it will
151 * be returned unchanged, otherwise a path suitable for use with the current
152 * browser will be returned.
153 *
154 * This default implementation is a no-op. Subtypes may override this function
155 * for custom tailored file handling.
156 *
157 * @param {!./webdriver.WebDriver} driver The driver for the current browser.
158 * @param {string} path The path to process.
159 * @return {!Promise<string>} A promise for the processed file path.
160 * @package
161 */
162 handleFile(driver, path) { // eslint-disable-line
163 return Promise.resolve(path)
164 }
165}
166
167/**
168 * Generic description of a single action to send to the remote end.
169 *
170 * @record
171 * @package
172 */
173class Action {
174 constructor() {
175 /** @type {!Action.Type} */
176 this.type
177 /** @type {(number|undefined)} */
178 this.duration
179 /** @type {(string|undefined)} */
180 this.value
181 /** @type {(Button|undefined)} */
182 this.button
183 /** @type {(number|undefined)} */
184 this.x
185 /** @type {(number|undefined)} */
186 this.y
187 }
188}
189
190/**
191 * @enum {string}
192 * @package
193 * @see <https://w3c.github.io/webdriver/webdriver-spec.html#terminology-0>
194 */
195Action.Type = {
196 KEY_DOWN: 'keyDown',
197 KEY_UP: 'keyUp',
198 PAUSE: 'pause',
199 POINTER_DOWN: 'pointerDown',
200 POINTER_UP: 'pointerUp',
201 POINTER_MOVE: 'pointerMove',
202 POINTER_CANCEL: 'pointerCancel',
203}
204
205/**
206 * Represents a user input device.
207 *
208 * @abstract
209 */
210class Device {
211 /**
212 * @param {Device.Type} type the input type.
213 * @param {string} id a unique ID for this device.
214 */
215 constructor(type, id) {
216 /** @private @const */ this.type_ = type
217 /** @private @const */ this.id_ = id
218 }
219
220 /** @return {!Object} the JSON encoding for this device. */
221 toJSON() {
222 return { type: this.type_, id: this.id_ }
223 }
224}
225
226/**
227 * Device types supported by the WebDriver protocol.
228 *
229 * @enum {string}
230 * @see <https://w3c.github.io/webdriver/webdriver-spec.html#input-source-state>
231 */
232Device.Type = {
233 KEY: 'key',
234 NONE: 'none',
235 POINTER: 'pointer',
236}
237
238/**
239 * @param {(string|Key|number)} key
240 * @return {string}
241 * @throws {!(InvalidArgumentError|RangeError)}
242 */
243function checkCodePoint(key) {
244 if (typeof key === 'number') {
245 return String.fromCodePoint(key)
246 }
247
248 if (typeof key !== 'string') {
249 throw new InvalidArgumentError(`key is not a string: ${key}`)
250 }
251
252 key = key.normalize()
253 if (Array.from(key).length != 1) {
254 throw new InvalidArgumentError(
255 `key input is not a single code point: ${key}`
256 )
257 }
258 return key
259}
260
261/**
262 * Keyboard input device.
263 *
264 * @final
265 * @see <https://www.w3.org/TR/webdriver/#dfn-key-input-source>
266 */
267class Keyboard extends Device {
268 /** @param {string} id the device ID. */
269 constructor(id) {
270 super(Device.Type.KEY, id)
271 }
272
273 /**
274 * Generates a key down action.
275 *
276 * @param {(Key|string|number)} key the key to press. This key may be
277 * specified as a {@link Key} value, a specific unicode code point,
278 * or a string containing a single unicode code point.
279 * @return {!Action} a new key down action.
280 * @package
281 */
282 keyDown(key) {
283 return { type: Action.Type.KEY_DOWN, value: checkCodePoint(key) }
284 }
285
286 /**
287 * Generates a key up action.
288 *
289 * @param {(Key|string|number)} key the key to press. This key may be
290 * specified as a {@link Key} value, a specific unicode code point,
291 * or a string containing a single unicode code point.
292 * @return {!Action} a new key up action.
293 * @package
294 */
295 keyUp(key) {
296 return { type: Action.Type.KEY_UP, value: checkCodePoint(key) }
297 }
298}
299
300/**
301 * Defines the reference point from which to compute offsets for
302 * {@linkplain ./input.Pointer#move pointer move} actions.
303 *
304 * @enum {string}
305 */
306const Origin = {
307 /** Compute offsets relative to the pointer's current position. */
308 POINTER: 'pointer',
309 /** Compute offsets relative to the viewport. */
310 VIEWPORT: 'viewport',
311}
312
313/**
314 * Pointer input device.
315 *
316 * @final
317 * @see <https://www.w3.org/TR/webdriver/#dfn-pointer-input-source>
318 */
319class Pointer extends Device {
320 /**
321 * @param {string} id the device ID.
322 * @param {Pointer.Type} type the pointer type.
323 */
324 constructor(id, type) {
325 super(Device.Type.POINTER, id)
326 /** @private @const */ this.pointerType_ = type
327 }
328
329 /** @override */
330 toJSON() {
331 return Object.assign(
332 { parameters: { pointerType: this.pointerType_ } },
333 super.toJSON()
334 )
335 }
336
337 /**
338 * @return {!Action} An action that cancels this pointer's current input.
339 * @package
340 */
341 cancel() {
342 return { type: Action.Type.POINTER_CANCEL }
343 }
344
345 /**
346 * @param {!Button=} button The button to press.
347 * @return {!Action} An action to press the specified button with this device.
348 * @package
349 */
350 press(button = Button.LEFT) {
351 return { type: Action.Type.POINTER_DOWN, button }
352 }
353
354 /**
355 * @param {!Button=} button The button to release.
356 * @return {!Action} An action to release the specified button with this
357 * device.
358 * @package
359 */
360 release(button = Button.LEFT) {
361 return { type: Action.Type.POINTER_UP, button }
362 }
363
364 /**
365 * Creates an action for moving the pointer `x` and `y` pixels from the
366 * specified `origin`. The `origin` may be defined as the pointer's
367 * {@linkplain Origin.POINTER current position}, the
368 * {@linkplain Origin.VIEWPORT viewport}, or the center of a specific
369 * {@linkplain ./webdriver.WebElement WebElement}.
370 *
371 * @param {{
372 * x: (number|undefined),
373 * y: (number|undefined),
374 * duration: (number|undefined),
375 * origin: (!Origin|!./webdriver.WebElement|undefined),
376 * }=} options the move options.
377 * @return {!Action} The new action.
378 * @package
379 */
380 move({ x = 0, y = 0, duration = 100, origin = Origin.VIEWPORT }) {
381 return { type: Action.Type.POINTER_MOVE, origin, duration, x, y }
382 }
383}
384
385/**
386 * The supported types of pointers.
387 * @enum {string}
388 */
389Pointer.Type = {
390 MOUSE: 'mouse',
391 PEN: 'pen',
392 TOUCH: 'touch',
393}
394
395/**
396 * User facing API for generating complex user gestures. This class should not
397 * be instantiated directly. Instead, users should create new instances by
398 * calling {@link ./webdriver.WebDriver#actions WebDriver.actions()}.
399 *
400 * ### Action Ticks
401 *
402 * Action sequences are divided into a series of "ticks". At each tick, the
403 * WebDriver remote end will perform a single action for each device included
404 * in the action sequence. At tick 0, the driver will perform the first action
405 * defined for each device, at tick 1 the second action for each device, and
406 * so on until all actions have been executed. If an individual device does
407 * not have an action defined at a particular tick, it will automatically
408 * pause.
409 *
410 * By default, action sequences will be synchronized so only one device has a
411 * define action in each tick. Consider the following code sample:
412 *
413 * const actions = driver.actions();
414 *
415 * await actions
416 * .keyDown(SHIFT)
417 * .move({origin: el})
418 * .press()
419 * .release()
420 * .keyUp(SHIFT)
421 * .perform();
422 *
423 * This sample produces the following sequence of ticks:
424 *
425 * | Device | Tick 1 | Tick 2 | Tick 3 | Tick 4 | Tick 5 |
426 * | -------- | -------------- | ------------------ | ------- | --------- | ------------ |
427 * | Keyboard | keyDown(SHIFT) | pause() | pause() | pause() | keyUp(SHIFT) |
428 * | Mouse | pause() | move({origin: el}) | press() | release() | pause() |
429 *
430 * If you'd like the remote end to execute actions with multiple devices
431 * simultaneously, you may pass `{async: true}` when creating the actions
432 * builder. With synchronization disabled (`{async: true}`), the ticks from our
433 * previous example become:
434 *
435 * | Device | Tick 1 | Tick 2 | Tick 3 |
436 * | -------- | ------------------ | ------------ | --------- |
437 * | Keyboard | keyDown(SHIFT) | keyUp(SHIFT) | |
438 * | Mouse | move({origin: el}) | press() | release() |
439 *
440 * When synchronization is disabled, it is your responsibility to insert
441 * {@linkplain #pause() pauses} for each device, as needed:
442 *
443 * const actions = driver.actions({async: true});
444 * const kb = actions.keyboard();
445 * const mouse = actions.mouse();
446 *
447 * actions.keyDown(SHIFT).pause(kb).pause(kb).key(SHIFT);
448 * actions.pause(mouse).move({origin: el}).press().release();
449 * actions.perform();
450 *
451 * With pauses insert for individual devices, we're back to:
452 *
453 * | Device | Tick 1 | Tick 2 | Tick 3 | Tick 4 |
454 * | -------- | -------------- | ------------------ | ------- | ------------ |
455 * | Keyboard | keyDown(SHIFT) | pause() | pause() | keyUp(SHIFT) |
456 * | Mouse | pause() | move({origin: el}) | press() | release() |
457 *
458 * #### Tick Durations
459 *
460 * The length of each action tick is however long it takes the remote end to
461 * execute the actions for every device in that tick. Most actions are
462 * "instantaneous", however, {@linkplain #pause pause} and
463 * {@linkplain #move pointer move} actions allow you to specify a duration for
464 * how long that action should take. The remote end will always wait for all
465 * actions within a tick to finish before starting the next tick, so a device
466 * may implicitly pause while waiting for other devices to finish.
467 *
468 * | Device | Tick 1 | Tick 2 |
469 * | --------- | --------------------- | ------- |
470 * | Pointer 1 | move({duration: 200}) | press() |
471 * | Pointer 2 | move({duration: 300}) | press() |
472 *
473 * In table above, the move for Pointer 1 should only take 200 ms, but the
474 * remote end will wait for the move for Pointer 2 to finish
475 * (an additional 100 ms) before proceeding to Tick 2.
476 *
477 * This implicit waiting also applies to pauses. In the table below, even though
478 * the keyboard only defines a pause of 100 ms, the remote end will wait an
479 * additional 200 ms for the mouse move to finish before moving to Tick 2.
480 *
481 * | Device | Tick 1 | Tick 2 |
482 * | -------- | --------------------- | -------------- |
483 * | Keyboard | pause(100) | keyDown(SHIFT) |
484 * | Mouse | move({duration: 300}) | |
485 *
486 * [client rect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getClientRects
487 * [bounding client rect]: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
488 *
489 * @final
490 * @see <https://www.w3.org/TR/webdriver/#actions>
491 */
492class Actions {
493 /**
494 * @param {!Executor} executor The object to execute the configured
495 * actions with.
496 * @param {{async: (boolean|undefined)}} options Options for this action
497 * sequence (see class description for details).
498 */
499 constructor(executor, { async = false } = {}) {
500 /** @private @const */
501 this.executor_ = executor
502
503 /** @private @const */
504 this.sync_ = !async
505
506 /** @private @const */
507 this.keyboard_ = new Keyboard('default keyboard')
508
509 /** @private @const */
510 this.mouse_ = new Pointer('default mouse', Pointer.Type.MOUSE)
511
512 /** @private @const {!Map<!Device, !Array<!Action>>} */
513 this.sequences_ = new Map([
514 [this.keyboard_, []],
515 [this.mouse_, []],
516 ])
517 }
518
519 /** @return {!Keyboard} the keyboard device handle. */
520 keyboard() {
521 return this.keyboard_
522 }
523
524 /** @return {!Pointer} the mouse pointer device handle. */
525 mouse() {
526 return this.mouse_
527 }
528
529 /**
530 * @param {!Device} device
531 * @return {!Array<!Action>}
532 * @private
533 */
534 sequence_(device) {
535 let sequence = this.sequences_.get(device)
536 if (!sequence) {
537 sequence = []
538 this.sequences_.set(device, sequence)
539 }
540 return sequence
541 }
542
543 /**
544 * Appends `actions` to the end of the current sequence for the given
545 * `device`. If device synchronization is enabled, after inserting the
546 * actions, pauses will be inserted for all other devices to ensure all action
547 * sequences are the same length.
548 *
549 * @param {!Device} device the device to update.
550 * @param {...!Action} actions the actions to insert.
551 * @return {!Actions} a self reference.
552 */
553 insert(device, ...actions) {
554 this.sequence_(device).push(...actions)
555 return this.sync_ ? this.synchronize() : this
556 }
557
558 /**
559 * Ensures the action sequence for every device referenced in this action
560 * sequence is the same length. For devices whose sequence is too short,
561 * this will insert {@linkplain #pause pauses} so that every device has an
562 * explicit action defined at each tick.
563 *
564 * @param {...!Device} devices The specific devices to synchronize.
565 * If unspecified, the action sequences for every device will be
566 * synchronized.
567 * @return {!Actions} a self reference.
568 */
569 synchronize(...devices) {
570 let sequences
571 let max = 0
572 if (devices.length === 0) {
573 for (const s of this.sequences_.values()) {
574 max = Math.max(max, s.length)
575 }
576 sequences = this.sequences_.values()
577 } else {
578 sequences = []
579 for (const device of devices) {
580 const seq = this.sequence_(device)
581 max = Math.max(max, seq.length)
582 sequences.push(seq)
583 }
584 }
585
586 const pause = { type: Action.Type.PAUSE, duration: 0 }
587 for (const seq of sequences) {
588 while (seq.length < max) {
589 seq.push(pause)
590 }
591 }
592
593 return this
594 }
595
596 /**
597 * Inserts a pause action for the specified devices, ensuring each device is
598 * idle for a tick. The length of the pause (in milliseconds) may be specified
599 * as the first parameter to this method (defaults to 0). Otherwise, you may
600 * just specify the individual devices that should pause.
601 *
602 * If no devices are specified, a pause action will be created (using the same
603 * duration) for every device.
604 *
605 * When device synchronization is enabled (the default for new {@link Actions}
606 * objects), there is no need to specify devices as pausing one automatically
607 * pauses the others for the same duration. In other words, the following are
608 * all equivalent:
609 *
610 * let a1 = driver.actions();
611 * a1.pause(100).perform();
612 *
613 * let a2 = driver.actions();
614 * a2.pause(100, a2.keyboard()).perform();
615 * // Synchronization ensures a2.mouse() is automatically paused too.
616 *
617 * let a3 = driver.actions();
618 * a3.pause(100, a3.keyboard(), a3.mouse()).perform();
619 *
620 * When device synchronization is _disabled_, you can cause individual devices
621 * to pause during a tick. For example, to hold the SHIFT key down while
622 * moving the mouse:
623 *
624 * let actions = driver.actions({async: true});
625 *
626 * actions.keyDown(Key.SHIFT);
627 * actions.pause(actions.mouse()) // Pause for shift down
628 * .press(Button.LEFT)
629 * .move({x: 10, y: 10})
630 * .release(Button.LEFT);
631 * actions
632 * .pause(
633 * actions.keyboard(), // Pause for press left
634 * actions.keyboard(), // Pause for move
635 * actions.keyboard()) // Pause for release left
636 * .keyUp(Key.SHIFT);
637 * await actions.perform();
638 *
639 * @param {(number|!Device)=} duration The length of the pause to insert, in
640 * milliseconds. Alternatively, the duration may be omitted (yielding a
641 * default 0 ms pause), and the first device to pause may be specified.
642 * @param {...!Device} devices The devices to insert the pause for. If no
643 * devices are specified, the pause will be inserted for _all_ devices.
644 * @return {!Actions} a self reference.
645 */
646 pause(duration, ...devices) {
647 if (duration instanceof Device) {
648 devices.push(duration)
649 duration = 0
650 } else if (!duration) {
651 duration = 0
652 }
653
654 const action = { type: Action.Type.PAUSE, duration }
655
656 // NB: need a properly typed variable for type checking.
657 /** @type {!Iterable<!Device>} */
658 const iterable = devices.length === 0 ? this.sequences_.keys() : devices
659 for (const device of iterable) {
660 this.sequence_(device).push(action)
661 }
662 return this.sync_ ? this.synchronize() : this
663 }
664
665 /**
666 * Inserts an action to press a single key.
667 *
668 * @param {(Key|string|number)} key the key to press. This key may be
669 * specified as a {@link Key} value, a specific unicode code point,
670 * or a string containing a single unicode code point.
671 * @return {!Actions} a self reference.
672 */
673 keyDown(key) {
674 return this.insert(this.keyboard_, this.keyboard_.keyDown(key))
675 }
676
677 /**
678 * Inserts an action to release a single key.
679 *
680 * @param {(Key|string|number)} key the key to release. This key may be
681 * specified as a {@link Key} value, a specific unicode code point,
682 * or a string containing a single unicode code point.
683 * @return {!Actions} a self reference.
684 */
685 keyUp(key) {
686 return this.insert(this.keyboard_, this.keyboard_.keyUp(key))
687 }
688
689 /**
690 * Inserts a sequence of actions to type the provided key sequence.
691 * For each key, this will record a pair of {@linkplain #keyDown keyDown}
692 * and {@linkplain #keyUp keyUp} actions. An implication of this pairing
693 * is that modifier keys (e.g. {@link ./input.Key.SHIFT Key.SHIFT}) will
694 * always be immediately released. In other words, `sendKeys(Key.SHIFT, 'a')`
695 * is the same as typing `sendKeys('a')`, _not_ `sendKeys('A')`.
696 *
697 * @param {...(Key|string|number)} keys the keys to type.
698 * @return {!Actions} a self reference.
699 */
700 sendKeys(...keys) {
701 const actions = []
702 for (const key of keys) {
703 if (typeof key === 'string') {
704 for (const symbol of key) {
705 actions.push(
706 this.keyboard_.keyDown(symbol),
707 this.keyboard_.keyUp(symbol)
708 )
709 }
710 } else {
711 actions.push(this.keyboard_.keyDown(key), this.keyboard_.keyUp(key))
712 }
713 }
714 return this.insert(this.keyboard_, ...actions)
715 }
716
717 /**
718 * Inserts an action to press a mouse button at the mouse's current location.
719 *
720 * @param {!Button=} button The button to press; defaults to `LEFT`.
721 * @return {!Actions} a self reference.
722 */
723 press(button = Button.LEFT) {
724 return this.insert(this.mouse_, this.mouse_.press(button))
725 }
726
727 /**
728 * Inserts an action to release a mouse button at the mouse's current
729 * location.
730 *
731 * @param {!Button=} button The button to release; defaults to `LEFT`.
732 * @return {!Actions} a self reference.
733 */
734 release(button = Button.LEFT) {
735 return this.insert(this.mouse_, this.mouse_.release(button))
736 }
737
738 /**
739 * Inserts an action for moving the mouse `x` and `y` pixels relative to the
740 * specified `origin`. The `origin` may be defined as the mouse's
741 * {@linkplain ./input.Origin.POINTER current position}, the
742 * {@linkplain ./input.Origin.VIEWPORT viewport}, or the center of a specific
743 * {@linkplain ./webdriver.WebElement WebElement}.
744 *
745 * You may adjust how long the remote end should take, in milliseconds, to
746 * perform the move using the `duration` parameter (defaults to 100 ms).
747 * The number of incremental move events generated over this duration is an
748 * implementation detail for the remote end.
749 *
750 * @param {{
751 * x: (number|undefined),
752 * y: (number|undefined),
753 * duration: (number|undefined),
754 * origin: (!Origin|!./webdriver.WebElement|undefined),
755 * }=} options The move options. Defaults to moving the mouse to the top-left
756 * corner of the viewport over 100ms.
757 * @return {!Actions} a self reference.
758 */
759 move({ x = 0, y = 0, duration = 100, origin = Origin.VIEWPORT } = {}) {
760 return this.insert(
761 this.mouse_,
762 this.mouse_.move({ x, y, duration, origin })
763 )
764 }
765
766 /**
767 * Short-hand for performing a simple left-click (down/up) with the mouse.
768 *
769 * @param {./webdriver.WebElement=} element If specified, the mouse will
770 * first be moved to the center of the element before performing the
771 * click.
772 * @return {!Actions} a self reference.
773 */
774 click(element) {
775 if (element) {
776 this.move({ origin: element })
777 }
778 return this.press().release()
779 }
780
781 /**
782 * Short-hand for performing a simple right-click (down/up) with the mouse.
783 *
784 * @param {./webdriver.WebElement=} element If specified, the mouse will
785 * first be moved to the center of the element before performing the
786 * click.
787 * @return {!Actions} a self reference.
788 */
789 contextClick(element) {
790 if (element) {
791 this.move({ origin: element })
792 }
793 return this.press(Button.RIGHT).release(Button.RIGHT)
794 }
795
796 /**
797 * Short-hand for performing a double left-click with the mouse.
798 *
799 * @param {./webdriver.WebElement=} element If specified, the mouse will
800 * first be moved to the center of the element before performing the
801 * click.
802 * @return {!Actions} a self reference.
803 */
804 doubleClick(element) {
805 return this.click(element).press().release()
806 }
807
808 /**
809 * Configures a drag-and-drop action consisting of the following steps:
810 *
811 * 1. Move to the center of the `from` element (element to be dragged).
812 * 2. Press the left mouse button.
813 * 3. If the `to` target is a {@linkplain ./webdriver.WebElement WebElement},
814 * move the mouse to its center. Otherwise, move the mouse by the
815 * specified offset.
816 * 4. Release the left mouse button.
817 *
818 * @param {!./webdriver.WebElement} from The element to press the left mouse
819 * button on to start the drag.
820 * @param {(!./webdriver.WebElement|{x: number, y: number})} to Either another
821 * element to drag to (will drag to the center of the element), or an
822 * object specifying the offset to drag by, in pixels.
823 * @return {!Actions} a self reference.
824 */
825 dragAndDrop(from, to) {
826 // Do not require up top to avoid a cycle that breaks static analysis.
827 const { WebElement } = require('./webdriver')
828 if (
829 !(to instanceof WebElement) &&
830 (!to || typeof to.x !== 'number' || typeof to.y !== 'number')
831 ) {
832 throw new InvalidArgumentError(
833 'Invalid drag target; must specify a WebElement or {x, y} offset'
834 )
835 }
836
837 this.move({ origin: from }).press()
838 if (to instanceof WebElement) {
839 this.move({ origin: to })
840 } else {
841 this.move({ x: to.x, y: to.y, origin: Origin.POINTER })
842 }
843 return this.release()
844 }
845
846 /**
847 * Releases all keys, pointers, and clears internal state.
848 *
849 * @return {!Promise<void>} a promise that will resolve when finished
850 * clearing all action state.
851 */
852 clear() {
853 for (const s of this.sequences_.values()) {
854 s.length = 0
855 }
856 return this.executor_.execute(new Command(Name.CLEAR_ACTIONS))
857 }
858
859 /**
860 * Performs the configured action sequence.
861 *
862 * @return {!Promise<void>} a promise that will resolve when all actions have
863 * been completed.
864 */
865 async perform() {
866 const _actions = []
867 this.sequences_.forEach((actions, device) => {
868 if (!isIdle(actions)) {
869 actions = actions.concat() // Defensive copy.
870 _actions.push(Object.assign({ actions }, device.toJSON()))
871 }
872 })
873
874 if (_actions.length === 0) {
875 return Promise.resolve()
876 }
877
878 await this.executor_.execute(
879 new Command(Name.ACTIONS).setParameter('actions', _actions)
880 )
881 }
882}
883
884/**
885 * @param {!Array<!Action>} actions
886 * @return {boolean}
887 */
888function isIdle(actions) {
889 return (
890 actions.length === 0 ||
891 actions.every((a) => a.type === Action.Type.PAUSE && !a.duration)
892 )
893}
894
895/**
896 * Script used to compute the offset from the center of a DOM element's first
897 * client rect from the top-left corner of the element's bounding client rect.
898 * The element's center point is computed using the algorithm defined here:
899 * <https://w3c.github.io/webdriver/webdriver-spec.html#dfn-center-point>.
900 *
901 * __This is only exported for use in internal unit tests. DO NOT USE.__
902 *
903 * @package
904 */
905const INTERNAL_COMPUTE_OFFSET_SCRIPT = `
906function computeOffset(el) {
907 var rect = el.getClientRects()[0];
908 var left = Math.max(0, Math.min(rect.x, rect.x + rect.width));
909 var right =
910 Math.min(window.innerWidth, Math.max(rect.x, rect.x + rect.width));
911 var top = Math.max(0, Math.min(rect.y, rect.y + rect.height));
912 var bot =
913 Math.min(window.innerHeight, Math.max(rect.y, rect.y + rect.height));
914 var x = Math.floor(0.5 * (left + right));
915 var y = Math.floor(0.5 * (top + bot));
916
917 var bbox = el.getBoundingClientRect();
918 return [x - bbox.left, y - bbox.top];
919}
920return computeOffset(arguments[0]);`
921
922// PUBLIC API
923
924module.exports = {
925 Action, // For documentation only.
926 Actions,
927 Button,
928 Device,
929 Key,
930 Keyboard,
931 FileDetector,
932 Origin,
933 Pointer,
934 INTERNAL_COMPUTE_OFFSET_SCRIPT,
935}