UNPKG

19.8 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 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 */
396class 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
601module.exports = {
602 ActionSequence: ActionSequence,
603 TouchSequence: TouchSequence,
604};