/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-env mozilla/frame-script */

const XHTML_NS = "http://www.w3.org/1999/xhtml";

const DEBUG_CONTRACTID = "@mozilla.org/xpcom/debug;1";
const PRINTSETTINGS_CONTRACTID = "@mozilla.org/gfx/printsettings-service;1";
const NS_OBSERVER_SERVICE_CONTRACTID = "@mozilla.org/observer-service;1";
const NS_GFXINFO_CONTRACTID = "@mozilla.org/gfx/info;1";
const IO_SERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";

// "<!--CLEAR-->"
const BLANK_URL_FOR_CLEARING =
  "data:text/html;charset=UTF-8,%3C%21%2D%2DCLEAR%2D%2D%3E";

const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);
const { onSpellCheck } = ChromeUtils.importESModule(
  "resource://reftest/AsyncSpellCheckTestHelper.sys.mjs"
);

// This will load chrome Custom Elements inside chrome documents:
ChromeUtils.importESModule(
  "resource://gre/modules/CustomElementsListener.sys.mjs"
);

var gBrowserIsRemote;
var gHaveCanvasSnapshot = false;
var gCurrentURL;
var gCurrentURLRecordResults;
var gCurrentURLTargetType;
var gCurrentTestType;
var gTimeoutHook = null;
var gFailureTimeout = null;
var gFailureReason;
var gAssertionCount = 0;
var gUpdateCanvasPromiseResolver = null;

var gDebug;
var gVerbose = false;

var gCurrentTestStartTime;
var gClearingForAssertionCheck = false;

const TYPE_LOAD = "load"; // test without a reference (just test that it does
// not assert, crash, hang, or leak)
const TYPE_SCRIPT = "script"; // test contains individual test results
const TYPE_PRINT = "print"; // test and reference will be printed to PDF's and
// compared structurally

// keep this in sync with globals.sys.mjs
const URL_TARGET_TYPE_TEST = 0; // first url
const URL_TARGET_TYPE_REFERENCE = 1; // second url, if any

function webNavigation() {
  return docShell.QueryInterface(Ci.nsIWebNavigation);
}

function webProgress() {
  return docShell
    .QueryInterface(Ci.nsIInterfaceRequestor)
    .getInterface(Ci.nsIWebProgress);
}

function windowUtilsForWindow(w) {
  return w.windowUtils;
}

function windowUtils() {
  return windowUtilsForWindow(content);
}

function IDForEventTarget(event) {
  try {
    return "'" + event.target.getAttribute("id") + "'";
  } catch (ex) {
    return "<unknown>";
  }
}

var progressListener = {
  onStateChange(webprogress, request, flags) {
    let uri;
    try {
      request.QueryInterface(Ci.nsIChannel);
      uri = request.originalURI.spec;
    } catch (ex) {
      return;
    }
    const WPL = Ci.nsIWebProgressListener;
    const endFlags =
      WPL.STATE_STOP | WPL.STATE_IS_WINDOW | WPL.STATE_IS_NETWORK;
    if ((flags & endFlags) == endFlags) {
      OnDocumentLoad(uri);
    }
  },
  QueryInterface: ChromeUtils.generateQI([
    "nsIWebProgressListener",
    "nsISupportsWeakReference",
  ]),
};

function OnInitialLoad() {
  removeEventListener("load", OnInitialLoad, true);

  gDebug = Cc[DEBUG_CONTRACTID].getService(Ci.nsIDebug2);
  if (gDebug.isDebugBuild) {
    gAssertionCount = gDebug.assertionCount;
  }
  gVerbose = !!Services.env.get("MOZ_REFTEST_VERBOSE");

  RegisterMessageListeners();

  var initInfo = SendContentReady();
  gBrowserIsRemote = initInfo.remote;

  webProgress().addProgressListener(
    progressListener,
    Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
  );

  LogInfo("Using browser remote=" + gBrowserIsRemote + "\n");
}

function SetFailureTimeout(cb, timeout, uri) {
  var targetTime = Date.now() + timeout;

  var wrapper = function () {
    // Timeouts can fire prematurely in some cases (e.g. in chaos mode). If this
    // happens, set another timeout for the remaining time.
    let remainingMs = targetTime - Date.now();
    if (remainingMs > 0) {
      SetFailureTimeout(cb, remainingMs);
    } else {
      cb();
    }
  };

  // Once OnDocumentLoad is called to handle the 'load' event it will update
  // this error message to reflect what stage of the processing it has reached
  // as it advances to each stage in turn.
  gFailureReason =
    "timed out after " + timeout + " ms waiting for 'load' event for " + uri;
  gFailureTimeout = setTimeout(wrapper, timeout);
}

function StartTestURI(type, uri, uriTargetType, timeout) {
  // The GC is only able to clean up compartments after the CC runs. Since
  // the JS ref tests disable the normal browser chrome and do not otherwise
  // create substatial DOM garbage, the CC tends not to run enough normally.
  windowUtils().runNextCollectorTimer();

  gCurrentTestType = type;
  gCurrentURL = uri;
  gCurrentURLTargetType = uriTargetType;
  gCurrentURLRecordResults = 0;

  gCurrentTestStartTime = Date.now();
  if (gFailureTimeout != null) {
    SendException("program error managing timeouts\n");
  }
  SetFailureTimeout(LoadFailed, timeout, uri);

  LoadURI(gCurrentURL);
}

function setupTextZoom(contentRootElement) {
  if (
    !contentRootElement ||
    !contentRootElement.hasAttribute("reftest-text-zoom")
  ) {
    return;
  }
  docShell.browsingContext.textZoom =
    contentRootElement.getAttribute("reftest-text-zoom");
}

function setupFullZoom(contentRootElement) {
  if (!contentRootElement || !contentRootElement.hasAttribute("reftest-zoom")) {
    return;
  }
  docShell.browsingContext.fullZoom =
    contentRootElement.getAttribute("reftest-zoom");
}

function resetZoomAndTextZoom() {
  docShell.browsingContext.fullZoom = 1.0;
  docShell.browsingContext.textZoom = 1.0;
}

function doPrintMode(contentRootElement) {
  // use getAttribute because className works differently in HTML and SVG
  if (contentRootElement && contentRootElement.hasAttribute("class")) {
    var classList = contentRootElement.getAttribute("class").split(/\s+/);
    if (classList.includes("reftest-print")) {
      SendException("reftest-print is obsolete, use reftest-paged instead");
      return false;
    }
    return classList.includes("reftest-paged");
  }
  return false;
}

