legacyaction.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  5. Cu.import("resource://gre/modules/Log.jsm");
  6. Cu.import("resource://gre/modules/Preferences.jsm");
  7. Cu.import("chrome://marionette/content/element.js");
  8. Cu.import("chrome://marionette/content/event.js");
  9. const CONTEXT_MENU_DELAY_PREF = "ui.click_hold_context_menus.delay";
  10. const DEFAULT_CONTEXT_MENU_DELAY = 750; // ms
  11. this.EXPORTED_SYMBOLS = ["legacyaction"];
  12. const logger = Log.repository.getLogger("Marionette");
  13. this.legacyaction = this.action = {};
  14. /**
  15. * Functionality for (single finger) action chains.
  16. */
  17. action.Chain = function (checkForInterrupted) {
  18. // for assigning unique ids to all touches
  19. this.nextTouchId = 1000;
  20. // keep track of active Touches
  21. this.touchIds = {};
  22. // last touch for each fingerId
  23. this.lastCoordinates = null;
  24. this.isTap = false;
  25. this.scrolling = false;
  26. // whether to send mouse event
  27. this.mouseEventsOnly = false;
  28. this.checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  29. if (typeof checkForInterrupted == "function") {
  30. this.checkForInterrupted = checkForInterrupted;
  31. } else {
  32. this.checkForInterrupted = () => {};
  33. }
  34. // determines if we create touch events
  35. this.inputSource = null;
  36. };
  37. action.Chain.prototype.dispatchActions = function (
  38. args,
  39. touchId,
  40. container,
  41. seenEls,
  42. touchProvider) {
  43. // Some touch events code in the listener needs to do ipc, so we can't
  44. // share this code across chrome/content.
  45. if (touchProvider) {
  46. this.touchProvider = touchProvider;
  47. }
  48. this.seenEls = seenEls;
  49. this.container = container;
  50. let commandArray = element.fromJson(
  51. args, seenEls, container.frame, container.shadowRoot);
  52. if (touchId == null) {
  53. touchId = this.nextTouchId++;
  54. }
  55. if (!container.frame.document.createTouch) {
  56. this.mouseEventsOnly = true;
  57. }
  58. let keyModifiers = {
  59. shiftKey: false,
  60. ctrlKey: false,
  61. altKey: false,
  62. metaKey: false,
  63. };
  64. return new Promise(resolve => {
  65. this.actions(commandArray, touchId, 0, keyModifiers, resolve);
  66. }).catch(this.resetValues);
  67. };
  68. /**
  69. * This function emit mouse event.
  70. *
  71. * @param {Document} doc
  72. * Current document.
  73. * @param {string} type
  74. * Type of event to dispatch.
  75. * @param {number} clickCount
  76. * Number of clicks, button notes the mouse button.
  77. * @param {number} elClientX
  78. * X coordinate of the mouse relative to the viewport.
  79. * @param {number} elClientY
  80. * Y coordinate of the mouse relative to the viewport.
  81. * @param {Object} modifiers
  82. * An object of modifier keys present.
  83. */
  84. action.Chain.prototype.emitMouseEvent = function (
  85. doc,
  86. type,
  87. elClientX,
  88. elClientY,
  89. button,
  90. clickCount,
  91. modifiers) {
  92. if (!this.checkForInterrupted()) {
  93. logger.debug(`Emitting ${type} mouse event ` +
  94. `at coordinates (${elClientX}, ${elClientY}) ` +
  95. `relative to the viewport, ` +
  96. `button: ${button}, ` +
  97. `clickCount: ${clickCount}`);
  98. let win = doc.defaultView;
  99. let domUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
  100. .getInterface(Ci.nsIDOMWindowUtils);
  101. let mods;
  102. if (typeof modifiers != "undefined") {
  103. mods = event.parseModifiers_(modifiers);
  104. } else {
  105. mods = 0;
  106. }
  107. domUtils.sendMouseEvent(
  108. type,
  109. elClientX,
  110. elClientY,
  111. button || 0,
  112. clickCount || 1,
  113. mods,
  114. false,
  115. 0,
  116. this.inputSource);
  117. }
  118. };
  119. /**
  120. * Reset any persisted values after a command completes.
  121. */
  122. action.Chain.prototype.resetValues = function() {
  123. this.container = null;
  124. this.seenEls = null;
  125. this.touchProvider = null;
  126. this.mouseEventsOnly = false;
  127. };
  128. /**
  129. * Emit events for each action in the provided chain.
  130. *
  131. * To emit touch events for each finger, one might send a [["press", id],
  132. * ["wait", 5], ["release"]] chain.
  133. *
  134. * @param {Array.<Array<?>>} chain
  135. * A multi-dimensional array of actions.
  136. * @param {Object.<string, number>} touchId
  137. * Represents the finger ID.
  138. * @param {number} i
  139. * Keeps track of the current action of the chain.
  140. * @param {Object.<string, boolean>} keyModifiers
  141. * Keeps track of keyDown/keyUp pairs through an action chain.
  142. * @param {function(?)} cb
  143. * Called on success.
  144. *
  145. * @return {Object.<string, number>}
  146. * Last finger ID, or an empty object.
  147. */
  148. action.Chain.prototype.actions = function (chain, touchId, i, keyModifiers, cb) {
  149. if (i == chain.length) {
  150. cb(touchId || null);
  151. this.resetValues();
  152. return;
  153. }
  154. let pack = chain[i];
  155. let command = pack[0];
  156. let el;
  157. let c;
  158. i++;
  159. if (["press", "wait", "keyDown", "keyUp", "click"].indexOf(command) == -1) {
  160. // if mouseEventsOnly, then touchIds isn't used
  161. if (!(touchId in this.touchIds) && !this.mouseEventsOnly) {
  162. this.resetValues();
  163. throw new WebDriverError("Element has not been pressed");
  164. }
  165. }
  166. switch (command) {
  167. case "keyDown":
  168. event.sendKeyDown(pack[1], keyModifiers, this.container.frame);
  169. this.actions(chain, touchId, i, keyModifiers, cb);
  170. break;
  171. case "keyUp":
  172. event.sendKeyUp(pack[1], keyModifiers, this.container.frame);
  173. this.actions(chain, touchId, i, keyModifiers, cb);
  174. break;
  175. case "click":
  176. el = this.seenEls.get(pack[1], this.container);
  177. let button = pack[2];
  178. let clickCount = pack[3];
  179. c = element.coordinates(el);
  180. this.mouseTap(el.ownerDocument, c.x, c.y, button, clickCount, keyModifiers);
  181. if (button == 2) {
  182. this.emitMouseEvent(el.ownerDocument, "contextmenu", c.x, c.y,
  183. button, clickCount, keyModifiers);
  184. }
  185. this.actions(chain, touchId, i, keyModifiers, cb);
  186. break;
  187. case "press":
  188. if (this.lastCoordinates) {
  189. this.generateEvents(
  190. "cancel",
  191. this.lastCoordinates[0],
  192. this.lastCoordinates[1],
  193. touchId,
  194. null,
  195. keyModifiers);
  196. this.resetValues();
  197. throw new WebDriverError(
  198. "Invalid Command: press cannot follow an active touch event");
  199. }
  200. // look ahead to check if we're scrolling,
  201. // needed for APZ touch dispatching
  202. if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) {
  203. this.scrolling = true;
  204. }
  205. el = this.seenEls.get(pack[1], this.container);
  206. c = element.coordinates(el, pack[2], pack[3]);
  207. touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers);
  208. this.actions(chain, touchId, i, keyModifiers, cb);
  209. break;
  210. case "release":
  211. this.generateEvents(
  212. "release",
  213. this.lastCoordinates[0],
  214. this.lastCoordinates[1],
  215. touchId,
  216. null,
  217. keyModifiers);
  218. this.actions(chain, null, i, keyModifiers, cb);
  219. this.scrolling = false;
  220. break;
  221. case "move":
  222. el = this.seenEls.get(pack[1], this.container);
  223. c = element.coordinates(el);
  224. this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers);
  225. this.actions(chain, touchId, i, keyModifiers, cb);
  226. break;
  227. case "moveByOffset":
  228. this.generateEvents(
  229. "move",
  230. this.lastCoordinates[0] + pack[1],
  231. this.lastCoordinates[1] + pack[2],
  232. touchId,
  233. null,
  234. keyModifiers);
  235. this.actions(chain, touchId, i, keyModifiers, cb);
  236. break;
  237. case "wait":
  238. if (pack[1] != null) {
  239. let time = pack[1] * 1000;
  240. // standard waiting time to fire contextmenu
  241. let standard = Preferences.get(
  242. CONTEXT_MENU_DELAY_PREF,
  243. DEFAULT_CONTEXT_MENU_DELAY);
  244. if (time >= standard && this.isTap) {
  245. chain.splice(i, 0, ["longPress"], ["wait", (time - standard) / 1000]);
  246. time = standard;
  247. }
  248. this.checkTimer.initWithCallback(
  249. () => this.actions(chain, touchId, i, keyModifiers, cb),
  250. time, Ci.nsITimer.TYPE_ONE_SHOT);
  251. } else {
  252. this.actions(chain, touchId, i, keyModifiers, cb);
  253. }
  254. break;
  255. case "cancel":
  256. this.generateEvents(
  257. "cancel",
  258. this.lastCoordinates[0],
  259. this.lastCoordinates[1],
  260. touchId,
  261. null,
  262. keyModifiers);
  263. this.actions(chain, touchId, i, keyModifiers, cb);
  264. this.scrolling = false;
  265. break;
  266. case "longPress":
  267. this.generateEvents(
  268. "contextmenu",
  269. this.lastCoordinates[0],
  270. this.lastCoordinates[1],
  271. touchId,
  272. null,
  273. keyModifiers);
  274. this.actions(chain, touchId, i, keyModifiers, cb);
  275. break;
  276. }
  277. };
  278. /**
  279. * Given an element and a pair of coordinates, returns an array of the
  280. * form [clientX, clientY, pageX, pageY, screenX, screenY].
  281. */
  282. action.Chain.prototype.getCoordinateInfo = function (el, corx, cory) {
  283. let win = el.ownerDocument.defaultView;
  284. return [
  285. corx, // clientX
  286. cory, // clientY
  287. corx + win.pageXOffset, // pageX
  288. cory + win.pageYOffset, // pageY
  289. corx + win.mozInnerScreenX, // screenX
  290. cory + win.mozInnerScreenY // screenY
  291. ];
  292. };
  293. /**
  294. * @param {number} x
  295. * X coordinate of the location to generate the event that is relative
  296. * to the viewport.
  297. * @param {number} y
  298. * Y coordinate of the location to generate the event that is relative
  299. * to the viewport.
  300. */
  301. action.Chain.prototype.generateEvents = function (
  302. type, x, y, touchId, target, keyModifiers) {
  303. this.lastCoordinates = [x, y];
  304. let doc = this.container.frame.document;
  305. switch (type) {
  306. case "tap":
  307. if (this.mouseEventsOnly) {
  308. this.mouseTap(
  309. touch.target.ownerDocument,
  310. touch.clientX,
  311. touch.clientY,
  312. null,
  313. null,
  314. keyModifiers);
  315. } else {
  316. touchId = this.nextTouchId++;
  317. let touch = this.touchProvider.createATouch(target, x, y, touchId);
  318. this.touchProvider.emitTouchEvent("touchstart", touch);
  319. this.touchProvider.emitTouchEvent("touchend", touch);
  320. this.mouseTap(
  321. touch.target.ownerDocument,
  322. touch.clientX,
  323. touch.clientY,
  324. null,
  325. null,
  326. keyModifiers);
  327. }
  328. this.lastCoordinates = null;
  329. break;
  330. case "press":
  331. this.isTap = true;
  332. if (this.mouseEventsOnly) {
  333. this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
  334. this.emitMouseEvent(doc, "mousedown", x, y, null, null, keyModifiers);
  335. } else {
  336. touchId = this.nextTouchId++;
  337. let touch = this.touchProvider.createATouch(target, x, y, touchId);
  338. this.touchProvider.emitTouchEvent("touchstart", touch);
  339. this.touchIds[touchId] = touch;
  340. return touchId;
  341. }
  342. break;
  343. case "release":
  344. if (this.mouseEventsOnly) {
  345. let [x, y] = this.lastCoordinates;
  346. this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
  347. } else {
  348. let touch = this.touchIds[touchId];
  349. let [x, y] = this.lastCoordinates;
  350. touch = this.touchProvider.createATouch(touch.target, x, y, touchId);
  351. this.touchProvider.emitTouchEvent("touchend", touch);
  352. if (this.isTap) {
  353. this.mouseTap(
  354. touch.target.ownerDocument,
  355. touch.clientX,
  356. touch.clientY,
  357. null,
  358. null,
  359. keyModifiers);
  360. }
  361. delete this.touchIds[touchId];
  362. }
  363. this.isTap = false;
  364. this.lastCoordinates = null;
  365. break;
  366. case "cancel":
  367. this.isTap = false;
  368. if (this.mouseEventsOnly) {
  369. let [x, y] = this.lastCoordinates;
  370. this.emitMouseEvent(doc, "mouseup", x, y, null, null, keyModifiers);
  371. } else {
  372. this.touchProvider.emitTouchEvent("touchcancel", this.touchIds[touchId]);
  373. delete this.touchIds[touchId];
  374. }
  375. this.lastCoordinates = null;
  376. break;
  377. case "move":
  378. this.isTap = false;
  379. if (this.mouseEventsOnly) {
  380. this.emitMouseEvent(doc, "mousemove", x, y, null, null, keyModifiers);
  381. } else {
  382. let touch = this.touchProvider.createATouch(
  383. this.touchIds[touchId].target, x, y, touchId);
  384. this.touchIds[touchId] = touch;
  385. this.touchProvider.emitTouchEvent("touchmove", touch);
  386. }
  387. break;
  388. case "contextmenu":
  389. this.isTap = false;
  390. let event = this.container.frame.document.createEvent("MouseEvents");
  391. if (this.mouseEventsOnly) {
  392. target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]);
  393. } else {
  394. target = this.touchIds[touchId].target;
  395. }
  396. let [clientX, clientY, pageX, pageY, screenX, screenY] =
  397. this.getCoordinateInfo(target, x, y);
  398. event.initMouseEvent(
  399. "contextmenu",
  400. true,
  401. true,
  402. target.ownerDocument.defaultView,
  403. 1,
  404. screenX,
  405. screenY,
  406. clientX,
  407. clientY,
  408. false,
  409. false,
  410. false,
  411. false,
  412. 0,
  413. null);
  414. target.dispatchEvent(event);
  415. break;
  416. default:
  417. throw new WebDriverError("Unknown event type: " + type);
  418. }
  419. this.checkForInterrupted();
  420. };
  421. action.Chain.prototype.mouseTap = function (doc, x, y, button, count, mod) {
  422. this.emitMouseEvent(doc, "mousemove", x, y, button, count, mod);
  423. this.emitMouseEvent(doc, "mousedown", x, y, button, count, mod);
  424. this.emitMouseEvent(doc, "mouseup", x, y, button, count, mod);
  425. };