Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 954aff6

Browse files
Bug 1648444 - [marionette] Use JSWindowActors for marionette reftests navigation r=marionette-reviewers,whimboo,jgraham
The goal is to stop using the listener.js framescript to support navigation in reftests using marionette. Instead we will port the current logic to JSWindowActor. Differential Revision: https://phabricator.services.mozilla.com/D92648
1 parent 637f9ed commit 954aff6

5 files changed

Lines changed: 288 additions & 145 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
"use strict";
6+
7+
const EXPORTED_SYMBOLS = ["MarionetteReftestFrameChild"];
8+
9+
const { XPCOMUtils } = ChromeUtils.import(
10+
"resource://gre/modules/XPCOMUtils.jsm"
11+
);
12+
13+
XPCOMUtils.defineLazyModuleGetters(this, {
14+
Log: "chrome://marionette/content/log.js",
15+
});
16+
17+
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
18+
19+
/**
20+
* Child JSWindowActor to handle navigation for reftests relying on marionette.
21+
*/
22+
class MarionetteReftestFrameChild extends JSWindowActorChild {
23+
constructor() {
24+
super();
25+
26+
// This promise will resolve with the URL recorded in the "load" event
27+
// handler. This URL will not be impacted by any hash modification that
28+
// might be performed by the test script.
29+
// The harness should be loaded before loading any test page, so the actors
30+
// should be registered before the "load" event is received for a test page.
31+
this._loadedURLPromise = new Promise(
32+
r => (this._resolveLoadedURLPromise = r)
33+
);
34+
}
35+
36+
handleEvent(event) {
37+
if (event.type == "load") {
38+
const url = event.target.location.href;
39+
logger.debug(`Handle load event with URL ${url}`);
40+
this._resolveLoadedURLPromise(url);
41+
}
42+
}
43+
44+
actorCreated() {
45+
logger.trace(
46+
`[${this.browsingContext.id}] Reftest Child actor created for window id ${this.manager.innerWindowId}`
47+
);
48+
}
49+
50+
async receiveMessage(msg) {
51+
const { name, data } = msg;
52+
53+
let result;
54+
switch (name) {
55+
case "MarionetteReftestFrameParent:reftestWait":
56+
result = await this.reftestWait(data);
57+
break;
58+
}
59+
return result;
60+
}
61+
62+
/**
63+
* Wait for a reftest page to be ready for screenshots:
64+
* - wait for the loadedURL to be available (see handleEvent)
65+
* - check if the URL matches the expected URL
66+
* - if present, wait for the "reftest-wait" classname to be removed from the
67+
* document element
68+
*
69+
* @param {Object} options
70+
* @param {String} options.url
71+
* The expected test page URL
72+
* @param {Boolean} options.useRemote
73+
* True when using e10s
74+
* @return {Boolean}
75+
* Returns true when the correct page is loaded and ready for
76+
* screenshots. Returns false if the page loaded bug does not have the
77+
* expected URL.
78+
*/
79+
async reftestWait(options = {}) {
80+
const { url, useRemote } = options;
81+
const loadedURL = await this._loadedURLPromise;
82+
if (loadedURL !== url) {
83+
logger.debug(
84+
`Window URL does not match the expected URL "${loadedURL}" !== "${url}"`
85+
);
86+
return false;
87+
}
88+
89+
const documentElement = this.document.documentElement;
90+
const hasReftestWait = documentElement.classList.contains("reftest-wait");
91+
92+
logger.debug("Waiting for event loop to spin");
93+
await new Promise(resolve => this.contentWindow.setTimeout(resolve, 0));
94+
95+
await this.paintComplete(useRemote);
96+
97+
if (hasReftestWait) {
98+
const event = new Event("TestRendered", { bubbles: true });
99+
documentElement.dispatchEvent(event);
100+
logger.info("Emitted TestRendered event");
101+
await this.reftestWaitRemoved();
102+
await this.paintComplete(useRemote);
103+
}
104+
if (
105+
this.contentWindow.innerWidth < documentElement.scrollWidth ||
106+
this.contentWindow.innerHeight < documentElement.scrollHeight
107+
) {
108+
logger.warn(
109+
`${url} overflows viewport (width: ${documentElement.scrollWidth}, height: ${documentElement.scrollHeight})`
110+
);
111+
}
112+
return true;
113+
}
114+
115+
paintComplete(useRemote) {
116+
logger.debug("Waiting for rendering");
117+
let windowUtils = this.contentWindow.windowUtils;
118+
return new Promise(resolve => {
119+
let maybeResolve = () => {
120+
this.flushRendering();
121+
if (useRemote) {
122+
// Flush display (paint)
123+
logger.debug("Force update of layer tree");
124+
windowUtils.updateLayerTree();
125+
}
126+
127+
if (windowUtils.isMozAfterPaintPending) {
128+
logger.debug("isMozAfterPaintPending: true");
129+
this.contentWindow.addEventListener("MozAfterPaint", maybeResolve, {
130+
once: true,
131+
});
132+
} else {
133+
// resolve at the start of the next frame in case of leftover paints
134+
logger.debug("isMozAfterPaintPending: false");
135+
this.contentWindow.requestAnimationFrame(() => {
136+
this.contentWindow.requestAnimationFrame(resolve);
137+
});
138+
}
139+
};
140+
maybeResolve();
141+
});
142+
}
143+
144+
reftestWaitRemoved() {
145+
logger.debug("Waiting for reftest-wait removal");
146+
return new Promise(resolve => {
147+
const documentElement = this.document.documentElement;
148+
let observer = new this.contentWindow.MutationObserver(() => {
149+
if (!documentElement.classList.contains("reftest-wait")) {
150+
observer.disconnect();
151+
logger.debug("reftest-wait removed");
152+
this.contentWindow.setTimeout(resolve, 0);
153+
}
154+
});
155+
if (documentElement.classList.contains("reftest-wait")) {
156+
observer.observe(documentElement, { attributes: true });
157+
} else {
158+
this.contentWindow.setTimeout(resolve, 0);
159+
}
160+
});
161+
}
162+
163+
flushRendering() {
164+
let anyPendingPaintsGeneratedInDescendants = false;
165+
166+
let windowUtils = this.contentWindow.windowUtils;
167+
168+
function flushWindow(win) {
169+
let utils = win.windowUtils;
170+
let afterPaintWasPending = utils.isMozAfterPaintPending;
171+
172+
let root = win.document.documentElement;
173+
if (root) {
174+
try {
175+
// Flush pending restyles and reflows for this window (layout)
176+
root.getBoundingClientRect();
177+
} catch (e) {
178+
logger.error("flushWindow failed", e);
179+
}
180+
}
181+
182+
if (!afterPaintWasPending && utils.isMozAfterPaintPending) {
183+
anyPendingPaintsGeneratedInDescendants = true;
184+
}
185+
186+
for (let i = 0; i < win.frames.length; ++i) {
187+
flushWindow(win.frames[i]);
188+
}
189+
}
190+
flushWindow(this.contentWindow);
191+
192+
if (
193+
anyPendingPaintsGeneratedInDescendants &&
194+
!windowUtils.isMozAfterPaintPending
195+
) {
196+
logger.error(
197+
"Descendant frame generated a MozAfterPaint event, " +
198+
"but the root document doesn't have one!"
199+
);
200+
}
201+
}
202+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
("use strict");
6+
7+
const EXPORTED_SYMBOLS = ["MarionetteReftestFrameParent"];
8+
9+
const { XPCOMUtils } = ChromeUtils.import(
10+
"resource://gre/modules/XPCOMUtils.jsm"
11+
);
12+
13+
XPCOMUtils.defineLazyModuleGetters(this, {
14+
Log: "chrome://marionette/content/log.js",
15+
});
16+
17+
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
18+
19+
/**
20+
* Parent JSWindowActor to handle navigation for reftests relying on marionette.
21+
*/
22+
class MarionetteReftestFrameParent extends JSWindowActorParent {
23+
actorCreated() {
24+
logger.trace(`[${this.browsingContext.id}] Reftest Parent actor created`);
25+
}
26+
27+
/**
28+
* Wait for the expected URL to be loaded.
29+
*
30+
* @param {String} url
31+
* The expected url.
32+
* @param {Boolean} useRemote
33+
* True if tests are running with e10s.
34+
* @return {Boolean} true if the page is fully loaded with the expected url,
35+
* false otherwise.
36+
*/
37+
async reftestWait(url, useRemote) {
38+
try {
39+
const isCorrectUrl = await this.sendQuery(
40+
"MarionetteReftestFrameParent:reftestWait",
41+
{
42+
url,
43+
useRemote,
44+
}
45+
);
46+
return isCorrectUrl;
47+
} catch (e) {
48+
if (e.name === "AbortError") {
49+
// If the query is aborted, the window global is being destroyed, most
50+
// likely because a navigation happened.
51+
return false;
52+
}
53+
54+
// Other errors should not be swallowed.
55+
throw e;
56+
}
57+
}
58+
}

testing/marionette/jar.mn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ marionette.jar:
88
content/action.js (action.js)
99
content/actors/MarionetteFrameChild.jsm (actors/MarionetteFrameChild.jsm)
1010
content/actors/MarionetteFrameParent.jsm (actors/MarionetteFrameParent.jsm)
11+
content/actors/MarionetteReftestFrameChild.jsm (actors/MarionetteReftestFrameChild.jsm)
12+
content/actors/MarionetteReftestFrameParent.jsm (actors/MarionetteReftestFrameParent.jsm)
1113
content/addon.js (addon.js)
1214
content/assert.js (assert.js)
1315
content/atom.js (atom.js)

0 commit comments

Comments
 (0)