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