diff options
Diffstat (limited to 'remote/marionette/actors/MarionetteReftestChild.sys.mjs')
-rw-r--r-- | remote/marionette/actors/MarionetteReftestChild.sys.mjs | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/remote/marionette/actors/MarionetteReftestChild.sys.mjs b/remote/marionette/actors/MarionetteReftestChild.sys.mjs new file mode 100644 index 0000000000..e1a9918af2 --- /dev/null +++ b/remote/marionette/actors/MarionetteReftestChild.sys.mjs @@ -0,0 +1,236 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +/** + * Child JSWindowActor to handle navigation for reftests relying on marionette. + */ +export class MarionetteReftestChild extends JSWindowActorChild { + constructor() { + super(); + + // This promise will resolve with the URL recorded in the "load" event + // handler. This URL will not be impacted by any hash modification that + // might be performed by the test script. + // The harness should be loaded before loading any test page, so the actors + // should be registered before the "load" event is received for a test page. + this._loadedURLPromise = new Promise( + r => (this._resolveLoadedURLPromise = r) + ); + } + + handleEvent(event) { + if (event.type == "load") { + const url = event.target.location.href; + lazy.logger.debug(`Handle load event with URL ${url}`); + this._resolveLoadedURLPromise(url); + } + } + + actorCreated() { + lazy.logger.trace( + `[${this.browsingContext.id}] Reftest actor created ` + + `for window id ${this.manager.innerWindowId}` + ); + } + + async receiveMessage(msg) { + const { name, data } = msg; + + let result; + switch (name) { + case "MarionetteReftestParent:flushRendering": + result = await this.flushRendering(data); + break; + case "MarionetteReftestParent:reftestWait": + result = await this.reftestWait(data); + break; + } + return result; + } + + /** + * Wait for a reftest page to be ready for screenshots: + * - wait for the loadedURL to be available (see handleEvent) + * - check if the URL matches the expected URL + * - if present, wait for the "reftest-wait" classname to be removed from the + * document element + * + * @param {Object} options + * @param {String} options.url + * The expected test page URL + * @param {Boolean} options.useRemote + * True when using e10s + * @return {Boolean} + * Returns true when the correct page is loaded and ready for + * screenshots. Returns false if the page loaded bug does not have the + * expected URL. + */ + async reftestWait(options = {}) { + const { url, useRemote } = options; + const loadedURL = await this._loadedURLPromise; + if (loadedURL !== url) { + lazy.logger.debug( + `Window URL does not match the expected URL "${loadedURL}" !== "${url}"` + ); + return false; + } + + const documentElement = this.document.documentElement; + const hasReftestWait = documentElement.classList.contains("reftest-wait"); + + lazy.logger.debug("Waiting for event loop to spin"); + await new Promise(resolve => + this.document.defaultView.setTimeout(resolve, 0) + ); + + await this.paintComplete({ useRemote, ignoreThrottledAnimations: true }); + + if (hasReftestWait) { + const event = new this.document.defaultView.Event("TestRendered", { + bubbles: true, + }); + documentElement.dispatchEvent(event); + lazy.logger.info("Emitted TestRendered event"); + await this.reftestWaitRemoved(); + await this.paintComplete({ useRemote, ignoreThrottledAnimations: false }); + } + if ( + this.document.defaultView.innerWidth < documentElement.scrollWidth || + this.document.defaultView.innerHeight < documentElement.scrollHeight + ) { + lazy.logger.warn( + `${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})` + ); + } + return true; + } + + paintComplete({ useRemote, ignoreThrottledAnimations }) { + lazy.logger.debug("Waiting for rendering"); + let windowUtils = this.document.defaultView.windowUtils; + return new Promise(resolve => { + let maybeResolve = () => { + this.flushRendering({ ignoreThrottledAnimations }); + if (useRemote) { + // Flush display (paint) + lazy.logger.debug("Force update of layer tree"); + windowUtils.updateLayerTree(); + } + + if (windowUtils.isMozAfterPaintPending) { + lazy.logger.debug("isMozAfterPaintPending: true"); + this.document.defaultView.addEventListener( + "MozAfterPaint", + maybeResolve, + { + once: true, + } + ); + } else { + // resolve at the start of the next frame in case of leftover paints + lazy.logger.debug("isMozAfterPaintPending: false"); + this.document.defaultView.requestAnimationFrame(() => { + this.document.defaultView.requestAnimationFrame(resolve); + }); + } + }; + maybeResolve(); + }); + } + + reftestWaitRemoved() { + lazy.logger.debug("Waiting for reftest-wait removal"); + return new Promise(resolve => { + const documentElement = this.document.documentElement; + let observer = new this.document.defaultView.MutationObserver(() => { + if (!documentElement.classList.contains("reftest-wait")) { + observer.disconnect(); + lazy.logger.debug("reftest-wait removed"); + this.document.defaultView.setTimeout(resolve, 0); + } + }); + if (documentElement.classList.contains("reftest-wait")) { + observer.observe(documentElement, { attributes: true }); + } else { + this.document.defaultView.setTimeout(resolve, 0); + } + }); + } + + /** + * Ensure layout is flushed in each frame + * + * @param {Object} options + * @param {Boolean} options.ignoreThrottledAnimations Don't flush + * the layout of throttled animations. We can end up in a + * situation where flushing a throttled animation causes + * mozAfterPaint events even when all rendering we care about + * should have ceased. See + * https://searchfox.org/mozilla-central/rev/d58860eb739af613774c942c3bb61754123e449b/layout/tools/reftest/reftest-content.js#723-729 + * for more detail. + */ + flushRendering(options = {}) { + let { ignoreThrottledAnimations } = options; + lazy.logger.debug( + `flushRendering ignoreThrottledAnimations:${ignoreThrottledAnimations}` + ); + let anyPendingPaintsGeneratedInDescendants = false; + + let windowUtils = this.document.defaultView.windowUtils; + + function flushWindow(win) { + let utils = win.windowUtils; + let afterPaintWasPending = utils.isMozAfterPaintPending; + + let root = win.document.documentElement; + if (root) { + try { + if (ignoreThrottledAnimations) { + utils.flushLayoutWithoutThrottledAnimations(); + } else { + root.getBoundingClientRect(); + } + } catch (e) { + lazy.logger.error("flushWindow failed", e); + } + } + + if (!afterPaintWasPending && utils.isMozAfterPaintPending) { + anyPendingPaintsGeneratedInDescendants = true; + } + + for (let i = 0; i < win.frames.length; ++i) { + // Skip remote frames, flushRendering will be called on their individual + // MarionetteReftest actor via _recursiveFlushRendering performed from + // the topmost MarionetteReftest actor. + if (!Cu.isRemoteProxy(win.frames[i])) { + flushWindow(win.frames[i]); + } + } + } + flushWindow(this.document.defaultView); + + if ( + anyPendingPaintsGeneratedInDescendants && + !windowUtils.isMozAfterPaintPending + ) { + lazy.logger.error( + "Descendant frame generated a MozAfterPaint event, " + + "but the root document doesn't have one!" + ); + } + } +} |