UNPKG

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