tor-browser

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

apz_test_utils.js (45797B)


      1 // Utilities for writing APZ tests using the framework added in bug 961289
      2 
      3 // ----------------------------------------------------------------------
      4 // Functions that convert the APZ test data into a more usable form.
      5 // Every place we have a WebIDL sequence whose elements are dictionaries
      6 // with two elements, a key, and a value, we convert this into a JS
      7 // object with a property for each key/value pair. (This is the structure
      8 // we really want, but we can't express in directly in WebIDL.)
      9 // ----------------------------------------------------------------------
     10 
     11 // getHitTestConfig() expects apz_test_native_event_utils.js to be loaded as well.
     12 /* import-globals-from apz_test_native_event_utils.js */
     13 
     14 function convertEntries(entries) {
     15  var result = {};
     16  for (var i = 0; i < entries.length; ++i) {
     17    result[entries[i].key] = entries[i].value;
     18  }
     19  return result;
     20 }
     21 
     22 function parsePoint(str) {
     23  var pieces = str.replace(/[()\s]+/g, "").split(",");
     24  SimpleTest.is(pieces.length, 2, "expected string of form (x,y)");
     25  for (var i = 0; i < 2; i++) {
     26    var eq = pieces[i].indexOf("=");
     27    if (eq >= 0) {
     28      pieces[i] = pieces[i].substring(eq + 1);
     29    }
     30  }
     31  return {
     32    x: parseInt(pieces[0]),
     33    y: parseInt(pieces[1]),
     34  };
     35 }
     36 
     37 // Given a VisualViewport object, return the visual viewport
     38 // rect relative to the page.
     39 function getVisualViewportRect(vv) {
     40  return {
     41    x: vv.pageLeft,
     42    y: vv.pageTop,
     43    width: vv.width,
     44    height: vv.height,
     45  };
     46 }
     47 
     48 // Return the offset of the visual viewport relative to the layout viewport.
     49 function getRelativeViewportOffset(window) {
     50  const offsetX = {};
     51  const offsetY = {};
     52  const utils = SpecialPowers.getDOMWindowUtils(window);
     53  utils.getVisualViewportOffsetRelativeToLayoutViewport(offsetX, offsetY);
     54  return {
     55    x: offsetX.value,
     56    y: offsetY.value,
     57  };
     58 }
     59 
     60 function parseRect(str) {
     61  var pieces = str.replace(/[()\s]+/g, "").split(",");
     62  SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)");
     63  for (var i = 0; i < 4; i++) {
     64    var eq = pieces[i].indexOf("=");
     65    if (eq >= 0) {
     66      pieces[i] = pieces[i].substring(eq + 1);
     67    }
     68  }
     69  return {
     70    x: parseInt(pieces[0]),
     71    y: parseInt(pieces[1]),
     72    width: parseInt(pieces[2]),
     73    height: parseInt(pieces[3]),
     74  };
     75 }
     76 
     77 // These functions expect rects with fields named x/y/width/height, such as
     78 // that returned by parseRect().
     79 function rectContains(haystack, needle) {
     80  return (
     81    haystack.x <= needle.x &&
     82    haystack.y <= needle.y &&
     83    haystack.x + haystack.width >= needle.x + needle.width &&
     84    haystack.y + haystack.height >= needle.y + needle.height
     85  );
     86 }
     87 function rectToString(rect) {
     88  return (
     89    "(" + rect.x + "," + rect.y + "," + rect.width + "," + rect.height + ")"
     90  );
     91 }
     92 function assertRectContainment(
     93  haystackRect,
     94  haystackDesc,
     95  needleRect,
     96  needleDesc
     97 ) {
     98  SimpleTest.ok(
     99    rectContains(haystackRect, needleRect),
    100    haystackDesc +
    101      " " +
    102      rectToString(haystackRect) +
    103      " should contain " +
    104      needleDesc +
    105      " " +
    106      rectToString(needleRect)
    107  );
    108 }
    109 
    110 function getPropertyAsRect(scrollFrames, scrollId, prop) {
    111  SimpleTest.ok(
    112    scrollId in scrollFrames,
    113    "expected scroll frame data for scroll id " + scrollId
    114  );
    115  var scrollFrameData = scrollFrames[scrollId];
    116  SimpleTest.ok(
    117    "displayport" in scrollFrameData,
    118    "expected a " + prop + " for scroll id " + scrollId
    119  );
    120  var value = scrollFrameData[prop];
    121  return parseRect(value);
    122 }
    123 
    124 function convertScrollFrameData(scrollFrames) {
    125  var result = {};
    126  for (var i = 0; i < scrollFrames.length; ++i) {
    127    result[scrollFrames[i].scrollId] = convertEntries(scrollFrames[i].entries);
    128  }
    129  return result;
    130 }
    131 
    132 function convertBuckets(buckets) {
    133  var result = {};
    134  for (var i = 0; i < buckets.length; ++i) {
    135    result[buckets[i].sequenceNumber] = convertScrollFrameData(
    136      buckets[i].scrollFrames
    137    );
    138  }
    139  return result;
    140 }
    141 
    142 function convertTestData(testData) {
    143  var result = {};
    144  result.paints = convertBuckets(testData.paints);
    145  result.repaintRequests = convertBuckets(testData.repaintRequests);
    146  return result;
    147 }
    148 
    149 // Returns the last bucket that has at least one scrollframe. This
    150 // is useful for skipping over buckets that are from empty transactions,
    151 // because those don't contain any useful data.
    152 function getLastNonemptyBucket(buckets) {
    153  for (var i = buckets.length - 1; i >= 0; --i) {
    154    if (buckets[i].scrollFrames.length) {
    155      return buckets[i];
    156    }
    157  }
    158  return null;
    159 }
    160 
    161 // Takes something like "matrix(1, 0, 0, 1, 234.024, 528.29023)"" and returns a number array
    162 function parseTransform(transform) {
    163  return /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/
    164    .exec(transform)
    165    .slice(1)
    166    .map(parseFloat);
    167 }
    168 
    169 function isTransformClose(a, b, name) {
    170  is(
    171    a.length,
    172    b.length,
    173    `expected transforms ${a} and ${b} to be the same length`
    174  );
    175  for (let i = 0; i < a.length; i++) {
    176    ok(Math.abs(a[i] - b[i]) < 0.01, name);
    177  }
    178 }
    179 
    180 // Given APZ test data for a single paint on the compositor side,
    181 // reconstruct the APZC tree structure from the 'parentScrollId'
    182 // entries that were logged. More specifically, the subset of the
    183 // APZC tree structure corresponding to the layer subtree for the
    184 // content process that triggered the paint, is reconstructed (as
    185 // the APZ test data only contains information abot this subtree).
    186 function buildApzcTree(paint) {
    187  // The APZC tree can potentially have multiple root nodes,
    188  // so we invent a node that is the parent of all roots.
    189  // This 'root' does not correspond to an APZC.
    190  var root = { scrollId: -1, children: [] };
    191  for (let scrollId in paint) {
    192    paint[scrollId].children = [];
    193    paint[scrollId].scrollId = scrollId;
    194  }
    195  for (let scrollId in paint) {
    196    var parentNode = null;
    197    if ("hasNoParentWithSameLayersId" in paint[scrollId]) {
    198      parentNode = root;
    199    } else if ("parentScrollId" in paint[scrollId]) {
    200      parentNode = paint[paint[scrollId].parentScrollId];
    201    }
    202    parentNode.children.push(paint[scrollId]);
    203  }
    204  return root;
    205 }
    206 
    207 // Given an APZC tree produced by buildApzcTree, return the RCD node in
    208 // the tree, or null if there was none.
    209 function findRcdNode(apzcTree) {
    210  // isRootContent will be undefined or "1"
    211  if (apzcTree.isRootContent) {
    212    return apzcTree;
    213  }
    214  for (var i = 0; i < apzcTree.children.length; i++) {
    215    var rcd = findRcdNode(apzcTree.children[i]);
    216    if (rcd != null) {
    217      return rcd;
    218    }
    219  }
    220  return null;
    221 }
    222 
    223 // Return whether an element whose id includes |elementId| has been layerized.
    224 // Assumes |elementId| will be present in the content description for the
    225 // element, and not in the content descriptions of other elements.
    226 function isLayerized(elementId) {
    227  var contentTestData =
    228    SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData();
    229  var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
    230  ok(nonEmptyBucket != null, "expected at least one nonempty paint");
    231  var seqno = nonEmptyBucket.sequenceNumber;
    232  contentTestData = convertTestData(contentTestData);
    233  var paint = contentTestData.paints[seqno];
    234  for (var scrollId in paint) {
    235    if ("contentDescription" in paint[scrollId]) {
    236      if (paint[scrollId].contentDescription.includes(elementId)) {
    237        return true;
    238      }
    239    }
    240  }
    241  return false;
    242 }
    243 
    244 // Return a rect (or null) that holds the last known content-side displayport
    245 // for a given element. (The element selection works the same way, and with
    246 // the same assumptions as the isLayerized function above).
    247 function getLastContentDisplayportFor(
    248  aElementId,
    249  aOptions = { expectPainted: true, popupElement: null }
    250 ) {
    251  var contentTestData = SpecialPowers.getDOMWindowUtils(
    252    aOptions.popupElement ? aOptions.popupElement.ownerGlobal : window
    253  ).getContentAPZTestData(aOptions.popupElement);
    254  if (contentTestData == undefined) {
    255    ok(!aOptions.expectPainted, "expected to have apz test data (1)");
    256    return null;
    257  }
    258  var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints);
    259  if (nonEmptyBucket == null) {
    260    ok(!aOptions.expectPainted, "expected to have apz test data (2)");
    261    return null;
    262  }
    263  var seqno = nonEmptyBucket.sequenceNumber;
    264  contentTestData = convertTestData(contentTestData);
    265  var paint = contentTestData.paints[seqno];
    266  for (var scrollId in paint) {
    267    if ("contentDescription" in paint[scrollId]) {
    268      if (paint[scrollId].contentDescription.includes(aElementId)) {
    269        if ("displayport" in paint[scrollId]) {
    270          return parseRect(paint[scrollId].displayport);
    271        }
    272      }
    273    }
    274  }
    275  return null;
    276 }
    277 
    278 // Return the APZC tree (as produced by buildApzcTree) for the last
    279 // non-empty paint received by the compositor.
    280 function getLastApzcTree() {
    281  let data = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
    282  if (data == undefined) {
    283    ok(false, "expected to have compositor apz test data");
    284    return null;
    285  }
    286  if (!data.paints.length) {
    287    ok(false, "expected to have at least one compositor paint bucket");
    288    return null;
    289  }
    290  var seqno = data.paints[data.paints.length - 1].sequenceNumber;
    291  data = convertTestData(data);
    292  return buildApzcTree(data.paints[seqno]);
    293 }
    294 
    295 // Return a promise that is resolved on the next rAF callback
    296 function promiseFrame(aWindow = window) {
    297  return new Promise(resolve => {
    298    aWindow.requestAnimationFrame(resolve);
    299  });
    300 }
    301 
    302 // Return a promise that is resolved on the next MozAfterPaint event
    303 function promiseAfterPaint() {
    304  return new Promise(resolve => {
    305    window.addEventListener("MozAfterPaint", resolve, { once: true });
    306  });
    307 }
    308 
    309 // This waits until any pending events on the APZ controller thread are
    310 // processed, and any resulting repaint requests are received by the main
    311 // thread. Note that while the repaint requests do get processed by the
    312 // APZ handler on the main thread, the repaints themselves may not have
    313 // occurred by the the returned promise resolves. If you want to wait
    314 // for those repaints, consider using promiseApzFlushedRepaints instead.
    315 function promiseOnlyApzControllerFlushedWithoutSetTimeout(
    316  aWindow = window,
    317  aElement
    318 ) {
    319  return new Promise(function (resolve) {
    320    var fail = false;
    321    var repaintDone = function () {
    322      dump("PromiseApzRepaintsFlushed: APZ flush done\n");
    323      SpecialPowers.Services.obs.removeObserver(
    324        repaintDone,
    325        "apz-repaints-flushed"
    326      );
    327      resolve(!fail);
    328    };
    329    SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed");
    330    if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints(aElement)) {
    331      dump(
    332        "PromiseApzRepaintsFlushed: Flushed APZ repaints, waiting for callback...\n"
    333      );
    334    } else {
    335      dump(
    336        "PromiseApzRepaintsFlushed: Flushing APZ repaints was a no-op, triggering callback directly...\n"
    337      );
    338      fail = true;
    339      repaintDone();
    340    }
    341  });
    342 }
    343 
    344 // Another variant of the above promiseOnlyApzControllerFlushedWithoutSetTimeout
    345 // but with a setTimeout(0) callback.
    346 // |aElement| is an optional argument to do
    347 // promiseOnlyApzControllerFlushedWithoutSetTimeout for the given |aElement|
    348 // rather than |aWindow|. If you want to do "apz-repaints-flushed" in popup
    349 // windows, you need to specify the element inside the popup window.
    350 function promiseOnlyApzControllerFlushed(aWindow = window, aElement) {
    351  return new Promise(resolve => {
    352    promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow, aElement).then(
    353      result => {
    354        setTimeout(() => resolve(result), 0);
    355      }
    356    );
    357  });
    358 }
    359 
    360 // Flush repaints, APZ pending repaints, and any repaints resulting from that
    361 // flush. This is particularly useful if the test needs to reach some sort of
    362 // "idle" state in terms of repaints. Usually just waiting for all paints
    363 // followed by flushApzRepaints is sufficient to flush all APZ state back to
    364 // the main thread, but it can leave a paint scheduled which will get triggered
    365 // at some later time. For tests that specifically test for painting at
    366 // specific times, this method is the way to go. Even if in doubt, this is the
    367 // preferred method as the extra step is "safe" and shouldn't interfere with
    368 // most tests.
    369 // If you want to do the flush in popup windows, you need to specify |aPopupElement|.
    370 async function promiseApzFlushedRepaints(aPopupElement = null) {
    371  if (aPopupElement) {
    372    SimpleTest.ok(XULPopupElement.isInstance(aPopupElement));
    373  }
    374  await promiseAllPaintsDone();
    375  await promiseOnlyApzControllerFlushed(
    376    aPopupElement ? aPopupElement.ownerGlobal : window,
    377    aPopupElement
    378  );
    379  await promiseAllPaintsDone();
    380 }
    381 
    382 // This function takes a set of subtests to run one at a time in new top-level
    383 // windows, and returns a Promise that is resolved once all the subtests are
    384 // done running.
    385 //
    386 // The aSubtests array is an array of objects with the following keys:
    387 //   file: required, the filename of the subtest.
    388 //   prefs: optional, an array of arrays containing key-value prefs to set.
    389 //   dp_suppression: optional, a boolean on whether or not to respect displayport
    390 //                   suppression during the test.
    391 //   onload: optional, a function that will be registered as a load event listener
    392 //           for the child window that will hold the subtest. the function will be
    393 //           passed exactly one argument, which will be the child window.
    394 //   windowFeatures: optional, will be passed to as the third argument of `window.open`.
    395 //                   See https://developer.mozilla.org/en-US/docs/Web/API/Window/open#windowfeatures
    396 // An example of an array is:
    397 //   aSubtests = [
    398 //     { 'file': 'test_file_name.html' },
    399 //     { 'file': 'test_file_2.html', 'prefs': [['pref.name', true], ['other.pref', 1000]], 'dp_suppression': false }
    400 //     { 'file': 'file_3.html', 'onload': function(w) { w.subtestDone(); } }
    401 //   ];
    402 //
    403 // Each subtest should call one of the subtestDone() or subtestFailed()
    404 // functions when it is done, to indicate that the window should be torn
    405 // down and the next test should run.
    406 // These functions are injected into the subtest's window by this
    407 // function prior to loading the subtest. For convenience, the |is| and |ok|
    408 // functions provided by SimpleTest are also mapped into the subtest's window.
    409 // For other things from the parent, the subtest can use window.opener.<whatever>
    410 // to access objects.
    411 function runSubtestsSeriallyInFreshWindows(aSubtests) {
    412  return new Promise(function (resolve, reject) {
    413    var testIndex = -1;
    414    var w = null;
    415 
    416    // If the "apz.subtest" pref has been set, only a single subtest whose name matches
    417    // the pref's value (if any) will be run.
    418    var onlyOneSubtest = SpecialPowers.getCharPref(
    419      "apz.subtest",
    420      /* default = */ ""
    421    );
    422 
    423    function advanceSubtestExecutionWithFailure(msg) {
    424      SimpleTest.ok(false, msg);
    425      advanceSubtestExecution();
    426    }
    427 
    428    async function advanceSubtestExecution() {
    429      var test = aSubtests[testIndex];
    430      if (w) {
    431        // Run any cleanup functions registered in the subtest
    432        // Guard against the subtest not loading apz_test_utils.js
    433        if (w.ApzCleanup) {
    434          w.ApzCleanup.execute();
    435        }
    436        if (typeof test.dp_suppression != "undefined") {
    437          // We modified the suppression when starting the test, so now undo that.
    438          SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(
    439            !test.dp_suppression
    440          );
    441        }
    442 
    443        if (test.prefs) {
    444          // We pushed some prefs for this test, pop them, and re-invoke
    445          // advanceSubtestExecution() after that's been processed
    446          SpecialPowers.popPrefEnv(function () {
    447            w.close();
    448            w = null;
    449            advanceSubtestExecution();
    450          });
    451          return;
    452        }
    453 
    454        w.close();
    455      }
    456 
    457      testIndex++;
    458      if (testIndex >= aSubtests.length) {
    459        resolve();
    460        return;
    461      }
    462 
    463      await SimpleTest.promiseFocus(window);
    464 
    465      test = aSubtests[testIndex];
    466 
    467      let recognizedProps = [
    468        "file",
    469        "prefs",
    470        "dp_suppression",
    471        "onload",
    472        "windowFeatures",
    473      ];
    474      for (let prop in test) {
    475        if (!recognizedProps.includes(prop)) {
    476          SimpleTest.ok(
    477            false,
    478            "Subtest " + test.file + " has unrecognized property '" + prop + "'"
    479          );
    480          setTimeout(function () {
    481            advanceSubtestExecution();
    482          }, 0);
    483          return;
    484        }
    485      }
    486 
    487      if (onlyOneSubtest && onlyOneSubtest != test.file) {
    488        SimpleTest.ok(
    489          true,
    490          "Skipping " +
    491            test.file +
    492            " because only " +
    493            onlyOneSubtest +
    494            " is being run"
    495        );
    496        setTimeout(function () {
    497          advanceSubtestExecution();
    498        }, 0);
    499        return;
    500      }
    501 
    502      SimpleTest.ok(true, "Starting subtest " + test.file);
    503 
    504      if (typeof test.dp_suppression != "undefined") {
    505        // Normally during a test, the displayport will get suppressed during page
    506        // load, and unsuppressed at a non-deterministic time during the test. The
    507        // unsuppression can trigger a repaint which interferes with the test, so
    508        // to avoid that we can force the displayport to be unsuppressed for the
    509        // entire test which is more deterministic.
    510        SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression(
    511          test.dp_suppression
    512        );
    513      }
    514 
    515      function spawnTest(aFile) {
    516        var subtestUrl =
    517          location.href.substring(0, location.href.lastIndexOf("/") + 1) +
    518          aFile;
    519        w = window.open(
    520          subtestUrl,
    521          "_blank",
    522          test.windowFeatures ? test.windowFeatures : ""
    523        );
    524        w.subtestDone = advanceSubtestExecution;
    525        w.subtestFailed = advanceSubtestExecutionWithFailure;
    526        w.isApzSubtest = true;
    527        w.SimpleTest = SimpleTest;
    528        w.dump = function (msg) {
    529          return dump(aFile + " | " + msg);
    530        };
    531        w.info = function (msg) {
    532          return info(aFile + " | " + msg);
    533        };
    534        w.is = function (a, b, msg) {
    535          return is(a, b, aFile + " | " + msg);
    536        };
    537        w.isnot = function (a, b, msg) {
    538          return isnot(a, b, aFile + " | " + msg);
    539        };
    540        w.isfuzzy = function (a, b, eps, msg) {
    541          return isfuzzy(a, b, eps, aFile + " | " + msg);
    542        };
    543        w.ok = function (cond, msg) {
    544          arguments[1] = aFile + " | " + msg;
    545          // Forward all arguments to SimpleTest.ok where we will check that ok() was
    546          // called with at most 2 arguments.
    547          return SimpleTest.ok.apply(SimpleTest, arguments);
    548        };
    549        w.todo_is = function (a, b, msg) {
    550          return todo_is(a, b, aFile + " | " + msg);
    551        };
    552        w.todo = function (cond, msg) {
    553          return todo(cond, aFile + " | " + msg);
    554        };
    555        if (test.onload) {
    556          w.addEventListener(
    557            "load",
    558            function () {
    559              test.onload(w);
    560            },
    561            { once: true }
    562          );
    563        }
    564        function urlResolves(url) {
    565          var request = new XMLHttpRequest();
    566          request.open("GET", url, false);
    567          request.send();
    568          return request.status !== 404;
    569        }
    570        if (!urlResolves(subtestUrl)) {
    571          SimpleTest.ok(
    572            false,
    573            "Subtest URL " +
    574              subtestUrl +
    575              " does not resolve. " +
    576              "Be sure it's present in the support-files section of mochitest.toml."
    577          );
    578          reject();
    579          return undefined;
    580        }
    581        return w;
    582      }
    583 
    584      if (test.prefs) {
    585        // Got some prefs for this subtest, push them
    586        await SpecialPowers.pushPrefEnv({ set: test.prefs });
    587      }
    588      w = spawnTest(test.file);
    589    }
    590 
    591    advanceSubtestExecution();
    592  }).catch(function (e) {
    593    SimpleTest.ok(false, "Error occurred while running subtests: " + e);
    594  });
    595 }
    596 
    597 function pushPrefs(prefs) {
    598  return SpecialPowers.pushPrefEnv({ set: prefs });
    599 }
    600 
    601 async function waitUntilApzStable() {
    602  await SimpleTest.promiseFocus(window);
    603  dump("WaitUntilApzStable: done promiseFocus\n");
    604  await promiseAllPaintsDone();
    605  dump("WaitUntilApzStable: done promiseAllPaintsDone\n");
    606  await promiseOnlyApzControllerFlushed();
    607  dump("WaitUntilApzStable: all done\n");
    608 }
    609 
    610 // This function returns a promise that is resolved after at least one paint
    611 // has been sent and processed by the compositor. This function can force
    612 // such a paint to happen if none are pending. This is useful to run after
    613 // the waitUntilApzStable() but before reading the compositor-side APZ test
    614 // data, because the test data for the content layers id only gets populated
    615 // on content layer tree updates *after* the root layer tree has a RefLayer
    616 // pointing to the contnet layer tree. waitUntilApzStable itself guarantees
    617 // that the root layer tree is pointing to the content layer tree, but does
    618 // not guarantee the subsequent paint; this function does that job.
    619 async function forceLayerTreeToCompositor() {
    620  // Modify a style property to force a layout flush
    621  document.body.style.boxSizing = "border-box";
    622  var utils = SpecialPowers.getDOMWindowUtils(window);
    623  if (!utils.isMozAfterPaintPending) {
    624    dump("Forcing a paint since none was pending already...\n");
    625    var testMode = utils.isTestControllingRefreshes;
    626    utils.advanceTimeAndRefresh(0);
    627    if (!testMode) {
    628      utils.restoreNormalRefresh();
    629    }
    630  }
    631  await promiseAllPaintsDone(null, true);
    632  await promiseOnlyApzControllerFlushed();
    633 }
    634 
    635 function isApzEnabled() {
    636  var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled;
    637  if (!enabled) {
    638    // All tests are required to have at least one assertion. Since APZ is
    639    // disabled, and the main test is presumably not going to run, we stick in
    640    // a dummy assertion here to keep the test passing.
    641    SimpleTest.ok(true, "APZ is not enabled; this test will be skipped");
    642  }
    643  return enabled;
    644 }
    645 
    646 function isKeyApzEnabled() {
    647  return isApzEnabled() && SpecialPowers.getBoolPref("apz.keyboard.enabled");
    648 }
    649 
    650 // Take a snapshot of the given rect, *including compositor transforms* (i.e.
    651 // includes async scroll transforms applied by APZ). If you don't need the
    652 // compositor transforms, you can probably get away with using
    653 // SpecialPowers.snapshotWindowWithOptions or one of the friendlier wrappers.
    654 // The rect provided is expected to be relative to the screen, for example as
    655 // returned by rectRelativeToScreen in apz_test_native_event_utils.js.
    656 // Example usage:
    657 //   var snapshot = getSnapshot(rectRelativeToScreen(myDiv));
    658 // which will take a snapshot of the 'myDiv' element. Note that if part of the
    659 // element is obscured by other things on top, the snapshot will include those
    660 // things. If it is clipped by a scroll container, the snapshot will include
    661 // that area anyway, so you will probably get parts of the scroll container in
    662 // the snapshot. If the rect extends outside the browser window then the
    663 // results are undefined.
    664 // The snapshot is returned in the form of a data URL.
    665 function getSnapshot(rect) {
    666  function parentProcessSnapshot() {
    667    /* eslint-env mozilla/chrome-script */
    668    addMessageListener("snapshot", function (parentRect) {
    669      var topWin = Services.wm.getMostRecentWindow("navigator:browser");
    670      if (!topWin) {
    671        topWin = Services.wm.getMostRecentWindow("navigator:geckoview");
    672      }
    673 
    674      // reposition the rect relative to the top-level browser window
    675      parentRect = JSON.parse(parentRect);
    676      parentRect.x -= topWin.mozInnerScreenX;
    677      parentRect.y -= topWin.mozInnerScreenY;
    678 
    679      // take the snapshot
    680      var canvas = topWin.document.createElementNS(
    681        "http://www.w3.org/1999/xhtml",
    682        "canvas"
    683      );
    684      canvas.width = parentRect.width;
    685      canvas.height = parentRect.height;
    686      var ctx = canvas.getContext("2d");
    687      ctx.drawWindow(
    688        topWin,
    689        parentRect.x,
    690        parentRect.y,
    691        parentRect.width,
    692        parentRect.height,
    693        "rgb(255,255,255)",
    694        ctx.DRAWWINDOW_DRAW_VIEW |
    695          ctx.DRAWWINDOW_USE_WIDGET_LAYERS |
    696          ctx.DRAWWINDOW_DRAW_CARET
    697      );
    698      return canvas.toDataURL();
    699    });
    700  }
    701 
    702  if (typeof getSnapshot.chromeHelper == "undefined") {
    703    // This is the first time getSnapshot is being called; do initialization
    704    getSnapshot.chromeHelper = SpecialPowers.loadChromeScript(
    705      parentProcessSnapshot
    706    );
    707    ApzCleanup.register(function () {
    708      getSnapshot.chromeHelper.destroy();
    709    });
    710  }
    711 
    712  return getSnapshot.chromeHelper.sendQuery("snapshot", JSON.stringify(rect));
    713 }
    714 
    715 // Takes the document's query string and parses it, assuming the query string
    716 // is composed of key-value pairs where the value is in JSON format. The object
    717 // returned contains the various values indexed by their respective keys. In
    718 // case of duplicate keys, the last value be used.
    719 // Examples:
    720 //   ?key="value"&key2=false&key3=500
    721 //     produces { "key": "value", "key2": false, "key3": 500 }
    722 //   ?key={"x":0,"y":50}&key2=[1,2,true]
    723 //     produces { "key": { "x": 0, "y": 0 }, "key2": [1, 2, true] }
    724 function getQueryArgs() {
    725  var args = {};
    726  if (location.search.length) {
    727    var params = location.search.substr(1).split("&");
    728    for (var p of params) {
    729      var [k, v] = p.split("=");
    730      args[k] = JSON.parse(v);
    731    }
    732  }
    733  return args;
    734 }
    735 
    736 // An async function that inserts a script element with the given URI into
    737 // the head of the document of the given window. This function returns when
    738 // the load or error event fires on the script element, indicating completion.
    739 async function injectScript(aScript, aWindow = window) {
    740  var e = aWindow.document.createElement("script");
    741  e.type = "text/javascript";
    742  let loadPromise = new Promise((resolve, reject) => {
    743    e.onload = function () {
    744      resolve();
    745    };
    746    e.onerror = function () {
    747      dump("Script [" + aScript + "] errored out\n");
    748      reject();
    749    };
    750  });
    751  e.src = aScript;
    752  aWindow.document.getElementsByTagName("head")[0].appendChild(e);
    753  await loadPromise;
    754 }
    755 
    756 // Compute some configuration information used for hit testing.
    757 // The computed information is cached to avoid recomputing it
    758 // each time this function is called.
    759 // The computed information is an object with three fields:
    760 //   utils: the nsIDOMWindowUtils instance for this window
    761 //   isWindow: true if the platform is Windows
    762 //   activateAllScrollFrames: true if prefs indicate all scroll frames are
    763 //                            activated with at least a minimal display port
    764 function getHitTestConfig() {
    765  if (!("hitTestConfig" in window)) {
    766    var utils = SpecialPowers.getDOMWindowUtils(window);
    767    var isWindows = getPlatform() == "windows";
    768    let activateAllScrollFrames =
    769      SpecialPowers.getBoolPref("apz.wr.activate_all_scroll_frames") ||
    770      (SpecialPowers.getBoolPref(
    771        "apz.wr.activate_all_scroll_frames_when_fission"
    772      ) &&
    773        SpecialPowers.Services.appinfo.fissionAutostart);
    774 
    775    window.hitTestConfig = {
    776      utils,
    777      isWindows,
    778      activateAllScrollFrames,
    779    };
    780  }
    781  return window.hitTestConfig;
    782 }
    783 
    784 // Compute the coordinates of the center of the given element. The argument
    785 // can either be a string (the id of the element desired) or the element
    786 // itself.
    787 function centerOf(element) {
    788  if (typeof element === "string") {
    789    element = document.getElementById(element);
    790  }
    791  var bounds = element.getBoundingClientRect();
    792  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
    793 }
    794 
    795 // Peform a compositor hit test at the given point and return the result.
    796 // |point| is expected to be in CSS coordinates relative to the layout
    797 // viewport, since this is what sendMouseEvent() expects. (Note that this
    798 // is different from sendNativeMouseEvent() which expects screen coordinates
    799 // relative to the screen.)
    800 // The returned object has two fields:
    801 //   hitInfo: a combination of APZHitResultFlags
    802 //   scrollId: the view-id of the scroll frame that was hit
    803 function hitTest(point, popupElement = null) {
    804  var utils = getHitTestConfig().utils;
    805  dump("Hit-testing point (" + point.x + ", " + point.y + ")\n");
    806  utils.sendMozMouseHitTestEvent(point.x, point.y, popupElement);
    807  var data = utils.getCompositorAPZTestData(popupElement);
    808  ok(
    809    data.hitResults.length >= 1,
    810    "Expected at least one hit result in the APZTestData"
    811  );
    812  var result = data.hitResults[data.hitResults.length - 1];
    813  return {
    814    hitInfo: result.hitResult,
    815    scrollId: result.scrollId,
    816    layersId: result.layersId,
    817  };
    818 }
    819 
    820 // Returns a canonical stringification of the hitInfo bitfield.
    821 function hitInfoToString(hitInfo) {
    822  var strs = [];
    823  for (var flag in APZHitResultFlags) {
    824    if ((hitInfo & APZHitResultFlags[flag]) != 0) {
    825      strs.push(flag);
    826    }
    827  }
    828  if (!strs.length) {
    829    return "INVISIBLE";
    830  }
    831  strs.sort(function (a, b) {
    832    return APZHitResultFlags[a] - APZHitResultFlags[b];
    833  });
    834  return strs.join(" | ");
    835 }
    836 
    837 // Takes an object returned by hitTest, along with the expected values, and
    838 // asserts that they match. Notably, it uses hitInfoToString to provide a
    839 // more useful message for the case that the hit info doesn't match
    840 function checkHitResult(
    841  hitResult,
    842  expectedHitInfo,
    843  expectedScrollId,
    844  expectedLayersId,
    845  desc
    846 ) {
    847  is(
    848    hitInfoToString(hitResult.hitInfo),
    849    hitInfoToString(expectedHitInfo),
    850    desc + " hit info"
    851  );
    852  is(hitResult.scrollId, expectedScrollId, desc + " scrollid");
    853  is(hitResult.layersId, expectedLayersId, desc + " layersid");
    854 }
    855 
    856 // Symbolic constants used by hitTestScrollbar().
    857 var ScrollbarTrackLocation = {
    858  START: 1,
    859  END: 2,
    860 };
    861 var LayerState = {
    862  ACTIVE: 1,
    863  INACTIVE: 2,
    864 };
    865 
    866 // Perform a hit test on the scrollbar(s) of a scroll frame.
    867 // This function takes a single argument which is expected to be
    868 // an object with the following fields:
    869 //   element: The scroll frame to perform the hit test on.
    870 //   directions: The direction(s) of scrollbars to test.
    871 //     If directions.vertical is true, the vertical scrollbar will be tested.
    872 //     If directions.horizontal is true, the horizontal scrollbar will be tested.
    873 //     Both may be true in a single call (in which case two tests are performed).
    874 //   expectedScrollId: The scroll id that is expected to be hit, if activateAllScrollFrames is false.
    875 //   expectedLayersId: The layers id that is expected to be hit.
    876 //   trackLocation: One of ScrollbarTrackLocation.{START, END}.
    877 //     Determines which end of the scrollbar track is targeted.
    878 //   expectThumb: Whether the scrollbar thumb is expected to be present
    879 //     at the targeted end of the scrollbar track.
    880 //   layerState: Whether the scroll frame is active or inactive.
    881 // The function performs the hit tests and asserts that the returned
    882 // hit test information is consistent with the passed parameters.
    883 // There is no return value.
    884 // Tests that use this function must set the pref
    885 // "layout.scrollbars.always-layerize-track".
    886 function hitTestScrollbar(params) {
    887  var config = getHitTestConfig();
    888 
    889  var elem = params.element;
    890 
    891  var boundingClientRect = elem.getBoundingClientRect();
    892 
    893  var verticalScrollbarWidth = boundingClientRect.width - elem.clientWidth;
    894  var horizontalScrollbarHeight = boundingClientRect.height - elem.clientHeight;
    895 
    896  // On windows, the scrollbar tracks have buttons on the end. When computing
    897  // coordinates for hit-testing we need to account for this. We assume the
    898  // buttons are square, and so can use the scrollbar width/height to estimate
    899  // the size of the buttons
    900  var scrollbarArrowButtonHeight = config.isWindows
    901    ? verticalScrollbarWidth
    902    : 0;
    903  var scrollbarArrowButtonWidth = config.isWindows
    904    ? horizontalScrollbarHeight
    905    : 0;
    906 
    907  // Compute the expected hit result flags.
    908  // The direction flag (APZHitResultFlags.SCROLLBAR_VERTICAL) is added in
    909  // later, for the vertical test only.
    910  // The APZHitResultFlags.SCROLLBAR flag will be present regardless of whether
    911  // the layer is active or inactive because we force layerization of scrollbar
    912  // tracks. Unfortunately not forcing the layerization results in different
    913  // behaviour on different platforms which makes testing harder.
    914  var expectedHitInfo = APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR;
    915  if (params.expectThumb) {
    916    // The thumb has listeners which are APZ-aware.
    917    expectedHitInfo |= APZHitResultFlags.APZ_AWARE_LISTENERS;
    918    var expectActive =
    919      config.activateAllScrollFrames || params.layerState == LayerState.ACTIVE;
    920    if (!expectActive) {
    921      expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME;
    922    }
    923    // We do not generate the layers for thumbs on inactive scrollframes.
    924    if (expectActive) {
    925      expectedHitInfo |= APZHitResultFlags.SCROLLBAR_THUMB;
    926    }
    927  }
    928 
    929  var expectedScrollId = params.expectedScrollId;
    930  if (config.activateAllScrollFrames) {
    931    expectedScrollId = config.utils.getViewId(params.element);
    932    if (params.layerState == LayerState.ACTIVE) {
    933      is(
    934        expectedScrollId,
    935        params.expectedScrollId,
    936        "Expected scrollId for active scrollframe should match"
    937      );
    938    }
    939  }
    940 
    941  var scrollframeMsg =
    942    params.layerState == LayerState.ACTIVE
    943      ? "active scrollframe"
    944      : "inactive scrollframe";
    945 
    946  // Hit-test the targeted areas, assuming we don't have overlay scrollbars
    947  // with zero dimensions.
    948  if (params.directions.vertical && verticalScrollbarWidth > 0) {
    949    var verticalScrollbarPoint = {
    950      x: boundingClientRect.right - verticalScrollbarWidth / 2,
    951      y:
    952        params.trackLocation == ScrollbarTrackLocation.START
    953          ? boundingClientRect.y + scrollbarArrowButtonHeight + 5
    954          : boundingClientRect.bottom -
    955            horizontalScrollbarHeight -
    956            scrollbarArrowButtonHeight -
    957            5,
    958    };
    959    checkHitResult(
    960      hitTest(verticalScrollbarPoint),
    961      expectedHitInfo | APZHitResultFlags.SCROLLBAR_VERTICAL,
    962      expectedScrollId,
    963      params.expectedLayersId,
    964      scrollframeMsg + " - vertical scrollbar"
    965    );
    966  }
    967 
    968  if (params.directions.horizontal && horizontalScrollbarHeight > 0) {
    969    var horizontalScrollbarPoint = {
    970      x:
    971        params.trackLocation == ScrollbarTrackLocation.START
    972          ? boundingClientRect.x + scrollbarArrowButtonWidth + 5
    973          : boundingClientRect.right -
    974            verticalScrollbarWidth -
    975            scrollbarArrowButtonWidth -
    976            5,
    977      y: boundingClientRect.bottom - horizontalScrollbarHeight / 2,
    978    };
    979    checkHitResult(
    980      hitTest(horizontalScrollbarPoint),
    981      expectedHitInfo,
    982      expectedScrollId,
    983      params.expectedLayersId,
    984      scrollframeMsg + " - horizontal scrollbar"
    985    );
    986  }
    987 }
    988 
    989 // Return a list of prefs for the given test identifier.
    990 function getPrefs(ident) {
    991  switch (ident) {
    992    case "TOUCH_EVENTS:PAN":
    993      return [
    994        // Dropping the touch slop to 0 makes the tests easier to write because
    995        // we can just do a one-pixel drag to get over the pan threshold rather
    996        // than having to hard-code some larger value.
    997        ["apz.touch_start_tolerance", "0.0"],
    998        // The touchstart from the drag can turn into a long-tap if the touch-move
    999        // events get held up. Try to prevent that by making long-taps require
   1000        // a 10 second hold. Note that we also cannot enable chaos mode on this
   1001        // test for this reason, since chaos mode can cause the long-press timer
   1002        // to fire sooner than the pref dictates.
   1003        ["ui.click_hold_context_menus.delay", 10000],
   1004        // The subtests in this test do touch-drags to pan the page, but we don't
   1005        // want those pans to turn into fling animations, so we increase the
   1006        // fling min velocity requirement absurdly high.
   1007        ["apz.fling_min_velocity_threshold", "10000"],
   1008        // The helper_div_pan's div gets a displayport on scroll, but if the
   1009        // test takes too long the displayport can expire before the new scroll
   1010        // position is synced back to the main thread. So we disable displayport
   1011        // expiry for these tests.
   1012        ["apz.displayport_expiry_ms", 0],
   1013        // We need to disable touch resampling during these tests because we
   1014        // rely on touch move events being processed without delay. Touch
   1015        // resampling only processes them once vsync fires.
   1016        ["android.touch_resampling.enabled", false],
   1017      ];
   1018    case "TOUCH_ACTION":
   1019      return [
   1020        ...getPrefs("TOUCH_EVENTS:PAN"),
   1021        ["apz.test.fails_with_native_injection", getPlatform() == "windows"],
   1022      ];
   1023    default:
   1024      return [];
   1025  }
   1026 }
   1027 
   1028 var ApzCleanup = {
   1029  _cleanups: [],
   1030 
   1031  register(func) {
   1032    if (!this._cleanups.length) {
   1033      if (!window.isApzSubtest) {
   1034        SimpleTest.registerCleanupFunction(this.execute.bind(this));
   1035      } // else ApzCleanup.execute is called from runSubtestsSeriallyInFreshWindows
   1036    }
   1037    this._cleanups.push(func);
   1038  },
   1039 
   1040  execute() {
   1041    while (this._cleanups.length) {
   1042      var func = this._cleanups.pop();
   1043      try {
   1044        func();
   1045      } catch (ex) {
   1046        SimpleTest.ok(
   1047          false,
   1048          "Subtest cleanup function [" +
   1049            func.toString() +
   1050            "] threw exception [" +
   1051            ex +
   1052            "] on page [" +
   1053            location.href +
   1054            "]"
   1055        );
   1056      }
   1057    }
   1058  },
   1059 };
   1060 
   1061 /**
   1062 * Returns a promise that will resolve if `eventTarget` receives an event of the
   1063 * given type that passes the given filter. Only the first matching message is
   1064 * used. The filter must be a function (or null); it is called with the event
   1065 * object and the call must return true to resolve the promise.
   1066 */
   1067 function promiseOneEvent(eventTarget, eventType, filter) {
   1068  return new Promise((resolve, reject) => {
   1069    eventTarget.addEventListener(eventType, function listener(e) {
   1070      let success = false;
   1071      if (filter == null) {
   1072        success = true;
   1073      } else if (typeof filter == "function") {
   1074        try {
   1075          success = filter(e);
   1076        } catch (ex) {
   1077          dump(
   1078            `ERROR: Filter passed to promiseOneEvent threw exception: ${ex}\n`
   1079          );
   1080          reject();
   1081          return;
   1082        }
   1083      } else {
   1084        dump(
   1085          "ERROR: Filter passed to promiseOneEvent was neither null nor a function\n"
   1086        );
   1087        reject();
   1088        return;
   1089      }
   1090      if (success) {
   1091        eventTarget.removeEventListener(eventType, listener);
   1092        resolve(e);
   1093      }
   1094    });
   1095  });
   1096 }
   1097 
   1098 function visualViewportAsZoomedRect() {
   1099  let vv = window.visualViewport;
   1100  return {
   1101    x: vv.pageLeft,
   1102    y: vv.pageTop,
   1103    w: vv.width,
   1104    h: vv.height,
   1105    z: vv.scale,
   1106  };
   1107 }
   1108 
   1109 // Pulls the latest compositor APZ test data and checks to see if the
   1110 // scroller with id `scrollerId` was checkerboarding. It also ensures that
   1111 // a scroller with id `scrollerId` was actually found in the test data.
   1112 // This function requires that "apz.test.logging_enabled" be set to true,
   1113 // in order for the test data to be logged.
   1114 function assertNotCheckerboarded(utils, scrollerId, msgPrefix) {
   1115  utils.advanceTimeAndRefresh(0);
   1116  var data = utils.getCompositorAPZTestData();
   1117  //dump(JSON.stringify(data, null, 4));
   1118  var found = false;
   1119  for (apzcData of data.additionalData) {
   1120    if (apzcData.key == scrollerId) {
   1121      var checkerboarding = apzcData.value
   1122        .split(",")
   1123        .includes("checkerboarding");
   1124      ok(!checkerboarding, `${msgPrefix}: scroller is not checkerboarding`);
   1125      found = true;
   1126    }
   1127  }
   1128  ok(found, `${msgPrefix}: Found the scroller in the APZ data`);
   1129  utils.restoreNormalRefresh();
   1130 }
   1131 
   1132 async function waitToClearOutAnyPotentialScrolls(aWindow) {
   1133  await promiseFrame(aWindow);
   1134  await promiseFrame(aWindow);
   1135  await promiseOnlyApzControllerFlushed(aWindow);
   1136  await promiseFrame(aWindow);
   1137  await promiseFrame(aWindow);
   1138 }
   1139 
   1140 function waitForScrollEvent(target) {
   1141  return new Promise(resolve => {
   1142    target.addEventListener("scroll", resolve, { once: true });
   1143  });
   1144 }
   1145 
   1146 // This is another variant of promiseApzFlushedRepaints.
   1147 // We need this function because, unfortunately, there is no easy way to use
   1148 // paint_listeners.js' functions and apz_test_utils.js' functions in popup
   1149 // contents opened by extensions either as scripts in the popup contents or
   1150 // scripts inside SpecialPowers.spawn because we can't use privileged functions
   1151 // in the popup contents' script, we can't use functions basically as it as in
   1152 // the sandboxed context either.
   1153 async function promiseApzFlushedRepaintsInPopup(popup) {
   1154  // Flush APZ repaints and waits for MozAfterPaint.
   1155  await SpecialPowers.spawn(popup, [], async () => {
   1156    const utils = SpecialPowers.getDOMWindowUtils(content.window);
   1157 
   1158    async function promiseAllPaintsDone() {
   1159      return new Promise(resolve => {
   1160        function waitForPaints() {
   1161          if (utils.isMozAfterPaintPending) {
   1162            dump("Waits for a MozAfterPaint event\n");
   1163            content.window.addEventListener(
   1164              "MozAfterPaint",
   1165              () => {
   1166                dump("Got a MozAfterPaint event\n");
   1167                waitForPaints();
   1168              },
   1169              { once: true }
   1170            );
   1171          } else {
   1172            dump("No more pending MozAfterPaint\n");
   1173            content.window.setTimeout(resolve, 0);
   1174          }
   1175        }
   1176        waitForPaints();
   1177      });
   1178    }
   1179    await promiseAllPaintsDone();
   1180 
   1181    await new Promise(resolve => {
   1182      var repaintDone = function () {
   1183        dump("APZ flush done\n");
   1184        SpecialPowers.Services.obs.removeObserver(
   1185          repaintDone,
   1186          "apz-repaints-flushed"
   1187        );
   1188        content.window.setTimeout(resolve, 0);
   1189      };
   1190      SpecialPowers.Services.obs.addObserver(
   1191        repaintDone,
   1192        "apz-repaints-flushed"
   1193      );
   1194      if (utils.flushApzRepaints()) {
   1195        dump("Flushed APZ repaints, waiting for callback...\n");
   1196      } else {
   1197        dump(
   1198          "Flushing APZ repaints was a no-op, triggering callback directly...\n"
   1199        );
   1200        repaintDone();
   1201      }
   1202    });
   1203 
   1204    await promiseAllPaintsDone();
   1205  });
   1206 }
   1207 
   1208 // A utility function to make sure there's no scroll animation on the given
   1209 // |aElement|.
   1210 async function cancelScrollAnimation(aElement, aWindow = window) {
   1211  // In fact there's no good way to directly cancel the active animation on the
   1212  // element, so we destroy the corresponding scrollable frame then reconstruct
   1213  // a new scrollable frame so that it clobbers the animation.
   1214  const originalStyle = aElement.style.display;
   1215  aElement.style.display = "none";
   1216  await aWindow.promiseApzFlushedRepaints();
   1217  aElement.style.display = originalStyle;
   1218  await aWindow.promiseApzFlushedRepaints();
   1219 }
   1220 
   1221 function collectSampledScrollOffsets(aElement, aPopupElement = null) {
   1222  const utils = SpecialPowers.getDOMWindowUtils(
   1223    aPopupElement ? aPopupElement.ownerGlobal : window
   1224  );
   1225  let data = utils.getCompositorAPZTestData(aPopupElement);
   1226  let sampledResults = data.sampledResults;
   1227 
   1228  const layersId = utils.getLayersId(aPopupElement);
   1229  const scrollId = utils.getViewId(aElement);
   1230 
   1231  return sampledResults.filter(
   1232    result =>
   1233      SpecialPowers.wrap(result).layersId == layersId &&
   1234      SpecialPowers.wrap(result).scrollId == scrollId
   1235  );
   1236 }
   1237 
   1238 function cloneVisualViewport() {
   1239  return {
   1240    offsetLeft: visualViewport.offsetLeft,
   1241    offsetTop: visualViewport.offsetTop,
   1242    pageLeft: visualViewport.pageLeft,
   1243    pageTop: visualViewport.pageTop,
   1244    width: visualViewport.width,
   1245    height: visualViewport.height,
   1246    scale: visualViewport.scale,
   1247  };
   1248 }
   1249 
   1250 function compareVisualViewport(
   1251  aVisualViewportValue1,
   1252  aVisualViewportValue2,
   1253  aMessage
   1254 ) {
   1255  for (let p in aVisualViewportValue1) {
   1256    // Due to the method difference of the calculation for double-tap-zoom in
   1257    // OOP iframes, we allow 1.0 difference in each visualViewport value.
   1258    // NOTE: Because of our layer pixel snapping (bug 1774315 and bug 1852884)
   1259    // the visual viewport metrics can have one more pixel difference so we
   1260    // allow it here.
   1261    const tolerance = 1.0 + 1.0;
   1262    isfuzzy(
   1263      aVisualViewportValue1[p],
   1264      aVisualViewportValue2[p],
   1265      aVisualViewportValue1.scale > 1.0
   1266        ? tolerance
   1267        : tolerance / aVisualViewportValue1.scale,
   1268      `${p} should be same on ${aMessage}`
   1269    );
   1270  }
   1271 }
   1272 
   1273 // Loads a URL in an iframe and waits until APZ is stable
   1274 async function setupIframe(aIFrame, aURL, aIsOffScreen = false) {
   1275  const iframeLoadPromise = promiseOneEvent(aIFrame, "load", null);
   1276  aIFrame.src = aURL;
   1277  await iframeLoadPromise;
   1278 
   1279  if (!aIsOffScreen) {
   1280    await SpecialPowers.spawn(aIFrame, [], async () => {
   1281      await content.wrappedJSObject.waitUntilApzStable();
   1282    });
   1283  }
   1284 }
   1285 
   1286 // Loads a URL in an iframe and replaces its origin to
   1287 // create an out-of-process iframe
   1288 async function setupCrossOriginIFrame(aIFrame, aUrl, aIsOffScreen = false) {
   1289  let iframeURL = SimpleTest.getTestFileURL(aUrl);
   1290  iframeURL = iframeURL.replace(window.location.origin, "https://example.com");
   1291  await setupIframe(aIFrame, iframeURL, aIsOffScreen);
   1292  if (!aIsOffScreen) {
   1293    await SpecialPowers.spawn(aIFrame, [], async () => {
   1294      await SpecialPowers.contentTransformsReceived(content);
   1295    });
   1296  }
   1297 }
   1298 
   1299 // Make sure APZ is ready for the popup.
   1300 // With enabling GPU process initiating APZ in the popup takes some time.
   1301 // Before the APZ has been initiated, calling flushApzRepaints() for the popup
   1302 // returns false.
   1303 async function ensureApzReadyForPopup(
   1304  aPopupElement,
   1305  aWindow = window,
   1306  aRetry = 10
   1307 ) {
   1308  let retry = 0;
   1309  while (
   1310    !SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints(aPopupElement)
   1311  ) {
   1312    await promiseFrame();
   1313    retry++;
   1314    if (retry > aRetry) {
   1315      ok(false, "The popup didn't initialize APZ");
   1316      return;
   1317    }
   1318  }
   1319 }