function setupPrintMode(contentRootElement) {
  var PSSVC = Cc[PRINTSETTINGS_CONTRACTID].getService(
    Ci.nsIPrintSettingsService
  );
  var ps = PSSVC.createNewPrintSettings();
  ps.paperWidth = 5;
  ps.paperHeight = 3;

  // Override any os-specific unwriteable margins
  ps.unwriteableMarginTop = 0;
  ps.unwriteableMarginLeft = 0;
  ps.unwriteableMarginBottom = 0;
  ps.unwriteableMarginRight = 0;

  ps.headerStrLeft = "";
  ps.headerStrCenter = "";
  ps.headerStrRight = "";
  ps.footerStrLeft = "";
  ps.footerStrCenter = "";
  ps.footerStrRight = "";

  const printBackgrounds = (() => {
    const attr = contentRootElement.getAttribute("reftest-paged-backgrounds");
    return !attr || attr != "false";
  })();
  ps.printBGColors = printBackgrounds;
  ps.printBGImages = printBackgrounds;

  docShell.docViewer.setPageModeForTesting(/* aPageMode */ true, ps);
}

// Message the parent process to ask it to print the current page to a PDF file.
function printToPdf() {
  let currentDoc = content.document;
  let isPrintSelection = false;
  let printRange = "";

  if (currentDoc) {
    let contentRootElement = currentDoc.documentElement;
    printRange = contentRootElement.getAttribute("reftest-print-range") || "";
  }

  if (printRange) {
    if (printRange === "selection") {
      isPrintSelection = true;
    } else if (
      !printRange.split(",").every(range => /^[1-9]\d*-[1-9]\d*$/.test(range))
    ) {
      SendException("invalid value for reftest-print-range");
      return;
    }
  }

  SendStartPrint(isPrintSelection, printRange);
}

function attrOrDefault(element, attr, def) {
  return element.hasAttribute(attr) ? Number(element.getAttribute(attr)) : def;
}

function setupViewport(contentRootElement) {
  if (!contentRootElement) {
    return;
  }

  var sw = attrOrDefault(contentRootElement, "reftest-scrollport-w", 0);
  var sh = attrOrDefault(contentRootElement, "reftest-scrollport-h", 0);
  if (sw !== 0 || sh !== 0) {
    LogInfo("Setting viewport to <w=" + sw + ", h=" + sh + ">");
    windowUtils().setVisualViewportSize(sw, sh);
  }

  var res = attrOrDefault(contentRootElement, "reftest-resolution", 1);
  if (res !== 1) {
    LogInfo("Setting resolution to " + res);
    windowUtils().setResolutionAndScaleTo(res);
  }

  // XXX support viewconfig when needed
}

function setupDisplayport() {
  let promise = content.windowGlobalChild
    .getActor("ReftestFission")
    .SetupDisplayportRoot();
  return promise.then(
    function (result) {
      for (let errorString of result.errorStrings) {
        LogError(errorString);
      }
      for (let infoString of result.infoStrings) {
        LogInfo(infoString);
      }
    },
    function (reason) {
      LogError("SetupDisplayportRoot returned promise rejected: " + reason);
    }
  );
}

// Returns whether any offsets were updated
function setupAsyncScrollOffsets(options) {
  let currentDoc = content.document;
  let contentRootElement = currentDoc ? currentDoc.documentElement : null;

  if (
    !contentRootElement ||
    !contentRootElement.hasAttribute("reftest-async-scroll")
  ) {
    return Promise.resolve(false);
  }

  let allowFailure = options.allowFailure;
  let promise = content.windowGlobalChild
    .getActor("ReftestFission")
    .sendQuery("SetupAsyncScrollOffsets", { allowFailure });
  return promise.then(
    function (result) {
      for (let errorString of result.errorStrings) {
        LogError(errorString);
      }
      for (let infoString of result.infoStrings) {
        LogInfo(infoString);
      }
      return result.updatedAny;
    },
    function (reason) {
      LogError(
        "SetupAsyncScrollOffsets SendQuery to parent promise rejected: " +
          reason
      );
      return false;
    }
  );
}

function setupAsyncZoom(options) {
  var currentDoc = content.document;
  var contentRootElement = currentDoc ? currentDoc.documentElement : null;

  if (
    !contentRootElement ||
    !contentRootElement.hasAttribute("reftest-async-zoom")
  ) {
    return false;
  }

  var zoom = attrOrDefault(contentRootElement, "reftest-async-zoom", 1);
  if (zoom != 1) {
    try {
      windowUtils().setAsyncZoom(contentRootElement, zoom);
      return true;
    } catch (e) {
      if (!options.allowFailure) {
        throw e;
      }
    }
  }
  return false;
}

function resetDisplayportAndViewport() {
  // XXX currently the displayport configuration lives on the
  // presshell and so is "reset" on nav when we get a new presshell.
}

function shouldWaitForPendingPaints() {
  // if gHaveCanvasSnapshot is false, we're not taking snapshots so
  // there is no need to wait for pending paints to be flushed.
  return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending;
}

function shouldWaitForReftestWaitRemoval(contentRootElement) {
  // use getAttribute because className works differently in HTML and SVG
  return (
    contentRootElement &&
    contentRootElement.hasAttribute("class") &&
    contentRootElement
      .getAttribute("class")
      .split(/\s+/)
      .includes("reftest-wait")
  );
}

function shouldSnapshotWholePage(contentRootElement) {
  // use getAttribute because className works differently in HTML and SVG
  return (
    contentRootElement &&
    contentRootElement.hasAttribute("class") &&
    contentRootElement
      .getAttribute("class")
      .split(/\s+/)
      .includes("reftest-snapshot-all")
  );
}

function shouldNotFlush(contentRootElement) {
  // use getAttribute because className works differently in HTML and SVG
  return (
    contentRootElement &&
    contentRootElement.hasAttribute("class") &&
    contentRootElement
      .getAttribute("class")
      .split(/\s+/)
      .includes("reftest-no-flush")
  );
}

function getNoPaintElements(contentRootElement) {
  return contentRootElement.getElementsByClassName("reftest-no-paint");
}
function getNoDisplayListElements(contentRootElement) {
  return contentRootElement.getElementsByClassName("reftest-no-display-list");
}
function getDisplayListElements(contentRootElement) {
  return contentRootElement.getElementsByClassName("reftest-display-list");
}

function getOpaqueLayerElements(contentRootElement) {
  return contentRootElement.getElementsByClassName("reftest-opaque-layer");
}

function getAssignedLayerMap(contentRootElement) {
  var layerNameToElementsMap = {};
  var elements = contentRootElement.querySelectorAll(
    "[reftest-assigned-layer]"
  );
  for (var i = 0; i < elements.length; ++i) {
    var element = elements[i];
    var layerName = element.getAttribute("reftest-assigned-layer");
    if (!(layerName in layerNameToElementsMap)) {
      layerNameToElementsMap[layerName] = [];
    }
    layerNameToElementsMap[layerName].push(element);
  }
  return layerNameToElementsMap;
}

