diff options
Diffstat (limited to 'layout/tools/reftest/reftest-content.js')
-rw-r--r-- | layout/tools/reftest/reftest-content.js | 1174 |
1 files changed, 1174 insertions, 0 deletions
diff --git a/layout/tools/reftest/reftest-content.js b/layout/tools/reftest/reftest-content.js new file mode 100644 index 000000000..f26cae8ef --- /dev/null +++ b/layout/tools/reftest/reftest-content.js @@ -0,0 +1,1174 @@ +/* -*- 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/. */ + +var CC = Components.classes; +const CI = Components.interfaces; +const CR = Components.results; +const CU = Components.utils; + +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 ENVIRONMENT_CONTRACTID = "@mozilla.org/process/environment;1"; +const NS_OBSERVER_SERVICE_CONTRACTID = "@mozilla.org/observer-service;1"; +const NS_GFXINFO_CONTRACTID = "@mozilla.org/gfx/info;1"; + +// "<!--CLEAR-->" +const BLANK_URL_FOR_CLEARING = "data:text/html;charset=UTF-8,%3C%21%2D%2DCLEAR%2D%2D%3E"; + +CU.import("resource://gre/modules/Timer.jsm"); +CU.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm"); + +var gBrowserIsRemote; +var gHaveCanvasSnapshot = false; +// Plugin layers can be updated asynchronously, so to make sure that all +// layer surfaces have the right content, we need to listen for explicit +// "MozPaintWait" and "MozPaintWaitFinished" events that signal when it's OK +// to take snapshots. We cannot take a snapshot while the number of +// "MozPaintWait" events fired exceeds the number of "MozPaintWaitFinished" +// events fired. We count the number of such excess events here. When +// the counter reaches zero we call gExplicitPendingPaintsCompleteHook. +var gExplicitPendingPaintCount = 0; +var gExplicitPendingPaintsCompleteHook; +var gCurrentURL; +var gCurrentTestType; +var gTimeoutHook = null; +var gFailureTimeout = null; +var gFailureReason; +var gAssertionCount = 0; +var gTestCount = 0; + +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 + +function markupDocumentViewer() { + return docShell.contentViewer; +} + +function webNavigation() { + return docShell.QueryInterface(CI.nsIWebNavigation); +} + +function windowUtilsForWindow(w) { + return w.QueryInterface(CI.nsIInterfaceRequestor) + .getInterface(CI.nsIDOMWindowUtils); +} + +function windowUtils() { + return windowUtilsForWindow(content); +} + +function IDForEventTarget(event) +{ + try { + return "'" + event.target.getAttribute('id') + "'"; + } catch (ex) { + return "<unknown>"; + } +} + +function PaintWaitListener(event) +{ + LogInfo("MozPaintWait received for ID " + IDForEventTarget(event)); + gExplicitPendingPaintCount++; +} + +function PaintWaitFinishedListener(event) +{ + LogInfo("MozPaintWaitFinished received for ID " + IDForEventTarget(event)); + gExplicitPendingPaintCount--; + if (gExplicitPendingPaintCount < 0) { + LogWarning("Underrun in gExplicitPendingPaintCount\n"); + gExplicitPendingPaintCount = 0; + } + if (gExplicitPendingPaintCount == 0 && + gExplicitPendingPaintsCompleteHook) { + gExplicitPendingPaintsCompleteHook(); + } +} + +function OnInitialLoad() +{ +#ifndef REFTEST_B2G + removeEventListener("load", OnInitialLoad, true); +#endif + + gDebug = CC[DEBUG_CONTRACTID].getService(CI.nsIDebug2); + var env = CC[ENVIRONMENT_CONTRACTID].getService(CI.nsIEnvironment); + gVerbose = !!env.get("MOZ_REFTEST_VERBOSE"); + + RegisterMessageListeners(); + + var initInfo = SendContentReady(); + gBrowserIsRemote = initInfo.remote; + + addEventListener("load", OnDocumentLoad, true); + + addEventListener("MozPaintWait", PaintWaitListener, true); + addEventListener("MozPaintWaitFinished", PaintWaitFinishedListener, true); + + LogInfo("Using browser remote="+ gBrowserIsRemote +"\n"); +} + +function SetFailureTimeout(cb, timeout) +{ + 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(); + } + } + + gFailureTimeout = setTimeout(wrapper, timeout); +} + +function StartTestURI(type, uri, 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. + ++gTestCount; + if (gTestCount % 1000 == 0) { + CU.forceGC(); + CU.forceCC(); + } + + // Reset gExplicitPendingPaintCount in case there was a timeout or + // the count is out of sync for some other reason + if (gExplicitPendingPaintCount != 0) { + LogWarning("Resetting gExplicitPendingPaintCount to zero (currently " + + gExplicitPendingPaintCount + "\n"); + gExplicitPendingPaintCount = 0; + } + + gCurrentTestType = type; + gCurrentURL = uri; + + gCurrentTestStartTime = Date.now(); + if (gFailureTimeout != null) { + SendException("program error managing timeouts\n"); + } + SetFailureTimeout(LoadFailed, timeout); + + LoadURI(gCurrentURL); +} + +function setupFullZoom(contentRootElement) { + if (!contentRootElement || !contentRootElement.hasAttribute('reftest-zoom')) + return; + markupDocumentViewer().fullZoom = + contentRootElement.getAttribute('reftest-zoom'); +} + +function resetZoom() { + markupDocumentViewer().fullZoom = 1.0; +} + +function doPrintMode(contentRootElement) { +#if REFTEST_B2G + // nsIPrintSettings not available in B2G + return false; +#else + // use getAttribute because className works differently in HTML and SVG + return contentRootElement && + contentRootElement.hasAttribute('class') && + contentRootElement.getAttribute('class').split(/\s+/) + .indexOf("reftest-print") != -1; +#endif +} + +function setupPrintMode() { + var PSSVC = + CC[PRINTSETTINGS_CONTRACTID].getService(CI.nsIPrintSettingsService); + var ps = PSSVC.newPrintSettings; + 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 = ""; + docShell.contentViewer.setPageMode(true, ps); +} + +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 scrollport to <w=" + sw + ", h=" + sh + ">"); + windowUtils().setScrollPositionClampingScrollPortSize(sw, sh); + } + + // XXX support resolution when needed + + // XXX support viewconfig when needed +} + +function setupDisplayport(contentRootElement) { + if (!contentRootElement) { + return; + } + + function setupDisplayportForElement(element, winUtils) { + var dpw = attrOrDefault(element, "reftest-displayport-w", 0); + var dph = attrOrDefault(element, "reftest-displayport-h", 0); + var dpx = attrOrDefault(element, "reftest-displayport-x", 0); + var dpy = attrOrDefault(element, "reftest-displayport-y", 0); + if (dpw !== 0 || dph !== 0 || dpx != 0 || dpy != 0) { + LogInfo("Setting displayport to <x="+ dpx +", y="+ dpy +", w="+ dpw +", h="+ dph +">"); + winUtils.setDisplayPortForElement(dpx, dpy, dpw, dph, element, 1); + } + } + + function setupDisplayportForElementSubtree(element, winUtils) { + setupDisplayportForElement(element, winUtils); + for (var c = element.firstElementChild; c; c = c.nextElementSibling) { + setupDisplayportForElementSubtree(c, winUtils); + } + if (element.contentDocument) { + LogInfo("Descending into subdocument"); + setupDisplayportForElementSubtree(element.contentDocument.documentElement, + windowUtilsForWindow(element.contentWindow)); + } + } + + if (contentRootElement.hasAttribute("reftest-async-scroll")) { + setupDisplayportForElementSubtree(contentRootElement, windowUtils()); + } else { + setupDisplayportForElement(contentRootElement, windowUtils()); + } +} + +// Returns whether any offsets were updated +function setupAsyncScrollOffsets(options) { + var currentDoc = content.document; + var contentRootElement = currentDoc ? currentDoc.documentElement : null; + + if (!contentRootElement) { + return false; + } + + function setupAsyncScrollOffsetsForElement(element, winUtils) { + var sx = attrOrDefault(element, "reftest-async-scroll-x", 0); + var sy = attrOrDefault(element, "reftest-async-scroll-y", 0); + if (sx != 0 || sy != 0) { + try { + // This might fail when called from RecordResult since layers + // may not have been constructed yet + winUtils.setAsyncScrollOffset(element, sx, sy); + return true; + } catch (e) { + if (!options.allowFailure) { + throw e; + } + } + } + return false; + } + + function setupAsyncScrollOffsetsForElementSubtree(element, winUtils) { + var updatedAny = setupAsyncScrollOffsetsForElement(element, winUtils); + for (var c = element.firstElementChild; c; c = c.nextElementSibling) { + if (setupAsyncScrollOffsetsForElementSubtree(c, winUtils)) { + updatedAny = true; + } + } + if (element.contentDocument) { + LogInfo("Descending into subdocument (async offsets)"); + if (setupAsyncScrollOffsetsForElementSubtree(element.contentDocument.documentElement, + windowUtilsForWindow(element.contentWindow))) { + updatedAny = true; + } + } + return updatedAny; + } + + var asyncScroll = contentRootElement.hasAttribute("reftest-async-scroll"); + if (asyncScroll) { + return setupAsyncScrollOffsetsForElementSubtree(contentRootElement, windowUtils()); + } + 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 shouldWaitForExplicitPaintWaiters() { + return gExplicitPendingPaintCount > 0; +} + +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+/) + .indexOf("reftest-wait") != -1; +} + +function shouldSnapshotWholePage(contentRootElement) { + // use getAttribute because className works differently in HTML and SVG + return contentRootElement && + contentRootElement.hasAttribute('class') && + contentRootElement.getAttribute('class').split(/\s+/) + .indexOf("reftest-snapshot-all") != -1; +} + +function getNoPaintElements(contentRootElement) { + return contentRootElement.getElementsByClassName('reftest-no-paint'); +} + +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; +} + +// 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; + +function FlushRendering() { + var anyPendingPaintsGeneratedInDescendants = false; + + function flushWindow(win) { + var utils = win.QueryInterface(CI.nsIInterfaceRequestor) + .getInterface(CI.nsIDOMWindowUtils); + var afterPaintWasPending = utils.isMozAfterPaintPending; + + var root = win.document.documentElement; + if (root && !root.classList.contains("reftest-no-flush")) { + try { + // Flush pending restyles and reflows for this window + root.getBoundingClientRect(); + } catch (e) { + LogWarning("flushWindow failed: " + e + "\n"); + } + } + + if (!afterPaintWasPending && utils.isMozAfterPaintPending) { + LogInfo("FlushRendering generated paint for window " + win.location.href); + anyPendingPaintsGeneratedInDescendants = true; + } + + for (var i = 0; i < win.frames.length; ++i) { + flushWindow(win.frames[i]); + } + } + + flushWindow(content); + + if (anyPendingPaintsGeneratedInDescendants && + !windowUtils().isMozAfterPaintPending) { + LogWarning("Internal error: descendant frame generated a MozAfterPaint event, but the root document doesn't have one!"); + } +} + +function WaitForTestEnd(contentRootElement, inPrintMode, spellCheckedElements) { + var stopAfterPaintReceived = false; + var currentDoc = content.document; + var state = STATE_WAITING_TO_FIRE_INVALIDATE_EVENT; + + 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; + } + + SendUpdateCanvasForEvent(event, contentRootElement); + // These events are fired immediately after a paint. Don't + // confuse ourselves by firing synchronously if we triggered the + // paint ourselves. + setTimeout(MakeProgress, 0); + } + + 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. + setTimeout(MakeProgress, 0); + } + + function ExplicitPaintsCompleteListener() { + LogInfo("ExplicitPaintsCompleteListener fired"); + // Since this can fire while painting, don't confuse ourselves by + // firing synchronously. It's fine to do this asynchronously. + setTimeout(MakeProgress, 0); + } + + function RemoveListeners() { + // OK, we can end the test now. + removeEventListener("MozAfterPaint", AfterPaintListener, false); + if (contentRootElement) { + contentRootElement.removeEventListener("DOMAttrModified", AttrModifiedListener, false); + } + gExplicitPendingPaintsCompleteHook = 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; + } + + FlushRendering(); + + switch (state) { + case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: { + LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT"); + if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) { + gFailureReason = "timed out waiting for pending paint count to reach zero"; + if (shouldWaitForExplicitPaintWaiters()) { + gFailureReason += " (waiting for MozPaintWaitFinished)"; + LogInfo("MakeProgress: waiting for MozPaintWaitFinished"); + } + if (shouldWaitForPendingPaints()) { + gFailureReason += " (waiting for MozAfterPaint)"; + LogInfo("MakeProgress: waiting for MozAfterPaint"); + } + return; + } + + state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL; + 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]); + } + 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(); + } + + if (hasReftestWait && !shouldWaitForReftestWaitRemoval(contentRootElement)) { + // MozReftestInvalidate handler removed reftest-wait. + // We expect something to have been invalidated... + FlushRendering(); + if (!shouldWaitForPendingPaints() && !shouldWaitForExplicitPaintWaiters()) { + LogWarning("MozInvalidateEvent didn't invalidate"); + } + } + // Try next state + MakeProgress(); + return; + } + + case STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL: + LogInfo("MakeProgress: STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL"); + if (shouldWaitForReftestWaitRemoval(contentRootElement)) { + gFailureReason = "timed out waiting for reftest-wait to be removed"; + LogInfo("MakeProgress: waiting for reftest-wait to be removed"); + return; + } + + // 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; + MakeProgress(); + }; + os.addObserver(flushWaiter, "apz-repaints-flushed", false); + + var willSnapshot = (gCurrentTestType != TYPE_SCRIPT) && + (gCurrentTestType != TYPE_LOAD); + var noFlush = + !(contentRootElement && + contentRootElement.classList.contains("reftest-no-flush")); + 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 (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) { + gFailureReason = "timed out waiting for pending paint count to " + + "reach zero (after reftest-wait removed and switch to print mode)"; + if (shouldWaitForExplicitPaintWaiters()) { + gFailureReason += " (waiting for MozPaintWaitFinished)"; + LogInfo("MakeProgress: waiting for MozPaintWaitFinished"); + } + if (shouldWaitForPendingPaints()) { + gFailureReason += " (waiting for MozAfterPaint)"; + LogInfo("MakeProgress: waiting for MozAfterPaint"); + } + return; + } + if (contentRootElement) { + var elements = getNoPaintElements(contentRootElement); + for (var i = 0; i < elements.length; ++i) { + if (windowUtils().checkAndClearPaintedState(elements[i])) { + SendFailedNoPaint(); + } + } + CheckLayerAssertions(contentRootElement); + } + LogInfo("MakeProgress: Completed"); + state = STATE_COMPLETED; + gFailureReason = "timed out while taking snapshot (bug in harness?)"; + RemoveListeners(); + CheckForProcessCrashExpectation(); + setTimeout(RecordResult, 0); + return; + } + } + + LogInfo("WaitForTestEnd: Adding listeners"); + addEventListener("MozAfterPaint", AfterPaintListener, false); + // If contentRootElement is null then shouldWaitForReftestWaitRemoval will + // always return false so we don't need a listener anyway + if (contentRootElement) { + contentRootElement.addEventListener("DOMAttrModified", AttrModifiedListener, false); + } + gExplicitPendingPaintsCompleteHook = ExplicitPaintsCompleteListener; + gTimeoutHook = RemoveListeners; + + // Listen for spell checks on spell-checked elements. + var numPendingSpellChecks = spellCheckedElements.length; + function decNumPendingSpellChecks() { + --numPendingSpellChecks; + 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. + SendInitCanvasWithSnapshot(); + MakeProgress(); +} + +function OnDocumentLoad(event) +{ + var currentDoc = content.document; + if (event.target != currentDoc) + // Ignore load events for subframes. + return; + + if (gClearingForAssertionCheck && + currentDoc.location.href == BLANK_URL_FOR_CLEARING) { + DoAssertionCheck(); + return; + } + + if (currentDoc.location.href != gCurrentURL) { + LogInfo("OnDocumentLoad fired for previous document"); + // Ignore load events for previous documents. + return; + } + + // 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.querySelectorAll(querySelector); + + var contentRootElement = currentDoc ? currentDoc.documentElement : null; + currentDoc = null; + setupFullZoom(contentRootElement); + setupViewport(contentRootElement); + setupDisplayport(contentRootElement); + var inPrintMode = false; + + 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. + FlushRendering(); + + // Take a snapshot now. We need to do this before we check whether + // we should wait, since this might trigger dispatching of + // MozPaintWait events and make shouldWaitForExplicitPaintWaiters() true + // below. + var painted = SendInitCanvasWithSnapshot(); + + if (shouldWaitForExplicitPaintWaiters() || + (!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, []); + } else { + CheckLayerAssertions(contentRootElement); + CheckForProcessCrashExpectation(contentRootElement); + RecordResult(); + } + } + + if (shouldWaitForReftestWaitRemoval(contentRootElement) || + shouldWaitForExplicitPaintWaiters() || + 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); }, 0); + } else { + if (doPrintMode(contentRootElement)) { + LogInfo("OnDocumentLoad setting up print mode"); + setupPrintMode(); + 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 CheckLayerAssertions(contentRootElement) +{ + if (!contentRootElement) { + return; + } + + var opaqueLayerElements = getOpaqueLayerElements(contentRootElement); + for (var i = 0; i < opaqueLayerElements.length; ++i) { + var elem = opaqueLayerElements[i]; + try { + if (!windowUtils().isPartOfOpaqueLayer(elem)) { + SendFailedOpaqueLayer(elementDescription(elem) + ' is not part of an opaque layer'); + } + } catch (e) { + SendFailedOpaqueLayer('got an exception while checking whether ' + elementDescription(elem) + ' is part of an opaque layer'); + } + } + var layerNameToElementsMap = getAssignedLayerMap(contentRootElement); + var oneOfEach = []; + // Check that elements with the same reftest-assigned-layer share the same PaintedLayer. + for (var layerName in layerNameToElementsMap) { + try { + var elements = layerNameToElementsMap[layerName]; + oneOfEach.push(elements[0]); + var numberOfLayers = windowUtils().numberOfAssignedPaintedLayers(elements, elements.length); + if (numberOfLayers !== 1) { + SendFailedAssignedLayer('these elements are assigned to ' + numberOfLayers + + ' different layers, instead of sharing just one layer: ' + + elements.map(elementDescription).join(', ')); + } + } catch (e) { + SendFailedAssignedLayer('got an exception while checking whether these elements share a layer: ' + + elements.map(elementDescription).join(', ')); + } + } + // Check that elements with different reftest-assigned-layer are assigned to different PaintedLayers. + if (oneOfEach.length > 0) { + try { + var numberOfLayers = windowUtils().numberOfAssignedPaintedLayers(oneOfEach, oneOfEach.length); + if (numberOfLayers !== oneOfEach.length) { + SendFailedAssignedLayer('these elements are assigned to ' + numberOfLayers + + ' different layers, instead of having none in common (expected ' + + oneOfEach.length + ' different layers): ' + + oneOfEach.map(elementDescription).join(', ')); + } + } catch (e) { + SendFailedAssignedLayer('got an exception while checking whether these elements are assigned to different layers: ' + + oneOfEach.map(elementDescription).join(', ')); + } + } +} + +function CheckForProcessCrashExpectation(contentRootElement) +{ + if (contentRootElement && + contentRootElement.hasAttribute('class') && + contentRootElement.getAttribute('class').split(/\s+/) + .indexOf("reftest-expect-process-crash") != -1) { + SendExpectProcessCrash(); + } +} + +function RecordResult() +{ + LogInfo("RecordResult fired"); + + var currentTestRunTime = Date.now() - gCurrentTestStartTime; + + clearTimeout(gFailureTimeout); + gFailureReason = null; + gFailureTimeout = null; + + 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). + var changedAsyncScrollZoom = false; + if (setupAsyncScrollOffsets({allowFailure:true})) { + changedAsyncScrollZoom = 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) +{ + var flags = webNavigation().LOAD_FLAGS_NONE; + webNavigation().loadURI(uri, flags, null, null, null); +} + +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 }); + } +} + +const SYNC_DEFAULT = 0x0; +const SYNC_ALLOW_DISABLE = 0x1; +function SynchronizeForSnapshot(flags) +{ + if (gCurrentTestType == TYPE_SCRIPT || + gCurrentTestType == TYPE_LOAD) { + // Script tests or load-only tests do not need any snapshotting + return; + } + + if (flags & SYNC_ALLOW_DISABLE) { + var docElt = content.document.documentElement; + if (docElt && docElt.hasAttribute("reftest-no-sync-layers")) { + LogInfo("Test file chose to skip SynchronizeForSnapshot"); + return; + } + } + + windowUtils().updateLayerTree(); + + // Setup async scroll offsets now, because any scrollable layers should + // have had their AsyncPanZoomControllers created. + setupAsyncScrollOffsets({allowFailure:false}); + 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:LoadTest", + function (m) { RecvLoadTest(m.json.type, m.json.uri, m.json.timeout); } + ); + addMessageListener( + "reftest:ResetRenderingState", + function (m) { RecvResetRenderingState(); } + ); +} + +function RecvClear() +{ + gClearingForAssertionCheck = true; + LoadURI(BLANK_URL_FOR_CLEARING); +} + +function RecvLoadTest(type, uri, timeout) +{ + StartTestURI(type, uri, timeout); +} + +function RecvLoadScriptTest(uri, timeout) +{ + StartTestURI(TYPE_SCRIPT, uri, timeout); +} + +function RecvResetRenderingState() +{ + resetZoom(); + resetDisplayportAndViewport(); +} + +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 = gfxInfo.getInfo(); + try { + info.D2DEnabled = gfxInfo.D2DEnabled; + info.DWriteEnabled = gfxInfo.DWriteEnabled; + } catch (e) { + info.D2DEnabled = false; + info.DWriteEnabled = false; + } + + 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 SendFailedOpaqueLayer(why) +{ + sendAsyncMessage("reftest:FailedOpaqueLayer", { why: why }); +} + +function SendFailedAssignedLayer(why) +{ + sendAsyncMessage("reftest:FailedAssignedLayer", { why: why }); +} + +// Return true if a snapshot was taken. +function SendInitCanvasWithSnapshot() +{ + // 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) { + SynchronizeForSnapshot(SYNC_DEFAULT); + } + + // 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. + var ret = sendSyncMessage("reftest:InitCanvasWithSnapshot")[0]; + + gHaveCanvasSnapshot = ret.painted; + return ret.painted; +} + +function SendScriptResults(runtimeMs, error, results) +{ + sendAsyncMessage("reftest:ScriptResults", + { runtimeMs: runtimeMs, error: error, results: results }); +} + +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('') + + '>'; +} + +function SendUpdateCanvasForEvent(event, contentRootElement) +{ + var win = content; + var scale = markupDocumentViewer().fullZoom; + + var rects = [ ]; + if (shouldSnapshotWholePage(contentRootElement)) { + // See comments in SendInitCanvasWithSnapshot() re: the split + // logic here. + if (!gBrowserIsRemote) { + sendSyncMessage("reftest:UpdateWholeCanvasForInvalidation"); + } else { + SynchronizeForSnapshot(SYNC_ALLOW_DISABLE); + sendAsyncMessage("reftest:UpdateWholeCanvasForInvalidation"); + } + return; + } + + var rectList = event.clientRects; + 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 }); + } + + // See comments in SendInitCanvasWithSnapshot() re: the split + // logic here. + if (!gBrowserIsRemote) { + sendSyncMessage("reftest:UpdateCanvasForInvalidation", { rects: rects }); + } else { + SynchronizeForSnapshot(SYNC_ALLOW_DISABLE); + sendAsyncMessage("reftest:UpdateCanvasForInvalidation", { rects: rects }); + } +} +#if REFTEST_B2G +OnInitialLoad(); +#else +if (content.document.readyState == "complete") { + // load event has already fired for content, get started + OnInitialLoad(); +} else { + addEventListener("load", OnInitialLoad, true); +} +#endif |