Bug 1550467 - Add a basic browser-mochitest for testing APZ+fission codepaths. r=botond,nika

This introduces the framework and helpers needed to do this kind of testing,
and adds a basic sanity test that ensures some basic functionality.

Differential Revision: https://phabricator.services.mozilla.com/D32187

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Kartikaya Gupta 2019-06-06 20:49:34 +00:00
parent aef7a3eaeb
commit 575727cc8b
9 changed files with 421 additions and 0 deletions

View File

@ -0,0 +1,128 @@
var EXPORTED_SYMBOLS = ["FissionTestHelperChild"];
// This code runs in the content process that holds the window to which
// this actor is attached. There is one instance of this class for each
// "inner window" (i.e. one per content document, including iframes/nested
// iframes).
// There is a 1:1 relationship between instances of this class and
// FissionTestHelperParent instances, and the pair are entangled such
// that they can communicate with each other regardless of which process
// they live in.
class FissionTestHelperChild extends JSWindowActorChild {
constructor() {
super();
this._msgCounter = 0;
this._oopifResponsePromiseResolvers = [];
}
cw() {
return this.contentWindow.wrappedJSObject;
}
initialize() {
// This exports a bunch of things into the content window so that
// the test can access them. Most things are scoped inside the
// FissionTestHelper object on the window to avoid polluting the global
// namespace.
let cw = this.cw();
Cu.exportFunction((cond, msg) => this.sendAsyncMessage("ok", {cond, msg}),
cw, { defineAs: "ok" });
let FissionTestHelper = Cu.createObjectIn(cw, { defineAs: "FissionTestHelper" });
FissionTestHelper.startTestPromise =
new cw.Promise(
Cu.exportFunction(
(resolve) => {
this._startTestPromiseResolver = resolve;
},
cw));
Cu.exportFunction(this.subtestDone.bind(this),
FissionTestHelper, { defineAs: "subtestDone" });
Cu.exportFunction(this.sendToOopif.bind(this),
FissionTestHelper, { defineAs: "sendToOopif" });
Cu.exportFunction(this.fireEventInEmbedder.bind(this),
FissionTestHelper, { defineAs: "fireEventInEmbedder" });
}
// Called by the subtest to indicate completion to the top-level browser-chrome
// mochitest.
subtestDone() {
let cw = this.cw();
if (cw.ApzCleanup) {
cw.ApzCleanup.execute();
}
this.sendAsyncMessage("Test:Complete", {});
}
// Called by the subtest to eval some code in the OOP iframe. This returns
// a promise that resolves to the return value from the eval.
sendToOopif(iframeElement, stringToEval) {
let browsingContextId = iframeElement.browsingContext.id;
let msgId = ++this._msgCounter;
let cw = this.cw();
let responsePromise = new cw.Promise(
Cu.exportFunction(
(resolve) => {
this._oopifResponsePromiseResolvers[msgId] = resolve;
},
cw));
this.sendAsyncMessage("EmbedderToOopif", {browsingContextId, msgId, stringToEval});
return responsePromise;
}
// Called by OOP iframes to dispatch an event in the embedder window. This
// can be used by the OOP iframe to asynchronously notify the embedder of
// things that happen. The embedder can use promiseOneEvent from
// helper_fission_utils.js to listen for these events.
fireEventInEmbedder(eventType, data) {
this.sendAsyncMessage("OopifToEmbedder", {eventType, data});
}
handleEvent(evt) {
switch (evt.type) {
case "DOMWindowCreated":
// Defer real initialization to when the FissionTestHelper:Init event
// is fired by the content. See comments in fission_subtest_init().
// Once bug 1557486 is fixed we can just register the FissionTestHelper:Init
// event directly instead of DOMWindowCreated.
this.contentWindow.addEventListener("FissionTestHelper:Init", this, { wantUntrusted: true });
break;
case "FissionTestHelper:Init":
this.initialize();
break;
}
}
receiveMessage(msg) {
switch (msg.name) {
case "Test:Start":
this._startTestPromiseResolver();
delete this._startTestPromiseResolver;
break;
case "FromEmbedder":
let evalResult = this.contentWindow.eval(msg.data.stringToEval);
this.sendAsyncMessage("OopifToEmbedder", {msgId: msg.data.msgId, evalResult});
break;
case "FromOopif":
if (typeof msg.data.msgId == "number") {
if (!(msg.data.msgId in this._oopifResponsePromiseResolvers)) {
dump("Error: FromOopif got a message with unknown numeric msgId in " + this.contentWindow.location.href + "\n");
}
this._oopifResponsePromiseResolvers[msg.data.msgId](msg.data.evalResult);
delete this._oopifResponsePromiseResolvers[msg.data.msgId];
} else if (typeof msg.data.eventType == "string") {
let cw = this.cw();
let event = new cw.Event(msg.data.eventType);
event.data = Cu.cloneInto(msg.data.data, cw);
this.contentWindow.dispatchEvent(event);
} else {
dump("Warning: Unrecognized FromOopif message received in " + this.contentWindow.location.href + "\n");
}
break;
}
}
}

