summaryrefslogtreecommitdiffstats
path: root/dom/ipc/tests/JSWindowActor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/ipc/tests/JSWindowActor
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/ipc/tests/JSWindowActor')
-rw-r--r--dom/ipc/tests/JSWindowActor/audio.oggbin0 -> 14293 bytes
-rw-r--r--dom/ipc/tests/JSWindowActor/browser.toml32
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_contentWindow.js35
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_crash_report.js114
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_destroy_callbacks.js193
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_event_listener.js171
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_getActor.js36
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_getActor_filter.js259
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_observer_notification.js118
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_process_childid.js27
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_registerWindowActor.js16
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_sendAsyncMessage.js71
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_sendQuery.js131
-rw-r--r--dom/ipc/tests/JSWindowActor/browser_uri_combination.js81
-rw-r--r--dom/ipc/tests/JSWindowActor/file_dummyChromePage.html1
-rw-r--r--dom/ipc/tests/JSWindowActor/file_mediaPlayback.html2
-rw-r--r--dom/ipc/tests/JSWindowActor/head.js82
17 files changed, 1369 insertions, 0 deletions
diff --git a/dom/ipc/tests/JSWindowActor/audio.ogg b/dom/ipc/tests/JSWindowActor/audio.ogg
new file mode 100644
index 0000000000..bed764fbf1
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/audio.ogg
Binary files differ
diff --git a/dom/ipc/tests/JSWindowActor/browser.toml b/dom/ipc/tests/JSWindowActor/browser.toml
new file mode 100644
index 0000000000..a9dc7e8b8f
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser.toml
@@ -0,0 +1,32 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["browser_contentWindow.js"]
+
+["browser_crash_report.js"]
+
+["browser_destroy_callbacks.js"]
+skip-if = ["!debug && os == 'mac'"] #Bug 1604538
+
+["browser_event_listener.js"]
+support-files = ["file_dummyChromePage.html"]
+
+["browser_getActor.js"]
+
+["browser_getActor_filter.js"]
+
+["browser_observer_notification.js"]
+support-files = [
+ "file_mediaPlayback.html",
+ "audio.ogg",
+]
+
+["browser_process_childid.js"]
+
+["browser_registerWindowActor.js"]
+
+["browser_sendAsyncMessage.js"]
+
+["browser_sendQuery.js"]
+
+["browser_uri_combination.js"]
diff --git a/dom/ipc/tests/JSWindowActor/browser_contentWindow.js b/dom/ipc/tests/JSWindowActor/browser_contentWindow.js
new file mode 100644
index 0000000000..e061fd895c
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_contentWindow.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const CONTENT_WINDOW_URL = "https://example.com/";
+
+declTest("contentWindow null when inner window inactive", {
+ matches: [CONTENT_WINDOW_URL + "*"],
+ url: CONTENT_WINDOW_URL + "?1",
+
+ async test(browser) {
+ // If Fission is disabled, the pref is no-op.
+ await SpecialPowers.pushPrefEnv({
+ set: [["fission.bfcacheInParent", true]],
+ });
+
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+
+ await actorParent.sendQuery("storeActor");
+
+ let url = CONTENT_WINDOW_URL + "?2";
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await loaded;
+
+ let result = await actorParent.sendQuery("checkActor");
+
+ is(result.status, "success", "Should succeed when bfcache is enabled");
+ ok(
+ result.valueIsNull,
+ "Should get a null contentWindow when inner window is inactive"
+ );
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_crash_report.js b/dom/ipc/tests/JSWindowActor/browser_crash_report.js
new file mode 100644
index 0000000000..f029f1a85a
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_crash_report.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("crash actor", {
+ allFrames: true,
+
+ async test(browser) {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ ok(true, "Cannot test crash annotations without a crash reporter");
+ return;
+ }
+
+ {
+ info("Creating a new tab.");
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let newTabBrowser = newTab.linkedBrowser;
+
+ let parent =
+ newTabBrowser.browsingContext.currentWindowGlobal.getActor(
+ "TestWindow"
+ );
+ ok(parent, "JSWindowActorParent should have value.");
+
+ await SpecialPowers.spawn(newTabBrowser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ is(
+ child.isInProcess,
+ false,
+ "Actor should be loaded in the content process."
+ );
+ // Make sure that the actor is loaded.
+ let actorChild = child.getActor("TestWindow");
+ is(
+ actorChild.show(),
+ "TestWindowChild",
+ "actor show should have value."
+ );
+ is(
+ actorChild.manager,
+ child,
+ "manager should match WindowGlobalChild."
+ );
+ });
+
+ info(
+ "Crashing from withing an actor. We should have an actor name and a message name."
+ );
+ let report = await BrowserTestUtils.crashFrame(
+ newTabBrowser,
+ /* shouldShowTabCrashPage = */ false,
+ /* shouldClearMinidumps = */ true,
+ /* browsingContext = */ null,
+ { asyncCrash: false }
+ );
+
+ is(report.JSActorName, "BrowserTestUtils");
+ is(report.JSActorMessage, "BrowserTestUtils:CrashFrame");
+
+ BrowserTestUtils.removeTab(newTab);
+ }
+
+ {
+ info("Creating a new tab for async crash");
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let newTabBrowser = newTab.linkedBrowser;
+
+ let parent =
+ newTabBrowser.browsingContext.currentWindowGlobal.getActor(
+ "TestWindow"
+ );
+ ok(parent, "JSWindowActorParent should have value.");
+
+ await SpecialPowers.spawn(newTabBrowser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ is(
+ child.isInProcess,
+ false,
+ "Actor should be loaded in the content process."
+ );
+ // Make sure that the actor is loaded.
+ let actorChild = child.getActor("TestWindow");
+ is(
+ actorChild.show(),
+ "TestWindowChild",
+ "actor show should have value."
+ );
+ is(
+ actorChild.manager,
+ child,
+ "manager should match WindowGlobalChild."
+ );
+ });
+
+ info(
+ "Crashing from without an actor. We should have neither an actor name nor a message name."
+ );
+ let report = await BrowserTestUtils.crashFrame(
+ newTabBrowser,
+ /* shouldShowTabCrashPage = */ false,
+ /* shouldClearMinidumps = */ true,
+ /* browsingContext = */ null,
+ { asyncCrash: true }
+ );
+
+ ok(!report.JSActorName);
+ ok(!report.JSActorMessage);
+
+ BrowserTestUtils.removeTab(newTab);
+ }
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_destroy_callbacks.js b/dom/ipc/tests/JSWindowActor/browser_destroy_callbacks.js
new file mode 100644
index 0000000000..74cbae9415
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_destroy_callbacks.js
@@ -0,0 +1,193 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("destroy actor by iframe remove", {
+ allFrames: true,
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Create and append an iframe into the window's document.
+ let frame = content.document.createElement("iframe");
+ frame.id = "frame";
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+ is(content.window.frames.length, 1, "There should be an iframe.");
+ let child = frame.contentWindow.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+
+ {
+ let error = actorChild.uninitializedGetterError;
+ const prop = "contentWindow";
+ Assert.ok(
+ error,
+ `Should get error accessing '${prop}' before actor initialization`
+ );
+ if (error) {
+ Assert.equal(
+ error.name,
+ "InvalidStateError",
+ "Error should be an InvalidStateError"
+ );
+ Assert.equal(
+ error.message,
+ `JSWindowActorChild.${prop} getter: Cannot access property '${prop}' before actor is initialized`,
+ "Error should have informative message"
+ );
+ }
+ }
+
+ let didDestroyPromise = new Promise(resolve => {
+ const TOPIC = "test-js-window-actor-diddestroy";
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ ok(data, "didDestroyCallback data should be true.");
+ is(subject, actorChild, "Should have this value");
+
+ Services.obs.removeObserver(obs, TOPIC);
+ // Make a trip through the event loop to ensure that the
+ // actor's manager has been cleared before running remaining
+ // checks.
+ Services.tm.dispatchToMainThread(resolve);
+ }, TOPIC);
+ });
+
+ info("Remove frame");
+ content.document.getElementById("frame").remove();
+ await didDestroyPromise;
+
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /InvalidStateError/,
+ "Should throw if frame destroy."
+ );
+
+ for (let prop of [
+ "document",
+ "browsingContext",
+ "docShell",
+ "contentWindow",
+ ]) {
+ let error;
+ try {
+ void actorChild[prop];
+ } catch (e) {
+ error = e;
+ }
+ Assert.ok(
+ error,
+ `Should get error accessing '${prop}' after actor destruction`
+ );
+ if (error) {
+ Assert.equal(
+ error.name,
+ "InvalidStateError",
+ "Error should be an InvalidStateError"
+ );
+ Assert.equal(
+ error.message,
+ `JSWindowActorChild.${prop} getter: Cannot access property '${prop}' after actor 'TestWindow' has been destroyed`,
+ "Error should have informative message"
+ );
+ }
+ }
+ });
+ },
+});
+
+declTest("destroy actor by page navigates", {
+ allFrames: true,
+
+ async test(browser) {
+ info("creating an in-process frame");
+ await SpecialPowers.spawn(browser, [URL], async function (url) {
+ let frame = content.document.createElement("iframe");
+ frame.src = url;
+ content.document.body.appendChild(frame);
+ });
+
+ info("navigating page");
+ await SpecialPowers.spawn(browser, [TEST_URL], async function (url) {
+ let frame = content.document.querySelector("iframe");
+ frame.contentWindow.location = url;
+ let child = frame.contentWindow.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+
+ let didDestroyPromise = new Promise(resolve => {
+ const TOPIC = "test-js-window-actor-diddestroy";
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ ok(data, "didDestroyCallback data should be true.");
+ is(subject, actorChild, "Should have this value");
+
+ Services.obs.removeObserver(obs, TOPIC);
+ resolve();
+ }, TOPIC);
+ });
+
+ await Promise.all([
+ didDestroyPromise,
+ ContentTaskUtils.waitForEvent(frame, "load"),
+ ]);
+
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /InvalidStateError/,
+ "Should throw if frame destroy."
+ );
+ });
+ },
+});
+
+declTest("destroy actor by tab being closed", {
+ allFrames: true,
+
+ async test(browser) {
+ info("creating a new tab");
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL);
+ let newTabBrowser = newTab.linkedBrowser;
+
+ let parent =
+ newTabBrowser.browsingContext.currentWindowGlobal.getActor("TestWindow");
+ ok(parent, "JSWindowActorParent should have value.");
+
+ // We can't depend on `SpecialPowers.spawn` to resolve our promise, as the
+ // frame message manager will be being shut down at the same time. Instead
+ // send messages over the per-process message manager which should still be
+ // active.
+ let didDestroyPromise = new Promise(resolve => {
+ Services.ppmm.addMessageListener(
+ "test-jswindowactor-diddestroy",
+ function onmessage(msg) {
+ Services.ppmm.removeMessageListener(
+ "test-jswindowactor-diddestroy",
+ onmessage
+ );
+ resolve();
+ }
+ );
+ });
+
+ info("setting up destroy listeners");
+ await SpecialPowers.spawn(newTabBrowser, [], () => {
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ if (subject != actorChild) {
+ return;
+ }
+ dump("DidDestroy called\n");
+ Services.obs.removeObserver(obs, "test-js-window-actor-diddestroy");
+ Services.cpmm.sendAsyncMessage("test-jswindowactor-diddestroy", data);
+ }, "test-js-window-actor-diddestroy");
+ });
+
+ info("removing new tab");
+ await BrowserTestUtils.removeTab(newTab);
+ info("waiting for destroy callbacks to fire");
+ await didDestroyPromise;
+ info("got didDestroy callback");
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_event_listener.js b/dom/ipc/tests/JSWindowActor/browser_event_listener.js
new file mode 100644
index 0000000000..725c2c3753
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_event_listener.js
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+async function createAndShowDropdown(browser) {
+ // Add a select element to the DOM of the loaded document.
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.body.innerHTML += `
+ <select id="testSelect">
+ <option>A</option>
+ <option>B</option>
+ </select>`;
+ });
+
+ // Click on the select to show the dropdown.
+ await BrowserTestUtils.synthesizeMouseAtCenter("#testSelect", {}, browser);
+}
+
+declTest("test event triggering actor creation", {
+ events: { mozshowdropdown: {} },
+
+ async test(browser) {
+ // Wait for the observer notification.
+ let observePromise = TestUtils.topicObserved(
+ "test-js-window-actor-parent-event"
+ );
+
+ await createAndShowDropdown(browser);
+
+ // Wait for the observer notification to fire, and inspect the results.
+ let [subject, data] = await observePromise;
+ is(data, "mozshowdropdown");
+
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+ ok(actorParent, "JSWindowActorParent should have value.");
+ is(
+ subject.wrappedJSObject,
+ actorParent,
+ "Should have been recieved on the right actor"
+ );
+ },
+});
+
+declTest("test createActor:false not triggering actor creation", {
+ events: { mozshowdropdown: { createActor: false } },
+
+ async test(browser) {
+ info("before actor has been created");
+ const TOPIC = "test-js-window-actor-parent-event";
+ function obs() {
+ ok(false, "actor should not be created");
+ }
+ Services.obs.addObserver(obs, TOPIC);
+
+ await createAndShowDropdown(browser);
+
+ // Bounce into the content process & create the actor after showing the
+ // dropdown, in order to be sure that if an event was going to be
+ // delivered, it already would've been.
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.windowGlobalChild.getActor("TestWindow");
+ });
+ ok(true, "observer notification should not have fired");
+ Services.obs.removeObserver(obs, TOPIC);
+
+ // ----------------------------------------------------
+ info("after actor has been created");
+ let observePromise = TestUtils.topicObserved(
+ "test-js-window-actor-parent-event"
+ );
+ await createAndShowDropdown(browser);
+
+ // Wait for the observer notification to fire, and inspect the results.
+ let [subject, data] = await observePromise;
+ is(data, "mozshowdropdown");
+
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+ ok(actorParent, "JSWindowActorParent should have value.");
+ is(
+ subject.wrappedJSObject,
+ actorParent,
+ "Should have been recieved on the right actor"
+ );
+ },
+});
+
+async function testEventProcessedOnce(browser, waitForUrl) {
+ let notificationCount = 0;
+ let firstNotificationResolve;
+ let firstNotification = new Promise(resolve => {
+ firstNotificationResolve = resolve;
+ });
+
+ const TOPIC = "test-js-window-actor-parent-event";
+ function obs(subject, topic, data) {
+ is(data, "mozwindowactortestevent");
+ notificationCount++;
+ if (firstNotificationResolve) {
+ firstNotificationResolve();
+ firstNotificationResolve = null;
+ }
+ }
+ Services.obs.addObserver(obs, TOPIC);
+
+ if (waitForUrl) {
+ info("Waiting for URI to be alright");
+ await TestUtils.waitForCondition(() => {
+ if (!browser.browsingContext?.currentWindowGlobal) {
+ info("No CWG yet");
+ return false;
+ }
+ return SpecialPowers.spawn(browser, [waitForUrl], async function (url) {
+ info(content.document.documentURI);
+ return content.document.documentURI.includes(url);
+ });
+ });
+ }
+
+ info("Dispatching event");
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.dispatchEvent(
+ new content.CustomEvent("mozwindowactortestevent", { bubbles: true })
+ );
+ });
+
+ info("Waiting for notification");
+ await firstNotification;
+
+ await new Promise(r => setTimeout(r, 0));
+
+ is(notificationCount, 1, "Should get only one notification");
+
+ Services.obs.removeObserver(obs, TOPIC);
+}
+
+declTest("test in-process content events are not processed twice", {
+ url: "about:preferences",
+ events: { mozwindowactortestevent: { wantUntrusted: true } },
+ async test(browser) {
+ is(
+ browser.getAttribute("type"),
+ "content",
+ "Should be a content <browser>"
+ );
+ is(browser.getAttribute("remotetype"), "", "Should not be remote");
+ await testEventProcessedOnce(browser);
+ },
+});
+
+declTest("test in-process chrome events are processed correctly", {
+ url: "about:blank",
+ events: { mozwindowactortestevent: { wantUntrusted: true } },
+ allFrames: true,
+ includeChrome: true,
+ async test(browser) {
+ let dialogBox = gBrowser.getTabDialogBox(browser);
+ let { dialogClosed, dialog } = dialogBox.open(
+ "chrome://mochitests/content/browser/dom/ipc/tests/JSWindowActor/file_dummyChromePage.html"
+ );
+ let chromeBrowser = dialog._frame;
+ is(chromeBrowser.getAttribute("type"), "", "Should be a chrome <browser>");
+ is(chromeBrowser.getAttribute("remotetype"), "", "Should not be remote");
+
+ await testEventProcessedOnce(chromeBrowser, "dummyChromePage.html");
+
+ dialog.close();
+ await dialogClosed;
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_getActor.js b/dom/ipc/tests/JSWindowActor/browser_getActor.js
new file mode 100644
index 0000000000..8d002daf99
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_getActor.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("getActor on both sides", {
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ ok(parent, "WindowGlobalParent should have value.");
+ let actorParent = parent.getActor("TestWindow");
+ is(actorParent.show(), "TestWindowParent", "actor show should have vaule.");
+ is(actorParent.manager, parent, "manager should match WindowGlobalParent.");
+
+ ok(
+ actorParent.sawActorCreated,
+ "Checking that we can observe parent creation"
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ is(
+ child.isInProcess,
+ false,
+ "Actor should be loaded in the content process."
+ );
+ let actorChild = child.getActor("TestWindow");
+ is(actorChild.show(), "TestWindowChild", "actor show should have vaule.");
+ is(actorChild.manager, child, "manager should match WindowGlobalChild.");
+
+ ok(
+ actorChild.sawActorCreated,
+ "Checking that we can observe child creation"
+ );
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_getActor_filter.js b/dom/ipc/tests/JSWindowActor/browser_getActor_filter.js
new file mode 100644
index 0000000000..a10697c989
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_getActor_filter.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+requestLongerTimeout(2);
+
+declTest("getActor with mismatch", {
+ matches: ["*://*/*"],
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ ok(parent, "WindowGlobalParent should have value.");
+ Assert.throws(
+ () => parent.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if it doesn't match."
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if it doesn't match."
+ );
+ });
+ },
+});
+
+declTest("getActor with matches", {
+ matches: ["*://*/*"],
+ url: TEST_URL,
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ ok(parent.getActor("TestWindow"), "JSWindowActorParent should have value.");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ ok(child.getActor("TestWindow"), "JSWindowActorChild should have value.");
+ });
+ },
+});
+
+declTest("getActor with iframe matches", {
+ allFrames: true,
+ matches: ["*://*/*"],
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [TEST_URL], async function (url) {
+ // Create and append an iframe into the window's document.
+ let frame = content.document.createElement("iframe");
+ frame.src = url;
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+
+ is(content.frames.length, 1, "There should be an iframe.");
+ await content.SpecialPowers.spawn(frame, [], () => {
+ let child = content.windowGlobalChild;
+ Assert.ok(
+ child.getActor("TestWindow"),
+ "JSWindowActorChild should have value."
+ );
+ });
+ });
+ },
+});
+
+declTest("getActor with iframe mismatch", {
+ allFrames: true,
+ matches: ["about:home"],
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [TEST_URL], async function (url) {
+ // Create and append an iframe into the window's document.
+ let frame = content.document.createElement("iframe");
+ frame.src = url;
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+
+ is(content.frames.length, 1, "There should be an iframe.");
+ await content.SpecialPowers.spawn(frame, [], () => {
+ let child = content.windowGlobalChild;
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if it doesn't match."
+ );
+ });
+ });
+ },
+});
+
+declTest("getActor with remoteType match", {
+ remoteTypes: ["web"],
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ ok(parent.getActor("TestWindow"), "JSWindowActorParent should have value.");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ ok(child.getActor("TestWindow"), "JSWindowActorChild should have value.");
+ });
+ },
+});
+
+declTest("getActor with iframe remoteType match", {
+ allFrames: true,
+ remoteTypes: ["web"],
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [TEST_URL], async function (url) {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ ok(child.getActor("TestWindow"), "JSWindowActorChild should have value.");
+
+ // Create and append an iframe into the window's document.
+ let frame = content.document.createElement("iframe");
+ frame.src = url;
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+
+ is(content.frames.length, 1, "There should be an iframe.");
+ await content.SpecialPowers.spawn(frame, [], () => {
+ child = content.windowGlobalChild;
+ Assert.ok(
+ child.getActor("TestWindow"),
+ "JSWindowActorChild should have value."
+ );
+ });
+ });
+ },
+});
+
+declTest("getActor with remoteType mismatch", {
+ remoteTypes: ["privilegedabout"],
+ url: TEST_URL,
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ Assert.throws(
+ () => parent.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if its remoteTypes don't match."
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if its remoteTypes don't match."
+ );
+ });
+ },
+});
+
+declTest("getActor with iframe messageManagerGroups match", {
+ allFrames: true,
+ messageManagerGroups: ["browsers"],
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ ok(parent.getActor("TestWindow"), "JSWindowActorParent should have value.");
+
+ await SpecialPowers.spawn(browser, [TEST_URL], async function (url) {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ ok(child.getActor("TestWindow"), "JSWindowActorChild should have value.");
+ });
+ },
+});
+
+declTest("getActor with iframe messageManagerGroups mismatch", {
+ allFrames: true,
+ messageManagerGroups: ["sidebars"],
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ Assert.throws(
+ () => parent.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if its messageManagerGroups doesn't match."
+ );
+
+ await SpecialPowers.spawn(browser, [TEST_URL], async function (url) {
+ let child = content.windowGlobalChild;
+ ok(child, "WindowGlobalChild should have value.");
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if its messageManagerGroups doesn't match."
+ );
+ });
+ },
+});
+
+declTest("getActor without allFrames", {
+ allFrames: false,
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Create and append an iframe into the window's document.
+ let frame = content.document.createElement("iframe");
+ content.document.body.appendChild(frame);
+ is(content.frames.length, 1, "There should be an iframe.");
+ let child = frame.contentWindow.windowGlobalChild;
+ Assert.throws(
+ () => child.getActor("TestWindow"),
+ /NotSupportedError/,
+ "Should throw if allFrames is false."
+ );
+ });
+ },
+});
+
+declTest("getActor with allFrames", {
+ allFrames: true,
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Create and append an iframe into the window's document.
+ let frame = content.document.createElement("iframe");
+ content.document.body.appendChild(frame);
+ is(content.frames.length, 1, "There should be an iframe.");
+ let child = frame.contentWindow.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+ });
+ },
+});
+
+declTest("getActor without includeChrome", {
+ includeChrome: false,
+
+ async test(_browser, win) {
+ let parent = win.docShell.browsingContext.currentWindowGlobal;
+ SimpleTest.doesThrow(
+ () => parent.getActor("TestWindow"),
+ "Should throw if includeChrome is false."
+ );
+ },
+});
+
+declTest("getActor with includeChrome", {
+ includeChrome: true,
+
+ async test(_browser, win) {
+ let parent = win.docShell.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+ ok(actorParent, "JSWindowActorParent should have value.");
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_observer_notification.js b/dom/ipc/tests/JSWindowActor/browser_observer_notification.js
new file mode 100644
index 0000000000..c0fe2249e8
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_observer_notification.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* eslint-disable no-unused-vars */
+declTest("test observer triggering actor creation", {
+ observers: ["test-js-window-actor-child-observer"],
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const TOPIC = "test-js-window-actor-child-observer";
+ Services.obs.notifyObservers(content.window, TOPIC, "dataString");
+
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+ let { subject, topic, data } = actorChild.lastObserved;
+
+ is(
+ subject.windowGlobalChild.getActor("TestWindow"),
+ actorChild,
+ "Should have been recieved on the right actor"
+ );
+ is(topic, TOPIC, "Topic matches");
+ is(data, "dataString", "Data matches");
+ });
+ },
+});
+
+declTest("test observers with null data", {
+ observers: ["test-js-window-actor-child-observer"],
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const TOPIC = "test-js-window-actor-child-observer";
+ Services.obs.notifyObservers(content.window, TOPIC);
+
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+ let { subject, topic, data } = actorChild.lastObserved;
+
+ is(
+ subject.windowGlobalChild.getActor("TestWindow"),
+ actorChild,
+ "Should have been recieved on the right actor"
+ );
+ is(topic, TOPIC, "Topic matches");
+ is(data, null, "Data matches");
+ });
+ },
+});
+
+declTest("observers don't notify with wrong window", {
+ observers: ["test-js-window-actor-child-observer"],
+
+ async test(browser) {
+ const MSG_RE =
+ /JSWindowActor TestWindow: expected window subject for topic 'test-js-window-actor-child-observer'/;
+ let expectMessage = new Promise(resolve => {
+ Services.console.registerListener(function consoleListener(msg) {
+ // Run everything async in order to avoid logging messages from the
+ // console listener.
+ Cu.dispatch(() => {
+ if (!MSG_RE.test(msg.message)) {
+ info("ignoring non-matching console message: " + msg.message);
+ return;
+ }
+ info("received console message: " + msg.message);
+ is(msg.logLevel, Ci.nsIConsoleMessage.error, "should be an error");
+
+ Services.console.unregisterListener(consoleListener);
+ resolve();
+ });
+ });
+ });
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const TOPIC = "test-js-window-actor-child-observer";
+ Services.obs.notifyObservers(null, TOPIC);
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+ is(
+ actorChild.lastObserved,
+ undefined,
+ "Should not receive wrong window's observer notification!"
+ );
+ });
+
+ await expectMessage;
+ },
+});
+
+declTest("observers notify with audio-playback", {
+ observers: ["audio-playback"],
+ url: "http://example.com/browser/dom/ipc/tests/JSWindowActor/file_mediaPlayback.html",
+
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ let audio = content.document.querySelector("audio");
+ audio.play();
+
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+
+ let observePromise = new Promise(resolve => {
+ actorChild.done = ({ subject, topic, data }) =>
+ resolve({ subject, topic, data });
+ });
+
+ let { subject, topic, data } = await observePromise;
+ is(topic, "audio-playback", "Topic matches");
+ is(data, "active", "Data matches");
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_process_childid.js b/dom/ipc/tests/JSWindowActor/browser_process_childid.js
new file mode 100644
index 0000000000..ba6db1dabb
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_process_childid.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that `process.childID` is defined.
+
+declTest("test childid", {
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ ok(
+ parent.domProcess.childID,
+ "parent domProcess.childID should have a value."
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [parent.domProcess.childID],
+ async function (parentChildID) {
+ ok(
+ ChromeUtils.domProcessChild.childID,
+ "child process.childID should have a value."
+ );
+ let childID = ChromeUtils.domProcessChild.childID;
+ is(parentChildID, childID);
+ }
+ );
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_registerWindowActor.js b/dom/ipc/tests/JSWindowActor/browser_registerWindowActor.js
new file mode 100644
index 0000000000..edaa99d2ed
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_registerWindowActor.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("double register", {
+ async test(_browser, _window, fileExt) {
+ SimpleTest.doesThrow(
+ () =>
+ ChromeUtils.registerWindowActor(
+ "TestWindow",
+ windowActorOptions[fileExt]
+ ),
+ "Should throw if register has duplicate name."
+ );
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_sendAsyncMessage.js b/dom/ipc/tests/JSWindowActor/browser_sendAsyncMessage.js
new file mode 100644
index 0000000000..9ba33a3936
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_sendAsyncMessage.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("asyncMessage testing", {
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+ ok(actorParent, "JSWindowActorParent should have value.");
+
+ await ContentTask.spawn(browser, {}, async function () {
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+
+ let promise = new Promise(resolve => {
+ actorChild.sendAsyncMessage("init", {});
+ actorChild.done = data => resolve(data);
+ }).then(data => {
+ ok(data.initial, "Initial should be true.");
+ ok(data.toParent, "ToParent should be true.");
+ ok(data.toChild, "ToChild should be true.");
+ });
+
+ await promise;
+ });
+ },
+});
+
+declTest("asyncMessage without both sides", {
+ async test(browser) {
+ // If we don't create a parent actor, make sure the parent actor
+ // gets created by having sent the message.
+ await ContentTask.spawn(browser, {}, async function () {
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+ ok(actorChild, "JSWindowActorChild should have value.");
+
+ let promise = new Promise(resolve => {
+ actorChild.sendAsyncMessage("init", {});
+ actorChild.done = data => resolve(data);
+ }).then(data => {
+ ok(data.initial, "Initial should be true.");
+ ok(data.toParent, "ToParent should be true.");
+ ok(data.toChild, "ToChild should be true.");
+ });
+
+ await promise;
+ });
+ },
+});
+
+declTest("asyncMessage can transfer MessagePorts", {
+ async test(browser) {
+ await ContentTask.spawn(browser, {}, async function () {
+ let child = content.windowGlobalChild;
+ let actorChild = child.getActor("TestWindow");
+
+ let { port1, port2 } = new MessageChannel();
+ actorChild.sendAsyncMessage("messagePort", { port: port2 }, [port2]);
+ let reply = await new Promise(resolve => {
+ port1.onmessage = message => {
+ resolve(message.data);
+ port1.close();
+ };
+ });
+
+ is(reply, "Message sent from parent over a MessagePort.");
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_sendQuery.js b/dom/ipc/tests/JSWindowActor/browser_sendQuery.js
new file mode 100644
index 0000000000..0d1a845949
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_sendQuery.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ERROR_LINE_NUMBERS = {
+ jsm: 39,
+ "sys.mjs": 36,
+};
+const EXCEPTION_LINE_NUMBERS = {
+ jsm: ERROR_LINE_NUMBERS.jsm + 3,
+ "sys.mjs": ERROR_LINE_NUMBERS["sys.mjs"] + 3,
+};
+const ERROR_COLUMN_NUMBERS = {
+ jsm: 31,
+ "sys.mjs": 31,
+};
+const EXCEPTION_COLUMN_NUMBERS = {
+ jsm: 22,
+ "sys.mjs": 22,
+};
+
+function maybeAsyncStack(offset, column) {
+ if (
+ Services.prefs.getBoolPref(
+ "javascript.options.asyncstack_capture_debuggee_only"
+ )
+ ) {
+ return "";
+ }
+
+ let stack = Error().stack.replace(/^.*?\n/, "");
+ return (
+ "JSActor query*" +
+ stack.replace(
+ /^([^\n]+?):(\d+):\d+/,
+ (m0, m1, m2) => `${m1}:${+m2 + offset}:${column}`
+ )
+ );
+}
+
+declTest("sendQuery Error", {
+ async test(browser, _window, fileExt) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+
+ let asyncStack = maybeAsyncStack(2, 8);
+ let error = await actorParent
+ .sendQuery("error", { message: "foo" })
+ .catch(e => e);
+
+ is(error.message, "foo", "Error should have the correct message");
+ is(error.name, "SyntaxError", "Error should have the correct name");
+ is(
+ error.stack,
+ `receiveMessage@resource://testing-common/TestWindowChild.${fileExt}:${ERROR_LINE_NUMBERS[fileExt]}:${ERROR_COLUMN_NUMBERS[fileExt]}\n` +
+ asyncStack,
+ "Error should have the correct stack"
+ );
+ },
+});
+
+declTest("sendQuery Exception", {
+ async test(browser, _window, fileExt) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+
+ let asyncStack = maybeAsyncStack(2, 8);
+ let error = await actorParent
+ .sendQuery("exception", {
+ message: "foo",
+ result: Cr.NS_ERROR_INVALID_ARG,
+ })
+ .catch(e => e);
+
+ is(error.message, "foo", "Error should have the correct message");
+ is(
+ error.result,
+ Cr.NS_ERROR_INVALID_ARG,
+ "Error should have the correct result code"
+ );
+ is(
+ error.stack,
+ `receiveMessage@resource://testing-common/TestWindowChild.${fileExt}:${EXCEPTION_LINE_NUMBERS[fileExt]}:${EXCEPTION_COLUMN_NUMBERS[fileExt]}\n` +
+ asyncStack,
+ "Error should have the correct stack"
+ );
+ },
+});
+
+declTest("sendQuery testing", {
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+ ok(actorParent, "JSWindowActorParent should have value.");
+
+ let { result } = await actorParent.sendQuery("asyncAdd", { a: 10, b: 20 });
+ is(result, 30);
+ },
+});
+
+declTest("sendQuery in-process early lifetime", {
+ url: "about:mozilla",
+ allFrames: true,
+
+ async test(browser) {
+ let iframe = browser.contentDocument.createElement("iframe");
+ browser.contentDocument.body.appendChild(iframe);
+ let wgc = iframe.contentWindow.windowGlobalChild;
+ let actorChild = wgc.getActor("TestWindow");
+ let { result } = await actorChild.sendQuery("asyncMul", { a: 10, b: 20 });
+ is(result, 200);
+ },
+});
+
+declTest("sendQuery unserializable reply", {
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal;
+ let actorParent = parent.getActor("TestWindow");
+ ok(actorParent, "JSWindowActorParent should have value");
+
+ try {
+ await actorParent.sendQuery("noncloneReply", {});
+ ok(false, "expected noncloneReply to be rejected");
+ } catch (error) {
+ ok(
+ error.message.includes("message reply cannot be cloned"),
+ "Error should have the correct message"
+ );
+ }
+ },
+});
diff --git a/dom/ipc/tests/JSWindowActor/browser_uri_combination.js b/dom/ipc/tests/JSWindowActor/browser_uri_combination.js
new file mode 100644
index 0000000000..ce46a3e3b6
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/browser_uri_combination.js
@@ -0,0 +1,81 @@
+add_task(function specify_both() {
+ // Specifying both should throw.
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerWindowActor("TestWindow", {
+ parent: {
+ moduleURI: "resource://testing-common/TestWindowParent.jsm",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestWindowChild.jsm",
+ esModuleURI: "resource://testing-common/TestWindowChild.sys.mjs",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerWindowActor("TestWindow", {
+ parent: {
+ esModuleURI: "resource://testing-common/TestWindowParent.sys.mjs",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestWindowChild.jsm",
+ esModuleURI: "resource://testing-common/TestWindowChild.sys.mjs",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerWindowActor("TestWindow", {
+ parent: {
+ moduleURI: "resource://testing-common/TestWindowParent.jsm",
+ esModuleURI: "resource://testing-common/TestWindowParent.sys.mjs",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestWindowChild.jsm",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerWindowActor("TestWindow", {
+ parent: {
+ moduleURI: "resource://testing-common/TestWindowParent.jsm",
+ esModuleURI: "resource://testing-common/TestWindowParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://testing-common/TestWindowChild.sys.mjs",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+});
+
+add_task(function specify_mixed() {
+ // Mixing JSM and ESM should work.
+
+ try {
+ ChromeUtils.registerWindowActor("TestWindow", {
+ parent: {
+ moduleURI: "resource://testing-common/TestWindowParent.jsm",
+ },
+ child: {
+ esModuleURI: "resource://testing-common/TestWindowChild.sys.mjs",
+ },
+ });
+ } finally {
+ ChromeUtils.unregisterWindowActor("TestWindow");
+ }
+
+ try {
+ ChromeUtils.registerWindowActor("TestWindow", {
+ parent: {
+ esModuleURI: "resource://testing-common/TestWindowParent.sys.mjs",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestWindowChild.jsm",
+ },
+ });
+ } finally {
+ ChromeUtils.unregisterWindowActor("TestWindow");
+ }
+});
diff --git a/dom/ipc/tests/JSWindowActor/file_dummyChromePage.html b/dom/ipc/tests/JSWindowActor/file_dummyChromePage.html
new file mode 100644
index 0000000000..c50eddd41f
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/file_dummyChromePage.html
@@ -0,0 +1 @@
+<!doctype html>
diff --git a/dom/ipc/tests/JSWindowActor/file_mediaPlayback.html b/dom/ipc/tests/JSWindowActor/file_mediaPlayback.html
new file mode 100644
index 0000000000..a6979287e2
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/file_mediaPlayback.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<audio src="audio.ogg" controls loop>
diff --git a/dom/ipc/tests/JSWindowActor/head.js b/dom/ipc/tests/JSWindowActor/head.js
new file mode 100644
index 0000000000..cfabd40aac
--- /dev/null
+++ b/dom/ipc/tests/JSWindowActor/head.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provide infrastructure for JSWindowActor tests.
+ */
+
+const URL = "about:blank";
+const TEST_URL = "http://test2.example.org/";
+let windowActorOptions = {
+ jsm: {
+ parent: {
+ moduleURI: "resource://testing-common/TestWindowParent.jsm",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestWindowChild.jsm",
+ },
+ },
+ "sys.mjs": {
+ parent: {
+ esModuleURI: "resource://testing-common/TestWindowParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://testing-common/TestWindowChild.sys.mjs",
+ },
+ },
+};
+
+function declTest(name, cfg) {
+ declTestWithOptions(name, cfg, "jsm");
+ declTestWithOptions(name, cfg, "sys.mjs");
+}
+
+function declTestWithOptions(name, cfg, fileExt) {
+ let {
+ url = "about:blank",
+ allFrames = false,
+ includeChrome = false,
+ matches,
+ remoteTypes,
+ messageManagerGroups,
+ events,
+ observers,
+ test,
+ } = cfg;
+
+ // Build the actor options object which will be used to register & unregister
+ // our window actor.
+ let actorOptions = {
+ parent: { ...windowActorOptions[fileExt].parent },
+ child: { ...windowActorOptions[fileExt].child, events, observers },
+ };
+ actorOptions.allFrames = allFrames;
+ actorOptions.includeChrome = includeChrome;
+ if (matches !== undefined) {
+ actorOptions.matches = matches;
+ }
+ if (remoteTypes !== undefined) {
+ actorOptions.remoteTypes = remoteTypes;
+ }
+ if (messageManagerGroups !== undefined) {
+ actorOptions.messageManagerGroups = messageManagerGroups;
+ }
+
+ // Add a new task for the actor test declared here.
+ add_task(async function () {
+ info("Entering test: " + name);
+
+ // Register our actor, and load a new tab with the relevant URL
+ ChromeUtils.registerWindowActor("TestWindow", actorOptions);
+ try {
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ info("browser ready");
+ await Promise.resolve(test(browser, window, fileExt));
+ });
+ } finally {
+ // Unregister the actor after the test is complete.
+ ChromeUtils.unregisterWindowActor("TestWindow");
+ info("Exiting test: " + name);
+ }
+ });
+}