diff options
Diffstat (limited to 'layout/tools/reftest/reftest-content.js')
-rw-r--r-- | layout/tools/reftest/reftest-content.js | 1530 |
1 files changed, 1530 insertions, 0 deletions
diff --git a/layout/tools/reftest/reftest-content.js b/layout/tools/reftest/reftest-content.js new file mode 100644 index 0000000000..2703cbc850 --- /dev/null +++ b/layout/tools/reftest/reftest-content.js @@ -0,0 +1,1530 @@ +/* -*- 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); +} |