const FlushMode = {
  ALL: 0,
  IGNORE_THROTTLED_ANIMATIONS: 1,
};

// Initial state. When the document has loaded and all MozAfterPaint events and
// all explicit paint waits are flushed, we can fire the MozReftestInvalidate
// event and move to the next state.
const STATE_WAITING_TO_FIRE_INVALIDATE_EVENT = 0;
// When reftest-wait has been removed from the root element, we can move to the
// next state.
const STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL = 1;
// When spell checking is done on all spell-checked elements, we can move to the
// next state.
const STATE_WAITING_FOR_SPELL_CHECKS = 2;
// When any pending compositor-side repaint requests have been flushed, we can
// move to the next state.
const STATE_WAITING_FOR_APZ_FLUSH = 3;
// When all MozAfterPaint events and all explicit paint waits are flushed, we're
// done and can move to the COMPLETED state.
const STATE_WAITING_TO_FINISH = 4;
const STATE_COMPLETED = 5;

async function FlushRendering(aFlushMode) {
  let browsingContext = content.docShell.browsingContext;
  let ignoreThrottledAnimations =
    aFlushMode === FlushMode.IGNORE_THROTTLED_ANIMATIONS;
  // Ensure the refresh driver ticks at least once, this ensures some
  // preference changes take effect.
  let needsAnimationFrame = IsSnapshottableTestType();
  try {
    let result = await content.windowGlobalChild
      .getActor("ReftestFission")
      .sendQuery("FlushRendering", {
        browsingContext,
        ignoreThrottledAnimations,
        needsAnimationFrame,
      });
    for (let errorString of result.errorStrings) {
      LogError(errorString);
    }
    for (let warningString of result.warningStrings) {
      LogWarning(warningString);
    }
    for (let infoString of result.infoStrings) {
      LogInfo(infoString);
    }
  } catch (reason) {
    // We expect actors to go away causing sendQuery's to fail, so
    // just note it.
    LogInfo("FlushRendering sendQuery to parent rejected: " + reason);
  }
}

