summaryrefslogtreecommitdiffstats
path: root/remote/marionette/actors/MarionetteReftestChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/actors/MarionetteReftestChild.sys.mjs')
-rw-r--r--remote/marionette/actors/MarionetteReftestChild.sys.mjs239
1 files changed, 239 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..fd73f88a88
--- /dev/null
+++ b/remote/marionette/actors/MarionetteReftestChild.sys.mjs
@@ -0,0 +1,239 @@
+/* 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 lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+ChromeUtils.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
+ * @param {boolean} options.warnOnOverflow
+ * True if we should check the content fits in the viewport.
+ * This isn't necessary for print reftests where we will render the full
+ * size of the paginated content.
+ * @returns {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 => lazy.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 (
+ options.warnOnOverflow &&
+ (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");
+ lazy.setTimeout(resolve, 0);
+ }
+ });
+ if (documentElement.classList.contains("reftest-wait")) {
+ observer.observe(documentElement, { attributes: true });
+ } else {
+ lazy.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!"
+ );
+ }
+ }
+}