Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
);
// 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);
}