Bug 1842773 - Part 5: Add ArrayBuffer.prototype.{maxByteLength,resizable} getters...
[gecko.git] / testing / mochitest / tests / SimpleTest / EventUtils.js
blob739c7052ea1f45c88f95404c1564187232738c88
1 /**
2  * EventUtils provides some utility methods for creating and sending DOM events.
3  *
4  *  When adding methods to this file, please add a performance test for it.
5  */
7 // Certain functions assume this is loaded into browser window scope.
8 // This is modifiable because certain chrome tests create their own gBrowser.
9 /* global gBrowser:true */
11 // This file is used both in privileged and unprivileged contexts, so we have to
12 // be careful about our access to Components.interfaces. We also want to avoid
13 // naming collisions with anything that might be defined in the scope that imports
14 // this script.
16 // Even if the real |Components| doesn't exist, we might shim in a simple JS
17 // placebo for compat. An easy way to differentiate this from the real thing
18 // is whether the property is read-only or not.  The real |Components| property
19 // is read-only.
20 /* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */
21 window.__defineGetter__("_EU_Ci", function () {
22   var c = Object.getOwnPropertyDescriptor(window, "Components");
23   return c && c.value && !c.writable ? Ci : SpecialPowers.Ci;
24 });
26 window.__defineGetter__("_EU_Cc", function () {
27   var c = Object.getOwnPropertyDescriptor(window, "Components");
28   return c && c.value && !c.writable ? Cc : SpecialPowers.Cc;
29 });
31 window.__defineGetter__("_EU_Cu", function () {
32   var c = Object.getOwnPropertyDescriptor(window, "Components");
33   return c && c.value && !c.writable ? Cu : SpecialPowers.Cu;
34 });
36 window.__defineGetter__("_EU_ChromeUtils", function () {
37   var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils");
38   return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils;
39 });
41 window.__defineGetter__("_EU_OS", function () {
42   delete this._EU_OS;
43   try {
44     this._EU_OS = _EU_ChromeUtils.import(
45       "resource://gre/modules/AppConstants.jsm"
46     ).platform;
47   } catch (ex) {
48     this._EU_OS = null;
49   }
50   return this._EU_OS;
51 });
53 function _EU_isMac(aWindow = window) {
54   if (window._EU_OS) {
55     return window._EU_OS == "macosx";
56   }
57   if (aWindow) {
58     try {
59       return aWindow.navigator.platform.indexOf("Mac") > -1;
60     } catch (ex) {}
61   }
62   return navigator.platform.indexOf("Mac") > -1;
65 function _EU_isWin(aWindow = window) {
66   if (window._EU_OS) {
67     return window._EU_OS == "win";
68   }
69   if (aWindow) {
70     try {
71       return aWindow.navigator.platform.indexOf("Win") > -1;
72     } catch (ex) {}
73   }
74   return navigator.platform.indexOf("Win") > -1;
77 function _EU_isLinux(aWindow = window) {
78   if (window._EU_OS) {
79     return window._EU_OS == "linux";
80   }
81   if (aWindow) {
82     try {
83       return aWindow.navigator.platform.startsWith("Linux");
84     } catch (ex) {}
85   }
86   return navigator.platform.startsWith("Linux");
89 function _EU_isAndroid(aWindow = window) {
90   if (window._EU_OS) {
91     return window._EU_OS == "android";
92   }
93   if (aWindow) {
94     try {
95       return aWindow.navigator.userAgent.includes("Android");
96     } catch (ex) {}
97   }
98   return navigator.userAgent.includes("Android");
101 function _EU_maybeWrap(o) {
102   // We're used in some contexts where there is no SpecialPowers and also in
103   // some where it exists but has no wrap() method.  And this is somewhat
104   // independent of whether window.Components is a thing...
105   var haveWrap = false;
106   try {
107     haveWrap = SpecialPowers.wrap != undefined;
108   } catch (e) {
109     // Just leave it false.
110   }
111   if (!haveWrap) {
112     // Not much we can do here.
113     return o;
114   }
115   var c = Object.getOwnPropertyDescriptor(window, "Components");
116   return c && c.value && !c.writable ? o : SpecialPowers.wrap(o);
119 function _EU_maybeUnwrap(o) {
120   var haveWrap = false;
121   try {
122     haveWrap = SpecialPowers.unwrap != undefined;
123   } catch (e) {
124     // Just leave it false.
125   }
126   if (!haveWrap) {
127     // Not much we can do here.
128     return o;
129   }
130   var c = Object.getOwnPropertyDescriptor(window, "Components");
131   return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
134 function _EU_getPlatform() {
135   if (_EU_isWin()) {
136     return "windows";
137   }
138   if (_EU_isMac()) {
139     return "mac";
140   }
141   if (_EU_isAndroid()) {
142     return "android";
143   }
144   if (_EU_isLinux()) {
145     return "linux";
146   }
147   return "unknown";
151  * promiseElementReadyForUserInput() dispatches mousemove events to aElement
152  * and waits one of them for a while.  Then, returns "resolved" state when it's
153  * successfully received.  Otherwise, if it couldn't receive mousemove event on
154  * it, this throws an exception.  So, aElement must be an element which is
155  * assumed non-collapsed visible element in the window.
157  * This is useful if you need to synthesize mouse events via the main process
158  * but your test cannot check whether the element is now in APZ to deliver
159  * a user input event.
160  */
161 async function promiseElementReadyForUserInput(
162   aElement,
163   aWindow = window,
164   aLogFunc = null
165 ) {
166   if (typeof aElement == "string") {
167     aElement = aWindow.document.getElementById(aElement);
168   }
170   function waitForMouseMoveForHittest() {
171     return new Promise(resolve => {
172       let timeout;
173       const onHit = () => {
174         if (aLogFunc) {
175           aLogFunc("mousemove received");
176         }
177         aWindow.clearInterval(timeout);
178         resolve(true);
179       };
180       aElement.addEventListener("mousemove", onHit, {
181         capture: true,
182         once: true,
183       });
184       timeout = aWindow.setInterval(() => {
185         if (aLogFunc) {
186           aLogFunc("mousemove not received in this 300ms");
187         }
188         aElement.removeEventListener("mousemove", onHit, {
189           capture: true,
190         });
191         resolve(false);
192       }, 300);
193       synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow);
194     });
195   }
196   for (let i = 0; i < 20; i++) {
197     if (await waitForMouseMoveForHittest()) {
198       return Promise.resolve();
199     }
200   }
201   throw new Error("The element or the window did not become interactive");
204 function getElement(id) {
205   return typeof id == "string" ? document.getElementById(id) : id;
208 this.$ = this.getElement;
210 function computeButton(aEvent) {
211   if (typeof aEvent.button != "undefined") {
212     return aEvent.button;
213   }
214   return aEvent.type == "contextmenu" ? 2 : 0;
217 function computeButtons(aEvent, utils) {
218   if (typeof aEvent.buttons != "undefined") {
219     return aEvent.buttons;
220   }
222   if (typeof aEvent.button != "undefined") {
223     return utils.MOUSE_BUTTONS_NOT_SPECIFIED;
224   }
226   if (typeof aEvent.type != "undefined" && aEvent.type != "mousedown") {
227     return utils.MOUSE_BUTTONS_NO_BUTTON;
228   }
230   return utils.MOUSE_BUTTONS_NOT_SPECIFIED;
234  * Send a mouse event to the node aTarget (aTarget can be an id, or an
235  * actual node) . The "event" passed in to aEvent is just a JavaScript
236  * object with the properties set that the real mouse event object should
237  * have. This includes the type of the mouse event. Pretty much all those
238  * properties are optional.
239  * E.g. to send an click event to the node with id 'node' you might do this:
241  * ``sendMouseEvent({type:'click'}, 'node');``
242  */
243 function sendMouseEvent(aEvent, aTarget, aWindow) {
244   if (
245     ![
246       "click",
247       "contextmenu",
248       "dblclick",
249       "mousedown",
250       "mouseup",
251       "mouseover",
252       "mouseout",
253     ].includes(aEvent.type)
254   ) {
255     throw new Error(
256       "sendMouseEvent doesn't know about event type '" + aEvent.type + "'"
257     );
258   }
260   if (!aWindow) {
261     aWindow = window;
262   }
264   if (typeof aTarget == "string") {
265     aTarget = aWindow.document.getElementById(aTarget);
266   }
268   var event = aWindow.document.createEvent("MouseEvent");
270   var typeArg = aEvent.type;
271   var canBubbleArg = true;
272   var cancelableArg = true;
273   var viewArg = aWindow;
274   var detailArg =
275     aEvent.detail ||
276     // eslint-disable-next-line no-nested-ternary
277     (aEvent.type == "click" ||
278     aEvent.type == "mousedown" ||
279     aEvent.type == "mouseup"
280       ? 1
281       : aEvent.type == "dblclick"
282       ? 2
283       : 0);
284   var screenXArg = aEvent.screenX || 0;
285   var screenYArg = aEvent.screenY || 0;
286   var clientXArg = aEvent.clientX || 0;
287   var clientYArg = aEvent.clientY || 0;
288   var ctrlKeyArg = aEvent.ctrlKey || false;
289   var altKeyArg = aEvent.altKey || false;
290   var shiftKeyArg = aEvent.shiftKey || false;
291   var metaKeyArg = aEvent.metaKey || false;
292   var buttonArg = computeButton(aEvent);
293   var relatedTargetArg = aEvent.relatedTarget || null;
295   event.initMouseEvent(
296     typeArg,
297     canBubbleArg,
298     cancelableArg,
299     viewArg,
300     detailArg,
301     screenXArg,
302     screenYArg,
303     clientXArg,
304     clientYArg,
305     ctrlKeyArg,
306     altKeyArg,
307     shiftKeyArg,
308     metaKeyArg,
309     buttonArg,
310     relatedTargetArg
311   );
313   // If documentURIObject exists or `window` is a stub object, we're in
314   // a chrome scope, so don't bother trying to go through SpecialPowers.
315   if (!window.document || window.document.documentURIObject) {
316     return aTarget.dispatchEvent(event);
317   }
318   return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
321 function isHidden(aElement) {
322   var box = aElement.getBoundingClientRect();
323   return box.width == 0 && box.height == 0;
327  * Send a drag event to the node aTarget (aTarget can be an id, or an
328  * actual node) . The "event" passed in to aEvent is just a JavaScript
329  * object with the properties set that the real drag event object should
330  * have. This includes the type of the drag event.
331  */
332 function sendDragEvent(aEvent, aTarget, aWindow = window) {
333   if (
334     ![
335       "drag",
336       "dragstart",
337       "dragend",
338       "dragover",
339       "dragenter",
340       "dragleave",
341       "drop",
342     ].includes(aEvent.type)
343   ) {
344     throw new Error(
345       "sendDragEvent doesn't know about event type '" + aEvent.type + "'"
346     );
347   }
349   if (typeof aTarget == "string") {
350     aTarget = aWindow.document.getElementById(aTarget);
351   }
353   /*
354    * Drag event cannot be performed if the element is hidden, except 'dragend'
355    * event where the element can becomes hidden after start dragging.
356    */
357   if (aEvent.type != "dragend" && isHidden(aTarget)) {
358     var targetName = aTarget.nodeName;
359     if ("id" in aTarget && aTarget.id) {
360       targetName += "#" + aTarget.id;
361     }
362     throw new Error(`${aEvent.type} event target ${targetName} is hidden`);
363   }
365   var event = aWindow.document.createEvent("DragEvent");
367   var typeArg = aEvent.type;
368   var canBubbleArg = true;
369   var cancelableArg = true;
370   var viewArg = aWindow;
371   var detailArg = aEvent.detail || 0;
372   var screenXArg = aEvent.screenX || 0;
373   var screenYArg = aEvent.screenY || 0;
374   var clientXArg = aEvent.clientX || 0;
375   var clientYArg = aEvent.clientY || 0;
376   var ctrlKeyArg = aEvent.ctrlKey || false;
377   var altKeyArg = aEvent.altKey || false;
378   var shiftKeyArg = aEvent.shiftKey || false;
379   var metaKeyArg = aEvent.metaKey || false;
380   var buttonArg = computeButton(aEvent);
381   var relatedTargetArg = aEvent.relatedTarget || null;
382   var dataTransfer = aEvent.dataTransfer || null;
384   event.initDragEvent(
385     typeArg,
386     canBubbleArg,
387     cancelableArg,
388     viewArg,
389     detailArg,
390     screenXArg,
391     screenYArg,
392     clientXArg,
393     clientYArg,
394     ctrlKeyArg,
395     altKeyArg,
396     shiftKeyArg,
397     metaKeyArg,
398     buttonArg,
399     relatedTargetArg,
400     dataTransfer
401   );
403   if (aEvent._domDispatchOnly) {
404     return aTarget.dispatchEvent(event);
405   }
407   var utils = _getDOMWindowUtils(aWindow);
408   return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
412  * Send the char aChar to the focused element.  This method handles casing of
413  * chars (sends the right charcode, and sends a shift key for uppercase chars).
414  * No other modifiers are handled at this point.
416  * For now this method only works for ASCII characters and emulates the shift
417  * key state on US keyboard layout.
418  */
419 function sendChar(aChar, aWindow) {
420   var hasShift;
421   // Emulate US keyboard layout for the shiftKey state.
422   switch (aChar) {
423     case "!":
424     case "@":
425     case "#":
426     case "$":
427     case "%":
428     case "^":
429     case "&":
430     case "*":
431     case "(":
432     case ")":
433     case "_":
434     case "+":
435     case "{":
436     case "}":
437     case ":":
438     case '"':
439     case "|":
440     case "<":
441     case ">":
442     case "?":
443       hasShift = true;
444       break;
445     default:
446       hasShift =
447         aChar.toLowerCase() != aChar.toUpperCase() &&
448         aChar == aChar.toUpperCase();
449       break;
450   }
451   synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
455  * Send the string aStr to the focused element.
457  * For now this method only works for ASCII characters and emulates the shift
458  * key state on US keyboard layout.
459  */
460 function sendString(aStr, aWindow) {
461   for (let i = 0; i < aStr.length; ++i) {
462     // Do not split a surrogate pair to call synthesizeKey.  Dispatching two
463     // sets of keydown and keyup caused by two calls of synthesizeKey is not
464     // good behavior.  It could happen due to a bug, but a surrogate pair should
465     // be introduced with one key press operation.  Therefore, calling it with
466     // a surrogate pair is the right thing.
467     // Note that TextEventDispatcher will consider whether a surrogate pair
468     // should cause one or two keypress events automatically.  Therefore, we
469     // don't need to check the related prefs here.
470     if (
471       (aStr.charCodeAt(i) & 0xfc00) == 0xd800 &&
472       i + 1 < aStr.length &&
473       (aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00
474     ) {
475       sendChar(aStr.substring(i, i + 2), aWindow);
476       i++;
477     } else {
478       sendChar(aStr.charAt(i), aWindow);
479     }
480   }
484  * Send the non-character key aKey to the focused node.
485  * The name of the key should be the part that comes after ``DOM_VK_`` in the
486  * KeyEvent constant name for this key.
487  * No modifiers are handled at this point.
488  */
489 function sendKey(aKey, aWindow) {
490   var keyName = "VK_" + aKey.toUpperCase();
491   synthesizeKey(keyName, { shiftKey: false }, aWindow);
495  * Parse the key modifier flags from aEvent. Used to share code between
496  * synthesizeMouse and synthesizeKey.
497  */
498 function _parseModifiers(aEvent, aWindow = window) {
499   var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils;
500   var mval = 0;
501   if (aEvent.shiftKey) {
502     mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
503   }
504   if (aEvent.ctrlKey) {
505     mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
506   }
507   if (aEvent.altKey) {
508     mval |= nsIDOMWindowUtils.MODIFIER_ALT;
509   }
510   if (aEvent.metaKey) {
511     mval |= nsIDOMWindowUtils.MODIFIER_META;
512   }
513   if (aEvent.accelKey) {
514     mval |= _EU_isMac(aWindow)
515       ? nsIDOMWindowUtils.MODIFIER_META
516       : nsIDOMWindowUtils.MODIFIER_CONTROL;
517   }
518   if (aEvent.altGrKey) {
519     mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
520   }
521   if (aEvent.capsLockKey) {
522     mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
523   }
524   if (aEvent.fnKey) {
525     mval |= nsIDOMWindowUtils.MODIFIER_FN;
526   }
527   if (aEvent.fnLockKey) {
528     mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK;
529   }
530   if (aEvent.numLockKey) {
531     mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
532   }
533   if (aEvent.scrollLockKey) {
534     mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
535   }
536   if (aEvent.symbolKey) {
537     mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL;
538   }
539   if (aEvent.symbolLockKey) {
540     mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
541   }
543   return mval;
547  * Synthesize a mouse event on a target. The actual client point is determined
548  * by taking the aTarget's client box and offseting it by aOffsetX and
549  * aOffsetY. This allows mouse clicks to be simulated by calling this method.
551  * aEvent is an object which may contain the properties:
552  *   `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`,
553  *   `button`, `type`.
554  *   For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`.
556  * If the type is specified, an mouse event of that type is fired. Otherwise,
557  * a mousedown followed by a mouseup is performed.
559  * aWindow is optional, and defaults to the current window object.
561  * Returns whether the event had preventDefault() called on it.
562  */
563 function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
564   var rect = aTarget.getBoundingClientRect();
565   return synthesizeMouseAtPoint(
566     rect.left + aOffsetX,
567     rect.top + aOffsetY,
568     aEvent,
569     aWindow
570   );
572 function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
573   var rect = aTarget.getBoundingClientRect();
574   return synthesizeTouchAtPoint(
575     rect.left + aOffsetX,
576     rect.top + aOffsetY,
577     aEvent,
578     aWindow
579   );
583  * Return the drag service.  Note that if we're in the headless mode, this
584  * may return null because the service may be never instantiated (e.g., on
585  * Linux).
586  */
587 function getDragService() {
588   try {
589     return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
590       _EU_Ci.nsIDragService
591     );
592   } catch (e) {
593     // If we're in the headless mode, the drag service may be never
594     // instantiated.  In this case, an exception is thrown.  Let's ignore
595     // any exceptions since without the drag service, nobody can create a
596     // drag session.
597     return null;
598   }
602  * End drag session if there is.
604  * TODO: This should synthesize "drop" if necessary.
606  * @param left          X offset in the viewport
607  * @param top           Y offset in the viewport
608  * @param aEvent        The event data, the modifiers are applied to the
609  *                      "dragend" event.
610  * @param aWindow       The window.
611  * @return              true if handled.  In this case, the caller should not
612  *                      synthesize DOM events basically.
613  */
614 function _maybeEndDragSession(left, top, aEvent, aWindow) {
615   const dragService = getDragService();
616   const dragSession = dragService?.getCurrentSession();
617   if (!dragSession) {
618     return false;
619   }
620   // FIXME: If dragSession.dragAction is not
621   // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we
622   // need to synthesize a "drop" event or call setDragEndPointForTests here to
623   // set proper left/top to `dragend` event.
624   try {
625     dragService.endDragSession(false, _parseModifiers(aEvent, aWindow));
626   } catch (e) {}
627   return true;
630 function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) {
631   const dragSession = getDragService()?.getCurrentSession();
632   if (!dragSession) {
633     return false;
634   }
635   const target = aWindow.document.elementFromPoint(left, top);
636   if (target) {
637     sendDragEvent(
638       createDragEventObject(
639         "dragover",
640         target,
641         aWindow,
642         dragSession.dataTransfer,
643         {
644           accelKey: aEvent.accelKey,
645           altKey: aEvent.altKey,
646           altGrKey: aEvent.altGrKey,
647           ctrlKey: aEvent.ctrlKey,
648           metaKey: aEvent.metaKey,
649           shiftKey: aEvent.shiftKey,
650           capsLockKey: aEvent.capsLockKey,
651           fnKey: aEvent.fnKey,
652           fnLockKey: aEvent.fnLockKey,
653           numLockKey: aEvent.numLockKey,
654           scrollLockKey: aEvent.scrollLockKey,
655           symbolKey: aEvent.symbolKey,
656           symbolLockKey: aEvent.symbolLockKey,
657         }
658       ),
659       target,
660       aWindow
661     );
662   }
663   return true;
667  * Synthesize a mouse event at a particular point in aWindow.
669  * aEvent is an object which may contain the properties:
670  *   `shiftKey`, `ctrlKey`, `altKey`, `metaKey`, `accessKey`, `clickCount`,
671  *   `button`, `type`.
672  *   For valid `type`s see nsIDOMWindowUtils' `sendMouseEvent`.
674  * If the type is specified, an mouse event of that type is fired. Otherwise,
675  * a mousedown followed by a mouseup is performed.
677  * aWindow is optional, and defaults to the current window object.
678  */
679 function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) {
680   if (aEvent.allowToHandleDragDrop) {
681     if (aEvent.type == "mouseup" || !aEvent.type) {
682       if (_maybeEndDragSession(left, top, aEvent, aWindow)) {
683         return false;
684       }
685     } else if (aEvent.type == "mousemove") {
686       if (_maybeSynthesizeDragOver(left, top, aEvent, aWindow)) {
687         return false;
688       }
689     }
690   }
692   var utils = _getDOMWindowUtils(aWindow);
693   var defaultPrevented = false;
695   if (utils) {
696     var button = computeButton(aEvent);
697     var clickCount = aEvent.clickCount || 1;
698     var modifiers = _parseModifiers(aEvent, aWindow);
699     var pressure = "pressure" in aEvent ? aEvent.pressure : 0;
701     // aWindow might be cross-origin from us.
702     var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent;
704     // Default source to mouse.
705     var inputSource =
706       "inputSource" in aEvent
707         ? aEvent.inputSource
708         : MouseEvent.MOZ_SOURCE_MOUSE;
709     // Compute a pointerId if needed.
710     var id;
711     if ("id" in aEvent) {
712       id = aEvent.id;
713     } else {
714       var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN;
715       id = isFromPen
716         ? utils.DEFAULT_PEN_POINTER_ID
717         : utils.DEFAULT_MOUSE_POINTER_ID;
718     }
720     var isDOMEventSynthesized =
721       "isSynthesized" in aEvent ? aEvent.isSynthesized : true;
722     var isWidgetEventSynthesized =
723       "isWidgetEventSynthesized" in aEvent
724         ? aEvent.isWidgetEventSynthesized
725         : false;
726     if ("type" in aEvent && aEvent.type) {
727       defaultPrevented = utils.sendMouseEvent(
728         aEvent.type,
729         left,
730         top,
731         button,
732         clickCount,
733         modifiers,
734         false,
735         pressure,
736         inputSource,
737         isDOMEventSynthesized,
738         isWidgetEventSynthesized,
739         computeButtons(aEvent, utils),
740         id
741       );
742     } else {
743       utils.sendMouseEvent(
744         "mousedown",
745         left,
746         top,
747         button,
748         clickCount,
749         modifiers,
750         false,
751         pressure,
752         inputSource,
753         isDOMEventSynthesized,
754         isWidgetEventSynthesized,
755         computeButtons(Object.assign({ type: "mousedown" }, aEvent), utils),
756         id
757       );
758       utils.sendMouseEvent(
759         "mouseup",
760         left,
761         top,
762         button,
763         clickCount,
764         modifiers,
765         false,
766         pressure,
767         inputSource,
768         isDOMEventSynthesized,
769         isWidgetEventSynthesized,
770         computeButtons(Object.assign({ type: "mouseup" }, aEvent), utils),
771         id
772       );
773     }
774   }
776   return defaultPrevented;
779 function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) {
780   var utils = _getDOMWindowUtils(aWindow);
781   let defaultPrevented = false;
783   if (utils) {
784     var id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID;
785     var rx = aEvent.rx || 1;
786     var ry = aEvent.ry || 1;
787     var angle = aEvent.angle || 0;
788     var force = aEvent.force || (aEvent.type === "touchend" ? 0 : 1);
789     var tiltX = aEvent.tiltX || 0;
790     var tiltY = aEvent.tiltY || 0;
791     var twist = aEvent.twist || 0;
792     var modifiers = _parseModifiers(aEvent, aWindow);
794     if ("type" in aEvent && aEvent.type) {
795       defaultPrevented = utils.sendTouchEvent(
796         aEvent.type,
797         [id],
798         [left],
799         [top],
800         [rx],
801         [ry],
802         [angle],
803         [force],
804         [tiltX],
805         [tiltY],
806         [twist],
807         modifiers
808       );
809     } else {
810       utils.sendTouchEvent(
811         "touchstart",
812         [id],
813         [left],
814         [top],
815         [rx],
816         [ry],
817         [angle],
818         [force],
819         [tiltX],
820         [tiltY],
821         [twist],
822         modifiers
823       );
824       utils.sendTouchEvent(
825         "touchend",
826         [id],
827         [left],
828         [top],
829         [rx],
830         [ry],
831         [angle],
832         [force],
833         [tiltX],
834         [tiltY],
835         [twist],
836         modifiers
837       );
838     }
839   }
840   return defaultPrevented;
843 // Call synthesizeMouse with coordinates at the center of aTarget.
844 function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) {
845   var rect = aTarget.getBoundingClientRect();
846   return synthesizeMouse(
847     aTarget,
848     rect.width / 2,
849     rect.height / 2,
850     aEvent,
851     aWindow
852   );
854 function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) {
855   var rect = aTarget.getBoundingClientRect();
856   synthesizeTouchAtPoint(
857     rect.left + rect.width / 2,
858     rect.top + rect.height / 2,
859     aEvent,
860     aWindow
861   );
865  * Synthesize a wheel event without flush layout at a particular point in
866  * aWindow.
868  * aEvent is an object which may contain the properties:
869  *   shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
870  *   deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum,
871  *   isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX,
872  *   expectedOverflowDeltaY
874  * deltaMode must be defined, others are ok even if undefined.
876  * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value.  The
877  * value is just checked as 0 or positive or negative.
879  * aWindow is optional, and defaults to the current window object.
880  */
881 function synthesizeWheelAtPoint(aLeft, aTop, aEvent, aWindow = window) {
882   var utils = _getDOMWindowUtils(aWindow);
883   if (!utils) {
884     return;
885   }
887   var modifiers = _parseModifiers(aEvent, aWindow);
888   var options = 0;
889   if (aEvent.isNoLineOrPageDelta) {
890     options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE;
891   }
892   if (aEvent.isMomentum) {
893     options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM;
894   }
895   if (aEvent.isCustomizedByPrefs) {
896     options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS;
897   }
898   if (typeof aEvent.expectedOverflowDeltaX !== "undefined") {
899     if (aEvent.expectedOverflowDeltaX === 0) {
900       options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO;
901     } else if (aEvent.expectedOverflowDeltaX > 0) {
902       options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE;
903     } else {
904       options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE;
905     }
906   }
907   if (typeof aEvent.expectedOverflowDeltaY !== "undefined") {
908     if (aEvent.expectedOverflowDeltaY === 0) {
909       options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO;
910     } else if (aEvent.expectedOverflowDeltaY > 0) {
911       options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE;
912     } else {
913       options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE;
914     }
915   }
917   // Avoid the JS warnings "reference to undefined property"
918   if (!aEvent.deltaX) {
919     aEvent.deltaX = 0;
920   }
921   if (!aEvent.deltaY) {
922     aEvent.deltaY = 0;
923   }
924   if (!aEvent.deltaZ) {
925     aEvent.deltaZ = 0;
926   }
928   var lineOrPageDeltaX =
929     // eslint-disable-next-line no-nested-ternary
930     aEvent.lineOrPageDeltaX != null
931       ? aEvent.lineOrPageDeltaX
932       : aEvent.deltaX > 0
933       ? Math.floor(aEvent.deltaX)
934       : Math.ceil(aEvent.deltaX);
935   var lineOrPageDeltaY =
936     // eslint-disable-next-line no-nested-ternary
937     aEvent.lineOrPageDeltaY != null
938       ? aEvent.lineOrPageDeltaY
939       : aEvent.deltaY > 0
940       ? Math.floor(aEvent.deltaY)
941       : Math.ceil(aEvent.deltaY);
942   utils.sendWheelEvent(
943     aLeft,
944     aTop,
945     aEvent.deltaX,
946     aEvent.deltaY,
947     aEvent.deltaZ,
948     aEvent.deltaMode,
949     modifiers,
950     lineOrPageDeltaX,
951     lineOrPageDeltaY,
952     options
953   );
957  * Synthesize a wheel event on a target. The actual client point is determined
958  * by taking the aTarget's client box and offseting it by aOffsetX and
959  * aOffsetY.
961  * aEvent is an object which may contain the properties:
962  *   shiftKey, ctrlKey, altKey, metaKey, accessKey, deltaX, deltaY, deltaZ,
963  *   deltaMode, lineOrPageDeltaX, lineOrPageDeltaY, isMomentum,
964  *   isNoLineOrPageDelta, isCustomizedByPrefs, expectedOverflowDeltaX,
965  *   expectedOverflowDeltaY
967  * deltaMode must be defined, others are ok even if undefined.
969  * expectedOverflowDeltaX and expectedOverflowDeltaY take integer value.  The
970  * value is just checked as 0 or positive or negative.
972  * aWindow is optional, and defaults to the current window object.
973  */
974 function synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) {
975   var rect = aTarget.getBoundingClientRect();
976   synthesizeWheelAtPoint(
977     rect.left + aOffsetX,
978     rect.top + aOffsetY,
979     aEvent,
980     aWindow
981   );
984 const _FlushModes = {
985   FLUSH: 0,
986   NOFLUSH: 1,
989 function _sendWheelAndPaint(
990   aTarget,
991   aOffsetX,
992   aOffsetY,
993   aEvent,
994   aCallback,
995   aFlushMode = _FlushModes.FLUSH,
996   aWindow = window
997 ) {
998   var utils = _getDOMWindowUtils(aWindow);
999   if (!utils) {
1000     return;
1001   }
1003   if (utils.isMozAfterPaintPending) {
1004     // If a paint is pending, then APZ may be waiting for a scroll acknowledgement
1005     // from the content thread. If we send a wheel event now, it could be ignored
1006     // by APZ (or its scroll offset could be overridden). To avoid problems we
1007     // just wait for the paint to complete.
1008     aWindow.waitForAllPaintsFlushed(function () {
1009       _sendWheelAndPaint(
1010         aTarget,
1011         aOffsetX,
1012         aOffsetY,
1013         aEvent,
1014         aCallback,
1015         aFlushMode,
1016         aWindow
1017       );
1018     });
1019     return;
1020   }
1022   var onwheel = function () {
1023     SpecialPowers.removeSystemEventListener(window, "wheel", onwheel);
1025     // Wait one frame since the wheel event has not caused a refresh observer
1026     // to be added yet.
1027     setTimeout(function () {
1028       utils.advanceTimeAndRefresh(1000);
1030       if (!aCallback) {
1031         utils.advanceTimeAndRefresh(0);
1032         return;
1033       }
1035       var waitForPaints = function () {
1036         SpecialPowers.Services.obs.removeObserver(
1037           waitForPaints,
1038           "apz-repaints-flushed"
1039         );
1040         aWindow.waitForAllPaintsFlushed(function () {
1041           utils.restoreNormalRefresh();
1042           aCallback();
1043         });
1044       };
1046       SpecialPowers.Services.obs.addObserver(
1047         waitForPaints,
1048         "apz-repaints-flushed"
1049       );
1050       if (!utils.flushApzRepaints(aWindow)) {
1051         waitForPaints();
1052       }
1053     }, 0);
1054   };
1056   // Listen for the system wheel event, because it happens after all of
1057   // the other wheel events, including legacy events.
1058   SpecialPowers.addSystemEventListener(aWindow, "wheel", onwheel);
1059   if (aFlushMode === _FlushModes.FLUSH) {
1060     synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
1061   } else {
1062     synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow);
1063   }
1067  * This is a wrapper around synthesizeWheel that waits for the wheel event
1068  * to be dispatched and for the subsequent layout/paints to be flushed.
1070  * This requires including paint_listener.js. Tests must call
1071  * DOMWindowUtils.restoreNormalRefresh() before finishing, if they use this
1072  * function.
1074  * If no callback is provided, the caller is assumed to have its own method of
1075  * determining scroll completion and the refresh driver is not automatically
1076  * restored.
1077  */
1078 function sendWheelAndPaint(
1079   aTarget,
1080   aOffsetX,
1081   aOffsetY,
1082   aEvent,
1083   aCallback,
1084   aWindow = window
1085 ) {
1086   _sendWheelAndPaint(
1087     aTarget,
1088     aOffsetX,
1089     aOffsetY,
1090     aEvent,
1091     aCallback,
1092     _FlushModes.FLUSH,
1093     aWindow
1094   );
1098  * Similar to sendWheelAndPaint but without flushing layout for obtaining
1099  * ``aTarget`` position in ``aWindow`` before sending the wheel event.
1100  * ``aOffsetX`` and ``aOffsetY`` should be offsets against aWindow.
1101  */
1102 function sendWheelAndPaintNoFlush(
1103   aTarget,
1104   aOffsetX,
1105   aOffsetY,
1106   aEvent,
1107   aCallback,
1108   aWindow = window
1109 ) {
1110   _sendWheelAndPaint(
1111     aTarget,
1112     aOffsetX,
1113     aOffsetY,
1114     aEvent,
1115     aCallback,
1116     _FlushModes.NOFLUSH,
1117     aWindow
1118   );
1121 function synthesizeNativeTapAtCenter(
1122   aTarget,
1123   aLongTap = false,
1124   aCallback = null,
1125   aWindow = window
1126 ) {
1127   let rect = aTarget.getBoundingClientRect();
1128   return synthesizeNativeTap(
1129     aTarget,
1130     rect.width / 2,
1131     rect.height / 2,
1132     aLongTap,
1133     aCallback,
1134     aWindow
1135   );
1138 function synthesizeNativeTap(
1139   aTarget,
1140   aOffsetX,
1141   aOffsetY,
1142   aLongTap = false,
1143   aCallback = null,
1144   aWindow = window
1145 ) {
1146   let utils = _getDOMWindowUtils(aWindow);
1147   if (!utils) {
1148     return;
1149   }
1151   let scale = aWindow.devicePixelRatio;
1152   let rect = aTarget.getBoundingClientRect();
1153   let x = (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale;
1154   let y = (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale;
1156   let observer = {
1157     observe: (subject, topic, data) => {
1158       if (aCallback && topic == "mouseevent") {
1159         aCallback(data);
1160       }
1161     },
1162   };
1163   utils.sendNativeTouchTap(x, y, aLongTap, observer);
1167  * Similar to synthesizeMouse but generates a native widget level event
1168  * (so will actually move the "real" mouse cursor etc. Be careful because
1169  * this can impact later code as well! (e.g. with hover states etc.)
1171  * @description There are 3 mutually exclusive ways of indicating the location of the
1172  * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``,
1173  * or pass ``screenX`` and ``screenY``. Do not attempt to mix these.
1175  * @param {object} aParams
1176  * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove"
1177  * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element
1178  * @param {Boolean} [aParams.atCenter]
1179  *        Instead of offsetX/Y, synthesize the event at center of `target`.
1180  * @param {Number} [aParams.offsetX]
1181  *        X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
1182  * @param {Number} [aParams.offsetY]
1183  *        Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
1184  * @param {Number} [aParams.screenX]
1185  *        X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
1186  *        Neither offsetX/Y nor atCenter must be set if this is set.
1187  * @param {Number} [aParams.screenY]
1188  *        Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
1189  *        Neither offsetX/Y nor atCenter must be set if this is set.
1190  * @param {String} [aParams.scale="screenPixelsPerCSSPixel"]
1191  *        If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used.
1192  *        If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel.
1193  * @param {Number} [aParams.button=0]
1194  *        Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
1195  * @param {Object} [aParams.modifiers={}]
1196  *        Active modifiers, see `_parseNativeModifiers`
1197  * @param {Window} [aParams.win=window]
1198  *        The window to use its utils. Defaults to the window in which EventUtils.js is running.
1199  * @param {Element} [aParams.elementOnWidget=target]
1200  *        Defaults to target. If element under the point is in another widget from target's widget,
1201  *        e.g., when it's in a XUL <panel>, specify this.
1202  */
1203 function synthesizeNativeMouseEvent(aParams, aCallback = null) {
1204   const {
1205     type,
1206     target,
1207     offsetX,
1208     offsetY,
1209     atCenter,
1210     screenX,
1211     screenY,
1212     scale = "screenPixelsPerCSSPixel",
1213     button = 0,
1214     modifiers = {},
1215     win = window,
1216     elementOnWidget = target,
1217   } = aParams;
1218   if (atCenter) {
1219     if (offsetX != undefined || offsetY != undefined) {
1220       throw Error(
1221         `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
1222       );
1223     }
1224     if (screenX != undefined || screenY != undefined) {
1225       throw Error(
1226         `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
1227       );
1228     }
1229     if (!target) {
1230       throw Error("atCenter is specified, but target is not specified");
1231     }
1232   } else if (offsetX != undefined && offsetY != undefined) {
1233     if (screenX != undefined || screenY != undefined) {
1234       throw Error(
1235         `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
1236       );
1237     }
1238     if (!target) {
1239       throw Error(
1240         "offsetX and offsetY are specified, but target is not specified"
1241       );
1242     }
1243   } else if (screenX != undefined && screenY != undefined) {
1244     if (offsetX != undefined || offsetY != undefined) {
1245       throw Error(
1246         `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
1247       );
1248     }
1249   }
1250   const utils = _getDOMWindowUtils(win);
1251   if (!utils) {
1252     return;
1253   }
1255   const rect = target?.getBoundingClientRect();
1256   let resolution = 1.0;
1257   try {
1258     resolution = _getDOMWindowUtils(win.top).getResolution();
1259   } catch (e) {
1260     // XXX How to get mobile viewport scale on Fission+xorigin since
1261     //     window.top access isn't allowed due to cross-origin?
1262   }
1263   const scaleValue = (() => {
1264     if (scale === "inScreenPixels") {
1265       return 1.0;
1266     }
1267     if (scale === "screenPixelsPerCSSPixel") {
1268       return win.devicePixelRatio;
1269     }
1270     throw Error(`invalid scale value (${scale}) is specified`);
1271   })();
1272   // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
1273   //     so use window.top's mozInnerScreen. But this won't work fission+xorigin
1274   //     with mobile viewport until mozInnerScreen returns valid value with
1275   //     scale.
1276   const x = (() => {
1277     if (screenX != undefined) {
1278       return screenX * scaleValue;
1279     }
1280     let winInnerOffsetX = win.mozInnerScreenX;
1281     try {
1282       winInnerOffsetX =
1283         win.top.mozInnerScreenX +
1284         (win.mozInnerScreenX - win.top.mozInnerScreenX) * resolution;
1285     } catch (e) {
1286       // XXX fission+xorigin test throws permission denied since win.top is
1287       //     cross-origin.
1288     }
1289     return (
1290       (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution +
1291         winInnerOffsetX) *
1292       scaleValue
1293     );
1294   })();
1295   const y = (() => {
1296     if (screenY != undefined) {
1297       return screenY * scaleValue;
1298     }
1299     let winInnerOffsetY = win.mozInnerScreenY;
1300     try {
1301       winInnerOffsetY =
1302         win.top.mozInnerScreenY +
1303         (win.mozInnerScreenY - win.top.mozInnerScreenY) * resolution;
1304     } catch (e) {
1305       // XXX fission+xorigin test throws permission denied since win.top is
1306       //     cross-origin.
1307     }
1308     return (
1309       (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution +
1310         winInnerOffsetY) *
1311       scaleValue
1312     );
1313   })();
1314   const modifierFlags = _parseNativeModifiers(modifiers);
1316   const observer = {
1317     observe: (subject, topic, data) => {
1318       if (aCallback && topic == "mouseevent") {
1319         aCallback(data);
1320       }
1321     },
1322   };
1323   if (type === "click") {
1324     utils.sendNativeMouseEvent(
1325       x,
1326       y,
1327       utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
1328       button,
1329       modifierFlags,
1330       elementOnWidget,
1331       function () {
1332         utils.sendNativeMouseEvent(
1333           x,
1334           y,
1335           utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
1336           button,
1337           modifierFlags,
1338           elementOnWidget,
1339           observer
1340         );
1341       }
1342     );
1343     return;
1344   }
1345   utils.sendNativeMouseEvent(
1346     x,
1347     y,
1348     (() => {
1349       switch (type) {
1350         case "mousedown":
1351           return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
1352         case "mouseup":
1353           return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
1354         case "mousemove":
1355           return utils.NATIVE_MOUSE_MESSAGE_MOVE;
1356         default:
1357           throw Error(`Invalid type is specified: ${type}`);
1358       }
1359     })(),
1360     button,
1361     modifierFlags,
1362     elementOnWidget,
1363     observer
1364   );
1367 function promiseNativeMouseEvent(aParams) {
1368   return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve));
1371 function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) {
1372   const listener = aParams.eventTargetToListen || aParams.target;
1373   const eventType = aParams.eventTypeToWait || aParams.type;
1374   listener.addEventListener(eventType, aCallback, {
1375     capture: true,
1376     once: true,
1377   });
1378   synthesizeNativeMouseEvent(aParams);
1381 function promiseNativeMouseEventAndWaitForEvent(aParams) {
1382   return new Promise(resolve =>
1383     synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve)
1384   );
1388  * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse
1389  * event to be dispatched to the target content.
1391  * This API is supposed to be used in those test cases that synthesize some
1392  * input events to chrome process and have some checks in content.
1393  */
1394 function synthesizeAndWaitNativeMouseMove(
1395   aTarget,
1396   aOffsetX,
1397   aOffsetY,
1398   aCallback,
1399   aWindow = window
1400 ) {
1401   let browser = gBrowser.selectedTab.linkedBrowser;
1402   let mm = browser.messageManager;
1403   let { ContentTask } = _EU_ChromeUtils.importESModule(
1404     "resource://testing-common/ContentTask.sys.mjs"
1405   );
1407   let eventRegisteredPromise = new Promise(resolve => {
1408     mm.addMessageListener(
1409       "Test:MouseMoveRegistered",
1410       function processed(message) {
1411         mm.removeMessageListener("Test:MouseMoveRegistered", processed);
1412         resolve();
1413       }
1414     );
1415   });
1416   let eventReceivedPromise = ContentTask.spawn(
1417     browser,
1418     [aOffsetX, aOffsetY],
1419     ([clientX, clientY]) => {
1420       return new Promise(resolve => {
1421         addEventListener("mousemove", function onMouseMoveEvent(e) {
1422           if (e.clientX == clientX && e.clientY == clientY) {
1423             removeEventListener("mousemove", onMouseMoveEvent);
1424             resolve();
1425           }
1426         });
1427         sendAsyncMessage("Test:MouseMoveRegistered");
1428       });
1429     }
1430   );
1431   eventRegisteredPromise.then(() => {
1432     synthesizeNativeMouseEvent({
1433       type: "mousemove",
1434       target: aTarget,
1435       offsetX: aOffsetX,
1436       offsetY: aOffsetY,
1437       win: aWindow,
1438     });
1439   });
1440   return eventReceivedPromise;
1444  * Synthesize a key event. It is targeted at whatever would be targeted by an
1445  * actual keypress by the user, typically the focused element.
1447  * @param {String} aKey
1448  *        Should be either:
1450  *        - key value (recommended).  If you specify a non-printable key name,
1451  *          prepend the ``KEY_`` prefix.  Otherwise, specifying a printable key, the
1452  *          key value should be specified.
1454  *        - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``).  This is available
1455  *          only for compatibility with legacy API.  Don't use this with new tests.
1457  * @param {Object} [aEvent]
1458  *        Optional event object with more specifics about the key event to
1459  *        synthesize.
1460  * @param {String} [aEvent.code]
1461  *        If you don't specify this explicitly, it'll be guessed from aKey
1462  *        of US keyboard layout.  Note that this value may be different
1463  *        between browsers.  For example, "Insert" is never set only on
1464  *        macOS since actual key operation won't cause this code value.
1465  *        In such case, the value becomes empty string.
1466  *        If you need to emulate non-US keyboard layout or virtual keyboard
1467  *        which doesn't emulate hardware key input, you should set this value
1468  *        to empty string explicitly.
1469  * @param {Number} [aEvent.repeat]
1470  *        If you emulate auto-repeat, you should set the count of repeat.
1471  *        This method will automatically synthesize keydown (and keypress).
1472  * @param {*} aEvent.location
1473  *        If you want to specify this, you can specify this explicitly.
1474  *        However, if you don't specify this value, it will be computed
1475  *        from code value.
1476  * @param {String} aEvent.type
1477  *        Basically, you shouldn't specify this.  Then, this function will
1478  *        synthesize keydown (, keypress) and keyup.
1479  *        If keydown is specified, this only fires keydown (and keypress if
1480  *        it should be fired).
1481  *        If keyup is specified, this only fires keyup.
1482  * @param {Number} aEvent.keyCode
1483  *        Must be 0 - 255 (0xFF). If this is specified explicitly,
1484  *        .keyCode value is initialized with this value.
1485  * @param {Window} aWindow
1486  *        Is optional and defaults to the current window object.
1487  * @param {Function} aCallback
1488  *        Is optional and can be used to receive notifications from TIP.
1490  * @description
1491  * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``,
1492  * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``,
1493  * ``shiftKey``, ``symbolKey``, ``symbolLockKey``
1494  * Basically, you shouldn't use these attributes.  nsITextInputProcessor
1495  * manages modifier key state when you synthesize modifier key events.
1496  * However, if some of these attributes are true, this function activates
1497  * the modifiers only during dispatching the key events.
1498  * Note that if some of these values are false, they are ignored (i.e.,
1499  * not inactivated with this function).
1501  */
1502 function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) {
1503   const event = aEvent === undefined || aEvent === null ? {} : aEvent;
1504   let dispatchKeydown =
1505     !("type" in event) || event.type === "keydown" || !event.type;
1506   const dispatchKeyup =
1507     !("type" in event) || event.type === "keyup" || !event.type;
1509   if (dispatchKeydown && aKey == "KEY_Escape") {
1510     let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event)));
1511     eventForKeydown.type = "keydown";
1512     if (
1513       _maybeEndDragSession(
1514         // TODO: We should set the last dragover point instead
1515         0,
1516         0,
1517         eventForKeydown,
1518         aWindow
1519       )
1520     ) {
1521       if (!dispatchKeyup) {
1522         return;
1523       }
1524       // We don't need to dispatch only keydown event because it's consumed by
1525       // the drag session.
1526       dispatchKeydown = false;
1527     }
1528   }
1530   var TIP = _getTIP(aWindow, aCallback);
1531   if (!TIP) {
1532     return;
1533   }
1534   var KeyboardEvent = _getKeyboardEvent(aWindow);
1535   var modifiers = _emulateToActivateModifiers(TIP, event, aWindow);
1536   var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow);
1537   var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
1539   try {
1540     if (dispatchKeydown) {
1541       TIP.keydown(keyEvent, keyEventDict.flags);
1542       if ("repeat" in event && event.repeat > 1) {
1543         keyEventDict.dictionary.repeat = true;
1544         var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
1545         for (var i = 1; i < event.repeat; i++) {
1546           TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
1547         }
1548       }
1549     }
1550     if (dispatchKeyup) {
1551       TIP.keyup(keyEvent, keyEventDict.flags);
1552     }
1553   } finally {
1554     _emulateToInactivateModifiers(TIP, modifiers, aWindow);
1555   }
1559  * This is a wrapper around synthesizeKey that waits for the key event to be
1560  * dispatched to the target content. It returns a promise which is resolved
1561  * when the content receives the key event.
1563  * This API is supposed to be used in those test cases that synthesize some
1564  * input events to chrome process and have some checks in content.
1565  */
1566 function synthesizeAndWaitKey(
1567   aKey,
1568   aEvent,
1569   aWindow = window,
1570   checkBeforeSynthesize,
1571   checkAfterSynthesize
1572 ) {
1573   let browser = gBrowser.selectedTab.linkedBrowser;
1574   let mm = browser.messageManager;
1575   let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow)
1576     .dictionary.keyCode;
1577   let { ContentTask } = _EU_ChromeUtils.importESModule(
1578     "resource://testing-common/ContentTask.sys.mjs"
1579   );
1581   let keyRegisteredPromise = new Promise(resolve => {
1582     mm.addMessageListener("Test:KeyRegistered", function processed(message) {
1583       mm.removeMessageListener("Test:KeyRegistered", processed);
1584       resolve();
1585     });
1586   });
1587   // eslint-disable-next-line no-shadow
1588   let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => {
1589     return new Promise(resolve => {
1590       addEventListener("keyup", function onKeyEvent(e) {
1591         if (e.keyCode == keyCode) {
1592           removeEventListener("keyup", onKeyEvent);
1593           resolve();
1594         }
1595       });
1596       sendAsyncMessage("Test:KeyRegistered");
1597     });
1598   });
1599   keyRegisteredPromise.then(() => {
1600     if (checkBeforeSynthesize) {
1601       checkBeforeSynthesize();
1602     }
1603     synthesizeKey(aKey, aEvent, aWindow);
1604     if (checkAfterSynthesize) {
1605       checkAfterSynthesize();
1606     }
1607   });
1608   return keyReceivedPromise;
1611 function _parseNativeModifiers(aModifiers, aWindow = window) {
1612   let modifiers = 0;
1613   if (aModifiers.capsLockKey) {
1614     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
1615   }
1616   if (aModifiers.numLockKey) {
1617     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
1618   }
1619   if (aModifiers.shiftKey) {
1620     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
1621   }
1622   if (aModifiers.shiftRightKey) {
1623     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
1624   }
1625   if (aModifiers.ctrlKey) {
1626     modifiers |=
1627       SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
1628   }
1629   if (aModifiers.ctrlRightKey) {
1630     modifiers |=
1631       SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
1632   }
1633   if (aModifiers.altKey) {
1634     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
1635   }
1636   if (aModifiers.altRightKey) {
1637     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
1638   }
1639   if (aModifiers.metaKey) {
1640     modifiers |=
1641       SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
1642   }
1643   if (aModifiers.metaRightKey) {
1644     modifiers |=
1645       SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
1646   }
1647   if (aModifiers.helpKey) {
1648     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
1649   }
1650   if (aModifiers.fnKey) {
1651     modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
1652   }
1653   if (aModifiers.numericKeyPadKey) {
1654     modifiers |=
1655       SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
1656   }
1658   if (aModifiers.accelKey) {
1659     modifiers |= _EU_isMac(aWindow)
1660       ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
1661       : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
1662   }
1663   if (aModifiers.accelRightKey) {
1664     modifiers |= _EU_isMac(aWindow)
1665       ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
1666       : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
1667   }
1668   if (aModifiers.altGrKey) {
1669     modifiers |= _EU_isMac(aWindow)
1670       ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
1671       : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
1672   }
1673   return modifiers;
1676 // Mac: Any unused number is okay for adding new keyboard layout.
1677 //      When you add new keyboard layout here, you need to modify
1678 //      TISInputSourceWrapper::InitByLayoutID().
1679 // Win: These constants can be found by inspecting registry keys under
1680 //      HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts
1682 const KEYBOARD_LAYOUT_ARABIC = {
1683   name: "Arabic",
1684   Mac: 6,
1685   Win: 0x00000401,
1686   hasAltGrOnWin: false,
1688 _defineConstant("KEYBOARD_LAYOUT_ARABIC", KEYBOARD_LAYOUT_ARABIC);
1689 const KEYBOARD_LAYOUT_ARABIC_PC = {
1690   name: "Arabic - PC",
1691   Mac: 7,
1692   Win: null,
1693   hasAltGrOnWin: false,
1695 _defineConstant("KEYBOARD_LAYOUT_ARABIC_PC", KEYBOARD_LAYOUT_ARABIC_PC);
1696 const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = {
1697   name: "Brazilian ABNT",
1698   Mac: null,
1699   Win: 0x00000416,
1700   hasAltGrOnWin: true,
1702 _defineConstant(
1703   "KEYBOARD_LAYOUT_BRAZILIAN_ABNT",
1704   KEYBOARD_LAYOUT_BRAZILIAN_ABNT
1706 const KEYBOARD_LAYOUT_DVORAK_QWERTY = {
1707   name: "Dvorak-QWERTY",
1708   Mac: 4,
1709   Win: null,
1710   hasAltGrOnWin: false,
1712 _defineConstant("KEYBOARD_LAYOUT_DVORAK_QWERTY", KEYBOARD_LAYOUT_DVORAK_QWERTY);
1713 const KEYBOARD_LAYOUT_EN_US = {
1714   name: "US",
1715   Mac: 0,
1716   Win: 0x00000409,
1717   hasAltGrOnWin: false,
1719 _defineConstant("KEYBOARD_LAYOUT_EN_US", KEYBOARD_LAYOUT_EN_US);
1720 const KEYBOARD_LAYOUT_FRENCH = {
1721   name: "French",
1722   Mac: 8, // Some keys mapped different from PC, e.g., Digit6, Digit8, Equal, Slash and Backslash
1723   Win: 0x0000040c,
1724   hasAltGrOnWin: true,
1726 _defineConstant("KEYBOARD_LAYOUT_FRENCH", KEYBOARD_LAYOUT_FRENCH);
1727 const KEYBOARD_LAYOUT_FRENCH_PC = {
1728   name: "French-PC",
1729   Mac: 13, // Compatible with Windows
1730   Win: 0x0000040c,
1731   hasAltGrOnWin: true,
1733 _defineConstant("KEYBOARD_LAYOUT_FRENCH_PC", KEYBOARD_LAYOUT_FRENCH_PC);
1734 const KEYBOARD_LAYOUT_GREEK = {
1735   name: "Greek",
1736   Mac: 1,
1737   Win: 0x00000408,
1738   hasAltGrOnWin: true,
1740 _defineConstant("KEYBOARD_LAYOUT_GREEK", KEYBOARD_LAYOUT_GREEK);
1741 const KEYBOARD_LAYOUT_GERMAN = {
1742   name: "German",
1743   Mac: 2,
1744   Win: 0x00000407,
1745   hasAltGrOnWin: true,
1747 _defineConstant("KEYBOARD_LAYOUT_GERMAN", KEYBOARD_LAYOUT_GERMAN);
1748 const KEYBOARD_LAYOUT_HEBREW = {
1749   name: "Hebrew",
1750   Mac: 9,
1751   Win: 0x0000040d,
1752   hasAltGrOnWin: true,
1754 _defineConstant("KEYBOARD_LAYOUT_HEBREW", KEYBOARD_LAYOUT_HEBREW);
1755 const KEYBOARD_LAYOUT_JAPANESE = {
1756   name: "Japanese",
1757   Mac: null,
1758   Win: 0x00000411,
1759   hasAltGrOnWin: false,
1761 _defineConstant("KEYBOARD_LAYOUT_JAPANESE", KEYBOARD_LAYOUT_JAPANESE);
1762 const KEYBOARD_LAYOUT_KHMER = {
1763   name: "Khmer",
1764   Mac: null,
1765   Win: 0x00000453,
1766   hasAltGrOnWin: true,
1767 }; // available on Win7 or later.
1768 _defineConstant("KEYBOARD_LAYOUT_KHMER", KEYBOARD_LAYOUT_KHMER);
1769 const KEYBOARD_LAYOUT_LITHUANIAN = {
1770   name: "Lithuanian",
1771   Mac: 10,
1772   Win: 0x00010427,
1773   hasAltGrOnWin: true,
1775 _defineConstant("KEYBOARD_LAYOUT_LITHUANIAN", KEYBOARD_LAYOUT_LITHUANIAN);
1776 const KEYBOARD_LAYOUT_NORWEGIAN = {
1777   name: "Norwegian",
1778   Mac: 11,
1779   Win: 0x00000414,
1780   hasAltGrOnWin: true,
1782 _defineConstant("KEYBOARD_LAYOUT_NORWEGIAN", KEYBOARD_LAYOUT_NORWEGIAN);
1783 const KEYBOARD_LAYOUT_RUSSIAN = {
1784   name: "Russian",
1785   Mac: null,
1786   Win: 0x00000419,
1787   hasAltGrOnWin: true, // No AltGr, but Ctrl + Alt + Digit8 introduces a char
1789 _defineConstant("KEYBOARD_LAYOUT_RUSSIAN", KEYBOARD_LAYOUT_RUSSIAN);
1790 const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = {
1791   name: "Russian - Mnemonic",
1792   Mac: null,
1793   Win: 0x00020419,
1794   hasAltGrOnWin: true,
1795 }; // available on Win8 or later.
1796 _defineConstant(
1797   "KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC",
1798   KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC
1800 const KEYBOARD_LAYOUT_SPANISH = {
1801   name: "Spanish",
1802   Mac: 12,
1803   Win: 0x0000040a,
1804   hasAltGrOnWin: true,
1806 _defineConstant("KEYBOARD_LAYOUT_SPANISH", KEYBOARD_LAYOUT_SPANISH);
1807 const KEYBOARD_LAYOUT_SWEDISH = {
1808   name: "Swedish",
1809   Mac: 3,
1810   Win: 0x0000041d,
1811   hasAltGrOnWin: true,
1813 _defineConstant("KEYBOARD_LAYOUT_SWEDISH", KEYBOARD_LAYOUT_SWEDISH);
1814 const KEYBOARD_LAYOUT_THAI = {
1815   name: "Thai",
1816   Mac: 5,
1817   Win: 0x0002041e,
1818   hasAltGrOnWin: false,
1820 _defineConstant("KEYBOARD_LAYOUT_THAI", KEYBOARD_LAYOUT_THAI);
1823  * synthesizeNativeKey() dispatches native key event on active window.
1824  * This is implemented only on Windows and Mac. Note that this function
1825  * dispatches the key event asynchronously and returns immediately. If a
1826  * callback function is provided, the callback will be called upon
1827  * completion of the key dispatch.
1829  * @param aKeyboardLayout       One of KEYBOARD_LAYOUT_* defined above.
1830  * @param aNativeKeyCode        A native keycode value defined in
1831  *                              NativeKeyCodes.js.
1832  * @param aModifiers            Modifier keys.  If no modifire key is pressed,
1833  *                              this must be {}.  Otherwise, one or more items
1834  *                              referred in _parseNativeModifiers() must be
1835  *                              true.
1836  * @param aChars                Specify characters which should be generated
1837  *                              by the key event.
1838  * @param aUnmodifiedChars      Specify characters of unmodified (except Shift)
1839  *                              aChar value.
1840  * @param aCallback             If provided, this callback will be invoked
1841  *                              once the native keys have been processed
1842  *                              by Gecko. Will never be called if this
1843  *                              function returns false.
1844  * @return                      True if this function succeed dispatching
1845  *                              native key event.  Otherwise, false.
1846  */
1848 function synthesizeNativeKey(
1849   aKeyboardLayout,
1850   aNativeKeyCode,
1851   aModifiers,
1852   aChars,
1853   aUnmodifiedChars,
1854   aCallback,
1855   aWindow = window
1856 ) {
1857   var utils = _getDOMWindowUtils(aWindow);
1858   if (!utils) {
1859     return false;
1860   }
1861   var nativeKeyboardLayout = null;
1862   if (_EU_isMac(aWindow)) {
1863     nativeKeyboardLayout = aKeyboardLayout.Mac;
1864   } else if (_EU_isWin(aWindow)) {
1865     nativeKeyboardLayout = aKeyboardLayout.Win;
1866   }
1867   if (nativeKeyboardLayout === null) {
1868     return false;
1869   }
1871   var observer = {
1872     observe(aSubject, aTopic, aData) {
1873       if (aCallback && aTopic == "keyevent") {
1874         aCallback(aData);
1875       }
1876     },
1877   };
1878   utils.sendNativeKeyEvent(
1879     nativeKeyboardLayout,
1880     aNativeKeyCode,
1881     _parseNativeModifiers(aModifiers, aWindow),
1882     aChars,
1883     aUnmodifiedChars,
1884     observer
1885   );
1886   return true;
1889 var _gSeenEvent = false;
1892  * Indicate that an event with an original target of aExpectedTarget and
1893  * a type of aExpectedEvent is expected to be fired, or not expected to
1894  * be fired.
1895  */
1896 function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) {
1897   if (!aExpectedTarget || !aExpectedEvent) {
1898     return null;
1899   }
1901   _gSeenEvent = false;
1903   var type =
1904     aExpectedEvent.charAt(0) == "!"
1905       ? aExpectedEvent.substring(1)
1906       : aExpectedEvent;
1907   var eventHandler = function (event) {
1908     var epassed =
1909       !_gSeenEvent &&
1910       event.originalTarget == aExpectedTarget &&
1911       event.type == type;
1912     is(
1913       epassed,
1914       true,
1915       aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "")
1916     );
1917     _gSeenEvent = true;
1918   };
1920   aExpectedTarget.addEventListener(type, eventHandler);
1921   return eventHandler;
1925  * Check if the event was fired or not. The event handler aEventHandler
1926  * will be removed.
1927  */
1928 function _checkExpectedEvent(
1929   aExpectedTarget,
1930   aExpectedEvent,
1931   aEventHandler,
1932   aTestName
1933 ) {
1934   if (aEventHandler) {
1935     var expectEvent = aExpectedEvent.charAt(0) != "!";
1936     var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
1937     aExpectedTarget.removeEventListener(type, aEventHandler);
1938     var desc = type + " event";
1939     if (!expectEvent) {
1940       desc += " not";
1941     }
1942     is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired");
1943   }
1945   _gSeenEvent = false;
1949  * Similar to synthesizeMouse except that a test is performed to see if an
1950  * event is fired at the right target as a result.
1952  * aExpectedTarget - the expected originalTarget of the event.
1953  * aExpectedEvent - the expected type of the event, such as 'select'.
1954  * aTestName - the test name when outputing results
1956  * To test that an event is not fired, use an expected type preceded by an
1957  * exclamation mark, such as '!select'. This might be used to test that a
1958  * click on a disabled element doesn't fire certain events for instance.
1960  * aWindow is optional, and defaults to the current window object.
1961  */
1962 function synthesizeMouseExpectEvent(
1963   aTarget,
1964   aOffsetX,
1965   aOffsetY,
1966   aEvent,
1967   aExpectedTarget,
1968   aExpectedEvent,
1969   aTestName,
1970   aWindow
1971 ) {
1972   var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
1973   synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
1974   _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
1978  * Similar to synthesizeKey except that a test is performed to see if an
1979  * event is fired at the right target as a result.
1981  * aExpectedTarget - the expected originalTarget of the event.
1982  * aExpectedEvent - the expected type of the event, such as 'select'.
1983  * aTestName - the test name when outputing results
1985  * To test that an event is not fired, use an expected type preceded by an
1986  * exclamation mark, such as '!select'.
1988  * aWindow is optional, and defaults to the current window object.
1989  */
1990 function synthesizeKeyExpectEvent(
1991   key,
1992   aEvent,
1993   aExpectedTarget,
1994   aExpectedEvent,
1995   aTestName,
1996   aWindow
1997 ) {
1998   var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
1999   synthesizeKey(key, aEvent, aWindow);
2000   _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
2003 function disableNonTestMouseEvents(aDisable) {
2004   var domutils = _getDOMWindowUtils();
2005   domutils.disableNonTestMouseEvents(aDisable);
2008 function _getDOMWindowUtils(aWindow = window) {
2009   // Leave this here as something, somewhere, passes a falsy argument
2010   // to this, causing the |window| default argument not to get picked up.
2011   if (!aWindow) {
2012     aWindow = window;
2013   }
2015   // If documentURIObject exists or `window` is a stub object, we're in
2016   // a chrome scope, so don't bother trying to go through SpecialPowers.
2017   if (!window.document || window.document.documentURIObject) {
2018     return aWindow.windowUtils;
2019   }
2021   // we need parent.SpecialPowers for:
2022   //  layout/base/tests/test_reftests_with_caret.html
2023   //  chrome: toolkit/content/tests/chrome/test_findbar.xul
2024   //  chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
2025   if ("SpecialPowers" in window && window.SpecialPowers != undefined) {
2026     return SpecialPowers.getDOMWindowUtils(aWindow);
2027   }
2028   if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) {
2029     return parent.SpecialPowers.getDOMWindowUtils(aWindow);
2030   }
2032   // TODO: this is assuming we are in chrome space
2033   return aWindow.windowUtils;
2036 function _defineConstant(name, value) {
2037   Object.defineProperty(this, name, {
2038     value,
2039     enumerable: true,
2040     writable: false,
2041   });
2044 const COMPOSITION_ATTR_RAW_CLAUSE =
2045   _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE;
2046 _defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE);
2047 const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE =
2048   _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE;
2049 _defineConstant(
2050   "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE",
2051   COMPOSITION_ATTR_SELECTED_RAW_CLAUSE
2053 const COMPOSITION_ATTR_CONVERTED_CLAUSE =
2054   _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE;
2055 _defineConstant(
2056   "COMPOSITION_ATTR_CONVERTED_CLAUSE",
2057   COMPOSITION_ATTR_CONVERTED_CLAUSE
2059 const COMPOSITION_ATTR_SELECTED_CLAUSE =
2060   _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE;
2061 _defineConstant(
2062   "COMPOSITION_ATTR_SELECTED_CLAUSE",
2063   COMPOSITION_ATTR_SELECTED_CLAUSE
2066 var TIPMap = new WeakMap();
2068 function _getTIP(aWindow, aCallback) {
2069   if (!aWindow) {
2070     aWindow = window;
2071   }
2072   var tip;
2073   if (TIPMap.has(aWindow)) {
2074     tip = TIPMap.get(aWindow);
2075   } else {
2076     tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance(
2077       _EU_Ci.nsITextInputProcessor
2078     );
2079     TIPMap.set(aWindow, tip);
2080   }
2081   if (!tip.beginInputTransactionForTests(aWindow, aCallback)) {
2082     tip = null;
2083     TIPMap.delete(aWindow);
2084   }
2085   return tip;
2088 function _getKeyboardEvent(aWindow = window) {
2089   if (typeof KeyboardEvent != "undefined") {
2090     try {
2091       // See if the object can be instantiated; sometimes this yields
2092       // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
2093       new KeyboardEvent("", {});
2094       return KeyboardEvent;
2095     } catch (ex) {}
2096   }
2097   if (typeof content != "undefined" && "KeyboardEvent" in content) {
2098     return content.KeyboardEvent;
2099   }
2100   return aWindow.KeyboardEvent;
2103 // eslint-disable-next-line complexity
2104 function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) {
2105   var KeyboardEvent = _getKeyboardEvent(aWindow);
2106   switch (aKeyCode) {
2107     case KeyboardEvent.DOM_VK_CANCEL:
2108       return "Cancel";
2109     case KeyboardEvent.DOM_VK_HELP:
2110       return "Help";
2111     case KeyboardEvent.DOM_VK_BACK_SPACE:
2112       return "Backspace";
2113     case KeyboardEvent.DOM_VK_TAB:
2114       return "Tab";
2115     case KeyboardEvent.DOM_VK_CLEAR:
2116       return "Clear";
2117     case KeyboardEvent.DOM_VK_RETURN:
2118       return "Enter";
2119     case KeyboardEvent.DOM_VK_SHIFT:
2120       return "Shift";
2121     case KeyboardEvent.DOM_VK_CONTROL:
2122       return "Control";
2123     case KeyboardEvent.DOM_VK_ALT:
2124       return "Alt";
2125     case KeyboardEvent.DOM_VK_PAUSE:
2126       return "Pause";
2127     case KeyboardEvent.DOM_VK_EISU:
2128       return "Eisu";
2129     case KeyboardEvent.DOM_VK_ESCAPE:
2130       return "Escape";
2131     case KeyboardEvent.DOM_VK_CONVERT:
2132       return "Convert";
2133     case KeyboardEvent.DOM_VK_NONCONVERT:
2134       return "NonConvert";
2135     case KeyboardEvent.DOM_VK_ACCEPT:
2136       return "Accept";
2137     case KeyboardEvent.DOM_VK_MODECHANGE:
2138       return "ModeChange";
2139     case KeyboardEvent.DOM_VK_PAGE_UP:
2140       return "PageUp";
2141     case KeyboardEvent.DOM_VK_PAGE_DOWN:
2142       return "PageDown";
2143     case KeyboardEvent.DOM_VK_END:
2144       return "End";
2145     case KeyboardEvent.DOM_VK_HOME:
2146       return "Home";
2147     case KeyboardEvent.DOM_VK_LEFT:
2148       return "ArrowLeft";
2149     case KeyboardEvent.DOM_VK_UP:
2150       return "ArrowUp";
2151     case KeyboardEvent.DOM_VK_RIGHT:
2152       return "ArrowRight";
2153     case KeyboardEvent.DOM_VK_DOWN:
2154       return "ArrowDown";
2155     case KeyboardEvent.DOM_VK_SELECT:
2156       return "Select";
2157     case KeyboardEvent.DOM_VK_PRINT:
2158       return "Print";
2159     case KeyboardEvent.DOM_VK_EXECUTE:
2160       return "Execute";
2161     case KeyboardEvent.DOM_VK_PRINTSCREEN:
2162       return "PrintScreen";
2163     case KeyboardEvent.DOM_VK_INSERT:
2164       return "Insert";
2165     case KeyboardEvent.DOM_VK_DELETE:
2166       return "Delete";
2167     case KeyboardEvent.DOM_VK_WIN:
2168       return "OS";
2169     case KeyboardEvent.DOM_VK_CONTEXT_MENU:
2170       return "ContextMenu";
2171     case KeyboardEvent.DOM_VK_SLEEP:
2172       return "Standby";
2173     case KeyboardEvent.DOM_VK_F1:
2174       return "F1";
2175     case KeyboardEvent.DOM_VK_F2:
2176       return "F2";
2177     case KeyboardEvent.DOM_VK_F3:
2178       return "F3";
2179     case KeyboardEvent.DOM_VK_F4:
2180       return "F4";
2181     case KeyboardEvent.DOM_VK_F5:
2182       return "F5";
2183     case KeyboardEvent.DOM_VK_F6:
2184       return "F6";
2185     case KeyboardEvent.DOM_VK_F7:
2186       return "F7";
2187     case KeyboardEvent.DOM_VK_F8:
2188       return "F8";
2189     case KeyboardEvent.DOM_VK_F9:
2190       return "F9";
2191     case KeyboardEvent.DOM_VK_F10:
2192       return "F10";
2193     case KeyboardEvent.DOM_VK_F11:
2194       return "F11";
2195     case KeyboardEvent.DOM_VK_F12:
2196       return "F12";
2197     case KeyboardEvent.DOM_VK_F13:
2198       return "F13";
2199     case KeyboardEvent.DOM_VK_F14:
2200       return "F14";
2201     case KeyboardEvent.DOM_VK_F15:
2202       return "F15";
2203     case KeyboardEvent.DOM_VK_F16:
2204       return "F16";
2205     case KeyboardEvent.DOM_VK_F17:
2206       return "F17";
2207     case KeyboardEvent.DOM_VK_F18:
2208       return "F18";
2209     case KeyboardEvent.DOM_VK_F19:
2210       return "F19";
2211     case KeyboardEvent.DOM_VK_F20:
2212       return "F20";
2213     case KeyboardEvent.DOM_VK_F21:
2214       return "F21";
2215     case KeyboardEvent.DOM_VK_F22:
2216       return "F22";
2217     case KeyboardEvent.DOM_VK_F23:
2218       return "F23";
2219     case KeyboardEvent.DOM_VK_F24:
2220       return "F24";
2221     case KeyboardEvent.DOM_VK_NUM_LOCK:
2222       return "NumLock";
2223     case KeyboardEvent.DOM_VK_SCROLL_LOCK:
2224       return "ScrollLock";
2225     case KeyboardEvent.DOM_VK_VOLUME_MUTE:
2226       return "AudioVolumeMute";
2227     case KeyboardEvent.DOM_VK_VOLUME_DOWN:
2228       return "AudioVolumeDown";
2229     case KeyboardEvent.DOM_VK_VOLUME_UP:
2230       return "AudioVolumeUp";
2231     case KeyboardEvent.DOM_VK_META:
2232       return "Meta";
2233     case KeyboardEvent.DOM_VK_ALTGR:
2234       return "AltGraph";
2235     case KeyboardEvent.DOM_VK_PROCESSKEY:
2236       return "Process";
2237     case KeyboardEvent.DOM_VK_ATTN:
2238       return "Attn";
2239     case KeyboardEvent.DOM_VK_CRSEL:
2240       return "CrSel";
2241     case KeyboardEvent.DOM_VK_EXSEL:
2242       return "ExSel";
2243     case KeyboardEvent.DOM_VK_EREOF:
2244       return "EraseEof";
2245     case KeyboardEvent.DOM_VK_PLAY:
2246       return "Play";
2247     default:
2248       return "Unidentified";
2249   }
2252 function _createKeyboardEventDictionary(
2253   aKey,
2254   aKeyEvent,
2255   aTIP = null,
2256   aWindow = window
2257 ) {
2258   var result = { dictionary: null, flags: 0 };
2259   var keyCodeIsDefined = "keyCode" in aKeyEvent;
2260   var keyCode =
2261     keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255
2262       ? aKeyEvent.keyCode
2263       : 0;
2264   var keyName = "Unidentified";
2265   var code = aKeyEvent.code;
2266   if (!aTIP) {
2267     aTIP = _getTIP(aWindow);
2268   }
2269   if (aKey.indexOf("KEY_") == 0) {
2270     keyName = aKey.substr("KEY_".length);
2271     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
2272     if (code === undefined) {
2273       code = aTIP.computeCodeValueOfNonPrintableKey(
2274         keyName,
2275         aKeyEvent.location
2276       );
2277     }
2278   } else if (aKey.indexOf("VK_") == 0) {
2279     keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey];
2280     if (!keyCode) {
2281       throw new Error("Unknown key: " + aKey);
2282     }
2283     keyName = _guessKeyNameFromKeyCode(keyCode, aWindow);
2284     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
2285     if (code === undefined) {
2286       code = aTIP.computeCodeValueOfNonPrintableKey(
2287         keyName,
2288         aKeyEvent.location
2289       );
2290     }
2291   } else if (aKey != "") {
2292     keyName = aKey;
2293     if (!keyCodeIsDefined) {
2294       keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
2295         aKey,
2296         aKeyEvent.location
2297       );
2298     }
2299     if (!keyCode) {
2300       result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
2301     }
2302     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
2303     if (code === undefined) {
2304       code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
2305         keyName,
2306         aKeyEvent.location
2307       );
2308     }
2309   }
2310   var locationIsDefined = "location" in aKeyEvent;
2311   if (locationIsDefined && aKeyEvent.location === 0) {
2312     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
2313   }
2314   if (aKeyEvent.doNotMarkKeydownAsProcessed) {
2315     result.flags |=
2316       _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED;
2317   }
2318   if (aKeyEvent.markKeyupAsProcessed) {
2319     result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED;
2320   }
2321   result.dictionary = {
2322     key: keyName,
2323     code,
2324     location: locationIsDefined ? aKeyEvent.location : 0,
2325     repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false,
2326     keyCode,
2327   };
2328   return result;
2331 function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) {
2332   if (!aKeyEvent) {
2333     return null;
2334   }
2335   var KeyboardEvent = _getKeyboardEvent(aWindow);
2337   var modifiers = {
2338     normal: [
2339       { key: "Alt", attr: "altKey" },
2340       { key: "AltGraph", attr: "altGraphKey" },
2341       { key: "Control", attr: "ctrlKey" },
2342       { key: "Fn", attr: "fnKey" },
2343       { key: "Meta", attr: "metaKey" },
2344       { key: "Shift", attr: "shiftKey" },
2345       { key: "Symbol", attr: "symbolKey" },
2346       { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" },
2347     ],
2348     lockable: [
2349       { key: "CapsLock", attr: "capsLockKey" },
2350       { key: "FnLock", attr: "fnLockKey" },
2351       { key: "NumLock", attr: "numLockKey" },
2352       { key: "ScrollLock", attr: "scrollLockKey" },
2353       { key: "SymbolLock", attr: "symbolLockKey" },
2354     ],
2355   };
2357   for (let i = 0; i < modifiers.normal.length; i++) {
2358     if (!aKeyEvent[modifiers.normal[i].attr]) {
2359       continue;
2360     }
2361     if (aTIP.getModifierState(modifiers.normal[i].key)) {
2362       continue; // already activated.
2363     }
2364     let event = new KeyboardEvent("", { key: modifiers.normal[i].key });
2365     aTIP.keydown(
2366       event,
2367       aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2368     );
2369     modifiers.normal[i].activated = true;
2370   }
2371   for (let i = 0; i < modifiers.lockable.length; i++) {
2372     if (!aKeyEvent[modifiers.lockable[i].attr]) {
2373       continue;
2374     }
2375     if (aTIP.getModifierState(modifiers.lockable[i].key)) {
2376       continue; // already activated.
2377     }
2378     let event = new KeyboardEvent("", { key: modifiers.lockable[i].key });
2379     aTIP.keydown(
2380       event,
2381       aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2382     );
2383     aTIP.keyup(
2384       event,
2385       aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2386     );
2387     modifiers.lockable[i].activated = true;
2388   }
2389   return modifiers;
2392 function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) {
2393   if (!aModifiers) {
2394     return;
2395   }
2396   var KeyboardEvent = _getKeyboardEvent(aWindow);
2397   for (let i = 0; i < aModifiers.normal.length; i++) {
2398     if (!aModifiers.normal[i].activated) {
2399       continue;
2400     }
2401     let event = new KeyboardEvent("", { key: aModifiers.normal[i].key });
2402     aTIP.keyup(
2403       event,
2404       aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2405     );
2406   }
2407   for (let i = 0; i < aModifiers.lockable.length; i++) {
2408     if (!aModifiers.lockable[i].activated) {
2409       continue;
2410     }
2411     if (!aTIP.getModifierState(aModifiers.lockable[i].key)) {
2412       continue; // who already inactivated this?
2413     }
2414     let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key });
2415     aTIP.keydown(
2416       event,
2417       aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2418     );
2419     aTIP.keyup(
2420       event,
2421       aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
2422     );
2423   }
2427  * Synthesize a composition event and keydown event and keyup events unless
2428  * you prevent to dispatch them explicitly (see aEvent.key's explanation).
2430  * Note that you shouldn't call this with "compositionstart" unless you need to
2431  * test compositionstart event which is NOT followed by compositionupdate
2432  * event immediately.  Typically, native IME starts composition with
2433  * a pair of keydown and keyup event and dispatch compositionstart and
2434  * compositionupdate (and non-standard text event) between them.  So, in most
2435  * cases, you should call synthesizeCompositionChange() directly.
2436  * If you call this with compositionstart, keyup event will be fired
2437  * immediately after compositionstart.  In other words, you should use
2438  * "compositionstart" only when you need to emulate IME which just starts
2439  * composition with compositionstart event but does not send composing text to
2440  * us until committing the composition.  This is behavior of some Chinese IMEs.
2442  * @param aEvent               The composition event information.  This must
2443  *                             have |type| member.  The value must be
2444  *                             "compositionstart", "compositionend",
2445  *                             "compositioncommitasis" or "compositioncommit".
2447  *                             And also this may have |data| and |locale| which
2448  *                             would be used for the value of each property of
2449  *                             the composition event.  Note that the |data| is
2450  *                             ignored if the event type is "compositionstart"
2451  *                             or "compositioncommitasis".
2453  *                             If |key| is undefined, "keydown" and "keyup"
2454  *                             events which are marked as "processed by IME"
2455  *                             are dispatched.  If |key| is not null, "keydown"
2456  *                             and/or "keyup" events are dispatched (if the
2457  *                             |key.type| is specified as "keydown", only
2458  *                             "keydown" event is dispatched).  Otherwise,
2459  *                             i.e., if |key| is null, neither "keydown" nor
2460  *                             "keyup" event is dispatched.
2462  *                             If |key.doNotMarkKeydownAsProcessed| is not true,
2463  *                             key value and keyCode value of "keydown" event
2464  *                             will be set to "Process" and DOM_VK_PROCESSKEY.
2465  *                             If |key.markKeyupAsProcessed| is true,
2466  *                             key value and keyCode value of "keyup" event
2467  *                             will be set to "Process" and DOM_VK_PROCESSKEY.
2468  * @param aWindow              Optional (If null, current |window| will be used)
2469  * @param aCallback            Optional (If non-null, use the callback for
2470  *                             receiving notifications to IME)
2471  */
2472 function synthesizeComposition(aEvent, aWindow = window, aCallback) {
2473   var TIP = _getTIP(aWindow, aCallback);
2474   if (!TIP) {
2475     return;
2476   }
2477   var KeyboardEvent = _getKeyboardEvent(aWindow);
2478   var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
2479   var keyEventDict = { dictionary: null, flags: 0 };
2480   var keyEvent = null;
2481   if (aEvent.key && typeof aEvent.key.key === "string") {
2482     keyEventDict = _createKeyboardEventDictionary(
2483       aEvent.key.key,
2484       aEvent.key,
2485       TIP,
2486       aWindow
2487     );
2488     keyEvent = new KeyboardEvent(
2489       // eslint-disable-next-line no-nested-ternary
2490       aEvent.key.type === "keydown"
2491         ? "keydown"
2492         : aEvent.key.type === "keyup"
2493         ? "keyup"
2494         : "",
2495       keyEventDict.dictionary
2496     );
2497   } else if (aEvent.key === undefined) {
2498     keyEventDict = _createKeyboardEventDictionary(
2499       "KEY_Process",
2500       {},
2501       TIP,
2502       aWindow
2503     );
2504     keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
2505   }
2506   try {
2507     switch (aEvent.type) {
2508       case "compositionstart":
2509         TIP.startComposition(keyEvent, keyEventDict.flags);
2510         break;
2511       case "compositioncommitasis":
2512         TIP.commitComposition(keyEvent, keyEventDict.flags);
2513         break;
2514       case "compositioncommit":
2515         TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags);
2516         break;
2517     }
2518   } finally {
2519     _emulateToInactivateModifiers(TIP, modifiers, aWindow);
2520   }
2523  * Synthesize eCompositionChange event which causes a DOM text event, may
2524  * cause compositionupdate event, and causes keydown event and keyup event
2525  * unless you prevent to dispatch them explicitly (see aEvent.key's
2526  * explanation).
2528  * Note that if you call this when there is no composition, compositionstart
2529  * event will be fired automatically.  This is better than you use
2530  * synthesizeComposition("compositionstart") in most cases.  See the
2531  * explanation of synthesizeComposition().
2533  * @param aEvent   The compositionchange event's information, this has
2534  *                 |composition| and |caret| members.  |composition| has
2535  *                 |string| and |clauses| members.  |clauses| must be array
2536  *                 object.  Each object has |length| and |attr|.  And |caret|
2537  *                 has |start| and |length|.  See the following tree image.
2539  *                 aEvent
2540  *                   +-- composition
2541  *                   |     +-- string
2542  *                   |     +-- clauses[]
2543  *                   |           +-- length
2544  *                   |           +-- attr
2545  *                   +-- caret
2546  *                   |     +-- start
2547  *                   |     +-- length
2548  *                   +-- key
2550  *                 Set the composition string to |composition.string|.  Set its
2551  *                 clauses information to the |clauses| array.
2553  *                 When it's composing, set the each clauses' length to the
2554  *                 |composition.clauses[n].length|.  The sum of the all length
2555  *                 values must be same as the length of |composition.string|.
2556  *                 Set nsICompositionStringSynthesizer.ATTR_* to the
2557  *                 |composition.clauses[n].attr|.
2559  *                 When it's not composing, set 0 to the
2560  *                 |composition.clauses[0].length| and
2561  *                 |composition.clauses[0].attr|.
2563  *                 Set caret position to the |caret.start|. It's offset from
2564  *                 the start of the composition string.  Set caret length to
2565  *                 |caret.length|.  If it's larger than 0, it should be wide
2566  *                 caret.  However, current nsEditor doesn't support wide
2567  *                 caret, therefore, you should always set 0 now.
2569  *                 If |key| is undefined, "keydown" and "keyup" events which
2570  *                 are marked as "processed by IME" are dispatched.  If |key|
2571  *                 is not null, "keydown" and/or "keyup" events are dispatched
2572  *                 (if the |key.type| is specified as "keydown", only "keydown"
2573  *                 event is dispatched).  Otherwise, i.e., if |key| is null,
2574  *                 neither "keydown" nor "keyup" event is dispatched.
2575  *                 If |key.doNotMarkKeydownAsProcessed| is not true, key value
2576  *                 and keyCode value of "keydown" event will be set to
2577  *                 "Process" and DOM_VK_PROCESSKEY.
2578  *                 If |key.markKeyupAsProcessed| is true key value and keyCode
2579  *                 value of "keyup" event will be set to "Process" and
2580  *                 DOM_VK_PROCESSKEY.
2582  * @param aWindow  Optional (If null, current |window| will be used)
2583  * @param aCallback     Optional (If non-null, use the callback for receiving
2584  *                      notifications to IME)
2585  */
2586 function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) {
2587   var TIP = _getTIP(aWindow, aCallback);
2588   if (!TIP) {
2589     return;
2590   }
2591   var KeyboardEvent = _getKeyboardEvent(aWindow);
2593   if (
2594     !aEvent.composition ||
2595     !aEvent.composition.clauses ||
2596     !aEvent.composition.clauses[0]
2597   ) {
2598     return;
2599   }
2601   TIP.setPendingCompositionString(aEvent.composition.string);
2602   if (aEvent.composition.clauses[0].length) {
2603     for (var i = 0; i < aEvent.composition.clauses.length; i++) {
2604       switch (aEvent.composition.clauses[i].attr) {
2605         case TIP.ATTR_RAW_CLAUSE:
2606         case TIP.ATTR_SELECTED_RAW_CLAUSE:
2607         case TIP.ATTR_CONVERTED_CLAUSE:
2608         case TIP.ATTR_SELECTED_CLAUSE:
2609           TIP.appendClauseToPendingComposition(
2610             aEvent.composition.clauses[i].length,
2611             aEvent.composition.clauses[i].attr
2612           );
2613           break;
2614         case 0:
2615           // Ignore dummy clause for the argument.
2616           break;
2617         default:
2618           throw new Error("invalid clause attribute specified");
2619       }
2620     }
2621   }
2623   if (aEvent.caret) {
2624     TIP.setCaretInPendingComposition(aEvent.caret.start);
2625   }
2627   var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
2628   try {
2629     var keyEventDict = { dictionary: null, flags: 0 };
2630     var keyEvent = null;
2631     if (aEvent.key && typeof aEvent.key.key === "string") {
2632       keyEventDict = _createKeyboardEventDictionary(
2633         aEvent.key.key,
2634         aEvent.key,
2635         TIP,
2636         aWindow
2637       );
2638       keyEvent = new KeyboardEvent(
2639         // eslint-disable-next-line no-nested-ternary
2640         aEvent.key.type === "keydown"
2641           ? "keydown"
2642           : aEvent.key.type === "keyup"
2643           ? "keyup"
2644           : "",
2645         keyEventDict.dictionary
2646       );
2647     } else if (aEvent.key === undefined) {
2648       keyEventDict = _createKeyboardEventDictionary(
2649         "KEY_Process",
2650         {},
2651         TIP,
2652         aWindow
2653       );
2654       keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
2655     }
2656     TIP.flushPendingComposition(keyEvent, keyEventDict.flags);
2657   } finally {
2658     _emulateToInactivateModifiers(TIP, modifiers, aWindow);
2659   }
2662 // Must be synchronized with nsIDOMWindowUtils.
2663 const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
2664 const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001;
2666 const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000;
2667 const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002;
2668 const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004;
2669 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008;
2670 const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010;
2671 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020;
2672 const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040;
2673 const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080;
2674 const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100;
2675 const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200;
2677 const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400;
2679 const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
2680 const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001;
2681 const SELECTION_SET_FLAG_REVERSE = 0x0002;
2684  * Synthesize a query text content event.
2686  * @param aOffset  The character offset.  0 means the first character in the
2687  *                 selection root.
2688  * @param aLength  The length of getting text.  If the length is too long,
2689  *                 the extra length is ignored.
2690  * @param aIsRelative   Optional (If true, aOffset is relative to start of
2691  *                      composition if there is, or start of selection.)
2692  * @param aWindow  Optional (If null, current |window| will be used)
2693  * @return         An nsIQueryContentEventResult object.  If this failed,
2694  *                 the result might be null.
2695  */
2696 function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) {
2697   var utils = _getDOMWindowUtils(aWindow);
2698   if (!utils) {
2699     return null;
2700   }
2701   var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
2702   if (aIsRelative === true) {
2703     flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
2704   }
2705   return utils.sendQueryContentEvent(
2706     utils.QUERY_TEXT_CONTENT,
2707     aOffset,
2708     aLength,
2709     0,
2710     0,
2711     flags
2712   );
2716  * Synthesize a query selected text event.
2718  * @param aSelectionType    Optional, one of QUERY_CONTENT_FLAG_SELECTION_*.
2719  *                          If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will
2720  *                          be used.
2721  * @param aWindow  Optional (If null, current |window| will be used)
2722  * @return         An nsIQueryContentEventResult object.  If this failed,
2723  *                 the result might be null.
2724  */
2725 function synthesizeQuerySelectedText(aSelectionType, aWindow) {
2726   var utils = _getDOMWindowUtils(aWindow);
2727   var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
2728   if (aSelectionType) {
2729     flags |= aSelectionType;
2730   }
2732   return utils.sendQueryContentEvent(
2733     utils.QUERY_SELECTED_TEXT,
2734     0,
2735     0,
2736     0,
2737     0,
2738     flags
2739   );
2743  * Synthesize a query caret rect event.
2745  * @param aOffset  The caret offset.  0 means left side of the first character
2746  *                 in the selection root.
2747  * @param aWindow  Optional (If null, current |window| will be used)
2748  * @return         An nsIQueryContentEventResult object.  If this failed,
2749  *                 the result might be null.
2750  */
2751 function synthesizeQueryCaretRect(aOffset, aWindow) {
2752   var utils = _getDOMWindowUtils(aWindow);
2753   if (!utils) {
2754     return null;
2755   }
2756   return utils.sendQueryContentEvent(
2757     utils.QUERY_CARET_RECT,
2758     aOffset,
2759     0,
2760     0,
2761     0,
2762     QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2763   );
2767  * Synthesize a selection set event.
2769  * @param aOffset  The character offset.  0 means the first character in the
2770  *                 selection root.
2771  * @param aLength  The length of the text.  If the length is too long,
2772  *                 the extra length is ignored.
2773  * @param aReverse If true, the selection is from |aOffset + aLength| to
2774  *                 |aOffset|.  Otherwise, from |aOffset| to |aOffset + aLength|.
2775  * @param aWindow  Optional (If null, current |window| will be used)
2776  * @return         True, if succeeded.  Otherwise false.
2777  */
2778 async function synthesizeSelectionSet(
2779   aOffset,
2780   aLength,
2781   aReverse,
2782   aWindow = window
2783 ) {
2784   const utils = _getDOMWindowUtils(aWindow);
2785   if (!utils) {
2786     return false;
2787   }
2788   // eSetSelection event will be compared with selection cache in
2789   // IMEContentObserver, but it may have not been updated yet.  Therefore, we
2790   // need to flush pending things of IMEContentObserver.
2791   await new Promise(resolve =>
2792     aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve))
2793   );
2794   const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0;
2795   return utils.sendSelectionSetEvent(aOffset, aLength, flags);
2799  * Synthesize a query text rect event.
2801  * @param aOffset  The character offset.  0 means the first character in the
2802  *                 selection root.
2803  * @param aLength  The length of the text.  If the length is too long,
2804  *                 the extra length is ignored.
2805  * @param aIsRelative   Optional (If true, aOffset is relative to start of
2806  *                      composition if there is, or start of selection.)
2807  * @param aWindow  Optional (If null, current |window| will be used)
2808  * @return         An nsIQueryContentEventResult object.  If this failed,
2809  *                 the result might be null.
2810  */
2811 function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) {
2812   if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") {
2813     throw new Error(
2814       "Maybe, you set Window object to the 3rd argument, but it should be a boolean value"
2815     );
2816   }
2817   var utils = _getDOMWindowUtils(aWindow);
2818   let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
2819   if (aIsRelative === true) {
2820     flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
2821   }
2822   return utils.sendQueryContentEvent(
2823     utils.QUERY_TEXT_RECT,
2824     aOffset,
2825     aLength,
2826     0,
2827     0,
2828     flags
2829   );
2833  * Synthesize a query text rect array event.
2835  * @param aOffset  The character offset.  0 means the first character in the
2836  *                 selection root.
2837  * @param aLength  The length of the text.  If the length is too long,
2838  *                 the extra length is ignored.
2839  * @param aWindow  Optional (If null, current |window| will be used)
2840  * @return         An nsIQueryContentEventResult object.  If this failed,
2841  *                 the result might be null.
2842  */
2843 function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) {
2844   var utils = _getDOMWindowUtils(aWindow);
2845   return utils.sendQueryContentEvent(
2846     utils.QUERY_TEXT_RECT_ARRAY,
2847     aOffset,
2848     aLength,
2849     0,
2850     0,
2851     QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2852   );
2856  * Synthesize a query editor rect event.
2858  * @param aWindow  Optional (If null, current |window| will be used)
2859  * @return         An nsIQueryContentEventResult object.  If this failed,
2860  *                 the result might be null.
2861  */
2862 function synthesizeQueryEditorRect(aWindow) {
2863   var utils = _getDOMWindowUtils(aWindow);
2864   return utils.sendQueryContentEvent(
2865     utils.QUERY_EDITOR_RECT,
2866     0,
2867     0,
2868     0,
2869     0,
2870     QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2871   );
2875  * Synthesize a character at point event.
2877  * @param aX, aY   The offset in the client area of the DOM window.
2878  * @param aWindow  Optional (If null, current |window| will be used)
2879  * @return         An nsIQueryContentEventResult object.  If this failed,
2880  *                 the result might be null.
2881  */
2882 function synthesizeCharAtPoint(aX, aY, aWindow) {
2883   var utils = _getDOMWindowUtils(aWindow);
2884   return utils.sendQueryContentEvent(
2885     utils.QUERY_CHARACTER_AT_POINT,
2886     0,
2887     0,
2888     aX,
2889     aY,
2890     QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
2891   );
2895  * INTERNAL USE ONLY
2896  * Create an event object to pass to sendDragEvent.
2898  * @param aType          The string represents drag event type.
2899  * @param aDestElement   The element to fire the drag event, used to calculate
2900  *                       screenX/Y and clientX/Y.
2901  * @param aDestWindow    Optional; Defaults to the current window object.
2902  * @param aDataTransfer  dataTransfer for current drag session.
2903  * @param aDragEvent     The object contains properties to override the event
2904  *                       object
2905  * @return               An object to pass to sendDragEvent.
2906  */
2907 function createDragEventObject(
2908   aType,
2909   aDestElement,
2910   aDestWindow,
2911   aDataTransfer,
2912   aDragEvent
2913 ) {
2914   var destRect = aDestElement.getBoundingClientRect();
2915   var destClientX = destRect.left + destRect.width / 2;
2916   var destClientY = destRect.top + destRect.height / 2;
2917   var destScreenX = aDestWindow.mozInnerScreenX + destClientX;
2918   var destScreenY = aDestWindow.mozInnerScreenY + destClientY;
2919   if ("clientX" in aDragEvent && !("screenX" in aDragEvent)) {
2920     aDragEvent.screenX = aDestWindow.mozInnerScreenX + aDragEvent.clientX;
2921   }
2922   if ("clientY" in aDragEvent && !("screenY" in aDragEvent)) {
2923     aDragEvent.screenY = aDestWindow.mozInnerScreenY + aDragEvent.clientY;
2924   }
2926   // Wrap only in plain mochitests
2927   let dataTransfer;
2928   if (aDataTransfer) {
2929     dataTransfer = _EU_maybeUnwrap(
2930       _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType)
2931     );
2933     // Copy over the drop effect. This isn't copied over by Clone, as it uses
2934     // more complex logic in the actual implementation (see
2935     // nsContentUtils::SetDataTransferInEvent for actual impl).
2936     dataTransfer.dropEffect = aDataTransfer.dropEffect;
2937   }
2939   return Object.assign(
2940     {
2941       type: aType,
2942       screenX: destScreenX,
2943       screenY: destScreenY,
2944       clientX: destClientX,
2945       clientY: destClientY,
2946       dataTransfer,
2947       _domDispatchOnly: aDragEvent._domDispatchOnly,
2948     },
2949     aDragEvent
2950   );
2954  * Emulate a event sequence of dragstart, dragenter, and dragover.
2956  * @param {Element} aSrcElement
2957  *        The element to use to start the drag.
2958  * @param {Element} aDestElement
2959  *        The element to fire the dragover, dragenter events
2960  * @param {Array}   aDragData
2961  *        The data to supply for the data transfer.
2962  *        This data is in the format:
2964  *        [
2965  *          [
2966  *            {"type": value, "data": value },
2967  *            ...,
2968  *          ],
2969  *          ...
2970  *        ]
2972  *        Pass null to avoid modifying dataTransfer.
2973  * @param {String} [aDropEffect="move"]
2974  *        The drop effect to set during the dragstart event, or 'move' if omitted.
2975  * @param {Window} [aWindow=window]
2976  *        The window in which the drag happens. Defaults to the window in which
2977  *        EventUtils.js is loaded.
2978  * @param {Window} [aDestWindow=aWindow]
2979  *        Used when aDestElement is in a different window than aSrcElement.
2980  *        Default is to match ``aWindow``.
2981  * @param {Object} [aDragEvent={}]
2982  *        Defaults to empty object. Overwrites an object passed to sendDragEvent.
2983  * @return {Array}
2984  *        A two element array, where the first element is the value returned
2985  *        from sendDragEvent for dragover event, and the second element is the
2986  *        dataTransfer for the current drag session.
2987  */
2988 function synthesizeDragOver(
2989   aSrcElement,
2990   aDestElement,
2991   aDragData,
2992   aDropEffect,
2993   aWindow,
2994   aDestWindow,
2995   aDragEvent = {}
2996 ) {
2997   if (!aWindow) {
2998     aWindow = window;
2999   }
3000   if (!aDestWindow) {
3001     aDestWindow = aWindow;
3002   }
3004   // eslint-disable-next-line mozilla/use-services
3005   const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService(
3006     _EU_Ci.nsIObserverService
3007   );
3008   const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
3009     _EU_Ci.nsIDragService
3010   );
3011   var sess = ds.getCurrentSession();
3013   // This method runs before other callbacks, and acts as a way to inject the
3014   // initial drag data into the DataTransfer.
3015   function fillDrag(event) {
3016     if (aDragData) {
3017       for (var i = 0; i < aDragData.length; i++) {
3018         var item = aDragData[i];
3019         for (var j = 0; j < item.length; j++) {
3020           _EU_maybeWrap(event.dataTransfer).mozSetDataAt(
3021             item[j].type,
3022             item[j].data,
3023             i
3024           );
3025         }
3026       }
3027     }
3028     event.dataTransfer.dropEffect = aDropEffect || "move";
3029     event.preventDefault();
3030   }
3032   function trapDrag(subject, topic) {
3033     if (topic == "on-datatransfer-available") {
3034       sess.dataTransfer = _EU_maybeUnwrap(
3035         _EU_maybeWrap(subject).mozCloneForEvent("drop")
3036       );
3037       sess.dataTransfer.dropEffect = subject.dropEffect;
3038     }
3039   }
3041   // need to use real mouse action
3042   aWindow.addEventListener("dragstart", fillDrag, true);
3043   obs.addObserver(trapDrag, "on-datatransfer-available");
3044   synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow);
3046   var rect = aSrcElement.getBoundingClientRect();
3047   var x = rect.width / 2;
3048   var y = rect.height / 2;
3049   synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow);
3050   synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow);
3051   aWindow.removeEventListener("dragstart", fillDrag, true);
3052   obs.removeObserver(trapDrag, "on-datatransfer-available");
3054   var dataTransfer = sess.dataTransfer;
3055   if (!dataTransfer) {
3056     throw new Error("No data transfer object after synthesizing the mouse!");
3057   }
3059   // The EventStateManager will fire our dragenter event if it needs to.
3060   var event = createDragEventObject(
3061     "dragover",
3062     aDestElement,
3063     aDestWindow,
3064     dataTransfer,
3065     aDragEvent
3066   );
3067   var result = sendDragEvent(event, aDestElement, aDestWindow);
3069   return [result, dataTransfer];
3073  * Emulate the drop event and mouseup event.
3074  * This should be called after synthesizeDragOver.
3076  * @param {*} aResult
3077  *        The first element of the array returned from ``synthesizeDragOver``.
3078  * @param {DataTransfer} aDataTransfer
3079  *        The second element of the array returned from ``synthesizeDragOver``.
3080  * @param {Element} aDestElement
3081  *        The element on which to fire the drop event.
3082  * @param {Window} [aDestWindow=window]
3083  *        The window in which the drop happens. Defaults to the window in which
3084  *        EventUtils.js is loaded.
3085  * @param {Object} [aDragEvent={}]
3086  *        Defaults to empty object. Overwrites an object passed to sendDragEvent.
3087  * @return {String}
3088  *        "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise.
3089  */
3090 function synthesizeDropAfterDragOver(
3091   aResult,
3092   aDataTransfer,
3093   aDestElement,
3094   aDestWindow,
3095   aDragEvent = {}
3096 ) {
3097   if (!aDestWindow) {
3098     aDestWindow = window;
3099   }
3101   var effect = aDataTransfer.dropEffect;
3102   var event;
3104   if (aResult) {
3105     effect = "none";
3106   } else if (effect != "none") {
3107     event = createDragEventObject(
3108       "drop",
3109       aDestElement,
3110       aDestWindow,
3111       aDataTransfer,
3112       aDragEvent
3113     );
3114     sendDragEvent(event, aDestElement, aDestWindow);
3115   }
3116   // Don't run accessibility checks for this click, since we're not actually
3117   // clicking. It's just generated as part of the drop.
3118   // this.AccessibilityUtils might not be set if this isn't a browser test or
3119   // if a browser test has loaded its own copy of EventUtils for some reason.
3120   // In the latter case, the test probably shouldn't do that.
3121   this.AccessibilityUtils?.suppressClickHandling(true);
3122   synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow);
3123   this.AccessibilityUtils?.suppressClickHandling(false);
3125   return effect;
3129  * Emulate a drag and drop by emulating a dragstart and firing events dragenter,
3130  * dragover, and drop.
3132  * @param {Element} aSrcElement
3133  *        The element to use to start the drag.
3134  * @param {Element} aDestElement
3135  *        The element to fire the dragover, dragenter events
3136  * @param {Array}   aDragData
3137  *        The data to supply for the data transfer.
3138  *        This data is in the format:
3140  *            [
3141  *              [
3142  *                {"type": value, "data": value },
3143  *                ...,
3144  *              ],
3145  *              ...
3146  *            ]
3148  *        Pass null to avoid modifying dataTransfer.
3149  * @param {String} [aDropEffect="move"]
3150  *        The drop effect to set during the dragstart event, or 'move' if omitted..
3151  * @param {Window} [aWindow=window]
3152  *        The window in which the drag happens. Defaults to the window in which
3153  *        EventUtils.js is loaded.
3154  * @param {Window} [aDestWindow=aWindow]
3155  *        Used when aDestElement is in a different window than aSrcElement.
3156  *        Default is to match ``aWindow``.
3157  * @param {Object} [aDragEvent={}]
3158  *        Defaults to empty object. Overwrites an object passed to sendDragEvent.
3159  * @return {String}
3160  *        The drop effect that was desired.
3161  */
3162 function synthesizeDrop(
3163   aSrcElement,
3164   aDestElement,
3165   aDragData,
3166   aDropEffect,
3167   aWindow,
3168   aDestWindow,
3169   aDragEvent = {}
3170 ) {
3171   if (!aWindow) {
3172     aWindow = window;
3173   }
3174   if (!aDestWindow) {
3175     aDestWindow = aWindow;
3176   }
3178   var ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
3179     _EU_Ci.nsIDragService
3180   );
3182   let dropAction;
3183   switch (aDropEffect) {
3184     case null:
3185     case undefined:
3186     case "move":
3187       dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
3188       break;
3189     case "copy":
3190       dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY;
3191       break;
3192     case "link":
3193       dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK;
3194       break;
3195     default:
3196       throw new Error(`${aDropEffect} is an invalid drop effect value`);
3197   }
3199   ds.startDragSessionForTests(dropAction);
3201   try {
3202     var [result, dataTransfer] = synthesizeDragOver(
3203       aSrcElement,
3204       aDestElement,
3205       aDragData,
3206       aDropEffect,
3207       aWindow,
3208       aDestWindow,
3209       aDragEvent
3210     );
3211     return synthesizeDropAfterDragOver(
3212       result,
3213       dataTransfer,
3214       aDestElement,
3215       aDestWindow,
3216       aDragEvent
3217     );
3218   } finally {
3219     ds.endDragSession(true, _parseModifiers(aDragEvent));
3220   }
3223 function _getFlattenedTreeParentNode(aNode) {
3224   return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode);
3227 function _getInclusiveFlattenedTreeParentElement(aNode) {
3228   for (
3229     let inclusiveAncestor = aNode;
3230     inclusiveAncestor;
3231     inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor)
3232   ) {
3233     if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) {
3234       return inclusiveAncestor;
3235     }
3236   }
3237   return null;
3240 function _nodeIsFlattenedTreeDescendantOf(
3241   aPossibleDescendant,
3242   aPossibleAncestor
3243 ) {
3244   do {
3245     if (aPossibleDescendant == aPossibleAncestor) {
3246       return true;
3247     }
3248     aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant);
3249   } while (aPossibleDescendant);
3250   return false;
3253 function _computeSrcElementFromSrcSelection(aSrcSelection) {
3254   let srcElement = aSrcSelection.focusNode;
3255   while (_EU_maybeWrap(srcElement).isNativeAnonymous) {
3256     srcElement = _getFlattenedTreeParentNode(srcElement);
3257   }
3258   if (srcElement.nodeType !== Node.ELEMENT_NODE) {
3259     srcElement = _getInclusiveFlattenedTreeParentElement(srcElement);
3260   }
3261   return srcElement;
3265  * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove,
3266  * and firing events dragenter, dragover, drop, and dragend.
3267  * This does not modify dataTransfer and tries to emulate the plain drag and
3268  * drop as much as possible, compared to synthesizeDrop.
3269  * Note that if synthesized dragstart is canceled, this throws an exception
3270  * because in such case, Gecko does not start drag session.
3272  * @param {Object} aParams
3273  * @param {Event} aParams.dragEvent
3274  *                The DnD events will be generated with modifiers specified with this.
3275  * @param {Element} aParams.srcElement
3276  *                The element to start dragging.  If srcSelection is
3277  *                set, this is computed for element at focus node.
3278  * @param {Selection|nil} aParams.srcSelection
3279  *                The selection to start to drag, set null if srcElement is set.
3280  * @param {Element|nil} aParams.destElement
3281  *                The element to drop on. Pass null to emulate a drop on an invalid target.
3282  * @param {Number} aParams.srcX
3283  *                The initial x coordinate inside srcElement or ignored if srcSelection is set.
3284  * @param {Number} aParams.srcY
3285  *                The initial y coordinate inside srcElement or ignored if srcSelection is set.
3286  * @param {Number} aParams.stepX
3287  *                The x-axis step for mousemove inside srcElement
3288  * @param {Number} aParams.stepY
3289  *                The y-axis step for mousemove inside srcElement
3290  * @param {Number} aParams.finalX
3291  *                The final x coordinate inside srcElement
3292  * @param {Number} aParams.finalY
3293  *                The final x coordinate inside srcElement
3294  * @param {Any} aParams.id
3295  *                The pointer event id
3296  * @param {Window} aParams.srcWindow
3297  *                The window for dispatching event on srcElement, defaults to the current window object.
3298  * @param {Window} aParams.destWindow
3299  *                The window for dispatching event on destElement, defaults to the current window object.
3300  * @param {Boolean} aParams.expectCancelDragStart
3301  *                Set to true if the test cancels "dragstart"
3302  * @param {Boolean} aParams.expectSrcElementDisconnected
3303  *                Set to true if srcElement will be disconnected and
3304  *                "dragend" event won't be fired.
3305  * @param {Function} aParams.logFunc
3306  *                Set function which takes one argument if you need to log rect of target.  E.g., `console.log`.
3307  */
3308 // eslint-disable-next-line complexity
3309 async function synthesizePlainDragAndDrop(aParams) {
3310   let {
3311     dragEvent = {},
3312     srcElement,
3313     srcSelection,
3314     destElement,
3315     srcX = 2,
3316     srcY = 2,
3317     stepX = 9,
3318     stepY = 9,
3319     finalX = srcX + stepX * 2,
3320     finalY = srcY + stepY * 2,
3321     id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID,
3322     srcWindow = window,
3323     destWindow = window,
3324     expectCancelDragStart = false,
3325     expectSrcElementDisconnected = false,
3326     logFunc,
3327   } = aParams;
3328   // Don't modify given dragEvent object because we modify dragEvent below and
3329   // callers may use the object multiple times so that callers must not assume
3330   // that it'll be modified.
3331   if (aParams.dragEvent !== undefined) {
3332     dragEvent = Object.assign({}, aParams.dragEvent);
3333   }
3335   function rectToString(aRect) {
3336     return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`;
3337   }
3339   if (logFunc) {
3340     logFunc("synthesizePlainDragAndDrop() -- START");
3341   }
3343   if (srcSelection) {
3344     srcElement = _computeSrcElementFromSrcSelection(srcSelection);
3345     let srcElementRect = srcElement.getBoundingClientRect();
3346     if (logFunc) {
3347       logFunc(
3348         `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}`
3349       );
3350     }
3351     // Use last selection client rect because nsIDragSession.sourceNode is
3352     // initialized from focus node which is usually in last rect.
3353     let selectionRectList = srcSelection.getRangeAt(0).getClientRects();
3354     let lastSelectionRect = selectionRectList[selectionRectList.length - 1];
3355     if (logFunc) {
3356       logFunc(
3357         `srcSelection.getRangeAt(0).getClientRects()[${
3358           selectionRectList.length - 1
3359         }]: ${rectToString(lastSelectionRect)}`
3360       );
3361     }
3362     // Click at center of last selection rect.
3363     srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2);
3364     srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2);
3365     // Then, adjust srcX and srcY for making them offset relative to
3366     // srcElementRect because they will be used when we call synthesizeMouse()
3367     // with srcElement.
3368     srcX = Math.floor(srcX - srcElementRect.left);
3369     srcY = Math.floor(srcY - srcElementRect.top);
3370     // Finally, recalculate finalX and finalY with new srcX and srcY if they
3371     // are not specified by the caller.
3372     if (aParams.finalX === undefined) {
3373       finalX = srcX + stepX * 2;
3374     }
3375     if (aParams.finalY === undefined) {
3376       finalY = srcY + stepY * 2;
3377     }
3378   } else if (logFunc) {
3379     logFunc(
3380       `srcElement.getBoundingClientRect(): ${rectToString(
3381         srcElement.getBoundingClientRect()
3382       )}`
3383     );
3384   }
3386   const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
3387     _EU_Ci.nsIDragService
3388   );
3390   const editingHost = (() => {
3391     if (!srcElement.matches(":read-write")) {
3392       return null;
3393     }
3394     let lastEditableElement = srcElement;
3395     for (
3396       let inclusiveAncestor =
3397         _getInclusiveFlattenedTreeParentElement(srcElement);
3398       inclusiveAncestor;
3399       inclusiveAncestor = _getInclusiveFlattenedTreeParentElement(
3400         _getFlattenedTreeParentNode(inclusiveAncestor)
3401       )
3402     ) {
3403       if (inclusiveAncestor.matches(":read-write")) {
3404         lastEditableElement = inclusiveAncestor;
3405         if (lastEditableElement == srcElement.ownerDocument.body) {
3406           break;
3407         }
3408       }
3409     }
3410     return lastEditableElement;
3411   })();
3412   try {
3413     _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(true);
3415     await new Promise(r => setTimeout(r, 0));
3417     let mouseDownEvent;
3418     function onMouseDown(aEvent) {
3419       mouseDownEvent = aEvent;
3420       if (logFunc) {
3421         logFunc(
3422           `"${aEvent.type}" event is fired on ${
3423             aEvent.target
3424           } (composedTarget: ${_EU_maybeUnwrap(
3425             _EU_maybeWrap(aEvent).composedTarget
3426           )}`
3427         );
3428       }
3429       if (
3430         !_nodeIsFlattenedTreeDescendantOf(
3431           _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3432           srcElement
3433         )
3434       ) {
3435         // If srcX and srcY does not point in one of rects in srcElement,
3436         // "mousedown" target is not in srcElement.  Such case must not
3437         // be expected by this API users so that we should throw an exception
3438         // for making debugging easier.
3439         throw new Error(
3440           'event target of "mousedown" is not srcElement nor its descendant'
3441         );
3442       }
3443     }
3444     try {
3445       srcWindow.addEventListener("mousedown", onMouseDown, { capture: true });
3446       synthesizeMouse(
3447         srcElement,
3448         srcX,
3449         srcY,
3450         { type: "mousedown", id },
3451         srcWindow
3452       );
3453       if (logFunc) {
3454         logFunc(`mousedown at ${srcX}, ${srcY}`);
3455       }
3456       if (!mouseDownEvent) {
3457         throw new Error('"mousedown" event is not fired');
3458       }
3459     } finally {
3460       srcWindow.removeEventListener("mousedown", onMouseDown, {
3461         capture: true,
3462       });
3463     }
3465     let dragStartEvent;
3466     function onDragStart(aEvent) {
3467       dragStartEvent = aEvent;
3468       if (logFunc) {
3469         logFunc(`"${aEvent.type}" event is fired`);
3470       }
3471       if (
3472         !_nodeIsFlattenedTreeDescendantOf(
3473           _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3474           srcElement
3475         )
3476       ) {
3477         // If srcX and srcY does not point in one of rects in srcElement,
3478         // "dragstart" target is not in srcElement.  Such case must not
3479         // be expected by this API users so that we should throw an exception
3480         // for making debugging easier.
3481         throw new Error(
3482           'event target of "dragstart" is not srcElement nor its descendant'
3483         );
3484       }
3485     }
3486     let dragEnterEvent;
3487     function onDragEnterGenerated(aEvent) {
3488       dragEnterEvent = aEvent;
3489     }
3490     srcWindow.addEventListener("dragstart", onDragStart, { capture: true });
3491     srcWindow.addEventListener("dragenter", onDragEnterGenerated, {
3492       capture: true,
3493     });
3494     try {
3495       // Wait for the next event tick after each event dispatch, so that UI
3496       // elements (e.g. menu) work like the real user input.
3497       await new Promise(r => setTimeout(r, 0));
3499       srcX += stepX;
3500       srcY += stepY;
3501       synthesizeMouse(
3502         srcElement,
3503         srcX,
3504         srcY,
3505         { type: "mousemove", id },
3506         srcWindow
3507       );
3508       if (logFunc) {
3509         logFunc(`first mousemove at ${srcX}, ${srcY}`);
3510       }
3512       await new Promise(r => setTimeout(r, 0));
3514       srcX += stepX;
3515       srcY += stepY;
3516       synthesizeMouse(
3517         srcElement,
3518         srcX,
3519         srcY,
3520         { type: "mousemove", id },
3521         srcWindow
3522       );
3523       if (logFunc) {
3524         logFunc(`second mousemove at ${srcX}, ${srcY}`);
3525       }
3527       await new Promise(r => setTimeout(r, 0));
3529       if (!dragStartEvent) {
3530         throw new Error('"dragstart" event is not fired');
3531       }
3532     } finally {
3533       srcWindow.removeEventListener("dragstart", onDragStart, {
3534         capture: true,
3535       });
3536       srcWindow.removeEventListener("dragenter", onDragEnterGenerated, {
3537         capture: true,
3538       });
3539     }
3541     let session = ds.getCurrentSession();
3542     if (!session) {
3543       if (expectCancelDragStart) {
3544         synthesizeMouse(
3545           srcElement,
3546           finalX,
3547           finalY,
3548           { type: "mouseup", id },
3549           srcWindow
3550         );
3551         return;
3552       }
3553       throw new Error("drag hasn't been started by the operation");
3554     } else if (expectCancelDragStart) {
3555       throw new Error("drag has been started by the operation");
3556     }
3558     if (destElement) {
3559       if (
3560         (srcElement != destElement && !dragEnterEvent) ||
3561         destElement != dragEnterEvent.target
3562       ) {
3563         if (logFunc) {
3564           logFunc(
3565             `destElement.getBoundingClientRect(): ${rectToString(
3566               destElement.getBoundingClientRect()
3567             )}`
3568           );
3569         }
3571         function onDragEnter(aEvent) {
3572           dragEnterEvent = aEvent;
3573           if (logFunc) {
3574             logFunc(`"${aEvent.type}" event is fired`);
3575           }
3576           if (aEvent.target != destElement) {
3577             throw new Error('event target of "dragenter" is not destElement');
3578           }
3579         }
3580         destWindow.addEventListener("dragenter", onDragEnter, {
3581           capture: true,
3582         });
3583         try {
3584           let event = createDragEventObject(
3585             "dragenter",
3586             destElement,
3587             destWindow,
3588             null,
3589             dragEvent
3590           );
3591           sendDragEvent(event, destElement, destWindow);
3592           if (!dragEnterEvent && !destElement.disabled) {
3593             throw new Error('"dragenter" event is not fired');
3594           }
3595           if (dragEnterEvent && destElement.disabled) {
3596             throw new Error(
3597               '"dragenter" event should not be fired on disable element'
3598             );
3599           }
3600         } finally {
3601           destWindow.removeEventListener("dragenter", onDragEnter, {
3602             capture: true,
3603           });
3604         }
3605       }
3607       let dragOverEvent;
3608       function onDragOver(aEvent) {
3609         dragOverEvent = aEvent;
3610         if (logFunc) {
3611           logFunc(`"${aEvent.type}" event is fired`);
3612         }
3613         if (aEvent.target != destElement) {
3614           throw new Error('event target of "dragover" is not destElement');
3615         }
3616       }
3617       destWindow.addEventListener("dragover", onDragOver, { capture: true });
3618       try {
3619         // dragover and drop are only fired to a valid drop target. If the
3620         // destElement parameter is null, this function is being used to
3621         // simulate a drag'n'drop over an invalid drop target.
3622         let event = createDragEventObject(
3623           "dragover",
3624           destElement,
3625           destWindow,
3626           null,
3627           dragEvent
3628         );
3629         sendDragEvent(event, destElement, destWindow);
3630         if (!dragOverEvent && !destElement.disabled) {
3631           throw new Error('"dragover" event is not fired');
3632         }
3633         if (dragEnterEvent && destElement.disabled) {
3634           throw new Error(
3635             '"dragover" event should not be fired on disable element'
3636           );
3637         }
3638       } finally {
3639         destWindow.removeEventListener("dragover", onDragOver, {
3640           capture: true,
3641         });
3642       }
3644       await new Promise(r => setTimeout(r, 0));
3646       // If there is not accept to drop the data, "drop" event shouldn't be
3647       // fired.
3648       // XXX nsIDragSession.canDrop is different only on Linux.  It must be
3649       //     a bug of gtk/nsDragService since it manages `mCanDrop` by itself.
3650       //     Thus, we should use nsIDragSession.dragAction instead.
3651       if (session.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE) {
3652         let dropEvent;
3653         function onDrop(aEvent) {
3654           dropEvent = aEvent;
3655           if (logFunc) {
3656             logFunc(`"${aEvent.type}" event is fired`);
3657           }
3658           if (
3659             !_nodeIsFlattenedTreeDescendantOf(
3660               _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3661               destElement
3662             )
3663           ) {
3664             throw new Error(
3665               'event target of "drop" is not destElement nor its descendant'
3666             );
3667           }
3668         }
3669         destWindow.addEventListener("drop", onDrop, { capture: true });
3670         try {
3671           let event = createDragEventObject(
3672             "drop",
3673             destElement,
3674             destWindow,
3675             null,
3676             dragEvent
3677           );
3678           sendDragEvent(event, destElement, destWindow);
3679           if (!dropEvent && session.canDrop) {
3680             throw new Error('"drop" event is not fired');
3681           }
3682         } finally {
3683           destWindow.removeEventListener("drop", onDrop, { capture: true });
3684         }
3685         return;
3686       }
3687     }
3689     // Since we don't synthesize drop event, we need to set drag end point
3690     // explicitly for "dragEnd" event which will be fired by
3691     // endDragSession().
3692     dragEvent.clientX = finalX;
3693     dragEvent.clientY = finalY;
3694     let event = createDragEventObject(
3695       "dragend",
3696       destElement || srcElement,
3697       destElement ? srcWindow : destWindow,
3698       null,
3699       dragEvent
3700     );
3701     session.setDragEndPointForTests(event.screenX, event.screenY);
3702   } finally {
3703     await new Promise(r => setTimeout(r, 0));
3705     if (ds.getCurrentSession()) {
3706       const sourceNode = ds.sourceNode;
3707       let dragEndEvent;
3708       function onDragEnd(aEvent) {
3709         dragEndEvent = aEvent;
3710         if (logFunc) {
3711           logFunc(`"${aEvent.type}" event is fired`);
3712         }
3713         if (
3714           !_nodeIsFlattenedTreeDescendantOf(
3715             _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
3716             srcElement
3717           ) &&
3718           _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost
3719         ) {
3720           throw new Error(
3721             'event target of "dragend" is not srcElement nor its descendant'
3722           );
3723         }
3724         if (expectSrcElementDisconnected) {
3725           throw new Error(
3726             `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${
3727               sourceNode?.isConnected ? "connected" : "null or disconnected"
3728             })`
3729           );
3730         }
3731       }
3732       srcWindow.addEventListener("dragend", onDragEnd, { capture: true });
3733       try {
3734         ds.endDragSession(true, _parseModifiers(dragEvent));
3735         if (!expectSrcElementDisconnected && !dragEndEvent) {
3736           // eslint-disable-next-line no-unsafe-finally
3737           throw new Error(
3738             `"dragend" event is not fired by nsIDragService.endDragSession()${
3739               ds.sourceNode && !ds.sourceNode.isConnected
3740                 ? "(sourceNode was disconnected)"
3741                 : ""
3742             }`
3743           );
3744         }
3745       } finally {
3746         srcWindow.removeEventListener("dragend", onDragEnd, { capture: true });
3747       }
3748     }
3749     _getDOMWindowUtils(srcWindow).disableNonTestMouseEvents(false);
3750     if (logFunc) {
3751       logFunc("synthesizePlainDragAndDrop() -- END");
3752     }
3753   }
3756 function _checkDataTransferItems(aDataTransfer, aExpectedDragData) {
3757   try {
3758     // We must wrap only in plain mochitests, not chrome
3759     let dataTransfer = _EU_maybeWrap(aDataTransfer);
3760     if (!dataTransfer) {
3761       return null;
3762     }
3763     if (
3764       aExpectedDragData == null ||
3765       dataTransfer.mozItemCount != aExpectedDragData.length
3766     ) {
3767       return dataTransfer;
3768     }
3769     for (let i = 0; i < dataTransfer.mozItemCount; i++) {
3770       let dtTypes = dataTransfer.mozTypesAt(i);
3771       if (dtTypes.length != aExpectedDragData[i].length) {
3772         return dataTransfer;
3773       }
3774       for (let j = 0; j < dtTypes.length; j++) {
3775         if (dtTypes[j] != aExpectedDragData[i][j].type) {
3776           return dataTransfer;
3777         }
3778         let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i);
3779         if (aExpectedDragData[i][j].eqTest) {
3780           if (
3781             !aExpectedDragData[i][j].eqTest(
3782               dtData,
3783               aExpectedDragData[i][j].data
3784             )
3785           ) {
3786             return dataTransfer;
3787           }
3788         } else if (aExpectedDragData[i][j].data != dtData) {
3789           return dataTransfer;
3790         }
3791       }
3792     }
3793   } catch (ex) {
3794     return ex;
3795   }
3796   return true;
3800  * This callback type is used with ``synthesizePlainDragAndCancel()``.
3801  * It should compare ``actualData`` and ``expectedData`` and return
3802  * true if the two should be considered equal, false otherwise.
3804  * @callback eqTest
3805  * @param {*} actualData
3806  * @param {*} expectedData
3807  * @return {boolean}
3808  */
3811  * synthesizePlainDragAndCancel() synthesizes drag start with
3812  * synthesizePlainDragAndDrop(), but always cancel it with preventing default
3813  * of "dragstart".  Additionally, this checks whether the dataTransfer of
3814  * "dragstart" event has only expected items.
3816  * @param {Object} aParams
3817  *        The params which is set to the argument of ``synthesizePlainDragAndDrop()``.
3818  * @param {Array} aExpectedDataTransferItems
3819  *        All expected dataTransfer items.
3820  *        This data is in the format:
3822  *        [
3823  *          [
3824  *            {"type": value, "data": value, eqTest: function}
3825  *            ...,
3826  *          ],
3827  *          ...
3828  *        ]
3830  *        This can also be null.
3831  *        You can optionally provide ``eqTest`` {@type eqTest} if the
3832  *        comparison to the expected data transfer items can't be done
3833  *        with x == y;
3834  * @return {boolean}
3835  *        true if aExpectedDataTransferItems matches with
3836  *        DragEvent.dataTransfer of "dragstart" event.
3837  *        Otherwise, the dataTransfer object (may be null) or
3838  *        thrown exception, NOT false.  Therefore, you shouldn't
3839  *        use.
3840  */
3841 async function synthesizePlainDragAndCancel(
3842   aParams,
3843   aExpectedDataTransferItems
3844 ) {
3845   let srcElement = aParams.srcSelection
3846     ? _computeSrcElementFromSrcSelection(aParams.srcSelection)
3847     : aParams.srcElement;
3848   let result;
3849   function onDragStart(aEvent) {
3850     aEvent.preventDefault();
3851     result = _checkDataTransferItems(
3852       aEvent.dataTransfer,
3853       aExpectedDataTransferItems
3854     );
3855   }
3856   SpecialPowers.addSystemEventListener(
3857     srcElement.ownerDocument,
3858     "dragstart",
3859     onDragStart,
3860     { capture: true }
3861   );
3862   try {
3863     aParams.expectCancelDragStart = true;
3864     await synthesizePlainDragAndDrop(aParams);
3865   } finally {
3866     SpecialPowers.removeSystemEventListener(
3867       srcElement.ownerDocument,
3868       "dragstart",
3869       onDragStart,
3870       { capture: true }
3871     );
3872   }
3873   return result;
3876 class EventCounter {
3877   constructor(aTarget, aType, aOptions = {}) {
3878     this.target = aTarget;
3879     this.type = aType;
3880     this.options = aOptions;
3882     this.eventCount = 0;
3883     // Bug 1512817:
3884     // SpecialPowers is picky and needs to be passed an explicit reference to
3885     // the function to be called. To avoid having to bind "this", we therefore
3886     // define the method this way, via a property.
3887     this.handleEvent = aEvent => {
3888       this.eventCount++;
3889     };
3891     if (aOptions.mozSystemGroup) {
3892       SpecialPowers.addSystemEventListener(
3893         aTarget,
3894         aType,
3895         this.handleEvent,
3896         aOptions.capture
3897       );
3898     } else {
3899       aTarget.addEventListener(aType, this, aOptions);
3900     }
3901   }
3903   unregister() {
3904     if (this.options.mozSystemGroup) {
3905       SpecialPowers.removeSystemEventListener(
3906         this.target,
3907         this.type,
3908         this.handleEvent,
3909         this.options.capture
3910       );
3911     } else {
3912       this.target.removeEventListener(this.type, this, this.options);
3913     }
3914   }
3916   get count() {
3917     return this.eventCount;
3918   }