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 | ;
|
19 |
|
20 | const command = require('./command');
|
21 | const error = require('./error');
|
22 | const input = require('./input');
|
23 |
|
24 |
|
25 | /**
|
26 | * @param {!IArrayLike} args .
|
27 | * @return {!Array} .
|
28 | */
|
29 | function flatten(args) {
|
30 | let result = [];
|
31 | for (let i = 0; i < args.length; i++) {
|
32 | let element = args[i];
|
33 | if (Array.isArray(element)) {
|
34 | result.push.apply(result, flatten(element));
|
35 | } else {
|
36 | result.push(element);
|
37 | }
|
38 | }
|
39 | return result;
|
40 | }
|
41 |
|
42 |
|
43 | const MODIFIER_KEYS = new Set([
|
44 | input.Key.ALT,
|
45 | input.Key.CONTROL,
|
46 | input.Key.SHIFT,
|
47 | input.Key.COMMAND
|
48 | ]);
|
49 |
|
50 |
|
51 | /**
|
52 | * Checks that a key is a modifier key.
|
53 | * @param {!input.Key} key The key to check.
|
54 | * @throws {error.InvalidArgumentError} If the key is not a modifier key.
|
55 | * @private
|
56 | */
|
57 | function checkModifierKey(key) {
|
58 | if (!MODIFIER_KEYS.has(key)) {
|
59 | throw new error.InvalidArgumentError('Not a modifier key');
|
60 | }
|
61 | }
|
62 |
|
63 |
|
64 | /**
|
65 | * Class for defining sequences of complex user interactions. Each sequence
|
66 | * will not be executed until {@link #perform} is called.
|
67 | *
|
68 | * This class should not be instantiated directly. Instead, obtain an instance
|
69 | * using {@link ./webdriver.WebDriver#actions() WebDriver.actions()}.
|
70 | *
|
71 | * Sample usage:
|
72 | *
|
73 | * driver.actions().
|
74 | * keyDown(Key.SHIFT).
|
75 | * click(element1).
|
76 | * click(element2).
|
77 | * dragAndDrop(element3, element4).
|
78 | * keyUp(Key.SHIFT).
|
79 | * perform();
|
80 | *
|
81 | */
|
82 | class ActionSequence {
|
83 | /**
|
84 | * @param {!./webdriver.WebDriver} driver The driver that should be used to
|
85 | * perform this action sequence.
|
86 | */
|
87 | constructor(driver) {
|
88 | /** @private {!./webdriver.WebDriver} */
|
89 | this.driver_ = driver;
|
90 |
|
91 | /** @private {!Array<{description: string, command: !command.Command}>} */
|
92 | this.actions_ = [];
|
93 | }
|
94 |
|
95 | /**
|
96 | * Schedules an action to be executed each time {@link #perform} is called on
|
97 | * this instance.
|
98 | *
|
99 | * @param {string} description A description of the command.
|
100 | * @param {!command.Command} command The command.
|
101 | * @private
|
102 | */
|
103 | schedule_(description, command) {
|
104 | this.actions_.push({
|
105 | description: description,
|
106 | command: command
|
107 | });
|
108 | }
|
109 |
|
110 | /**
|
111 | * Executes this action sequence.
|
112 | *
|
113 | * @return {!./promise.Thenable} A promise that will be resolved once
|
114 | * this sequence has completed.
|
115 | */
|
116 | perform() {
|
117 | // Make a protected copy of the scheduled actions. This will protect against
|
118 | // users defining additional commands before this sequence is actually
|
119 | // executed.
|
120 | let actions = this.actions_.concat();
|
121 | let driver = this.driver_;
|
122 | return driver.controlFlow().execute(function() {
|
123 | let results = actions.map(action => {
|
124 | return driver.schedule(action.command, action.description);
|
125 | });
|
126 | return Promise.all(results);
|
127 | }, 'ActionSequence.perform');
|
128 | }
|
129 |
|
130 | /**
|
131 | * Moves the mouse. The location to move to may be specified in terms of the
|
132 | * mouse's current location, an offset relative to the top-left corner of an
|
133 | * element, or an element (in which case the middle of the element is used).
|
134 | *
|
135 | * @param {(!./webdriver.WebElement|{x: number, y: number})} location The
|
136 | * location to drag to, as either another WebElement or an offset in
|
137 | * pixels.
|
138 | * @param {{x: number, y: number}=} opt_offset If the target {@code location}
|
139 | * is defined as a {@link ./webdriver.WebElement}, this parameter defines
|
140 | * an offset within that element. The offset should be specified in pixels
|
141 | * relative to the top-left corner of the element's bounding box. If
|
142 | * omitted, the element's center will be used as the target offset.
|
143 | * @return {!ActionSequence} A self reference.
|
144 | */
|
145 | mouseMove(location, opt_offset) {
|
146 | let cmd = new command.Command(command.Name.MOVE_TO);
|
147 |
|
148 | if (typeof location.x === 'number') {
|
149 | setOffset(/** @type {{x: number, y: number}} */(location));
|
150 | } else {
|
151 | cmd.setParameter('element', location.getId());
|
152 | if (opt_offset) {
|
153 | setOffset(opt_offset);
|
154 | }
|
155 | }
|
156 |
|
157 | this.schedule_('mouseMove', cmd);
|
158 | return this;
|
159 |
|
160 | /** @param {{x: number, y: number}} offset The offset to use. */
|
161 | function setOffset(offset) {
|
162 | cmd.setParameter('xoffset', offset.x || 0);
|
163 | cmd.setParameter('yoffset', offset.y || 0);
|
164 | }
|
165 | }
|
166 |
|
167 | /**
|
168 | * Schedules a mouse action.
|
169 | * @param {string} description A simple descriptive label for the scheduled
|
170 | * action.
|
171 | * @param {!command.Name} commandName The name of the command.
|
172 | * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
|
173 | * the element to interact with or the button to click with.
|
174 | * Defaults to {@link input.Button.LEFT} if neither an element nor
|
175 | * button is specified.
|
176 | * @param {input.Button=} opt_button The button to use. Defaults to
|
177 | * {@link input.Button.LEFT}. Ignored if the previous argument is
|
178 | * provided as a button.
|
179 | * @return {!ActionSequence} A self reference.
|
180 | * @private
|
181 | */
|
182 | scheduleMouseAction_(
|
183 | description, commandName, opt_elementOrButton, opt_button) {
|
184 | let button;
|
185 | if (typeof opt_elementOrButton === 'number') {
|
186 | button = opt_elementOrButton;
|
187 | } else {
|
188 | if (opt_elementOrButton) {
|
189 | this.mouseMove(
|
190 | /** @type {!./webdriver.WebElement} */ (opt_elementOrButton));
|
191 | }
|
192 | button = opt_button !== void(0) ? opt_button : input.Button.LEFT;
|
193 | }
|
194 |
|
195 | let cmd = new command.Command(commandName).
|
196 | setParameter('button', button);
|
197 | this.schedule_(description, cmd);
|
198 | return this;
|
199 | }
|
200 |
|
201 | /**
|
202 | * Presses a mouse button. The mouse button will not be released until
|
203 | * {@link #mouseUp} is called, regardless of whether that call is made in this
|
204 | * sequence or another. The behavior for out-of-order events (e.g. mouseDown,
|
205 | * click) is undefined.
|
206 | *
|
207 | * If an element is provided, the mouse will first be moved to the center
|
208 | * of that element. This is equivalent to:
|
209 | *
|
210 | * sequence.mouseMove(element).mouseDown()
|
211 | *
|
212 | * Warning: this method currently only supports the left mouse button. See
|
213 | * [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047).
|
214 | *
|
215 | * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
|
216 | * the element to interact with or the button to click with.
|
217 | * Defaults to {@link input.Button.LEFT} if neither an element nor
|
218 | * button is specified.
|
219 | * @param {input.Button=} opt_button The button to use. Defaults to
|
220 | * {@link input.Button.LEFT}. Ignored if a button is provided as the
|
221 | * first argument.
|
222 | * @return {!ActionSequence} A self reference.
|
223 | */
|
224 | mouseDown(opt_elementOrButton, opt_button) {
|
225 | return this.scheduleMouseAction_('mouseDown',
|
226 | command.Name.MOUSE_DOWN, opt_elementOrButton, opt_button);
|
227 | }
|
228 |
|
229 | /**
|
230 | * Releases a mouse button. Behavior is undefined for calling this function
|
231 | * without a previous call to {@link #mouseDown}.
|
232 | *
|
233 | * If an element is provided, the mouse will first be moved to the center
|
234 | * of that element. This is equivalent to:
|
235 | *
|
236 | * sequence.mouseMove(element).mouseUp()
|
237 | *
|
238 | * Warning: this method currently only supports the left mouse button. See
|
239 | * [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047).
|
240 | *
|
241 | * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
|
242 | * the element to interact with or the button to click with.
|
243 | * Defaults to {@link input.Button.LEFT} if neither an element nor
|
244 | * button is specified.
|
245 | * @param {input.Button=} opt_button The button to use. Defaults to
|
246 | * {@link input.Button.LEFT}. Ignored if a button is provided as the
|
247 | * first argument.
|
248 | * @return {!ActionSequence} A self reference.
|
249 | */
|
250 | mouseUp(opt_elementOrButton, opt_button) {
|
251 | return this.scheduleMouseAction_('mouseUp',
|
252 | command.Name.MOUSE_UP, opt_elementOrButton, opt_button);
|
253 | }
|
254 |
|
255 | /**
|
256 | * Convenience function for performing a "drag and drop" manuever. The target
|
257 | * element may be moved to the location of another element, or by an offset (in
|
258 | * pixels).
|
259 | *
|
260 | * @param {!./webdriver.WebElement} element The element to drag.
|
261 | * @param {(!./webdriver.WebElement|{x: number, y: number})} location The
|
262 | * location to drag to, either as another WebElement or an offset in
|
263 | * pixels.
|
264 | * @return {!ActionSequence} A self reference.
|
265 | */
|
266 | dragAndDrop(element, location) {
|
267 | return this.mouseDown(element).mouseMove(location).mouseUp();
|
268 | }
|
269 |
|
270 | /**
|
271 | * Clicks a mouse button.
|
272 | *
|
273 | * If an element is provided, the mouse will first be moved to the center
|
274 | * of that element. This is equivalent to:
|
275 | *
|
276 | * sequence.mouseMove(element).click()
|
277 | *
|
278 | * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
|
279 | * the element to interact with or the button to click with.
|
280 | * Defaults to {@link input.Button.LEFT} if neither an element nor
|
281 | * button is specified.
|
282 | * @param {input.Button=} opt_button The button to use. Defaults to
|
283 | * {@link input.Button.LEFT}. Ignored if a button is provided as the
|
284 | * first argument.
|
285 | * @return {!ActionSequence} A self reference.
|
286 | */
|
287 | click(opt_elementOrButton, opt_button) {
|
288 | return this.scheduleMouseAction_('click',
|
289 | command.Name.CLICK, opt_elementOrButton, opt_button);
|
290 | }
|
291 |
|
292 | /**
|
293 | * Double-clicks a mouse button.
|
294 | *
|
295 | * If an element is provided, the mouse will first be moved to the center of
|
296 | * that element. This is equivalent to:
|
297 | *
|
298 | * sequence.mouseMove(element).doubleClick()
|
299 | *
|
300 | * Warning: this method currently only supports the left mouse button. See
|
301 | * [issue 4047](http://code.google.com/p/selenium/issues/detail?id=4047).
|
302 | *
|
303 | * @param {(./webdriver.WebElement|input.Button)=} opt_elementOrButton Either
|
304 | * the element to interact with or the button to click with.
|
305 | * Defaults to {@link input.Button.LEFT} if neither an element nor
|
306 | * button is specified.
|
307 | * @param {input.Button=} opt_button The button to use. Defaults to
|
308 | * {@link input.Button.LEFT}. Ignored if a button is provided as the
|
309 | * first argument.
|
310 | * @return {!ActionSequence} A self reference.
|
311 | */
|
312 | doubleClick(opt_elementOrButton, opt_button) {
|
313 | return this.scheduleMouseAction_('doubleClick',
|
314 | command.Name.DOUBLE_CLICK, opt_elementOrButton, opt_button);
|
315 | }
|
316 |
|
317 | /**
|
318 | * Schedules a keyboard action.
|
319 | *
|
320 | * @param {string} description A simple descriptive label for the scheduled
|
321 | * action.
|
322 | * @param {!Array<(string|!input.Key)>} keys The keys to send.
|
323 | * @return {!ActionSequence} A self reference.
|
324 | * @private
|
325 | */
|
326 | scheduleKeyboardAction_(description, keys) {
|
327 | let cmd = new command.Command(command.Name.SEND_KEYS_TO_ACTIVE_ELEMENT)
|
328 | .setParameter('value', keys);
|
329 | this.schedule_(description, cmd);
|
330 | return this;
|
331 | }
|
332 |
|
333 | /**
|
334 | * Performs a modifier key press. The modifier key is <em>not released</em>
|
335 | * until {@link #keyUp} or {@link #sendKeys} is called. The key press will be
|
336 | * targetted at the currently focused element.
|
337 | *
|
338 | * @param {!input.Key} key The modifier key to push. Must be one of
|
339 | * {ALT, CONTROL, SHIFT, COMMAND, META}.
|
340 | * @return {!ActionSequence} A self reference.
|
341 | * @throws {error.InvalidArgumentError} If the key is not a valid modifier
|
342 | * key.
|
343 | */
|
344 | keyDown(key) {
|
345 | checkModifierKey(key);
|
346 | return this.scheduleKeyboardAction_('keyDown', [key]);
|
347 | }
|
348 |
|
349 | /**
|
350 | * Performs a modifier key release. The release is targetted at the currently
|
351 | * focused element.
|
352 | * @param {!input.Key} key The modifier key to release. Must be one of
|
353 | * {ALT, CONTROL, SHIFT, COMMAND, META}.
|
354 | * @return {!ActionSequence} A self reference.
|
355 | * @throws {error.InvalidArgumentError} If the key is not a valid modifier
|
356 | * key.
|
357 | */
|
358 | keyUp(key) {
|
359 | checkModifierKey(key);
|
360 | return this.scheduleKeyboardAction_('keyUp', [key]);
|
361 | }
|
362 |
|
363 | /**
|
364 | * Simulates typing multiple keys. Each modifier key encountered in the
|
365 | * sequence will not be released until it is encountered again. All key events
|
366 | * will be targetted at the currently focused element.
|
367 | *
|
368 | * @param {...(string|!input.Key|!Array<(string|!input.Key)>)} var_args
|
369 | * The keys to type.
|
370 | * @return {!ActionSequence} A self reference.
|
371 | * @throws {Error} If the key is not a valid modifier key.
|
372 | */
|
373 | sendKeys(var_args) {
|
374 | let keys = flatten(arguments);
|
375 | return this.scheduleKeyboardAction_('sendKeys', keys);
|
376 | }
|
377 | }
|
378 |
|
379 |
|
380 | /**
|
381 | * Class for defining sequences of user touch interactions. Each sequence
|
382 | * will not be executed until {@link #perform} is called.
|
383 | *
|
384 | * This class should not be instantiated directly. Instead, obtain an instance
|
385 | * using {@link ./webdriver.WebDriver#touchActions() WebDriver.touchActions()}.
|
386 | *
|
387 | * Sample usage:
|
388 | *
|
389 | * driver.touchActions().
|
390 | * tapAndHold({x: 0, y: 0}).
|
391 | * move({x: 3, y: 4}).
|
392 | * release({x: 10, y: 10}).
|
393 | * perform();
|
394 | *
|
395 | */
|
396 | class TouchSequence {
|
397 | /**
|
398 | * @param {!./webdriver.WebDriver} driver The driver that should be used to
|
399 | * perform this action sequence.
|
400 | */
|
401 | constructor(driver) {
|
402 | /** @private {!./webdriver.WebDriver} */
|
403 | this.driver_ = driver;
|
404 |
|
405 | /** @private {!Array<{description: string, command: !command.Command}>} */
|
406 | this.actions_ = [];
|
407 | }
|
408 |
|
409 | /**
|
410 | * Schedules an action to be executed each time {@link #perform} is called on
|
411 | * this instance.
|
412 | * @param {string} description A description of the command.
|
413 | * @param {!command.Command} command The command.
|
414 | * @private
|
415 | */
|
416 | schedule_(description, command) {
|
417 | this.actions_.push({
|
418 | description: description,
|
419 | command: command
|
420 | });
|
421 | }
|
422 |
|
423 | /**
|
424 | * Executes this action sequence.
|
425 | * @return {!./promise.Thenable} A promise that will be resolved once
|
426 | * this sequence has completed.
|
427 | */
|
428 | perform() {
|
429 | // Make a protected copy of the scheduled actions. This will protect against
|
430 | // users defining additional commands before this sequence is actually
|
431 | // executed.
|
432 | let actions = this.actions_.concat();
|
433 | let driver = this.driver_;
|
434 | return driver.controlFlow().execute(function() {
|
435 | let results = actions.map(action => {
|
436 | return driver.schedule(action.command, action.description);
|
437 | });
|
438 | return Promise.all(results);
|
439 | }, 'TouchSequence.perform');
|
440 | }
|
441 |
|
442 | /**
|
443 | * Taps an element.
|
444 | *
|
445 | * @param {!./webdriver.WebElement} elem The element to tap.
|
446 | * @return {!TouchSequence} A self reference.
|
447 | */
|
448 | tap(elem) {
|
449 | let cmd = new command.Command(command.Name.TOUCH_SINGLE_TAP).
|
450 | setParameter('element', elem.getId());
|
451 |
|
452 | this.schedule_('tap', cmd);
|
453 | return this;
|
454 | }
|
455 |
|
456 | /**
|
457 | * Double taps an element.
|
458 | *
|
459 | * @param {!./webdriver.WebElement} elem The element to double tap.
|
460 | * @return {!TouchSequence} A self reference.
|
461 | */
|
462 | doubleTap(elem) {
|
463 | let cmd = new command.Command(command.Name.TOUCH_DOUBLE_TAP).
|
464 | setParameter('element', elem.getId());
|
465 |
|
466 | this.schedule_('doubleTap', cmd);
|
467 | return this;
|
468 | }
|
469 |
|
470 | /**
|
471 | * Long press on an element.
|
472 | *
|
473 | * @param {!./webdriver.WebElement} elem The element to long press.
|
474 | * @return {!TouchSequence} A self reference.
|
475 | */
|
476 | longPress(elem) {
|
477 | let cmd = new command.Command(command.Name.TOUCH_LONG_PRESS).
|
478 | setParameter('element', elem.getId());
|
479 |
|
480 | this.schedule_('longPress', cmd);
|
481 | return this;
|
482 | }
|
483 |
|
484 | /**
|
485 | * Touch down at the given location.
|
486 | *
|
487 | * @param {{x: number, y: number}} location The location to touch down at.
|
488 | * @return {!TouchSequence} A self reference.
|
489 | */
|
490 | tapAndHold(location) {
|
491 | let cmd = new command.Command(command.Name.TOUCH_DOWN).
|
492 | setParameter('x', location.x).
|
493 | setParameter('y', location.y);
|
494 |
|
495 | this.schedule_('tapAndHold', cmd);
|
496 | return this;
|
497 | }
|
498 |
|
499 | /**
|
500 | * Move a held {@linkplain #tapAndHold touch} to the specified location.
|
501 | *
|
502 | * @param {{x: number, y: number}} location The location to move to.
|
503 | * @return {!TouchSequence} A self reference.
|
504 | */
|
505 | move(location) {
|
506 | let cmd = new command.Command(command.Name.TOUCH_MOVE).
|
507 | setParameter('x', location.x).
|
508 | setParameter('y', location.y);
|
509 |
|
510 | this.schedule_('move', cmd);
|
511 | return this;
|
512 | }
|
513 |
|
514 | /**
|
515 | * Release a held {@linkplain #tapAndHold touch} at the specified location.
|
516 | *
|
517 | * @param {{x: number, y: number}} location The location to release at.
|
518 | * @return {!TouchSequence} A self reference.
|
519 | */
|
520 | release(location) {
|
521 | let cmd = new command.Command(command.Name.TOUCH_UP).
|
522 | setParameter('x', location.x).
|
523 | setParameter('y', location.y);
|
524 |
|
525 | this.schedule_('release', cmd);
|
526 | return this;
|
527 | }
|
528 |
|
529 | /**
|
530 | * Scrolls the touch screen by the given offset.
|
531 | *
|
532 | * @param {{x: number, y: number}} offset The offset to scroll to.
|
533 | * @return {!TouchSequence} A self reference.
|
534 | */
|
535 | scroll(offset) {
|
536 | let cmd = new command.Command(command.Name.TOUCH_SCROLL).
|
537 | setParameter('xoffset', offset.x).
|
538 | setParameter('yoffset', offset.y);
|
539 |
|
540 | this.schedule_('scroll', cmd);
|
541 | return this;
|
542 | }
|
543 |
|
544 | /**
|
545 | * Scrolls the touch screen, starting on `elem` and moving by the specified
|
546 | * offset.
|
547 | *
|
548 | * @param {!./webdriver.WebElement} elem The element where scroll starts.
|
549 | * @param {{x: number, y: number}} offset The offset to scroll to.
|
550 | * @return {!TouchSequence} A self reference.
|
551 | */
|
552 | scrollFromElement(elem, offset) {
|
553 | let cmd = new command.Command(command.Name.TOUCH_SCROLL).
|
554 | setParameter('element', elem.getId()).
|
555 | setParameter('xoffset', offset.x).
|
556 | setParameter('yoffset', offset.y);
|
557 |
|
558 | this.schedule_('scrollFromElement', cmd);
|
559 | return this;
|
560 | }
|
561 |
|
562 | /**
|
563 | * Flick, starting anywhere on the screen, at speed xspeed and yspeed.
|
564 | *
|
565 | * @param {{xspeed: number, yspeed: number}} speed The speed to flick in each
|
566 | direction, in pixels per second.
|
567 | * @return {!TouchSequence} A self reference.
|
568 | */
|
569 | flick(speed) {
|
570 | let cmd = new command.Command(command.Name.TOUCH_FLICK).
|
571 | setParameter('xspeed', speed.xspeed).
|
572 | setParameter('yspeed', speed.yspeed);
|
573 |
|
574 | this.schedule_('flick', cmd);
|
575 | return this;
|
576 | }
|
577 |
|
578 | /**
|
579 | * Flick starting at elem and moving by x and y at specified speed.
|
580 | *
|
581 | * @param {!./webdriver.WebElement} elem The element where flick starts.
|
582 | * @param {{x: number, y: number}} offset The offset to flick to.
|
583 | * @param {number} speed The speed to flick at in pixels per second.
|
584 | * @return {!TouchSequence} A self reference.
|
585 | */
|
586 | flickElement(elem, offset, speed) {
|
587 | let cmd = new command.Command(command.Name.TOUCH_FLICK).
|
588 | setParameter('element', elem.getId()).
|
589 | setParameter('xoffset', offset.x).
|
590 | setParameter('yoffset', offset.y).
|
591 | setParameter('speed', speed);
|
592 |
|
593 | this.schedule_('flickElement', cmd);
|
594 | return this;
|
595 | }
|
596 | }
|
597 |
|
598 |
|
599 | // PUBLIC API
|
600 |
|
601 | module.exports = {
|
602 | ActionSequence: ActionSequence,
|
603 | TouchSequence: TouchSequence,
|
604 | };
|