mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-11 16:32:59 +00:00
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:
parent
aef7a3eaeb
commit
575727cc8b
128
gfx/layers/apz/test/mochitest/FissionTestHelperChild.jsm
Normal file
128
gfx/layers/apz/test/mochitest/FissionTestHelperChild.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
80
gfx/layers/apz/test/mochitest/FissionTestHelperParent.jsm
Normal file
80
gfx/layers/apz/test/mochitest/FissionTestHelperParent.jsm
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
7
gfx/layers/apz/test/mochitest/browser.ini
Normal file
7
gfx/layers/apz/test/mochitest/browser.ini
Normal 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_*.*
|
61
gfx/layers/apz/test/mochitest/browser_test_group_fission.js
Normal file
61
gfx/layers/apz/test/mochitest/browser_test_group_fission.js
Normal 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);
|
||||
}
|
||||
});
|
40
gfx/layers/apz/test/mochitest/helper_fission_basic.html
Normal file
40
gfx/layers/apz/test/mochitest/helper_fission_basic.html
Normal 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>
|
17
gfx/layers/apz/test/mochitest/helper_fission_empty.html
Normal file
17
gfx/layers/apz/test/mochitest/helper_fission_empty.html
Normal 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>
|
70
gfx/layers/apz/test/mochitest/helper_fission_utils.js
Normal file
70
gfx/layers/apz/test/mochitest/helper_fission_utils.js
Normal 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;
|
||||
};
|
||||
}
|
@ -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']
|
||||
|
Loading…
Reference in New Issue
Block a user