function WaitForTestEnd(
  contentRootElement,
  inPrintMode,
  spellCheckedElements,
  forURL
) {
  // WaitForTestEnd works via the MakeProgress function below. It is responsible for
  // moving through the states listed above and calling FlushRendering. We also listen
  // for a number of events, the most important of which is the AfterPaintListener,
  // which is responsible for updating the canvas after paints. In a fission world
  // FlushRendering and updating the canvas must necessarily be async operations.
  // During these async operations we want to wait for them to finish and we don't
  // want to try to do anything else (what would we even want to do while only some of
  // the processes involved have flushed layout or updated their layer trees?). So
  // we call OperationInProgress whenever we are about to go back to the event loop
  // during one of these calls, and OperationCompleted when it finishes. This prevents
  // anything else from running while we wait and getting us into a confused state. We
  // then record anything that happens while we are waiting to make sure that the
  // right actions are triggered. The possible actions are basically calling
  // MakeProgress from a setTimeout, and updating the canvas for an after paint event.
  // The after paint listener just stashes the rects and we update them after a
  // completed MakeProgress call. This is handled by
  // HandlePendingTasksAfterMakeProgress, which also waits for any pending after paint
  // events. The general sequence of events is:
  //   - MakeProgress
  //   - HandlePendingTasksAfterMakeProgress
  //     - wait for after paint event if one is pending
  //     - update canvas for after paint events we have received
  //   - MakeProgress
  //   etc

  function CheckForLivenessOfContentRootElement() {
    if (contentRootElement && Cu.isDeadWrapper(contentRootElement)) {
      contentRootElement = null;
    }
  }

  var setTimeoutCallMakeProgressWhenComplete = false;

  var operationInProgress = false;
  function OperationInProgress() {
    if (operationInProgress) {
      LogWarning("Nesting atomic operations?");
    }
    operationInProgress = true;
  }
  function OperationCompleted() {
    if (!operationInProgress) {
      LogWarning("Mismatched OperationInProgress/OperationCompleted calls?");
    }
    operationInProgress = false;
    if (setTimeoutCallMakeProgressWhenComplete) {
      setTimeoutCallMakeProgressWhenComplete = false;
      setTimeout(CallMakeProgress, 0);
    }
  }
  function AssertNoOperationInProgress() {
    if (operationInProgress) {
      LogWarning("AssertNoOperationInProgress but operationInProgress");
    }
  }

  var updateCanvasPending = false;
  var updateCanvasRects = [];

  var currentDoc = content.document;
  var state = STATE_WAITING_TO_FIRE_INVALIDATE_EVENT;

  var setTimeoutMakeProgressPending = false;

  function CallSetTimeoutMakeProgress() {
    if (setTimeoutMakeProgressPending) {
      return;
    }
    setTimeoutMakeProgressPending = true;
    setTimeout(CallMakeProgress, 0);
  }

  // This should only ever be called from a timeout.
  function CallMakeProgress() {
    if (operationInProgress) {
      setTimeoutCallMakeProgressWhenComplete = true;
      return;
    }
    setTimeoutMakeProgressPending = false;
    MakeProgress();
  }

  var waitingForAnAfterPaint = false;

  // Updates the canvas if there are pending updates for it. Checks if we
  // need to call MakeProgress.
  function HandlePendingTasksAfterMakeProgress() {
    AssertNoOperationInProgress();

    if (
      (state == STATE_WAITING_TO_FIRE_INVALIDATE_EVENT ||
        state == STATE_WAITING_TO_FINISH) &&
      shouldWaitForPendingPaints()
    ) {
      LogInfo(
        "HandlePendingTasksAfterMakeProgress waiting for a MozAfterPaint"
      );
      // We are in a state where we wait for MozAfterPaint to clear and a
      // MozAfterPaint event is pending, give it a chance to fire, but don't
      // let anything else run.
      waitingForAnAfterPaint = true;
      OperationInProgress();
      return;
    }

    if (updateCanvasPending) {
      LogInfo("HandlePendingTasksAfterMakeProgress updating canvas");
      updateCanvasPending = false;
      let rects = updateCanvasRects;
      updateCanvasRects = [];
      OperationInProgress();
      CheckForLivenessOfContentRootElement();
      let promise = SendUpdateCanvasForEvent(forURL, rects, contentRootElement);
      promise.then(function () {
        OperationCompleted();
        // After paint events are fired immediately after a paint (one
        // of the things that can call us). Don't confuse ourselves by
        // firing synchronously if we triggered the paint ourselves.
        CallSetTimeoutMakeProgress();
      });
    }
  }

  // true if rectA contains rectB
  function Contains(rectA, rectB) {
    return (
      rectA.left <= rectB.left &&
      rectB.right <= rectA.right &&
      rectA.top <= rectB.top &&
      rectB.bottom <= rectA.bottom
    );
  }
  // true if some rect in rectList contains rect
  function ContainedIn(rectList, rect) {
    for (let i = 0; i < rectList.length; ++i) {
      if (Contains(rectList[i], rect)) {
        return true;
      }
    }
    return false;
  }

  function AfterPaintListener(event) {
    LogInfo("AfterPaintListener in " + event.target.document.location.href);
    if (event.target.document != currentDoc) {
      // ignore paint events for subframes or old documents in the window.
      // Invalidation in subframes will cause invalidation in the toplevel document anyway.
      return;
    }

    updateCanvasPending = true;
    for (let r of event.clientRects) {
      if (ContainedIn(updateCanvasRects, r)) {
        continue;
      }

      // Copy the rect; it's content and we are chrome, which means if the
      // document goes away (and it can in some crashtests) our reference
      // to it will be turned into a dead wrapper that we can't acccess.
      updateCanvasRects.push({
        left: r.left,
        top: r.top,
        right: r.right,
        bottom: r.bottom,
      });
    }

    if (waitingForAnAfterPaint) {
      waitingForAnAfterPaint = false;
      OperationCompleted();
    }

    if (!operationInProgress) {
      HandlePendingTasksAfterMakeProgress();
    }
    // Otherwise we know that eventually after the operation finishes we
    // will get a MakeProgress and/or HandlePendingTasksAfterMakeProgress
    // call, so we don't need to do anything.
  }

  function FromChildAfterPaintListener(event) {
    LogInfo(
      "FromChildAfterPaintListener from " + event.detail.originalTargetUri
    );

    updateCanvasPending = true;
    for (let r of event.detail.rects) {
      if (ContainedIn(updateCanvasRects, r)) {
        continue;
      }

      // Copy the rect; it's content and we are chrome, which means if the
      // document goes away (and it can in some crashtests) our reference
      // to it will be turned into a dead wrapper that we can't acccess.
      updateCanvasRects.push({
        left: r.left,
        top: r.top,
        right: r.right,
        bottom: r.bottom,
      });
    }

    if (!operationInProgress) {
      HandlePendingTasksAfterMakeProgress();
    }
    // Otherwise we know that eventually after the operation finishes we
    // will get a MakeProgress and/or HandlePendingTasksAfterMakeProgress
    // call, so we don't need to do anything.
  }

  let attrModifiedObserver;
  function AttrModifiedListener() {
    LogInfo("AttrModifiedListener fired");
    // Wait for the next return-to-event-loop before continuing --- for
    // example, the attribute may have been modified in an subdocument's
    // load event handler, in which case we need load event processing
    // to complete and unsuppress painting before we check isMozAfterPaintPending.
    CallSetTimeoutMakeProgress();
  }

  function RemoveListeners() {
    // OK, we can end the test now.
    removeEventListener("MozAfterPaint", AfterPaintListener, false);
    removeEventListener(
      "Reftest:MozAfterPaintFromChild",
      FromChildAfterPaintListener,
      false
    );
    CheckForLivenessOfContentRootElement();
    if (attrModifiedObserver) {
      if (!Cu.isDeadWrapper(attrModifiedObserver)) {
        attrModifiedObserver.disconnect();
      }
      attrModifiedObserver = null;
    }
    gTimeoutHook = null;
    // Make sure we're in the COMPLETED state just in case
    // (this may be called via the test-timeout hook)
    state = STATE_COMPLETED;
  }

  // Everything that could cause shouldWaitForXXX() to
  // change from returning true to returning false is monitored via some kind
  // of event listener which eventually calls this function.
  function MakeProgress() {
    if (state >= STATE_COMPLETED) {
      LogInfo("MakeProgress: STATE_COMPLETED");
      return;
    }

    LogInfo("MakeProgress");

    // We don't need to flush styles any more when we are in the state
    // after reftest-wait has removed.
    OperationInProgress();
    let promise = Promise.resolve(undefined);
    if (state != STATE_WAITING_TO_FINISH) {
      // If we are waiting for the MozReftestInvalidate event we don't want
      // to flush throttled animations. Flushing throttled animations can
      // continue to cause new MozAfterPaint events even when all the
      // rendering we're concerned about should have ceased. Since
      // MozReftestInvalidate won't be sent until we finish waiting for all
      // MozAfterPaint events, we should avoid flushing throttled animations
      // here or else we'll never leave this state.
      let flushMode =
        state === STATE_WAITING_TO_FIRE_INVALIDATE_EVENT
          ? FlushMode.IGNORE_THROTTLED_ANIMATIONS
          : FlushMode.ALL;
      promise = FlushRendering(flushMode);
    }
    promise.then(function () {
      OperationCompleted();
      MakeProgress2();
      // If there is an operation in progress then we know there will be
      // a MakeProgress call is will happen after it finishes.
      if (!operationInProgress) {
        HandlePendingTasksAfterMakeProgress();
      }
    });
  }

  // eslint-disable-next-line complexity
  function MakeProgress2() {
    switch (state) {
      case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: {
        LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT");
        if (shouldWaitForPendingPaints() || updateCanvasPending) {
          gFailureReason =
            "timed out waiting for pending paint count to reach zero";
          if (shouldWaitForPendingPaints()) {
            gFailureReason += " (waiting for MozAfterPaint)";
            LogInfo("MakeProgress: waiting for MozAfterPaint");
          }
          if (updateCanvasPending) {
            gFailureReason += " (waiting for updateCanvasPending)";
            LogInfo("MakeProgress: waiting for updateCanvasPending");
          }
          return;
        }

        state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL;
        CheckForLivenessOfContentRootElement();
        var hasReftestWait =
          shouldWaitForReftestWaitRemoval(contentRootElement);
        // Notify the test document that now is a good time to test some invalidation
        LogInfo("MakeProgress: dispatching MozReftestInvalidate");
        if (contentRootElement) {
          let elements = getNoPaintElements(contentRootElement);
          for (let i = 0; i < elements.length; ++i) {
            windowUtils().checkAndClearPaintedState(elements[i]);
          }
          elements = getNoDisplayListElements(contentRootElement);
          for (let i = 0; i < elements.length; ++i) {
            windowUtils().checkAndClearDisplayListState(elements[i]);
          }
          elements = getDisplayListElements(contentRootElement);
          for (let i = 0; i < elements.length; ++i) {
            windowUtils().checkAndClearDisplayListState(elements[i]);
          }
          var notification = content.document.createEvent("Events");
          notification.initEvent("MozReftestInvalidate", true, false);
          contentRootElement.dispatchEvent(notification);
        } else {
          LogInfo(
            "MakeProgress: couldn't send MozReftestInvalidate event because content root element does not exist"
          );
        }

        CheckForLivenessOfContentRootElement();
        if (!inPrintMode && doPrintMode(contentRootElement)) {
          LogInfo("MakeProgress: setting up print mode");
          setupPrintMode(contentRootElement);
        }

        CheckForLivenessOfContentRootElement();
        if (
          hasReftestWait &&
          !shouldWaitForReftestWaitRemoval(contentRootElement)
        ) {
          // MozReftestInvalidate handler removed reftest-wait.
          // We expect something to have been invalidated...
          OperationInProgress();
          let promise = FlushRendering(FlushMode.ALL);
          promise.then(function () {
            OperationCompleted();
            if (!updateCanvasPending && !shouldWaitForPendingPaints()) {
              LogWarning("MozInvalidateEvent didn't invalidate");
            }
            MakeProgress();
          });
          return;
        }
        // Try next state
        MakeProgress();
        return;
      }

      case STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL:
        LogInfo("MakeProgress: STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL");
        CheckForLivenessOfContentRootElement();
        if (shouldWaitForReftestWaitRemoval(contentRootElement)) {
          gFailureReason = "timed out waiting for reftest-wait to be removed";
          LogInfo("MakeProgress: waiting for reftest-wait to be removed");
          return;
        }

        if (shouldNotFlush(contentRootElement)) {
          // If reftest-no-flush is specified, we need to set
          // updateCanvasPending explicitly to take the latest snapshot
          // since animation changes on the compositor thread don't invoke
          // any MozAfterPaint events at all.
          // NOTE: We don't add any rects to updateCanvasRects here since
          // SendUpdateCanvasForEvent() will handle this case properly
          // without any rects.
          updateCanvasPending = true;
        }
        // Try next state
        state = STATE_WAITING_FOR_SPELL_CHECKS;
        MakeProgress();
        return;

      case STATE_WAITING_FOR_SPELL_CHECKS:
        LogInfo("MakeProgress: STATE_WAITING_FOR_SPELL_CHECKS");
        if (numPendingSpellChecks) {
          gFailureReason = "timed out waiting for spell checks to end";
          LogInfo("MakeProgress: waiting for spell checks to end");
          return;
        }

        state = STATE_WAITING_FOR_APZ_FLUSH;
        LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
        gFailureReason = "timed out waiting for APZ flush to complete";

        var flushWaiter = function (aSubject, aTopic) {
          if (aTopic) {
            LogInfo("MakeProgress: apz-repaints-flushed fired");
          }
          Services.obs.removeObserver(flushWaiter, "apz-repaints-flushed");
          state = STATE_WAITING_TO_FINISH;
          if (operationInProgress) {
            CallSetTimeoutMakeProgress();
          } else {
            MakeProgress();
          }
        };
        Services.obs.addObserver(flushWaiter, "apz-repaints-flushed");

        var willSnapshot = IsSnapshottableTestType();
        CheckForLivenessOfContentRootElement();
        var noFlush = !shouldNotFlush(contentRootElement);
        if (noFlush && willSnapshot && windowUtils().flushApzRepaints()) {
          LogInfo("MakeProgress: done requesting APZ flush");
        } else {
          LogInfo("MakeProgress: APZ flush not required");
          flushWaiter(null, null, null);
        }
        return;

      case STATE_WAITING_FOR_APZ_FLUSH:
        LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
        // Nothing to do here; once we get the apz-repaints-flushed event
        // we will go to STATE_WAITING_TO_FINISH
        return;

      case STATE_WAITING_TO_FINISH:
        LogInfo("MakeProgress: STATE_WAITING_TO_FINISH");
        if (shouldWaitForPendingPaints() || updateCanvasPending) {
          gFailureReason =
            "timed out waiting for pending paint count to " +
            "reach zero (after reftest-wait removed and switch to print mode)";
          if (shouldWaitForPendingPaints()) {
            gFailureReason += " (waiting for MozAfterPaint)";
            LogInfo("MakeProgress: waiting for MozAfterPaint");
          }
          if (updateCanvasPending) {
            gFailureReason += " (waiting for updateCanvasPending)";
            LogInfo("MakeProgress: waiting for updateCanvasPending");
          }
          return;
        }
        CheckForLivenessOfContentRootElement();
        if (contentRootElement) {
          let elements = getNoPaintElements(contentRootElement);
          for (let i = 0; i < elements.length; ++i) {
            if (windowUtils().checkAndClearPaintedState(elements[i])) {
              SendFailedNoPaint();
            }
          }
          // We only support retained display lists in the content process
          // right now, so don't fail reftest-no-display-list tests when
          // we don't have e10s.
          if (gBrowserIsRemote) {
            elements = getNoDisplayListElements(contentRootElement);
            for (let i = 0; i < elements.length; ++i) {
              if (windowUtils().checkAndClearDisplayListState(elements[i])) {
                SendFailedNoDisplayList();
              }
            }
            elements = getDisplayListElements(contentRootElement);
            for (let i = 0; i < elements.length; ++i) {
              if (!windowUtils().checkAndClearDisplayListState(elements[i])) {
                SendFailedDisplayList();
              }
            }
          }
        }

        if (!IsSnapshottableTestType()) {
          // If we're not snapshotting the test, at least do a sync round-trip
          // to the compositor to ensure that all the rendering messages
          // related to this test get processed. Otherwise problems triggered
          // by this test may only manifest as failures in a later test.
          LogInfo("MakeProgress: Doing sync flush to compositor");
          gFailureReason = "timed out while waiting for sync compositor flush";
          windowUtils().syncFlushCompositor();
        }

        LogInfo("MakeProgress: Completed");
        state = STATE_COMPLETED;
        gFailureReason = "timed out while taking snapshot (bug in harness?)";
        RemoveListeners();
        CheckForLivenessOfContentRootElement();
        CheckForProcessCrashExpectation(contentRootElement);
        setTimeout(RecordResult, 0, forURL);
    }
  }

  LogInfo("WaitForTestEnd: Adding listeners");
  addEventListener("MozAfterPaint", AfterPaintListener, false);
  addEventListener(
    "Reftest:MozAfterPaintFromChild",
    FromChildAfterPaintListener,
    false
  );

  // If contentRootElement is null then shouldWaitForReftestWaitRemoval will
  // always return false so we don't need a listener anyway
  CheckForLivenessOfContentRootElement();
  if (contentRootElement?.hasAttribute("class")) {
    attrModifiedObserver =
      // ownerGlobal doesn't exist in content windows.
      // eslint-disable-next-line mozilla/use-ownerGlobal
      new contentRootElement.ownerDocument.defaultView.MutationObserver(
        AttrModifiedListener
      );
    attrModifiedObserver.observe(contentRootElement, { attributes: true });
  }
  gTimeoutHook = RemoveListeners;

  // Listen for spell checks on spell-checked elements.
  var numPendingSpellChecks = spellCheckedElements.length;
  function decNumPendingSpellChecks() {
    --numPendingSpellChecks;
    if (operationInProgress) {
      CallSetTimeoutMakeProgress();
    } else {
      MakeProgress();
    }
  }
  for (let editable of spellCheckedElements) {
    try {
      onSpellCheck(editable, decNumPendingSpellChecks);
    } catch (err) {
      // The element may not have an editor, so ignore it.
      setTimeout(decNumPendingSpellChecks, 0);
    }
  }

  // Take a full snapshot now that all our listeners are set up. This
  // ensures it's impossible for us to miss updates between taking the snapshot
  // and adding our listeners.
  OperationInProgress();
  let promise = SendInitCanvasWithSnapshot(forURL);
  promise.then(function () {
    OperationCompleted();
    MakeProgress();
  });
}