View File

@ -0,0 +1,80 @@
var EXPORTED_SYMBOLS = ["FissionTestHelperParent"];
// This code always runs in the parent process. There is one instance of
// this class for each "inner window" (should be one per content document,
// including iframes/nested iframes).
// There is a 1:1 relationship between instances of this class and
// FissionTestHelperChild instances, and the pair are entangled such
// that they can communicate with each other regardless of which process
// they live in.
class FissionTestHelperParent extends JSWindowActorParent {
constructor() {
super();
this._testCompletePromise = new Promise((resolve) => {
this._testCompletePromiseResolver = resolve;
});
}
embedderWindow() {
let embedder = this.manager.browsingContext.embedderWindowGlobal;
// embedder is of type WindowGlobalParent, defined in WindowGlobalActors.webidl
if (!embedder) {
dump("ERROR: no embedder found in FissionTestHelperParent\n");
}
return embedder;
}
docURI() {
return this.manager.documentURI.spec;
}
// Returns a promise that is resolved when this parent actor receives a
// "Test:Complete" message from the child.
getTestCompletePromise() {
return this._testCompletePromise;
}
startTest() {
this.sendAsyncMessage("Test:Start", {});
}
receiveMessage(msg) {
switch (msg.name) {
case "ok":
FissionTestHelperParent.SimpleTest.ok(msg.data.cond, this.docURI() + " | " + msg.data.msg);
break;
case "Test:Complete":
this._testCompletePromiseResolver();
break;
case "EmbedderToOopif":
// This relays messages from the embedder to an OOP-iframe. The browsing
// context id in the message data identifies the OOP-iframe.
let oopifBrowsingContext = BrowsingContext.get(msg.data.browsingContextId);
if (oopifBrowsingContext == null) {
FissionTestHelperParent.SimpleTest.ok(false, "EmbedderToOopif couldn't find oopif");
break;
}
let oopifActor = oopifBrowsingContext.currentWindowGlobal.getActor("FissionTestHelper");
if (!oopifActor) {
FissionTestHelperParent.SimpleTest.ok(false, "EmbedderToOopif couldn't find oopif actor");
break;
}
oopifActor.sendAsyncMessage("FromEmbedder", msg.data);
break;
case "OopifToEmbedder":
// This relays messages from the OOP-iframe to the top-level content
// window which is embedding it.
let embedderActor = this.embedderWindow().getActor("FissionTestHelper");
if (!embedderActor) {
FissionTestHelperParent.SimpleTest.ok(false, "OopifToEmbedder couldn't find embedder");
break;
}
embedderActor.sendAsyncMessage("FromOopif", msg.data);
break;
}
}
}

View File

@ -516,6 +516,23 @@ function runContinuation(testFunction) {
};
}
// Same as runContinuation, except it takes an async generator, and doesn't
// invoke it with any callback, since the generator doesn't need one.
function runAsyncContinuation(testFunction) {
return async function() {
var asyncContinuation = testFunction();
try {
var ret = await asyncContinuation.next();
while (!ret.done) {
ret = await asyncContinuation.next();
}
} catch (ex) {
SimpleTest.ok(false, "APZ async test continuation failed with exception: " + ex);
throw ex;
}
};
}
// Take a snapshot of the given rect, *including compositor transforms* (i.e.
// includes async scroll transforms applied by APZ). If you don't need the
// compositor transforms, you can probably get away with using

View File

@ -0,0 +1,7 @@
[browser_test_group_fission.js]
support-files =
apz_test_native_event_utils.js
apz_test_utils.js
FissionTestHelperParent.jsm
FissionTestHelperChild.jsm
helper_fission_*.*

View File

@ -0,0 +1,61 @@
add_task(async function test_main() {
function httpURL(filename) {
let chromeURL = getRootDirectory(gTestPath) + filename;
return chromeURL.replace("chrome://mochitests/content/", "http://mochi.test:8888/");
}
// Each of these URLs will get opened in a new top-level browser window that
// is fission-enabled.
var test_urls = [
httpURL("helper_fission_basic.html", null),
// add additional tests here
];
let fissionWindow = await BrowserTestUtils.openNewBrowserWindow({fission: true});
// We import the JSM here so that we can install functions on the class
// below.
const {FissionTestHelperParent} = ChromeUtils.import(
getRootDirectory(gTestPath) + "FissionTestHelperParent.jsm");
FissionTestHelperParent.SimpleTest = SimpleTest;
ChromeUtils.registerWindowActor("FissionTestHelper", {
parent: {
moduleURI: getRootDirectory(gTestPath) + "FissionTestHelperParent.jsm",
},
child: {
moduleURI: getRootDirectory(gTestPath) + "FissionTestHelperChild.jsm",
events: {
"DOMWindowCreated": {},
},
},
allFrames: true,
});
try {
for (var url of test_urls) {
dump(`Starting test ${url}\n`);
// Load the test URL and tell it to get started, and wait until it reports
// completion.
await BrowserTestUtils.withNewTab(
{gBrowser: fissionWindow.gBrowser, url},
async (browser) => {
let tabActor = browser.browsingContext.currentWindowGlobal.getActor("FissionTestHelper");
let donePromise = tabActor.getTestCompletePromise();
tabActor.startTest();
await donePromise;
});
dump(`Finished test ${url}\n`);
}
} finally {
// Delete stuff we added to FissionTestHelperParent, beacuse the object will
// outlive this test, and leaving stuff on it may leak the things reachable
// from it.
delete FissionTestHelperParent.SimpleTest;
// Teardown
ChromeUtils.unregisterWindowActor("FissionTestHelper");
await BrowserTestUtils.closeWindow(fissionWindow);
}
});

