Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
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
* 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!"
);
}
}
}