async function OnDocumentLoad(uri) {
  if (gClearingForAssertionCheck) {
    if (uri == BLANK_URL_FOR_CLEARING) {
      DoAssertionCheck();
      return;
    }

    // It's likely the previous test document reloads itself and causes the
    // attempt of loading blank page fails. In this case we should retry
    // loading the blank page.
    LogInfo("Retry loading a blank page");
    setTimeout(LoadURI, 0, BLANK_URL_FOR_CLEARING);
    return;
  }

  if (uri != gCurrentURL) {
    LogInfo("OnDocumentLoad fired for previous document");
    // Ignore load events for previous documents.
    return;
  }

  var currentDoc = content && content.document;

  // Collect all editable, spell-checked elements.  It may be the case that
  // not all the elements that match this selector will be spell checked: for
  // example, a textarea without a spellcheck attribute may have a parent with
  // spellcheck=false, or script may set spellcheck=false on an element whose
  // markup sets it to true.  But that's OK since onSpellCheck detects the
  // absence of spell checking, too.
  var querySelector =
    '*[class~="spell-checked"],' +
    'textarea:not([spellcheck="false"]),' +
    'input[spellcheck]:-moz-any([spellcheck=""],[spellcheck="true"]),' +
    '*[contenteditable]:-moz-any([contenteditable=""],[contenteditable="true"])';
  var spellCheckedElements = currentDoc
    ? currentDoc.querySelectorAll(querySelector)
    : [];

  var contentRootElement = currentDoc ? currentDoc.documentElement : null;
  currentDoc = null;
  setupFullZoom(contentRootElement);
  setupTextZoom(contentRootElement);
  setupViewport(contentRootElement);
  await setupDisplayport(contentRootElement);
  var inPrintMode = false;

  async function AfterOnLoadScripts() {
    // Regrab the root element, because the document may have changed.
    var contentRootElement = content.document
      ? content.document.documentElement
      : null;

    // Flush the document in case it got modified in a load event handler.
    await FlushRendering(FlushMode.ALL);

    // Take a snapshot now.
    let painted = await SendInitCanvasWithSnapshot(uri);

    if (contentRootElement && Cu.isDeadWrapper(contentRootElement)) {
      contentRootElement = null;
    }

    if (
      (!inPrintMode && doPrintMode(contentRootElement)) ||
      // If we didn't force a paint above, in
      // InitCurrentCanvasWithSnapshot, so we should wait for a
      // paint before we consider them done.
      !painted
    ) {
      LogInfo("AfterOnLoadScripts belatedly entering WaitForTestEnd");
      // Go into reftest-wait mode belatedly.
      WaitForTestEnd(contentRootElement, inPrintMode, [], uri);
    } else {
      CheckForProcessCrashExpectation(contentRootElement);
      RecordResult(uri);
    }
  }

  if (
    shouldWaitForReftestWaitRemoval(contentRootElement) ||
    spellCheckedElements.length
  ) {
    // Go into reftest-wait mode immediately after painting has been
    // unsuppressed, after the onload event has finished dispatching.
    gFailureReason =
      "timed out waiting for test to complete (trying to get into WaitForTestEnd)";
    LogInfo("OnDocumentLoad triggering WaitForTestEnd");
    setTimeout(function () {
      WaitForTestEnd(
        contentRootElement,
        inPrintMode,
        spellCheckedElements,
        uri
      );
    }, 0);
  } else {
    if (doPrintMode(contentRootElement)) {
      LogInfo("OnDocumentLoad setting up print mode");
      setupPrintMode(contentRootElement);
      inPrintMode = true;
    }

    // Since we can't use a bubbling-phase load listener from chrome,
    // this is a capturing phase listener.  So do setTimeout twice, the
    // first to get us after the onload has fired in the content, and
    // the second to get us after any setTimeout(foo, 0) in the content.
    gFailureReason =
      "timed out waiting for test to complete (waiting for onload scripts to complete)";
    LogInfo("OnDocumentLoad triggering AfterOnLoadScripts");
    setTimeout(function () {
      setTimeout(AfterOnLoadScripts, 0);
    }, 0);
  }
}