View File

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Basic sanity test that runs inside a fission-enabled window</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/paint_listener.js"></script>
<script src="helper_fission_utils.js"></script>
<script src="apz_test_utils.js"></script>
<script>
fission_subtest_init();
FissionTestHelper.startTestPromise
.then(waitUntilApzStable)
.then(loadOOPIFrame("testframe", "helper_fission_empty.html"))
.then(waitUntilApzStable)
.then(runAsyncContinuation(test))
.then(FissionTestHelper.subtestDone, FissionTestHelper.subtestDone);
// The actual test
async function* test() {
let iframeElement = document.getElementById("testframe");
ok(SpecialPowers.wrap(window)
.docShell
.QueryInterface(SpecialPowers.Ci.nsILoadContext)
.useRemoteSubframes,
"OOP iframe is actually OOP");
let iframeResult = await FissionTestHelper.sendToOopif(iframeElement, "20 + 22");
ok(iframeResult == 42, "Basic content fission test works");
}
</script>
</head>
<body>
<iframe id="testframe"></iframe>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<script>
// This is an empty document that serves as a OOPIF content document that be
// reused by different fission subtests. The subtest can eval stuff in this
// document using the sendToOopif helper and thereby populate this document
// with whatever is needed. This allows the subtest to more "contained" in a
// single file and avoids having to create new dummy files for each subtest.
function loaded() {
window.dispatchEvent(new Event("FissionTestHelper:Init"));
FissionTestHelper.fireEventInEmbedder("OOPIF:Load", {content: window.location.href});
}
</script>
<body onload="loaded()">
</body>
</html>

View File

@ -0,0 +1,70 @@
function fission_subtest_init() {
// Silence SimpleTest warning about missing assertions by having it wait
// indefinitely. We don't need to give it an explicit finish because the
// entire window this test runs in will be closed after subtestDone is called.
SimpleTest.waitForExplicitFinish();
// This is the point at which we inject the ok, is, subtestDone, etc. functions
// into this window. In particular this function should run after SimpleTest.js
// is imported, otherwise SimpleTest.js will clobber the functions with its
// own versions. This is implicitly enforced because if we call this function
// before SimpleTest.js is imported, the above line will throw an exception.
window.dispatchEvent(new Event("FissionTestHelper:Init"));
}
/**
* Returns a promise that will resolve if the `window` receives an event of the
* given type that passes the given filter. Only the first matching message is
* used. The filter must be a function (or null); it is called with the event
* object and the call must return true to resolve the promise.
*/
function promiseOneEvent(eventType, filter) {
return new Promise((resolve, reject) => {
window.addEventListener(eventType, function listener(e) {
let success = false;
if (filter == null) {
success = true;
} else if (typeof filter == "function") {
try {
success = filter(e);
} catch (ex) {
dump(`ERROR: Filter passed to promiseOneEvent threw exception: ${ex}\n`);
reject();
return;
}
} else {
dump("ERROR: Filter passed to promiseOneEvent was neither null nor a function\n");
reject();
return;
}
if (success) {
window.removeEventListener(eventType, listener);
resolve(e);
}
});
});
}
/**
* Starts loading the given `iframePage` in the iframe element with the given
* id, and waits for it to load.
* Note that calling this function doesn't do the load directly; instead it
* returns an async function which can be added to a thenable chain.
*/
function loadOOPIFrame(iframeElementId, iframePage) {
return async function() {
if (window.location.href.startsWith("https://example.com/")) {
dump(`WARNING: Calling loadOOPIFrame from ${window.location.href} so the iframe may not be OOP\n`);
ok(false, "Current origin is not example.com:443");
}
let url = "https://example.com/browser/gfx/layers/apz/test/mochitest/" + iframePage;
let loadPromise = promiseOneEvent("OOPIF:Load", function(e) {
return (typeof e.data.content) == "string" &&
e.data.content == url;
});
let elem = document.getElementById(iframeElementId);
elem.src = url;
await loadPromise;
};
}

View File

@ -587,6 +587,7 @@ if CONFIG['ENABLE_TESTS']:
MOCHITEST_MANIFESTS += ['apz/test/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['apz/test/mochitest/chrome.ini']
BROWSER_CHROME_MANIFESTS += ['apz/test/mochitest/browser.ini']
CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS']
CXXFLAGS += CONFIG['TK_CFLAGS']