/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- /
/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
/* 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/. */

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.jsm
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, status) {
    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;
        }
        return classList.includes("reftest-paged");
    }
}

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.contentViewer.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(contentRootElement) {
    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 != false) {
            LogWarning("Nesting atomic operations?");
        }
        operationInProgress = true;
    }
    function OperationCompleted() {
        if (operationInProgress != true) {
            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 stopAfterPaintReceived = false;
    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.
    }

    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 (contentRootElement) {
            contentRootElement.removeEventListener("DOMAttrModified", AttrModifiedListener);
        }
        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.
          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();
            }
        });
    }

    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) {
                var elements = getNoPaintElements(contentRootElement);
                for (var i = 0; i < elements.length; ++i) {
                  windowUtils().checkAndClearPaintedState(elements[i]);
                }
                elements = getNoDisplayListElements(contentRootElement);
                for (var i = 0; i < elements.length; ++i) {
                  windowUtils().checkAndClearDisplayListState(elements[i]);
                }
                elements = getDisplayListElements(contentRootElement);
                for (var i = 0; i < elements.length; ++i) {
                  windowUtils().checkAndClearDisplayListState(elements[i]);
                }
                var notification = content.document.createEvent("Events");
                notification.initEvent("MozReftestInvalidate", true, false);
                contentRootElement.dispatchEvent(notification);
            }

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

            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 os = Cc[NS_OBSERVER_SERVICE_CONTRACTID].getService(Ci.nsIObserverService);
            var flushWaiter = function(aSubject, aTopic, aData) {
                if (aTopic) LogInfo("MakeProgress: apz-repaints-flushed fired");
                os.removeObserver(flushWaiter, "apz-repaints-flushed");
                state = STATE_WAITING_TO_FINISH;
                if (operationInProgress) {
                    CallSetTimeoutMakeProgress();
                } else {
                    MakeProgress();
                }
            };
            os.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) {
              var elements = getNoPaintElements(contentRootElement);
              for (var 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 (var i = 0; i < elements.length; ++i) {
                    if (windowUtils().checkAndClearDisplayListState(elements[i])) {
                        SendFailedNoDisplayList();
                    }
                }
                elements = getDisplayListElements(contentRootElement);
                for (var 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);
            return;
        }
    }

    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) {
      contentRootElement.addEventListener("DOMAttrModified", AttrModifiedListener);
    }
    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 == 0) {
            // 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(result) {
            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(result) {
            setupAsyncZoom({allowFailure:false});
        });
    });
}

function RegisterMessageListeners()
{
    addMessageListener(
        "reftest:Clear",
        function (m) { 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 (m) { 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: what });
}

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

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

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

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

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

function SendFailedAssignedLayer(why)
{
    sendAsyncMessage("reftest:FailedAssignedLayer", { why: 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: runtimeMs, error: error, results: results });
}

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

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

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

function SendTestDone(runtimeMs)
{
    sendAsyncMessage("reftest:TestDone", { runtimeMs: 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 win = content;
    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: left, top: top, right: right, bottom: bottom });
        }

        message = "reftest:UpdateCanvasForInvalidation";
    }

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

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