function CheckForProcessCrashExpectation(contentRootElement) {
  if (
    contentRootElement &&
    contentRootElement.hasAttribute("class") &&
    contentRootElement
      .getAttribute("class")
      .split(/\s+/)
      .includes("reftest-expect-process-crash")
  ) {
    SendExpectProcessCrash();
  }
}

async function RecordResult(forURL) {
  if (forURL != gCurrentURL) {
    LogInfo("RecordResult fired for previous document");
    return;
  }

  if (gCurrentURLRecordResults > 0) {
    LogInfo("RecordResult fired extra times");
    FinishTestItem();
    return;
  }
  gCurrentURLRecordResults++;

  LogInfo("RecordResult fired");

  var currentTestRunTime = Date.now() - gCurrentTestStartTime;

  clearTimeout(gFailureTimeout);
  gFailureReason = null;
  gFailureTimeout = null;
  gCurrentURL = null;
  gCurrentURLTargetType = undefined;

  if (gCurrentTestType == TYPE_PRINT) {
    printToPdf();
    return;
  }
  if (gCurrentTestType == TYPE_SCRIPT) {
    var error = "";
    var testwindow = content;

    if (testwindow.wrappedJSObject) {
      testwindow = testwindow.wrappedJSObject;
    }

    var testcases;
    if (
      !testwindow.getTestCases ||
      typeof testwindow.getTestCases != "function"
    ) {
      // Force an unexpected failure to alert the test author to fix the test.
      error = "test must provide a function getTestCases(). (SCRIPT)\n";
    } else if (!(testcases = testwindow.getTestCases())) {
      // Force an unexpected failure to alert the test author to fix the test.
      error =
        "test's getTestCases() must return an Array-like Object. (SCRIPT)\n";
    } else if (!testcases.length) {
      // This failure may be due to a JavaScript Engine bug causing
      // early termination of the test. If we do not allow silent
      // failure, the driver will report an error.
    }

    var results = [];
    if (!error) {
      // FIXME/bug 618176: temporary workaround
      for (var i = 0; i < testcases.length; ++i) {
        var test = testcases[i];
        results.push({
          passed: test.testPassed(),
          description: test.testDescription(),
        });
      }
      //results = testcases.map(function(test) {
      //        return { passed: test.testPassed(),
      //                 description: test.testDescription() };
    }

    SendScriptResults(currentTestRunTime, error, results);
    FinishTestItem();
    return;
  }

  // Setup async scroll offsets now in case SynchronizeForSnapshot is not
  // called (due to reftest-no-sync-layers being supplied, or in the single
  // process case).
  let changedAsyncScrollZoom = await setupAsyncScrollOffsets({
    allowFailure: true,
  });
  if (setupAsyncZoom({ allowFailure: true })) {
    changedAsyncScrollZoom = true;
  }
  if (changedAsyncScrollZoom && !gBrowserIsRemote) {
    sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
  }

  SendTestDone(currentTestRunTime);
  FinishTestItem();
}

