action.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349
  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. "use strict";
  5. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  6. Cu.import("resource://gre/modules/Task.jsm");
  7. Cu.import("chrome://marionette/content/assert.js");
  8. Cu.import("chrome://marionette/content/element.js");
  9. Cu.import("chrome://marionette/content/error.js");
  10. Cu.import("chrome://marionette/content/event.js");
  11. this.EXPORTED_SYMBOLS = ["action"];
  12. // TODO? With ES 2016 and Symbol you can make a safer approximation
  13. // to an enum e.g. https://gist.github.com/xmlking/e86e4f15ec32b12c4689
  14. /**
  15. * Implements WebDriver Actions API: a low-level interface for providing
  16. * virtualised device input to the web browser.
  17. */
  18. this.action = {
  19. Pause: "pause",
  20. KeyDown: "keyDown",
  21. KeyUp: "keyUp",
  22. PointerDown: "pointerDown",
  23. PointerUp: "pointerUp",
  24. PointerMove: "pointerMove",
  25. PointerCancel: "pointerCancel",
  26. };
  27. const ACTIONS = {
  28. none: new Set([action.Pause]),
  29. key: new Set([action.Pause, action.KeyDown, action.KeyUp]),
  30. pointer: new Set([
  31. action.Pause,
  32. action.PointerDown,
  33. action.PointerUp,
  34. action.PointerMove,
  35. action.PointerCancel,
  36. ]),
  37. };
  38. /** Map from normalized key value to UI Events modifier key name */
  39. const MODIFIER_NAME_LOOKUP = {
  40. "Alt": "alt",
  41. "Shift": "shift",
  42. "Control": "ctrl",
  43. "Meta": "meta",
  44. };
  45. /** Map from raw key (codepoint) to normalized key value */
  46. const NORMALIZED_KEY_LOOKUP = {
  47. "\uE000": "Unidentified",
  48. "\uE001": "Cancel",
  49. "\uE002": "Help",
  50. "\uE003": "Backspace",
  51. "\uE004": "Tab",
  52. "\uE005": "Clear",
  53. "\uE006": "Return",
  54. "\uE007": "Enter",
  55. "\uE008": "Shift",
  56. "\uE009": "Control",
  57. "\uE00A": "Alt",
  58. "\uE00B": "Pause",
  59. "\uE00C": "Escape",
  60. "\uE00D": " ",
  61. "\uE00E": "PageUp",
  62. "\uE00F": "PageDown",
  63. "\uE010": "End",
  64. "\uE011": "Home",
  65. "\uE012": "ArrowLeft",
  66. "\uE013": "ArrowUp",
  67. "\uE014": "ArrowRight",
  68. "\uE015": "ArrowDown",
  69. "\uE016": "Insert",
  70. "\uE017": "Delete",
  71. "\uE018": ";",
  72. "\uE019": "=",
  73. "\uE01A": "0",
  74. "\uE01B": "1",
  75. "\uE01C": "2",
  76. "\uE01D": "3",
  77. "\uE01E": "4",
  78. "\uE01F": "5",
  79. "\uE020": "6",
  80. "\uE021": "7",
  81. "\uE022": "8",
  82. "\uE023": "9",
  83. "\uE024": "*",
  84. "\uE025": "+",
  85. "\uE026": ",",
  86. "\uE027": "-",
  87. "\uE028": ".",
  88. "\uE029": "/",
  89. "\uE031": "F1",
  90. "\uE032": "F2",
  91. "\uE033": "F3",
  92. "\uE034": "F4",
  93. "\uE035": "F5",
  94. "\uE036": "F6",
  95. "\uE037": "F7",
  96. "\uE038": "F8",
  97. "\uE039": "F9",
  98. "\uE03A": "F10",
  99. "\uE03B": "F11",
  100. "\uE03C": "F12",
  101. "\uE03D": "Meta",
  102. "\uE040": "ZenkakuHankaku",
  103. "\uE050": "Shift",
  104. "\uE051": "Control",
  105. "\uE052": "Alt",
  106. "\uE053": "Meta",
  107. "\uE054": "PageUp",
  108. "\uE055": "PageDown",
  109. "\uE056": "End",
  110. "\uE057": "Home",
  111. "\uE058": "ArrowLeft",
  112. "\uE059": "ArrowUp",
  113. "\uE05A": "ArrowRight",
  114. "\uE05B": "ArrowDown",
  115. "\uE05C": "Insert",
  116. "\uE05D": "Delete",
  117. };
  118. /** Map from raw key (codepoint) to key location */
  119. const KEY_LOCATION_LOOKUP = {
  120. "\uE007": 1,
  121. "\uE008": 1,
  122. "\uE009": 1,
  123. "\uE00A": 1,
  124. "\uE01A": 3,
  125. "\uE01B": 3,
  126. "\uE01C": 3,
  127. "\uE01D": 3,
  128. "\uE01E": 3,
  129. "\uE01F": 3,
  130. "\uE020": 3,
  131. "\uE021": 3,
  132. "\uE022": 3,
  133. "\uE023": 3,
  134. "\uE024": 3,
  135. "\uE025": 3,
  136. "\uE026": 3,
  137. "\uE027": 3,
  138. "\uE028": 3,
  139. "\uE029": 3,
  140. "\uE03D": 1,
  141. "\uE050": 2,
  142. "\uE051": 2,
  143. "\uE052": 2,
  144. "\uE053": 2,
  145. "\uE054": 3,
  146. "\uE055": 3,
  147. "\uE056": 3,
  148. "\uE057": 3,
  149. "\uE058": 3,
  150. "\uE059": 3,
  151. "\uE05A": 3,
  152. "\uE05B": 3,
  153. "\uE05C": 3,
  154. "\uE05D": 3,
  155. };
  156. const KEY_CODE_LOOKUP = {
  157. "\uE00A": "AltLeft",
  158. "\uE052": "AltRight",
  159. "\uE015": "ArrowDown",
  160. "\uE012": "ArrowLeft",
  161. "\uE014": "ArrowRight",
  162. "\uE013": "ArrowUp",
  163. "`": "Backquote",
  164. "~": "Backquote",
  165. "\\": "Backslash",
  166. "|": "Backslash",
  167. "\uE003": "Backspace",
  168. "[": "BracketLeft",
  169. "{": "BracketLeft",
  170. "]": "BracketRight",
  171. "}": "BracketRight",
  172. ",": "Comma",
  173. "<": "Comma",
  174. "\uE009": "ControlLeft",
  175. "\uE051": "ControlRight",
  176. "\uE017": "Delete",
  177. ")": "Digit0",
  178. "0": "Digit0",
  179. "!": "Digit1",
  180. "1": "Digit1",
  181. "2": "Digit2",
  182. "@": "Digit2",
  183. "#": "Digit3",
  184. "3": "Digit3",
  185. "$": "Digit4",
  186. "4": "Digit4",
  187. "%": "Digit5",
  188. "5": "Digit5",
  189. "6": "Digit6",
  190. "^": "Digit6",
  191. "&": "Digit7",
  192. "7": "Digit7",
  193. "*": "Digit8",
  194. "8": "Digit8",
  195. "(": "Digit9",
  196. "9": "Digit9",
  197. "\uE010": "End",
  198. "\uE006": "Enter",
  199. "+": "Equal",
  200. "=": "Equal",
  201. "\uE00C": "Escape",
  202. "\uE031": "F1",
  203. "\uE03A": "F10",
  204. "\uE03B": "F11",
  205. "\uE03C": "F12",
  206. "\uE032": "F2",
  207. "\uE033": "F3",
  208. "\uE034": "F4",
  209. "\uE035": "F5",
  210. "\uE036": "F6",
  211. "\uE037": "F7",
  212. "\uE038": "F8",
  213. "\uE039": "F9",
  214. "\uE002": "Help",
  215. "\uE011": "Home",
  216. "\uE016": "Insert",
  217. "<": "IntlBackslash",
  218. ">": "IntlBackslash",
  219. "A": "KeyA",
  220. "a": "KeyA",
  221. "B": "KeyB",
  222. "b": "KeyB",
  223. "C": "KeyC",
  224. "c": "KeyC",
  225. "D": "KeyD",
  226. "d": "KeyD",
  227. "E": "KeyE",
  228. "e": "KeyE",
  229. "F": "KeyF",
  230. "f": "KeyF",
  231. "G": "KeyG",
  232. "g": "KeyG",
  233. "H": "KeyH",
  234. "h": "KeyH",
  235. "I": "KeyI",
  236. "i": "KeyI",
  237. "J": "KeyJ",
  238. "j": "KeyJ",
  239. "K": "KeyK",
  240. "k": "KeyK",
  241. "L": "KeyL",
  242. "l": "KeyL",
  243. "M": "KeyM",
  244. "m": "KeyM",
  245. "N": "KeyN",
  246. "n": "KeyN",
  247. "O": "KeyO",
  248. "o": "KeyO",
  249. "P": "KeyP",
  250. "p": "KeyP",
  251. "Q": "KeyQ",
  252. "q": "KeyQ",
  253. "R": "KeyR",
  254. "r": "KeyR",
  255. "S": "KeyS",
  256. "s": "KeyS",
  257. "T": "KeyT",
  258. "t": "KeyT",
  259. "U": "KeyU",
  260. "u": "KeyU",
  261. "V": "KeyV",
  262. "v": "KeyV",
  263. "W": "KeyW",
  264. "w": "KeyW",
  265. "X": "KeyX",
  266. "x": "KeyX",
  267. "Y": "KeyY",
  268. "y": "KeyY",
  269. "Z": "KeyZ",
  270. "z": "KeyZ",
  271. "-": "Minus",
  272. "_": "Minus",
  273. "\uE01A": "Numpad0",
  274. "\uE05C": "Numpad0",
  275. "\uE01B": "Numpad1",
  276. "\uE056": "Numpad1",
  277. "\uE01C": "Numpad2",
  278. "\uE05B": "Numpad2",
  279. "\uE01D": "Numpad3",
  280. "\uE055": "Numpad3",
  281. "\uE01E": "Numpad4",
  282. "\uE058": "Numpad4",
  283. "\uE01F": "Numpad5",
  284. "\uE020": "Numpad6",
  285. "\uE05A": "Numpad6",
  286. "\uE021": "Numpad7",
  287. "\uE057": "Numpad7",
  288. "\uE022": "Numpad8",
  289. "\uE059": "Numpad8",
  290. "\uE023": "Numpad9",
  291. "\uE054": "Numpad9",
  292. "\uE024": "NumpadAdd",
  293. "\uE026": "NumpadComma",
  294. "\uE028": "NumpadDecimal",
  295. "\uE05D": "NumpadDecimal",
  296. "\uE029": "NumpadDivide",
  297. "\uE007": "NumpadEnter",
  298. "\uE024": "NumpadMultiply",
  299. "\uE026": "NumpadSubtract",
  300. "\uE03D": "OSLeft",
  301. "\uE053": "OSRight",
  302. "\uE01E": "PageDown",
  303. "\uE01F": "PageUp",
  304. ".": "Period",
  305. ">": "Period",
  306. "\"": "Quote",
  307. "'": "Quote",
  308. ":": "Semicolon",
  309. ";": "Semicolon",
  310. "\uE008": "ShiftLeft",
  311. "\uE050": "ShiftRight",
  312. "/": "Slash",
  313. "?": "Slash",
  314. "\uE00D": "Space",
  315. " ": "Space",
  316. "\uE004": "Tab",
  317. };
  318. /** Represents possible values for a pointer-move origin. */
  319. action.PointerOrigin = {
  320. Viewport: "viewport",
  321. Pointer: "pointer",
  322. };
  323. /**
  324. * Look up a PointerOrigin.
  325. *
  326. * @param {?} obj
  327. * Origin for a pointerMove action.
  328. *
  329. * @return {?}
  330. * A pointer origin that is either "viewport" (default), "pointer", or a
  331. * web-element reference.
  332. *
  333. * @throws {InvalidArgumentError}
  334. * If |obj| is not a valid origin.
  335. */
  336. action.PointerOrigin.get = function(obj) {
  337. let origin = obj;
  338. if (typeof obj == "undefined") {
  339. origin = this.Viewport;
  340. } else if (typeof obj == "string") {
  341. let name = capitalize(obj);
  342. assert.in(name, this, error.pprint`Unknown pointer-move origin: ${obj}`);
  343. origin = this[name];
  344. } else if (!element.isWebElementReference(obj)) {
  345. throw new InvalidArgumentError("Expected 'origin' to be a string or a " +
  346. `web element reference, got: ${obj}`);
  347. }
  348. return origin;
  349. };
  350. /** Represents possible subtypes for a pointer input source. */
  351. action.PointerType = {
  352. Mouse: "mouse",
  353. // TODO For now, only mouse is supported
  354. //Pen: "pen",
  355. //Touch: "touch",
  356. };
  357. /**
  358. * Look up a PointerType.
  359. *
  360. * @param {string} str
  361. * Name of pointer type.
  362. *
  363. * @return {string}
  364. * A pointer type for processing pointer parameters.
  365. *
  366. * @throws {InvalidArgumentError}
  367. * If |str| is not a valid pointer type.
  368. */
  369. action.PointerType.get = function (str) {
  370. let name = capitalize(str);
  371. assert.in(name, this, error.pprint`Unknown pointerType: ${str}`);
  372. return this[name];
  373. };
  374. /**
  375. * Input state associated with current session. This is a map between input ID and
  376. * the device state for that input source, with one entry for each active input source.
  377. *
  378. * Initialized in listener.js
  379. */
  380. action.inputStateMap = undefined;
  381. /**
  382. * List of |action.Action| associated with current session. Used to manage dispatching
  383. * events when resetting the state of the input sources. Reset operations are assumed
  384. * to be idempotent.
  385. *
  386. * Initialized in listener.js
  387. */
  388. action.inputsToCancel = undefined;
  389. /**
  390. * Represents device state for an input source.
  391. */
  392. class InputState {
  393. constructor() {
  394. this.type = this.constructor.name.toLowerCase();
  395. }
  396. /**
  397. * Check equality of this InputState object with another.
  398. *
  399. * @para{?} other
  400. * Object representing an input state.
  401. * @return {boolean}
  402. * True if |this| has the same |type| as |other|.
  403. */
  404. is(other) {
  405. if (typeof other == "undefined") {
  406. return false;
  407. }
  408. return this.type === other.type;
  409. }
  410. toString() {
  411. return `[object ${this.constructor.name}InputState]`;
  412. }
  413. /**
  414. * @param {?} obj
  415. * Object with property |type| and optionally |parameters| or |pointerType|,
  416. * representing an action sequence or an action item.
  417. *
  418. * @return {action.InputState}
  419. * An |action.InputState| object for the type of the |actionSequence|.
  420. *
  421. * @throws {InvalidArgumentError}
  422. * If |actionSequence.type| is not valid.
  423. */
  424. static fromJson(obj) {
  425. let type = obj.type;
  426. assert.in(type, ACTIONS, error.pprint`Unknown action type: ${type}`);
  427. let name = type == "none" ? "Null" : capitalize(type);
  428. if (name == "Pointer") {
  429. if (!obj.pointerType && (!obj.parameters || !obj.parameters.pointerType)) {
  430. throw new InvalidArgumentError(
  431. error.pprint`Expected obj to have pointerType, got: ${obj}`);
  432. }
  433. let pointerType = obj.pointerType || obj.parameters.pointerType;
  434. return new action.InputState[name](pointerType);
  435. } else {
  436. return new action.InputState[name]();
  437. }
  438. }
  439. }
  440. /** Possible kinds of |InputState| for supported input sources. */
  441. action.InputState = {};
  442. /**
  443. * Input state associated with a keyboard-type device.
  444. */
  445. action.InputState.Key = class Key extends InputState {
  446. constructor() {
  447. super();
  448. this.pressed = new Set();
  449. this.alt = false;
  450. this.shift = false;
  451. this.ctrl = false;
  452. this.meta = false;
  453. }
  454. /**
  455. * Update modifier state according to |key|.
  456. *
  457. * @param {string} key
  458. * Normalized key value of a modifier key.
  459. * @param {boolean} value
  460. * Value to set the modifier attribute to.
  461. *
  462. * @throws {InvalidArgumentError}
  463. * If |key| is not a modifier.
  464. */
  465. setModState(key, value) {
  466. if (key in MODIFIER_NAME_LOOKUP) {
  467. this[MODIFIER_NAME_LOOKUP[key]] = value;
  468. } else {
  469. throw new InvalidArgumentError("Expected 'key' to be one of " +
  470. `${Object.keys(MODIFIER_NAME_LOOKUP)}; got: ${key}`);
  471. }
  472. }
  473. /**
  474. * Check whether |key| is pressed.
  475. *
  476. * @param {string} key
  477. * Normalized key value.
  478. *
  479. * @return {boolean}
  480. * True if |key| is in set of pressed keys.
  481. */
  482. isPressed(key) {
  483. return this.pressed.has(key);
  484. }
  485. /**
  486. * Add |key| to the set of pressed keys.
  487. *
  488. * @param {string} key
  489. * Normalized key value.
  490. *
  491. * @return {boolean}
  492. * True if |key| is in list of pressed keys.
  493. */
  494. press(key) {
  495. return this.pressed.add(key);
  496. }
  497. /**
  498. * Remove |key| from the set of pressed keys.
  499. *
  500. * @param {string} key
  501. * Normalized key value.
  502. *
  503. * @return {boolean}
  504. * True if |key| was present before removal, false otherwise.
  505. */
  506. release(key) {
  507. return this.pressed.delete(key);
  508. }
  509. };
  510. /**
  511. * Input state not associated with a specific physical device.
  512. */
  513. action.InputState.Null = class Null extends InputState {
  514. constructor() {
  515. super();
  516. this.type = "none";
  517. }
  518. };
  519. /**
  520. * Input state associated with a pointer-type input device.
  521. *
  522. * @param {string} subtype
  523. * Kind of pointing device: mouse, pen, touch.
  524. *
  525. * @throws {InvalidArgumentError}
  526. * If subtype is undefined or an invalid pointer type.
  527. */
  528. action.InputState.Pointer = class Pointer extends InputState {
  529. constructor(subtype) {
  530. super();
  531. this.pressed = new Set();
  532. assert.defined(subtype, error.pprint`Expected subtype to be defined, got: ${subtype}`);
  533. this.subtype = action.PointerType.get(subtype);
  534. this.x = 0;
  535. this.y = 0;
  536. }
  537. /**
  538. * Check whether |button| is pressed.
  539. *
  540. * @param {number} button
  541. * Positive integer that refers to a mouse button.
  542. *
  543. * @return {boolean}
  544. * True if |button| is in set of pressed buttons.
  545. */
  546. isPressed(button) {
  547. assert.positiveInteger(button);
  548. return this.pressed.has(button);
  549. }
  550. /**
  551. * Add |button| to the set of pressed keys.
  552. *
  553. * @param {number} button
  554. * Positive integer that refers to a mouse button.
  555. *
  556. * @return {Set}
  557. * Set of pressed buttons.
  558. */
  559. press(button) {
  560. assert.positiveInteger(button);
  561. return this.pressed.add(button);
  562. }
  563. /**
  564. * Remove |button| from the set of pressed buttons.
  565. *
  566. * @param {number} button
  567. * A positive integer that refers to a mouse button.
  568. *
  569. * @return {boolean}
  570. * True if |button| was present before removals, false otherwise.
  571. */
  572. release(button) {
  573. assert.positiveInteger(button);
  574. return this.pressed.delete(button);
  575. }
  576. };
  577. /**
  578. * Repesents an action for dispatch. Used in |action.Chain| and |action.Sequence|.
  579. *
  580. * @param {string} id
  581. * Input source ID.
  582. * @param {string} type
  583. * Action type: none, key, pointer.
  584. * @param {string} subtype
  585. * Action subtype: pause, keyUp, keyDown, pointerUp, pointerDown, pointerMove, pointerCancel.
  586. *
  587. * @throws {InvalidArgumentError}
  588. * If any parameters are undefined.
  589. */
  590. action.Action = class {
  591. constructor(id, type, subtype) {
  592. if ([id, type, subtype].includes(undefined)) {
  593. throw new InvalidArgumentError("Missing id, type or subtype");
  594. }
  595. for (let attr of [id, type, subtype]) {
  596. assert.string(attr, error.pprint`Expected string, got: ${attr}`);
  597. }
  598. this.id = id;
  599. this.type = type;
  600. this.subtype = subtype;
  601. };
  602. toString() {
  603. return `[action ${this.type}]`;
  604. }
  605. /**
  606. * @param {?} actionSequence
  607. * Object representing sequence of actions from one input source.
  608. * @param {?} actionItem
  609. * Object representing a single action from |actionSequence|.
  610. *
  611. * @return {action.Action}
  612. * An action that can be dispatched; corresponds to |actionItem|.
  613. *
  614. * @throws {InvalidArgumentError}
  615. * If any |actionSequence| or |actionItem| attributes are invalid.
  616. * @throws {UnsupportedOperationError}
  617. * If |actionItem.type| is |pointerCancel|.
  618. */
  619. static fromJson(actionSequence, actionItem) {
  620. let type = actionSequence.type;
  621. let id = actionSequence.id;
  622. let subtypes = ACTIONS[type];
  623. if (!subtypes) {
  624. throw new InvalidArgumentError("Unknown type: " + type);
  625. }
  626. let subtype = actionItem.type;
  627. if (!subtypes.has(subtype)) {
  628. throw new InvalidArgumentError(`Unknown subtype for ${type} action: ${subtype}`);
  629. }
  630. let item = new action.Action(id, type, subtype);
  631. if (type === "pointer") {
  632. action.processPointerAction(id,
  633. action.PointerParameters.fromJson(actionSequence.parameters), item);
  634. }
  635. switch (item.subtype) {
  636. case action.KeyUp:
  637. case action.KeyDown:
  638. let key = actionItem.value;
  639. // TODO countGraphemes
  640. // TODO key.value could be a single code point like "\uE012" (see rawKey)
  641. // or "grapheme cluster"
  642. assert.string(key,
  643. error.pprint("Expected 'value' to be a string that represents single code point " +
  644. `or grapheme cluster, got: ${key}`));
  645. item.value = key;
  646. break;
  647. case action.PointerDown:
  648. case action.PointerUp:
  649. assert.positiveInteger(actionItem.button,
  650. error.pprint`Expected 'button' (${actionItem.button}) to be >= 0`);
  651. item.button = actionItem.button;
  652. break;
  653. case action.PointerMove:
  654. item.duration = actionItem.duration;
  655. if (typeof item.duration != "undefined"){
  656. assert.positiveInteger(item.duration,
  657. error.pprint`Expected 'duration' (${item.duration}) to be >= 0`);
  658. }
  659. item.origin = action.PointerOrigin.get(actionItem.origin);
  660. item.x = actionItem.x;
  661. if (typeof item.x != "undefined") {
  662. assert.integer(item.x, error.pprint`Expected 'x' (${item.x}) to be an Integer`);
  663. }
  664. item.y = actionItem.y;
  665. if (typeof item.y != "undefined") {
  666. assert.integer(item.y, error.pprint`Expected 'y' (${item.y}) to be an Integer`);
  667. }
  668. break;
  669. case action.PointerCancel:
  670. throw new UnsupportedOperationError();
  671. break;
  672. case action.Pause:
  673. item.duration = actionItem.duration;
  674. if (typeof item.duration != "undefined") {
  675. assert.positiveInteger(item.duration,
  676. error.pprint`Expected 'duration' (${item.duration}) to be >= 0`);
  677. }
  678. break;
  679. }
  680. return item;
  681. }
  682. };
  683. /**
  684. * Represents a series of ticks, specifying which actions to perform at each tick.
  685. */
  686. action.Chain = class extends Array {
  687. toString() {
  688. return `[chain ${super.toString()}]`;
  689. }
  690. /**
  691. * @param {Array.<?>} actions
  692. * Array of objects that each represent an action sequence.
  693. *
  694. * @return {action.Chain}
  695. * Transpose of |actions| such that actions to be performed in a single tick
  696. * are grouped together.
  697. *
  698. * @throws {InvalidArgumentError}
  699. * If |actions| is not an Array.
  700. */
  701. static fromJson(actions) {
  702. assert.array(actions,
  703. error.pprint`Expected 'actions' to be an Array, got: ${actions}`);
  704. let actionsByTick = new action.Chain();
  705. // TODO check that each actionSequence in actions refers to a different input ID
  706. for (let actionSequence of actions) {
  707. let inputSourceActions = action.Sequence.fromJson(actionSequence);
  708. for (let i = 0; i < inputSourceActions.length; i++) {
  709. // new tick
  710. if (actionsByTick.length < (i + 1)) {
  711. actionsByTick.push([]);
  712. }
  713. actionsByTick[i].push(inputSourceActions[i]);
  714. }
  715. }
  716. return actionsByTick;
  717. }
  718. };
  719. /**
  720. * Represents one input source action sequence; this is essentially an |Array.<action.Action>|.
  721. */
  722. action.Sequence = class extends Array {
  723. toString() {
  724. return `[sequence ${super.toString()}]`;
  725. }
  726. /**
  727. * @param {?} actionSequence
  728. * Object that represents a sequence action items for one input source.
  729. *
  730. * @return {action.Sequence}
  731. * Sequence of actions that can be dispatched.
  732. *
  733. * @throws {InvalidArgumentError}
  734. * If |actionSequence.id| is not a string or it's aleady mapped
  735. * to an |action.InputState} incompatible with |actionSequence.type|.
  736. * If |actionSequence.actions| is not an Array.
  737. */
  738. static fromJson(actionSequence) {
  739. // used here to validate 'type' in addition to InputState type below
  740. let inputSourceState = InputState.fromJson(actionSequence);
  741. let id = actionSequence.id;
  742. assert.defined(id, "Expected 'id' to be defined");
  743. assert.string(id, error.pprint`Expected 'id' to be a string, got: ${id}`);
  744. let actionItems = actionSequence.actions;
  745. assert.array(actionItems,
  746. error.pprint("Expected 'actionSequence.actions' to be an Array, " +
  747. `got: ${actionSequence.actions}`));
  748. if (!action.inputStateMap.has(id)) {
  749. action.inputStateMap.set(id, inputSourceState);
  750. } else if (!action.inputStateMap.get(id).is(inputSourceState)) {
  751. throw new InvalidArgumentError(
  752. `Expected ${id} to be mapped to ${inputSourceState}, ` +
  753. `got: ${action.inputStateMap.get(id)}`);
  754. }
  755. let actions = new action.Sequence();
  756. for (let actionItem of actionItems) {
  757. actions.push(action.Action.fromJson(actionSequence, actionItem));
  758. }
  759. return actions;
  760. }
  761. };
  762. /**
  763. * Represents parameters in an action for a pointer input source.
  764. *
  765. * @param {string=} pointerType
  766. * Type of pointing device. If the parameter is undefined, "mouse" is used.
  767. */
  768. action.PointerParameters = class {
  769. constructor(pointerType = "mouse") {
  770. this.pointerType = action.PointerType.get(pointerType);
  771. }
  772. toString() {
  773. return `[pointerParameters ${this.pointerType}]`;
  774. }
  775. /**
  776. * @param {?} parametersData
  777. * Object that represents pointer parameters.
  778. *
  779. * @return {action.PointerParameters}
  780. * Validated pointer paramters.
  781. */
  782. static fromJson(parametersData) {
  783. if (typeof parametersData == "undefined") {
  784. return new action.PointerParameters();
  785. } else {
  786. return new action.PointerParameters(parametersData.pointerType);
  787. }
  788. }
  789. };
  790. /**
  791. * Adds |pointerType| attribute to Action |act|. Helper function
  792. * for |action.Action.fromJson|.
  793. *
  794. * @param {string} id
  795. * Input source ID.
  796. * @param {action.PointerParams} pointerParams
  797. * Input source pointer parameters.
  798. * @param {action.Action} act
  799. * Action to be updated.
  800. *
  801. * @throws {InvalidArgumentError}
  802. * If |id| is already mapped to an |action.InputState| that is
  803. * not compatible with |act.type| or |pointerParams.pointerType|.
  804. */
  805. action.processPointerAction = function processPointerAction(id, pointerParams, act) {
  806. if (action.inputStateMap.has(id) && action.inputStateMap.get(id).type !== act.type) {
  807. throw new InvalidArgumentError(
  808. `Expected 'id' ${id} to be mapped to InputState whose type is ` +
  809. `${action.inputStateMap.get(id).type}, got: ${act.type}`);
  810. }
  811. let pointerType = pointerParams.pointerType;
  812. if (action.inputStateMap.has(id) && action.inputStateMap.get(id).subtype !== pointerType) {
  813. throw new InvalidArgumentError(
  814. `Expected 'id' ${id} to be mapped to InputState whose subtype is ` +
  815. `${action.inputStateMap.get(id).subtype}, got: ${pointerType}`);
  816. }
  817. act.pointerType = pointerParams.pointerType;
  818. };
  819. /** Collect properties associated with KeyboardEvent */
  820. action.Key = class {
  821. constructor(rawKey) {
  822. this.key = NORMALIZED_KEY_LOOKUP[rawKey] || rawKey;
  823. this.code = KEY_CODE_LOOKUP[rawKey];
  824. this.location = KEY_LOCATION_LOOKUP[rawKey] || 0;
  825. this.altKey = false;
  826. this.shiftKey = false;
  827. this.ctrlKey = false;
  828. this.metaKey = false;
  829. this.repeat = false;
  830. this.isComposing = false;
  831. // Prevent keyCode from being guessed in event.js; we don't want to use it anyway.
  832. this.keyCode = 0;
  833. }
  834. update(inputState) {
  835. this.altKey = inputState.alt;
  836. this.shiftKey = inputState.shift;
  837. this.ctrlKey = inputState.ctrl;
  838. this.metaKey = inputState.meta;
  839. }
  840. };
  841. /** Collect properties associated with MouseEvent */
  842. action.Mouse = class {
  843. constructor(type, button = 0) {
  844. this.type = type;
  845. assert.positiveInteger(button);
  846. this.button = button;
  847. this.buttons = 0;
  848. }
  849. update(inputState) {
  850. let allButtons = Array.from(inputState.pressed);
  851. this.buttons = allButtons.reduce((a, i) => a + Math.pow(2, i), 0);
  852. }
  853. };
  854. /**
  855. * Dispatch a chain of actions over |chain.length| ticks.
  856. *
  857. * This is done by creating a Promise for each tick that resolves once all the
  858. * Promises for individual tick-actions are resolved. The next tick's actions are
  859. * not dispatched until the Promise for the current tick is resolved.
  860. *
  861. * @param {action.Chain} chain
  862. * Actions grouped by tick; each element in |chain| is a sequence of
  863. * actions for one tick.
  864. * @param {element.Store} seenEls
  865. * Element store.
  866. * @param {?} container
  867. * Object with |frame| attribute of type |nsIDOMWindow|.
  868. *
  869. * @return {Promise}
  870. * Promise for dispatching all actions in |chain|.
  871. */
  872. action.dispatch = function(chain, seenEls, container) {
  873. let chainEvents = Task.spawn(function*() {
  874. for (let tickActions of chain) {
  875. yield action.dispatchTickActions(
  876. tickActions, action.computeTickDuration(tickActions), seenEls, container);
  877. }
  878. });
  879. return chainEvents;
  880. };
  881. /**
  882. * Dispatch sequence of actions for one tick.
  883. *
  884. * This creates a Promise for one tick that resolves once the Promise for each
  885. * tick-action is resolved, which takes at least |tickDuration| milliseconds.
  886. * The resolved set of events for each tick is followed by firing of pending DOM events.
  887. *
  888. * Note that the tick-actions are dispatched in order, but they may have different
  889. * durations and therefore may not end in the same order.
  890. *
  891. * @param {Array.<action.Action>} tickActions
  892. * List of actions for one tick.
  893. * @param {number} tickDuration
  894. * Duration in milliseconds of this tick.
  895. * @param {element.Store} seenEls
  896. * Element store.
  897. * @param {?} container
  898. * Object with |frame| attribute of type |nsIDOMWindow|.
  899. *
  900. * @return {Promise}
  901. * Promise for dispatching all tick-actions and pending DOM events.
  902. */
  903. action.dispatchTickActions = function(tickActions, tickDuration, seenEls, container) {
  904. let pendingEvents = tickActions.map(toEvents(tickDuration, seenEls, container));
  905. return Promise.all(pendingEvents).then(() => flushEvents(container));
  906. };
  907. /**
  908. * Compute tick duration in milliseconds for a collection of actions.
  909. *
  910. * @param {Array.<action.Action>} tickActions
  911. * List of actions for one tick.
  912. *
  913. * @return {number}
  914. * Longest action duration in |tickActions| if any, or 0.
  915. */
  916. action.computeTickDuration = function(tickActions) {
  917. let max = 0;
  918. for (let a of tickActions) {
  919. let affectsWallClockTime = a.subtype == action.Pause ||
  920. (a.type == "pointer" && a.subtype == action.PointerMove);
  921. if (affectsWallClockTime && a.duration) {
  922. max = Math.max(a.duration, max);
  923. }
  924. }
  925. return max;
  926. };
  927. /**
  928. * Compute viewport coordinates of pointer target based on given origin.
  929. *
  930. * @param {action.Action} a
  931. * Action that specifies pointer origin and x and y coordinates of target.
  932. * @param {action.InputState} inputState
  933. * Input state that specifies current x and y coordinates of pointer.
  934. * @param {Map.<string, number>=} center
  935. * Object representing x and y coordinates of an element center-point.
  936. * This is only used if |a.origin| is a web element reference.
  937. *
  938. * @return {Map.<string, number>}
  939. * x and y coordinates of pointer destination.
  940. */
  941. action.computePointerDestination = function(a, inputState, center = undefined) {
  942. let {x, y} = a;
  943. switch (a.origin) {
  944. case action.PointerOrigin.Viewport:
  945. break;
  946. case action.PointerOrigin.Pointer:
  947. x += inputState.x;
  948. y += inputState.y;
  949. break;
  950. default:
  951. // origin represents web element
  952. assert.defined(center);
  953. assert.in("x", center);
  954. assert.in("y", center);
  955. x += center.x;
  956. y += center.y;
  957. }
  958. return {"x": x, "y": y};
  959. };
  960. /**
  961. * Create a closure to use as a map from action definitions to Promise events.
  962. *
  963. * @param {number} tickDuration
  964. * Duration in milliseconds of this tick.
  965. * @param {element.Store} seenEls
  966. * Element store.
  967. * @param {?} container
  968. * Object with |frame| attribute of type |nsIDOMWindow|.
  969. *
  970. * @return {function(action.Action): Promise}
  971. * Function that takes an action and returns a Promise for dispatching
  972. * the event that corresponds to that action.
  973. */
  974. function toEvents(tickDuration, seenEls, container) {
  975. return function (a) {
  976. let inputState = action.inputStateMap.get(a.id);
  977. switch (a.subtype) {
  978. case action.KeyUp:
  979. return dispatchKeyUp(a, inputState, container.frame);
  980. case action.KeyDown:
  981. return dispatchKeyDown(a, inputState, container.frame);
  982. case action.PointerDown:
  983. return dispatchPointerDown(a, inputState, container.frame);
  984. case action.PointerUp:
  985. return dispatchPointerUp(a, inputState, container.frame);
  986. case action.PointerMove:
  987. return dispatchPointerMove(a, inputState, tickDuration, seenEls, container);
  988. case action.PointerCancel:
  989. throw new UnsupportedOperationError();
  990. case action.Pause:
  991. return dispatchPause(a, tickDuration);
  992. }
  993. };
  994. }
  995. /**
  996. * Dispatch a keyDown action equivalent to pressing a key on a keyboard.
  997. *
  998. * @param {action.Action} a
  999. * Action to dispatch.
  1000. * @param {action.InputState} inputState
  1001. * Input state for this action's input source.
  1002. * @param {nsIDOMWindow} win
  1003. * Current window.
  1004. *
  1005. * @return {Promise}
  1006. * Promise to dispatch at least a keydown event, and keypress if appropriate.
  1007. */
  1008. function dispatchKeyDown(a, inputState, win) {
  1009. return new Promise(resolve => {
  1010. let keyEvent = new action.Key(a.value);
  1011. keyEvent.repeat = inputState.isPressed(keyEvent.key);
  1012. inputState.press(keyEvent.key);
  1013. if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
  1014. inputState.setModState(keyEvent.key, true);
  1015. }
  1016. // Append a copy of |a| with keyUp subtype
  1017. action.inputsToCancel.push(Object.assign({}, a, {subtype: action.KeyUp}));
  1018. keyEvent.update(inputState);
  1019. event.sendKeyDown(keyEvent.key, keyEvent, win);
  1020. resolve();
  1021. });
  1022. }
  1023. /**
  1024. * Dispatch a keyUp action equivalent to releasing a key on a keyboard.
  1025. *
  1026. * @param {action.Action} a
  1027. * Action to dispatch.
  1028. * @param {action.InputState} inputState
  1029. * Input state for this action's input source.
  1030. * @param {nsIDOMWindow} win
  1031. * Current window.
  1032. *
  1033. * @return {Promise}
  1034. * Promise to dispatch a keyup event.
  1035. */
  1036. function dispatchKeyUp(a, inputState, win) {
  1037. return new Promise(resolve => {
  1038. let keyEvent = new action.Key(a.value);
  1039. if (!inputState.isPressed(keyEvent.key)) {
  1040. resolve();
  1041. return;
  1042. }
  1043. if (keyEvent.key in MODIFIER_NAME_LOOKUP) {
  1044. inputState.setModState(keyEvent.key, false);
  1045. }
  1046. inputState.release(keyEvent.key);
  1047. keyEvent.update(inputState);
  1048. event.sendKeyUp(keyEvent.key, keyEvent, win);
  1049. resolve();
  1050. });
  1051. }
  1052. /**
  1053. * Dispatch a pointerDown action equivalent to pressing a pointer-device
  1054. * button.
  1055. *
  1056. * @param {action.Action} a
  1057. * Action to dispatch.
  1058. * @param {action.InputState} inputState
  1059. * Input state for this action's input source.
  1060. * @param {nsIDOMWindow} win
  1061. * Current window.
  1062. *
  1063. * @return {Promise}
  1064. * Promise to dispatch at least a pointerdown event.
  1065. */
  1066. function dispatchPointerDown(a, inputState, win) {
  1067. return new Promise(resolve => {
  1068. if (inputState.isPressed(a.button)) {
  1069. resolve();
  1070. return;
  1071. }
  1072. inputState.press(a.button);
  1073. // Append a copy of |a| with pointerUp subtype
  1074. action.inputsToCancel.push(Object.assign({}, a, {subtype: action.PointerUp}));
  1075. switch (inputState.subtype) {
  1076. case action.PointerType.Mouse:
  1077. let mouseEvent = new action.Mouse("mousedown", a.button);
  1078. mouseEvent.update(inputState);
  1079. event.synthesizeMouseAtPoint(inputState.x, inputState.y, mouseEvent, win);
  1080. break;
  1081. case action.PointerType.Pen:
  1082. case action.PointerType.Touch:
  1083. throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
  1084. break;
  1085. default:
  1086. throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
  1087. }
  1088. resolve();
  1089. });
  1090. }
  1091. /**
  1092. * Dispatch a pointerUp action equivalent to releasing a pointer-device
  1093. * button.
  1094. *
  1095. * @param {action.Action} a
  1096. * Action to dispatch.
  1097. * @param {action.InputState} inputState
  1098. * Input state for this action's input source.
  1099. * @param {nsIDOMWindow} win
  1100. * Current window.
  1101. *
  1102. * @return {Promise}
  1103. * Promise to dispatch at least a pointerup event.
  1104. */
  1105. function dispatchPointerUp(a, inputState, win) {
  1106. return new Promise(resolve => {
  1107. if (!inputState.isPressed(a.button)) {
  1108. resolve();
  1109. return;
  1110. }
  1111. inputState.release(a.button);
  1112. switch (inputState.subtype) {
  1113. case action.PointerType.Mouse:
  1114. let mouseEvent = new action.Mouse("mouseup", a.button);
  1115. mouseEvent.update(inputState);
  1116. event.synthesizeMouseAtPoint(inputState.x, inputState.y,
  1117. mouseEvent, win);
  1118. break;
  1119. case action.PointerType.Pen:
  1120. case action.PointerType.Touch:
  1121. throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
  1122. default:
  1123. throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
  1124. }
  1125. resolve();
  1126. });
  1127. }
  1128. /**
  1129. * Dispatch a pointerMove action equivalent to moving pointer device in a line.
  1130. *
  1131. * If the action duration is 0, the pointer jumps immediately to the target coordinates.
  1132. * Otherwise, events are synthesized to mimic a pointer travelling in a discontinuous,
  1133. * approximately straight line, with the pointer coordinates being updated around 60
  1134. * times per second.
  1135. *
  1136. * @param {action.Action} a
  1137. * Action to dispatch.
  1138. * @param {action.InputState} inputState
  1139. * Input state for this action's input source.
  1140. * @param {element.Store} seenEls
  1141. * Element store.
  1142. * @param {?} container
  1143. * Object with |frame| attribute of type |nsIDOMWindow|.
  1144. *
  1145. * @return {Promise}
  1146. * Promise to dispatch at least one pointermove event, as well as mousemove events
  1147. * as appropriate.
  1148. */
  1149. function dispatchPointerMove(a, inputState, tickDuration, seenEls, container) {
  1150. const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  1151. // interval between pointermove increments in ms, based on common vsync
  1152. const fps60 = 17;
  1153. return new Promise(resolve => {
  1154. const start = Date.now();
  1155. const [startX, startY] = [inputState.x, inputState.y];
  1156. let target = action.computePointerDestination(a, inputState,
  1157. getElementCenter(a.origin, seenEls, container));
  1158. const [targetX, targetY] = [target.x, target.y];
  1159. if (!inViewPort(targetX, targetY, container.frame)) {
  1160. throw new MoveTargetOutOfBoundsError(
  1161. `(${targetX}, ${targetY}) is out of bounds of viewport ` +
  1162. `width (${container.frame.innerWidth}) and height (${container.frame.innerHeight})`);
  1163. }
  1164. const duration = typeof a.duration == "undefined" ? tickDuration : a.duration;
  1165. if (duration === 0) {
  1166. // move pointer to destination in one step
  1167. performOnePointerMove(inputState, targetX, targetY, container.frame);
  1168. resolve();
  1169. return;
  1170. }
  1171. const distanceX = targetX - startX;
  1172. const distanceY = targetY - startY;
  1173. const ONE_SHOT = Ci.nsITimer.TYPE_ONE_SHOT;
  1174. let intermediatePointerEvents = Task.spawn(function* () {
  1175. // wait |fps60| ms before performing first incremental pointer move
  1176. yield new Promise(resolveTimer =>
  1177. timer.initWithCallback(resolveTimer, fps60, ONE_SHOT)
  1178. );
  1179. let durationRatio = Math.floor(Date.now() - start) / duration;
  1180. const epsilon = fps60 / duration / 10;
  1181. while ((1 - durationRatio) > epsilon) {
  1182. let x = Math.floor(durationRatio * distanceX + startX);
  1183. let y = Math.floor(durationRatio * distanceY + startY);
  1184. performOnePointerMove(inputState, x, y, container.frame);
  1185. // wait |fps60| ms before performing next pointer move
  1186. yield new Promise(resolveTimer =>
  1187. timer.initWithCallback(resolveTimer, fps60, ONE_SHOT));
  1188. durationRatio = Math.floor(Date.now() - start) / duration;
  1189. }
  1190. });
  1191. // perform last pointer move after all incremental moves are resolved and
  1192. // durationRatio is close enough to 1
  1193. intermediatePointerEvents.then(() => {
  1194. performOnePointerMove(inputState, targetX, targetY, container.frame);
  1195. resolve();
  1196. });
  1197. });
  1198. }
  1199. function performOnePointerMove(inputState, targetX, targetY, win) {
  1200. if (targetX == inputState.x && targetY == inputState.y) {
  1201. return;
  1202. }
  1203. switch (inputState.subtype) {
  1204. case action.PointerType.Mouse:
  1205. let mouseEvent = new action.Mouse("mousemove");
  1206. mouseEvent.update(inputState);
  1207. //TODO both pointermove (if available) and mousemove
  1208. event.synthesizeMouseAtPoint(targetX, targetY, mouseEvent, win);
  1209. break;
  1210. case action.PointerType.Pen:
  1211. case action.PointerType.Touch:
  1212. throw new UnsupportedOperationError("Only 'mouse' pointer type is supported");
  1213. default:
  1214. throw new TypeError(`Unknown pointer type: ${inputState.subtype}`);
  1215. }
  1216. inputState.x = targetX;
  1217. inputState.y = targetY;
  1218. }
  1219. /**
  1220. * Dispatch a pause action equivalent waiting for |a.duration| milliseconds, or a
  1221. * default time interval of |tickDuration|.
  1222. *
  1223. * @param {action.Action} a
  1224. * Action to dispatch.
  1225. * @param {number} tickDuration
  1226. * Duration in milliseconds of this tick.
  1227. *
  1228. * @return {Promise}
  1229. * Promise that is resolved after the specified time interval.
  1230. */
  1231. function dispatchPause(a, tickDuration) {
  1232. const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  1233. let duration = typeof a.duration == "undefined" ? tickDuration : a.duration;
  1234. return new Promise(resolve =>
  1235. timer.initWithCallback(resolve, duration, Ci.nsITimer.TYPE_ONE_SHOT)
  1236. );
  1237. }
  1238. // helpers
  1239. /**
  1240. * Force any pending DOM events to fire.
  1241. *
  1242. * @param {?} container
  1243. * Object with |frame| attribute of type |nsIDOMWindow|.
  1244. *
  1245. * @return {Promise}
  1246. * Promise to flush DOM events.
  1247. */
  1248. function flushEvents(container) {
  1249. return new Promise(resolve => container.frame.requestAnimationFrame(resolve));
  1250. }
  1251. function capitalize(str) {
  1252. assert.string(str);
  1253. return str.charAt(0).toUpperCase() + str.slice(1);
  1254. }
  1255. function inViewPort(x, y, win) {
  1256. assert.number(x, `Expected x to be finite number`);
  1257. assert.number(y, `Expected y to be finite number`);
  1258. // Viewport includes scrollbars if rendered.
  1259. return !(x < 0 || y < 0 || x > win.innerWidth || y > win.innerHeight);
  1260. }
  1261. function getElementCenter(elementReference, seenEls, container) {
  1262. if (element.isWebElementReference(elementReference)) {
  1263. let uuid = elementReference[element.Key] || elementReference[element.LegacyKey];
  1264. let el = seenEls.get(uuid, container);
  1265. return element.coordinates(el);
  1266. }
  1267. }