tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

EventUtils.js (162319B)


      1 /* eslint-disable no-nested-ternary */
      2 /**
      3 * EventUtils provides some utility methods for creating and sending DOM events.
      4 *
      5 *  When adding methods to this file, please add a performance test for it.
      6 */
      7 
      8 // Certain functions assume this is loaded into browser window scope.
      9 // This is modifiable because certain chrome tests create their own gBrowser.
     10 /* global gBrowser:true */
     11 
     12 // This file is used both in privileged and unprivileged contexts, so we have to
     13 // be careful about our access to Components.interfaces. We also want to avoid
     14 // naming collisions with anything that might be defined in the scope that imports
     15 // this script.
     16 //
     17 // Even if the real |Components| doesn't exist, we might shim in a simple JS
     18 // placebo for compat. An easy way to differentiate this from the real thing
     19 // is whether the property is read-only or not.  The real |Components| property
     20 // is read-only.
     21 /* global _EU_Ci, _EU_Cc, _EU_Cu, _EU_ChromeUtils, _EU_OS */
     22 window.__defineGetter__("_EU_Ci", function () {
     23  var c = Object.getOwnPropertyDescriptor(window, "Components");
     24  return c && c.value && !c.writable ? Ci : SpecialPowers.Ci;
     25 });
     26 
     27 window.__defineGetter__("_EU_Cc", function () {
     28  var c = Object.getOwnPropertyDescriptor(window, "Components");
     29  return c && c.value && !c.writable ? Cc : SpecialPowers.Cc;
     30 });
     31 
     32 window.__defineGetter__("_EU_Cu", function () {
     33  var c = Object.getOwnPropertyDescriptor(window, "Components");
     34  return c && c.value && !c.writable ? Cu : SpecialPowers.Cu;
     35 });
     36 
     37 window.__defineGetter__("_EU_ChromeUtils", function () {
     38  var c = Object.getOwnPropertyDescriptor(window, "ChromeUtils");
     39  return c && c.value && !c.writable ? ChromeUtils : SpecialPowers.ChromeUtils;
     40 });
     41 
     42 window.__defineGetter__("_EU_OS", function () {
     43  delete this._EU_OS;
     44  try {
     45    this._EU_OS = _EU_ChromeUtils.importESModule(
     46      "resource://gre/modules/AppConstants.sys.mjs"
     47    ).platform;
     48  } catch (ex) {
     49    this._EU_OS = null;
     50  }
     51  return this._EU_OS;
     52 });
     53 
     54 function _EU_isMac(aWindow = window) {
     55  if (window._EU_OS) {
     56    return window._EU_OS == "macosx";
     57  }
     58  if (aWindow) {
     59    try {
     60      return aWindow.navigator.platform.indexOf("Mac") > -1;
     61    } catch (ex) {}
     62  }
     63  return navigator.platform.indexOf("Mac") > -1;
     64 }
     65 
     66 function _EU_isWin(aWindow = window) {
     67  if (window._EU_OS) {
     68    return window._EU_OS == "win";
     69  }
     70  if (aWindow) {
     71    try {
     72      return aWindow.navigator.platform.indexOf("Win") > -1;
     73    } catch (ex) {}
     74  }
     75  return navigator.platform.indexOf("Win") > -1;
     76 }
     77 
     78 function _EU_isLinux(aWindow = window) {
     79  if (window._EU_OS) {
     80    return window._EU_OS == "linux";
     81  }
     82  if (aWindow) {
     83    try {
     84      return aWindow.navigator.platform.startsWith("Linux");
     85    } catch (ex) {}
     86  }
     87  return navigator.platform.startsWith("Linux");
     88 }
     89 
     90 function _EU_isAndroid(aWindow = window) {
     91  if (window._EU_OS) {
     92    return window._EU_OS == "android";
     93  }
     94  if (aWindow) {
     95    try {
     96      return aWindow.navigator.userAgent.includes("Android");
     97    } catch (ex) {}
     98  }
     99  return navigator.userAgent.includes("Android");
    100 }
    101 
    102 function _EU_maybeWrap(o) {
    103  // We're used in some contexts where there is no SpecialPowers and also in
    104  // some where it exists but has no wrap() method.  And this is somewhat
    105  // independent of whether window.Components is a thing...
    106  var haveWrap = false;
    107  try {
    108    haveWrap = SpecialPowers.wrap != undefined;
    109  } catch (e) {
    110    // Just leave it false.
    111  }
    112  if (!haveWrap) {
    113    // Not much we can do here.
    114    return o;
    115  }
    116  var c = Object.getOwnPropertyDescriptor(window, "Components");
    117  return c && c.value && !c.writable ? o : SpecialPowers.wrap(o);
    118 }
    119 
    120 function _EU_maybeUnwrap(o) {
    121  var haveWrap = false;
    122  try {
    123    haveWrap = SpecialPowers.unwrap != undefined;
    124  } catch (e) {
    125    // Just leave it false.
    126  }
    127  if (!haveWrap) {
    128    // Not much we can do here.
    129    return o;
    130  }
    131  var c = Object.getOwnPropertyDescriptor(window, "Components");
    132  return c && c.value && !c.writable ? o : SpecialPowers.unwrap(o);
    133 }
    134 
    135 function _EU_getPlatform() {
    136  if (_EU_isWin()) {
    137    return "windows";
    138  }
    139  if (_EU_isMac()) {
    140    return "mac";
    141  }
    142  if (_EU_isAndroid()) {
    143    return "android";
    144  }
    145  if (_EU_isLinux()) {
    146    return "linux";
    147  }
    148  return "unknown";
    149 }
    150 
    151 function _EU_roundDevicePixels(aMaybeFractionalPixels) {
    152  return Math.floor(aMaybeFractionalPixels + 0.5);
    153 }
    154 
    155 /**
    156 * promiseElementReadyForUserInput() dispatches mousemove events to aElement
    157 * and waits one of them for a while.  Then, returns "resolved" state when it's
    158 * successfully received.  Otherwise, if it couldn't receive mousemove event on
    159 * it, this throws an exception.  So, aElement must be an element which is
    160 * assumed non-collapsed visible element in the window.
    161 *
    162 * This is useful if you need to synthesize mouse events via the main process
    163 * but your test cannot check whether the element is now in APZ to deliver
    164 * a user input event.
    165 */
    166 async function promiseElementReadyForUserInput(
    167  aElement,
    168  aWindow = window,
    169  aLogFunc = null
    170 ) {
    171  if (typeof aElement == "string") {
    172    aElement = aWindow.document.getElementById(aElement);
    173  }
    174 
    175  function waitForMouseMoveForHittest() {
    176    return new Promise(resolve => {
    177      let timeout;
    178      const onHit = () => {
    179        if (aLogFunc) {
    180          aLogFunc("mousemove received");
    181        }
    182        aWindow.clearInterval(timeout);
    183        resolve(true);
    184      };
    185      aElement.addEventListener("mousemove", onHit, {
    186        capture: true,
    187        once: true,
    188      });
    189      timeout = aWindow.setInterval(() => {
    190        if (aLogFunc) {
    191          aLogFunc("mousemove not received in this 300ms");
    192        }
    193        aElement.removeEventListener("mousemove", onHit, {
    194          capture: true,
    195        });
    196        resolve(false);
    197      }, 300);
    198      synthesizeMouseAtCenter(aElement, { type: "mousemove" }, aWindow);
    199    });
    200  }
    201  for (let i = 0; i < 20; i++) {
    202    if (await waitForMouseMoveForHittest()) {
    203      return Promise.resolve();
    204    }
    205  }
    206  throw new Error("The element or the window did not become interactive");
    207 }
    208 
    209 function getElement(id) {
    210  return typeof id == "string" ? document.getElementById(id) : id;
    211 }
    212 
    213 this.$ = this.getElement;
    214 
    215 function computeButton(aEvent) {
    216  if (typeof aEvent.button != "undefined") {
    217    return aEvent.button;
    218  }
    219  return aEvent.type == "contextmenu" ? 2 : 0;
    220 }
    221 
    222 /**
    223 * Send a mouse event to the node aTarget (aTarget can be an id, or an
    224 * actual node) . The "event" passed in to aEvent is just a JavaScript
    225 * object with the properties set that the real mouse event object should
    226 * have. This includes the type of the mouse event. Pretty much all those
    227 * properties are optional.
    228 * E.g. to send an click event to the node with id 'node' you might do this:
    229 *
    230 * ``sendMouseEvent({type:'click'}, 'node');``
    231 */
    232 function sendMouseEvent(aEvent, aTarget, aWindow) {
    233  if (
    234    ![
    235      "click",
    236      "contextmenu",
    237      "dblclick",
    238      "mousedown",
    239      "mouseup",
    240      "mouseover",
    241      "mouseout",
    242    ].includes(aEvent.type)
    243  ) {
    244    throw new Error(
    245      "sendMouseEvent doesn't know about event type '" + aEvent.type + "'"
    246    );
    247  }
    248 
    249  if (!aWindow) {
    250    aWindow = window;
    251  }
    252 
    253  if (typeof aTarget == "string") {
    254    aTarget = aWindow.document.getElementById(aTarget);
    255  }
    256 
    257  let dict = {
    258    bubbles: true,
    259    cancelable: true,
    260    view: aWindow,
    261    detail:
    262      aEvent.detail ||
    263      // eslint-disable-next-line no-nested-ternary
    264      (aEvent.type == "click" ||
    265      aEvent.type == "mousedown" ||
    266      aEvent.type == "mouseup"
    267        ? 1
    268        : aEvent.type == "dblclick"
    269          ? 2
    270          : 0),
    271    screenX: aEvent.screenX || 0,
    272    screenY: aEvent.screenY || 0,
    273    clientX: aEvent.clientX || 0,
    274    clientY: aEvent.clientY || 0,
    275    ctrlKey: aEvent.ctrlKey || false,
    276    altKey: aEvent.altKey || false,
    277    shiftKey: aEvent.shiftKey || false,
    278    metaKey: aEvent.metaKey || false,
    279    button: computeButton(aEvent),
    280    // FIXME: Set buttons
    281    relatedTarget: aEvent.relatedTarget || null,
    282  };
    283 
    284  let event =
    285    aEvent.type == "click" || aEvent.type == "contextmenu"
    286      ? new aWindow.PointerEvent(aEvent.type, dict)
    287      : new aWindow.MouseEvent(aEvent.type, dict);
    288 
    289  // If documentURIObject exists or `window` is a stub object, we're in
    290  // a chrome scope, so don't bother trying to go through SpecialPowers.
    291  if (!window.document || window.document.documentURIObject) {
    292    return aTarget.dispatchEvent(event);
    293  }
    294  return SpecialPowers.dispatchEvent(aWindow, aTarget, event);
    295 }
    296 
    297 function isHidden(aElement) {
    298  var box = aElement.getBoundingClientRect();
    299  return box.width == 0 && box.height == 0;
    300 }
    301 
    302 /**
    303 * Send a drag event to the node aTarget (aTarget can be an id, or an
    304 * actual node) . The "event" passed in to aEvent is just a JavaScript
    305 * object with the properties set that the real drag event object should
    306 * have. This includes the type of the drag event.
    307 *
    308 * @returns {boolean}
    309 */
    310 function sendDragEvent(aEvent, aTarget, aWindow = window) {
    311  if (
    312    ![
    313      "drag",
    314      "dragstart",
    315      "dragend",
    316      "dragover",
    317      "dragenter",
    318      "dragleave",
    319      "drop",
    320    ].includes(aEvent.type)
    321  ) {
    322    throw new Error(
    323      "sendDragEvent doesn't know about event type '" + aEvent.type + "'"
    324    );
    325  }
    326 
    327  if (typeof aTarget == "string") {
    328    aTarget = aWindow.document.getElementById(aTarget);
    329  }
    330 
    331  /*
    332   * Drag event cannot be performed if the element is hidden, except 'dragend'
    333   * event where the element can becomes hidden after start dragging.
    334   */
    335  if (aEvent.type != "dragend" && isHidden(aTarget)) {
    336    var targetName = aTarget.nodeName;
    337    if ("id" in aTarget && aTarget.id) {
    338      targetName += "#" + aTarget.id;
    339    }
    340    throw new Error(`${aEvent.type} event target ${targetName} is hidden`);
    341  }
    342 
    343  var event = aWindow.document.createEvent("DragEvent");
    344 
    345  var typeArg = aEvent.type;
    346  var canBubbleArg = true;
    347  var cancelableArg = true;
    348  var viewArg = aWindow;
    349  var detailArg = aEvent.detail || 0;
    350  var screenXArg = aEvent.screenX || 0;
    351  var screenYArg = aEvent.screenY || 0;
    352  var clientXArg = aEvent.clientX || 0;
    353  var clientYArg = aEvent.clientY || 0;
    354  var ctrlKeyArg = aEvent.ctrlKey || false;
    355  var altKeyArg = aEvent.altKey || false;
    356  var shiftKeyArg = aEvent.shiftKey || false;
    357  var metaKeyArg = aEvent.metaKey || false;
    358  var buttonArg = computeButton(aEvent);
    359  var relatedTargetArg = aEvent.relatedTarget || null;
    360  var dataTransfer = aEvent.dataTransfer || null;
    361 
    362  event.initDragEvent(
    363    typeArg,
    364    canBubbleArg,
    365    cancelableArg,
    366    viewArg,
    367    detailArg,
    368    Math.round(screenXArg),
    369    Math.round(screenYArg),
    370    Math.round(clientXArg),
    371    Math.round(clientYArg),
    372    ctrlKeyArg,
    373    altKeyArg,
    374    shiftKeyArg,
    375    metaKeyArg,
    376    buttonArg,
    377    relatedTargetArg,
    378    dataTransfer
    379  );
    380 
    381  if (aEvent._domDispatchOnly) {
    382    return aTarget.dispatchEvent(event);
    383  }
    384 
    385  var utils = _getDOMWindowUtils(aWindow);
    386  return utils.dispatchDOMEventViaPresShellForTesting(aTarget, event);
    387 }
    388 
    389 /**
    390 * Send the char aChar to the focused element.  This method handles casing of
    391 * chars (sends the right charcode, and sends a shift key for uppercase chars).
    392 * No other modifiers are handled at this point.
    393 *
    394 * For now this method only works for ASCII characters and emulates the shift
    395 * key state on US keyboard layout.
    396 */
    397 function sendChar(aChar, aWindow) {
    398  var hasShift;
    399  // Emulate US keyboard layout for the shiftKey state.
    400  switch (aChar) {
    401    case "!":
    402    case "@":
    403    case "#":
    404    case "$":
    405    case "%":
    406    case "^":
    407    case "&":
    408    case "*":
    409    case "(":
    410    case ")":
    411    case "_":
    412    case "+":
    413    case "{":
    414    case "}":
    415    case ":":
    416    case '"':
    417    case "|":
    418    case "<":
    419    case ">":
    420    case "?":
    421      hasShift = true;
    422      break;
    423    default:
    424      hasShift =
    425        aChar.toLowerCase() != aChar.toUpperCase() &&
    426        aChar == aChar.toUpperCase();
    427      break;
    428  }
    429  synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
    430 }
    431 
    432 /**
    433 * Send the string aStr to the focused element.
    434 *
    435 * For now this method only works for ASCII characters and emulates the shift
    436 * key state on US keyboard layout.
    437 */
    438 function sendString(aStr, aWindow) {
    439  for (let i = 0; i < aStr.length; ++i) {
    440    // Do not split a surrogate pair to call synthesizeKey.  Dispatching two
    441    // sets of keydown and keyup caused by two calls of synthesizeKey is not
    442    // good behavior.  It could happen due to a bug, but a surrogate pair should
    443    // be introduced with one key press operation.  Therefore, calling it with
    444    // a surrogate pair is the right thing.
    445    // Note that TextEventDispatcher will consider whether a surrogate pair
    446    // should cause one or two keypress events automatically.  Therefore, we
    447    // don't need to check the related prefs here.
    448    if (
    449      (aStr.charCodeAt(i) & 0xfc00) == 0xd800 &&
    450      i + 1 < aStr.length &&
    451      (aStr.charCodeAt(i + 1) & 0xfc00) == 0xdc00
    452    ) {
    453      sendChar(aStr.substring(i, i + 2), aWindow);
    454      i++;
    455    } else {
    456      sendChar(aStr.charAt(i), aWindow);
    457    }
    458  }
    459 }
    460 
    461 /**
    462 * Send the non-character key aKey to the focused node.
    463 * The name of the key should be the part that comes after ``DOM_VK_`` in the
    464 * KeyEvent constant name for this key.
    465 * No modifiers are handled at this point.
    466 */
    467 function sendKey(aKey, aWindow) {
    468  var keyName = "VK_" + aKey.toUpperCase();
    469  synthesizeKey(keyName, { shiftKey: false }, aWindow);
    470 }
    471 
    472 /**
    473 * Parse the key modifier flags from aEvent. Used to share code between
    474 * synthesizeMouse and synthesizeKey.
    475 */
    476 function _parseModifiers(aEvent, aWindow = window) {
    477  var nsIDOMWindowUtils = _EU_Ci.nsIDOMWindowUtils;
    478  var mval = 0;
    479  if (aEvent.shiftKey) {
    480    mval |= nsIDOMWindowUtils.MODIFIER_SHIFT;
    481  }
    482  if (aEvent.ctrlKey) {
    483    mval |= nsIDOMWindowUtils.MODIFIER_CONTROL;
    484  }
    485  if (aEvent.altKey) {
    486    mval |= nsIDOMWindowUtils.MODIFIER_ALT;
    487  }
    488  if (aEvent.metaKey) {
    489    mval |= nsIDOMWindowUtils.MODIFIER_META;
    490  }
    491  if (aEvent.accelKey) {
    492    mval |= _EU_isMac(aWindow)
    493      ? nsIDOMWindowUtils.MODIFIER_META
    494      : nsIDOMWindowUtils.MODIFIER_CONTROL;
    495  }
    496  if (aEvent.altGrKey) {
    497    mval |= nsIDOMWindowUtils.MODIFIER_ALTGRAPH;
    498  }
    499  if (aEvent.capsLockKey) {
    500    mval |= nsIDOMWindowUtils.MODIFIER_CAPSLOCK;
    501  }
    502  if (aEvent.fnKey) {
    503    mval |= nsIDOMWindowUtils.MODIFIER_FN;
    504  }
    505  if (aEvent.fnLockKey) {
    506    mval |= nsIDOMWindowUtils.MODIFIER_FNLOCK;
    507  }
    508  if (aEvent.numLockKey) {
    509    mval |= nsIDOMWindowUtils.MODIFIER_NUMLOCK;
    510  }
    511  if (aEvent.scrollLockKey) {
    512    mval |= nsIDOMWindowUtils.MODIFIER_SCROLLLOCK;
    513  }
    514  if (aEvent.symbolKey) {
    515    mval |= nsIDOMWindowUtils.MODIFIER_SYMBOL;
    516  }
    517  if (aEvent.symbolLockKey) {
    518    mval |= nsIDOMWindowUtils.MODIFIER_SYMBOLLOCK;
    519  }
    520 
    521  return mval;
    522 }
    523 
    524 /**
    525 * Return the drag service.  Note that if we're in the headless mode, this
    526 * may return null because the service may be never instantiated (e.g., on
    527 * Linux).
    528 */
    529 function getDragService() {
    530  try {
    531    return _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
    532      _EU_Ci.nsIDragService
    533    );
    534  } catch (e) {
    535    // If we're in the headless mode, the drag service may be never
    536    // instantiated.  In this case, an exception is thrown.  Let's ignore
    537    // any exceptions since without the drag service, nobody can create a
    538    // drag session.
    539    return null;
    540  }
    541 }
    542 
    543 /**
    544 * End drag session if there is.
    545 *
    546 * TODO: This should synthesize "drop" if necessary.
    547 *
    548 * @param left          X offset in the viewport
    549 * @param top           Y offset in the viewport
    550 * @param aEvent        The event data, the modifiers are applied to the
    551 *                      "dragend" event.
    552 * @param aWindow       The window.
    553 * @return              true if handled.  In this case, the caller should not
    554 *                      synthesize DOM events basically.
    555 */
    556 function _maybeEndDragSession(left, top, aEvent, aWindow) {
    557  let utils = _getDOMWindowUtils(aWindow);
    558  const dragSession = utils.dragSession;
    559  if (!dragSession) {
    560    return false;
    561  }
    562  // FIXME: If dragSession.dragAction is not
    563  // nsIDragService.DRAGDROP_ACTION_NONE nor aEvent.type is not `keydown`, we
    564  // need to synthesize a "drop" event or call setDragEndPointForTests here to
    565  // set proper left/top to `dragend` event.
    566  try {
    567    dragSession.endDragSession(false, _parseModifiers(aEvent, aWindow));
    568  } catch (e) {}
    569  return true;
    570 }
    571 
    572 function _maybeSynthesizeDragOver(left, top, aEvent, aWindow) {
    573  let utils = _getDOMWindowUtils(aWindow);
    574  const dragSession = utils.dragSession;
    575  if (!dragSession) {
    576    return false;
    577  }
    578  const target = aWindow.document.elementFromPoint(left, top);
    579  if (target) {
    580    sendDragEvent(
    581      createDragEventObject(
    582        "dragover",
    583        target,
    584        aWindow,
    585        dragSession.dataTransfer,
    586        {
    587          accelKey: aEvent.accelKey,
    588          altKey: aEvent.altKey,
    589          altGrKey: aEvent.altGrKey,
    590          ctrlKey: aEvent.ctrlKey,
    591          metaKey: aEvent.metaKey,
    592          shiftKey: aEvent.shiftKey,
    593          capsLockKey: aEvent.capsLockKey,
    594          fnKey: aEvent.fnKey,
    595          fnLockKey: aEvent.fnLockKey,
    596          numLockKey: aEvent.numLockKey,
    597          scrollLockKey: aEvent.scrollLockKey,
    598          symbolKey: aEvent.symbolKey,
    599          symbolLockKey: aEvent.symbolLockKey,
    600        }
    601      ),
    602      target,
    603      aWindow
    604    );
    605  }
    606  return true;
    607 }
    608 
    609 /**
    610 * @typedef {object} MouseEventData
    611 *
    612 * @property {string} [accessKey] - The character or key associated with
    613 *     the access key event. Typically a single character used to activate a UI
    614 *     element via keyboard shortcuts (e.g., Alt + accessKey).
    615 * @property {boolean} [altKey] - If set to `true`, the Alt key will be
    616 *     considered pressed.
    617 * @property {boolean} [asyncEnabled] - If `true`, the event is
    618 *     dispatched to the parent process through APZ, without being injected
    619 *     into the OS event queue.
    620 * @property {number} [button=0] - Button to synthesize.
    621 * @property {number} [buttons] - Indicates which mouse buttons are pressed
    622 *     when a mouse event is triggered.
    623 * @property {number} [clickCount=1] - Number of clicks that have to be performed.
    624 * @property {boolean} [ctrlKey] - If set to `true`, the Ctrl key will
    625 *     be considered pressed.
    626 * @property {number} [id] - A unique identifier for the pointer causing the event.
    627 * @property {number} [inputSource] - Input source, see MouseEvent for values.
    628 *     Defaults to MouseEvent.MOZ_SOURCE_MOUSE.
    629 * @property {boolean} [isSynthesized] - Controls Event.isSynthesized value that
    630 *     helps identifying test related events
    631 * @property {boolean} [isWidgetEventSynthesized] - Controls WidgetMouseEvent.mReason value.
    632 * @property {boolean} [metaKey] - If set to `true`, the Meta key will
    633 *     be considered pressed.
    634 * @property {number} [pressure=0] - Touch input pressure (0.0 -> 1.0).
    635 * @property {boolean} [shiftKey] - If set to `true`, the Shift key will
    636 *     be considered pressed.
    637 * @property {string} [type] - Event type to synthesize. If not specified
    638 *     a `mousedown` followed by a `mouseup` are performed.
    639 *
    640 * @see nsIDOMWindowUtils.sendMouseEvent
    641 */
    642 
    643 /**
    644 * Synthesize a mouse event on a target.
    645 *
    646 * The actual client point is determined by taking the aTarget's client box
    647 * and offsetting it by aOffsetX and aOffsetY.
    648 *
    649 * Note that additional events may be fired as a result of this call. For
    650 * instance, typically a click event will be fired as a result of a
    651 * mousedown and mouseup in sequence.
    652 *
    653 * @param {Element} aTarget - DOM element to dispatch the event on.
    654 * @param {number} aOffsetX - X offset in CSS pixels from the element’s left edge.
    655 * @param {number} aOffsetY - Y offset in CSS pixels from the element’s top edge.
    656 * @param {MouseEventData} aEvent - Details of the mouse event to dispatch.
    657 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
    658 * @param {Function} [aCallback] - A callback function that is invoked when the
    659 *                                 mouse event is dispatched.
    660 *
    661 * @returns {boolean} Whether the event had preventDefault() called on it.
    662 */
    663 function synthesizeMouse(
    664  aTarget,
    665  aOffsetX,
    666  aOffsetY,
    667  aEvent,
    668  aWindow,
    669  aCallback
    670 ) {
    671  var rect = aTarget.getBoundingClientRect();
    672  return synthesizeMouseAtPoint(
    673    rect.left + aOffsetX,
    674    rect.top + aOffsetY,
    675    aEvent,
    676    aWindow,
    677    aCallback
    678  );
    679 }
    680 
    681 /**
    682 * Synthesize a mouse event in `aWindow` at a point.
    683 *
    684 * `nsIDOMWindowUtils.sendMouseEvent` takes floats for the coordinates.
    685 * Therefore, don't round or truncate the values.
    686 *
    687 * Note that additional events may be fired as a result of this call. For
    688 * instance, typically a click event will be fired as a result of a
    689 * mousedown and mouseup in sequence.
    690 *
    691 * @param {number} aLeft - Floating-point value for the X offset in CSS pixels.
    692 * @param {number} aTop - Floating-point value for the Y offset in CSS pixels.
    693 * @param {MouseEventData} aEvent - Details of the mouse event to dispatch.
    694 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
    695 * @param {Function} [aCallback] - A callback function that is invoked when the
    696 *                                 mouse event is dispatched.
    697 *
    698 * @returns {boolean} Whether the event had preventDefault() called on it.
    699 */
    700 function synthesizeMouseAtPoint(
    701  aLeft,
    702  aTop,
    703  aEvent,
    704  aWindow = window,
    705  aCallback
    706 ) {
    707  if (aEvent.allowToHandleDragDrop) {
    708    if (aEvent.type == "mouseup" || !aEvent.type) {
    709      if (_maybeEndDragSession(aLeft, aTop, aEvent, aWindow)) {
    710        return false;
    711      }
    712    } else if (aEvent.type == "mousemove") {
    713      if (_maybeSynthesizeDragOver(aLeft, aTop, aEvent, aWindow)) {
    714        return false;
    715      }
    716    }
    717  }
    718 
    719  var utils = _getDOMWindowUtils(aWindow);
    720  var defaultPrevented = false;
    721 
    722  if (utils) {
    723    var button = computeButton(aEvent);
    724    var clickCount = aEvent.clickCount || 1;
    725    var modifiers = _parseModifiers(aEvent, aWindow);
    726    var pressure = "pressure" in aEvent ? aEvent.pressure : 0;
    727 
    728    // aWindow might be cross-origin from us.
    729    var MouseEvent = _EU_maybeWrap(aWindow).MouseEvent;
    730 
    731    // Default source to mouse.
    732    var inputSource =
    733      "inputSource" in aEvent
    734        ? aEvent.inputSource
    735        : MouseEvent.MOZ_SOURCE_MOUSE;
    736    // Compute a pointerId if needed.
    737    var id;
    738    if ("id" in aEvent) {
    739      id = aEvent.id;
    740    } else {
    741      var isFromPen = inputSource === MouseEvent.MOZ_SOURCE_PEN;
    742      id = isFromPen
    743        ? utils.DEFAULT_PEN_POINTER_ID
    744        : utils.DEFAULT_MOUSE_POINTER_ID;
    745    }
    746 
    747    // FYI: Window.synthesizeMouseEvent takes floats for the coordinates.
    748    // Therefore, don't round/truncate the fractional values.
    749    const isDOMEventSynthesized =
    750      "isSynthesized" in aEvent ? aEvent.isSynthesized : true;
    751    const isWidgetEventSynthesized =
    752      "isWidgetEventSynthesized" in aEvent
    753        ? aEvent.isWidgetEventSynthesized
    754        : false;
    755    const isAsyncEnabled =
    756      "asyncEnabled" in aEvent ? aEvent.asyncEnabled : false;
    757 
    758    if ("type" in aEvent && aEvent.type) {
    759      defaultPrevented = _EU_maybeWrap(aWindow).synthesizeMouseEvent(
    760        aEvent.type,
    761        aLeft,
    762        aTop,
    763        {
    764          identifier: id,
    765          button,
    766          buttons: aEvent.buttons,
    767          clickCount,
    768          modifiers,
    769          pressure,
    770          inputSource,
    771        },
    772        {
    773          isDOMEventSynthesized,
    774          isWidgetEventSynthesized,
    775          isAsyncEnabled,
    776        },
    777        aCallback
    778      );
    779    } else {
    780      _EU_maybeWrap(aWindow).synthesizeMouseEvent(
    781        "mousedown",
    782        aLeft,
    783        aTop,
    784        {
    785          identifier: id,
    786          button,
    787          buttons: aEvent.buttons,
    788          clickCount,
    789          modifiers,
    790          pressure,
    791          inputSource,
    792        },
    793        {
    794          isDOMEventSynthesized,
    795          isWidgetEventSynthesized,
    796          isAsyncEnabled,
    797        },
    798        aCallback
    799      );
    800      _EU_maybeWrap(aWindow).synthesizeMouseEvent(
    801        "mouseup",
    802        aLeft,
    803        aTop,
    804        {
    805          identifier: id,
    806          button,
    807          buttons: aEvent.buttons,
    808          clickCount,
    809          modifiers,
    810          pressure,
    811          inputSource,
    812        },
    813        {
    814          isDOMEventSynthesized,
    815          isWidgetEventSynthesized,
    816          isAsyncEnabled,
    817        },
    818        aCallback
    819      );
    820    }
    821  }
    822 
    823  return defaultPrevented;
    824 }
    825 
    826 /**
    827 * Synthesize a mouse event at the center of `aTarget`.
    828 *
    829 * Note that additional events may be fired as a result of this call. For
    830 * instance, typically a click event will be fired as a result of a
    831 * mousedown and mouseup in sequence.
    832 *
    833 * @param {Element} aTarget - DOM element to dispatch the event on.
    834 * @param {MouseEventData} aEvent - Details of the mouse event to dispatch.
    835 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
    836 * @param {Function} [aCallback] - A callback function that is invoked when the
    837 *                                 mouse event is dispatched.
    838 *
    839 * @returns {boolean} Whether the event had preventDefault() called on it.
    840 */
    841 function synthesizeMouseAtCenter(aTarget, aEvent, aWindow, aCallback) {
    842  var rect = aTarget.getBoundingClientRect();
    843 
    844  return synthesizeMouse(
    845    aTarget,
    846    rect.width / 2,
    847    rect.height / 2,
    848    aEvent,
    849    aWindow,
    850    aCallback
    851  );
    852 }
    853 
    854 /**
    855 * @typedef {object} TouchEventData
    856 * @property {boolean} [aEvent.asyncEnabled] - If `true`, the event is
    857 * dispatched to the parent process through APZ, without being injected
    858 * into the OS event queue.
    859 * @property {string} [aEvent.type] - The touch event type. If undefined,
    860 * "touchstart" and "touchend" will be synthesized at same point.
    861 * @property {number | number[]} [aEvent.id] - The touch id. If you don't specify this,
    862 * default touch id will be used for first touch and further touch ids
    863 * are the values incremented from the first id.
    864 * @property {number | number[]} [aEvent.ry] - The X radius in CSS pixels of the touch
    865 * @property {number | number[]} [aEvent.ry] - The Y radius in CSS pixels of the touch
    866 * @property {number | number[]} [aEvent.angle] - The angle in degrees
    867 * @property {number | number[]} [aEvent.force] - The force of the touch
    868 * @property {number | number[]} [aEvent.tiltX] - The X tilt of the touch
    869 * @property {number | number[]} [aEvent.tiltY] - The Y tilt of the touch
    870 * @property {number | number[]} [aEvent.twist] - The twist of the touch
    871 */
    872 
    873 /**
    874 * Synthesize one or more touches on aTarget. aTarget can be either Element
    875 * or Array of Elements.  aOffsetX, aOffsetY, aEvent.id, aEvent.rx, aEvent.ry,
    876 * aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY and aEvent.twist can
    877 * be either number or array of numbers (can be mixed).  If you specify array
    878 * to synthesize a multi-touch, you need to specify same length arrays.  If
    879 * you don't specify array to them, same values (or computed default values for
    880 * aEvent.id) are used for all touches.
    881 *
    882 * @param {Element | Element[]} aTarget - The target element which you specify
    883 * relative offset from its top-left.
    884 * @param {number | number[]} aOffsetX - The relative offset from left of aTarget.
    885 * @param {number | number[]} aOffsetY - The relative offset from top of aTarget.
    886 * @param {TouchEventData} aEvent - Details of the touch event to dispatch
    887 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
    888 *
    889 * @returns true if and only if aEvent.type is specified and default of the
    890 * event is prevented.
    891 */
    892 function synthesizeTouch(
    893  aTarget,
    894  aOffsetX,
    895  aOffsetY,
    896  aEvent = {},
    897  aWindow = window
    898 ) {
    899  let rectX, rectY;
    900  if (Array.isArray(aTarget)) {
    901    let lastTarget, lastTargetRect;
    902    aTarget.forEach(target => {
    903      const rect =
    904        target == lastTarget ? lastTargetRect : target.getBoundingClientRect();
    905      rectX.push(rect.left);
    906      rectY.push(rect.top);
    907      lastTarget = target;
    908      lastTargetRect = rect;
    909    });
    910  } else {
    911    const rect = aTarget.getBoundingClientRect();
    912    rectX = [rect.left];
    913    rectY = [rect.top];
    914  }
    915  const offsetX = (() => {
    916    if (Array.isArray(aOffsetX)) {
    917      let ret = [];
    918      aOffsetX.forEach((value, index) => {
    919        ret.push(value + rectX[Math.min(index, rectX.length - 1)]);
    920      });
    921      return ret;
    922    }
    923    return aOffsetX + rectX[0];
    924  })();
    925  const offsetY = (() => {
    926    if (Array.isArray(aOffsetY)) {
    927      let ret = [];
    928      aOffsetY.forEach((value, index) => {
    929        ret.push(value + rectY[Math.min(index, rectY.length - 1)]);
    930      });
    931      return ret;
    932    }
    933    return aOffsetY + rectY[0];
    934  })();
    935  return synthesizeTouchAtPoint(offsetX, offsetY, aEvent, aWindow);
    936 }
    937 
    938 /**
    939 * Synthesize one or more touches at the points. aLeft, aTop, aEvent.id,
    940 * aEvent.rx, aEvent.ry, aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY
    941 * and aEvent.twist can be either number or array of numbers (can be mixed).
    942 * If you specify array to synthesize a multi-touch, you need to specify same
    943 * length arrays.  If you don't specify array to them, same values are used for
    944 * all touches.
    945 *
    946 * @param {number | number[]} aLeft - The relative offset from left of aTarget.
    947 * @param {number | number[]} aTop - The relative offset from top of aTarget.
    948 * @param {TouchEventData} aEvent - Details of the touch event to dispatch
    949 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
    950 *
    951 * @returns true if and only if aEvent.type is specified and default of the
    952 * event is prevented.
    953 */
    954 function synthesizeTouchAtPoint(aLeft, aTop, aEvent = {}, aWindow = window) {
    955  let utils = _getDOMWindowUtils(aWindow);
    956  if (!utils) {
    957    return false;
    958  }
    959 
    960  if (
    961    Array.isArray(aLeft) &&
    962    Array.isArray(aTop) &&
    963    aLeft.length != aTop.length
    964  ) {
    965    throw new Error(`aLeft and aTop should be same length array`);
    966  }
    967 
    968  const arrayLength = Array.isArray(aLeft)
    969    ? aLeft.length
    970    : Array.isArray(aTop)
    971      ? aTop.length
    972      : 1;
    973 
    974  function throwExceptionIfDifferentLengthArray(aArray, aName) {
    975    if (Array.isArray(aArray) && arrayLength !== aArray.length) {
    976      throw new Error(`${aName} is different length array`);
    977    }
    978  }
    979  const leftArray = (() => {
    980    if (Array.isArray(aLeft)) {
    981      for (let i = 0; i < aLeft.length; i++) {
    982        aLeft[i] = _EU_roundDevicePixels(aLeft[i]);
    983      }
    984      return aLeft;
    985    }
    986    return new Array(arrayLength).fill(_EU_roundDevicePixels(aLeft));
    987  })();
    988  const topArray = (() => {
    989    if (Array.isArray(aTop)) {
    990      throwExceptionIfDifferentLengthArray(aTop, "aTop");
    991      for (let i = 0; i < aTop.length; i++) {
    992        aTop[i] = _EU_roundDevicePixels(aTop[i]);
    993      }
    994      return aTop;
    995    }
    996    return new Array(arrayLength).fill(_EU_roundDevicePixels(aTop));
    997  })();
    998  const idArray = (() => {
    999    if ("id" in aEvent && Array.isArray(aEvent.id)) {
   1000      throwExceptionIfDifferentLengthArray(aEvent.id, "aEvent.id");
   1001      return aEvent.id;
   1002    }
   1003    let id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID;
   1004    let ret = [];
   1005    for (let i = 0; i < arrayLength; i++) {
   1006      ret.push(id++);
   1007    }
   1008    return ret;
   1009  })();
   1010  function getSameLengthArrayOfEventProperty(aProperty, aDefaultValue) {
   1011    if (aProperty in aEvent && Array.isArray(aEvent[aProperty])) {
   1012      throwExceptionIfDifferentLengthArray(
   1013        aEvent.rx,
   1014        arrayLength,
   1015        `aEvent.${aProperty}`
   1016      );
   1017      return aEvent[aProperty];
   1018    }
   1019    return new Array(arrayLength).fill(aEvent[aProperty] || aDefaultValue);
   1020  }
   1021  const rxArray = getSameLengthArrayOfEventProperty("rx", 1);
   1022  const ryArray = getSameLengthArrayOfEventProperty("ry", 1);
   1023  const angleArray = getSameLengthArrayOfEventProperty("angle", 0);
   1024  const forceArray = getSameLengthArrayOfEventProperty(
   1025    "force",
   1026    aEvent.type === "touchend" ? 0 : 1
   1027  );
   1028  const tiltXArray = getSameLengthArrayOfEventProperty("tiltX", 0);
   1029  const tiltYArray = getSameLengthArrayOfEventProperty("tiltY", 0);
   1030  const twistArray = getSameLengthArrayOfEventProperty("twist", 0);
   1031 
   1032  const modifiers = _parseModifiers(aEvent, aWindow);
   1033 
   1034  const asyncOption = aEvent.asyncEnabled
   1035    ? utils.ASYNC_ENABLED
   1036    : utils.ASYNC_DISABLED;
   1037 
   1038  const args = [
   1039    idArray,
   1040    leftArray,
   1041    topArray,
   1042    rxArray,
   1043    ryArray,
   1044    angleArray,
   1045    forceArray,
   1046    tiltXArray,
   1047    tiltYArray,
   1048    twistArray,
   1049    modifiers,
   1050    asyncOption,
   1051  ];
   1052 
   1053  const sender =
   1054    aEvent.mozInputSource === "pen" ? "sendTouchEventAsPen" : "sendTouchEvent";
   1055 
   1056  if ("type" in aEvent && aEvent.type) {
   1057    return utils[sender](aEvent.type, ...args);
   1058  }
   1059 
   1060  utils[sender]("touchstart", ...args);
   1061  utils[sender]("touchend", ...args);
   1062  return false;
   1063 }
   1064 
   1065 /**
   1066 * Synthesize one or more touches at the center of your target
   1067 *
   1068 * @param {Element | Element[]} aTarget - The target element
   1069 * @param {TouchEventData} aEvent - Details of the touch event to dispatch
   1070 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
   1071 */
   1072 function synthesizeTouchAtCenter(aTarget, aEvent = {}, aWindow = window) {
   1073  var rect = aTarget.getBoundingClientRect();
   1074  synthesizeTouchAtPoint(
   1075    rect.left + rect.width / 2,
   1076    rect.top + rect.height / 2,
   1077    aEvent,
   1078    aWindow
   1079  );
   1080 }
   1081 
   1082 /**
   1083 * @typedef {object} WheelEventData
   1084 * @property {string} [aEvent.accessKey] - The character or key associated with
   1085 *     the access key event. Typically a single character used to activate a UI
   1086 *     element via keyboard shortcuts (e.g., Alt + accessKey).
   1087 * @property {boolean} [aEvent.altKey] - If set to `true`, the Alt key will be
   1088 *     considered pressed.
   1089 * @property {boolean} [aEvent.asyncEnabled] - If `true`, the event is
   1090 *     dispatched to the parent process through APZ, without being injected
   1091 *     into the OS event queue.
   1092 * @property {boolean} [aEvent.ctrlKey] - If set to `true`, the Ctrl key will
   1093 *     be considered pressed.
   1094 * @property {number} [aEvent.deltaMode=WheelEvent.DOM_DELTA_PIXEL] - Delta Mode
   1095 *     for scrolling (pixel, line, or page), which must be one of the
   1096 *     `WheelEvent.DOM_DELTA_*` constants.
   1097 * @property {number} [aEvent.deltaX=0] - Floating-point value in CSS pixels to
   1098 *     scroll in the x direction.
   1099 * @property {number} [aEvent.deltaY=0] - Floating-point value in CSS pixels to
   1100 *     scroll in the y direction.
   1101 * @property {number} [aEvent.deltaZ=0] - Floating-point value in CSS pixels to
   1102 *     scroll in the z direction.
   1103 * @property {number} [aEvent.expectedOverflowDeltaX] - Decimal value
   1104 *     indicating horizontal scroll overflow. Only the sign is checked: `0`,
   1105 *     positive, or negative.
   1106 * @property {number} [aEvent.expectedOverflowDeltaY] - Decimal value
   1107 *     indicating vertical scroll overflow. Only the sign is checked: `0`,
   1108 *     positive, or negative.
   1109 * @property {boolean} [aEvent.isCustomizedByPrefs] - If set to `true` the
   1110 *     delta values are computed from preferences.
   1111 * @property {boolean} [aEvent.isMomentum] - If set to `true` the event will be
   1112 *     caused by momentum.
   1113 * @property {boolean} [aEvent.isNoLineOrPageDelta] - If `true`, the creator
   1114 *     does not set `lineOrPageDeltaX/Y`. When a widget wheel event is
   1115 *     generated from this object, those fields will be automatically
   1116 *     calculated during dispatch by the `EventStateManager`.
   1117 * @property {number} [aEvent.lineOrPageDeltaX] - If set to a non-zero value
   1118 *      for a `DOM_DELTA_PIXEL` event, the EventStateManager will dispatch a
   1119 *     `NS_MOUSE_SCROLL` event for a horizontal scroll.
   1120 * @property {number} [aEvent.lineOrPageDeltaY] - If set to a non-zero value
   1121 *     for a `DOM_DELTA_PIXEL` event, the EventStateManager will dispatch a
   1122 *     `NS_MOUSE_SCROLL` event for a vertical scroll.
   1123 * @property {boolean} [aEvent.metaKey] - If set to `true`, the Meta key will
   1124 *     be considered pressed.
   1125 * @property {boolean} [aEvent.shiftKey] - If set to `true`, the Shift key will
   1126 *     be considered pressed.
   1127 */
   1128 
   1129 /**
   1130 * Synthesize a wheel event in `aWindow` at a point, without flushing layout.
   1131 *
   1132 * `nsIDOMWindowUtils.sendWheelEvent` takes floats for the coordinates.
   1133 * Therefore, don't round or truncate the values.
   1134 *
   1135 * @param {number} aLeft - Floating-point value for the X offset in CSS pixels.
   1136 * @param {number} aTop - Floating-point value for the Y offset in CSS pixels.
   1137 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch.
   1138 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
   1139 * @param {Function} [aCallback=null] - A callback function that is invoked when
   1140 *                                      the wheel event is dispatched.
   1141 */
   1142 function synthesizeWheelAtPoint(
   1143  aLeft,
   1144  aTop,
   1145  aEvent,
   1146  aWindow = window,
   1147  aCallback = null
   1148 ) {
   1149  var utils = _getDOMWindowUtils(aWindow);
   1150  if (!utils) {
   1151    return;
   1152  }
   1153 
   1154  var modifiers = _parseModifiers(aEvent, aWindow);
   1155  var options = 0;
   1156 
   1157  if (aEvent.isNoLineOrPageDelta) {
   1158    options |= utils.WHEEL_EVENT_CAUSED_BY_NO_LINE_OR_PAGE_DELTA_DEVICE;
   1159  }
   1160  if (aEvent.isMomentum) {
   1161    options |= utils.WHEEL_EVENT_CAUSED_BY_MOMENTUM;
   1162  }
   1163  if (aEvent.isCustomizedByPrefs) {
   1164    options |= utils.WHEEL_EVENT_CUSTOMIZED_BY_USER_PREFS;
   1165  }
   1166  if (typeof aEvent.expectedOverflowDeltaX !== "undefined") {
   1167    if (aEvent.expectedOverflowDeltaX === 0) {
   1168      options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_ZERO;
   1169    } else if (aEvent.expectedOverflowDeltaX > 0) {
   1170      options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_POSITIVE;
   1171    } else {
   1172      options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_X_NEGATIVE;
   1173    }
   1174  }
   1175  if (typeof aEvent.expectedOverflowDeltaY !== "undefined") {
   1176    if (aEvent.expectedOverflowDeltaY === 0) {
   1177      options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_ZERO;
   1178    } else if (aEvent.expectedOverflowDeltaY > 0) {
   1179      options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_POSITIVE;
   1180    } else {
   1181      options |= utils.WHEEL_EVENT_EXPECTED_OVERFLOW_DELTA_Y_NEGATIVE;
   1182    }
   1183  }
   1184  if (aEvent.asyncEnabled) {
   1185    options |= utils.WHEEL_EVENT_ASYNC_ENABLED;
   1186  }
   1187 
   1188  // Avoid the JS warnings "reference to undefined property"
   1189  if (!aEvent.deltaMode) {
   1190    aEvent.deltaMode = WheelEvent.DOM_DELTA_PIXEL;
   1191  }
   1192  if (!aEvent.deltaX) {
   1193    aEvent.deltaX = 0;
   1194  }
   1195  if (!aEvent.deltaY) {
   1196    aEvent.deltaY = 0;
   1197  }
   1198  if (!aEvent.deltaZ) {
   1199    aEvent.deltaZ = 0;
   1200  }
   1201 
   1202  var lineOrPageDeltaX =
   1203    // eslint-disable-next-line no-nested-ternary
   1204    aEvent.lineOrPageDeltaX != null
   1205      ? aEvent.lineOrPageDeltaX
   1206      : aEvent.deltaX > 0
   1207        ? Math.floor(aEvent.deltaX)
   1208        : Math.ceil(aEvent.deltaX);
   1209  var lineOrPageDeltaY =
   1210    // eslint-disable-next-line no-nested-ternary
   1211    aEvent.lineOrPageDeltaY != null
   1212      ? aEvent.lineOrPageDeltaY
   1213      : aEvent.deltaY > 0
   1214        ? Math.floor(aEvent.deltaY)
   1215        : Math.ceil(aEvent.deltaY);
   1216 
   1217  utils.sendWheelEvent(
   1218    aLeft,
   1219    aTop,
   1220    aEvent.deltaX,
   1221    aEvent.deltaY,
   1222    aEvent.deltaZ,
   1223    aEvent.deltaMode,
   1224    modifiers,
   1225    lineOrPageDeltaX,
   1226    lineOrPageDeltaY,
   1227    options,
   1228    aCallback
   1229  );
   1230 }
   1231 
   1232 /**
   1233 * Synthesize a wheel event on a target.
   1234 *
   1235 * The actual client point is determined by taking the aTarget's client box
   1236 * and offsetting it by aOffsetX and aOffsetY.
   1237 *
   1238 * @param {Element} aTarget - DOM element to dispatch the event on.
   1239 * @param {number} aOffsetX - X offset in CSS pixels from the element’s left edge.
   1240 * @param {number} aOffsetY - Y offset in CSS pixels from the element’s top edge.
   1241 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch.
   1242 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
   1243 * @param {Function} [aCallback=null] - A callback function that is invoked when
   1244 *                                      the wheel event is dispatched.
   1245 */
   1246 function synthesizeWheel(
   1247  aTarget,
   1248  aOffsetX,
   1249  aOffsetY,
   1250  aEvent,
   1251  aWindow = window,
   1252  aCallback = null
   1253 ) {
   1254  var rect = aTarget.getBoundingClientRect();
   1255  synthesizeWheelAtPoint(
   1256    rect.left + aOffsetX,
   1257    rect.top + aOffsetY,
   1258    aEvent,
   1259    aWindow,
   1260    aCallback
   1261  );
   1262 }
   1263 
   1264 const _FlushModes = {
   1265  FLUSH: 0,
   1266  NOFLUSH: 1,
   1267 };
   1268 
   1269 function _sendWheelAndPaint(
   1270  aTarget,
   1271  aOffsetX,
   1272  aOffsetY,
   1273  aEvent,
   1274  aCallback,
   1275  aFlushMode = _FlushModes.FLUSH,
   1276  aWindow = window
   1277 ) {
   1278  var utils = _getDOMWindowUtils(aWindow);
   1279  if (!utils) {
   1280    return;
   1281  }
   1282 
   1283  if (utils.isMozAfterPaintPending) {
   1284    // If a paint is pending, then APZ may be waiting for a scroll acknowledgement
   1285    // from the content thread. If we send a wheel event now, it could be ignored
   1286    // by APZ (or its scroll offset could be overridden). To avoid problems we
   1287    // just wait for the paint to complete.
   1288    aWindow.waitForAllPaintsFlushed(function () {
   1289      _sendWheelAndPaint(
   1290        aTarget,
   1291        aOffsetX,
   1292        aOffsetY,
   1293        aEvent,
   1294        aCallback,
   1295        aFlushMode,
   1296        aWindow
   1297      );
   1298    });
   1299    return;
   1300  }
   1301 
   1302  var onwheel = function () {
   1303    SpecialPowers.wrap(window).removeEventListener("wheel", onwheel, {
   1304      mozSystemGroup: true,
   1305    });
   1306 
   1307    // Wait one frame since the wheel event has not caused a refresh observer
   1308    // to be added yet.
   1309    setTimeout(function () {
   1310      utils.advanceTimeAndRefresh(1000);
   1311 
   1312      if (!aCallback) {
   1313        utils.advanceTimeAndRefresh(0);
   1314        return;
   1315      }
   1316 
   1317      var waitForPaints = function () {
   1318        SpecialPowers.Services.obs.removeObserver(
   1319          waitForPaints,
   1320          "apz-repaints-flushed"
   1321        );
   1322        aWindow.waitForAllPaintsFlushed(function () {
   1323          utils.restoreNormalRefresh();
   1324          aCallback();
   1325        });
   1326      };
   1327 
   1328      SpecialPowers.Services.obs.addObserver(
   1329        waitForPaints,
   1330        "apz-repaints-flushed"
   1331      );
   1332      if (!utils.flushApzRepaints()) {
   1333        waitForPaints();
   1334      }
   1335    }, 0);
   1336  };
   1337 
   1338  // Listen for the system wheel event, because it happens after all of
   1339  // the other wheel events, including legacy events.
   1340  SpecialPowers.wrap(aWindow).addEventListener("wheel", onwheel, {
   1341    mozSystemGroup: true,
   1342  });
   1343  if (aFlushMode === _FlushModes.FLUSH) {
   1344    synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
   1345  } else {
   1346    synthesizeWheelAtPoint(aOffsetX, aOffsetY, aEvent, aWindow);
   1347  }
   1348 }
   1349 
   1350 /**
   1351 * Wrapper around synthesizeWheel that waits for the wheel event to be
   1352 * dispatched and for any resulting layout and paint operations to flush.
   1353 *
   1354 * Requires including `paint_listener.js`. Tests using this function must call
   1355 * `DOMWindowUtils.restoreNormalRefresh()` before finishing.
   1356 *
   1357 * @param {Element} aTarget - DOM element to dispatch the event on.
   1358 * @param {number} aOffsetX - X offset in CSS pixels from the element’s left edge.
   1359 * @param {number} aOffsetY - Y offset in CSS pixels from the element’s top edge.
   1360 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch.
   1361 * @param {Function} [aCallback] - Called after paint flush, if provided. If not,
   1362 *     the caller is expected to handle scroll completion manually. In this case,
   1363 *     the refresh driver will not be restored automatically.
   1364 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
   1365 */
   1366 function sendWheelAndPaint(
   1367  aTarget,
   1368  aOffsetX,
   1369  aOffsetY,
   1370  aEvent,
   1371  aCallback,
   1372  aWindow = window
   1373 ) {
   1374  _sendWheelAndPaint(
   1375    aTarget,
   1376    aOffsetX,
   1377    aOffsetY,
   1378    aEvent,
   1379    aCallback,
   1380    _FlushModes.FLUSH,
   1381    aWindow
   1382  );
   1383 }
   1384 
   1385 /**
   1386 * Similar to `sendWheelAndPaint()`, but skips layout flush when resolving
   1387 * `aTarget`'s position in `aWindow` before dispatching the wheel event.
   1388 *
   1389 * @param {Element} aTarget - DOM element to dispatch the event on.
   1390 * @param {number} aOffsetX - X offset in CSS pixels from the `aWindow`’s left edge.
   1391 * @param {number} aOffsetY - Y offset in CSS pixels from the `aWindow`’s top edge.
   1392 * @param {WheelEventData} aEvent - Details of the wheel event to dispatch.
   1393 * @param {Function} [aCallback] - Called after paint, if provided. If not,
   1394 *     the caller is expected to handle scroll completion manually. In this case,
   1395 *     the refresh driver will not be restored automatically.
   1396 * @param {DOMWindow} [aWindow=window] - DOM window used to dispatch the event.
   1397 */
   1398 function sendWheelAndPaintNoFlush(
   1399  aTarget,
   1400  aOffsetX,
   1401  aOffsetY,
   1402  aEvent,
   1403  aCallback,
   1404  aWindow = window
   1405 ) {
   1406  _sendWheelAndPaint(
   1407    aTarget,
   1408    aOffsetX,
   1409    aOffsetY,
   1410    aEvent,
   1411    aCallback,
   1412    _FlushModes.NOFLUSH,
   1413    aWindow
   1414  );
   1415 }
   1416 
   1417 function synthesizeNativeTapAtCenter(
   1418  aTarget,
   1419  aLongTap = false,
   1420  aCallback = null,
   1421  aWindow = window
   1422 ) {
   1423  let rect = aTarget.getBoundingClientRect();
   1424  return synthesizeNativeTap(
   1425    aTarget,
   1426    rect.width / 2,
   1427    rect.height / 2,
   1428    aLongTap,
   1429    aCallback,
   1430    aWindow
   1431  );
   1432 }
   1433 
   1434 function synthesizeNativeTap(
   1435  aTarget,
   1436  aOffsetX,
   1437  aOffsetY,
   1438  aLongTap = false,
   1439  aCallback = null,
   1440  aWindow = window
   1441 ) {
   1442  let utils = _getDOMWindowUtils(aWindow);
   1443  if (!utils) {
   1444    return;
   1445  }
   1446 
   1447  let scale = aWindow.devicePixelRatio;
   1448  let rect = aTarget.getBoundingClientRect();
   1449  let x = _EU_roundDevicePixels(
   1450    (aWindow.mozInnerScreenX + rect.left + aOffsetX) * scale
   1451  );
   1452  let y = _EU_roundDevicePixels(
   1453    (aWindow.mozInnerScreenY + rect.top + aOffsetY) * scale
   1454  );
   1455 
   1456  utils.sendNativeTouchTap(x, y, aLongTap, aCallback);
   1457 }
   1458 
   1459 /**
   1460 * Similar to synthesizeMouse but generates a native widget level event
   1461 * (so will actually move the "real" mouse cursor etc. Be careful because
   1462 * this can impact later code as well! (e.g. with hover states etc.)
   1463 *
   1464 * @description There are 3 mutually exclusive ways of indicating the location of the
   1465 * mouse event: set ``atCenter``, or pass ``offsetX`` and ``offsetY``,
   1466 * or pass ``screenX`` and ``screenY``. Do not attempt to mix these.
   1467 *
   1468 * @param {object} aParams
   1469 * @param {string} aParams.type "click", "mousedown", "mouseup" or "mousemove"
   1470 * @param {Element} aParams.target Origin of offsetX and offsetY, must be an element
   1471 * @param {boolean} [aParams.atCenter]
   1472 *        Instead of offsetX/Y, synthesize the event at center of `target`.
   1473 * @param {number} [aParams.offsetX]
   1474 *        X offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
   1475 * @param {number} [aParams.offsetY]
   1476 *        Y offset in `target` (in CSS pixels if `scale` is "screenPixelsPerCSSPixel")
   1477 * @param {number} [aParams.screenX]
   1478 *        X offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
   1479 *        Neither offsetX/Y nor atCenter must be set if this is set.
   1480 * @param {number} [aParams.screenY]
   1481 *        Y offset in screen (in CSS pixels if `scale` is "screenPixelsPerCSSPixel"),
   1482 *        Neither offsetX/Y nor atCenter must be set if this is set.
   1483 * @param {string} [aParams.scale="screenPixelsPerCSSPixel"]
   1484 *        If scale is "screenPixelsPerCSSPixel", devicePixelRatio will be used.
   1485 *        If scale is "inScreenPixels", clientX/Y nor scaleX/Y are not adjusted with screenPixelsPerCSSPixel.
   1486 * @param {number} [aParams.button=0]
   1487 *        Defaults to 0, if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
   1488 * @param {object} [aParams.modifiers={}]
   1489 *        Active modifiers, see `_parseNativeModifiers`
   1490 * @param {DOMWindow} [aParams.win=window]
   1491 *        The window to use its utils. Defaults to the window in which EventUtils.js is running.
   1492 * @param {Element} [aParams.elementOnWidget=target]
   1493 *        Defaults to target. If element under the point is in another widget from target's widget,
   1494 *        e.g., when it's in a XUL <panel>, specify this.
   1495 */
   1496 function synthesizeNativeMouseEvent(aParams, aCallback = null) {
   1497  const {
   1498    type,
   1499    target,
   1500    offsetX,
   1501    offsetY,
   1502    atCenter,
   1503    screenX,
   1504    screenY,
   1505    scale = "screenPixelsPerCSSPixel",
   1506    button = 0,
   1507    modifiers = {},
   1508    win = window,
   1509    elementOnWidget = target,
   1510  } = aParams;
   1511  if (atCenter) {
   1512    if (offsetX != undefined || offsetY != undefined) {
   1513      throw Error(
   1514        `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
   1515      );
   1516    }
   1517    if (screenX != undefined || screenY != undefined) {
   1518      throw Error(
   1519        `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
   1520      );
   1521    }
   1522    if (!target) {
   1523      throw Error("atCenter is specified, but target is not specified");
   1524    }
   1525  } else if (offsetX != undefined && offsetY != undefined) {
   1526    if (screenX != undefined || screenY != undefined) {
   1527      throw Error(
   1528        `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
   1529      );
   1530    }
   1531    if (!target) {
   1532      throw Error(
   1533        "offsetX and offsetY are specified, but target is not specified"
   1534      );
   1535    }
   1536  } else if (screenX != undefined && screenY != undefined) {
   1537    if (offsetX != undefined || offsetY != undefined) {
   1538      throw Error(
   1539        `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
   1540      );
   1541    }
   1542  }
   1543  const utils = _getDOMWindowUtils(win);
   1544  if (!utils) {
   1545    return;
   1546  }
   1547 
   1548  const rect = target?.getBoundingClientRect();
   1549  const resolution = _getTopWindowResolution(win);
   1550  const scaleValue = (() => {
   1551    if (scale === "inScreenPixels") {
   1552      return 1.0;
   1553    }
   1554    if (scale === "screenPixelsPerCSSPixel") {
   1555      return win.devicePixelRatio;
   1556    }
   1557    throw Error(`invalid scale value (${scale}) is specified`);
   1558  })();
   1559  // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
   1560  //     so use window.top's mozInnerScreen. But this won't work fission+xorigin
   1561  //     with mobile viewport until mozInnerScreen returns valid value with
   1562  //     scale.
   1563  const x = _EU_roundDevicePixels(
   1564    (() => {
   1565      if (screenX != undefined) {
   1566        return screenX * scaleValue;
   1567      }
   1568      const winInnerOffsetX = _getScreenXInUnscaledCSSPixels(win);
   1569      return (
   1570        (((atCenter ? rect.width / 2 : offsetX) + rect.left) * resolution +
   1571          winInnerOffsetX) *
   1572        scaleValue
   1573      );
   1574    })()
   1575  );
   1576  const y = _EU_roundDevicePixels(
   1577    (() => {
   1578      if (screenY != undefined) {
   1579        return screenY * scaleValue;
   1580      }
   1581      const winInnerOffsetY = _getScreenYInUnscaledCSSPixels(win);
   1582      return (
   1583        (((atCenter ? rect.height / 2 : offsetY) + rect.top) * resolution +
   1584          winInnerOffsetY) *
   1585        scaleValue
   1586      );
   1587    })()
   1588  );
   1589  const modifierFlags = _parseNativeModifiers(modifiers);
   1590 
   1591  if (type === "click") {
   1592    utils.sendNativeMouseEvent(
   1593      x,
   1594      y,
   1595      utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
   1596      button,
   1597      modifierFlags,
   1598      elementOnWidget,
   1599      function () {
   1600        utils.sendNativeMouseEvent(
   1601          x,
   1602          y,
   1603          utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
   1604          button,
   1605          modifierFlags,
   1606          elementOnWidget,
   1607          aCallback
   1608        );
   1609      }
   1610    );
   1611    return;
   1612  }
   1613  utils.sendNativeMouseEvent(
   1614    x,
   1615    y,
   1616    (() => {
   1617      switch (type) {
   1618        case "mousedown":
   1619          return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
   1620        case "mouseup":
   1621          return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
   1622        case "mousemove":
   1623          return utils.NATIVE_MOUSE_MESSAGE_MOVE;
   1624        default:
   1625          throw Error(`Invalid type is specified: ${type}`);
   1626      }
   1627    })(),
   1628    button,
   1629    modifierFlags,
   1630    elementOnWidget,
   1631    aCallback
   1632  );
   1633 }
   1634 
   1635 function promiseNativeMouseEvent(aParams) {
   1636  return new Promise(resolve => synthesizeNativeMouseEvent(aParams, resolve));
   1637 }
   1638 
   1639 function synthesizeNativeMouseEventAndWaitForEvent(aParams, aCallback) {
   1640  const listener = aParams.eventTargetToListen || aParams.target;
   1641  const eventType = aParams.eventTypeToWait || aParams.type;
   1642  listener.addEventListener(eventType, aCallback, {
   1643    capture: true,
   1644    once: true,
   1645  });
   1646  synthesizeNativeMouseEvent(aParams);
   1647 }
   1648 
   1649 function promiseNativeMouseEventAndWaitForEvent(aParams) {
   1650  return new Promise(resolve =>
   1651    synthesizeNativeMouseEventAndWaitForEvent(aParams, resolve)
   1652  );
   1653 }
   1654 
   1655 /**
   1656 * This is a wrapper around synthesizeNativeMouseEvent that waits for the mouse
   1657 * event to be dispatched to the target content.
   1658 *
   1659 * This API is supposed to be used in those test cases that synthesize some
   1660 * input events to chrome process and have some checks in content.
   1661 */
   1662 function synthesizeAndWaitNativeMouseMove(
   1663  aTarget,
   1664  aOffsetX,
   1665  aOffsetY,
   1666  aCallback,
   1667  aWindow = window
   1668 ) {
   1669  let browser = gBrowser.selectedTab.linkedBrowser;
   1670  let mm = browser.messageManager;
   1671  let { ContentTask } = _EU_ChromeUtils.importESModule(
   1672    "resource://testing-common/ContentTask.sys.mjs"
   1673  );
   1674 
   1675  let eventRegisteredPromise = new Promise(resolve => {
   1676    mm.addMessageListener("Test:MouseMoveRegistered", function processed() {
   1677      mm.removeMessageListener("Test:MouseMoveRegistered", processed);
   1678      resolve();
   1679    });
   1680  });
   1681  let eventReceivedPromise = ContentTask.spawn(
   1682    browser,
   1683    [aOffsetX, aOffsetY],
   1684    ([clientX, clientY]) => {
   1685      return new Promise(resolve => {
   1686        addEventListener("mousemove", function onMouseMoveEvent(e) {
   1687          if (e.clientX == clientX && e.clientY == clientY) {
   1688            removeEventListener("mousemove", onMouseMoveEvent);
   1689            resolve();
   1690          }
   1691        });
   1692        sendAsyncMessage("Test:MouseMoveRegistered");
   1693      });
   1694    }
   1695  );
   1696  eventRegisteredPromise.then(() => {
   1697    synthesizeNativeMouseEvent({
   1698      type: "mousemove",
   1699      target: aTarget,
   1700      offsetX: aOffsetX,
   1701      offsetY: aOffsetY,
   1702      win: aWindow,
   1703    });
   1704  });
   1705  return eventReceivedPromise;
   1706 }
   1707 
   1708 /**
   1709 * Synthesize a key event. It is targeted at whatever would be targeted by an
   1710 * actual keypress by the user, typically the focused element.
   1711 *
   1712 * @param {string} aKey
   1713 *        Should be either:
   1714 *
   1715 *        - key value (recommended).  If you specify a non-printable key name,
   1716 *          prepend the ``KEY_`` prefix.  Otherwise, specifying a printable key, the
   1717 *          key value should be specified.
   1718 *
   1719 *        - keyCode name starting with ``VK_`` (e.g., ``VK_RETURN``).  This is available
   1720 *          only for compatibility with legacy API.  Don't use this with new tests.
   1721 *
   1722 * @param {object} [aEvent]
   1723 *        Optional event object with more specifics about the key event to
   1724 *        synthesize.
   1725 * @param {string} [aEvent.code]
   1726 *        If you don't specify this explicitly, it'll be guessed from aKey
   1727 *        of US keyboard layout.  Note that this value may be different
   1728 *        between browsers.  For example, "Insert" is never set only on
   1729 *        macOS since actual key operation won't cause this code value.
   1730 *        In such case, the value becomes empty string.
   1731 *        If you need to emulate non-US keyboard layout or virtual keyboard
   1732 *        which doesn't emulate hardware key input, you should set this value
   1733 *        to empty string explicitly.
   1734 * @param {number} [aEvent.repeat]
   1735 *        If you emulate auto-repeat, you should set the count of repeat.
   1736 *        This method will automatically synthesize keydown (and keypress).
   1737 * @param {*} aEvent.location
   1738 *        If you want to specify this, you can specify this explicitly.
   1739 *        However, if you don't specify this value, it will be computed
   1740 *        from code value.
   1741 * @param {string} aEvent.type
   1742 *        Basically, you shouldn't specify this.  Then, this function will
   1743 *        synthesize keydown (, keypress) and keyup.
   1744 *        If keydown is specified, this only fires keydown (and keypress if
   1745 *        it should be fired).
   1746 *        If keyup is specified, this only fires keyup.
   1747 * @param {number} aEvent.keyCode
   1748 *        Must be 0 - 255 (0xFF). If this is specified explicitly,
   1749 *        .keyCode value is initialized with this value.
   1750 * @param {DOMWindow} [aWindow=window]
   1751 *        DOM window used to dispatch the event.
   1752 * @param {Function} aCallback
   1753 *        Is optional and can be used to receive notifications from TIP.
   1754 *
   1755 * @description
   1756 * ``accelKey``, ``altKey``, ``altGraphKey``, ``ctrlKey``, ``capsLockKey``,
   1757 * ``fnKey``, ``fnLockKey``, ``numLockKey``, ``metaKey``, ``scrollLockKey``,
   1758 * ``shiftKey``, ``symbolKey``, ``symbolLockKey``
   1759 * Basically, you shouldn't use these attributes.  nsITextInputProcessor
   1760 * manages modifier key state when you synthesize modifier key events.
   1761 * However, if some of these attributes are true, this function activates
   1762 * the modifiers only during dispatching the key events.
   1763 * Note that if some of these values are false, they are ignored (i.e.,
   1764 * not inactivated with this function).
   1765 */
   1766 function synthesizeKey(aKey, aEvent = undefined, aWindow = window, aCallback) {
   1767  const event = aEvent === undefined || aEvent === null ? {} : aEvent;
   1768  let dispatchKeydown =
   1769    !("type" in event) || event.type === "keydown" || !event.type;
   1770  const dispatchKeyup =
   1771    !("type" in event) || event.type === "keyup" || !event.type;
   1772 
   1773  if (dispatchKeydown && aKey == "KEY_Escape") {
   1774    let eventForKeydown = Object.assign({}, JSON.parse(JSON.stringify(event)));
   1775    eventForKeydown.type = "keydown";
   1776    if (
   1777      _maybeEndDragSession(
   1778        // TODO: We should set the last dragover point instead
   1779        0,
   1780        0,
   1781        eventForKeydown,
   1782        aWindow
   1783      )
   1784    ) {
   1785      if (!dispatchKeyup) {
   1786        return;
   1787      }
   1788      // We don't need to dispatch only keydown event because it's consumed by
   1789      // the drag session.
   1790      dispatchKeydown = false;
   1791    }
   1792  }
   1793 
   1794  var TIP = _getTIP(aWindow, aCallback);
   1795  if (!TIP) {
   1796    return;
   1797  }
   1798  var KeyboardEvent = _getKeyboardEvent(aWindow);
   1799  var modifiers = _emulateToActivateModifiers(TIP, event, aWindow);
   1800  var keyEventDict = _createKeyboardEventDictionary(aKey, event, TIP, aWindow);
   1801  var keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
   1802 
   1803  try {
   1804    if (dispatchKeydown) {
   1805      TIP.keydown(keyEvent, keyEventDict.flags);
   1806      if ("repeat" in event && event.repeat > 1) {
   1807        keyEventDict.dictionary.repeat = true;
   1808        var repeatedKeyEvent = new KeyboardEvent("", keyEventDict.dictionary);
   1809        for (var i = 1; i < event.repeat; i++) {
   1810          TIP.keydown(repeatedKeyEvent, keyEventDict.flags);
   1811        }
   1812      }
   1813    }
   1814    if (dispatchKeyup) {
   1815      TIP.keyup(keyEvent, keyEventDict.flags);
   1816    }
   1817  } finally {
   1818    _emulateToInactivateModifiers(TIP, modifiers, aWindow);
   1819  }
   1820 }
   1821 
   1822 /**
   1823 * This is a wrapper around synthesizeKey that waits for the key event to be
   1824 * dispatched to the target content. It returns a promise which is resolved
   1825 * when the content receives the key event.
   1826 *
   1827 * This API is supposed to be used in those test cases that synthesize some
   1828 * input events to chrome process and have some checks in content.
   1829 */
   1830 function synthesizeAndWaitKey(
   1831  aKey,
   1832  aEvent,
   1833  aWindow = window,
   1834  checkBeforeSynthesize,
   1835  checkAfterSynthesize
   1836 ) {
   1837  let browser = gBrowser.selectedTab.linkedBrowser;
   1838  let mm = browser.messageManager;
   1839  let keyCode = _createKeyboardEventDictionary(aKey, aEvent, null, aWindow)
   1840    .dictionary.keyCode;
   1841  let { ContentTask } = _EU_ChromeUtils.importESModule(
   1842    "resource://testing-common/ContentTask.sys.mjs"
   1843  );
   1844 
   1845  let keyRegisteredPromise = new Promise(resolve => {
   1846    mm.addMessageListener("Test:KeyRegistered", function processed() {
   1847      mm.removeMessageListener("Test:KeyRegistered", processed);
   1848      resolve();
   1849    });
   1850  });
   1851  // eslint-disable-next-line no-shadow
   1852  let keyReceivedPromise = ContentTask.spawn(browser, keyCode, keyCode => {
   1853    return new Promise(resolve => {
   1854      addEventListener("keyup", function onKeyEvent(e) {
   1855        if (e.keyCode == keyCode) {
   1856          removeEventListener("keyup", onKeyEvent);
   1857          resolve();
   1858        }
   1859      });
   1860      sendAsyncMessage("Test:KeyRegistered");
   1861    });
   1862  });
   1863  keyRegisteredPromise.then(() => {
   1864    if (checkBeforeSynthesize) {
   1865      checkBeforeSynthesize();
   1866    }
   1867    synthesizeKey(aKey, aEvent, aWindow);
   1868    if (checkAfterSynthesize) {
   1869      checkAfterSynthesize();
   1870    }
   1871  });
   1872  return keyReceivedPromise;
   1873 }
   1874 
   1875 function _parseNativeModifiers(aModifiers, aWindow = window) {
   1876  let modifiers = 0;
   1877  if (aModifiers.capsLockKey) {
   1878    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
   1879  }
   1880  if (aModifiers.numLockKey) {
   1881    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
   1882  }
   1883  if (aModifiers.shiftKey) {
   1884    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
   1885  }
   1886  if (aModifiers.shiftRightKey) {
   1887    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
   1888  }
   1889  if (aModifiers.ctrlKey) {
   1890    modifiers |=
   1891      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
   1892  }
   1893  if (aModifiers.ctrlRightKey) {
   1894    modifiers |=
   1895      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
   1896  }
   1897  if (aModifiers.altKey) {
   1898    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
   1899  }
   1900  if (aModifiers.altRightKey) {
   1901    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
   1902  }
   1903  if (aModifiers.metaKey) {
   1904    modifiers |=
   1905      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
   1906  }
   1907  if (aModifiers.metaRightKey) {
   1908    modifiers |=
   1909      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
   1910  }
   1911  if (aModifiers.helpKey) {
   1912    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
   1913  }
   1914  if (aModifiers.fnKey) {
   1915    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
   1916  }
   1917  if (aModifiers.numericKeyPadKey) {
   1918    modifiers |=
   1919      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
   1920  }
   1921 
   1922  if (aModifiers.accelKey) {
   1923    modifiers |= _EU_isMac(aWindow)
   1924      ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
   1925      : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
   1926  }
   1927  if (aModifiers.accelRightKey) {
   1928    modifiers |= _EU_isMac(aWindow)
   1929      ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
   1930      : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
   1931  }
   1932  if (aModifiers.altGrKey) {
   1933    modifiers |= _EU_isMac(aWindow)
   1934      ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
   1935      : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
   1936  }
   1937  return modifiers;
   1938 }
   1939 
   1940 // Mac: Any unused number is okay for adding new keyboard layout.
   1941 //      When you add new keyboard layout here, you need to modify
   1942 //      TISInputSourceWrapper::InitByLayoutID().
   1943 // Win: These constants can be found by inspecting registry keys under
   1944 //      HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Keyboard Layouts
   1945 
   1946 const KEYBOARD_LAYOUT_ARABIC = {
   1947  name: "Arabic",
   1948  Mac: 6,
   1949  Win: 0x00000401,
   1950  hasAltGrOnWin: false,
   1951 };
   1952 _defineConstant("KEYBOARD_LAYOUT_ARABIC", KEYBOARD_LAYOUT_ARABIC);
   1953 const KEYBOARD_LAYOUT_ARABIC_PC = {
   1954  name: "Arabic - PC",
   1955  Mac: 7,
   1956  Win: null,
   1957  hasAltGrOnWin: false,
   1958 };
   1959 _defineConstant("KEYBOARD_LAYOUT_ARABIC_PC", KEYBOARD_LAYOUT_ARABIC_PC);
   1960 const KEYBOARD_LAYOUT_BRAZILIAN_ABNT = {
   1961  name: "Brazilian ABNT",
   1962  Mac: null,
   1963  Win: 0x00000416,
   1964  hasAltGrOnWin: true,
   1965 };
   1966 _defineConstant(
   1967  "KEYBOARD_LAYOUT_BRAZILIAN_ABNT",
   1968  KEYBOARD_LAYOUT_BRAZILIAN_ABNT
   1969 );
   1970 const KEYBOARD_LAYOUT_DVORAK_QWERTY = {
   1971  name: "Dvorak-QWERTY",
   1972  Mac: 4,
   1973  Win: null,
   1974  hasAltGrOnWin: false,
   1975 };
   1976 _defineConstant("KEYBOARD_LAYOUT_DVORAK_QWERTY", KEYBOARD_LAYOUT_DVORAK_QWERTY);
   1977 const KEYBOARD_LAYOUT_EN_US = {
   1978  name: "US",
   1979  Mac: 0,
   1980  Win: 0x00000409,
   1981  hasAltGrOnWin: false,
   1982 };
   1983 _defineConstant("KEYBOARD_LAYOUT_EN_US", KEYBOARD_LAYOUT_EN_US);
   1984 const KEYBOARD_LAYOUT_FRENCH = {
   1985  name: "French",
   1986  Mac: 8, // Some keys mapped different from PC, e.g., Digit6, Digit8, Equal, Slash and Backslash
   1987  Win: 0x0000040c,
   1988  hasAltGrOnWin: true,
   1989 };
   1990 _defineConstant("KEYBOARD_LAYOUT_FRENCH", KEYBOARD_LAYOUT_FRENCH);
   1991 const KEYBOARD_LAYOUT_FRENCH_PC = {
   1992  name: "French-PC",
   1993  Mac: 13, // Compatible with Windows
   1994  Win: 0x0000040c,
   1995  hasAltGrOnWin: true,
   1996 };
   1997 _defineConstant("KEYBOARD_LAYOUT_FRENCH_PC", KEYBOARD_LAYOUT_FRENCH_PC);
   1998 const KEYBOARD_LAYOUT_GREEK = {
   1999  name: "Greek",
   2000  Mac: 1,
   2001  Win: 0x00000408,
   2002  hasAltGrOnWin: true,
   2003 };
   2004 _defineConstant("KEYBOARD_LAYOUT_GREEK", KEYBOARD_LAYOUT_GREEK);
   2005 const KEYBOARD_LAYOUT_GERMAN = {
   2006  name: "German",
   2007  Mac: 2,
   2008  Win: 0x00000407,
   2009  hasAltGrOnWin: true,
   2010 };
   2011 _defineConstant("KEYBOARD_LAYOUT_GERMAN", KEYBOARD_LAYOUT_GERMAN);
   2012 const KEYBOARD_LAYOUT_HEBREW = {
   2013  name: "Hebrew",
   2014  Mac: 9,
   2015  Win: 0x0000040d,
   2016  hasAltGrOnWin: true,
   2017 };
   2018 _defineConstant("KEYBOARD_LAYOUT_HEBREW", KEYBOARD_LAYOUT_HEBREW);
   2019 const KEYBOARD_LAYOUT_JAPANESE = {
   2020  name: "Japanese",
   2021  Mac: null,
   2022  Win: 0x00000411,
   2023  hasAltGrOnWin: false,
   2024 };
   2025 _defineConstant("KEYBOARD_LAYOUT_JAPANESE", KEYBOARD_LAYOUT_JAPANESE);
   2026 const KEYBOARD_LAYOUT_KHMER = {
   2027  name: "Khmer",
   2028  Mac: null,
   2029  Win: 0x00000453,
   2030  hasAltGrOnWin: true,
   2031 }; // available on Win7 or later.
   2032 _defineConstant("KEYBOARD_LAYOUT_KHMER", KEYBOARD_LAYOUT_KHMER);
   2033 const KEYBOARD_LAYOUT_LITHUANIAN = {
   2034  name: "Lithuanian",
   2035  Mac: 10,
   2036  Win: 0x00010427,
   2037  hasAltGrOnWin: true,
   2038 };
   2039 _defineConstant("KEYBOARD_LAYOUT_LITHUANIAN", KEYBOARD_LAYOUT_LITHUANIAN);
   2040 const KEYBOARD_LAYOUT_NORWEGIAN = {
   2041  name: "Norwegian",
   2042  Mac: 11,
   2043  Win: 0x00000414,
   2044  hasAltGrOnWin: true,
   2045 };
   2046 _defineConstant("KEYBOARD_LAYOUT_NORWEGIAN", KEYBOARD_LAYOUT_NORWEGIAN);
   2047 const KEYBOARD_LAYOUT_RUSSIAN = {
   2048  name: "Russian",
   2049  Mac: null,
   2050  Win: 0x00000419,
   2051  hasAltGrOnWin: true, // No AltGr, but Ctrl + Alt + Digit8 introduces a char
   2052 };
   2053 _defineConstant("KEYBOARD_LAYOUT_RUSSIAN", KEYBOARD_LAYOUT_RUSSIAN);
   2054 const KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC = {
   2055  name: "Russian - Mnemonic",
   2056  Mac: null,
   2057  Win: 0x00020419,
   2058  hasAltGrOnWin: true,
   2059 }; // available on Win8 or later.
   2060 _defineConstant(
   2061  "KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC",
   2062  KEYBOARD_LAYOUT_RUSSIAN_MNEMONIC
   2063 );
   2064 const KEYBOARD_LAYOUT_SPANISH = {
   2065  name: "Spanish",
   2066  Mac: 12,
   2067  Win: 0x0000040a,
   2068  hasAltGrOnWin: true,
   2069 };
   2070 _defineConstant("KEYBOARD_LAYOUT_SPANISH", KEYBOARD_LAYOUT_SPANISH);
   2071 const KEYBOARD_LAYOUT_SWEDISH = {
   2072  name: "Swedish",
   2073  Mac: 3,
   2074  Win: 0x0000041d,
   2075  hasAltGrOnWin: true,
   2076 };
   2077 _defineConstant("KEYBOARD_LAYOUT_SWEDISH", KEYBOARD_LAYOUT_SWEDISH);
   2078 const KEYBOARD_LAYOUT_THAI = {
   2079  name: "Thai",
   2080  Mac: 5,
   2081  Win: 0x0002041e,
   2082  hasAltGrOnWin: false,
   2083 };
   2084 _defineConstant("KEYBOARD_LAYOUT_THAI", KEYBOARD_LAYOUT_THAI);
   2085 
   2086 /**
   2087 * synthesizeNativeKey() dispatches native key event on active window.
   2088 * This is implemented only on Windows and Mac. Note that this function
   2089 * dispatches the key event asynchronously and returns immediately. If a
   2090 * callback function is provided, the callback will be called upon
   2091 * completion of the key dispatch.
   2092 *
   2093 * @param aKeyboardLayout       One of KEYBOARD_LAYOUT_* defined above.
   2094 * @param aNativeKeyCode        A native keycode value defined in
   2095 *                              NativeKeyCodes.js.
   2096 * @param aModifiers            Modifier keys.  If no modifire key is pressed,
   2097 *                              this must be {}.  Otherwise, one or more items
   2098 *                              referred in _parseNativeModifiers() must be
   2099 *                              true.
   2100 * @param aChars                Specify characters which should be generated
   2101 *                              by the key event.
   2102 * @param aUnmodifiedChars      Specify characters of unmodified (except Shift)
   2103 *                              aChar value.
   2104 * @param aCallback             If provided, this callback will be invoked
   2105 *                              once the native keys have been processed
   2106 *                              by Gecko. Will never be called if this
   2107 *                              function returns false.
   2108 * @return                      True if this function succeed dispatching
   2109 *                              native key event.  Otherwise, false.
   2110 */
   2111 
   2112 function synthesizeNativeKey(
   2113  aKeyboardLayout,
   2114  aNativeKeyCode,
   2115  aModifiers,
   2116  aChars,
   2117  aUnmodifiedChars,
   2118  aCallback,
   2119  aWindow = window
   2120 ) {
   2121  var utils = _getDOMWindowUtils(aWindow);
   2122  if (!utils) {
   2123    return false;
   2124  }
   2125  var nativeKeyboardLayout = null;
   2126  if (_EU_isMac(aWindow)) {
   2127    nativeKeyboardLayout = aKeyboardLayout.Mac;
   2128  } else if (_EU_isWin(aWindow)) {
   2129    nativeKeyboardLayout = aKeyboardLayout.Win;
   2130  }
   2131  if (nativeKeyboardLayout === null) {
   2132    return false;
   2133  }
   2134 
   2135  utils.sendNativeKeyEvent(
   2136    nativeKeyboardLayout,
   2137    aNativeKeyCode,
   2138    _parseNativeModifiers(aModifiers, aWindow),
   2139    aChars,
   2140    aUnmodifiedChars,
   2141    aCallback
   2142  );
   2143  return true;
   2144 }
   2145 
   2146 var _gSeenEvent = false;
   2147 
   2148 /**
   2149 * Indicate that an event with an original target of aExpectedTarget and
   2150 * a type of aExpectedEvent is expected to be fired, or not expected to
   2151 * be fired.
   2152 */
   2153 function _expectEvent(aExpectedTarget, aExpectedEvent, aTestName) {
   2154  if (!aExpectedTarget || !aExpectedEvent) {
   2155    return null;
   2156  }
   2157 
   2158  _gSeenEvent = false;
   2159 
   2160  var type =
   2161    aExpectedEvent.charAt(0) == "!"
   2162      ? aExpectedEvent.substring(1)
   2163      : aExpectedEvent;
   2164  var eventHandler = function (event) {
   2165    var epassed =
   2166      !_gSeenEvent &&
   2167      event.originalTarget == aExpectedTarget &&
   2168      event.type == type;
   2169    is(
   2170      epassed,
   2171      true,
   2172      aTestName + " " + type + " event target " + (_gSeenEvent ? "twice" : "")
   2173    );
   2174    _gSeenEvent = true;
   2175  };
   2176 
   2177  aExpectedTarget.addEventListener(type, eventHandler);
   2178  return eventHandler;
   2179 }
   2180 
   2181 /**
   2182 * Check if the event was fired or not. The event handler aEventHandler
   2183 * will be removed.
   2184 */
   2185 function _checkExpectedEvent(
   2186  aExpectedTarget,
   2187  aExpectedEvent,
   2188  aEventHandler,
   2189  aTestName
   2190 ) {
   2191  if (aEventHandler) {
   2192    var expectEvent = aExpectedEvent.charAt(0) != "!";
   2193    var type = expectEvent ? aExpectedEvent : aExpectedEvent.substring(1);
   2194    aExpectedTarget.removeEventListener(type, aEventHandler);
   2195    var desc = type + " event";
   2196    if (!expectEvent) {
   2197      desc += " not";
   2198    }
   2199    is(_gSeenEvent, expectEvent, aTestName + " " + desc + " fired");
   2200  }
   2201 
   2202  _gSeenEvent = false;
   2203 }
   2204 
   2205 /**
   2206 * Similar to synthesizeMouse except that a test is performed to see if an
   2207 * event is fired at the right target as a result.
   2208 *
   2209 * aExpectedTarget - the expected originalTarget of the event.
   2210 * aExpectedEvent - the expected type of the event, such as 'select'.
   2211 * aTestName - the test name when outputing results
   2212 *
   2213 * To test that an event is not fired, use an expected type preceded by an
   2214 * exclamation mark, such as '!select'. This might be used to test that a
   2215 * click on a disabled element doesn't fire certain events for instance.
   2216 *
   2217 * aWindow is optional, and defaults to the current window object.
   2218 */
   2219 function synthesizeMouseExpectEvent(
   2220  aTarget,
   2221  aOffsetX,
   2222  aOffsetY,
   2223  aEvent,
   2224  aExpectedTarget,
   2225  aExpectedEvent,
   2226  aTestName,
   2227  aWindow
   2228 ) {
   2229  var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
   2230  synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow);
   2231  _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
   2232 }
   2233 
   2234 /**
   2235 * Similar to synthesizeKey except that a test is performed to see if an
   2236 * event is fired at the right target as a result.
   2237 *
   2238 * aExpectedTarget - the expected originalTarget of the event.
   2239 * aExpectedEvent - the expected type of the event, such as 'select'.
   2240 * aTestName - the test name when outputing results
   2241 *
   2242 * To test that an event is not fired, use an expected type preceded by an
   2243 * exclamation mark, such as '!select'.
   2244 *
   2245 * aWindow is optional, and defaults to the current window object.
   2246 */
   2247 function synthesizeKeyExpectEvent(
   2248  key,
   2249  aEvent,
   2250  aExpectedTarget,
   2251  aExpectedEvent,
   2252  aTestName,
   2253  aWindow
   2254 ) {
   2255  var eventHandler = _expectEvent(aExpectedTarget, aExpectedEvent, aTestName);
   2256  synthesizeKey(key, aEvent, aWindow);
   2257  _checkExpectedEvent(aExpectedTarget, aExpectedEvent, eventHandler, aTestName);
   2258 }
   2259 
   2260 function disableNonTestMouseEvents(aDisable) {
   2261  var domutils = _getDOMWindowUtils();
   2262  domutils.disableNonTestMouseEvents(aDisable);
   2263 }
   2264 
   2265 function _getDOMWindowUtils(aWindow = window) {
   2266  // Leave this here as something, somewhere, passes a falsy argument
   2267  // to this, causing the |window| default argument not to get picked up.
   2268  if (!aWindow) {
   2269    aWindow = window;
   2270  }
   2271 
   2272  // If documentURIObject exists or `window` is a stub object, we're in
   2273  // a chrome scope, so don't bother trying to go through SpecialPowers.
   2274  if (!aWindow.document || aWindow.document.documentURIObject) {
   2275    return aWindow.windowUtils;
   2276  }
   2277 
   2278  // we need parent.SpecialPowers for:
   2279  //  layout/base/tests/test_reftests_with_caret.html
   2280  //  chrome: toolkit/content/tests/chrome/test_findbar.xul
   2281  //  chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
   2282  if ("SpecialPowers" in aWindow && aWindow.SpecialPowers != undefined) {
   2283    return aWindow.SpecialPowers.getDOMWindowUtils(aWindow);
   2284  }
   2285  if (
   2286    "SpecialPowers" in aWindow.parent &&
   2287    aWindow.parent.SpecialPowers != undefined
   2288  ) {
   2289    return aWindow.parent.SpecialPowers.getDOMWindowUtils(aWindow);
   2290  }
   2291 
   2292  // TODO: this is assuming we are in chrome space
   2293  return aWindow.windowUtils;
   2294 }
   2295 
   2296 /**
   2297 * @param {DOMWindow} [aWindow] - DOM window
   2298 * @returns The scaling value applied to the top window.
   2299 */
   2300 function _getTopWindowResolution(aWindow) {
   2301  let resolution = 1.0;
   2302  try {
   2303    resolution = _getDOMWindowUtils(aWindow.top).getResolution();
   2304  } catch (e) {
   2305    // XXX How to get mobile viewport scale on Fission+xorigin since
   2306    //     window.top access isn't allowed due to cross-origin?
   2307  }
   2308  return resolution;
   2309 }
   2310 
   2311 /**
   2312 * @param {DOMWindow} [aWindow] - The DOM window which you want
   2313 * to get its x-offset in the screen.
   2314 * @returns The screenX of aWindow in the unscaled CSS pixels.
   2315 */
   2316 function _getScreenXInUnscaledCSSPixels(aWindow) {
   2317  // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
   2318  //     so use window.top's mozInnerScreen. But this won't work fission+xorigin
   2319  //     with mobile viewport until mozInnerScreen returns valid value with
   2320  //     scale.
   2321  let winInnerOffsetX = aWindow.mozInnerScreenX;
   2322  try {
   2323    winInnerOffsetX =
   2324      aWindow.top.mozInnerScreenX +
   2325      (aWindow.mozInnerScreenX - aWindow.top.mozInnerScreenX) *
   2326        _getTopWindowResolution(aWindow);
   2327  } catch (e) {
   2328    // XXX fission+xorigin test throws permission denied since win.top is
   2329    //     cross-origin.
   2330  }
   2331  return winInnerOffsetX;
   2332 }
   2333 
   2334 /**
   2335 * @param {DOMWindow} [aWindow] - The DOM window which you want
   2336 * to get its y-offset in the screen.
   2337 * @returns The screenY of aWindow in the unscaled CSS pixels.
   2338 */
   2339 function _getScreenYInUnscaledCSSPixels(aWindow) {
   2340  // XXX mozInnerScreen might be invalid value on mobile viewport (Bug 1701546),
   2341  //     so use window.top's mozInnerScreen. But this won't work fission+xorigin
   2342  //     with mobile viewport until mozInnerScreen returns valid value with
   2343  //     scale.
   2344  let winInnerOffsetY = aWindow.mozInnerScreenY;
   2345  try {
   2346    winInnerOffsetY =
   2347      aWindow.top.mozInnerScreenY +
   2348      (aWindow.mozInnerScreenY - aWindow.top.mozInnerScreenY) *
   2349        _getTopWindowResolution(aWindow);
   2350  } catch (e) {
   2351    // XXX fission+xorigin test throws permission denied since win.top is
   2352    //     cross-origin.
   2353  }
   2354  return winInnerOffsetY;
   2355 }
   2356 
   2357 function _defineConstant(name, value) {
   2358  Object.defineProperty(this, name, {
   2359    value,
   2360    enumerable: true,
   2361    writable: false,
   2362  });
   2363 }
   2364 
   2365 const COMPOSITION_ATTR_RAW_CLAUSE =
   2366  _EU_Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE;
   2367 _defineConstant("COMPOSITION_ATTR_RAW_CLAUSE", COMPOSITION_ATTR_RAW_CLAUSE);
   2368 const COMPOSITION_ATTR_SELECTED_RAW_CLAUSE =
   2369  _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE;
   2370 _defineConstant(
   2371  "COMPOSITION_ATTR_SELECTED_RAW_CLAUSE",
   2372  COMPOSITION_ATTR_SELECTED_RAW_CLAUSE
   2373 );
   2374 const COMPOSITION_ATTR_CONVERTED_CLAUSE =
   2375  _EU_Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE;
   2376 _defineConstant(
   2377  "COMPOSITION_ATTR_CONVERTED_CLAUSE",
   2378  COMPOSITION_ATTR_CONVERTED_CLAUSE
   2379 );
   2380 const COMPOSITION_ATTR_SELECTED_CLAUSE =
   2381  _EU_Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE;
   2382 _defineConstant(
   2383  "COMPOSITION_ATTR_SELECTED_CLAUSE",
   2384  COMPOSITION_ATTR_SELECTED_CLAUSE
   2385 );
   2386 
   2387 var TIPMap = new WeakMap();
   2388 
   2389 function _getTIP(aWindow, aCallback) {
   2390  if (!aWindow) {
   2391    aWindow = window;
   2392  }
   2393  var tip;
   2394  if (TIPMap.has(aWindow)) {
   2395    tip = TIPMap.get(aWindow);
   2396  } else {
   2397    tip = _EU_Cc["@mozilla.org/text-input-processor;1"].createInstance(
   2398      _EU_Ci.nsITextInputProcessor
   2399    );
   2400    TIPMap.set(aWindow, tip);
   2401  }
   2402  if (!tip.beginInputTransactionForTests(aWindow, aCallback)) {
   2403    tip = null;
   2404    TIPMap.delete(aWindow);
   2405  }
   2406  return tip;
   2407 }
   2408 
   2409 function _getKeyboardEvent(aWindow = window) {
   2410  if (typeof KeyboardEvent != "undefined") {
   2411    try {
   2412      // See if the object can be instantiated; sometimes this yields
   2413      // 'TypeError: can't access dead object' or 'KeyboardEvent is not a constructor'.
   2414      new KeyboardEvent("", {});
   2415      return KeyboardEvent;
   2416    } catch (ex) {}
   2417  }
   2418  if (typeof content != "undefined" && "KeyboardEvent" in content) {
   2419    return content.KeyboardEvent;
   2420  }
   2421  return aWindow.KeyboardEvent;
   2422 }
   2423 
   2424 // eslint-disable-next-line complexity
   2425 function _guessKeyNameFromKeyCode(aKeyCode, aWindow = window) {
   2426  var KeyboardEvent = _getKeyboardEvent(aWindow);
   2427  switch (aKeyCode) {
   2428    case KeyboardEvent.DOM_VK_CANCEL:
   2429      return "Cancel";
   2430    case KeyboardEvent.DOM_VK_HELP:
   2431      return "Help";
   2432    case KeyboardEvent.DOM_VK_BACK_SPACE:
   2433      return "Backspace";
   2434    case KeyboardEvent.DOM_VK_TAB:
   2435      return "Tab";
   2436    case KeyboardEvent.DOM_VK_CLEAR:
   2437      return "Clear";
   2438    case KeyboardEvent.DOM_VK_RETURN:
   2439      return "Enter";
   2440    case KeyboardEvent.DOM_VK_SHIFT:
   2441      return "Shift";
   2442    case KeyboardEvent.DOM_VK_CONTROL:
   2443      return "Control";
   2444    case KeyboardEvent.DOM_VK_ALT:
   2445      return "Alt";
   2446    case KeyboardEvent.DOM_VK_PAUSE:
   2447      return "Pause";
   2448    case KeyboardEvent.DOM_VK_EISU:
   2449      return "Eisu";
   2450    case KeyboardEvent.DOM_VK_ESCAPE:
   2451      return "Escape";
   2452    case KeyboardEvent.DOM_VK_CONVERT:
   2453      return "Convert";
   2454    case KeyboardEvent.DOM_VK_NONCONVERT:
   2455      return "NonConvert";
   2456    case KeyboardEvent.DOM_VK_ACCEPT:
   2457      return "Accept";
   2458    case KeyboardEvent.DOM_VK_MODECHANGE:
   2459      return "ModeChange";
   2460    case KeyboardEvent.DOM_VK_PAGE_UP:
   2461      return "PageUp";
   2462    case KeyboardEvent.DOM_VK_PAGE_DOWN:
   2463      return "PageDown";
   2464    case KeyboardEvent.DOM_VK_END:
   2465      return "End";
   2466    case KeyboardEvent.DOM_VK_HOME:
   2467      return "Home";
   2468    case KeyboardEvent.DOM_VK_LEFT:
   2469      return "ArrowLeft";
   2470    case KeyboardEvent.DOM_VK_UP:
   2471      return "ArrowUp";
   2472    case KeyboardEvent.DOM_VK_RIGHT:
   2473      return "ArrowRight";
   2474    case KeyboardEvent.DOM_VK_DOWN:
   2475      return "ArrowDown";
   2476    case KeyboardEvent.DOM_VK_SELECT:
   2477      return "Select";
   2478    case KeyboardEvent.DOM_VK_PRINT:
   2479      return "Print";
   2480    case KeyboardEvent.DOM_VK_EXECUTE:
   2481      return "Execute";
   2482    case KeyboardEvent.DOM_VK_PRINTSCREEN:
   2483      return "PrintScreen";
   2484    case KeyboardEvent.DOM_VK_INSERT:
   2485      return "Insert";
   2486    case KeyboardEvent.DOM_VK_DELETE:
   2487      return "Delete";
   2488    case KeyboardEvent.DOM_VK_WIN:
   2489      return "OS";
   2490    case KeyboardEvent.DOM_VK_CONTEXT_MENU:
   2491      return "ContextMenu";
   2492    case KeyboardEvent.DOM_VK_SLEEP:
   2493      return "Standby";
   2494    case KeyboardEvent.DOM_VK_F1:
   2495      return "F1";
   2496    case KeyboardEvent.DOM_VK_F2:
   2497      return "F2";
   2498    case KeyboardEvent.DOM_VK_F3:
   2499      return "F3";
   2500    case KeyboardEvent.DOM_VK_F4:
   2501      return "F4";
   2502    case KeyboardEvent.DOM_VK_F5:
   2503      return "F5";
   2504    case KeyboardEvent.DOM_VK_F6:
   2505      return "F6";
   2506    case KeyboardEvent.DOM_VK_F7:
   2507      return "F7";
   2508    case KeyboardEvent.DOM_VK_F8:
   2509      return "F8";
   2510    case KeyboardEvent.DOM_VK_F9:
   2511      return "F9";
   2512    case KeyboardEvent.DOM_VK_F10:
   2513      return "F10";
   2514    case KeyboardEvent.DOM_VK_F11:
   2515      return "F11";
   2516    case KeyboardEvent.DOM_VK_F12:
   2517      return "F12";
   2518    case KeyboardEvent.DOM_VK_F13:
   2519      return "F13";
   2520    case KeyboardEvent.DOM_VK_F14:
   2521      return "F14";
   2522    case KeyboardEvent.DOM_VK_F15:
   2523      return "F15";
   2524    case KeyboardEvent.DOM_VK_F16:
   2525      return "F16";
   2526    case KeyboardEvent.DOM_VK_F17:
   2527      return "F17";
   2528    case KeyboardEvent.DOM_VK_F18:
   2529      return "F18";
   2530    case KeyboardEvent.DOM_VK_F19:
   2531      return "F19";
   2532    case KeyboardEvent.DOM_VK_F20:
   2533      return "F20";
   2534    case KeyboardEvent.DOM_VK_F21:
   2535      return "F21";
   2536    case KeyboardEvent.DOM_VK_F22:
   2537      return "F22";
   2538    case KeyboardEvent.DOM_VK_F23:
   2539      return "F23";
   2540    case KeyboardEvent.DOM_VK_F24:
   2541      return "F24";
   2542    case KeyboardEvent.DOM_VK_NUM_LOCK:
   2543      return "NumLock";
   2544    case KeyboardEvent.DOM_VK_SCROLL_LOCK:
   2545      return "ScrollLock";
   2546    case KeyboardEvent.DOM_VK_VOLUME_MUTE:
   2547      return "AudioVolumeMute";
   2548    case KeyboardEvent.DOM_VK_VOLUME_DOWN:
   2549      return "AudioVolumeDown";
   2550    case KeyboardEvent.DOM_VK_VOLUME_UP:
   2551      return "AudioVolumeUp";
   2552    case KeyboardEvent.DOM_VK_META:
   2553      return "Meta";
   2554    case KeyboardEvent.DOM_VK_ALTGR:
   2555      return "AltGraph";
   2556    case KeyboardEvent.DOM_VK_PROCESSKEY:
   2557      return "Process";
   2558    case KeyboardEvent.DOM_VK_ATTN:
   2559      return "Attn";
   2560    case KeyboardEvent.DOM_VK_CRSEL:
   2561      return "CrSel";
   2562    case KeyboardEvent.DOM_VK_EXSEL:
   2563      return "ExSel";
   2564    case KeyboardEvent.DOM_VK_EREOF:
   2565      return "EraseEof";
   2566    case KeyboardEvent.DOM_VK_PLAY:
   2567      return "Play";
   2568    default:
   2569      return "Unidentified";
   2570  }
   2571 }
   2572 
   2573 function _createKeyboardEventDictionary(
   2574  aKey,
   2575  aKeyEvent,
   2576  aTIP = null,
   2577  aWindow = window
   2578 ) {
   2579  var result = { dictionary: null, flags: 0 };
   2580  var keyCodeIsDefined = "keyCode" in aKeyEvent;
   2581  var keyCode =
   2582    keyCodeIsDefined && aKeyEvent.keyCode >= 0 && aKeyEvent.keyCode <= 255
   2583      ? aKeyEvent.keyCode
   2584      : 0;
   2585  var keyName = "Unidentified";
   2586  var code = aKeyEvent.code;
   2587  if (!aTIP) {
   2588    aTIP = _getTIP(aWindow);
   2589  }
   2590  if (aKey.indexOf("KEY_") == 0) {
   2591    keyName = aKey.substr("KEY_".length);
   2592    result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
   2593    if (code === undefined) {
   2594      code = aTIP.computeCodeValueOfNonPrintableKey(
   2595        keyName,
   2596        aKeyEvent.location
   2597      );
   2598    }
   2599  } else if (aKey.indexOf("VK_") == 0) {
   2600    keyCode = _getKeyboardEvent(aWindow)["DOM_" + aKey];
   2601    if (!keyCode) {
   2602      throw new Error("Unknown key: " + aKey);
   2603    }
   2604    keyName = _guessKeyNameFromKeyCode(keyCode, aWindow);
   2605    result.flags |= _EU_Ci.nsITextInputProcessor.KEY_NON_PRINTABLE_KEY;
   2606    if (code === undefined) {
   2607      code = aTIP.computeCodeValueOfNonPrintableKey(
   2608        keyName,
   2609        aKeyEvent.location
   2610      );
   2611    }
   2612  } else if (aKey != "") {
   2613    keyName = aKey;
   2614    if (!keyCodeIsDefined) {
   2615      keyCode = aTIP.guessKeyCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
   2616        aKey,
   2617        aKeyEvent.location
   2618      );
   2619    }
   2620    if (!keyCode) {
   2621      result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
   2622    }
   2623    result.flags |= _EU_Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
   2624    if (code === undefined) {
   2625      code = aTIP.guessCodeValueOfPrintableKeyInUSEnglishKeyboardLayout(
   2626        keyName,
   2627        aKeyEvent.location
   2628      );
   2629    }
   2630  }
   2631  var locationIsDefined = "location" in aKeyEvent;
   2632  if (locationIsDefined && aKeyEvent.location === 0) {
   2633    result.flags |= _EU_Ci.nsITextInputProcessor.KEY_KEEP_KEY_LOCATION_STANDARD;
   2634  }
   2635  if (aKeyEvent.doNotMarkKeydownAsProcessed) {
   2636    result.flags |=
   2637      _EU_Ci.nsITextInputProcessor.KEY_DONT_MARK_KEYDOWN_AS_PROCESSED;
   2638  }
   2639  if (aKeyEvent.markKeyupAsProcessed) {
   2640    result.flags |= _EU_Ci.nsITextInputProcessor.KEY_MARK_KEYUP_AS_PROCESSED;
   2641  }
   2642  result.dictionary = {
   2643    key: keyName,
   2644    code,
   2645    location: locationIsDefined ? aKeyEvent.location : 0,
   2646    repeat: "repeat" in aKeyEvent ? aKeyEvent.repeat === true : false,
   2647    keyCode,
   2648  };
   2649  return result;
   2650 }
   2651 
   2652 function _emulateToActivateModifiers(aTIP, aKeyEvent, aWindow = window) {
   2653  if (!aKeyEvent) {
   2654    return null;
   2655  }
   2656  var KeyboardEvent = _getKeyboardEvent(aWindow);
   2657 
   2658  var modifiers = {
   2659    normal: [
   2660      { key: "Alt", attr: "altKey" },
   2661      { key: "AltGraph", attr: "altGraphKey" },
   2662      { key: "Control", attr: "ctrlKey" },
   2663      { key: "Fn", attr: "fnKey" },
   2664      { key: "Meta", attr: "metaKey" },
   2665      { key: "Shift", attr: "shiftKey" },
   2666      { key: "Symbol", attr: "symbolKey" },
   2667      { key: _EU_isMac(aWindow) ? "Meta" : "Control", attr: "accelKey" },
   2668    ],
   2669    lockable: [
   2670      { key: "CapsLock", attr: "capsLockKey" },
   2671      { key: "FnLock", attr: "fnLockKey" },
   2672      { key: "NumLock", attr: "numLockKey" },
   2673      { key: "ScrollLock", attr: "scrollLockKey" },
   2674      { key: "SymbolLock", attr: "symbolLockKey" },
   2675    ],
   2676  };
   2677 
   2678  for (let i = 0; i < modifiers.normal.length; i++) {
   2679    if (!aKeyEvent[modifiers.normal[i].attr]) {
   2680      continue;
   2681    }
   2682    if (aTIP.getModifierState(modifiers.normal[i].key)) {
   2683      continue; // already activated.
   2684    }
   2685    let event = new KeyboardEvent("", { key: modifiers.normal[i].key });
   2686    aTIP.keydown(
   2687      event,
   2688      aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
   2689    );
   2690    modifiers.normal[i].activated = true;
   2691  }
   2692  for (let i = 0; i < modifiers.lockable.length; i++) {
   2693    if (!aKeyEvent[modifiers.lockable[i].attr]) {
   2694      continue;
   2695    }
   2696    if (aTIP.getModifierState(modifiers.lockable[i].key)) {
   2697      continue; // already activated.
   2698    }
   2699    let event = new KeyboardEvent("", { key: modifiers.lockable[i].key });
   2700    aTIP.keydown(
   2701      event,
   2702      aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
   2703    );
   2704    aTIP.keyup(
   2705      event,
   2706      aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
   2707    );
   2708    modifiers.lockable[i].activated = true;
   2709  }
   2710  return modifiers;
   2711 }
   2712 
   2713 function _emulateToInactivateModifiers(aTIP, aModifiers, aWindow = window) {
   2714  if (!aModifiers) {
   2715    return;
   2716  }
   2717  var KeyboardEvent = _getKeyboardEvent(aWindow);
   2718  for (let i = 0; i < aModifiers.normal.length; i++) {
   2719    if (!aModifiers.normal[i].activated) {
   2720      continue;
   2721    }
   2722    let event = new KeyboardEvent("", { key: aModifiers.normal[i].key });
   2723    aTIP.keyup(
   2724      event,
   2725      aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
   2726    );
   2727  }
   2728  for (let i = 0; i < aModifiers.lockable.length; i++) {
   2729    if (!aModifiers.lockable[i].activated) {
   2730      continue;
   2731    }
   2732    if (!aTIP.getModifierState(aModifiers.lockable[i].key)) {
   2733      continue; // who already inactivated this?
   2734    }
   2735    let event = new KeyboardEvent("", { key: aModifiers.lockable[i].key });
   2736    aTIP.keydown(
   2737      event,
   2738      aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
   2739    );
   2740    aTIP.keyup(
   2741      event,
   2742      aTIP.KEY_NON_PRINTABLE_KEY | aTIP.KEY_DONT_DISPATCH_MODIFIER_KEY_EVENT
   2743    );
   2744  }
   2745 }
   2746 
   2747 /**
   2748 * Synthesize a composition event and keydown event and keyup events unless
   2749 * you prevent to dispatch them explicitly (see aEvent.key's explanation).
   2750 *
   2751 * Note that you shouldn't call this with "compositionstart" unless you need to
   2752 * test compositionstart event which is NOT followed by compositionupdate
   2753 * event immediately.  Typically, native IME starts composition with
   2754 * a pair of keydown and keyup event and dispatch compositionstart and
   2755 * compositionupdate (and non-standard text event) between them.  So, in most
   2756 * cases, you should call synthesizeCompositionChange() directly.
   2757 * If you call this with compositionstart, keyup event will be fired
   2758 * immediately after compositionstart.  In other words, you should use
   2759 * "compositionstart" only when you need to emulate IME which just starts
   2760 * composition with compositionstart event but does not send composing text to
   2761 * us until committing the composition.  This is behavior of some Chinese IMEs.
   2762 *
   2763 * @param aEvent               The composition event information.  This must
   2764 *                             have |type| member.  The value must be
   2765 *                             "compositionstart", "compositionend",
   2766 *                             "compositioncommitasis" or "compositioncommit".
   2767 *
   2768 *                             And also this may have |data| and |locale| which
   2769 *                             would be used for the value of each property of
   2770 *                             the composition event.  Note that the |data| is
   2771 *                             ignored if the event type is "compositionstart"
   2772 *                             or "compositioncommitasis".
   2773 *
   2774 *                             If |key| is undefined, "keydown" and "keyup"
   2775 *                             events which are marked as "processed by IME"
   2776 *                             are dispatched.  If |key| is not null, "keydown"
   2777 *                             and/or "keyup" events are dispatched (if the
   2778 *                             |key.type| is specified as "keydown", only
   2779 *                             "keydown" event is dispatched).  Otherwise,
   2780 *                             i.e., if |key| is null, neither "keydown" nor
   2781 *                             "keyup" event is dispatched.
   2782 *
   2783 *                             If |key.doNotMarkKeydownAsProcessed| is not true,
   2784 *                             key value and keyCode value of "keydown" event
   2785 *                             will be set to "Process" and DOM_VK_PROCESSKEY.
   2786 *                             If |key.markKeyupAsProcessed| is true,
   2787 *                             key value and keyCode value of "keyup" event
   2788 *                             will be set to "Process" and DOM_VK_PROCESSKEY.
   2789 * @param aWindow              Optional (If null, current |window| will be used)
   2790 * @param aCallback            Optional (If non-null, use the callback for
   2791 *                             receiving notifications to IME)
   2792 */
   2793 function synthesizeComposition(aEvent, aWindow = window, aCallback) {
   2794  var TIP = _getTIP(aWindow, aCallback);
   2795  if (!TIP) {
   2796    return;
   2797  }
   2798  var KeyboardEvent = _getKeyboardEvent(aWindow);
   2799  var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
   2800  var keyEventDict = { dictionary: null, flags: 0 };
   2801  var keyEvent = null;
   2802  if (aEvent.key && typeof aEvent.key.key === "string") {
   2803    keyEventDict = _createKeyboardEventDictionary(
   2804      aEvent.key.key,
   2805      aEvent.key,
   2806      TIP,
   2807      aWindow
   2808    );
   2809    keyEvent = new KeyboardEvent(
   2810      // eslint-disable-next-line no-nested-ternary
   2811      aEvent.key.type === "keydown"
   2812        ? "keydown"
   2813        : aEvent.key.type === "keyup"
   2814          ? "keyup"
   2815          : "",
   2816      keyEventDict.dictionary
   2817    );
   2818  } else if (aEvent.key === undefined) {
   2819    keyEventDict = _createKeyboardEventDictionary(
   2820      "KEY_Process",
   2821      {},
   2822      TIP,
   2823      aWindow
   2824    );
   2825    keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
   2826  }
   2827  try {
   2828    switch (aEvent.type) {
   2829      case "compositionstart":
   2830        TIP.startComposition(keyEvent, keyEventDict.flags);
   2831        break;
   2832      case "compositioncommitasis":
   2833        TIP.commitComposition(keyEvent, keyEventDict.flags);
   2834        break;
   2835      case "compositioncommit":
   2836        TIP.commitCompositionWith(aEvent.data, keyEvent, keyEventDict.flags);
   2837        break;
   2838    }
   2839  } finally {
   2840    _emulateToInactivateModifiers(TIP, modifiers, aWindow);
   2841  }
   2842 }
   2843 /**
   2844 * Synthesize eCompositionChange event which causes a DOM text event, may
   2845 * cause compositionupdate event, and causes keydown event and keyup event
   2846 * unless you prevent to dispatch them explicitly (see aEvent.key's
   2847 * explanation).
   2848 *
   2849 * Note that if you call this when there is no composition, compositionstart
   2850 * event will be fired automatically.  This is better than you use
   2851 * synthesizeComposition("compositionstart") in most cases.  See the
   2852 * explanation of synthesizeComposition().
   2853 *
   2854 * @param aEvent   The compositionchange event's information, this has
   2855 *                 |composition| and |caret| members.  |composition| has
   2856 *                 |string| and |clauses| members.  |clauses| must be array
   2857 *                 object.  Each object has |length| and |attr|.  And |caret|
   2858 *                 has |start| and |length|.  See the following tree image.
   2859 *
   2860 *                 aEvent
   2861 *                   +-- composition
   2862 *                   |     +-- string
   2863 *                   |     +-- clauses[]
   2864 *                   |           +-- length
   2865 *                   |           +-- attr
   2866 *                   +-- caret
   2867 *                   |     +-- start
   2868 *                   |     +-- length
   2869 *                   +-- key
   2870 *
   2871 *                 Set the composition string to |composition.string|.  Set its
   2872 *                 clauses information to the |clauses| array.
   2873 *
   2874 *                 When it's composing, set the each clauses' length to the
   2875 *                 |composition.clauses[n].length|.  The sum of the all length
   2876 *                 values must be same as the length of |composition.string|.
   2877 *                 Set nsICompositionStringSynthesizer.ATTR_* to the
   2878 *                 |composition.clauses[n].attr|.
   2879 *
   2880 *                 When it's not composing, set 0 to the
   2881 *                 |composition.clauses[0].length| and
   2882 *                 |composition.clauses[0].attr|.
   2883 *
   2884 *                 Set caret position to the |caret.start|. It's offset from
   2885 *                 the start of the composition string.  Set caret length to
   2886 *                 |caret.length|.  If it's larger than 0, it should be wide
   2887 *                 caret.  However, current nsEditor doesn't support wide
   2888 *                 caret, therefore, you should always set 0 now.
   2889 *
   2890 *                 If |key| is undefined, "keydown" and "keyup" events which
   2891 *                 are marked as "processed by IME" are dispatched.  If |key|
   2892 *                 is not null, "keydown" and/or "keyup" events are dispatched
   2893 *                 (if the |key.type| is specified as "keydown", only "keydown"
   2894 *                 event is dispatched).  Otherwise, i.e., if |key| is null,
   2895 *                 neither "keydown" nor "keyup" event is dispatched.
   2896 *                 If |key.doNotMarkKeydownAsProcessed| is not true, key value
   2897 *                 and keyCode value of "keydown" event will be set to
   2898 *                 "Process" and DOM_VK_PROCESSKEY.
   2899 *                 If |key.markKeyupAsProcessed| is true key value and keyCode
   2900 *                 value of "keyup" event will be set to "Process" and
   2901 *                 DOM_VK_PROCESSKEY.
   2902 *
   2903 * @param aWindow  Optional (If null, current |window| will be used)
   2904 * @param aCallback     Optional (If non-null, use the callback for receiving
   2905 *                      notifications to IME)
   2906 */
   2907 function synthesizeCompositionChange(aEvent, aWindow = window, aCallback) {
   2908  var TIP = _getTIP(aWindow, aCallback);
   2909  if (!TIP) {
   2910    return;
   2911  }
   2912  var KeyboardEvent = _getKeyboardEvent(aWindow);
   2913 
   2914  if (
   2915    !aEvent.composition ||
   2916    !aEvent.composition.clauses ||
   2917    !aEvent.composition.clauses[0]
   2918  ) {
   2919    return;
   2920  }
   2921 
   2922  TIP.setPendingCompositionString(aEvent.composition.string);
   2923  if (aEvent.composition.clauses[0].length) {
   2924    for (var i = 0; i < aEvent.composition.clauses.length; i++) {
   2925      switch (aEvent.composition.clauses[i].attr) {
   2926        case TIP.ATTR_RAW_CLAUSE:
   2927        case TIP.ATTR_SELECTED_RAW_CLAUSE:
   2928        case TIP.ATTR_CONVERTED_CLAUSE:
   2929        case TIP.ATTR_SELECTED_CLAUSE:
   2930          TIP.appendClauseToPendingComposition(
   2931            aEvent.composition.clauses[i].length,
   2932            aEvent.composition.clauses[i].attr
   2933          );
   2934          break;
   2935        case 0:
   2936          // Ignore dummy clause for the argument.
   2937          break;
   2938        default:
   2939          throw new Error("invalid clause attribute specified");
   2940      }
   2941    }
   2942  }
   2943 
   2944  if (aEvent.caret) {
   2945    TIP.setCaretInPendingComposition(aEvent.caret.start);
   2946  }
   2947 
   2948  var modifiers = _emulateToActivateModifiers(TIP, aEvent.key, aWindow);
   2949  try {
   2950    var keyEventDict = { dictionary: null, flags: 0 };
   2951    var keyEvent = null;
   2952    if (aEvent.key && typeof aEvent.key.key === "string") {
   2953      keyEventDict = _createKeyboardEventDictionary(
   2954        aEvent.key.key,
   2955        aEvent.key,
   2956        TIP,
   2957        aWindow
   2958      );
   2959      keyEvent = new KeyboardEvent(
   2960        // eslint-disable-next-line no-nested-ternary
   2961        aEvent.key.type === "keydown"
   2962          ? "keydown"
   2963          : aEvent.key.type === "keyup"
   2964            ? "keyup"
   2965            : "",
   2966        keyEventDict.dictionary
   2967      );
   2968    } else if (aEvent.key === undefined) {
   2969      keyEventDict = _createKeyboardEventDictionary(
   2970        "KEY_Process",
   2971        {},
   2972        TIP,
   2973        aWindow
   2974      );
   2975      keyEvent = new KeyboardEvent("", keyEventDict.dictionary);
   2976    }
   2977    TIP.flushPendingComposition(keyEvent, keyEventDict.flags);
   2978  } finally {
   2979    _emulateToInactivateModifiers(TIP, modifiers, aWindow);
   2980  }
   2981 }
   2982 
   2983 // Must be synchronized with nsIDOMWindowUtils.
   2984 const QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
   2985 const QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK = 0x0001;
   2986 
   2987 const QUERY_CONTENT_FLAG_SELECTION_NORMAL = 0x0000;
   2988 const QUERY_CONTENT_FLAG_SELECTION_SPELLCHECK = 0x0002;
   2989 const QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT = 0x0004;
   2990 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT = 0x0008;
   2991 const QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT = 0x0010;
   2992 const QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT = 0x0020;
   2993 const QUERY_CONTENT_FLAG_SELECTION_ACCESSIBILITY = 0x0040;
   2994 const QUERY_CONTENT_FLAG_SELECTION_FIND = 0x0080;
   2995 const QUERY_CONTENT_FLAG_SELECTION_URLSECONDARY = 0x0100;
   2996 const QUERY_CONTENT_FLAG_SELECTION_URLSTRIKEOUT = 0x0200;
   2997 
   2998 const QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT = 0x0400;
   2999 
   3000 const SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK = 0x0000;
   3001 const SELECTION_SET_FLAG_USE_XP_LINE_BREAK = 0x0001;
   3002 const SELECTION_SET_FLAG_REVERSE = 0x0002;
   3003 
   3004 /**
   3005 * Synthesize a query text content event.
   3006 *
   3007 * @param aOffset  The character offset.  0 means the first character in the
   3008 *                 selection root.
   3009 * @param aLength  The length of getting text.  If the length is too long,
   3010 *                 the extra length is ignored.
   3011 * @param aIsRelative   Optional (If true, aOffset is relative to start of
   3012 *                      composition if there is, or start of selection.)
   3013 * @param aWindow  Optional (If null, current |window| will be used)
   3014 * @return         An nsIQueryContentEventResult object.  If this failed,
   3015 *                 the result might be null.
   3016 */
   3017 function synthesizeQueryTextContent(aOffset, aLength, aIsRelative, aWindow) {
   3018  var utils = _getDOMWindowUtils(aWindow);
   3019  if (!utils) {
   3020    return null;
   3021  }
   3022  var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
   3023  if (aIsRelative === true) {
   3024    flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
   3025  }
   3026  return utils.sendQueryContentEvent(
   3027    utils.QUERY_TEXT_CONTENT,
   3028    aOffset,
   3029    aLength,
   3030    0,
   3031    0,
   3032    flags
   3033  );
   3034 }
   3035 
   3036 /**
   3037 * Synthesize a query selected text event.
   3038 *
   3039 * @param aSelectionType    Optional, one of QUERY_CONTENT_FLAG_SELECTION_*.
   3040 *                          If null, QUERY_CONTENT_FLAG_SELECTION_NORMAL will
   3041 *                          be used.
   3042 * @param aWindow  Optional (If null, current |window| will be used)
   3043 * @return         An nsIQueryContentEventResult object.  If this failed,
   3044 *                 the result might be null.
   3045 */
   3046 function synthesizeQuerySelectedText(aSelectionType, aWindow) {
   3047  var utils = _getDOMWindowUtils(aWindow);
   3048  var flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
   3049  if (aSelectionType) {
   3050    flags |= aSelectionType;
   3051  }
   3052 
   3053  return utils.sendQueryContentEvent(
   3054    utils.QUERY_SELECTED_TEXT,
   3055    0,
   3056    0,
   3057    0,
   3058    0,
   3059    flags
   3060  );
   3061 }
   3062 
   3063 /**
   3064 * Synthesize a query caret rect event.
   3065 *
   3066 * @param aOffset  The caret offset.  0 means left side of the first character
   3067 *                 in the selection root.
   3068 * @param aWindow  Optional (If null, current |window| will be used)
   3069 * @return         An nsIQueryContentEventResult object.  If this failed,
   3070 *                 the result might be null.
   3071 */
   3072 function synthesizeQueryCaretRect(aOffset, aWindow) {
   3073  var utils = _getDOMWindowUtils(aWindow);
   3074  if (!utils) {
   3075    return null;
   3076  }
   3077  return utils.sendQueryContentEvent(
   3078    utils.QUERY_CARET_RECT,
   3079    aOffset,
   3080    0,
   3081    0,
   3082    0,
   3083    QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
   3084  );
   3085 }
   3086 
   3087 /**
   3088 * Synthesize a selection set event.
   3089 *
   3090 * @param aOffset  The character offset.  0 means the first character in the
   3091 *                 selection root.
   3092 * @param aLength  The length of the text.  If the length is too long,
   3093 *                 the extra length is ignored.
   3094 * @param aReverse If true, the selection is from |aOffset + aLength| to
   3095 *                 |aOffset|.  Otherwise, from |aOffset| to |aOffset + aLength|.
   3096 * @param aWindow  Optional (If null, current |window| will be used)
   3097 * @return         True, if succeeded.  Otherwise false.
   3098 */
   3099 async function synthesizeSelectionSet(
   3100  aOffset,
   3101  aLength,
   3102  aReverse,
   3103  aWindow = window
   3104 ) {
   3105  const utils = _getDOMWindowUtils(aWindow);
   3106  if (!utils) {
   3107    return false;
   3108  }
   3109  // eSetSelection event will be compared with selection cache in
   3110  // IMEContentObserver, but it may have not been updated yet.  Therefore, we
   3111  // need to flush pending things of IMEContentObserver.
   3112  await new Promise(resolve =>
   3113    aWindow.requestAnimationFrame(() => aWindow.requestAnimationFrame(resolve))
   3114  );
   3115  const flags = aReverse ? SELECTION_SET_FLAG_REVERSE : 0;
   3116  return utils.sendSelectionSetEvent(aOffset, aLength, flags);
   3117 }
   3118 
   3119 /**
   3120 * Synthesize a query text rect event.
   3121 *
   3122 * @param aOffset  The character offset.  0 means the first character in the
   3123 *                 selection root.
   3124 * @param aLength  The length of the text.  If the length is too long,
   3125 *                 the extra length is ignored.
   3126 * @param aIsRelative   Optional (If true, aOffset is relative to start of
   3127 *                      composition if there is, or start of selection.)
   3128 * @param aWindow  Optional (If null, current |window| will be used)
   3129 * @return         An nsIQueryContentEventResult object.  If this failed,
   3130 *                 the result might be null.
   3131 */
   3132 function synthesizeQueryTextRect(aOffset, aLength, aIsRelative, aWindow) {
   3133  if (aIsRelative !== undefined && typeof aIsRelative !== "boolean") {
   3134    throw new Error(
   3135      "Maybe, you set Window object to the 3rd argument, but it should be a boolean value"
   3136    );
   3137  }
   3138  var utils = _getDOMWindowUtils(aWindow);
   3139  let flags = QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK;
   3140  if (aIsRelative === true) {
   3141    flags |= QUERY_CONTENT_FLAG_OFFSET_RELATIVE_TO_INSERTION_POINT;
   3142  }
   3143  return utils.sendQueryContentEvent(
   3144    utils.QUERY_TEXT_RECT,
   3145    aOffset,
   3146    aLength,
   3147    0,
   3148    0,
   3149    flags
   3150  );
   3151 }
   3152 
   3153 /**
   3154 * Synthesize a query text rect array event.
   3155 *
   3156 * @param aOffset  The character offset.  0 means the first character in the
   3157 *                 selection root.
   3158 * @param aLength  The length of the text.  If the length is too long,
   3159 *                 the extra length is ignored.
   3160 * @param aWindow  Optional (If null, current |window| will be used)
   3161 * @return         An nsIQueryContentEventResult object.  If this failed,
   3162 *                 the result might be null.
   3163 */
   3164 function synthesizeQueryTextRectArray(aOffset, aLength, aWindow) {
   3165  var utils = _getDOMWindowUtils(aWindow);
   3166  return utils.sendQueryContentEvent(
   3167    utils.QUERY_TEXT_RECT_ARRAY,
   3168    aOffset,
   3169    aLength,
   3170    0,
   3171    0,
   3172    QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
   3173  );
   3174 }
   3175 
   3176 /**
   3177 * Synthesize a query editor rect event.
   3178 *
   3179 * @param aWindow  Optional (If null, current |window| will be used)
   3180 * @return         An nsIQueryContentEventResult object.  If this failed,
   3181 *                 the result might be null.
   3182 */
   3183 function synthesizeQueryEditorRect(aWindow) {
   3184  var utils = _getDOMWindowUtils(aWindow);
   3185  return utils.sendQueryContentEvent(
   3186    utils.QUERY_EDITOR_RECT,
   3187    0,
   3188    0,
   3189    0,
   3190    0,
   3191    QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
   3192  );
   3193 }
   3194 
   3195 /**
   3196 * Synthesize a character at point event.
   3197 *
   3198 * @param aX, aY   The offset in the client area of the DOM window.
   3199 * @param aWindow  Optional (If null, current |window| will be used)
   3200 * @return         An nsIQueryContentEventResult object.  If this failed,
   3201 *                 the result might be null.
   3202 */
   3203 function synthesizeCharAtPoint(aX, aY, aWindow) {
   3204  var utils = _getDOMWindowUtils(aWindow);
   3205  return utils.sendQueryContentEvent(
   3206    utils.QUERY_CHARACTER_AT_POINT,
   3207    0,
   3208    0,
   3209    aX,
   3210    aY,
   3211    QUERY_CONTENT_FLAG_USE_NATIVE_LINE_BREAK
   3212  );
   3213 }
   3214 
   3215 /**
   3216 * INTERNAL USE ONLY
   3217 * Create an event object to pass to sendDragEvent.
   3218 *
   3219 * @param aType          The string represents drag event type.
   3220 * @param aDestElement   The element to fire the drag event, used to calculate
   3221 *                       screenX/Y and clientX/Y.
   3222 * @param aDestWindow    Optional; Defaults to the current window object.
   3223 * @param aDataTransfer  dataTransfer for current drag session.
   3224 * @param aDragEvent     The object contains properties to override the event
   3225 *                       object
   3226 * @return               An object to pass to sendDragEvent.
   3227 */
   3228 function createDragEventObject(
   3229  aType,
   3230  aDestElement,
   3231  aDestWindow,
   3232  aDataTransfer,
   3233  aDragEvent
   3234 ) {
   3235  const resolution = _getTopWindowResolution(aDestWindow.top);
   3236  const destRect = aDestElement.getBoundingClientRect();
   3237  // If clientX and/or clientY are specified, we should use them.  Otherwise,
   3238  // use the center of the dest element.
   3239  const destClientXInCSSPixels =
   3240    "clientX" in aDragEvent && !("screenX" in aDragEvent)
   3241      ? aDragEvent.clientX
   3242      : destRect.left + destRect.width / 2;
   3243  const destClientYInCSSPixels =
   3244    "clientY" in aDragEvent && !("screenY" in aDragEvent)
   3245      ? aDragEvent.clientY
   3246      : destRect.top + destRect.height / 2;
   3247 
   3248  const devicePixelRatio = aDestWindow.devicePixelRatio;
   3249  const destScreenXInDevicePixels =
   3250    (_getScreenXInUnscaledCSSPixels(aDestWindow) +
   3251      destClientXInCSSPixels * resolution) *
   3252    devicePixelRatio;
   3253  const destScreenYInDevicePixels =
   3254    (_getScreenYInUnscaledCSSPixels(aDestWindow) +
   3255      destClientYInCSSPixels * resolution) *
   3256    devicePixelRatio;
   3257 
   3258  // Wrap only in plain mochitests
   3259  let dataTransfer;
   3260  if (aDataTransfer) {
   3261    dataTransfer = _EU_maybeUnwrap(
   3262      _EU_maybeWrap(aDataTransfer).mozCloneForEvent(aType)
   3263    );
   3264 
   3265    // Copy over the drop effect. This isn't copied over by Clone, as it uses
   3266    // more complex logic in the actual implementation (see
   3267    // nsContentUtils::SetDataTransferInEvent for actual impl).
   3268    dataTransfer.dropEffect = aDataTransfer.dropEffect;
   3269  }
   3270  return Object.assign(
   3271    {
   3272      type: aType,
   3273      screenX: _EU_roundDevicePixels(destScreenXInDevicePixels),
   3274      screenY: _EU_roundDevicePixels(destScreenYInDevicePixels),
   3275      clientX: _EU_roundDevicePixels(destClientXInCSSPixels),
   3276      clientY: _EU_roundDevicePixels(destClientYInCSSPixels),
   3277      dataTransfer,
   3278      _domDispatchOnly: aDragEvent._domDispatchOnly,
   3279    },
   3280    aDragEvent
   3281  );
   3282 }
   3283 
   3284 /**
   3285 * Emulate a event sequence of dragstart, dragenter, and dragover.
   3286 *
   3287 * @param {Element} aSrcElement
   3288 *        The element to use to start the drag.
   3289 * @param {Element} aDestElement
   3290 *        The element to fire the dragover, dragenter events
   3291 * @param {Array}   aDragData
   3292 *        The data to supply for the data transfer.
   3293 *        This data is in the format:
   3294 *
   3295 *        [
   3296 *          [
   3297 *            {"type": value, "data": value },
   3298 *            ...,
   3299 *          ],
   3300 *          ...
   3301 *        ]
   3302 *
   3303 *        Pass null to avoid modifying dataTransfer.
   3304 * @param {string} [aDropEffect="move"]
   3305 *        The drop effect to set during the dragstart event, or 'move' if omitted.
   3306 * @param {DOMWindow} [aWindow=window]
   3307 *        The DOM window in which the drag happens. Defaults to the window in which
   3308 *        EventUtils.js is loaded.
   3309 * @param {DOMWindow} [aDestWindow=aWindow]
   3310 *        Used when aDestElement is in a different DOM window than aSrcElement.
   3311 *        Default is to match ``aWindow``.
   3312 * @param {object} [aDragEvent={}]
   3313 *        Defaults to empty object. Overwrites an object passed to sendDragEvent.
   3314 * @return {[boolean, DataTransfer]}
   3315 *        A two element array, where the first element is the value returned
   3316 *        from sendDragEvent for dragover event, and the second element is the
   3317 *        dataTransfer for the current drag session.
   3318 */
   3319 function synthesizeDragOver(
   3320  aSrcElement,
   3321  aDestElement,
   3322  aDragData,
   3323  aDropEffect,
   3324  aWindow,
   3325  aDestWindow,
   3326  aDragEvent = {}
   3327 ) {
   3328  if (!aWindow) {
   3329    aWindow = window;
   3330  }
   3331  if (!aDestWindow) {
   3332    aDestWindow = aWindow;
   3333  }
   3334 
   3335  // eslint-disable-next-line mozilla/use-services
   3336  const obs = _EU_Cc["@mozilla.org/observer-service;1"].getService(
   3337    _EU_Ci.nsIObserverService
   3338  );
   3339  let utils = _getDOMWindowUtils(aWindow);
   3340  var sess = utils.dragSession;
   3341 
   3342  // This method runs before other callbacks, and acts as a way to inject the
   3343  // initial drag data into the DataTransfer.
   3344  function fillDrag(event) {
   3345    if (aDragData) {
   3346      for (var i = 0; i < aDragData.length; i++) {
   3347        var item = aDragData[i];
   3348        for (var j = 0; j < item.length; j++) {
   3349          _EU_maybeWrap(event.dataTransfer).mozSetDataAt(
   3350            item[j].type,
   3351            item[j].data,
   3352            i
   3353          );
   3354        }
   3355      }
   3356    }
   3357    event.dataTransfer.dropEffect = aDropEffect || "move";
   3358    event.preventDefault();
   3359  }
   3360 
   3361  function trapDrag(subject, topic) {
   3362    if (topic == "on-datatransfer-available") {
   3363      sess.dataTransfer = _EU_maybeUnwrap(
   3364        _EU_maybeWrap(subject).mozCloneForEvent("drop")
   3365      );
   3366      sess.dataTransfer.dropEffect = subject.dropEffect;
   3367    }
   3368  }
   3369 
   3370  // need to use real mouse action
   3371  aWindow.addEventListener("dragstart", fillDrag, true);
   3372  obs.addObserver(trapDrag, "on-datatransfer-available");
   3373  synthesizeMouseAtCenter(aSrcElement, { type: "mousedown" }, aWindow);
   3374 
   3375  var rect = aSrcElement.getBoundingClientRect();
   3376  var x = rect.width / 2;
   3377  var y = rect.height / 2;
   3378  synthesizeMouse(aSrcElement, x, y, { type: "mousemove" }, aWindow);
   3379  synthesizeMouse(aSrcElement, x + 10, y + 10, { type: "mousemove" }, aWindow);
   3380  aWindow.removeEventListener("dragstart", fillDrag, true);
   3381  obs.removeObserver(trapDrag, "on-datatransfer-available");
   3382 
   3383  var dataTransfer = sess.dataTransfer;
   3384  if (!dataTransfer) {
   3385    throw new Error("No data transfer object after synthesizing the mouse!");
   3386  }
   3387 
   3388  // The EventStateManager will fire our dragenter event if it needs to.
   3389  var event = createDragEventObject(
   3390    "dragover",
   3391    aDestElement,
   3392    aDestWindow,
   3393    dataTransfer,
   3394    aDragEvent
   3395  );
   3396  var result = sendDragEvent(event, aDestElement, aDestWindow);
   3397 
   3398  return [result, dataTransfer];
   3399 }
   3400 
   3401 /**
   3402 * Emulate the drop event and mouseup event.
   3403 * This should be called after synthesizeDragOver.
   3404 *
   3405 * @param {*} aResult
   3406 *        The first element of the array returned from ``synthesizeDragOver``.
   3407 * @param {DataTransfer} aDataTransfer
   3408 *        The second element of the array returned from ``synthesizeDragOver``.
   3409 * @param {Element} aDestElement
   3410 *        The element on which to fire the drop event.
   3411 * @param {DOMWindow} [aDestWindow=window]
   3412 *        The DOM window in which the drop happens. Defaults to the window in which
   3413 *        EventUtils.js is loaded.
   3414 * @param {object} [aDragEvent={}]
   3415 *        Defaults to empty object. Overwrites an object passed to sendDragEvent.
   3416 * @return {string}
   3417 *        "none" if aResult is true, ``aDataTransfer.dropEffect`` otherwise.
   3418 */
   3419 function synthesizeDropAfterDragOver(
   3420  aResult,
   3421  aDataTransfer,
   3422  aDestElement,
   3423  aDestWindow,
   3424  aDragEvent = {}
   3425 ) {
   3426  if (!aDestWindow) {
   3427    aDestWindow = window;
   3428  }
   3429 
   3430  var effect = aDataTransfer.dropEffect;
   3431  var event;
   3432 
   3433  if (aResult) {
   3434    effect = "none";
   3435  } else if (effect != "none") {
   3436    event = createDragEventObject(
   3437      "drop",
   3438      aDestElement,
   3439      aDestWindow,
   3440      aDataTransfer,
   3441      aDragEvent
   3442    );
   3443    sendDragEvent(event, aDestElement, aDestWindow);
   3444  }
   3445  // Don't run accessibility checks for this click, since we're not actually
   3446  // clicking. It's just generated as part of the drop.
   3447  // this.AccessibilityUtils might not be set if this isn't a browser test or
   3448  // if a browser test has loaded its own copy of EventUtils for some reason.
   3449  // In the latter case, the test probably shouldn't do that.
   3450  this.AccessibilityUtils?.suppressClickHandling(true);
   3451  synthesizeMouse(aDestElement, 2, 2, { type: "mouseup" }, aDestWindow);
   3452  this.AccessibilityUtils?.suppressClickHandling(false);
   3453 
   3454  return effect;
   3455 }
   3456 
   3457 /**
   3458 * Calls `nsIDragService.startDragSessionForTests`, which is required before
   3459 * any other code can use `nsIDOMWindowUtils.dragSession`. Most notably,
   3460 * a drag session is required before populating a drag-drop event's
   3461 * `dataTransfer` property.
   3462 *
   3463 * @param {Window} aWindow
   3464 * @param {typeof DataTransfer.prototype.dropEffect} aDropEffect
   3465 */
   3466 function startDragSession(aWindow, aDropEffect) {
   3467  const ds = _EU_Cc["@mozilla.org/widget/dragservice;1"].getService(
   3468    _EU_Ci.nsIDragService
   3469  );
   3470 
   3471  let dropAction;
   3472  switch (aDropEffect) {
   3473    case null:
   3474    case undefined:
   3475    case "move":
   3476      dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
   3477      break;
   3478    case "copy":
   3479      dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_COPY;
   3480      break;
   3481    case "link":
   3482      dropAction = _EU_Ci.nsIDragService.DRAGDROP_ACTION_LINK;
   3483      break;
   3484    default:
   3485      throw new Error(`${aDropEffect} is an invalid drop effect value`);
   3486  }
   3487 
   3488  ds.startDragSessionForTests(aWindow, dropAction);
   3489 }
   3490 
   3491 /**
   3492 * Emulate a drag and drop by emulating a dragstart and firing events dragenter,
   3493 * dragover, and drop.
   3494 *
   3495 * @param {Element} aSrcElement
   3496 *        The element to use to start the drag.
   3497 * @param {Element} aDestElement
   3498 *        The element to fire the dragover, dragenter events
   3499 * @param {Array}   aDragData
   3500 *        The data to supply for the data transfer.
   3501 *        This data is in the format:
   3502 *
   3503 *            [
   3504 *              [
   3505 *                {"type": value, "data": value },
   3506 *                ...,
   3507 *              ],
   3508 *              ...
   3509 *            ]
   3510 *
   3511 *        Pass null to avoid modifying dataTransfer.
   3512 * @param {string} [aDropEffect="move"]
   3513 *        The drop effect to set during the dragstart event, or 'move' if omitted..
   3514 * @param {DOMWindow} [aWindow=window]
   3515 *        The DOM window in which the drag happens. Defaults to the window in which
   3516 *        EventUtils.js is loaded.
   3517 * @param {DOMWindow} [aDestWindow=aWindow]
   3518 *        Used when aDestElement is in a different DOM window than aSrcElement.
   3519 *        Default is to match ``aWindow``.
   3520 * @param {object} [aDragEvent={}]
   3521 *        Defaults to empty object. Overwrites an object passed to sendDragEvent.
   3522 * @return {string}
   3523 *        The drop effect that was desired.
   3524 */
   3525 function synthesizeDrop(
   3526  aSrcElement,
   3527  aDestElement,
   3528  aDragData,
   3529  aDropEffect,
   3530  aWindow,
   3531  aDestWindow,
   3532  aDragEvent = {}
   3533 ) {
   3534  if (!aWindow) {
   3535    aWindow = window;
   3536  }
   3537  if (!aDestWindow) {
   3538    aDestWindow = aWindow;
   3539  }
   3540 
   3541  startDragSession(aWindow, aDropEffect);
   3542 
   3543  try {
   3544    var [result, dataTransfer] = synthesizeDragOver(
   3545      aSrcElement,
   3546      aDestElement,
   3547      aDragData,
   3548      aDropEffect,
   3549      aWindow,
   3550      aDestWindow,
   3551      aDragEvent
   3552    );
   3553    return synthesizeDropAfterDragOver(
   3554      result,
   3555      dataTransfer,
   3556      aDestElement,
   3557      aDestWindow,
   3558      aDragEvent
   3559    );
   3560  } finally {
   3561    let srcWindowUtils = _getDOMWindowUtils(aWindow);
   3562    const srcDragSession = srcWindowUtils.dragSession;
   3563    if (srcDragSession) {
   3564      // After each event handler, there is a microtask checkpoint.
   3565      // Event handlers or microtasks might've already ended our drag session.
   3566      // E.g. in SubDialog.open during browser_toolbar_drop_bookmarklet.js
   3567      srcDragSession.endDragSession(true, _parseModifiers(aDragEvent));
   3568    }
   3569  }
   3570 }
   3571 
   3572 function _getFlattenedTreeParentNode(aNode) {
   3573  return _EU_maybeUnwrap(_EU_maybeWrap(aNode).flattenedTreeParentNode);
   3574 }
   3575 
   3576 function _getInclusiveFlattenedTreeParentElement(aNode) {
   3577  for (
   3578    let inclusiveAncestor = aNode;
   3579    inclusiveAncestor;
   3580    inclusiveAncestor = _getFlattenedTreeParentNode(inclusiveAncestor)
   3581  ) {
   3582    if (inclusiveAncestor.nodeType == Node.ELEMENT_NODE) {
   3583      return inclusiveAncestor;
   3584    }
   3585  }
   3586  return null;
   3587 }
   3588 
   3589 function _nodeIsFlattenedTreeDescendantOf(
   3590  aPossibleDescendant,
   3591  aPossibleAncestor
   3592 ) {
   3593  do {
   3594    if (aPossibleDescendant == aPossibleAncestor) {
   3595      return true;
   3596    }
   3597    aPossibleDescendant = _getFlattenedTreeParentNode(aPossibleDescendant);
   3598  } while (aPossibleDescendant);
   3599  return false;
   3600 }
   3601 
   3602 function _computeSrcElementFromSrcSelection(aSrcSelection) {
   3603  let srcElement = _EU_maybeUnwrap(
   3604    _EU_maybeWrap(aSrcSelection).mayCrossShadowBoundaryFocusNode
   3605  );
   3606  while (_EU_maybeWrap(srcElement).isNativeAnonymous) {
   3607    srcElement = _getFlattenedTreeParentNode(srcElement);
   3608  }
   3609  if (srcElement.nodeType !== Node.ELEMENT_NODE) {
   3610    srcElement = _getInclusiveFlattenedTreeParentElement(srcElement);
   3611  }
   3612  return srcElement;
   3613 }
   3614 
   3615 /**
   3616 * Emulate a drag and drop by emulating a dragstart by mousedown and mousemove,
   3617 * and firing events dragenter, dragover, drop, and dragend.
   3618 * This does not modify dataTransfer and tries to emulate the plain drag and
   3619 * drop as much as possible, compared to synthesizeDrop.
   3620 * Note that if synthesized dragstart is canceled, this throws an exception
   3621 * because in such case, Gecko does not start drag session.
   3622 *
   3623 * @param {object} aParams
   3624 * @param {Event} aParams.dragEvent
   3625 *                The DnD events will be generated with modifiers specified with this.
   3626 * @param {Element} aParams.srcElement
   3627 *                The element to start dragging.  If srcSelection is
   3628 *                set, this is computed for element at focus node.
   3629 * @param {Selection|null} aParams.srcSelection
   3630 *                The selection to start to drag, set null if srcElement is set.
   3631 * @param {Element|null} aParams.destElement
   3632 *                The element to drop on. Pass null to emulate a drop on an invalid target.
   3633 * @param {number} aParams.srcX
   3634 *                The initial x coordinate inside srcElement or ignored if srcSelection is set.
   3635 * @param {number} aParams.srcY
   3636 *                The initial y coordinate inside srcElement or ignored if srcSelection is set.
   3637 * @param {number} aParams.stepX
   3638 *                The x-axis step for mousemove inside srcElement
   3639 * @param {number} aParams.stepY
   3640 *                The y-axis step for mousemove inside srcElement
   3641 * @param {number} aParams.finalX
   3642 *                The final x coordinate inside srcElement
   3643 * @param {number} aParams.finalY
   3644 *                The final x coordinate inside srcElement
   3645 * @param {Any} aParams.id
   3646 *                The pointer event id
   3647 * @param {DOMWindow} aParams.srcWindow
   3648 *                The DOM window for dispatching event on srcElement, defaults to the current window object.
   3649 * @param {DOMWindow} aParams.destWindow
   3650 *                The DOM window for dispatching event on destElement, defaults to the current window object.
   3651 * @param {boolean} aParams.expectCancelDragStart
   3652 *                Set to true if the test cancels "dragstart"
   3653 * @param {boolean} aParams.expectSrcElementDisconnected
   3654 *                Set to true if srcElement will be disconnected and
   3655 *                "dragend" event won't be fired.
   3656 * @param {Function} aParams.logFunc
   3657 *                Set function which takes one argument if you need to log rect of target.  E.g., `console.log`.
   3658 */
   3659 // eslint-disable-next-line complexity
   3660 async function synthesizePlainDragAndDrop(aParams) {
   3661  let {
   3662    dragEvent = {},
   3663    srcElement,
   3664    srcSelection,
   3665    destElement,
   3666    srcX = 2,
   3667    srcY = 2,
   3668    stepX = 9,
   3669    stepY = 9,
   3670    finalX = srcX + stepX * 2,
   3671    finalY = srcY + stepY * 2,
   3672    id = _getDOMWindowUtils(window).DEFAULT_MOUSE_POINTER_ID,
   3673    srcWindow = window,
   3674    destWindow = window,
   3675    expectCancelDragStart = false,
   3676    expectSrcElementDisconnected = false,
   3677    logFunc,
   3678  } = aParams;
   3679  // Don't modify given dragEvent object because we modify dragEvent below and
   3680  // callers may use the object multiple times so that callers must not assume
   3681  // that it'll be modified.
   3682  if (aParams.dragEvent !== undefined) {
   3683    dragEvent = Object.assign({}, aParams.dragEvent);
   3684  }
   3685 
   3686  function rectToString(aRect) {
   3687    return `left: ${aRect.left}, top: ${aRect.top}, right: ${aRect.right}, bottom: ${aRect.bottom}`;
   3688  }
   3689 
   3690  let srcWindowUtils = _getDOMWindowUtils(srcWindow);
   3691  let destWindowUtils = _getDOMWindowUtils(destWindow);
   3692 
   3693  if (logFunc) {
   3694    logFunc("synthesizePlainDragAndDrop() -- START");
   3695  }
   3696 
   3697  if (srcSelection) {
   3698    srcElement = _computeSrcElementFromSrcSelection(srcSelection);
   3699    let srcElementRect = srcElement.getBoundingClientRect();
   3700    if (logFunc) {
   3701      logFunc(
   3702        `srcElement.getBoundingClientRect(): ${rectToString(srcElementRect)}`
   3703      );
   3704    }
   3705    // Use last selection client rect because nsIDragSession.sourceNode is
   3706    // initialized from focus node which is usually in last rect.
   3707    let selectionRectList = SpecialPowers.wrap(
   3708      srcSelection.getRangeAt(0)
   3709    ).getAllowCrossShadowBoundaryClientRects();
   3710    let lastSelectionRect = selectionRectList[selectionRectList.length - 1];
   3711    if (logFunc) {
   3712      logFunc(
   3713        `srcSelection.getRangeAt(0).getClientRects()[${
   3714          selectionRectList.length - 1
   3715        }]: ${rectToString(lastSelectionRect)}`
   3716      );
   3717    }
   3718    // Click at center of last selection rect.
   3719    srcX = Math.floor(lastSelectionRect.left + lastSelectionRect.width / 2);
   3720    srcY = Math.floor(lastSelectionRect.top + lastSelectionRect.height / 2);
   3721    // Then, adjust srcX and srcY for making them offset relative to
   3722    // srcElementRect because they will be used when we call synthesizeMouse()
   3723    // with srcElement.
   3724    srcX = Math.floor(srcX - srcElementRect.left);
   3725    srcY = Math.floor(srcY - srcElementRect.top);
   3726    // Finally, recalculate finalX and finalY with new srcX and srcY if they
   3727    // are not specified by the caller.
   3728    if (aParams.finalX === undefined) {
   3729      finalX = srcX + stepX * 2;
   3730    }
   3731    if (aParams.finalY === undefined) {
   3732      finalY = srcY + stepY * 2;
   3733    }
   3734  } else if (logFunc) {
   3735    logFunc(
   3736      `srcElement.getBoundingClientRect(): ${rectToString(
   3737        srcElement.getBoundingClientRect()
   3738      )}`
   3739    );
   3740  }
   3741 
   3742  const editingHost = (() => {
   3743    if (!srcElement.matches(":read-write")) {
   3744      return null;
   3745    }
   3746    let lastEditableElement = srcElement;
   3747    for (
   3748      let inclusiveAncestor =
   3749        _getInclusiveFlattenedTreeParentElement(srcElement);
   3750      inclusiveAncestor;
   3751      inclusiveAncestor = _getInclusiveFlattenedTreeParentElement(
   3752        _getFlattenedTreeParentNode(inclusiveAncestor)
   3753      )
   3754    ) {
   3755      if (inclusiveAncestor.matches(":read-write")) {
   3756        lastEditableElement = inclusiveAncestor;
   3757        if (lastEditableElement == srcElement.ownerDocument.body) {
   3758          break;
   3759        }
   3760      }
   3761    }
   3762    return lastEditableElement;
   3763  })();
   3764  try {
   3765    srcWindowUtils.disableNonTestMouseEvents(true);
   3766 
   3767    await new Promise(r => setTimeout(r, 0));
   3768 
   3769    let mouseDownEvent;
   3770    function onMouseDown(aEvent) {
   3771      mouseDownEvent = aEvent;
   3772      if (logFunc) {
   3773        logFunc(
   3774          `"${aEvent.type}" event is fired on ${
   3775            aEvent.target
   3776          } (composedTarget: ${_EU_maybeUnwrap(
   3777            _EU_maybeWrap(aEvent).composedTarget
   3778          )}`
   3779        );
   3780      }
   3781      if (
   3782        !_nodeIsFlattenedTreeDescendantOf(
   3783          _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
   3784          srcElement
   3785        )
   3786      ) {
   3787        // If srcX and srcY does not point in one of rects in srcElement,
   3788        // "mousedown" target is not in srcElement.  Such case must not
   3789        // be expected by this API users so that we should throw an exception
   3790        // for making debugging easier.
   3791        throw new Error(
   3792          'event target of "mousedown" is not srcElement nor its descendant'
   3793        );
   3794      }
   3795    }
   3796    try {
   3797      srcWindow.addEventListener("mousedown", onMouseDown, { capture: true });
   3798      synthesizeMouse(
   3799        srcElement,
   3800        srcX,
   3801        srcY,
   3802        { type: "mousedown", id },
   3803        srcWindow
   3804      );
   3805      if (logFunc) {
   3806        logFunc(`mousedown at ${srcX}, ${srcY}`);
   3807      }
   3808      if (!mouseDownEvent) {
   3809        throw new Error('"mousedown" event is not fired');
   3810      }
   3811    } finally {
   3812      srcWindow.removeEventListener("mousedown", onMouseDown, {
   3813        capture: true,
   3814      });
   3815    }
   3816 
   3817    let dragStartEvent;
   3818    function onDragStart(aEvent) {
   3819      dragStartEvent = aEvent;
   3820      if (logFunc) {
   3821        logFunc(`"${aEvent.type}" event is fired`);
   3822      }
   3823      if (
   3824        !_nodeIsFlattenedTreeDescendantOf(
   3825          _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
   3826          srcElement
   3827        )
   3828      ) {
   3829        // If srcX and srcY does not point in one of rects in srcElement,
   3830        // "dragstart" target is not in srcElement.  Such case must not
   3831        // be expected by this API users so that we should throw an exception
   3832        // for making debugging easier.
   3833        throw new Error(
   3834          'event target of "dragstart" is not srcElement nor its descendant'
   3835        );
   3836      }
   3837    }
   3838    let dragEnterEvent;
   3839    function onDragEnterGenerated(aEvent) {
   3840      dragEnterEvent = aEvent;
   3841    }
   3842    srcWindow.addEventListener("dragstart", onDragStart, { capture: true });
   3843    srcWindow.addEventListener("dragenter", onDragEnterGenerated, {
   3844      capture: true,
   3845    });
   3846    try {
   3847      // Wait for the next event tick after each event dispatch, so that UI
   3848      // elements (e.g. menu) work like the real user input.
   3849      await new Promise(r => setTimeout(r, 0));
   3850 
   3851      srcX += stepX;
   3852      srcY += stepY;
   3853      synthesizeMouse(
   3854        srcElement,
   3855        srcX,
   3856        srcY,
   3857        { type: "mousemove", id },
   3858        srcWindow
   3859      );
   3860      if (logFunc) {
   3861        logFunc(`first mousemove at ${srcX}, ${srcY}`);
   3862      }
   3863 
   3864      await new Promise(r => setTimeout(r, 0));
   3865 
   3866      srcX += stepX;
   3867      srcY += stepY;
   3868      synthesizeMouse(
   3869        srcElement,
   3870        srcX,
   3871        srcY,
   3872        { type: "mousemove", id },
   3873        srcWindow
   3874      );
   3875      if (logFunc) {
   3876        logFunc(`second mousemove at ${srcX}, ${srcY}`);
   3877      }
   3878 
   3879      await new Promise(r => setTimeout(r, 0));
   3880 
   3881      if (!dragStartEvent) {
   3882        throw new Error('"dragstart" event is not fired');
   3883      }
   3884    } finally {
   3885      srcWindow.removeEventListener("dragstart", onDragStart, {
   3886        capture: true,
   3887      });
   3888      srcWindow.removeEventListener("dragenter", onDragEnterGenerated, {
   3889        capture: true,
   3890      });
   3891    }
   3892 
   3893    let srcSession = srcWindowUtils.dragSession;
   3894    if (!srcSession) {
   3895      if (expectCancelDragStart) {
   3896        synthesizeMouse(
   3897          srcElement,
   3898          finalX,
   3899          finalY,
   3900          { type: "mouseup", id },
   3901          srcWindow
   3902        );
   3903        return;
   3904      }
   3905      throw new Error("drag hasn't been started by the operation");
   3906    } else if (expectCancelDragStart) {
   3907      throw new Error("drag has been started by the operation");
   3908    }
   3909 
   3910    if (destElement) {
   3911      if (
   3912        (srcElement != destElement && !dragEnterEvent) ||
   3913        destElement != dragEnterEvent.target
   3914      ) {
   3915        if (logFunc) {
   3916          logFunc(
   3917            `destElement.getBoundingClientRect(): ${rectToString(
   3918              destElement.getBoundingClientRect()
   3919            )}`
   3920          );
   3921        }
   3922 
   3923        function onDragEnter(aEvent) {
   3924          dragEnterEvent = aEvent;
   3925          if (logFunc) {
   3926            logFunc(`"${aEvent.type}" event is fired`);
   3927          }
   3928          if (aEvent.target != destElement) {
   3929            throw new Error('event target of "dragenter" is not destElement');
   3930          }
   3931        }
   3932        destWindow.addEventListener("dragenter", onDragEnter, {
   3933          capture: true,
   3934        });
   3935        try {
   3936          let event = createDragEventObject(
   3937            "dragenter",
   3938            destElement,
   3939            destWindow,
   3940            null,
   3941            dragEvent
   3942          );
   3943          sendDragEvent(event, destElement, destWindow);
   3944          if (!dragEnterEvent && !destElement.disabled) {
   3945            throw new Error('"dragenter" event is not fired');
   3946          }
   3947          if (dragEnterEvent && destElement.disabled) {
   3948            throw new Error(
   3949              '"dragenter" event should not be fired on disable element'
   3950            );
   3951          }
   3952        } finally {
   3953          destWindow.removeEventListener("dragenter", onDragEnter, {
   3954            capture: true,
   3955          });
   3956        }
   3957      }
   3958 
   3959      let dragOverEvent;
   3960      function onDragOver(aEvent) {
   3961        dragOverEvent = aEvent;
   3962        if (logFunc) {
   3963          logFunc(`"${aEvent.type}" event is fired`);
   3964        }
   3965        if (aEvent.target != destElement) {
   3966          throw new Error('event target of "dragover" is not destElement');
   3967        }
   3968      }
   3969      destWindow.addEventListener("dragover", onDragOver, { capture: true });
   3970      try {
   3971        // dragover and drop are only fired to a valid drop target. If the
   3972        // destElement parameter is null, this function is being used to
   3973        // simulate a drag'n'drop over an invalid drop target.
   3974        let event = createDragEventObject(
   3975          "dragover",
   3976          destElement,
   3977          destWindow,
   3978          null,
   3979          dragEvent
   3980        );
   3981        sendDragEvent(event, destElement, destWindow);
   3982        if (!dragOverEvent && !destElement.disabled) {
   3983          throw new Error('"dragover" event is not fired');
   3984        }
   3985        if (dragEnterEvent && destElement.disabled) {
   3986          throw new Error(
   3987            '"dragover" event should not be fired on disable element'
   3988          );
   3989        }
   3990      } finally {
   3991        destWindow.removeEventListener("dragover", onDragOver, {
   3992          capture: true,
   3993        });
   3994      }
   3995 
   3996      await new Promise(r => setTimeout(r, 0));
   3997 
   3998      // If there is not accept to drop the data, "drop" event shouldn't be
   3999      // fired.
   4000      // XXX nsIDragSession.canDrop is different only on Linux.  It must be
   4001      //     a bug of gtk/nsDragService since it manages `mCanDrop` by itself.
   4002      //     Thus, we should use nsIDragSession.dragAction instead.
   4003      let destSession = destWindowUtils.dragSession;
   4004      if (
   4005        destSession.dragAction != _EU_Ci.nsIDragService.DRAGDROP_ACTION_NONE
   4006      ) {
   4007        let dropEvent;
   4008        function onDrop(aEvent) {
   4009          dropEvent = aEvent;
   4010          if (logFunc) {
   4011            logFunc(`"${aEvent.type}" event is fired`);
   4012          }
   4013          if (
   4014            !_nodeIsFlattenedTreeDescendantOf(
   4015              _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
   4016              destElement
   4017            )
   4018          ) {
   4019            throw new Error(
   4020              'event target of "drop" is not destElement nor its descendant'
   4021            );
   4022          }
   4023        }
   4024        destWindow.addEventListener("drop", onDrop, { capture: true });
   4025        try {
   4026          let event = createDragEventObject(
   4027            "drop",
   4028            destElement,
   4029            destWindow,
   4030            null,
   4031            dragEvent
   4032          );
   4033          sendDragEvent(event, destElement, destWindow);
   4034          if (!dropEvent && destSession.canDrop) {
   4035            throw new Error('"drop" event is not fired');
   4036          }
   4037        } finally {
   4038          destWindow.removeEventListener("drop", onDrop, { capture: true });
   4039        }
   4040        return;
   4041      }
   4042    }
   4043 
   4044    // Since we don't synthesize drop event, we need to set drag end point
   4045    // explicitly for "dragEnd" event which will be fired by
   4046    // endDragSession().
   4047    dragEvent.clientX = srcElement.getBoundingClientRect().x + finalX;
   4048    dragEvent.clientY = srcElement.getBoundingClientRect().y + finalY;
   4049    let event = createDragEventObject(
   4050      "dragend",
   4051      srcElement,
   4052      srcWindow,
   4053      null,
   4054      dragEvent
   4055    );
   4056    srcSession.setDragEndPointForTests(event.screenX, event.screenY);
   4057    if (logFunc) {
   4058      logFunc(
   4059        `dragend event client (X,Y) = (${event.clientX}, ${event.clientY})`
   4060      );
   4061      logFunc(
   4062        `dragend event screen (X,Y) = (${event.screenX}, ${event.screenY})`
   4063      );
   4064    }
   4065  } finally {
   4066    await new Promise(r => setTimeout(r, 0));
   4067 
   4068    if (srcWindowUtils.dragSession) {
   4069      const sourceNode = srcWindowUtils.dragSession.sourceNode;
   4070      let dragEndEvent;
   4071      function onDragEnd(aEvent) {
   4072        dragEndEvent = aEvent;
   4073        if (logFunc) {
   4074          logFunc(`"${aEvent.type}" event is fired`);
   4075        }
   4076        if (
   4077          !_nodeIsFlattenedTreeDescendantOf(
   4078            _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget),
   4079            srcElement
   4080          ) &&
   4081          _EU_maybeUnwrap(_EU_maybeWrap(aEvent).composedTarget) != editingHost
   4082        ) {
   4083          throw new Error(
   4084            'event target of "dragend" is not srcElement nor its descendant'
   4085          );
   4086        }
   4087        if (expectSrcElementDisconnected) {
   4088          throw new Error(
   4089            `"dragend" event shouldn't be fired when the source node is disconnected (the source node is ${
   4090              sourceNode?.isConnected ? "connected" : "null or disconnected"
   4091            })`
   4092          );
   4093        }
   4094      }
   4095      srcWindow.addEventListener("dragend", onDragEnd, { capture: true });
   4096      try {
   4097        srcWindowUtils.dragSession.endDragSession(
   4098          true,
   4099          _parseModifiers(dragEvent)
   4100        );
   4101        if (!expectSrcElementDisconnected && !dragEndEvent) {
   4102          // eslint-disable-next-line no-unsafe-finally
   4103          throw new Error(
   4104            `"dragend" event is not fired by nsIDragSession.endDragSession()${
   4105              srcWindowUtils.dragSession.sourceNode &&
   4106              !srcWindowUtils.dragSession.sourceNode.isConnected
   4107                ? "(sourceNode was disconnected)"
   4108                : ""
   4109            }`
   4110          );
   4111        }
   4112      } finally {
   4113        srcWindow.removeEventListener("dragend", onDragEnd, { capture: true });
   4114      }
   4115    }
   4116    srcWindowUtils.disableNonTestMouseEvents(false);
   4117    if (logFunc) {
   4118      logFunc("synthesizePlainDragAndDrop() -- END");
   4119    }
   4120  }
   4121 }
   4122 
   4123 function _checkDataTransferItems(aDataTransfer, aExpectedDragData) {
   4124  try {
   4125    // We must wrap only in plain mochitests, not chrome
   4126    let dataTransfer = _EU_maybeWrap(aDataTransfer);
   4127    if (!dataTransfer) {
   4128      return null;
   4129    }
   4130    if (
   4131      aExpectedDragData == null ||
   4132      dataTransfer.mozItemCount != aExpectedDragData.length
   4133    ) {
   4134      return dataTransfer;
   4135    }
   4136    for (let i = 0; i < dataTransfer.mozItemCount; i++) {
   4137      let dtTypes = dataTransfer.mozTypesAt(i);
   4138      if (dtTypes.length != aExpectedDragData[i].length) {
   4139        return dataTransfer;
   4140      }
   4141      for (let j = 0; j < dtTypes.length; j++) {
   4142        if (dtTypes[j] != aExpectedDragData[i][j].type) {
   4143          return dataTransfer;
   4144        }
   4145        let dtData = dataTransfer.mozGetDataAt(dtTypes[j], i);
   4146        if (aExpectedDragData[i][j].eqTest) {
   4147          if (
   4148            !aExpectedDragData[i][j].eqTest(
   4149              dtData,
   4150              aExpectedDragData[i][j].data
   4151            )
   4152          ) {
   4153            return dataTransfer;
   4154          }
   4155        } else if (aExpectedDragData[i][j].data != dtData) {
   4156          return dataTransfer;
   4157        }
   4158      }
   4159    }
   4160  } catch (ex) {
   4161    return ex;
   4162  }
   4163  return true;
   4164 }
   4165 
   4166 /**
   4167 * @typedef {(actualData: any, expectedData: any) -> boolean} eqTest
   4168 *   This callback type is used with ``synthesizePlainDragAndCancel()``.
   4169 *   It should compare ``actualData`` and ``expectedData`` and return
   4170 *   true if the two should be considered equal, false otherwise.
   4171 */
   4172 
   4173 /**
   4174 * synthesizePlainDragAndCancel() synthesizes drag start with
   4175 * synthesizePlainDragAndDrop(), but always cancel it with preventing default
   4176 * of "dragstart".  Additionally, this checks whether the dataTransfer of
   4177 * "dragstart" event has only expected items.
   4178 *
   4179 * @param {object} aParams
   4180 *        The params which is set to the argument of ``synthesizePlainDragAndDrop()``.
   4181 * @param {Array} aExpectedDataTransferItems
   4182 *        All expected dataTransfer items.
   4183 *        This data is in the format:
   4184 *
   4185 *        [
   4186 *          [
   4187 *            {"type": value, "data": value, "eqTest": eqTest}
   4188 *            ...,
   4189 *          ],
   4190 *          ...
   4191 *        ]
   4192 *
   4193 *        This can also be null.
   4194 *        You can optionally provide ``eqTest`` if the
   4195 *        comparison to the expected data transfer items can't be done
   4196 *        with x == y;
   4197 * @return {boolean}
   4198 *        true if aExpectedDataTransferItems matches with
   4199 *        DragEvent.dataTransfer of "dragstart" event.
   4200 *        Otherwise, the dataTransfer object (may be null) or
   4201 *        thrown exception, NOT false.  Therefore, you shouldn't
   4202 *        use.
   4203 */
   4204 async function synthesizePlainDragAndCancel(
   4205  aParams,
   4206  aExpectedDataTransferItems
   4207 ) {
   4208  let srcElement = aParams.srcSelection
   4209    ? _computeSrcElementFromSrcSelection(aParams.srcSelection)
   4210    : aParams.srcElement;
   4211  let result;
   4212  function onDragStart(aEvent) {
   4213    aEvent.preventDefault();
   4214    result = _checkDataTransferItems(
   4215      aEvent.dataTransfer,
   4216      aExpectedDataTransferItems
   4217    );
   4218  }
   4219  SpecialPowers.wrap(srcElement.ownerDocument).addEventListener(
   4220    "dragstart",
   4221    onDragStart,
   4222    { capture: true, mozSystemGroup: true }
   4223  );
   4224  try {
   4225    aParams.expectCancelDragStart = true;
   4226    await synthesizePlainDragAndDrop(aParams);
   4227  } finally {
   4228    SpecialPowers.wrap(srcElement.ownerDocument).removeEventListener(
   4229      "dragstart",
   4230      onDragStart,
   4231      { capture: true, mozSystemGroup: true }
   4232    );
   4233  }
   4234  return result;
   4235 }
   4236 
   4237 async function _synthesizeMockDndFromChild(aParams) {
   4238  // Since we know that this is the (only) content process that will be involved
   4239  // in the drag, we can set this for the caller.
   4240  const ds = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"].getService(
   4241    SpecialPowers.Ci.nsIDragService
   4242  );
   4243  ds.neverAllowSessionIsSynthesizedForTests = true;
   4244 
   4245  let sourceElt = document.getElementById(aParams.srcElement);
   4246  let targetElt = document.getElementById(aParams.targetElement);
   4247 
   4248  // The spawnChrome call below may return before the DND is complete since
   4249  // the parent process will not synchronize with the child for child-initiated
   4250  // drags.  So we need to wait for the dragend here.  If the drag motion is
   4251  // expected to not result in a drag session then we wait for the mouseup
   4252  // instead.
   4253  let resolveEndPromise;
   4254  let endPromise = new Promise(res => {
   4255    resolveEndPromise = res;
   4256  });
   4257  let endEvent = aParams.expectNoDragEvents ? "mouseup" : "dragend";
   4258  sourceElt.addEventListener(
   4259    endEvent,
   4260    () => {
   4261      resolveEndPromise();
   4262    },
   4263    { once: true }
   4264  );
   4265 
   4266  // The parent call will not get the element positions, so set 'sourceOffset' to
   4267  // the screen coordinates for the drag start and 'targetOffset' for the screen
   4268  // position to drag to.
   4269  const scale = window.devicePixelRatio;
   4270  let sourceOffset = [
   4271    (window.mozInnerScreenX + sourceElt.offsetLeft) * scale +
   4272      aParams.sourceOffset[0],
   4273    (window.mozInnerScreenY + sourceElt.offsetTop) * scale +
   4274      aParams.sourceOffset[1],
   4275  ];
   4276  let targetOffset = [
   4277    (window.mozInnerScreenX + targetElt.offsetLeft) * scale +
   4278      aParams.targetOffset[0],
   4279    (window.mozInnerScreenY + targetElt.offsetTop) * scale +
   4280      aParams.targetOffset[1],
   4281  ];
   4282  let params = {
   4283    srcElement: aParams.srcElement,
   4284    targetElement: aParams.targetElement,
   4285    sourceOffset,
   4286    targetOffset,
   4287    step: aParams.step,
   4288    expectCancelDragStart: aParams.expectCancelDragStart,
   4289    cancel: aParams.cancel,
   4290    expectSrcElementDisconnected: aParams.expectSrcElementDisconnected,
   4291    expectDragLeave: aParams.expectDragLeave,
   4292    expectNoDragEvents: aParams.expectNoDragEvents,
   4293    expectNoDragTargetEvents: aParams.expectNoDragTargetEvents,
   4294    contextLabel: aParams.contextLabel,
   4295    throwOnExtraMessage: aParams.throwOnExtraMessage,
   4296  };
   4297 
   4298  let record =
   4299    aParams.record ||
   4300    ((cond, msg, _, stack) => {
   4301      if (cond) {
   4302        console.error(msg + "\n" + stack);
   4303      }
   4304    });
   4305  let info = aParams.info || console.log;
   4306 
   4307  try {
   4308    await SpecialPowers.spawnChrome([params], async _params => {
   4309      let params = {
   4310        sourceBrowsingCxt: browsingContext,
   4311        targetBrowsingCxt: browsingContext,
   4312        record: () => {},
   4313        info: () => {},
   4314        ..._params,
   4315      };
   4316      await EventUtils.synthesizeMockDragAndDrop(params);
   4317    });
   4318    await endPromise;
   4319  } catch (ex) {
   4320    // Display any exceptions since EventUtils does not propagate them.
   4321    record(
   4322      true,
   4323      `Parent synthesizeMockDragAndDrop threw exception: ${ex}`,
   4324      null,
   4325      ex.stack
   4326    );
   4327  } finally {
   4328    info("Remote synthesizeMockDragAndDrop has completed.");
   4329  }
   4330 }
   4331 
   4332 /**
   4333 * Emulate a drag and drop by generating a dragstart from mousedown and mousemove,
   4334 * then firing events dragover and drop (or dragleave if expectDragLeave is set).
   4335 * This does not modify dataTransfer and tries to emulate the plain drag and
   4336 * drop as much as possible, compared to synthesizeDrop and
   4337 * synthesizePlainDragAndDrop.  MockDragService is used in place of the native
   4338 * nsIDragService implementation.  All coordinates are in client space.
   4339 *
   4340 * This method can be called from the parent process, in which case it will
   4341 * perform checks of DND internals (if 'record' is set).  It can also be
   4342 * called from content processes, in which case the drag is over the window
   4343 * that is in context, and no checks of DND internals will occur.
   4344 *
   4345 * @param {object} aParams
   4346 * @param {Window} aParams.sourceBrowsingCxt
   4347 *                The BrowsingContext (possibly remote) that contains
   4348 *                srcElement.  Only set in parent process.
   4349 * @param {Window} aParams.targetBrowsingCxt
   4350 *                The BrowsingContext (possibly remote) that contains
   4351 *                targetElement.  Only set in parent process.
   4352 *                Default is sourceBrowsingCxt.
   4353 * @param {Element} aParams.srcElement
   4354 *                ID of the element to drag.
   4355 * @param {Element|null} aParams.targetElement
   4356 *                ID of the element to drop on.
   4357 * @param {number} aParams.sourceOffset
   4358 *                The 2D offset from the source element at which the drag
   4359 *                starts.  Default is [0,0].
   4360 * @param {number} aParams.targetOffset
   4361 *                The 2D offset from the target element at which the drag ends.
   4362 *                Default is [0,0].
   4363 * @param {number} aParams.step
   4364 *                The 2D step for intermediate dragging mousemoves.
   4365 *                Default is [5,5].
   4366 * @param {boolean} aParams.expectCancelDragStart
   4367 *                Set to true if srcElement is set up to cancel "dragstart"
   4368 * @param {number} aParams.cancel
   4369 *                The 2D coord the mouse is moved to as the last step if
   4370 *                expectCancelDragStart is set
   4371 * @param {boolean} aParams.expectSrcElementDisconnected
   4372 *                Set to true if srcElement will be disconnected and
   4373 *                "dragend" event won't be fired.
   4374 * @param {boolean} aParams.expectDragLeave
   4375 *                Set to true if the drop event will be converted to a
   4376 *                dragleave before it is sent (e.g. it was rejected by a
   4377 *                content analysis check).
   4378 * @param {boolean} aParams.expectNoDragEvents
   4379 *                Set to true if no mouse or drag events should be received
   4380 *                on the source or target.
   4381 * @param {boolean} aParams.expectNoDragTargetEvents
   4382 *                Set to true if the drag should be blocked from sending
   4383 *                events to the target.
   4384 * @param {boolean} aParams.dropPromise
   4385 *                A promise that the caller will resolve before we check
   4386 *                that the drop has happened.  Default is a pre-resolved
   4387 *                promise.
   4388 * @param {string} aParms.contextLabel
   4389 *                Label that will appear in each output message.  Useful to
   4390 *                distinguish between concurrent calls.  Default is none.
   4391 * @param {boolean} aParams.throwOnExtraMessage
   4392 *                Throw an exception in child process when an unexpected
   4393 *                event is received.  Used for debugging.  Default is false.
   4394 * @param {Function} aParams.record
   4395 *                Four-parameter function that logs the results of a remote
   4396 *                assertion.  The parameters are (condition, message, ignored,
   4397 *                stack).  This is the type of the mochitest report function.
   4398 *                Pass the empty function, or call this from content, to skip
   4399 *                testing of DND internals.
   4400 *                This parameter is required in the parent process and is
   4401 *                optional in content processes.
   4402 * @param {Function} aParams.info
   4403 *                One-parameter info logging function.  This is the type of
   4404 *                the mochitest info function.  Pass the empty function, or
   4405 *                call this from content, to skip testing of DND internals.
   4406 *                This parameter is required in the parent process and is
   4407 *                optional in content processes.
   4408 * @param {object} aParams.dragController
   4409 *                MockDragController that the function should use.  This
   4410 *                function will automatically generate one if none is given.
   4411 *                This can only be set in the parent process.
   4412 */
   4413 // eslint-disable-next-line complexity
   4414 async function synthesizeMockDragAndDrop(aParams) {
   4415  // eslint-disable-next-line mozilla/use-services
   4416  let appinfo = _EU_Cc["@mozilla.org/xre/app-info;1"].getService(
   4417    _EU_Ci.nsIXULRuntime
   4418  );
   4419  if (appinfo.processType !== appinfo.PROCESS_TYPE_DEFAULT) {
   4420    await _synthesizeMockDndFromChild(aParams);
   4421    return;
   4422  }
   4423 
   4424  const {
   4425    srcElement,
   4426    targetElement,
   4427    sourceOffset = [0, 0],
   4428    targetOffset = [0, 0],
   4429    step = [5, 5],
   4430    cancel = [0, 0],
   4431    sourceBrowsingCxt,
   4432    targetBrowsingCxt = sourceBrowsingCxt,
   4433    expectCancelDragStart = false,
   4434    expectSrcElementDisconnected = false,
   4435    expectDragLeave = false,
   4436    expectNoDragEvents = false,
   4437    dropPromise = Promise.resolve(undefined),
   4438    contextLabel = "",
   4439    throwOnExtraMessage = false,
   4440  } = aParams;
   4441 
   4442  let { dragController = null, expectNoDragTargetEvents = false } = aParams;
   4443 
   4444  // Configure test reporting functions
   4445  const prefix = contextLabel ? `[${contextLabel}]| ` : "";
   4446  const info = msg => {
   4447    aParams.info(`${prefix}${msg}`);
   4448  };
   4449  const record = (cond, msg, _, stack) => {
   4450    aParams.record(cond, `${prefix}${msg}`, null, stack);
   4451  };
   4452  const ok = (cond, msg) => {
   4453    record(cond, msg, null, Components.stack.caller);
   4454  };
   4455 
   4456  info("synthesizeMockDragAndDrop() -- START");
   4457 
   4458  // Validate parameters
   4459  ok(sourceBrowsingCxt, "sourceBrowsingCxt was given");
   4460  ok(
   4461    sourceBrowsingCxt != targetBrowsingCxt || srcElement != targetElement,
   4462    "sourceBrowsingCxt+Element cannot be the same as targetBrowsingCxt+Element"
   4463  );
   4464 
   4465  // no drag implies no drag target
   4466  expectNoDragTargetEvents |= expectNoDragEvents;
   4467 
   4468  // Returns true if one browsing context is an ancestor of the other.
   4469  let browsingContextsAreRelated = function (cxt1, cxt2) {
   4470    return cxt1.top == cxt2.top;
   4471  };
   4472 
   4473  // The rules for accessing the dataTransfer from internal drags in Gecko
   4474  // during drag event handlers are as follows:
   4475  //
   4476  // dragstart:
   4477  //   Always grants read-write access
   4478  // dragenter/dragover/dragleave:
   4479  //   If dom.events.dataTransfer.protected.enabled is set:
   4480  //     Read-only permission is granted if any of these holds:
   4481  //       * The drag target's browsing context is the same as the drag
   4482  //         source's (e.g. dragging inside of one frame on a web page).
   4483  //       * The drag source and target are the same domain/principal and
   4484  //         one has a browsing context that is an ancestor of the other
   4485  //         (e.g. one is an iframe nested inside of the other).
   4486  //       * The principal of the drag target element is privileged (not
   4487  //         a content principal).
   4488  //   Otherwise:
   4489  //     Permission is never granted
   4490  // drop:
   4491  //   Always grants read-only permission
   4492  // dragend:
   4493  //   Read-only permission is granted if
   4494  //   dom.events.dataTransfer.protected.enabled is set.
   4495  //
   4496  // dragstart and dragend are special because they target the drag-source,
   4497  // not the drag-target.
   4498  // eslint-disable-next-line mozilla/use-services
   4499  let prefs = _EU_Cc["@mozilla.org/preferences-service;1"].getService(
   4500    Ci.nsIPrefBranch
   4501  );
   4502  let expectProtectedDataTransferAccessSource = !prefs.getBoolPref(
   4503    "dom.events.dataTransfer.protected.enabled"
   4504  );
   4505  let expectProtectedDataTransferAccessTarget =
   4506    expectProtectedDataTransferAccessSource &&
   4507    browsingContextsAreRelated(targetBrowsingCxt, sourceBrowsingCxt);
   4508 
   4509  info(
   4510    `expectProtectedDataTransferAccessSource: ${expectProtectedDataTransferAccessSource}`
   4511  );
   4512  info(
   4513    `expectProtectedDataTransferAccessTarget: ${expectProtectedDataTransferAccessTarget}`
   4514  );
   4515 
   4516  // Essentially the entire function is in a try block so that we can make sure
   4517  // that the mock drag service is removed and non-test mouse events are
   4518  // restored.
   4519  const { MockRegistrar } = ChromeUtils.importESModule(
   4520    "resource://testing-common/MockRegistrar.sys.mjs"
   4521  );
   4522  let dragServiceCid;
   4523  let sourceCxt;
   4524  let targetCxt;
   4525  let srcWindowUtils = _getDOMWindowUtils(sourceBrowsingCxt.ownerGlobal);
   4526  let targetWindowUtils = _getDOMWindowUtils(targetBrowsingCxt.ownerGlobal);
   4527 
   4528  try {
   4529    // Disable native mouse events to avoid external interference while the test
   4530    // runs.  One call disables for all windows.
   4531    if (srcWindowUtils) {
   4532      srcWindowUtils.disableNonTestMouseEvents(true);
   4533    }
   4534    if (targetWindowUtils) {
   4535      targetWindowUtils.disableNonTestMouseEvents(true);
   4536    }
   4537 
   4538    // Install mock drag service.
   4539    if (!dragController) {
   4540      info("No dragController was given so creating mock drag service");
   4541      const oldDragService = _EU_Cc[
   4542        "@mozilla.org/widget/dragservice;1"
   4543      ].getService(_EU_Ci.nsIDragService);
   4544      dragController = oldDragService.getMockDragController();
   4545      dragServiceCid = MockRegistrar.register(
   4546        "@mozilla.org/widget/dragservice;1",
   4547        dragController.mockDragService
   4548      );
   4549      ok(dragServiceCid, "MockDragService was registered");
   4550      // If the mock failed then don't continue or else we will trigger native
   4551      // DND behavior.
   4552      if (!dragServiceCid) {
   4553        throw new Error("MockDragService failed to register");
   4554      }
   4555    }
   4556 
   4557    const mockDragService = _EU_Cc[
   4558      "@mozilla.org/widget/dragservice;1"
   4559    ].getService(_EU_Ci.nsIDragService);
   4560 
   4561    let runChecks = globalThis.hasOwnProperty("SpecialPowers");
   4562    if (runChecks) {
   4563      // Variables that are added to the child actor objects.
   4564      const srcVars = {
   4565        expectCancelDragStart,
   4566        expectSrcElementDisconnected,
   4567        expectNoDragEvents,
   4568        expectProtectedDataTransferAccess:
   4569          expectProtectedDataTransferAccessSource,
   4570        dragElementId: srcElement,
   4571      };
   4572      const targetVars = {
   4573        expectDragLeave,
   4574        expectNoDragTargetEvents,
   4575        expectProtectedDataTransferAccess:
   4576          expectProtectedDataTransferAccessTarget,
   4577        dragElementId: targetElement,
   4578      };
   4579      const bothVars = {
   4580        contextLabel,
   4581        throwOnExtraMessage,
   4582        relevantEvents: [
   4583          "mousedown",
   4584          "mouseup",
   4585          "dragstart",
   4586          "dragenter",
   4587          "dragover",
   4588          "drop",
   4589          "dragleave",
   4590          "dragend",
   4591        ],
   4592      };
   4593 
   4594      const makeDragSourceContext = async (aBC, aRemoteVars) => {
   4595        let { DragSourceParentContext } = ChromeUtils.importESModule(
   4596          "chrome://mochikit/content/tests/SimpleTest/DragSourceParentContext.sys.mjs"
   4597        );
   4598 
   4599        let ret = new DragSourceParentContext(aBC, aRemoteVars, SpecialPowers);
   4600        await ret.initialize();
   4601        return ret;
   4602      };
   4603 
   4604      const makeDragTargetContext = async (aBC, aRemoteVars) => {
   4605        let { DragTargetParentContext } = ChromeUtils.importESModule(
   4606          "chrome://mochikit/content/tests/SimpleTest/DragTargetParentContext.sys.mjs"
   4607        );
   4608 
   4609        let ret = new DragTargetParentContext(aBC, aRemoteVars, SpecialPowers);
   4610        await ret.initialize();
   4611        return ret;
   4612      };
   4613 
   4614      [sourceCxt, targetCxt] = await Promise.all([
   4615        makeDragSourceContext(sourceBrowsingCxt, { ...srcVars, ...bothVars }),
   4616        makeDragTargetContext(targetBrowsingCxt, {
   4617          ...targetVars,
   4618          ...bothVars,
   4619        }),
   4620      ]);
   4621    } else {
   4622      // We don't have SpecialPowers so we cannot perform DND checks.  Make
   4623      // them empty functions.
   4624      info("synthesizeMockDragAndDrop will skip DND checks");
   4625      let dragParentBaseCxt = {
   4626        expect: () => {},
   4627        checkExpected: () => {},
   4628        checkHasDrag: () => {},
   4629        checkSessionHasAction: () => {},
   4630        synchronize: () => {},
   4631        cleanup: () => {},
   4632      };
   4633      sourceCxt = {
   4634        getElementPositions: () => {
   4635          return { screenPos: [0, 0] };
   4636        },
   4637        checkMouseDown: () => {},
   4638        checkDragStart: () => {},
   4639        checkDragEnd: () => {},
   4640        ...dragParentBaseCxt,
   4641      };
   4642      targetCxt = {
   4643        getElementPositions: () => {
   4644          return { screenPos: [0, 0] };
   4645        },
   4646        checkDropOrDragLeave: () => {},
   4647        ...dragParentBaseCxt,
   4648      };
   4649    }
   4650 
   4651    // Get element positions in screen coords
   4652    let add2d = (a, b) => {
   4653      return [a[0] + b[0], a[1] + b[1]];
   4654    };
   4655    let srcPos = add2d(
   4656      (await sourceCxt.getElementPositions()).screenPos,
   4657      sourceOffset
   4658    );
   4659    let targetPos = add2d(
   4660      (await targetCxt.getElementPositions()).screenPos,
   4661      targetOffset
   4662    );
   4663    info(`screenSrcPos: ${srcPos} | screenTargetPos: ${targetPos}`);
   4664 
   4665    // Send and verify the mousedown on src.
   4666    if (!expectNoDragEvents) {
   4667      sourceCxt.expect("mousedown");
   4668    }
   4669 
   4670    // Take ceiling of ccoordinates to make sure that the integer coordinates
   4671    // are over the element.
   4672    let currentSrcScreenPos = [Math.ceil(srcPos[0]), Math.ceil(srcPos[1])];
   4673    info(
   4674      `sending mousedown at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}`
   4675    );
   4676    dragController.sendEvent(
   4677      sourceBrowsingCxt,
   4678      Ci.nsIMockDragServiceController.eMouseDown,
   4679      currentSrcScreenPos[0],
   4680      currentSrcScreenPos[1]
   4681    );
   4682    info(`mousedown sent`);
   4683 
   4684    await sourceCxt.synchronize();
   4685 
   4686    await sourceCxt.checkMouseDown();
   4687 
   4688    let contentInvokedDragPromise;
   4689 
   4690    info("setting up content-invoked-drag observer and expecting dragstart");
   4691    if (!expectNoDragEvents) {
   4692      sourceCxt.expect("dragstart");
   4693      // Set up observable for content-invoked-drag, which is sent when the
   4694      // parent learns that content has begun a drag session.
   4695      contentInvokedDragPromise = new Promise(cb => {
   4696        Services.obs.addObserver(function observe() {
   4697          info("content-invoked-drag observer received message");
   4698          Services.obs.removeObserver(observe, "content-invoked-drag");
   4699          cb();
   4700        }, "content-invoked-drag");
   4701      });
   4702    }
   4703 
   4704    // It takes two mouse-moves to initiate a drag session.
   4705    currentSrcScreenPos = [
   4706      currentSrcScreenPos[0] + step[0],
   4707      currentSrcScreenPos[1] + step[1],
   4708    ];
   4709    info(
   4710      `first mousemove at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}`
   4711    );
   4712    dragController.sendEvent(
   4713      sourceBrowsingCxt,
   4714      Ci.nsIMockDragServiceController.eMouseMove,
   4715      currentSrcScreenPos[0],
   4716      currentSrcScreenPos[1]
   4717    );
   4718    info(`first mousemove sent`);
   4719 
   4720    currentSrcScreenPos = [
   4721      currentSrcScreenPos[0] + step[0],
   4722      currentSrcScreenPos[1] + step[1],
   4723    ];
   4724    info(
   4725      `second mousemove at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}`
   4726    );
   4727    dragController.sendEvent(
   4728      sourceBrowsingCxt,
   4729      Ci.nsIMockDragServiceController.eMouseMove,
   4730      currentSrcScreenPos[0],
   4731      currentSrcScreenPos[1]
   4732    );
   4733    info(`second mousemove sent`);
   4734 
   4735    if (!expectNoDragEvents) {
   4736      info("waiting for content-invoked-drag observable");
   4737      await contentInvokedDragPromise;
   4738      ok(true, "content-invoked-drag was received");
   4739    }
   4740 
   4741    info("checking dragstart");
   4742    await sourceCxt.checkDragStart();
   4743 
   4744    if (expectNoDragEvents) {
   4745      ok(
   4746        !mockDragService.getCurrentSession(),
   4747        "Drag was properly blocked from starting."
   4748      );
   4749      dragController.sendEvent(
   4750        sourceBrowsingCxt,
   4751        Ci.nsIMockDragServiceController.eMouseUp,
   4752        cancel[0],
   4753        cancel[1]
   4754      );
   4755      return;
   4756    }
   4757 
   4758    // Another move creates the drag session in the parent process (but we need
   4759    // to wait for the src process to get there).
   4760    currentSrcScreenPos = [
   4761      currentSrcScreenPos[0] + step[0],
   4762      currentSrcScreenPos[1] + step[1],
   4763    ];
   4764    info(
   4765      `third mousemove at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}`
   4766    );
   4767    dragController.sendEvent(
   4768      sourceBrowsingCxt,
   4769      Ci.nsIMockDragServiceController.eMouseMove,
   4770      currentSrcScreenPos[0],
   4771      currentSrcScreenPos[1]
   4772    );
   4773    info(`third mousemove sent`);
   4774 
   4775    ok(mockDragService.getCurrentSession(), `Parent process has drag session.`);
   4776 
   4777    if (expectCancelDragStart) {
   4778      dragController.sendEvent(
   4779        sourceBrowsingCxt,
   4780        Ci.nsIMockDragServiceController.eMouseUp,
   4781        cancel[0],
   4782        cancel[1]
   4783      );
   4784      return;
   4785    }
   4786 
   4787    await sourceCxt.checkExpected();
   4788 
   4789    // Implementation detail: EventStateManager::GenerateDragDropEnterExit
   4790    // expects the source to get at least one dragover before leaving the
   4791    // widget or else it fails to send dragenter/dragleave events to the
   4792    // browsers.
   4793    info("synthesizing dragover inside source");
   4794    sourceCxt.expect("dragenter");
   4795    sourceCxt.expect("dragover");
   4796    currentSrcScreenPos = [
   4797      currentSrcScreenPos[0] + step[0],
   4798      currentSrcScreenPos[1] + step[1],
   4799    ];
   4800    info(`dragover at ${currentSrcScreenPos[0]}, ${currentSrcScreenPos[1]}`);
   4801    dragController.sendEvent(
   4802      sourceBrowsingCxt,
   4803      Ci.nsIMockDragServiceController.eDragOver,
   4804      currentSrcScreenPos[0],
   4805      currentSrcScreenPos[1]
   4806    );
   4807 
   4808    info(`dragover sent`);
   4809    await sourceCxt.checkExpected();
   4810 
   4811    let currentTargetScreenPos = [
   4812      Math.ceil(targetPos[0]),
   4813      Math.ceil(targetPos[1]),
   4814    ];
   4815 
   4816    // The next step is to drag to the target element.
   4817    if (!expectNoDragTargetEvents) {
   4818      sourceCxt.expect("dragleave");
   4819    }
   4820 
   4821    if (
   4822      sourceBrowsingCxt.top.embedderElement !==
   4823      targetBrowsingCxt.top.embedderElement
   4824    ) {
   4825      // Send dragexit and dragenter only if we are dragging to another widget.
   4826      // If we are dragging in the same widget then dragenter does not involve
   4827      // the parent process.  This mirrors the native behavior. In the
   4828      // widget-to-widget case, the source gets the dragexit immediately but
   4829      // the target won't get a dragenter in content until we send a dragover --
   4830      // this is because dragenters are generated by the EventStateManager and
   4831      // are not forwarded remotely.
   4832      // NB: dragleaves are synthesized by Gecko from dragexits.
   4833      info("synthesizing dragexit and dragenter to enter new widget");
   4834      if (!expectNoDragTargetEvents) {
   4835        info("This will generate dragleave on the source");
   4836      }
   4837 
   4838      dragController.sendEvent(
   4839        sourceBrowsingCxt,
   4840        Ci.nsIMockDragServiceController.eDragExit,
   4841        currentTargetScreenPos[0],
   4842        currentTargetScreenPos[1]
   4843      );
   4844 
   4845      dragController.sendEvent(
   4846        targetBrowsingCxt,
   4847        Ci.nsIMockDragServiceController.eDragEnter,
   4848        currentTargetScreenPos[0],
   4849        currentTargetScreenPos[1]
   4850      );
   4851 
   4852      await sourceCxt.synchronize();
   4853 
   4854      await sourceCxt.checkExpected();
   4855      await targetCxt.checkExpected();
   4856    }
   4857 
   4858    info(
   4859      "Synthesizing dragover over target.  This will first generate a dragenter."
   4860    );
   4861    if (!expectNoDragTargetEvents) {
   4862      targetCxt.expect("dragenter");
   4863      targetCxt.expect("dragover");
   4864    }
   4865 
   4866    dragController.sendEvent(
   4867      targetBrowsingCxt,
   4868      Ci.nsIMockDragServiceController.eDragOver,
   4869      currentTargetScreenPos[0],
   4870      currentTargetScreenPos[1]
   4871    );
   4872 
   4873    await targetCxt.checkExpected();
   4874 
   4875    let expectedMessage = expectDragLeave ? "dragleave" : "drop";
   4876 
   4877    if (expectNoDragTargetEvents) {
   4878      await targetCxt.checkHasDrag(false);
   4879    } else {
   4880      await targetCxt.checkSessionHasAction();
   4881      targetCxt.expect(expectedMessage);
   4882    }
   4883 
   4884    if (!expectSrcElementDisconnected) {
   4885      await sourceCxt.checkHasDrag(true);
   4886      sourceCxt.expect("dragend");
   4887    }
   4888 
   4889    info(
   4890      `issuing drop event that should be ` +
   4891        `${
   4892          !expectNoDragTargetEvents
   4893            ? `received as a ${expectedMessage} event`
   4894            : "ignored"
   4895        }, followed by a dragend event`
   4896    );
   4897 
   4898    currentTargetScreenPos = [
   4899      currentTargetScreenPos[0] + step[0],
   4900      currentTargetScreenPos[1] + step[1],
   4901    ];
   4902    dragController.sendEvent(
   4903      targetBrowsingCxt,
   4904      Ci.nsIMockDragServiceController.eDrop,
   4905      currentTargetScreenPos[0],
   4906      currentTargetScreenPos[1]
   4907    );
   4908 
   4909    // Wait for any caller-supplied dropPromise before continuing.
   4910    await dropPromise;
   4911 
   4912    if (!expectNoDragTargetEvents) {
   4913      await targetCxt.checkDropOrDragLeave();
   4914    } else {
   4915      await targetCxt.checkExpected();
   4916    }
   4917 
   4918    if (!expectSrcElementDisconnected) {
   4919      await sourceCxt.checkDragEnd();
   4920    } else {
   4921      await sourceCxt.checkExpected();
   4922    }
   4923 
   4924    ok(
   4925      !mockDragService.getCurrentSession(),
   4926      `Parent process does not have a drag session.`
   4927    );
   4928  } catch (e) {
   4929    // Any exception is a test failure.
   4930    record(false, e.toString(), null, e.stack);
   4931    throw e;
   4932  } finally {
   4933    if (sourceCxt) {
   4934      await sourceCxt.cleanup();
   4935    }
   4936    if (targetCxt) {
   4937      await targetCxt.cleanup();
   4938    }
   4939 
   4940    if (dragServiceCid) {
   4941      MockRegistrar.unregister(dragServiceCid);
   4942    }
   4943 
   4944    if (srcWindowUtils) {
   4945      srcWindowUtils.disableNonTestMouseEvents(false);
   4946    }
   4947    if (targetWindowUtils) {
   4948      targetWindowUtils.disableNonTestMouseEvents(false);
   4949    }
   4950 
   4951    info("synthesizeMockDragAndDrop() -- END");
   4952  }
   4953 }
   4954 
   4955 class EventCounter {
   4956  constructor(aTarget, aType, aOptions = {}) {
   4957    this.target = aTarget;
   4958    this.type = aType;
   4959    this.options = aOptions;
   4960 
   4961    this.eventCount = 0;
   4962    // Bug 1512817:
   4963    // SpecialPowers is picky and needs to be passed an explicit reference to
   4964    // the function to be called. To avoid having to bind "this", we therefore
   4965    // define the method this way, via a property.
   4966    this.handleEvent = () => {
   4967      this.eventCount++;
   4968    };
   4969 
   4970    SpecialPowers.wrap(aTarget).addEventListener(
   4971      aType,
   4972      this.handleEvent,
   4973      aOptions
   4974    );
   4975  }
   4976 
   4977  unregister() {
   4978    SpecialPowers.wrap(this.target).removeEventListener(
   4979      this.type,
   4980      this.handleEvent,
   4981      this.options
   4982    );
   4983  }
   4984 
   4985  get count() {
   4986    return this.eventCount;
   4987  }
   4988 }