function LoadFailed() {
  if (gTimeoutHook) {
    gTimeoutHook();
  }
  gFailureTimeout = null;
  SendFailedLoad(gFailureReason);
}

function FinishTestItem() {
  gHaveCanvasSnapshot = false;
}

function DoAssertionCheck() {
  gClearingForAssertionCheck = false;

  var numAsserts = 0;
  if (gDebug.isDebugBuild) {
    var newAssertionCount = gDebug.assertionCount;
    numAsserts = newAssertionCount - gAssertionCount;
    gAssertionCount = newAssertionCount;
  }
  SendAssertionCount(numAsserts);
}

function LoadURI(uri) {
  let loadURIOptions = {
    triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
  };
  webNavigation().loadURI(Services.io.newURI(uri), loadURIOptions);
}

function LogError(str) {
  if (gVerbose) {
    sendSyncMessage("reftest:Log", { type: "error", msg: str });
  } else {
    sendAsyncMessage("reftest:Log", { type: "error", msg: str });
  }
}

function LogWarning(str) {
  if (gVerbose) {
    sendSyncMessage("reftest:Log", { type: "warning", msg: str });
  } else {
    sendAsyncMessage("reftest:Log", { type: "warning", msg: str });
  }
}

function LogInfo(str) {
  if (gVerbose) {
    sendSyncMessage("reftest:Log", { type: "info", msg: str });
  } else {
    sendAsyncMessage("reftest:Log", { type: "info", msg: str });
  }
}

function IsSnapshottableTestType() {
  // Script, load-only, and PDF-print tests do not need any snapshotting.
  return !(
    gCurrentTestType == TYPE_SCRIPT ||
    gCurrentTestType == TYPE_LOAD ||
    gCurrentTestType == TYPE_PRINT
  );
}

const SYNC_DEFAULT = 0x0;
const SYNC_ALLOW_DISABLE = 0x1;
// Returns a promise that resolve when the snapshot is done.
function SynchronizeForSnapshot(flags) {
  if (!IsSnapshottableTestType()) {
    return Promise.resolve(undefined);
  }

  if (flags & SYNC_ALLOW_DISABLE) {
    var docElt = content.document.documentElement;
    if (
      docElt &&
      (docElt.hasAttribute("reftest-no-sync-layers") || shouldNotFlush(docElt))
    ) {
      LogInfo("Test file chose to skip SynchronizeForSnapshot");
      return Promise.resolve(undefined);
    }
  }

  let browsingContext = content.docShell.browsingContext;
  let promise = content.windowGlobalChild
    .getActor("ReftestFission")
    .sendQuery("UpdateLayerTree", { browsingContext });
  return promise.then(
    function (result) {
      for (let errorString of result.errorStrings) {
        LogError(errorString);
      }
      for (let infoString of result.infoStrings) {
        LogInfo(infoString);
      }

      // Setup async scroll offsets now, because any scrollable layers should
      // have had their AsyncPanZoomControllers created.
      return setupAsyncScrollOffsets({ allowFailure: false }).then(function () {
        setupAsyncZoom({ allowFailure: false });
      });
    },
    function (reason) {
      // We expect actors to go away causing sendQuery's to fail, so
      // just note it.
      LogInfo("UpdateLayerTree sendQuery to parent rejected: " + reason);

      // Setup async scroll offsets now, because any scrollable layers should
      // have had their AsyncPanZoomControllers created.
      return setupAsyncScrollOffsets({ allowFailure: false }).then(function () {
        setupAsyncZoom({ allowFailure: false });
      });
    }
  );
}

function RegisterMessageListeners() {
  addMessageListener("reftest:Clear", function () {
    RecvClear();
  });
  addMessageListener("reftest:LoadScriptTest", function (m) {
    RecvLoadScriptTest(m.json.uri, m.json.timeout);
  });
  addMessageListener("reftest:LoadPrintTest", function (m) {
    RecvLoadPrintTest(m.json.uri, m.json.timeout);
  });
  addMessageListener("reftest:LoadTest", function (m) {
    RecvLoadTest(m.json.type, m.json.uri, m.json.uriTargetType, m.json.timeout);
  });
  addMessageListener("reftest:ResetRenderingState", function () {
    RecvResetRenderingState();
  });
  addMessageListener("reftest:PrintDone", function (m) {
    RecvPrintDone(m.json.status, m.json.fileName);
  });
  addMessageListener("reftest:UpdateCanvasWithSnapshotDone", function (m) {
    RecvUpdateCanvasWithSnapshotDone(m.json.painted);
  });
}

function RecvClear() {
  gClearingForAssertionCheck = true;
  LoadURI(BLANK_URL_FOR_CLEARING);
}

function RecvLoadTest(type, uri, uriTargetType, timeout) {
  StartTestURI(type, uri, uriTargetType, timeout);
}

function RecvLoadScriptTest(uri, timeout) {
  StartTestURI(TYPE_SCRIPT, uri, URL_TARGET_TYPE_TEST, timeout);
}

function RecvLoadPrintTest(uri, timeout) {
  StartTestURI(TYPE_PRINT, uri, URL_TARGET_TYPE_TEST, timeout);
}

function RecvResetRenderingState() {
  resetZoomAndTextZoom();
  resetDisplayportAndViewport();
}

function RecvPrintDone(status, fileName) {
  const currentTestRunTime = Date.now() - gCurrentTestStartTime;
  SendPrintResult(currentTestRunTime, status, fileName);
  FinishTestItem();
}

function RecvUpdateCanvasWithSnapshotDone(painted) {
  gUpdateCanvasPromiseResolver(painted);
}

function SendAssertionCount(numAssertions) {
  sendAsyncMessage("reftest:AssertionCount", { count: numAssertions });
}

