tor-browser

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

reftest-content.js (53583B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* eslint-env mozilla/frame-script */
      6 
      7 const XHTML_NS = "http://www.w3.org/1999/xhtml";
      8 
      9 const DEBUG_CONTRACTID = "@mozilla.org/xpcom/debug;1";
     10 const PRINTSETTINGS_CONTRACTID = "@mozilla.org/gfx/printsettings-service;1";
     11 const NS_OBSERVER_SERVICE_CONTRACTID = "@mozilla.org/observer-service;1";
     12 const NS_GFXINFO_CONTRACTID = "@mozilla.org/gfx/info;1";
     13 const IO_SERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
     14 
     15 // "<!--CLEAR-->"
     16 const BLANK_URL_FOR_CLEARING =
     17  "data:text/html;charset=UTF-8,%3C%21%2D%2DCLEAR%2D%2D%3E";
     18 
     19 const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
     20  "resource://gre/modules/Timer.sys.mjs"
     21 );
     22 const { onSpellCheck } = ChromeUtils.importESModule(
     23  "resource://reftest/AsyncSpellCheckTestHelper.sys.mjs"
     24 );
     25 
     26 // This will load chrome Custom Elements inside chrome documents:
     27 ChromeUtils.importESModule(
     28  "resource://gre/modules/CustomElementsListener.sys.mjs"
     29 );
     30 
     31 var gBrowserIsRemote;
     32 var gHaveCanvasSnapshot = false;
     33 var gCurrentURL;
     34 var gCurrentURLRecordResults;
     35 var gCurrentURLTargetType;
     36 var gCurrentTestType;
     37 var gTimeoutHook = null;
     38 var gFailureTimeout = null;
     39 var gFailureReason;
     40 var gAssertionCount = 0;
     41 var gUpdateCanvasPromiseResolver = null;
     42 
     43 var gDebug;
     44 var gVerbose = false;
     45 
     46 var gCurrentTestStartTime;
     47 var gClearingForAssertionCheck = false;
     48 
     49 const TYPE_LOAD = "load"; // test without a reference (just test that it does
     50 // not assert, crash, hang, or leak)
     51 const TYPE_SCRIPT = "script"; // test contains individual test results
     52 const TYPE_PRINT = "print"; // test and reference will be printed to PDF's and
     53 // compared structurally
     54 
     55 // keep this in sync with globals.sys.mjs
     56 const URL_TARGET_TYPE_TEST = 0; // first url
     57 const URL_TARGET_TYPE_REFERENCE = 1; // second url, if any
     58 
     59 function webNavigation() {
     60  return docShell.QueryInterface(Ci.nsIWebNavigation);
     61 }
     62 
     63 function webProgress() {
     64  return docShell
     65    .QueryInterface(Ci.nsIInterfaceRequestor)
     66    .getInterface(Ci.nsIWebProgress);
     67 }
     68 
     69 function windowUtilsForWindow(w) {
     70  return w.windowUtils;
     71 }
     72 
     73 function windowUtils() {
     74  return windowUtilsForWindow(content);
     75 }
     76 
     77 function IDForEventTarget(event) {
     78  try {
     79    return "'" + event.target.getAttribute("id") + "'";
     80  } catch (ex) {
     81    return "<unknown>";
     82  }
     83 }
     84 
     85 var progressListener = {
     86  onStateChange(webprogress, request, flags) {
     87    let uri;
     88    try {
     89      request.QueryInterface(Ci.nsIChannel);
     90      uri = request.originalURI.spec;
     91    } catch (ex) {
     92      return;
     93    }
     94    const WPL = Ci.nsIWebProgressListener;
     95    const endFlags =
     96      WPL.STATE_STOP | WPL.STATE_IS_WINDOW | WPL.STATE_IS_NETWORK;
     97    if ((flags & endFlags) == endFlags) {
     98      OnDocumentLoad(uri);
     99    }
    100  },
    101  QueryInterface: ChromeUtils.generateQI([
    102    "nsIWebProgressListener",
    103    "nsISupportsWeakReference",
    104  ]),
    105 };
    106 
    107 function OnInitialLoad() {
    108  removeEventListener("load", OnInitialLoad, true);
    109 
    110  gDebug = Cc[DEBUG_CONTRACTID].getService(Ci.nsIDebug2);
    111  if (gDebug.isDebugBuild) {
    112    gAssertionCount = gDebug.assertionCount;
    113  }
    114  gVerbose = !!Services.env.get("MOZ_REFTEST_VERBOSE");
    115 
    116  RegisterMessageListeners();
    117 
    118  var initInfo = SendContentReady();
    119  gBrowserIsRemote = initInfo.remote;
    120 
    121  webProgress().addProgressListener(
    122    progressListener,
    123    Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
    124  );
    125 
    126  LogInfo("Using browser remote=" + gBrowserIsRemote + "\n");
    127 }
    128 
    129 function SetFailureTimeout(cb, timeout, uri) {
    130  var targetTime = Date.now() + timeout;
    131 
    132  var wrapper = function () {
    133    // Timeouts can fire prematurely in some cases (e.g. in chaos mode). If this
    134    // happens, set another timeout for the remaining time.
    135    let remainingMs = targetTime - Date.now();
    136    if (remainingMs > 0) {
    137      SetFailureTimeout(cb, remainingMs);
    138    } else {
    139      cb();
    140    }
    141  };
    142 
    143  // Once OnDocumentLoad is called to handle the 'load' event it will update
    144  // this error message to reflect what stage of the processing it has reached
    145  // as it advances to each stage in turn.
    146  gFailureReason =
    147    "timed out after " + timeout + " ms waiting for 'load' event for " + uri;
    148  gFailureTimeout = setTimeout(wrapper, timeout);
    149 }
    150 
    151 function StartTestURI(type, uri, uriTargetType, timeout) {
    152  // The GC is only able to clean up compartments after the CC runs. Since
    153  // the JS ref tests disable the normal browser chrome and do not otherwise
    154  // create substatial DOM garbage, the CC tends not to run enough normally.
    155  windowUtils().runNextCollectorTimer();
    156 
    157  gCurrentTestType = type;
    158  gCurrentURL = uri;
    159  gCurrentURLTargetType = uriTargetType;
    160  gCurrentURLRecordResults = 0;
    161 
    162  gCurrentTestStartTime = Date.now();
    163  if (gFailureTimeout != null) {
    164    SendException("program error managing timeouts\n");
    165  }
    166  SetFailureTimeout(LoadFailed, timeout, uri);
    167 
    168  LoadURI(gCurrentURL);
    169 }
    170 
    171 function setupTextZoom(contentRootElement) {
    172  if (
    173    !contentRootElement ||
    174    !contentRootElement.hasAttribute("reftest-text-zoom")
    175  ) {
    176    return;
    177  }
    178  docShell.browsingContext.textZoom =
    179    contentRootElement.getAttribute("reftest-text-zoom");
    180 }
    181 
    182 function setupFullZoom(contentRootElement) {
    183  if (!contentRootElement || !contentRootElement.hasAttribute("reftest-zoom")) {
    184    return;
    185  }
    186  docShell.browsingContext.fullZoom =
    187    contentRootElement.getAttribute("reftest-zoom");
    188 }
    189 
    190 function resetZoomAndTextZoom() {
    191  docShell.browsingContext.fullZoom = 1.0;
    192  docShell.browsingContext.textZoom = 1.0;
    193 }
    194 
    195 function doPrintMode(contentRootElement) {
    196  // use getAttribute because className works differently in HTML and SVG
    197  if (contentRootElement && contentRootElement.hasAttribute("class")) {
    198    var classList = contentRootElement.getAttribute("class").split(/\s+/);
    199    if (classList.includes("reftest-print")) {
    200      SendException("reftest-print is obsolete, use reftest-paged instead");
    201      return false;
    202    }
    203    return classList.includes("reftest-paged");
    204  }
    205  return false;
    206 }
    207 
    208 function setupPrintMode(contentRootElement) {
    209  var PSSVC = Cc[PRINTSETTINGS_CONTRACTID].getService(
    210    Ci.nsIPrintSettingsService
    211  );
    212  var ps = PSSVC.createNewPrintSettings();
    213  ps.paperWidth = 5;
    214  ps.paperHeight = 3;
    215 
    216  // Override any os-specific unwriteable margins
    217  ps.unwriteableMarginTop = 0;
    218  ps.unwriteableMarginLeft = 0;
    219  ps.unwriteableMarginBottom = 0;
    220  ps.unwriteableMarginRight = 0;
    221 
    222  ps.headerStrLeft = "";
    223  ps.headerStrCenter = "";
    224  ps.headerStrRight = "";
    225  ps.footerStrLeft = "";
    226  ps.footerStrCenter = "";
    227  ps.footerStrRight = "";
    228 
    229  const printBackgrounds = (() => {
    230    const attr = contentRootElement.getAttribute("reftest-paged-backgrounds");
    231    return !attr || attr != "false";
    232  })();
    233  ps.printBGColors = printBackgrounds;
    234  ps.printBGImages = printBackgrounds;
    235 
    236  docShell.docViewer.setPageModeForTesting(/* aPageMode */ true, ps);
    237 }
    238 
    239 // Message the parent process to ask it to print the current page to a PDF file.
    240 function printToPdf() {
    241  let currentDoc = content.document;
    242  let isPrintSelection = false;
    243  let printRange = "";
    244 
    245  if (currentDoc) {
    246    let contentRootElement = currentDoc.documentElement;
    247    printRange = contentRootElement.getAttribute("reftest-print-range") || "";
    248  }
    249 
    250  if (printRange) {
    251    if (printRange === "selection") {
    252      isPrintSelection = true;
    253    } else if (
    254      !printRange.split(",").every(range => /^[1-9]\d*-[1-9]\d*$/.test(range))
    255    ) {
    256      SendException("invalid value for reftest-print-range");
    257      return;
    258    }
    259  }
    260 
    261  SendStartPrint(isPrintSelection, printRange);
    262 }
    263 
    264 function attrOrDefault(element, attr, def) {
    265  return element.hasAttribute(attr) ? Number(element.getAttribute(attr)) : def;
    266 }
    267 
    268 function setupViewport(contentRootElement) {
    269  if (!contentRootElement) {
    270    return;
    271  }
    272 
    273  var sw = attrOrDefault(contentRootElement, "reftest-scrollport-w", 0);
    274  var sh = attrOrDefault(contentRootElement, "reftest-scrollport-h", 0);
    275  if (sw !== 0 || sh !== 0) {
    276    LogInfo("Setting viewport to <w=" + sw + ", h=" + sh + ">");
    277    windowUtils().setVisualViewportSize(sw, sh);
    278  }
    279 
    280  var res = attrOrDefault(contentRootElement, "reftest-resolution", 1);
    281  if (res !== 1) {
    282    LogInfo("Setting resolution to " + res);
    283    windowUtils().setResolutionAndScaleTo(res);
    284  }
    285 
    286  // XXX support viewconfig when needed
    287 }
    288 
    289 function setupDisplayport() {
    290  let promise = content.windowGlobalChild
    291    .getActor("ReftestFission")
    292    .SetupDisplayportRoot();
    293  return promise.then(
    294    function (result) {
    295      for (let errorString of result.errorStrings) {
    296        LogError(errorString);
    297      }
    298      for (let infoString of result.infoStrings) {
    299        LogInfo(infoString);
    300      }
    301    },
    302    function (reason) {
    303      LogError("SetupDisplayportRoot returned promise rejected: " + reason);
    304    }
    305  );
    306 }
    307 
    308 // Returns whether any offsets were updated
    309 function setupAsyncScrollOffsets(options) {
    310  let currentDoc = content.document;
    311  let contentRootElement = currentDoc ? currentDoc.documentElement : null;
    312 
    313  if (
    314    !contentRootElement ||
    315    !contentRootElement.hasAttribute("reftest-async-scroll")
    316  ) {
    317    return Promise.resolve(false);
    318  }
    319 
    320  let allowFailure = options.allowFailure;
    321  let promise = content.windowGlobalChild
    322    .getActor("ReftestFission")
    323    .sendQuery("SetupAsyncScrollOffsets", { allowFailure });
    324  return promise.then(
    325    function (result) {
    326      for (let errorString of result.errorStrings) {
    327        LogError(errorString);
    328      }
    329      for (let infoString of result.infoStrings) {
    330        LogInfo(infoString);
    331      }
    332      return result.updatedAny;
    333    },
    334    function (reason) {
    335      LogError(
    336        "SetupAsyncScrollOffsets SendQuery to parent promise rejected: " +
    337          reason
    338      );
    339      return false;
    340    }
    341  );
    342 }
    343 
    344 function setupAsyncZoom(options) {
    345  var currentDoc = content.document;
    346  var contentRootElement = currentDoc ? currentDoc.documentElement : null;
    347 
    348  if (
    349    !contentRootElement ||
    350    !contentRootElement.hasAttribute("reftest-async-zoom")
    351  ) {
    352    return false;
    353  }
    354 
    355  var zoom = attrOrDefault(contentRootElement, "reftest-async-zoom", 1);
    356  if (zoom != 1) {
    357    try {
    358      windowUtils().setAsyncZoom(contentRootElement, zoom);
    359      return true;
    360    } catch (e) {
    361      if (!options.allowFailure) {
    362        throw e;
    363      }
    364    }
    365  }
    366  return false;
    367 }
    368 
    369 function resetDisplayportAndViewport() {
    370  // XXX currently the displayport configuration lives on the
    371  // presshell and so is "reset" on nav when we get a new presshell.
    372 }
    373 
    374 function shouldWaitForPendingPaints() {
    375  // if gHaveCanvasSnapshot is false, we're not taking snapshots so
    376  // there is no need to wait for pending paints to be flushed.
    377  return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending;
    378 }
    379 
    380 function shouldWaitForReftestWaitRemoval(contentRootElement) {
    381  // use getAttribute because className works differently in HTML and SVG
    382  return (
    383    contentRootElement &&
    384    contentRootElement.hasAttribute("class") &&
    385    contentRootElement
    386      .getAttribute("class")
    387      .split(/\s+/)
    388      .includes("reftest-wait")
    389  );
    390 }
    391 
    392 function shouldSnapshotWholePage(contentRootElement) {
    393  // use getAttribute because className works differently in HTML and SVG
    394  return (
    395    contentRootElement &&
    396    contentRootElement.hasAttribute("class") &&
    397    contentRootElement
    398      .getAttribute("class")
    399      .split(/\s+/)
    400      .includes("reftest-snapshot-all")
    401  );
    402 }
    403 
    404 function shouldNotFlush(contentRootElement) {
    405  // use getAttribute because className works differently in HTML and SVG
    406  return (
    407    contentRootElement &&
    408    contentRootElement.hasAttribute("class") &&
    409    contentRootElement
    410      .getAttribute("class")
    411      .split(/\s+/)
    412      .includes("reftest-no-flush")
    413  );
    414 }
    415 
    416 function getNoPaintElements(contentRootElement) {
    417  return contentRootElement.getElementsByClassName("reftest-no-paint");
    418 }
    419 function getNoDisplayListElements(contentRootElement) {
    420  return contentRootElement.getElementsByClassName("reftest-no-display-list");
    421 }
    422 function getDisplayListElements(contentRootElement) {
    423  return contentRootElement.getElementsByClassName("reftest-display-list");
    424 }
    425 
    426 function getOpaqueLayerElements(contentRootElement) {
    427  return contentRootElement.getElementsByClassName("reftest-opaque-layer");
    428 }
    429 
    430 function getAssignedLayerMap(contentRootElement) {
    431  var layerNameToElementsMap = {};
    432  var elements = contentRootElement.querySelectorAll(
    433    "[reftest-assigned-layer]"
    434  );
    435  for (var i = 0; i < elements.length; ++i) {
    436    var element = elements[i];
    437    var layerName = element.getAttribute("reftest-assigned-layer");
    438    if (!(layerName in layerNameToElementsMap)) {
    439      layerNameToElementsMap[layerName] = [];
    440    }
    441    layerNameToElementsMap[layerName].push(element);
    442  }
    443  return layerNameToElementsMap;
    444 }
    445 
    446 const FlushMode = {
    447  ALL: 0,
    448  IGNORE_THROTTLED_ANIMATIONS: 1,
    449 };
    450 
    451 // Initial state. When the document has loaded and all MozAfterPaint events and
    452 // all explicit paint waits are flushed, we can fire the MozReftestInvalidate
    453 // event and move to the next state.
    454 const STATE_WAITING_TO_FIRE_INVALIDATE_EVENT = 0;
    455 // When reftest-wait has been removed from the root element, we can move to the
    456 // next state.
    457 const STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL = 1;
    458 // When spell checking is done on all spell-checked elements, we can move to the
    459 // next state.
    460 const STATE_WAITING_FOR_SPELL_CHECKS = 2;
    461 // When any pending compositor-side repaint requests have been flushed, we can
    462 // move to the next state.
    463 const STATE_WAITING_FOR_APZ_FLUSH = 3;
    464 // When all MozAfterPaint events and all explicit paint waits are flushed, we're
    465 // done and can move to the COMPLETED state.
    466 const STATE_WAITING_TO_FINISH = 4;
    467 const STATE_COMPLETED = 5;
    468 
    469 async function FlushRendering(aFlushMode) {
    470  let browsingContext = content.docShell.browsingContext;
    471  let ignoreThrottledAnimations =
    472    aFlushMode === FlushMode.IGNORE_THROTTLED_ANIMATIONS;
    473  // Ensure the refresh driver ticks at least once, this ensures some
    474  // preference changes take effect.
    475  let needsAnimationFrame = IsSnapshottableTestType();
    476  try {
    477    let result = await content.windowGlobalChild
    478      .getActor("ReftestFission")
    479      .sendQuery("FlushRendering", {
    480        browsingContext,
    481        ignoreThrottledAnimations,
    482        needsAnimationFrame,
    483      });
    484    for (let errorString of result.errorStrings) {
    485      LogError(errorString);
    486    }
    487    for (let warningString of result.warningStrings) {
    488      LogWarning(warningString);
    489    }
    490    for (let infoString of result.infoStrings) {
    491      LogInfo(infoString);
    492    }
    493  } catch (reason) {
    494    // We expect actors to go away causing sendQuery's to fail, so
    495    // just note it.
    496    LogInfo("FlushRendering sendQuery to parent rejected: " + reason);
    497  }
    498 }
    499 
    500 function WaitForTestEnd(
    501  contentRootElement,
    502  inPrintMode,
    503  spellCheckedElements,
    504  forURL
    505 ) {
    506  // WaitForTestEnd works via the MakeProgress function below. It is responsible for
    507  // moving through the states listed above and calling FlushRendering. We also listen
    508  // for a number of events, the most important of which is the AfterPaintListener,
    509  // which is responsible for updating the canvas after paints. In a fission world
    510  // FlushRendering and updating the canvas must necessarily be async operations.
    511  // During these async operations we want to wait for them to finish and we don't
    512  // want to try to do anything else (what would we even want to do while only some of
    513  // the processes involved have flushed layout or updated their layer trees?). So
    514  // we call OperationInProgress whenever we are about to go back to the event loop
    515  // during one of these calls, and OperationCompleted when it finishes. This prevents
    516  // anything else from running while we wait and getting us into a confused state. We
    517  // then record anything that happens while we are waiting to make sure that the
    518  // right actions are triggered. The possible actions are basically calling
    519  // MakeProgress from a setTimeout, and updating the canvas for an after paint event.
    520  // The after paint listener just stashes the rects and we update them after a
    521  // completed MakeProgress call. This is handled by
    522  // HandlePendingTasksAfterMakeProgress, which also waits for any pending after paint
    523  // events. The general sequence of events is:
    524  //   - MakeProgress
    525  //   - HandlePendingTasksAfterMakeProgress
    526  //     - wait for after paint event if one is pending
    527  //     - update canvas for after paint events we have received
    528  //   - MakeProgress
    529  //   etc
    530 
    531  function CheckForLivenessOfContentRootElement() {
    532    if (contentRootElement && Cu.isDeadWrapper(contentRootElement)) {
    533      contentRootElement = null;
    534    }
    535  }
    536 
    537  var setTimeoutCallMakeProgressWhenComplete = false;
    538 
    539  var operationInProgress = false;
    540  function OperationInProgress() {
    541    if (operationInProgress) {
    542      LogWarning("Nesting atomic operations?");
    543    }
    544    operationInProgress = true;
    545  }
    546  function OperationCompleted() {
    547    if (!operationInProgress) {
    548      LogWarning("Mismatched OperationInProgress/OperationCompleted calls?");
    549    }
    550    operationInProgress = false;
    551    if (setTimeoutCallMakeProgressWhenComplete) {
    552      setTimeoutCallMakeProgressWhenComplete = false;
    553      setTimeout(CallMakeProgress, 0);
    554    }
    555  }
    556  function AssertNoOperationInProgress() {
    557    if (operationInProgress) {
    558      LogWarning("AssertNoOperationInProgress but operationInProgress");
    559    }
    560  }
    561 
    562  var updateCanvasPending = false;
    563  var updateCanvasRects = [];
    564 
    565  var currentDoc = content.document;
    566  var state = STATE_WAITING_TO_FIRE_INVALIDATE_EVENT;
    567 
    568  var setTimeoutMakeProgressPending = false;
    569 
    570  function CallSetTimeoutMakeProgress() {
    571    if (setTimeoutMakeProgressPending) {
    572      return;
    573    }
    574    setTimeoutMakeProgressPending = true;
    575    setTimeout(CallMakeProgress, 0);
    576  }
    577 
    578  // This should only ever be called from a timeout.
    579  function CallMakeProgress() {
    580    if (operationInProgress) {
    581      setTimeoutCallMakeProgressWhenComplete = true;
    582      return;
    583    }
    584    setTimeoutMakeProgressPending = false;
    585    MakeProgress();
    586  }
    587 
    588  var waitingForAnAfterPaint = false;
    589 
    590  // Updates the canvas if there are pending updates for it. Checks if we
    591  // need to call MakeProgress.
    592  function HandlePendingTasksAfterMakeProgress() {
    593    AssertNoOperationInProgress();
    594 
    595    if (
    596      (state == STATE_WAITING_TO_FIRE_INVALIDATE_EVENT ||
    597        state == STATE_WAITING_TO_FINISH) &&
    598      shouldWaitForPendingPaints()
    599    ) {
    600      LogInfo(
    601        "HandlePendingTasksAfterMakeProgress waiting for a MozAfterPaint"
    602      );
    603      // We are in a state where we wait for MozAfterPaint to clear and a
    604      // MozAfterPaint event is pending, give it a chance to fire, but don't
    605      // let anything else run.
    606      waitingForAnAfterPaint = true;
    607      OperationInProgress();
    608      return;
    609    }
    610 
    611    if (updateCanvasPending) {
    612      LogInfo("HandlePendingTasksAfterMakeProgress updating canvas");
    613      updateCanvasPending = false;
    614      let rects = updateCanvasRects;
    615      updateCanvasRects = [];
    616      OperationInProgress();
    617      CheckForLivenessOfContentRootElement();
    618      let promise = SendUpdateCanvasForEvent(forURL, rects, contentRootElement);
    619      promise.then(function () {
    620        OperationCompleted();
    621        // After paint events are fired immediately after a paint (one
    622        // of the things that can call us). Don't confuse ourselves by
    623        // firing synchronously if we triggered the paint ourselves.
    624        CallSetTimeoutMakeProgress();
    625      });
    626    }
    627  }
    628 
    629  // true if rectA contains rectB
    630  function Contains(rectA, rectB) {
    631    return (
    632      rectA.left <= rectB.left &&
    633      rectB.right <= rectA.right &&
    634      rectA.top <= rectB.top &&
    635      rectB.bottom <= rectA.bottom
    636    );
    637  }
    638  // true if some rect in rectList contains rect
    639  function ContainedIn(rectList, rect) {
    640    for (let i = 0; i < rectList.length; ++i) {
    641      if (Contains(rectList[i], rect)) {
    642        return true;
    643      }
    644    }
    645    return false;
    646  }
    647 
    648  function AfterPaintListener(event) {
    649    LogInfo("AfterPaintListener in " + event.target.document.location.href);
    650    if (event.target.document != currentDoc) {
    651      // ignore paint events for subframes or old documents in the window.
    652      // Invalidation in subframes will cause invalidation in the toplevel document anyway.
    653      return;
    654    }
    655 
    656    updateCanvasPending = true;
    657    for (let r of event.clientRects) {
    658      if (ContainedIn(updateCanvasRects, r)) {
    659        continue;
    660      }
    661 
    662      // Copy the rect; it's content and we are chrome, which means if the
    663      // document goes away (and it can in some crashtests) our reference
    664      // to it will be turned into a dead wrapper that we can't acccess.
    665      updateCanvasRects.push({
    666        left: r.left,
    667        top: r.top,
    668        right: r.right,
    669        bottom: r.bottom,
    670      });
    671    }
    672 
    673    if (waitingForAnAfterPaint) {
    674      waitingForAnAfterPaint = false;
    675      OperationCompleted();
    676    }
    677 
    678    if (!operationInProgress) {
    679      HandlePendingTasksAfterMakeProgress();
    680    }
    681    // Otherwise we know that eventually after the operation finishes we
    682    // will get a MakeProgress and/or HandlePendingTasksAfterMakeProgress
    683    // call, so we don't need to do anything.
    684  }
    685 
    686  function FromChildAfterPaintListener(event) {
    687    LogInfo(
    688      "FromChildAfterPaintListener from " + event.detail.originalTargetUri
    689    );
    690 
    691    updateCanvasPending = true;
    692    for (let r of event.detail.rects) {
    693      if (ContainedIn(updateCanvasRects, r)) {
    694        continue;
    695      }
    696 
    697      // Copy the rect; it's content and we are chrome, which means if the
    698      // document goes away (and it can in some crashtests) our reference
    699      // to it will be turned into a dead wrapper that we can't acccess.
    700      updateCanvasRects.push({
    701        left: r.left,
    702        top: r.top,
    703        right: r.right,
    704        bottom: r.bottom,
    705      });
    706    }
    707 
    708    if (!operationInProgress) {
    709      HandlePendingTasksAfterMakeProgress();
    710    }
    711    // Otherwise we know that eventually after the operation finishes we
    712    // will get a MakeProgress and/or HandlePendingTasksAfterMakeProgress
    713    // call, so we don't need to do anything.
    714  }
    715 
    716  let attrModifiedObserver;
    717  function AttrModifiedListener() {
    718    LogInfo("AttrModifiedListener fired");
    719    // Wait for the next return-to-event-loop before continuing --- for
    720    // example, the attribute may have been modified in an subdocument's
    721    // load event handler, in which case we need load event processing
    722    // to complete and unsuppress painting before we check isMozAfterPaintPending.
    723    CallSetTimeoutMakeProgress();
    724  }
    725 
    726  function RemoveListeners() {
    727    // OK, we can end the test now.
    728    removeEventListener("MozAfterPaint", AfterPaintListener, false);
    729    removeEventListener(
    730      "Reftest:MozAfterPaintFromChild",
    731      FromChildAfterPaintListener,
    732      false
    733    );
    734    CheckForLivenessOfContentRootElement();
    735    if (attrModifiedObserver) {
    736      if (!Cu.isDeadWrapper(attrModifiedObserver)) {
    737        attrModifiedObserver.disconnect();
    738      }
    739      attrModifiedObserver = null;
    740    }
    741    gTimeoutHook = null;
    742    // Make sure we're in the COMPLETED state just in case
    743    // (this may be called via the test-timeout hook)
    744    state = STATE_COMPLETED;
    745  }
    746 
    747  // Everything that could cause shouldWaitForXXX() to
    748  // change from returning true to returning false is monitored via some kind
    749  // of event listener which eventually calls this function.
    750  function MakeProgress() {
    751    if (state >= STATE_COMPLETED) {
    752      LogInfo("MakeProgress: STATE_COMPLETED");
    753      return;
    754    }
    755 
    756    LogInfo("MakeProgress");
    757 
    758    // We don't need to flush styles any more when we are in the state
    759    // after reftest-wait has removed.
    760    OperationInProgress();
    761    let promise = Promise.resolve(undefined);
    762    if (state != STATE_WAITING_TO_FINISH) {
    763      // If we are waiting for the MozReftestInvalidate event we don't want
    764      // to flush throttled animations. Flushing throttled animations can
    765      // continue to cause new MozAfterPaint events even when all the
    766      // rendering we're concerned about should have ceased. Since
    767      // MozReftestInvalidate won't be sent until we finish waiting for all
    768      // MozAfterPaint events, we should avoid flushing throttled animations
    769      // here or else we'll never leave this state.
    770      let flushMode =
    771        state === STATE_WAITING_TO_FIRE_INVALIDATE_EVENT
    772          ? FlushMode.IGNORE_THROTTLED_ANIMATIONS
    773          : FlushMode.ALL;
    774      promise = FlushRendering(flushMode);
    775    }
    776    promise.then(function () {
    777      OperationCompleted();
    778      MakeProgress2();
    779      // If there is an operation in progress then we know there will be
    780      // a MakeProgress call is will happen after it finishes.
    781      if (!operationInProgress) {
    782        HandlePendingTasksAfterMakeProgress();
    783      }
    784    });
    785  }
    786 
    787  // eslint-disable-next-line complexity
    788  function MakeProgress2() {
    789    switch (state) {
    790      case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: {
    791        LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT");
    792        if (shouldWaitForPendingPaints() || updateCanvasPending) {
    793          gFailureReason =
    794            "timed out waiting for pending paint count to reach zero";
    795          if (shouldWaitForPendingPaints()) {
    796            gFailureReason += " (waiting for MozAfterPaint)";
    797            LogInfo("MakeProgress: waiting for MozAfterPaint");
    798          }
    799          if (updateCanvasPending) {
    800            gFailureReason += " (waiting for updateCanvasPending)";
    801            LogInfo("MakeProgress: waiting for updateCanvasPending");
    802          }
    803          return;
    804        }
    805 
    806        state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL;
    807        CheckForLivenessOfContentRootElement();
    808        var hasReftestWait =
    809          shouldWaitForReftestWaitRemoval(contentRootElement);
    810        // Notify the test document that now is a good time to test some invalidation
    811        LogInfo("MakeProgress: dispatching MozReftestInvalidate");
    812        if (contentRootElement) {
    813          let elements = getNoPaintElements(contentRootElement);
    814          for (let i = 0; i < elements.length; ++i) {
    815            windowUtils().checkAndClearPaintedState(elements[i]);
    816          }
    817          elements = getNoDisplayListElements(contentRootElement);
    818          for (let i = 0; i < elements.length; ++i) {
    819            windowUtils().checkAndClearDisplayListState(elements[i]);
    820          }
    821          elements = getDisplayListElements(contentRootElement);
    822          for (let i = 0; i < elements.length; ++i) {
    823            windowUtils().checkAndClearDisplayListState(elements[i]);
    824          }
    825          var notification = content.document.createEvent("Events");
    826          notification.initEvent("MozReftestInvalidate", true, false);
    827          contentRootElement.dispatchEvent(notification);
    828        } else {
    829          LogInfo(
    830            "MakeProgress: couldn't send MozReftestInvalidate event because content root element does not exist"
    831          );
    832        }
    833 
    834        CheckForLivenessOfContentRootElement();
    835        if (!inPrintMode && doPrintMode(contentRootElement)) {
    836          LogInfo("MakeProgress: setting up print mode");
    837          setupPrintMode(contentRootElement);
    838        }
    839 
    840        CheckForLivenessOfContentRootElement();
    841        if (
    842          hasReftestWait &&
    843          !shouldWaitForReftestWaitRemoval(contentRootElement)
    844        ) {
    845          // MozReftestInvalidate handler removed reftest-wait.
    846          // We expect something to have been invalidated...
    847          OperationInProgress();
    848          let promise = FlushRendering(FlushMode.ALL);
    849          promise.then(function () {
    850            OperationCompleted();
    851            if (!updateCanvasPending && !shouldWaitForPendingPaints()) {
    852              LogWarning("MozInvalidateEvent didn't invalidate");
    853            }
    854            MakeProgress();
    855          });
    856          return;
    857        }
    858        // Try next state
    859        MakeProgress();
    860        return;
    861      }
    862 
    863      case STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL:
    864        LogInfo("MakeProgress: STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL");
    865        CheckForLivenessOfContentRootElement();
    866        if (shouldWaitForReftestWaitRemoval(contentRootElement)) {
    867          gFailureReason = "timed out waiting for reftest-wait to be removed";
    868          LogInfo("MakeProgress: waiting for reftest-wait to be removed");
    869          return;
    870        }
    871 
    872        if (shouldNotFlush(contentRootElement)) {
    873          // If reftest-no-flush is specified, we need to set
    874          // updateCanvasPending explicitly to take the latest snapshot
    875          // since animation changes on the compositor thread don't invoke
    876          // any MozAfterPaint events at all.
    877          // NOTE: We don't add any rects to updateCanvasRects here since
    878          // SendUpdateCanvasForEvent() will handle this case properly
    879          // without any rects.
    880          updateCanvasPending = true;
    881        }
    882        // Try next state
    883        state = STATE_WAITING_FOR_SPELL_CHECKS;
    884        MakeProgress();
    885        return;
    886 
    887      case STATE_WAITING_FOR_SPELL_CHECKS:
    888        LogInfo("MakeProgress: STATE_WAITING_FOR_SPELL_CHECKS");
    889        if (numPendingSpellChecks) {
    890          gFailureReason = "timed out waiting for spell checks to end";
    891          LogInfo("MakeProgress: waiting for spell checks to end");
    892          return;
    893        }
    894 
    895        state = STATE_WAITING_FOR_APZ_FLUSH;
    896        LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
    897        gFailureReason = "timed out waiting for APZ flush to complete";
    898 
    899        var flushWaiter = function (aSubject, aTopic) {
    900          if (aTopic) {
    901            LogInfo("MakeProgress: apz-repaints-flushed fired");
    902          }
    903          Services.obs.removeObserver(flushWaiter, "apz-repaints-flushed");
    904          state = STATE_WAITING_TO_FINISH;
    905          if (operationInProgress) {
    906            CallSetTimeoutMakeProgress();
    907          } else {
    908            MakeProgress();
    909          }
    910        };
    911        Services.obs.addObserver(flushWaiter, "apz-repaints-flushed");
    912 
    913        var willSnapshot = IsSnapshottableTestType();
    914        CheckForLivenessOfContentRootElement();
    915        var noFlush = !shouldNotFlush(contentRootElement);
    916        if (noFlush && willSnapshot && windowUtils().flushApzRepaints()) {
    917          LogInfo("MakeProgress: done requesting APZ flush");
    918        } else {
    919          LogInfo("MakeProgress: APZ flush not required");
    920          flushWaiter(null, null, null);
    921        }
    922        return;
    923 
    924      case STATE_WAITING_FOR_APZ_FLUSH:
    925        LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
    926        // Nothing to do here; once we get the apz-repaints-flushed event
    927        // we will go to STATE_WAITING_TO_FINISH
    928        return;
    929 
    930      case STATE_WAITING_TO_FINISH:
    931        LogInfo("MakeProgress: STATE_WAITING_TO_FINISH");
    932        if (shouldWaitForPendingPaints() || updateCanvasPending) {
    933          gFailureReason =
    934            "timed out waiting for pending paint count to " +
    935            "reach zero (after reftest-wait removed and switch to print mode)";
    936          if (shouldWaitForPendingPaints()) {
    937            gFailureReason += " (waiting for MozAfterPaint)";
    938            LogInfo("MakeProgress: waiting for MozAfterPaint");
    939          }
    940          if (updateCanvasPending) {
    941            gFailureReason += " (waiting for updateCanvasPending)";
    942            LogInfo("MakeProgress: waiting for updateCanvasPending");
    943          }
    944          return;
    945        }
    946        CheckForLivenessOfContentRootElement();
    947        if (contentRootElement) {
    948          let elements = getNoPaintElements(contentRootElement);
    949          for (let i = 0; i < elements.length; ++i) {
    950            if (windowUtils().checkAndClearPaintedState(elements[i])) {
    951              SendFailedNoPaint();
    952            }
    953          }
    954          // We only support retained display lists in the content process
    955          // right now, so don't fail reftest-no-display-list tests when
    956          // we don't have e10s.
    957          if (gBrowserIsRemote) {
    958            elements = getNoDisplayListElements(contentRootElement);
    959            for (let i = 0; i < elements.length; ++i) {
    960              if (windowUtils().checkAndClearDisplayListState(elements[i])) {
    961                SendFailedNoDisplayList();
    962              }
    963            }
    964            elements = getDisplayListElements(contentRootElement);
    965            for (let i = 0; i < elements.length; ++i) {
    966              if (!windowUtils().checkAndClearDisplayListState(elements[i])) {
    967                SendFailedDisplayList();
    968              }
    969            }
    970          }
    971        }
    972 
    973        if (!IsSnapshottableTestType()) {
    974          // If we're not snapshotting the test, at least do a sync round-trip
    975          // to the compositor to ensure that all the rendering messages
    976          // related to this test get processed. Otherwise problems triggered
    977          // by this test may only manifest as failures in a later test.
    978          LogInfo("MakeProgress: Doing sync flush to compositor");
    979          gFailureReason = "timed out while waiting for sync compositor flush";
    980          windowUtils().syncFlushCompositor();
    981        }
    982 
    983        LogInfo("MakeProgress: Completed");
    984        state = STATE_COMPLETED;
    985        gFailureReason = "timed out while taking snapshot (bug in harness?)";
    986        RemoveListeners();
    987        CheckForLivenessOfContentRootElement();
    988        CheckForProcessCrashExpectation(contentRootElement);
    989        setTimeout(RecordResult, 0, forURL);
    990    }
    991  }
    992 
    993  LogInfo("WaitForTestEnd: Adding listeners");
    994  addEventListener("MozAfterPaint", AfterPaintListener, false);
    995  addEventListener(
    996    "Reftest:MozAfterPaintFromChild",
    997    FromChildAfterPaintListener,
    998    false
    999  );
   1000 
   1001  // If contentRootElement is null then shouldWaitForReftestWaitRemoval will
   1002  // always return false so we don't need a listener anyway
   1003  CheckForLivenessOfContentRootElement();
   1004  if (contentRootElement?.hasAttribute("class")) {
   1005    attrModifiedObserver =
   1006      // ownerGlobal doesn't exist in content windows.
   1007      // eslint-disable-next-line mozilla/use-ownerGlobal
   1008      new contentRootElement.ownerDocument.defaultView.MutationObserver(
   1009        AttrModifiedListener
   1010      );
   1011    attrModifiedObserver.observe(contentRootElement, { attributes: true });
   1012  }
   1013  gTimeoutHook = RemoveListeners;
   1014 
   1015  // Listen for spell checks on spell-checked elements.
   1016  var numPendingSpellChecks = spellCheckedElements.length;
   1017  function decNumPendingSpellChecks() {
   1018    --numPendingSpellChecks;
   1019    if (operationInProgress) {
   1020      CallSetTimeoutMakeProgress();
   1021    } else {
   1022      MakeProgress();
   1023    }
   1024  }
   1025  for (let editable of spellCheckedElements) {
   1026    try {
   1027      onSpellCheck(editable, decNumPendingSpellChecks);
   1028    } catch (err) {
   1029      // The element may not have an editor, so ignore it.
   1030      setTimeout(decNumPendingSpellChecks, 0);
   1031    }
   1032  }
   1033 
   1034  // Take a full snapshot now that all our listeners are set up. This
   1035  // ensures it's impossible for us to miss updates between taking the snapshot
   1036  // and adding our listeners.
   1037  OperationInProgress();
   1038  let promise = SendInitCanvasWithSnapshot(forURL);
   1039  promise.then(function () {
   1040    OperationCompleted();
   1041    MakeProgress();
   1042  });
   1043 }
   1044 
   1045 async function OnDocumentLoad(uri) {
   1046  if (gClearingForAssertionCheck) {
   1047    if (uri == BLANK_URL_FOR_CLEARING) {
   1048      DoAssertionCheck();
   1049      return;
   1050    }
   1051 
   1052    // It's likely the previous test document reloads itself and causes the
   1053    // attempt of loading blank page fails. In this case we should retry
   1054    // loading the blank page.
   1055    LogInfo("Retry loading a blank page");
   1056    setTimeout(LoadURI, 0, BLANK_URL_FOR_CLEARING);
   1057    return;
   1058  }
   1059 
   1060  if (uri != gCurrentURL) {
   1061    LogInfo("OnDocumentLoad fired for previous document");
   1062    // Ignore load events for previous documents.
   1063    return;
   1064  }
   1065 
   1066  var currentDoc = content && content.document;
   1067 
   1068  // Collect all editable, spell-checked elements.  It may be the case that
   1069  // not all the elements that match this selector will be spell checked: for
   1070  // example, a textarea without a spellcheck attribute may have a parent with
   1071  // spellcheck=false, or script may set spellcheck=false on an element whose
   1072  // markup sets it to true.  But that's OK since onSpellCheck detects the
   1073  // absence of spell checking, too.
   1074  var querySelector =
   1075    '*[class~="spell-checked"],' +
   1076    'textarea:not([spellcheck="false"]),' +
   1077    'input[spellcheck]:-moz-any([spellcheck=""],[spellcheck="true"]),' +
   1078    '*[contenteditable]:-moz-any([contenteditable=""],[contenteditable="true"])';
   1079  var spellCheckedElements = currentDoc
   1080    ? currentDoc.querySelectorAll(querySelector)
   1081    : [];
   1082 
   1083  var contentRootElement = currentDoc ? currentDoc.documentElement : null;
   1084  currentDoc = null;
   1085  setupFullZoom(contentRootElement);
   1086  setupTextZoom(contentRootElement);
   1087  setupViewport(contentRootElement);
   1088  await setupDisplayport(contentRootElement);
   1089  var inPrintMode = false;
   1090 
   1091  async function AfterOnLoadScripts() {
   1092    // Regrab the root element, because the document may have changed.
   1093    var contentRootElement = content.document
   1094      ? content.document.documentElement
   1095      : null;
   1096 
   1097    // Flush the document in case it got modified in a load event handler.
   1098    await FlushRendering(FlushMode.ALL);
   1099 
   1100    // Take a snapshot now.
   1101    let painted = await SendInitCanvasWithSnapshot(uri);
   1102 
   1103    if (contentRootElement && Cu.isDeadWrapper(contentRootElement)) {
   1104      contentRootElement = null;
   1105    }
   1106 
   1107    if (
   1108      (!inPrintMode && doPrintMode(contentRootElement)) ||
   1109      // If we didn't force a paint above, in
   1110      // InitCurrentCanvasWithSnapshot, so we should wait for a
   1111      // paint before we consider them done.
   1112      !painted
   1113    ) {
   1114      LogInfo("AfterOnLoadScripts belatedly entering WaitForTestEnd");
   1115      // Go into reftest-wait mode belatedly.
   1116      WaitForTestEnd(contentRootElement, inPrintMode, [], uri);
   1117    } else {
   1118      CheckForProcessCrashExpectation(contentRootElement);
   1119      RecordResult(uri);
   1120    }
   1121  }
   1122 
   1123  if (
   1124    shouldWaitForReftestWaitRemoval(contentRootElement) ||
   1125    spellCheckedElements.length
   1126  ) {
   1127    // Go into reftest-wait mode immediately after painting has been
   1128    // unsuppressed, after the onload event has finished dispatching.
   1129    gFailureReason =
   1130      "timed out waiting for test to complete (trying to get into WaitForTestEnd)";
   1131    LogInfo("OnDocumentLoad triggering WaitForTestEnd");
   1132    setTimeout(function () {
   1133      WaitForTestEnd(
   1134        contentRootElement,
   1135        inPrintMode,
   1136        spellCheckedElements,
   1137        uri
   1138      );
   1139    }, 0);
   1140  } else {
   1141    if (doPrintMode(contentRootElement)) {
   1142      LogInfo("OnDocumentLoad setting up print mode");
   1143      setupPrintMode(contentRootElement);
   1144      inPrintMode = true;
   1145    }
   1146 
   1147    // Since we can't use a bubbling-phase load listener from chrome,
   1148    // this is a capturing phase listener.  So do setTimeout twice, the
   1149    // first to get us after the onload has fired in the content, and
   1150    // the second to get us after any setTimeout(foo, 0) in the content.
   1151    gFailureReason =
   1152      "timed out waiting for test to complete (waiting for onload scripts to complete)";
   1153    LogInfo("OnDocumentLoad triggering AfterOnLoadScripts");
   1154    setTimeout(function () {
   1155      setTimeout(AfterOnLoadScripts, 0);
   1156    }, 0);
   1157  }
   1158 }
   1159 
   1160 function CheckForProcessCrashExpectation(contentRootElement) {
   1161  if (
   1162    contentRootElement &&
   1163    contentRootElement.hasAttribute("class") &&
   1164    contentRootElement
   1165      .getAttribute("class")
   1166      .split(/\s+/)
   1167      .includes("reftest-expect-process-crash")
   1168  ) {
   1169    SendExpectProcessCrash();
   1170  }
   1171 }
   1172 
   1173 async function RecordResult(forURL) {
   1174  if (forURL != gCurrentURL) {
   1175    LogInfo("RecordResult fired for previous document");
   1176    return;
   1177  }
   1178 
   1179  if (gCurrentURLRecordResults > 0) {
   1180    LogInfo("RecordResult fired extra times");
   1181    FinishTestItem();
   1182    return;
   1183  }
   1184  gCurrentURLRecordResults++;
   1185 
   1186  LogInfo("RecordResult fired");
   1187 
   1188  var currentTestRunTime = Date.now() - gCurrentTestStartTime;
   1189 
   1190  clearTimeout(gFailureTimeout);
   1191  gFailureReason = null;
   1192  gFailureTimeout = null;
   1193  gCurrentURL = null;
   1194  gCurrentURLTargetType = undefined;
   1195 
   1196  if (gCurrentTestType == TYPE_PRINT) {
   1197    printToPdf();
   1198    return;
   1199  }
   1200  if (gCurrentTestType == TYPE_SCRIPT) {
   1201    var error = "";
   1202    var testwindow = content;
   1203 
   1204    if (testwindow.wrappedJSObject) {
   1205      testwindow = testwindow.wrappedJSObject;
   1206    }
   1207 
   1208    var testcases;
   1209    if (
   1210      !testwindow.getTestCases ||
   1211      typeof testwindow.getTestCases != "function"
   1212    ) {
   1213      // Force an unexpected failure to alert the test author to fix the test.
   1214      error = "test must provide a function getTestCases(). (SCRIPT)\n";
   1215    } else if (!(testcases = testwindow.getTestCases())) {
   1216      // Force an unexpected failure to alert the test author to fix the test.
   1217      error =
   1218        "test's getTestCases() must return an Array-like Object. (SCRIPT)\n";
   1219    } else if (!testcases.length) {
   1220      // This failure may be due to a JavaScript Engine bug causing
   1221      // early termination of the test. If we do not allow silent
   1222      // failure, the driver will report an error.
   1223    }
   1224 
   1225    var results = [];
   1226    if (!error) {
   1227      // FIXME/bug 618176: temporary workaround
   1228      for (var i = 0; i < testcases.length; ++i) {
   1229        var test = testcases[i];
   1230        results.push({
   1231          passed: test.testPassed(),
   1232          description: test.testDescription(),
   1233        });
   1234      }
   1235      //results = testcases.map(function(test) {
   1236      //        return { passed: test.testPassed(),
   1237      //                 description: test.testDescription() };
   1238    }
   1239 
   1240    SendScriptResults(currentTestRunTime, error, results);
   1241    FinishTestItem();
   1242    return;
   1243  }
   1244 
   1245  // Setup async scroll offsets now in case SynchronizeForSnapshot is not
   1246  // called (due to reftest-no-sync-layers being supplied, or in the single
   1247  // process case).
   1248  let changedAsyncScrollZoom = await setupAsyncScrollOffsets({
   1249    allowFailure: true,
   1250  });
   1251  if (setupAsyncZoom({ allowFailure: true })) {
   1252    changedAsyncScrollZoom = true;
   1253  }
   1254  if (changedAsyncScrollZoom && !gBrowserIsRemote) {
   1255    sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
   1256  }
   1257 
   1258  SendTestDone(currentTestRunTime);
   1259  FinishTestItem();
   1260 }
   1261 
   1262 function LoadFailed() {
   1263  if (gTimeoutHook) {
   1264    gTimeoutHook();
   1265  }
   1266  gFailureTimeout = null;
   1267  SendFailedLoad(gFailureReason);
   1268 }
   1269 
   1270 function FinishTestItem() {
   1271  gHaveCanvasSnapshot = false;
   1272 }
   1273 
   1274 function DoAssertionCheck() {
   1275  gClearingForAssertionCheck = false;
   1276 
   1277  var numAsserts = 0;
   1278  if (gDebug.isDebugBuild) {
   1279    var newAssertionCount = gDebug.assertionCount;
   1280    numAsserts = newAssertionCount - gAssertionCount;
   1281    gAssertionCount = newAssertionCount;
   1282  }
   1283  SendAssertionCount(numAsserts);
   1284 }
   1285 
   1286 function LoadURI(uri) {
   1287  let loadURIOptions = {
   1288    triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
   1289  };
   1290  webNavigation().loadURI(Services.io.newURI(uri), loadURIOptions);
   1291 }
   1292 
   1293 function LogError(str) {
   1294  if (gVerbose) {
   1295    sendSyncMessage("reftest:Log", { type: "error", msg: str });
   1296  } else {
   1297    sendAsyncMessage("reftest:Log", { type: "error", msg: str });
   1298  }
   1299 }
   1300 
   1301 function LogWarning(str) {
   1302  if (gVerbose) {
   1303    sendSyncMessage("reftest:Log", { type: "warning", msg: str });
   1304  } else {
   1305    sendAsyncMessage("reftest:Log", { type: "warning", msg: str });
   1306  }
   1307 }
   1308 
   1309 function LogInfo(str) {
   1310  if (gVerbose) {
   1311    sendSyncMessage("reftest:Log", { type: "info", msg: str });
   1312  } else {
   1313    sendAsyncMessage("reftest:Log", { type: "info", msg: str });
   1314  }
   1315 }
   1316 
   1317 function IsSnapshottableTestType() {
   1318  // Script, load-only, and PDF-print tests do not need any snapshotting.
   1319  return !(
   1320    gCurrentTestType == TYPE_SCRIPT ||
   1321    gCurrentTestType == TYPE_LOAD ||
   1322    gCurrentTestType == TYPE_PRINT
   1323  );
   1324 }
   1325 
   1326 const SYNC_DEFAULT = 0x0;
   1327 const SYNC_ALLOW_DISABLE = 0x1;
   1328 // Returns a promise that resolve when the snapshot is done.
   1329 function SynchronizeForSnapshot(flags) {
   1330  if (!IsSnapshottableTestType()) {
   1331    return Promise.resolve(undefined);
   1332  }
   1333 
   1334  if (flags & SYNC_ALLOW_DISABLE) {
   1335    var docElt = content.document.documentElement;
   1336    if (
   1337      docElt &&
   1338      (docElt.hasAttribute("reftest-no-sync-layers") || shouldNotFlush(docElt))
   1339    ) {
   1340      LogInfo("Test file chose to skip SynchronizeForSnapshot");
   1341      return Promise.resolve(undefined);
   1342    }
   1343  }
   1344 
   1345  let browsingContext = content.docShell.browsingContext;
   1346  let promise = content.windowGlobalChild
   1347    .getActor("ReftestFission")
   1348    .sendQuery("UpdateLayerTree", { browsingContext });
   1349  return promise.then(
   1350    function (result) {
   1351      for (let errorString of result.errorStrings) {
   1352        LogError(errorString);
   1353      }
   1354      for (let infoString of result.infoStrings) {
   1355        LogInfo(infoString);
   1356      }
   1357 
   1358      // Setup async scroll offsets now, because any scrollable layers should
   1359      // have had their AsyncPanZoomControllers created.
   1360      return setupAsyncScrollOffsets({ allowFailure: false }).then(function () {
   1361        setupAsyncZoom({ allowFailure: false });
   1362      });
   1363    },
   1364    function (reason) {
   1365      // We expect actors to go away causing sendQuery's to fail, so
   1366      // just note it.
   1367      LogInfo("UpdateLayerTree sendQuery to parent rejected: " + reason);
   1368 
   1369      // Setup async scroll offsets now, because any scrollable layers should
   1370      // have had their AsyncPanZoomControllers created.
   1371      return setupAsyncScrollOffsets({ allowFailure: false }).then(function () {
   1372        setupAsyncZoom({ allowFailure: false });
   1373      });
   1374    }
   1375  );
   1376 }
   1377 
   1378 function RegisterMessageListeners() {
   1379  addMessageListener("reftest:Clear", function () {
   1380    RecvClear();
   1381  });
   1382  addMessageListener("reftest:LoadScriptTest", function (m) {
   1383    RecvLoadScriptTest(m.json.uri, m.json.timeout);
   1384  });
   1385  addMessageListener("reftest:LoadPrintTest", function (m) {
   1386    RecvLoadPrintTest(m.json.uri, m.json.timeout);
   1387  });
   1388  addMessageListener("reftest:LoadTest", function (m) {
   1389    RecvLoadTest(m.json.type, m.json.uri, m.json.uriTargetType, m.json.timeout);
   1390  });
   1391  addMessageListener("reftest:ResetRenderingState", function () {
   1392    RecvResetRenderingState();
   1393  });
   1394  addMessageListener("reftest:PrintDone", function (m) {
   1395    RecvPrintDone(m.json.status, m.json.fileName);
   1396  });
   1397  addMessageListener("reftest:UpdateCanvasWithSnapshotDone", function (m) {
   1398    RecvUpdateCanvasWithSnapshotDone(m.json.painted);
   1399  });
   1400 }
   1401 
   1402 function RecvClear() {
   1403  gClearingForAssertionCheck = true;
   1404  LoadURI(BLANK_URL_FOR_CLEARING);
   1405 }
   1406 
   1407 function RecvLoadTest(type, uri, uriTargetType, timeout) {
   1408  StartTestURI(type, uri, uriTargetType, timeout);
   1409 }
   1410 
   1411 function RecvLoadScriptTest(uri, timeout) {
   1412  StartTestURI(TYPE_SCRIPT, uri, URL_TARGET_TYPE_TEST, timeout);
   1413 }
   1414 
   1415 function RecvLoadPrintTest(uri, timeout) {
   1416  StartTestURI(TYPE_PRINT, uri, URL_TARGET_TYPE_TEST, timeout);
   1417 }
   1418 
   1419 function RecvResetRenderingState() {
   1420  resetZoomAndTextZoom();
   1421  resetDisplayportAndViewport();
   1422 }
   1423 
   1424 function RecvPrintDone(status, fileName) {
   1425  const currentTestRunTime = Date.now() - gCurrentTestStartTime;
   1426  SendPrintResult(currentTestRunTime, status, fileName);
   1427  FinishTestItem();
   1428 }
   1429 
   1430 function RecvUpdateCanvasWithSnapshotDone(painted) {
   1431  gUpdateCanvasPromiseResolver(painted);
   1432 }
   1433 
   1434 function SendAssertionCount(numAssertions) {
   1435  sendAsyncMessage("reftest:AssertionCount", { count: numAssertions });
   1436 }
   1437 
   1438 function SendContentReady() {
   1439  let gfxInfo =
   1440    NS_GFXINFO_CONTRACTID in Cc &&
   1441    Cc[NS_GFXINFO_CONTRACTID].getService(Ci.nsIGfxInfo);
   1442 
   1443  let info = {};
   1444 
   1445  try {
   1446    info.DWriteEnabled = gfxInfo.DWriteEnabled;
   1447    info.EmbeddedInFirefoxReality = gfxInfo.EmbeddedInFirefoxReality;
   1448  } catch (e) {
   1449    info.DWriteEnabled = false;
   1450    info.EmbeddedInFirefoxReality = false;
   1451  }
   1452 
   1453  info.AzureCanvasBackend = gfxInfo.AzureCanvasBackend;
   1454  info.AzureContentBackend = gfxInfo.AzureContentBackend;
   1455 
   1456  return sendSyncMessage("reftest:ContentReady", { gfx: info })[0];
   1457 }
   1458 
   1459 function SendException(what) {
   1460  sendAsyncMessage("reftest:Exception", { what });
   1461 }
   1462 
   1463 function SendFailedLoad(why) {
   1464  sendAsyncMessage("reftest:FailedLoad", { why });
   1465 }
   1466 
   1467 function SendFailedNoPaint() {
   1468  sendAsyncMessage("reftest:FailedNoPaint");
   1469 }
   1470 
   1471 function SendFailedNoDisplayList() {
   1472  sendAsyncMessage("reftest:FailedNoDisplayList");
   1473 }
   1474 
   1475 function SendFailedDisplayList() {
   1476  sendAsyncMessage("reftest:FailedDisplayList");
   1477 }
   1478 
   1479 function SendFailedOpaqueLayer(why) {
   1480  sendAsyncMessage("reftest:FailedOpaqueLayer", { why });
   1481 }
   1482 
   1483 function SendFailedAssignedLayer(why) {
   1484  sendAsyncMessage("reftest:FailedAssignedLayer", { why });
   1485 }
   1486 
   1487 // Returns a promise that resolves to a bool that indicates if a snapshot was taken.
   1488 async function SendInitCanvasWithSnapshot(forURL) {
   1489  if (forURL != gCurrentURL) {
   1490    LogInfo("SendInitCanvasWithSnapshot called for previous document");
   1491    // Lie and say we painted because it doesn't matter, this is a test we
   1492    // are already done with that is clearing out. Then AfterOnLoadScripts
   1493    // should finish quicker if that is who is calling us.
   1494    return Promise.resolve(true);
   1495  }
   1496 
   1497  // If we're in the same process as the top-level XUL window, then
   1498  // drawing that window will also update our layers, so no
   1499  // synchronization is needed.
   1500  //
   1501  // NB: this is a test-harness optimization only, it must not
   1502  // affect the validity of the tests.
   1503  if (gBrowserIsRemote) {
   1504    await SynchronizeForSnapshot(SYNC_DEFAULT);
   1505    let promise = new Promise(resolve => {
   1506      gUpdateCanvasPromiseResolver = resolve;
   1507    });
   1508    sendAsyncMessage("reftest:InitCanvasWithSnapshot");
   1509 
   1510    gHaveCanvasSnapshot = await promise;
   1511    return gHaveCanvasSnapshot;
   1512  }
   1513 
   1514  // For in-process browser, we have to make a synchronous request
   1515  // here to make the above optimization valid, so that MozWaitPaint
   1516  // events dispatched (synchronously) during painting are received
   1517  // before we check the paint-wait counter.  For out-of-process
   1518  // browser though, it doesn't wrt correctness whether this request
   1519  // is sync or async.
   1520  let promise = new Promise(resolve => {
   1521    gUpdateCanvasPromiseResolver = resolve;
   1522  });
   1523  sendAsyncMessage("reftest:InitCanvasWithSnapshot");
   1524 
   1525  gHaveCanvasSnapshot = await promise;
   1526  return Promise.resolve(gHaveCanvasSnapshot);
   1527 }
   1528 
   1529 function SendScriptResults(runtimeMs, error, results) {
   1530  sendAsyncMessage("reftest:ScriptResults", {
   1531    runtimeMs,
   1532    error,
   1533    results,
   1534  });
   1535 }
   1536 
   1537 function SendStartPrint(isPrintSelection, printRange) {
   1538  sendAsyncMessage("reftest:StartPrint", { isPrintSelection, printRange });
   1539 }
   1540 
   1541 function SendPrintResult(runtimeMs, status, fileName) {
   1542  sendAsyncMessage("reftest:PrintResult", {
   1543    runtimeMs,
   1544    status,
   1545    fileName,
   1546  });
   1547 }
   1548 
   1549 function SendExpectProcessCrash() {
   1550  sendAsyncMessage("reftest:ExpectProcessCrash");
   1551 }
   1552 
   1553 function SendTestDone(runtimeMs) {
   1554  sendAsyncMessage("reftest:TestDone", { runtimeMs });
   1555 }
   1556 
   1557 function roundTo(x, fraction) {
   1558  return Math.round(x / fraction) * fraction;
   1559 }
   1560 
   1561 function elementDescription(element) {
   1562  return (
   1563    "<" +
   1564    element.localName +
   1565    [].slice
   1566      .call(element.attributes)
   1567      .map(attr => ` ${attr.nodeName}="${attr.value}"`)
   1568      .join("") +
   1569    ">"
   1570  );
   1571 }
   1572 
   1573 async function SendUpdateCanvasForEvent(forURL, rectList, contentRootElement) {
   1574  if (forURL != gCurrentURL) {
   1575    LogInfo("SendUpdateCanvasForEvent called for previous document");
   1576    // This is a test we are already done with that is clearing out.
   1577    // Don't do anything.
   1578    return;
   1579  }
   1580 
   1581  var scale = docShell.browsingContext.fullZoom;
   1582 
   1583  var rects = [];
   1584  if (shouldSnapshotWholePage(contentRootElement)) {
   1585    // See comments in SendInitCanvasWithSnapshot() re: the split
   1586    // logic here.
   1587    if (!gBrowserIsRemote) {
   1588      sendSyncMessage("reftest:UpdateWholeCanvasForInvalidation");
   1589    } else {
   1590      await SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
   1591      let promise = new Promise(resolve => {
   1592        gUpdateCanvasPromiseResolver = resolve;
   1593      });
   1594      sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
   1595      await promise;
   1596    }
   1597    return;
   1598  }
   1599 
   1600  var message;
   1601 
   1602  if (!windowUtils().isMozAfterPaintPending) {
   1603    // Webrender doesn't have invalidation, and animations on the compositor
   1604    // don't invoke any MozAfterEvent which means we have no invalidated
   1605    // rect so we just invalidate the whole screen once we don't have
   1606    // anymore paints pending. This will force the snapshot.
   1607 
   1608    LogInfo("Sending update whole canvas for invalidation");
   1609    message = "reftest:UpdateWholeCanvasForInvalidation";
   1610  } else {
   1611    LogInfo("SendUpdateCanvasForEvent with " + rectList.length + " rects");
   1612    for (var i = 0; i < rectList.length; ++i) {
   1613      var r = rectList[i];
   1614      // Set left/top/right/bottom to "device pixel" boundaries
   1615      var left = Math.floor(roundTo(r.left * scale, 0.001));
   1616      var top = Math.floor(roundTo(r.top * scale, 0.001));
   1617      var right = Math.ceil(roundTo(r.right * scale, 0.001));
   1618      var bottom = Math.ceil(roundTo(r.bottom * scale, 0.001));
   1619      LogInfo("Rect: " + left + " " + top + " " + right + " " + bottom);
   1620 
   1621      rects.push({ left, top, right, bottom });
   1622    }
   1623 
   1624    message = "reftest:UpdateCanvasForInvalidation";
   1625  }
   1626 
   1627  // See comments in SendInitCanvasWithSnapshot() re: the split
   1628  // logic here.
   1629  if (!gBrowserIsRemote) {
   1630    sendSyncMessage(message, { rects });
   1631  } else {
   1632    await SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
   1633    let promise = new Promise(resolve => {
   1634      gUpdateCanvasPromiseResolver = resolve;
   1635    });
   1636    sendAsyncMessage(message, { rects });
   1637    await promise;
   1638  }
   1639 }
   1640 
   1641 if (content.document.readyState == "complete") {
   1642  // load event has already fired for content, get started
   1643  OnInitialLoad();
   1644 } else {
   1645  addEventListener("load", OnInitialLoad, true);
   1646 }