tor-browser

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

apz_test_native_event_utils.js (62405B)


      1 // ownerGlobal isn't defined in content privileged windows.
      2 /* eslint-disable mozilla/use-ownerGlobal */
      3 
      4 // Utilities for synthesizing of native events.
      5 
      6 async function getResolution() {
      7  let resolution = -1; // bogus value in case DWU fails us
      8  // Use window.top to get the root content window which is what has
      9  // the resolution.
     10  resolution = await SpecialPowers.spawn(window.top, [], () => {
     11    return SpecialPowers.getDOMWindowUtils(content.window).getResolution();
     12  });
     13  return resolution;
     14 }
     15 
     16 function getPlatform() {
     17  if (navigator.platform.indexOf("Win") == 0) {
     18    return "windows";
     19  }
     20  if (navigator.platform.indexOf("Mac") == 0) {
     21    return "mac";
     22  }
     23  // Check for Android before Linux
     24  if (navigator.appVersion.includes("Android")) {
     25    return "android";
     26  }
     27  if (navigator.platform.indexOf("Linux") == 0) {
     28    return "linux";
     29  }
     30  return "unknown";
     31 }
     32 
     33 function nativeVerticalWheelEventMsg() {
     34  switch (getPlatform()) {
     35    case "windows":
     36      return 0x020a; // WM_MOUSEWHEEL
     37    case "mac":
     38      var useWheelCodepath = SpecialPowers.getBoolPref(
     39        "apz.test.mac.synth_wheel_input",
     40        false
     41      );
     42      // Default to 1 (kCGScrollPhaseBegan) to trigger PanGestureInput events
     43      // from widget code. Allow setting a pref to override this behaviour and
     44      // trigger ScrollWheelInput events instead.
     45      return useWheelCodepath ? 0 : 1;
     46    case "linux":
     47      return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
     48  }
     49  throw new Error(
     50    "Native wheel events not supported on platform " + getPlatform()
     51  );
     52 }
     53 
     54 function nativeHorizontalWheelEventMsg() {
     55  switch (getPlatform()) {
     56    case "windows":
     57      return 0x020e; // WM_MOUSEHWHEEL
     58    case "mac":
     59      return 0; // value is unused, can be anything
     60    case "linux":
     61      return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway
     62  }
     63  throw new Error(
     64    "Native wheel events not supported on platform " + getPlatform()
     65  );
     66 }
     67 
     68 function nativeArrowDownKey() {
     69  switch (getPlatform()) {
     70    case "windows":
     71      return WIN_VK_DOWN;
     72    case "mac":
     73      return MAC_VK_DownArrow;
     74  }
     75  throw new Error(
     76    "Native key events not supported on platform " + getPlatform()
     77  );
     78 }
     79 
     80 function nativeArrowUpKey() {
     81  switch (getPlatform()) {
     82    case "windows":
     83      return WIN_VK_UP;
     84    case "mac":
     85      return MAC_VK_UpArrow;
     86  }
     87  throw new Error(
     88    "Native key events not supported on platform " + getPlatform()
     89  );
     90 }
     91 
     92 function targetIsWindow(aTarget) {
     93  return aTarget.Window && aTarget instanceof aTarget.Window;
     94 }
     95 
     96 function targetIsTopWindow(aTarget) {
     97  if (!targetIsWindow(aTarget)) {
     98    return false;
     99  }
    100  return aTarget == aTarget.top;
    101 }
    102 
    103 // Given an event target which may be a window or an element, get the associated window.
    104 function windowForTarget(aTarget) {
    105  if (targetIsWindow(aTarget)) {
    106    return aTarget;
    107  }
    108  return aTarget.ownerDocument.defaultView;
    109 }
    110 
    111 // Given an event target which may be a window or an element, get the associated element.
    112 function elementForTarget(aTarget) {
    113  if (targetIsWindow(aTarget)) {
    114    return aTarget.document.documentElement;
    115  }
    116  return aTarget;
    117 }
    118 
    119 // Given an event target which may be a window or an element, get the associatd nsIDOMWindowUtils.
    120 function utilsForTarget(aTarget) {
    121  return SpecialPowers.getDOMWindowUtils(windowForTarget(aTarget));
    122 }
    123 
    124 // Given a pixel scrolling delta, converts it to the platform's native units.
    125 function nativeScrollUnits(aTarget, aDimen) {
    126  switch (getPlatform()) {
    127    case "linux": {
    128      // GTK deltas are treated as line height divided by 3 by gecko.
    129      var targetWindow = windowForTarget(aTarget);
    130      var targetElement = elementForTarget(aTarget);
    131      var lineHeight =
    132        targetWindow.getComputedStyle(targetElement)["font-size"];
    133      return aDimen / (parseInt(lineHeight) * 3);
    134    }
    135  }
    136  return aDimen;
    137 }
    138 
    139 function parseNativeModifiers(aModifiers, aWindow = window) {
    140  let modifiers = 0;
    141  if (aModifiers.capsLockKey) {
    142    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK;
    143  }
    144  if (aModifiers.numLockKey) {
    145    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK;
    146  }
    147  if (aModifiers.shiftKey) {
    148    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT;
    149  }
    150  if (aModifiers.shiftRightKey) {
    151    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT;
    152  }
    153  if (aModifiers.ctrlKey) {
    154    modifiers |=
    155      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
    156  }
    157  if (aModifiers.ctrlRightKey) {
    158    modifiers |=
    159      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
    160  }
    161  if (aModifiers.altKey) {
    162    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT;
    163  }
    164  if (aModifiers.altRightKey) {
    165    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT;
    166  }
    167  if (aModifiers.metaKey) {
    168    modifiers |=
    169      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT;
    170  }
    171  if (aModifiers.metaRightKey) {
    172    modifiers |=
    173      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT;
    174  }
    175  if (aModifiers.helpKey) {
    176    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP;
    177  }
    178  if (aModifiers.fnKey) {
    179    modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION;
    180  }
    181  if (aModifiers.numericKeyPadKey) {
    182    modifiers |=
    183      SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD;
    184  }
    185 
    186  if (aModifiers.accelKey) {
    187    modifiers |= _EU_isMac(aWindow)
    188      ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT
    189      : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT;
    190  }
    191  if (aModifiers.accelRightKey) {
    192    modifiers |= _EU_isMac(aWindow)
    193      ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT
    194      : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT;
    195  }
    196  if (aModifiers.altGrKey) {
    197    modifiers |= _EU_isMac(aWindow)
    198      ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT
    199      : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH;
    200  }
    201  return modifiers;
    202 }
    203 
    204 // Several event sythesization functions below (and their helpers) take a "target"
    205 // parameter which may be either an element or a window. For such functions,
    206 // the target's "bounding rect" refers to the bounding client rect for an element,
    207 // and the window's origin for a window.
    208 // Not all functions have been "upgraded" to allow a window argument yet; feel
    209 // free to upgrade others as necessary.
    210 
    211 // Get the origin of |aTarget| relative to the root content document's
    212 // visual viewport in CSS coordinates.
    213 // |aTarget| may be an element (contained in the root content document or
    214 // a subdocument) or, as a special case, the root content window.
    215 // FIXME: Support iframe windows as targets.
    216 function _getTargetRect(aTarget, atCenter) {
    217  let rect = { left: 0, top: 0, width: 0, height: 0 };
    218 
    219  aTarget = SpecialPowers.wrap(aTarget);
    220  let containingWindow = null;
    221  if (
    222    aTarget instanceof Window ||
    223    (aTarget.Window && aTarget instanceof aTarget.Window)
    224  ) {
    225    // If the target is the root content window, its origin relative
    226    // to the visual viewport is (0, 0).
    227 
    228    // FIXME: Compute proper rect against the root content window
    229 
    230    // leave rect as all 0's. The top/left is correct, but the width/height is
    231    // not necessarily correct, just assert that we are not sending event to the
    232    // center of the target so that we are not using the width/height.
    233    ok(!atCenter, "atCenter not supported with window targets, todo");
    234    containingWindow = aTarget;
    235  } else {
    236    // Otherwise, we have an element. Start with the origin of
    237    // its bounding client rect which is relative to the enclosing
    238    // document's layout viewport. Note that for iframes, the
    239    // layout viewport is also the visual viewport.
    240 
    241    const boundingClientRect = aTarget.getBoundingClientRect();
    242    rect.left = boundingClientRect.left;
    243    rect.top = boundingClientRect.top;
    244    rect.width = boundingClientRect.width;
    245    rect.height = boundingClientRect.height;
    246    containingWindow = aTarget.ownerDocument.defaultView;
    247  }
    248 
    249  // Iterate up the window hierarchy until we reach the root
    250  // content window, adding the offsets of any iframe windows
    251  // relative to their parent window.
    252  while (containingWindow.browsingContext.embedderElement) {
    253    const iframe = containingWindow.browsingContext.embedderElement;
    254    // The offset of the iframe window relative to the parent window
    255    // includes the iframe's border, and the iframe's origin in its
    256    // containing document.
    257    const style = iframe.ownerDocument.defaultView.getComputedStyle(iframe);
    258    const borderLeft = parseFloat(style.borderLeftWidth) || 0;
    259    const borderTop = parseFloat(style.borderTopWidth) || 0;
    260    const borderRight = parseFloat(style.borderRightWidth) || 0;
    261    const borderBottom = parseFloat(style.borderBottomWidth) || 0;
    262    const paddingLeft = parseFloat(style.paddingLeft) || 0;
    263    const paddingTop = parseFloat(style.paddingTop) || 0;
    264    const paddingRight = parseFloat(style.paddingRight) || 0;
    265    const paddingBottom = parseFloat(style.paddingBottom) || 0;
    266    const iframeRect = iframe.getBoundingClientRect();
    267    rect.left += iframeRect.left + borderLeft + paddingLeft;
    268    rect.top += iframeRect.top + borderTop + paddingTop;
    269    if (
    270      rect.left + rect.width >
    271      iframeRect.right - borderRight - paddingRight
    272    ) {
    273      rect.width = Math.max(
    274        iframeRect.right - borderRight - paddingRight - rect.left,
    275        0
    276      );
    277    }
    278    if (
    279      rect.top + rect.height >
    280      iframeRect.bottom - borderBottom - paddingBottom
    281    ) {
    282      rect.height = Math.max(
    283        iframeRect.bottom - borderBottom - paddingBottom - rect.top,
    284        0
    285      );
    286    }
    287    aTarget = iframe;
    288    containingWindow = aTarget.ownerDocument.defaultView;
    289  }
    290 
    291  return { rect, window: containingWindow };
    292 }
    293 
    294 // Returns the in-process root window for the given |aWindow|.
    295 function getInProcessRootWindow(aWindow) {
    296  let window = SpecialPowers.wrap(aWindow);
    297  while (window.browsingContext.embedderElement) {
    298    window = window.browsingContext.embedderElement.ownerDocument.defaultView;
    299  }
    300  return window;
    301 }
    302 
    303 // Convert (offsetX, offsetY) of target or center of it, in CSS pixels to device
    304 // pixels relative to the screen.
    305 // TODO: this function currently does not incorporate some CSS transforms on
    306 // elements enclosing target, e.g. scale transforms.
    307 async function coordinatesRelativeToScreen(aParams) {
    308  const {
    309    target, // The target element or window
    310    offsetX, // X offset relative to `target`
    311    offsetY, // Y offset relative to `target`
    312    atCenter, // Instead of offsetX/offsetY, return center of `target`
    313  } = aParams;
    314  // Note that |window| might not be the root content window, for two
    315  // possible reasons:
    316  //  1. The mochitest that's calling into this function is not using a mechanism
    317  //     like runSubtestsSeriallyInFreshWindows() to load the test page in
    318  //     a top-level context, so it's loaded into an iframe by the mochitest
    319  //     harness.
    320  //  2. The mochitest itself creates an iframe and calls this function from
    321  //     script running in the context of the iframe.
    322  // Since the resolution applies to the top level content document, below we
    323  // use the mozInnerScreen{X,Y} of the top level content window (window.top)
    324  // only for the case where this function gets called in the top level content
    325  // document. In other cases we use nsIDOMWindowUtils.toScreenRect().
    326 
    327  // We do often specify `window` as the target, if it's the top level window,
    328  // `nsIDOMWindowUtils.toScreenRect` isn't suitable because the function is
    329  // supposed to be called with values in the document coords, so for example
    330  // if desktop zoom is being applied, (0, 0) in the document coords might be
    331  // outside of the visual viewport, i.e. it's going to be negative with the
    332  // `toScreenRect` conversion, whereas the call sites with `window` of this
    333  // function expect (0, 0) position should be the visual viport's offset. So
    334  // in such cases we simply use mozInnerScreen{X,Y} to convert the given value
    335  // to the screen coords.
    336  if (target instanceof Window && window.parent == window) {
    337    const resolution = await getResolution();
    338    const deviceScale = window.devicePixelRatio;
    339    return {
    340      x:
    341        window.mozInnerScreenX * deviceScale +
    342        (atCenter ? 0 : offsetX) * resolution * deviceScale,
    343      y:
    344        window.mozInnerScreenY * deviceScale +
    345        (atCenter ? 0 : offsetY) * resolution * deviceScale,
    346    };
    347  }
    348 
    349  const rectAndWindow = _getTargetRect(target, atCenter);
    350 
    351  const inProcessRootWindow = getInProcessRootWindow(window);
    352 
    353  if (
    354    !(inProcessRootWindow.location.href === rectAndWindow.window.location.href)
    355  ) {
    356    info(
    357      "warning: coordinatesRelativeToScreen using coords based on one window in another, this will likely produce incorrect results"
    358    );
    359    info(
    360      "inProcessRootWindow.location.href " + inProcessRootWindow.location.href
    361    );
    362    info(
    363      "rectAndWindow.window.location.href " + rectAndWindow.window.location.href
    364    );
    365  }
    366  // This doesn't hold yet.
    367  //ok(inProcessRootWindow.location.href === rectAndWindow.window.location.href, "same root window");
    368 
    369  const utils = SpecialPowers.wrap(
    370    SpecialPowers.getDOMWindowUtils(inProcessRootWindow)
    371  );
    372  const positionInScreenCoords = utils.toScreenRect(
    373    rectAndWindow.rect.left +
    374      (atCenter ? rectAndWindow.rect.width / 2 : offsetX),
    375    rectAndWindow.rect.top +
    376      (atCenter ? rectAndWindow.rect.height / 2 : offsetY),
    377    0,
    378    0
    379  );
    380 
    381  return {
    382    x: positionInScreenCoords.x,
    383    y: positionInScreenCoords.y,
    384  };
    385 }
    386 
    387 // Get the bounding box of aElement, and return it in device pixels
    388 // relative to the screen.
    389 // TODO: This function should probably take into account the resolution and
    390 //       the relative viewport rect like coordinatesRelativeToScreen() does.
    391 function rectRelativeToScreen(aElement) {
    392  var targetWindow = aElement.ownerDocument.defaultView;
    393  var scale = targetWindow.devicePixelRatio;
    394  var rect = aElement.getBoundingClientRect();
    395  return {
    396    x: (targetWindow.mozInnerScreenX + rect.left) * scale,
    397    y: (targetWindow.mozInnerScreenY + rect.top) * scale,
    398    width: rect.width * scale,
    399    height: rect.height * scale,
    400  };
    401 }
    402 
    403 // Synthesizes a native mousewheel event and returns immediately. This does not
    404 // guarantee anything; you probably want to use one of the other functions below
    405 // which actually wait for results.
    406 // aX and aY are relative to the top-left of |aTarget|'s bounding rect.
    407 // aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined
    408 // if not needed.
    409 async function synthesizeNativeWheel(
    410  aTarget,
    411  aX,
    412  aY,
    413  aDeltaX,
    414  aDeltaY,
    415  aObserver
    416 ) {
    417  var pt = await coordinatesRelativeToScreen({
    418    offsetX: aX,
    419    offsetY: aY,
    420    target: aTarget,
    421  });
    422  if (aDeltaX && aDeltaY) {
    423    throw new Error(
    424      "Simultaneous wheeling of horizontal and vertical is not supported on all platforms."
    425    );
    426  }
    427  aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
    428  aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
    429  var msg = aDeltaX
    430    ? nativeHorizontalWheelEventMsg()
    431    : nativeVerticalWheelEventMsg();
    432  var utils = utilsForTarget(aTarget);
    433  var element = elementForTarget(aTarget);
    434  utils.sendNativeMouseScrollEvent(
    435    pt.x,
    436    pt.y,
    437    msg,
    438    aDeltaX,
    439    aDeltaY,
    440    0,
    441    0,
    442    // Specify MOUSESCROLL_SCROLL_LINES if the test wants to run through wheel
    443    // input code path on Mac since it's normal mouse wheel inputs.
    444    SpecialPowers.getBoolPref("apz.test.mac.synth_wheel_input", false)
    445      ? SpecialPowers.DOMWindowUtils.MOUSESCROLL_SCROLL_LINES
    446      : 0,
    447    element,
    448    aObserver
    449  );
    450  return true;
    451 }
    452 
    453 // Synthesizes a native pan gesture event and returns immediately.
    454 // NOTE: This works only on Mac.
    455 // You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and
    456 // kCGScrollPhaseEnded = 4 for |aPhase|.
    457 async function synthesizeNativePanGestureEvent(
    458  aTarget,
    459  aX,
    460  aY,
    461  aDeltaX,
    462  aDeltaY,
    463  aPhase,
    464  aObserver
    465 ) {
    466  if (getPlatform() != "mac") {
    467    throw new Error(
    468      `synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}`
    469    );
    470  }
    471 
    472  var pt = await coordinatesRelativeToScreen({
    473    offsetX: aX,
    474    offsetY: aY,
    475    target: aTarget,
    476  });
    477  if (aDeltaX && aDeltaY) {
    478    throw new Error(
    479      "Simultaneous panning of horizontal and vertical is not supported."
    480    );
    481  }
    482 
    483  aDeltaX = nativeScrollUnits(aTarget, aDeltaX);
    484  aDeltaY = nativeScrollUnits(aTarget, aDeltaY);
    485 
    486  var element = elementForTarget(aTarget);
    487  var utils = utilsForTarget(aTarget);
    488  utils.sendNativeMouseScrollEvent(
    489    pt.x,
    490    pt.y,
    491    aPhase,
    492    aDeltaX,
    493    aDeltaY,
    494    0 /* deltaZ */,
    495    0 /* modifiers */,
    496    0 /* scroll event unit pixel */,
    497    element,
    498    aObserver
    499  );
    500 
    501  return true;
    502 }
    503 
    504 // Sends a native touchpad pan event and resolve the returned promise once the
    505 // request has been successfully made to the OS.
    506 // NOTE: This works only on Windows and Linux.
    507 // You can specify nsIDOMWindowUtils.PHASE_BEGIN, PHASE_UPDATE and PHASE_END
    508 // for |aPhase|.
    509 async function promiseNativeTouchpadPanEventAndWaitForObserver(
    510  aTarget,
    511  aX,
    512  aY,
    513  aDeltaX,
    514  aDeltaY,
    515  aPhase
    516 ) {
    517  if (getPlatform() != "windows" && getPlatform() != "linux") {
    518    throw new Error(
    519      `promiseNativeTouchpadPanEventAndWaitForObserver doesn't work on ${getPlatform()}`
    520    );
    521  }
    522 
    523  let pt = await coordinatesRelativeToScreen({
    524    offsetX: aX,
    525    offsetY: aY,
    526    target: aTarget,
    527  });
    528 
    529  const utils = utilsForTarget(aTarget);
    530 
    531  return new Promise(resolve => {
    532    utils.sendNativeTouchpadPan(
    533      aPhase,
    534      pt.x,
    535      pt.y,
    536      aDeltaX,
    537      aDeltaY,
    538      0,
    539      resolve
    540    );
    541  });
    542 }
    543 
    544 async function synthesizeSimpleGestureEvent(
    545  aElement,
    546  aType,
    547  aX,
    548  aY,
    549  aDirection,
    550  aDelta,
    551  aModifiers,
    552  aClickCount
    553 ) {
    554  let pt = await coordinatesRelativeToScreen({
    555    offsetX: aX,
    556    offsetY: aY,
    557    target: aElement,
    558  });
    559 
    560  let utils = utilsForTarget(aElement);
    561  utils.sendSimpleGestureEvent(
    562    aType,
    563    pt.x,
    564    pt.y,
    565    aDirection,
    566    aDelta,
    567    aModifiers,
    568    aClickCount
    569  );
    570 }
    571 
    572 // Synthesizes a native pan gesture event and resolve the returned promise once the
    573 // request has been successfully made to the OS.
    574 function promiseNativePanGestureEventAndWaitForObserver(
    575  aElement,
    576  aX,
    577  aY,
    578  aDeltaX,
    579  aDeltaY,
    580  aPhase
    581 ) {
    582  return new Promise(resolve => {
    583    synthesizeNativePanGestureEvent(
    584      aElement,
    585      aX,
    586      aY,
    587      aDeltaX,
    588      aDeltaY,
    589      aPhase,
    590      resolve
    591    );
    592  });
    593 }
    594 
    595 // Synthesizes a native mousewheel event and resolve the returned promise once the
    596 // request has been successfully made to the OS. This does not necessarily
    597 // guarantee that the OS generates the event we requested. See
    598 // synthesizeNativeWheel for details on the parameters.
    599 function promiseNativeWheelAndWaitForObserver(
    600  aElement,
    601  aX,
    602  aY,
    603  aDeltaX,
    604  aDeltaY
    605 ) {
    606  return new Promise(resolve => {
    607    synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, resolve);
    608  });
    609 }
    610 
    611 // Synthesizes a native mousewheel event and resolve the returned promise once the
    612 // wheel event is dispatched to |aTarget|'s containing window. If the event
    613 // targets content in a subdocument, |aTarget| should be inside the
    614 // subdocument (or the subdocument's window). See synthesizeNativeWheel for
    615 // details on the other parameters.
    616 function promiseNativeWheelAndWaitForWheelEvent(
    617  aTarget,
    618  aX,
    619  aY,
    620  aDeltaX,
    621  aDeltaY
    622 ) {
    623  return new Promise((resolve, reject) => {
    624    var targetWindow = windowForTarget(aTarget);
    625    targetWindow.addEventListener(
    626      "wheel",
    627      function () {
    628        setTimeout(resolve, 0);
    629      },
    630      { once: true }
    631    );
    632    try {
    633      synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY);
    634    } catch (e) {
    635      reject(e);
    636    }
    637  });
    638 }
    639 
    640 // Synthesizes a native mousewheel event and resolves the returned promise once the
    641 // first resulting scroll event is dispatched to |aTarget|'s containing window.
    642 // If the event targets content in a subdocument, |aTarget| should be inside
    643 // the subdocument (or the subdocument's window).  See synthesizeNativeWheel
    644 // for details on the other parameters.
    645 function promiseNativeWheelAndWaitForScrollEvent(
    646  aTarget,
    647  aX,
    648  aY,
    649  aDeltaX,
    650  aDeltaY
    651 ) {
    652  return new Promise((resolve, reject) => {
    653    var targetWindow = windowForTarget(aTarget);
    654    targetWindow.addEventListener(
    655      "scroll",
    656      function () {
    657        setTimeout(resolve, 0);
    658      },
    659      { capture: true, once: true }
    660    ); // scroll events don't always bubble
    661    try {
    662      synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY);
    663    } catch (e) {
    664      reject(e);
    665    }
    666  });
    667 }
    668 
    669 async function synthesizeTouchpadPinch(scales, focusX, focusY, options) {
    670  var scalesAndFoci = [];
    671 
    672  for (let i = 0; i < scales.length; i++) {
    673    scalesAndFoci.push([scales[i], focusX, focusY]);
    674  }
    675 
    676  await synthesizeTouchpadGesture(scalesAndFoci, options);
    677 }
    678 
    679 // scalesAndFoci is an array of [scale, focusX, focuxY] tuples.
    680 async function synthesizeTouchpadGesture(scalesAndFoci, options) {
    681  // Check for options, fill in defaults if appropriate.
    682  let waitForTransformEnd =
    683    options.waitForTransformEnd !== undefined
    684      ? options.waitForTransformEnd
    685      : true;
    686  let waitForFrames =
    687    options.waitForFrames !== undefined ? options.waitForFrames : false;
    688 
    689  // Register the listener for the TransformEnd observer topic
    690  let transformEndPromise = promiseTransformEnd();
    691 
    692  var modifierFlags = 0;
    693  var utils = utilsForTarget(document.body);
    694  for (let i = 0; i < scalesAndFoci.length; i++) {
    695    var pt = await coordinatesRelativeToScreen({
    696      offsetX: scalesAndFoci[i][1],
    697      offsetY: scalesAndFoci[i][2],
    698      target: document.body,
    699    });
    700    var phase;
    701    if (i === 0) {
    702      phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN;
    703    } else if (i === scalesAndFoci.length - 1) {
    704      phase = SpecialPowers.DOMWindowUtils.PHASE_END;
    705    } else {
    706      phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE;
    707    }
    708    utils.sendNativeTouchpadPinch(
    709      phase,
    710      scalesAndFoci[i][0],
    711      pt.x,
    712      pt.y,
    713      modifierFlags
    714    );
    715    if (waitForFrames) {
    716      await promiseFrame();
    717    }
    718  }
    719 
    720  // Wait for TransformEnd to fire.
    721  if (waitForTransformEnd) {
    722    await transformEndPromise;
    723  }
    724 }
    725 
    726 async function synthesizeTouchpadPan(
    727  focusX,
    728  focusY,
    729  deltaXs,
    730  deltaYs,
    731  options
    732 ) {
    733  // Check for options, fill in defaults if appropriate.
    734  let waitForTransformEnd =
    735    options.waitForTransformEnd !== undefined
    736      ? options.waitForTransformEnd
    737      : true;
    738  let waitForFrames =
    739    options.waitForFrames !== undefined ? options.waitForFrames : false;
    740 
    741  // Register the listener for the TransformEnd observer topic
    742  let transformEndPromise = promiseTransformEnd();
    743 
    744  var modifierFlags = 0;
    745  var pt = await coordinatesRelativeToScreen({
    746    offsetX: focusX,
    747    offsetY: focusY,
    748    target: document.body,
    749  });
    750  var utils = utilsForTarget(document.body);
    751  for (let i = 0; i < deltaXs.length; i++) {
    752    var phase;
    753    if (i === 0) {
    754      phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN;
    755    } else if (i === deltaXs.length - 1) {
    756      phase = SpecialPowers.DOMWindowUtils.PHASE_END;
    757    } else {
    758      phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE;
    759    }
    760    utils.sendNativeTouchpadPan(
    761      phase,
    762      pt.x,
    763      pt.y,
    764      deltaXs[i],
    765      deltaYs[i],
    766      modifierFlags
    767    );
    768    if (waitForFrames) {
    769      await promiseFrame();
    770    }
    771  }
    772 
    773  // Wait for TransformEnd to fire.
    774  if (waitForTransformEnd) {
    775    await transformEndPromise;
    776  }
    777 }
    778 
    779 // Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels
    780 // relative to the top-left of |aTarget|'s bounding rect.
    781 async function synthesizeNativeTouch(
    782  aTarget,
    783  aX,
    784  aY,
    785  aType,
    786  aObserver = null,
    787  aTouchId = 0
    788 ) {
    789  var pt = await coordinatesRelativeToScreen({
    790    offsetX: aX,
    791    offsetY: aY,
    792    target: aTarget,
    793  });
    794  var utils = utilsForTarget(aTarget);
    795  utils.sendNativeTouchPoint(
    796    aTouchId,
    797    aType,
    798    pt.x,
    799    pt.y,
    800    1,
    801    90,
    802    aObserver,
    803    aTarget instanceof Element ? aTarget : null
    804  );
    805  return true;
    806 }
    807 
    808 function sendBasicNativePointerInput(
    809  utils,
    810  aId,
    811  aPointerType,
    812  aState,
    813  aX,
    814  aY,
    815  aObserver,
    816  aElement,
    817  { pressure = 1, twist = 0, tiltX = 0, tiltY = 0, button = 0 } = {}
    818 ) {
    819  switch (aPointerType) {
    820    case "touch":
    821      utils.sendNativeTouchPoint(
    822        aId,
    823        aState,
    824        aX,
    825        aY,
    826        pressure,
    827        90,
    828        aObserver,
    829        aElement
    830      );
    831      break;
    832    case "pen":
    833      utils.sendNativePenInput(
    834        aId,
    835        aState,
    836        aX,
    837        aY,
    838        pressure,
    839        twist,
    840        tiltX,
    841        tiltY,
    842        button,
    843        aObserver,
    844        aElement
    845      );
    846      break;
    847    default:
    848      throw new Error(`Not supported: ${aPointerType}`);
    849  }
    850 }
    851 
    852 async function promiseNativePointerInput(
    853  aTarget,
    854  aPointerType,
    855  aState,
    856  aX,
    857  aY,
    858  options
    859 ) {
    860  const pt = await coordinatesRelativeToScreen({
    861    offsetX: aX,
    862    offsetY: aY,
    863    target: aTarget,
    864  });
    865  const utils = utilsForTarget(aTarget);
    866  return new Promise(resolve => {
    867    sendBasicNativePointerInput(
    868      utils,
    869      options?.pointerId ?? 0,
    870      aPointerType,
    871      aState,
    872      pt.x,
    873      pt.y,
    874      resolve,
    875      aTarget instanceof Element ? aTarget : null,
    876      options
    877    );
    878  });
    879 }
    880 
    881 /**
    882 * Function to generate native pointer events as a sequence.
    883 *
    884 * @param aTarget is the element or window whose bounding rect the coordinates are
    885 *   relative to.
    886 * @param aPointerType "touch" or "pen".
    887 * @param aPositions is a 2D array of position data. It is indexed as [row][column],
    888 *   where advancing the row counter moves forward in time, and each column
    889 *   represents a single pointer. Each row must have exactly
    890 *   the same number of columns, and the number of columns must match the length
    891 *   of the aPointerIds parameter.
    892 *   For each row, each entry is either an object with x and y fields,
    893 *   or a null. A null value indicates that the pointer should be "lifted"
    894 *   (i.e. send a touchend for that touch input). A non-null value therefore
    895 *   indicates the position of the pointer input.
    896 *   This function takes care of the state tracking necessary to send
    897 *   pointerup/pointerdown inputs as necessary as the pointers go up and down.
    898 * @param aObserver is the observer that will get registered on the very last
    899 *   native pointer synthesis call this function makes.
    900 * @param aPointerIds is an array holding the pointer ID values.
    901 */
    902 async function synthesizeNativePointerSequences(
    903  aTarget,
    904  aPointerType,
    905  aPositions,
    906  aObserver = null,
    907  aPointerIds = [0],
    908  options
    909 ) {
    910  // We use lastNonNullValue to figure out which synthesizeNativeTouch call
    911  // will be the last one we make, so that we can register aObserver on it.
    912  var lastNonNullValue = -1;
    913  for (let i = 0; i < aPositions.length; i++) {
    914    if (aPositions[i] == null) {
    915      throw new Error(`aPositions[${i}] was unexpectedly null`);
    916    }
    917    if (aPositions[i].length != aPointerIds.length) {
    918      throw new Error(
    919        `aPositions[${i}] did not have the expected number of positions; ` +
    920          `expected ${aPointerIds.length} pointers but found ${aPositions[i].length}`
    921      );
    922    }
    923    for (let j = 0; j < aPointerIds.length; j++) {
    924      if (aPositions[i][j] != null) {
    925        lastNonNullValue = i * aPointerIds.length + j;
    926        // Do the conversion to screen space before actually synthesizing
    927        // the events, otherwise the screen space may change as a result of
    928        // the touch inputs and the conversion may not work as intended.
    929        aPositions[i][j] = await coordinatesRelativeToScreen({
    930          offsetX: aPositions[i][j].x,
    931          offsetY: aPositions[i][j].y,
    932          target: aTarget,
    933        });
    934      }
    935    }
    936  }
    937  if (lastNonNullValue < 0) {
    938    throw new Error("All values in positions array were null!");
    939  }
    940 
    941  // Insert a row of nulls at the end of aPositions, to ensure that all
    942  // touches get removed. If the touches have already been removed this will
    943  // just add an extra no-op iteration in the aPositions loop below.
    944  var allNullRow = new Array(aPointerIds.length);
    945  allNullRow.fill(null);
    946  aPositions.push(allNullRow);
    947 
    948  // The last sendNativeTouchPoint call will be the TOUCH_REMOVE which happens
    949  // one iteration of aPosition after the last non-null value.
    950  var lastSynthesizeCall = lastNonNullValue + aPointerIds.length;
    951 
    952  // track which touches are down and which are up. start with all up
    953  var currentPositions = new Array(aPointerIds.length);
    954  currentPositions.fill(null);
    955 
    956  var utils = utilsForTarget(aTarget);
    957  // Iterate over the position data now, and generate the touches requested
    958  for (let i = 0; i < aPositions.length; i++) {
    959    for (let j = 0; j < aPointerIds.length; j++) {
    960      if (aPositions[i][j] == null) {
    961        // null means lift the finger
    962        if (currentPositions[j] == null) {
    963          // it's already lifted, do nothing
    964        } else {
    965          // synthesize the touch-up. If this is the last call we're going to
    966          // make, pass the observer as well
    967          var thisIndex = i * aPointerIds.length + j;
    968          var observer = lastSynthesizeCall == thisIndex ? aObserver : null;
    969          sendBasicNativePointerInput(
    970            utils,
    971            aPointerIds[j],
    972            aPointerType,
    973            SpecialPowers.DOMWindowUtils.TOUCH_REMOVE,
    974            currentPositions[j].x,
    975            currentPositions[j].y,
    976            observer,
    977            aTarget instanceof Element ? aTarget : null,
    978            options
    979          );
    980          currentPositions[j] = null;
    981        }
    982      } else {
    983        sendBasicNativePointerInput(
    984          utils,
    985          aPointerIds[j],
    986          aPointerType,
    987          SpecialPowers.DOMWindowUtils.TOUCH_CONTACT,
    988          aPositions[i][j].x,
    989          aPositions[i][j].y,
    990          null,
    991          aTarget instanceof Element ? aTarget : null,
    992          options
    993        );
    994        currentPositions[j] = aPositions[i][j];
    995      }
    996    }
    997  }
    998  return true;
    999 }
   1000 
   1001 async function synthesizeNativeTouchSequences(
   1002  aTarget,
   1003  aPositions,
   1004  aObserver = null,
   1005  aTouchIds = [0]
   1006 ) {
   1007  await synthesizeNativePointerSequences(
   1008    aTarget,
   1009    "touch",
   1010    aPositions,
   1011    aObserver,
   1012    aTouchIds
   1013  );
   1014 }
   1015 
   1016 async function synthesizeNativePointerDrag(
   1017  aTarget,
   1018  aPointerType,
   1019  aX,
   1020  aY,
   1021  aDeltaX,
   1022  aDeltaY,
   1023  aObserver = null,
   1024  aPointerId = 0,
   1025  options
   1026 ) {
   1027  var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY));
   1028  var positions = [[{ x: aX, y: aY }]];
   1029  for (var i = 1; i < steps; i++) {
   1030    var dx = i * (aDeltaX / steps);
   1031    var dy = i * (aDeltaY / steps);
   1032    var pos = { x: aX + dx, y: aY + dy };
   1033    positions.push([pos]);
   1034  }
   1035  positions.push([{ x: aX + aDeltaX, y: aY + aDeltaY }]);
   1036  return synthesizeNativePointerSequences(
   1037    aTarget,
   1038    aPointerType,
   1039    positions,
   1040    aObserver,
   1041    [aPointerId],
   1042    options
   1043  );
   1044 }
   1045 
   1046 // Note that when calling this function you'll want to make sure that the pref
   1047 // "apz.touch_start_tolerance" is set to 0, or some of the touchmove will get
   1048 // consumed to overcome the panning threshold.
   1049 async function synthesizeNativeTouchDrag(
   1050  aTarget,
   1051  aX,
   1052  aY,
   1053  aDeltaX,
   1054  aDeltaY,
   1055  aObserver = null,
   1056  aTouchId = 0
   1057 ) {
   1058  return synthesizeNativePointerDrag(
   1059    aTarget,
   1060    "touch",
   1061    aX,
   1062    aY,
   1063    aDeltaX,
   1064    aDeltaY,
   1065    aObserver,
   1066    aTouchId
   1067  );
   1068 }
   1069 
   1070 function promiseNativePointerDrag(
   1071  aTarget,
   1072  aPointerType,
   1073  aX,
   1074  aY,
   1075  aDeltaX,
   1076  aDeltaY,
   1077  aPointerId = 0,
   1078  options
   1079 ) {
   1080  return new Promise(resolve => {
   1081    synthesizeNativePointerDrag(
   1082      aTarget,
   1083      aPointerType,
   1084      aX,
   1085      aY,
   1086      aDeltaX,
   1087      aDeltaY,
   1088      resolve,
   1089      aPointerId,
   1090      options
   1091    );
   1092  });
   1093 }
   1094 
   1095 // Promise-returning variant of synthesizeNativeTouchDrag
   1096 function promiseNativeTouchDrag(
   1097  aTarget,
   1098  aX,
   1099  aY,
   1100  aDeltaX,
   1101  aDeltaY,
   1102  aTouchId = 0
   1103 ) {
   1104  return new Promise(resolve => {
   1105    synthesizeNativeTouchDrag(
   1106      aTarget,
   1107      aX,
   1108      aY,
   1109      aDeltaX,
   1110      aDeltaY,
   1111      resolve,
   1112      aTouchId
   1113    );
   1114  });
   1115 }
   1116 
   1117 // Tapping is essentially a dragging with no move
   1118 function promiseNativePointerTap(aTarget, aPointerType, aX, aY, options) {
   1119  return promiseNativePointerDrag(
   1120    aTarget,
   1121    aPointerType,
   1122    aX,
   1123    aY,
   1124    0,
   1125    0,
   1126    options?.pointerId ?? 0,
   1127    options
   1128  );
   1129 }
   1130 
   1131 async function synthesizeNativeTap(aTarget, aX, aY, aObserver = null) {
   1132  var pt = await coordinatesRelativeToScreen({
   1133    offsetX: aX,
   1134    offsetY: aY,
   1135    target: aTarget,
   1136  });
   1137  let utils = utilsForTarget(aTarget);
   1138  utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver);
   1139  return true;
   1140 }
   1141 
   1142 // only currently implemented on macOS
   1143 async function synthesizeNativeTouchpadDoubleTap(aTarget, aX, aY) {
   1144  ok(
   1145    getPlatform() == "mac",
   1146    "only implemented on mac. implement sendNativeTouchpadDoubleTap for this platform," +
   1147      " see bug 1696802 for how it was done on macOS"
   1148  );
   1149  let pt = await coordinatesRelativeToScreen({
   1150    offsetX: aX,
   1151    offsetY: aY,
   1152    target: aTarget,
   1153  });
   1154  let utils = utilsForTarget(aTarget);
   1155  utils.sendNativeTouchpadDoubleTap(pt.x, pt.y, 0);
   1156  return true;
   1157 }
   1158 
   1159 // If the event targets content in a subdocument, |aTarget| should be inside the
   1160 // subdocument (or the subdocument window).
   1161 async function synthesizeNativeMouseEventWithAPZ(aParams, aObserver = null) {
   1162  if (aParams.win !== undefined) {
   1163    throw Error(
   1164      "Are you trying to use EventUtils' API? `win` won't be used with synthesizeNativeMouseClickWithAPZ."
   1165    );
   1166  }
   1167  if (aParams.scale !== undefined) {
   1168    throw Error(
   1169      "Are you trying to use EventUtils' API? `scale` won't be used with synthesizeNativeMouseClickWithAPZ."
   1170    );
   1171  }
   1172  if (aParams.elementOnWidget !== undefined) {
   1173    throw Error(
   1174      "Are you trying to use EventUtils' API? `elementOnWidget` won't be used with synthesizeNativeMouseClickWithAPZ."
   1175    );
   1176  }
   1177  const {
   1178    type, // "click", "mousedown", "mouseup" or "mousemove"
   1179    target, // Origin of offsetX and offsetY, must be an element
   1180    offsetX, // X offset in `target` in CSS Pixels
   1181    offsetY, // Y offset in `target` in CSS pixels
   1182    atCenter, // Instead of offsetX/Y, synthesize the event at center of `target`
   1183    screenX, // X offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set
   1184    screenY, // Y offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set
   1185    button = 0, // if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button
   1186    modifiers = {}, // Active modifiers, see `parseNativeModifiers`
   1187  } = aParams;
   1188  if (atCenter) {
   1189    if (offsetX != undefined || offsetY != undefined) {
   1190      throw Error(
   1191        `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
   1192      );
   1193    }
   1194    if (screenX != undefined || screenY != undefined) {
   1195      throw Error(
   1196        `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
   1197      );
   1198    }
   1199  } else if (offsetX != undefined && offsetY != undefined) {
   1200    if (screenX != undefined || screenY != undefined) {
   1201      throw Error(
   1202        `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified`
   1203      );
   1204    }
   1205  } else if (screenX != undefined && screenY != undefined) {
   1206    if (offsetX != undefined || offsetY != undefined) {
   1207      throw Error(
   1208        `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified`
   1209      );
   1210    }
   1211  }
   1212  const pt = await (async () => {
   1213    if (screenX != undefined) {
   1214      return { x: screenX, y: screenY };
   1215    }
   1216    return coordinatesRelativeToScreen({
   1217      offsetX,
   1218      offsetY,
   1219      atCenter,
   1220      target,
   1221    });
   1222  })();
   1223  const utils = utilsForTarget(target);
   1224  const element = elementForTarget(target);
   1225  const modifierFlags = parseNativeModifiers(modifiers);
   1226  if (type === "click") {
   1227    utils.sendNativeMouseEvent(
   1228      pt.x,
   1229      pt.y,
   1230      utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
   1231      button,
   1232      modifierFlags,
   1233      element,
   1234      function () {
   1235        utils.sendNativeMouseEvent(
   1236          pt.x,
   1237          pt.y,
   1238          utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
   1239          button,
   1240          modifierFlags,
   1241          element,
   1242          aObserver
   1243        );
   1244      }
   1245    );
   1246    return;
   1247  }
   1248 
   1249  utils.sendNativeMouseEvent(
   1250    pt.x,
   1251    pt.y,
   1252    (() => {
   1253      switch (type) {
   1254        case "mousedown":
   1255          return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN;
   1256        case "mouseup":
   1257          return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP;
   1258        case "mousemove":
   1259          return utils.NATIVE_MOUSE_MESSAGE_MOVE;
   1260        default:
   1261          throw Error(`Invalid type is specified: ${type}`);
   1262      }
   1263    })(),
   1264    button,
   1265    modifierFlags,
   1266    element,
   1267    aObserver
   1268  );
   1269 }
   1270 
   1271 function promiseNativeMouseEventWithAPZ(aParams) {
   1272  return new Promise(resolve =>
   1273    synthesizeNativeMouseEventWithAPZ(aParams, resolve)
   1274  );
   1275 }
   1276 
   1277 // See synthesizeNativeMouseEventWithAPZ for the detail of aParams.
   1278 function promiseNativeMouseEventWithAPZAndWaitForEvent(aParams) {
   1279  return new Promise(resolve => {
   1280    const targetWindow = windowForTarget(aParams.target);
   1281    const eventType = aParams.eventTypeToWait || aParams.type;
   1282    targetWindow.addEventListener(eventType, resolve, {
   1283      once: true,
   1284    });
   1285    synthesizeNativeMouseEventWithAPZ(aParams);
   1286  });
   1287 }
   1288 
   1289 // Move the mouse to (dx, dy) relative to |target|, and scroll the wheel
   1290 // at that location.
   1291 // Moving the mouse is necessary to avoid wheel events from two consecutive
   1292 // promiseMoveMouseAndScrollWheelOver() calls on different elements being incorrectly
   1293 // considered as part of the same wheel transaction.
   1294 // We also wait for the mouse move event to be processed before sending the
   1295 // wheel event, otherwise there is a chance they might get reordered, and
   1296 // we have the transaction problem again.
   1297 // This function returns a promise that is resolved when the resulting wheel
   1298 // (if waitForScroll = false) or scroll (if waitForScroll = true) event is
   1299 // received.
   1300 function promiseMoveMouseAndScrollWheelOver(
   1301  target,
   1302  dx,
   1303  dy,
   1304  waitForScroll = true,
   1305  scrollDelta = 10
   1306 ) {
   1307  let p = promiseNativeMouseEventWithAPZAndWaitForEvent({
   1308    type: "mousemove",
   1309    target,
   1310    offsetX: dx,
   1311    offsetY: dy,
   1312  });
   1313  if (waitForScroll) {
   1314    p = p.then(() => {
   1315      info(
   1316        "Printing something here to avoid failure; see https://bugzilla.mozilla.org/show_bug.cgi?id=1776963"
   1317      );
   1318      return promiseNativeWheelAndWaitForScrollEvent(
   1319        target,
   1320        dx,
   1321        dy,
   1322        0,
   1323        -scrollDelta
   1324      );
   1325    });
   1326  } else {
   1327    p = p.then(() => {
   1328      return promiseNativeWheelAndWaitForWheelEvent(
   1329        target,
   1330        dx,
   1331        dy,
   1332        0,
   1333        -scrollDelta
   1334      );
   1335    });
   1336  }
   1337  return p;
   1338 }
   1339 
   1340 async function scrollbarDragStart(aTarget, aScaleFactor) {
   1341  var targetElement = elementForTarget(aTarget);
   1342  var w = {},
   1343    h = {};
   1344  utilsForTarget(aTarget).getScrollbarSizes(targetElement, w, h);
   1345  var verticalScrollbarWidth = w.value;
   1346  if (verticalScrollbarWidth == 0) {
   1347    return null;
   1348  }
   1349 
   1350  var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons
   1351  var startX = targetElement.clientWidth + verticalScrollbarWidth / 2;
   1352  var startY = upArrowHeight + 5; // start dragging somewhere in the thumb
   1353  startX *= aScaleFactor;
   1354  startY *= aScaleFactor;
   1355 
   1356  // targetElement.clientWidth is unaffected by the zoom, but if the target
   1357  // is the root content window, the distance from the window origin to the
   1358  // scrollbar in CSS pixels does decrease proportionally to the zoom,
   1359  // so the CSS coordinates we return need to be scaled accordingly.
   1360  if (targetIsTopWindow(aTarget)) {
   1361    var resolution = await getResolution();
   1362    startX /= resolution;
   1363    startY /= resolution;
   1364  }
   1365 
   1366  return { x: startX, y: startY };
   1367 }
   1368 
   1369 // Synthesizes events to drag |target|'s vertical scrollbar by the distance
   1370 // specified, synthesizing a mousemove for each increment as specified.
   1371 // Returns null if the element doesn't have a vertical scrollbar. Otherwise,
   1372 // returns an async function that should be invoked after the mousemoves have been
   1373 // processed by the widget code, to end the scrollbar drag. Mousemoves being
   1374 // processed by the widget code can be detected by listening for the mousemove
   1375 // events in the caller, or for some other event that is triggered by the
   1376 // mousemove, such as the scroll event resulting from the scrollbar drag.
   1377 // The aScaleFactor argument should be provided if the scrollframe has been
   1378 // scaled by an enclosing CSS transform. (TODO: this is a workaround for the
   1379 // fact that coordinatesRelativeToScreen is supposed to do this automatically
   1380 // but it currently does not).
   1381 // Note: helper_scrollbar_snap_bug1501062.html contains a copy of this code
   1382 // with modifications. Fixes here should be copied there if appropriate.
   1383 // |target| can be an element (for subframes) or a window (for root frames).
   1384 async function promiseVerticalScrollbarDrag(
   1385  aTarget,
   1386  aDistance = 20,
   1387  aIncrement = 5,
   1388  aScaleFactor = 1
   1389 ) {
   1390  var startPoint = await scrollbarDragStart(aTarget, aScaleFactor);
   1391  var targetElement = elementForTarget(aTarget);
   1392  if (startPoint == null) {
   1393    return null;
   1394  }
   1395 
   1396  dump(
   1397    "Starting drag at " +
   1398      startPoint.x +
   1399      ", " +
   1400      startPoint.y +
   1401      " from top-left of #" +
   1402      targetElement.id +
   1403      "\n"
   1404  );
   1405 
   1406  // Move the mouse to the scrollbar thumb and drag it down
   1407  await promiseNativeMouseEventWithAPZ({
   1408    target: aTarget,
   1409    offsetX: startPoint.x,
   1410    offsetY: startPoint.y,
   1411    type: "mousemove",
   1412  });
   1413  // mouse down
   1414  await promiseNativeMouseEventWithAPZ({
   1415    target: aTarget,
   1416    offsetX: startPoint.x,
   1417    offsetY: startPoint.y,
   1418    type: "mousedown",
   1419  });
   1420  // drag vertically by |aIncrement| until we reach the specified distance
   1421  for (var y = aIncrement; y < aDistance; y += aIncrement) {
   1422    await promiseNativeMouseEventWithAPZ({
   1423      target: aTarget,
   1424      offsetX: startPoint.x,
   1425      offsetY: startPoint.y + y,
   1426      type: "mousemove",
   1427    });
   1428  }
   1429  await promiseNativeMouseEventWithAPZ({
   1430    target: aTarget,
   1431    offsetX: startPoint.x,
   1432    offsetY: startPoint.y + aDistance,
   1433    type: "mousemove",
   1434  });
   1435 
   1436  // and return an async function to call afterwards to finish up the drag
   1437  return async function () {
   1438    dump("Finishing drag of #" + targetElement.id + "\n");
   1439    await promiseNativeMouseEventWithAPZ({
   1440      target: aTarget,
   1441      offsetX: startPoint.x,
   1442      offsetY: startPoint.y + aDistance,
   1443      type: "mouseup",
   1444    });
   1445  };
   1446 }
   1447 
   1448 // This is similar to promiseVerticalScrollbarDrag except this triggers
   1449 // the vertical scrollbar drag with a touch drag input. This function
   1450 // returns true if a scrollbar was present and false if no scrollbar
   1451 // was found for the given element.
   1452 async function promiseVerticalScrollbarTouchDrag(
   1453  aTarget,
   1454  aDistance = 20,
   1455  aScaleFactor = 1
   1456 ) {
   1457  var startPoint = await scrollbarDragStart(aTarget, aScaleFactor);
   1458  var targetElement = elementForTarget(aTarget);
   1459  if (startPoint == null) {
   1460    return false;
   1461  }
   1462 
   1463  dump(
   1464    "Starting touch drag at " +
   1465      startPoint.x +
   1466      ", " +
   1467      startPoint.y +
   1468      " from top-left of #" +
   1469      targetElement.id +
   1470      "\n"
   1471  );
   1472 
   1473  await promiseNativeTouchDrag(
   1474    aTarget,
   1475    startPoint.x,
   1476    startPoint.y,
   1477    0,
   1478    aDistance
   1479  );
   1480 
   1481  return true;
   1482 }
   1483 
   1484 // Synthesizes a native mouse drag, starting at offset (mouseX, mouseY) from
   1485 // the given target. The drag occurs in the given number of steps, to a final
   1486 // destination of (mouseX + distanceX, mouseY + distanceY) from the target.
   1487 // Returns a promise (wrapped in a function, so it doesn't execute immediately)
   1488 // that should be awaited after the mousemoves have been processed by the widget
   1489 // code, to end the drag. This is important otherwise the OS can sometimes
   1490 // reorder the events and the drag doesn't have the intended effect (see
   1491 // bug 1368603).
   1492 // Example usage:
   1493 //   let dragFinisher = await promiseNativeMouseDrag(myElement, 0, 0);
   1494 //   await myIndicationThatDragHadAnEffect;
   1495 //   await dragFinisher();
   1496 async function promiseNativeMouseDrag(
   1497  target,
   1498  mouseX,
   1499  mouseY,
   1500  distanceX = 20,
   1501  distanceY = 20,
   1502  steps = 20
   1503 ) {
   1504  var targetElement = elementForTarget(target);
   1505  dump(
   1506    "Starting drag at " +
   1507      mouseX +
   1508      ", " +
   1509      mouseY +
   1510      " from top-left of #" +
   1511      targetElement.id +
   1512      "\n"
   1513  );
   1514 
   1515  // Move the mouse to the target position
   1516  await promiseNativeMouseEventWithAPZ({
   1517    target,
   1518    offsetX: mouseX,
   1519    offsetY: mouseY,
   1520    type: "mousemove",
   1521  });
   1522  // mouse down
   1523  await promiseNativeMouseEventWithAPZ({
   1524    target,
   1525    offsetX: mouseX,
   1526    offsetY: mouseY,
   1527    type: "mousedown",
   1528  });
   1529  // drag vertically by |increment| until we reach the specified distance
   1530  for (var s = 1; s <= steps; s++) {
   1531    let dx = distanceX * (s / steps);
   1532    let dy = distanceY * (s / steps);
   1533    dump(`Dragging to ${mouseX + dx}, ${mouseY + dy} from target\n`);
   1534    await promiseNativeMouseEventWithAPZ({
   1535      target,
   1536      offsetX: mouseX + dx,
   1537      offsetY: mouseY + dy,
   1538      type: "mousemove",
   1539    });
   1540  }
   1541 
   1542  // and return a function-wrapped promise to call afterwards to finish the drag
   1543  return function () {
   1544    return promiseNativeMouseEventWithAPZ({
   1545      target,
   1546      offsetX: mouseX + distanceX,
   1547      offsetY: mouseY + distanceY,
   1548      type: "mouseup",
   1549    });
   1550  };
   1551 }
   1552 
   1553 // Synthesizes a native touch sequence of events corresponding to a pinch-zoom-in
   1554 // at the given focus point. The focus point must be specified in CSS coordinates
   1555 // relative to the document body.
   1556 async function pinchZoomInTouchSequence(focusX, focusY) {
   1557  // prettier-ignore
   1558  var zoom_in = [
   1559      [ { x: focusX - 25, y: focusY - 50 }, { x: focusX + 25, y: focusY + 50 } ],
   1560      [ { x: focusX - 30, y: focusY - 80 }, { x: focusX + 30, y: focusY + 80 } ],
   1561      [ { x: focusX - 35, y: focusY - 110 }, { x: focusX + 40, y: focusY + 110 } ],
   1562      [ { x: focusX - 40, y: focusY - 140 }, { x: focusX + 45, y: focusY + 140 } ],
   1563      [ { x: focusX - 45, y: focusY - 170 }, { x: focusX + 50, y: focusY + 170 } ],
   1564      [ { x: focusX - 50, y: focusY - 200 }, { x: focusX + 55, y: focusY + 200 } ],
   1565  ];
   1566 
   1567  var touchIds = [0, 1];
   1568  return synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds);
   1569 }
   1570 
   1571 // Returns a promise that is resolved when the observer service dispatches a
   1572 // message with the given topic.
   1573 function promiseTopic(aTopic) {
   1574  return new Promise((resolve, reject) => {
   1575    SpecialPowers.Services.obs.addObserver(function observer(
   1576      subject,
   1577      topic,
   1578      data
   1579    ) {
   1580      try {
   1581        SpecialPowers.Services.obs.removeObserver(observer, topic);
   1582        resolve([subject, data]);
   1583      } catch (ex) {
   1584        SpecialPowers.Services.obs.removeObserver(observer, topic);
   1585        reject(ex);
   1586      }
   1587    }, aTopic);
   1588  });
   1589 }
   1590 
   1591 // Returns a promise that is resolved when a APZ transform ends.
   1592 function promiseTransformEnd() {
   1593  return promiseTopic("APZ:TransformEnd");
   1594 }
   1595 
   1596 function promiseScrollend(aTarget = window) {
   1597  return promiseOneEvent(aTarget, "scrollend");
   1598 }
   1599 
   1600 // Returns a promise that resolves after the indicated number
   1601 // of touchend events have fired on the given target element.
   1602 function promiseTouchEnd(element, count = 1) {
   1603  return new Promise(resolve => {
   1604    var eventCount = 0;
   1605    var counterFunction = function () {
   1606      eventCount++;
   1607      if (eventCount == count) {
   1608        element.removeEventListener("touchend", counterFunction, {
   1609          passive: true,
   1610        });
   1611        resolve();
   1612      }
   1613    };
   1614    element.addEventListener("touchend", counterFunction, { passive: true });
   1615  });
   1616 }
   1617 
   1618 // This generates a touch-based pinch zoom-in gesture that is expected
   1619 // to succeed. It returns after APZ has completed the zoom and reaches the end
   1620 // of the transform. The focus point is expected to be in CSS coordinates
   1621 // relative to the document body.
   1622 async function pinchZoomInWithTouch(focusX, focusY) {
   1623  // Register the listener for the TransformEnd observer topic
   1624  let transformEndPromise = promiseTopic("APZ:TransformEnd");
   1625 
   1626  // Dispatch all the touch events
   1627  await pinchZoomInTouchSequence(focusX, focusY);
   1628 
   1629  // Wait for TransformEnd to fire.
   1630  await transformEndPromise;
   1631 }
   1632 // This generates a touchpad pinch zoom-in gesture that is expected
   1633 // to succeed. It returns after APZ has completed the zoom and reaches the end
   1634 // of the transform. The focus point is expected to be in CSS coordinates
   1635 // relative to the document body.
   1636 async function pinchZoomInWithTouchpad(focusX, focusY, options = {}) {
   1637  var zoomIn = [
   1638    1.0, 1.019531, 1.035156, 1.037156, 1.039156, 1.054688, 1.056688, 1.070312,
   1639    1.072312, 1.089844, 1.091844, 1.109375, 1.128906, 1.144531, 1.160156,
   1640    1.175781, 1.191406, 1.207031, 1.222656, 1.234375, 1.246094, 1.261719,
   1641    1.273438, 1.285156, 1.296875, 1.3125, 1.328125, 1.347656, 1.363281,
   1642    1.382812, 1.402344, 1.421875, 1.0,
   1643  ];
   1644  await synthesizeTouchpadPinch(zoomIn, focusX, focusY, options);
   1645 }
   1646 
   1647 async function pinchZoomInAndPanWithTouchpad(options = {}) {
   1648  var x = 584;
   1649  var y = 347;
   1650  var scalesAndFoci = [];
   1651  // Zoom
   1652  for (var scale = 1.0; scale <= 2.0; scale += 0.2) {
   1653    scalesAndFoci.push([scale, x, y]);
   1654  }
   1655  // Pan (due to a limitation of the current implementation, events
   1656  // for which the scale doesn't change are dropped, so vary the
   1657  // scale slightly as well).
   1658  for (var i = 1; i <= 20; i++) {
   1659    x -= 4;
   1660    y -= 5;
   1661    scalesAndFoci.push([scale + 0.01 * i, x, y]);
   1662  }
   1663  await synthesizeTouchpadGesture(scalesAndFoci, options);
   1664 }
   1665 
   1666 async function pinchZoomOutWithTouchpad(focusX, focusY, options = {}) {
   1667  // The last item equal one to indicate scale end
   1668  var zoomOut = [
   1669    1.0, 1.375, 1.359375, 1.339844, 1.316406, 1.296875, 1.277344, 1.257812,
   1670    1.238281, 1.21875, 1.199219, 1.175781, 1.15625, 1.132812, 1.101562,
   1671    1.078125, 1.054688, 1.03125, 1.011719, 0.992188, 0.972656, 0.953125,
   1672    0.933594, 1.0,
   1673  ];
   1674  await synthesizeTouchpadPinch(zoomOut, focusX, focusY, options);
   1675 }
   1676 
   1677 async function pinchZoomInOutWithTouchpad(focusX, focusY, options = {}) {
   1678  // Use the same scale for two events in a row to make sure the code handles this properly.
   1679  var zoomInOut = [
   1680    1.0, 1.082031, 1.089844, 1.097656, 1.101562, 1.109375, 1.121094, 1.128906,
   1681    1.128906, 1.125, 1.097656, 1.074219, 1.054688, 1.035156, 1.015625, 1.0, 1.0,
   1682  ];
   1683  await synthesizeTouchpadPinch(zoomInOut, focusX, focusY, options);
   1684 }
   1685 // This generates a touch-based pinch gesture that is expected to succeed
   1686 // and trigger an APZ:TransformEnd observer notification.
   1687 // It returns after that notification has been dispatched.
   1688 // The coordinates of touch events in `touchSequence` are expected to be
   1689 // in CSS coordinates relative to the document body.
   1690 async function synthesizeNativeTouchAndWaitForTransformEnd(
   1691  touchSequence,
   1692  touchIds
   1693 ) {
   1694  // Register the listener for the TransformEnd observer topic
   1695  let transformEndPromise = promiseTopic("APZ:TransformEnd");
   1696 
   1697  // Dispatch all the touch events
   1698  await synthesizeNativeTouchSequences(
   1699    document.body,
   1700    touchSequence,
   1701    null,
   1702    touchIds
   1703  );
   1704 
   1705  // Wait for TransformEnd to fire.
   1706  await transformEndPromise;
   1707 }
   1708 
   1709 // Returns a touch sequence for a pinch-zoom-out operation in the center
   1710 // of the visual viewport. The touch sequence returned is in CSS coordinates
   1711 // relative to the document body.
   1712 function pinchZoomOutTouchSequenceAtCenter() {
   1713  // Divide the half of visual viewport size by 8, then cause touch events
   1714  // starting from the 7th furthest away from the center towards the center.
   1715  const deltaX = window.visualViewport.width / 16;
   1716  const deltaY = window.visualViewport.height / 16;
   1717  const centerX =
   1718    window.visualViewport.pageLeft + window.visualViewport.width / 2;
   1719  const centerY =
   1720    window.visualViewport.pageTop + window.visualViewport.height / 2;
   1721  // prettier-ignore
   1722  var zoom_out = [
   1723      [ { x: centerX - (deltaX * 6), y: centerY - (deltaY * 6) },
   1724        { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) } ],
   1725      [ { x: centerX - (deltaX * 5), y: centerY - (deltaY * 5) },
   1726        { x: centerX + (deltaX * 5), y: centerY + (deltaY * 5) } ],
   1727      [ { x: centerX - (deltaX * 4), y: centerY - (deltaY * 4) },
   1728        { x: centerX + (deltaX * 4), y: centerY + (deltaY * 4) } ],
   1729      [ { x: centerX - (deltaX * 3), y: centerY - (deltaY * 3) },
   1730        { x: centerX + (deltaX * 3), y: centerY + (deltaY * 3) } ],
   1731      [ { x: centerX - (deltaX * 2), y: centerY - (deltaY * 2) },
   1732        { x: centerX + (deltaX * 2), y: centerY + (deltaY * 2) } ],
   1733      [ { x: centerX - (deltaX * 1), y: centerY - (deltaY * 1) },
   1734        { x: centerX + (deltaX * 1), y: centerY + (deltaY * 1) } ],
   1735  ];
   1736  return zoom_out;
   1737 }
   1738 
   1739 // This generates a touch-based pinch zoom-out gesture that is expected
   1740 // to succeed. It returns after APZ has completed the zoom and reaches the end
   1741 // of the transform. The touch inputs are directed to the center of the
   1742 // current visual viewport.
   1743 async function pinchZoomOutWithTouchAtCenter() {
   1744  var zoom_out = pinchZoomOutTouchSequenceAtCenter();
   1745  var touchIds = [0, 1];
   1746  await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds);
   1747 }
   1748 
   1749 // useTouchpad is only currently implemented on macOS
   1750 async function synthesizeDoubleTap(element, x, y, useTouchpad) {
   1751  if (useTouchpad) {
   1752    await synthesizeNativeTouchpadDoubleTap(element, x, y);
   1753  } else {
   1754    await synthesizeNativeTap(element, x, y);
   1755    await synthesizeNativeTap(element, x, y);
   1756  }
   1757 }
   1758 // useTouchpad is only currently implemented on macOS
   1759 async function doubleTapOn(element, x, y, useTouchpad) {
   1760  let transformEndPromise = promiseTransformEnd();
   1761 
   1762  await synthesizeDoubleTap(element, x, y, useTouchpad);
   1763 
   1764  // Wait for the APZ:TransformEnd to fire
   1765  await transformEndPromise;
   1766 
   1767  // Flush state so we can query an accurate resolution
   1768  await promiseApzFlushedRepaints();
   1769 }
   1770 
   1771 const NativePanHandlerForLinux = {
   1772  beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
   1773  updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
   1774  endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
   1775  promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
   1776  delta: -50,
   1777 };
   1778 
   1779 const NativePanHandlerForWindows = {
   1780  beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
   1781  updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
   1782  endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
   1783  promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
   1784  delta: 50,
   1785 };
   1786 
   1787 const NativePanHandlerForMac = {
   1788  // From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc.
   1789  beginPhase: 1, // kCGScrollPhaseBegan
   1790  updatePhase: 2, // kCGScrollPhaseChanged
   1791  endPhase: 4, // kCGScrollPhaseEnded
   1792  promiseNativePanEvent: promiseNativePanGestureEventAndWaitForObserver,
   1793  delta: -50,
   1794 };
   1795 
   1796 const NativePanHandlerForHeadless = {
   1797  beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN,
   1798  updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE,
   1799  endPhase: SpecialPowers.DOMWindowUtils.PHASE_END,
   1800  promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver,
   1801  delta: 50,
   1802 };
   1803 
   1804 function getPanHandler() {
   1805  if (SpecialPowers.isHeadless) {
   1806    return NativePanHandlerForHeadless;
   1807  }
   1808 
   1809  switch (getPlatform()) {
   1810    case "linux":
   1811      return NativePanHandlerForLinux;
   1812    case "windows":
   1813      return NativePanHandlerForWindows;
   1814    case "mac":
   1815      return NativePanHandlerForMac;
   1816    default:
   1817      throw new Error(
   1818        "There's no native pan handler on platform " + getPlatform()
   1819      );
   1820  }
   1821 }
   1822 
   1823 // Lazily get `NativePanHandler` to avoid an exception where we don't support
   1824 // native pan events (e.g. Android).
   1825 if (!window.hasOwnProperty("NativePanHandler")) {
   1826  Object.defineProperty(window, "NativePanHandler", {
   1827    get() {
   1828      return getPanHandler();
   1829    },
   1830  });
   1831 }
   1832 
   1833 async function panRightToLeftBegin(aElement, aX, aY, aMultiplier) {
   1834  await NativePanHandler.promiseNativePanEvent(
   1835    aElement,
   1836    aX,
   1837    aY,
   1838    NativePanHandler.delta * aMultiplier,
   1839    0,
   1840    NativePanHandler.beginPhase
   1841  );
   1842 }
   1843 
   1844 async function panRightToLeftUpdate(aElement, aX, aY, aMultiplier) {
   1845  await NativePanHandler.promiseNativePanEvent(
   1846    aElement,
   1847    aX,
   1848    aY,
   1849    NativePanHandler.delta * aMultiplier,
   1850    0,
   1851    NativePanHandler.updatePhase
   1852  );
   1853 }
   1854 
   1855 async function panRightToLeftEnd(aElement, aX, aY) {
   1856  await NativePanHandler.promiseNativePanEvent(
   1857    aElement,
   1858    aX,
   1859    aY,
   1860    0,
   1861    0,
   1862    NativePanHandler.endPhase
   1863  );
   1864 }
   1865 
   1866 async function panRightToLeft(aElement, aX, aY, aMultiplier) {
   1867  await panRightToLeftBegin(aElement, aX, aY, aMultiplier);
   1868  await panRightToLeftUpdate(aElement, aX, aY, aMultiplier);
   1869  await panRightToLeftEnd(aElement, aX, aY, aMultiplier);
   1870 }
   1871 
   1872 async function panLeftToRight(aElement, aX, aY, aMultiplier) {
   1873  await panLeftToRightBegin(aElement, aX, aY, aMultiplier);
   1874  await panLeftToRightUpdate(aElement, aX, aY, aMultiplier);
   1875  await panLeftToRightEnd(aElement, aX, aY, aMultiplier);
   1876 }
   1877 
   1878 async function panLeftToRightBegin(aElement, aX, aY, aMultiplier) {
   1879  await NativePanHandler.promiseNativePanEvent(
   1880    aElement,
   1881    aX,
   1882    aY,
   1883    -NativePanHandler.delta * aMultiplier,
   1884    0,
   1885    NativePanHandler.beginPhase
   1886  );
   1887 }
   1888 
   1889 async function panLeftToRightUpdate(aElement, aX, aY, aMultiplier) {
   1890  await NativePanHandler.promiseNativePanEvent(
   1891    aElement,
   1892    aX,
   1893    aY,
   1894    -NativePanHandler.delta * aMultiplier,
   1895    0,
   1896    NativePanHandler.updatePhase
   1897  );
   1898  await NativePanHandler.promiseNativePanEvent(
   1899    aElement,
   1900    aX,
   1901    aY,
   1902    -NativePanHandler.delta * aMultiplier,
   1903    0,
   1904    NativePanHandler.updatePhase
   1905  );
   1906 }
   1907 
   1908 async function panLeftToRightEnd(aElement, aX, aY) {
   1909  await NativePanHandler.promiseNativePanEvent(
   1910    aElement,
   1911    aX,
   1912    aY,
   1913    0,
   1914    0,
   1915    NativePanHandler.endPhase
   1916  );
   1917 }
   1918 
   1919 // Close the context menu on desktop platforms.
   1920 // NOTE: This function doesn't work if the context menu isn't open.
   1921 async function closeContextMenu() {
   1922  if (getPlatform() == "android") {
   1923    return;
   1924  }
   1925 
   1926  const contextmenuClosedPromise = SpecialPowers.spawnChrome([], async () => {
   1927    const menu = this.browsingContext.topChromeWindow.document.getElementById(
   1928      "contentAreaContextMenu"
   1929    );
   1930    ok(
   1931      menu.state == "open" || menu.state == "showing",
   1932      "This function is supposed to work only if the context menu is open or showing"
   1933    );
   1934 
   1935    return new Promise(resolve => {
   1936      menu.addEventListener(
   1937        "popuphidden",
   1938        () => {
   1939          resolve();
   1940        },
   1941        { once: true }
   1942      );
   1943      menu.hidePopup();
   1944    });
   1945  });
   1946 
   1947  await contextmenuClosedPromise;
   1948 }
   1949 
   1950 // Get a list of prefs which should be used for a subtest which wants to
   1951 // generate a smooth scroll animation using an input event. The smooth
   1952 // scroll animation is slowed down so the test can perform other actions
   1953 // while it's still in progress.
   1954 function getSmoothScrollPrefs(aInputType, aMsdPhysics) {
   1955  let result = [];
   1956  // Some callers just want the default and don't pass in aMsdPhysics.
   1957  if (aMsdPhysics !== undefined) {
   1958    result.push(["general.smoothScroll.msdPhysics.enabled", aMsdPhysics]);
   1959  } else {
   1960    aMsdPhysics = SpecialPowers.getBoolPref(
   1961      "general.smoothScroll.msdPhysics.enabled"
   1962    );
   1963  }
   1964  if (aInputType == "wheel") {
   1965    // We want to test real wheel events rather than pan events.
   1966    result.push(["apz.test.mac.synth_wheel_input", true]);
   1967  } /* keyboard input */ else {
   1968    // The default verticalScrollDistance (which is 3) is too small for native
   1969    // keyboard scrolling, it sometimes produces same scroll offsets in the early
   1970    // stages of the smooth animation.
   1971    result.push(["toolkit.scrollbox.verticalScrollDistance", 5]);
   1972  }
   1973  // Use a longer animation duration to avoid the situation that the
   1974  // animation stops accidentally in between each arrow input event.
   1975  // If the situation happens, scroll offsets will not change at the moment.
   1976  if (aMsdPhysics) {
   1977    // Prefs for MSD physics (applicable to any input type).
   1978    result.push(
   1979      ...[
   1980        ["general.smoothScroll.msdPhysics.motionBeginSpringConstant", 20],
   1981        ["general.smoothScroll.msdPhysics.regularSpringConstant", 20],
   1982        ["general.smoothScroll.msdPhysics.slowdownMinDeltaRatio", 0.1],
   1983        ["general.smoothScroll.msdPhysics.slowdownSpringConstant", 20],
   1984      ]
   1985    );
   1986  } else if (aInputType == "wheel") {
   1987    // Prefs for Bezier physics with wheel input.
   1988    result.push(
   1989      ...[
   1990        ["general.smoothScroll.mouseWheel.durationMaxMS", 1500],
   1991        ["general.smoothScroll.mouseWheel.durationMinMS", 1500],
   1992      ]
   1993    );
   1994  } else {
   1995    // Prefs for Bezier physics with keyboard input.
   1996    result.push(
   1997      ...[
   1998        ["general.smoothScroll.lines.durationMaxMS", 1500],
   1999        ["general.smoothScroll.lines.durationMinMS", 1500],
   2000      ]
   2001    );
   2002  }
   2003  return result;
   2004 }
   2005 
   2006 function buildRelativeScrollSmoothnessVariants(aInputType, aScrollMethods) {
   2007  let subtests = [];
   2008  for (let scrollMethod of aScrollMethods) {
   2009    subtests.push({
   2010      file: `helper_relative_scroll_smoothness.html?input-type=${aInputType}&scroll-method=${scrollMethod}&strict=true`,
   2011      prefs: [
   2012        ["apz.test.logging_enabled", true],
   2013        ...getSmoothScrollPrefs(aInputType, /* Bezier physics */ false),
   2014      ],
   2015    });
   2016    // For MSD physics, run the test with strict=false. The shape of the
   2017    // animation curve is highly timing dependent, and we can't guarantee
   2018    // that an animation will run long enough until the next input event
   2019    // arrives.
   2020    subtests.push({
   2021      file: `helper_relative_scroll_smoothness.html?input-type=${aInputType}&scroll-method=${scrollMethod}&strict=false`,
   2022      prefs: [
   2023        ["apz.test.logging_enabled", true],
   2024        ...getSmoothScrollPrefs(aInputType, /* MSD physics */ true),
   2025      ],
   2026    });
   2027  }
   2028  return subtests;
   2029 }
   2030 
   2031 // Right now this is only meaningful on Linux.
   2032 async function getWindowProtocol() {
   2033  if (getPlatform() != "linux") {
   2034    return "";
   2035  }
   2036 
   2037  return await SpecialPowers.spawnChrome([], () => {
   2038    try {
   2039      return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo)
   2040        .windowProtocol;
   2041    } catch {
   2042      return "";
   2043    }
   2044  });
   2045 }