actions.js 19 KB

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