function SendContentReady() {
  let gfxInfo =
    NS_GFXINFO_CONTRACTID in Cc &&
    Cc[NS_GFXINFO_CONTRACTID].getService(Ci.nsIGfxInfo);

  let info = {};

  try {
    info.D2DEnabled = gfxInfo.D2DEnabled;
    info.DWriteEnabled = gfxInfo.DWriteEnabled;
    info.EmbeddedInFirefoxReality = gfxInfo.EmbeddedInFirefoxReality;
  } catch (e) {
    info.D2DEnabled = false;
    info.DWriteEnabled = false;
    info.EmbeddedInFirefoxReality = false;
  }

  info.AzureCanvasBackend = gfxInfo.AzureCanvasBackend;
  info.AzureContentBackend = gfxInfo.AzureContentBackend;

  return sendSyncMessage("reftest:ContentReady", { gfx: info })[0];
}

function SendException(what) {
  sendAsyncMessage("reftest:Exception", { what });
}

function SendFailedLoad(why) {
  sendAsyncMessage("reftest:FailedLoad", { why });
}

function SendFailedNoPaint() {
  sendAsyncMessage("reftest:FailedNoPaint");
}

function SendFailedNoDisplayList() {
  sendAsyncMessage("reftest:FailedNoDisplayList");
}

function SendFailedDisplayList() {
  sendAsyncMessage("reftest:FailedDisplayList");
}

function SendFailedOpaqueLayer(why) {
  sendAsyncMessage("reftest:FailedOpaqueLayer", { why });
}

function SendFailedAssignedLayer(why) {
  sendAsyncMessage("reftest:FailedAssignedLayer", { why });
}

// Returns a promise that resolves to a bool that indicates if a snapshot was taken.
async function SendInitCanvasWithSnapshot(forURL) {
  if (forURL != gCurrentURL) {
    LogInfo("SendInitCanvasWithSnapshot called for previous document");
    // Lie and say we painted because it doesn't matter, this is a test we
    // are already done with that is clearing out. Then AfterOnLoadScripts
    // should finish quicker if that is who is calling us.
    return Promise.resolve(true);
  }

  // If we're in the same process as the top-level XUL window, then
  // drawing that window will also update our layers, so no
  // synchronization is needed.
  //
  // NB: this is a test-harness optimization only, it must not
  // affect the validity of the tests.
  if (gBrowserIsRemote) {
    await SynchronizeForSnapshot(SYNC_DEFAULT);
    let promise = new Promise(resolve => {
      gUpdateCanvasPromiseResolver = resolve;
    });
    sendAsyncMessage("reftest:InitCanvasWithSnapshot");

    gHaveCanvasSnapshot = await promise;
    return gHaveCanvasSnapshot;
  }

  // For in-process browser, we have to make a synchronous request
  // here to make the above optimization valid, so that MozWaitPaint
  // events dispatched (synchronously) during painting are received
  // before we check the paint-wait counter.  For out-of-process
  // browser though, it doesn't wrt correctness whether this request
  // is sync or async.
  let promise = new Promise(resolve => {
    gUpdateCanvasPromiseResolver = resolve;
  });
  sendAsyncMessage("reftest:InitCanvasWithSnapshot");

  gHaveCanvasSnapshot = await promise;
  return Promise.resolve(gHaveCanvasSnapshot);
}

function SendScriptResults(runtimeMs, error, results) {
  sendAsyncMessage("reftest:ScriptResults", {
    runtimeMs,
    error,
    results,
  });
}

function SendStartPrint(isPrintSelection, printRange) {
  sendAsyncMessage("reftest:StartPrint", { isPrintSelection, printRange });
}

function SendPrintResult(runtimeMs, status, fileName) {
  sendAsyncMessage("reftest:PrintResult", {
    runtimeMs,
    status,
    fileName,
  });
}

function SendExpectProcessCrash() {
  sendAsyncMessage("reftest:ExpectProcessCrash");
}

function SendTestDone(runtimeMs) {
  sendAsyncMessage("reftest:TestDone", { runtimeMs });
}

function roundTo(x, fraction) {
  return Math.round(x / fraction) * fraction;
}

function elementDescription(element) {
  return (
    "<" +
    element.localName +
    [].slice
      .call(element.attributes)
      .map(attr => ` ${attr.nodeName}="${attr.value}"`)
      .join("") +
    ">"
  );
}

async function SendUpdateCanvasForEvent(forURL, rectList, contentRootElement) {
  if (forURL != gCurrentURL) {
    LogInfo("SendUpdateCanvasForEvent called for previous document");
    // This is a test we are already done with that is clearing out.
    // Don't do anything.
    return;
  }

  var scale = docShell.browsingContext.fullZoom;

  var rects = [];
  if (shouldSnapshotWholePage(contentRootElement)) {
    // See comments in SendInitCanvasWithSnapshot() re: the split
    // logic here.
    if (!gBrowserIsRemote) {
      sendSyncMessage("reftest:UpdateWholeCanvasForInvalidation");
    } else {
      await SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
      let promise = new Promise(resolve => {
        gUpdateCanvasPromiseResolver = resolve;
      });
      sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation");
      await promise;
    }
    return;
  }

  var message;

  if (!windowUtils().isMozAfterPaintPending) {
    // Webrender doesn't have invalidation, and animations on the compositor
    // don't invoke any MozAfterEvent which means we have no invalidated
    // rect so we just invalidate the whole screen once we don't have
    // anymore paints pending. This will force the snapshot.

    LogInfo("Sending update whole canvas for invalidation");
    message = "reftest:UpdateWholeCanvasForInvalidation";
  } else {
    LogInfo("SendUpdateCanvasForEvent with " + rectList.length + " rects");
    for (var i = 0; i < rectList.length; ++i) {
      var r = rectList[i];
      // Set left/top/right/bottom to "device pixel" boundaries
      var left = Math.floor(roundTo(r.left * scale, 0.001));
      var top = Math.floor(roundTo(r.top * scale, 0.001));
      var right = Math.ceil(roundTo(r.right * scale, 0.001));
      var bottom = Math.ceil(roundTo(r.bottom * scale, 0.001));
      LogInfo("Rect: " + left + " " + top + " " + right + " " + bottom);

      rects.push({ left, top, right, bottom });
    }

    message = "reftest:UpdateCanvasForInvalidation";
  }

  // See comments in SendInitCanvasWithSnapshot() re: the split
  // logic here.
  if (!gBrowserIsRemote) {
    sendSyncMessage(message, { rects });
  } else {
    await SynchronizeForSnapshot(SYNC_ALLOW_DISABLE);
    let promise = new Promise(resolve => {
      gUpdateCanvasPromiseResolver = resolve;
    });
    sendAsyncMessage(message, { rects });
    await promise;
  }
}

if (content.document.readyState == "complete") {
  // load event has already fired for content, get started
  OnInitialLoad();
} else {
  addEventListener("load", OnInitialLoad, true);
}
