summaryrefslogtreecommitdiffstats
path: root/dom/ipc/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/ipc/tests/JSProcessActor/browser.toml18
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_devtools_loader.js57
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_getActor.js47
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_getActor_filter.js80
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_observer_notification.js41
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_registerProcessActor.js16
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_sendAsyncMessage.js51
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_sendQuery.js99
-rw-r--r--dom/ipc/tests/JSProcessActor/browser_uri_combination.js81
-rw-r--r--dom/ipc/tests/JSProcessActor/head.js88
-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
-rw-r--r--dom/ipc/tests/blob_verify.sjs26
-rw-r--r--dom/ipc/tests/browser.toml90
-rw-r--r--dom/ipc/tests/browser_CrashService_crash.js72
-rw-r--r--dom/ipc/tests/browser_ProcessPriorityManager.js963
-rw-r--r--dom/ipc/tests/browser_bug1646088.js71
-rw-r--r--dom/ipc/tests/browser_bug1686194.js47
-rw-r--r--dom/ipc/tests/browser_cancel_content_js.js65
-rw-r--r--dom/ipc/tests/browser_child_clipboard_restricted.js93
-rw-r--r--dom/ipc/tests/browser_content_shutdown_with_endless_js.js86
-rw-r--r--dom/ipc/tests/browser_crash_oopiframe.js245
-rw-r--r--dom/ipc/tests/browser_domainPolicy.js187
-rw-r--r--dom/ipc/tests/browser_gc_schedule.js379
-rw-r--r--dom/ipc/tests/browser_hide_tooltip.js38
-rw-r--r--dom/ipc/tests/browser_layers_unloaded_while_interruptingJS.js32
-rw-r--r--dom/ipc/tests/browser_memory_distribution_telemetry.js93
-rw-r--r--dom/ipc/tests/browser_pbrowser_creation_failure.js57
-rw-r--r--dom/ipc/tests/browser_subframesPreferUsed.js82
-rw-r--r--dom/ipc/tests/browser_very_fission.js38
-rw-r--r--dom/ipc/tests/browser_wpi_base.js305
-rw-r--r--dom/ipc/tests/browser_wpi_isolate_everything.js27
-rw-r--r--dom/ipc/tests/browser_wpi_isolate_high_value.js27
-rw-r--r--dom/ipc/tests/browser_wpi_isolate_nothing.js27
-rw-r--r--dom/ipc/tests/chrome.toml6
-rw-r--r--dom/ipc/tests/file_broadcast_currenturi_onload.html66
-rw-r--r--dom/ipc/tests/file_cancel_content_js.html18
-rw-r--r--dom/ipc/tests/file_cross_frame.html12
-rw-r--r--dom/ipc/tests/file_disableScript.html11
-rw-r--r--dom/ipc/tests/file_domainPolicy_base.html8
-rw-r--r--dom/ipc/tests/file_dummy.html7
-rw-r--r--dom/ipc/tests/file_endless_js.html17
-rw-r--r--dom/ipc/tests/mochitest.toml30
-rw-r--r--dom/ipc/tests/process_error.xhtml60
-rw-r--r--dom/ipc/tests/test_Preallocated.html51
-rw-r--r--dom/ipc/tests/test_bcg_processes.html45
-rw-r--r--dom/ipc/tests/test_blob_sliced_from_child_process.js140
-rw-r--r--dom/ipc/tests/test_blob_sliced_from_parent_process.js167
-rw-r--r--dom/ipc/tests/test_browsingcontext_currenturi.html131
-rw-r--r--dom/ipc/tests/test_bug1086684.js99
-rw-r--r--dom/ipc/tests/test_child_docshell.js90
-rw-r--r--dom/ipc/tests/test_process_error.xhtml22
-rw-r--r--dom/ipc/tests/test_sharedMap.js377
-rw-r--r--dom/ipc/tests/test_temporaryfile_stream.html84
-rw-r--r--dom/ipc/tests/test_window_open_discarded_bc.html40
-rw-r--r--dom/ipc/tests/xpcshell.toml16
71 files changed, 6494 insertions, 0 deletions
diff --git a/dom/ipc/tests/JSProcessActor/browser.toml b/dom/ipc/tests/JSProcessActor/browser.toml
new file mode 100644
index 0000000000..807b54dd81
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser.toml
@@ -0,0 +1,18 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["browser_devtools_loader.js"]
+
+["browser_getActor.js"]
+
+["browser_getActor_filter.js"]
+
+["browser_observer_notification.js"]
+
+["browser_registerProcessActor.js"]
+
+["browser_sendAsyncMessage.js"]
+
+["browser_sendQuery.js"]
+
+["browser_uri_combination.js"]
diff --git a/dom/ipc/tests/JSProcessActor/browser_devtools_loader.js b/dom/ipc/tests/JSProcessActor/browser_devtools_loader.js
new file mode 100644
index 0000000000..0d2699c5a9
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_devtools_loader.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("getActor in the regular shared loader", {
+ loadInDevToolsLoader: false,
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal.domProcess;
+ let parentActor = parent.getActor("TestProcessActor");
+ ok(parentActor, "JSProcessActorParent should have value.");
+ is(
+ Cu.getRealmLocation(Cu.getGlobalForObject(parentActor)),
+ "shared JSM global",
+ "The JSActor module in the parent process should be loaded in the shared global"
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = ChromeUtils.domProcessChild;
+ ok(child, "DOMProcessChild should have value.");
+ let childActor = child.getActor("TestProcessActor");
+ ok(childActor, "JSProcessActorChild should have value.");
+ is(
+ Cu.getRealmLocation(Cu.getGlobalForObject(childActor)),
+ "shared JSM global",
+ "The JSActor module in the child process should be loaded in the shared global"
+ );
+ });
+ },
+});
+
+declTest("getActor in the distinct DevTools loader", {
+ loadInDevToolsLoader: true,
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal.domProcess;
+ let parentActor = parent.getActor("TestProcessActor");
+ ok(parentActor, "JSProcessActorParent should have value.");
+ is(
+ Cu.getRealmLocation(Cu.getGlobalForObject(parentActor)),
+ "DevTools global",
+ "The JSActor module in the parent process should be loaded in the distinct DevTools global"
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = ChromeUtils.domProcessChild;
+ ok(child, "DOMProcessChild should have value.");
+ let childActor = child.getActor("TestProcessActor");
+ ok(childActor, "JSProcessActorChild should have value.");
+ is(
+ Cu.getRealmLocation(Cu.getGlobalForObject(childActor)),
+ "DevTools global",
+ "The JSActor module in the child process should be loaded in the distinct DevTools global"
+ );
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_getActor.js b/dom/ipc/tests/JSProcessActor/browser_getActor.js
new file mode 100644
index 0000000000..e111f1a274
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_getActor.js
@@ -0,0 +1,47 @@
+/* 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.domProcess;
+ ok(parent, "WindowGlobalParent should have value.");
+ let actorParent = parent.getActor("TestProcessActor");
+ is(
+ actorParent.show(),
+ "TestProcessActorParent",
+ "actor `show` should have value."
+ );
+ is(
+ actorParent.manager,
+ parent,
+ "manager should match WindowGlobalParent.domProcess"
+ );
+
+ ok(
+ actorParent.sawActorCreated,
+ "Checking that we can observe parent creation"
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = ChromeUtils.domProcessChild;
+ ok(child, "WindowGlobalChild should have value.");
+ let actorChild = child.getActor("TestProcessActor");
+ is(
+ actorChild.show(),
+ "TestProcessActorChild",
+ "actor show should have vaule."
+ );
+ is(
+ actorChild.manager,
+ child,
+ "manager should match ChromeUtils.domProcessChild."
+ );
+
+ ok(
+ actorChild.sawActorCreated,
+ "Checking that we can observe child creation"
+ );
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_getActor_filter.js b/dom/ipc/tests/JSProcessActor/browser_getActor_filter.js
new file mode 100644
index 0000000000..f79340fd89
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_getActor_filter.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+declTest("getActor with remoteType match", {
+ remoteTypes: ["web"],
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal.domProcess;
+ ok(
+ parent.getActor("TestProcessActor"),
+ "JSProcessActorParent should have value."
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = ChromeUtils.domProcessChild;
+ ok(child, "DOMProcessChild should have value.");
+ ok(
+ child.getActor("TestProcessActor"),
+ "JSProcessActorChild should have value."
+ );
+ });
+ },
+});
+
+declTest("getActor with remoteType mismatch", {
+ remoteTypes: ["privilegedabout"],
+ url: TEST_URL,
+
+ async test(browser) {
+ let parent = browser.browsingContext.currentWindowGlobal.domProcess;
+ Assert.throws(
+ () => parent.getActor("TestProcessActor"),
+ /NotSupportedError/,
+ "Should throw if its remoteTypes don't match."
+ );
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ let child = ChromeUtils.domProcessChild;
+ ok(child, "DOMProcessChild should have value.");
+ Assert.throws(
+ () => child.getActor("TestProcessActor"),
+ /NotSupportedError/,
+ "Should throw if its remoteTypes don't match."
+ );
+ });
+ },
+});
+
+declTest("getActor without includeParent", {
+ includeParent: false,
+
+ async test(_browser, win) {
+ let parent = win.docShell.browsingContext.currentWindowGlobal.domProcess;
+ SimpleTest.doesThrow(
+ () => parent.getActor("TestProcessActor"),
+ "Should throw if includeParent is false."
+ );
+
+ let child = ChromeUtils.domProcessChild;
+ SimpleTest.doesThrow(
+ () => child.getActor("TestProcessActor"),
+ "Should throw if includeParent is false."
+ );
+ },
+});
+
+declTest("getActor with includeParent", {
+ includeParent: true,
+
+ async test(_browser, win) {
+ let parent = win.docShell.browsingContext.currentWindowGlobal.domProcess;
+ let actorParent = parent.getActor("TestProcessActor");
+ ok(actorParent, "JSProcessActorParent should have value.");
+
+ let child = ChromeUtils.domProcessChild;
+ let actorChild = child.getActor("TestProcessActor");
+ ok(actorChild, "JSProcessActorChild should have value.");
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_observer_notification.js b/dom/ipc/tests/JSProcessActor/browser_observer_notification.js
new file mode 100644
index 0000000000..28dfa16481
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_observer_notification.js
@@ -0,0 +1,41 @@
+/* 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", {
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const TOPIC = "test-js-content-actor-child-observer";
+ Services.obs.notifyObservers(content.window, TOPIC, "dataString");
+
+ let child = ChromeUtils.domProcessChild;
+ let actorChild = child.getActor("TestProcessActor");
+ ok(actorChild, "JSProcessActorChild should have value.");
+ ok(
+ actorChild.lastObserved,
+ "JSProcessActorChild lastObserved should have value."
+ );
+ let { subject, topic, data } = actorChild.lastObserved;
+ is(topic, TOPIC, "Topic matches");
+ is(data, "dataString", "Data matches");
+ });
+ },
+});
+
+declTest("test observers with null data", {
+ async test(browser) {
+ await SpecialPowers.spawn(browser, [], async function () {
+ const TOPIC = "test-js-content-actor-child-observer";
+ Services.obs.notifyObservers(content.window, TOPIC);
+
+ let child = ChromeUtils.domProcessChild;
+ let actorChild = child.getActor("TestProcessActor");
+ ok(actorChild, "JSProcessActorChild should have value.");
+ let { subject, topic, data } = actorChild.lastObserved;
+
+ is(topic, TOPIC, "Topic matches");
+ is(data, null, "Data matches");
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_registerProcessActor.js b/dom/ipc/tests/JSProcessActor/browser_registerProcessActor.js
new file mode 100644
index 0000000000..1fa4e1c17c
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_registerProcessActor.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.registerContentActor(
+ "TestProcessActor",
+ processActorOptions[fileExt]
+ ),
+ "Should throw if register has duplicate name."
+ );
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_sendAsyncMessage.js b/dom/ipc/tests/JSProcessActor/browser_sendAsyncMessage.js
new file mode 100644
index 0000000000..02ad64ee8b
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_sendAsyncMessage.js
@@ -0,0 +1,51 @@
+/* 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.domProcess;
+ let actorParent = parent.getActor("TestProcessActor");
+ ok(actorParent, "JSProcessActorParent should have value.");
+
+ await ContentTask.spawn(browser, {}, async function () {
+ let child = ChromeUtils.domProcessChild;
+ let actorChild = child.getActor("TestProcessActor");
+ ok(actorChild, "JSProcessActorChild 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 = ChromeUtils.domProcessChild;
+ let actorChild = child.getActor("TestProcessActor");
+ ok(actorChild, "JSProcessActorChild 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;
+ });
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_sendQuery.js b/dom/ipc/tests/JSProcessActor/browser_sendQuery.js
new file mode 100644
index 0000000000..5a9767bf3a
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_sendQuery.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const ERROR_LINE_NUMBERS = {
+ jsm: 31,
+ "sys.mjs": 28,
+};
+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.domProcess;
+ let actorParent = parent.getActor("TestProcessActor");
+
+ 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/TestProcessActorChild.${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.domProcess;
+ let actorParent = parent.getActor("TestProcessActor");
+
+ 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/TestProcessActorChild.${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.domProcess;
+ let actorParent = parent.getActor("TestProcessActor");
+ ok(actorParent, "JSWindowActorParent should have value.");
+
+ let { result } = await actorParent.sendQuery("asyncAdd", { a: 10, b: 20 });
+ is(result, 30);
+ },
+});
diff --git a/dom/ipc/tests/JSProcessActor/browser_uri_combination.js b/dom/ipc/tests/JSProcessActor/browser_uri_combination.js
new file mode 100644
index 0000000000..33394cf54a
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/browser_uri_combination.js
@@ -0,0 +1,81 @@
+add_task(function specify_both() {
+ // Specifying both should throw.
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerProcessActor("TestProcessActor", {
+ parent: {
+ moduleURI: "resource://testing-common/TestProcessActorParent.jsm",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestProcessActorChild.jsm",
+ esModuleURI: "resource://testing-common/TestProcessActorChild.sys.mjs",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerProcessActor("TestProcessActor", {
+ parent: {
+ esModuleURI: "resource://testing-common/TestProcessActorParent.sys.mjs",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestProcessActorChild.jsm",
+ esModuleURI: "resource://testing-common/TestProcessActorChild.sys.mjs",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerProcessActor("TestProcessActor", {
+ parent: {
+ moduleURI: "resource://testing-common/TestProcessActorParent.jsm",
+ esModuleURI: "resource://testing-common/TestProcessActorParent.sys.mjs",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestProcessActorChild.jsm",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+
+ SimpleTest.doesThrow(() => {
+ ChromeUtils.registerProcessActor("TestProcessActor", {
+ parent: {
+ moduleURI: "resource://testing-common/TestProcessActorParent.jsm",
+ esModuleURI: "resource://testing-common/TestProcessActorParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://testing-common/TestProcessActorChild.sys.mjs",
+ },
+ });
+ }, "Should throw if both moduleURI and esModuleURI are specified.");
+});
+
+add_task(function specify_mixed() {
+ // Mixing JSM and ESM should work.
+
+ try {
+ ChromeUtils.registerProcessActor("TestProcessActor", {
+ parent: {
+ moduleURI: "resource://testing-common/TestProcessActorParent.jsm",
+ },
+ child: {
+ esModuleURI: "resource://testing-common/TestProcessActorChild.sys.mjs",
+ },
+ });
+ } finally {
+ ChromeUtils.unregisterProcessActor("TestProcessActor");
+ }
+
+ try {
+ ChromeUtils.registerProcessActor("TestProcessActor", {
+ parent: {
+ esModuleURI: "resource://testing-common/TestProcessActorParent.sys.mjs",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestProcessActorChild.jsm",
+ },
+ });
+ } finally {
+ ChromeUtils.unregisterProcessActor("TestProcessActor");
+ }
+});
diff --git a/dom/ipc/tests/JSProcessActor/head.js b/dom/ipc/tests/JSProcessActor/head.js
new file mode 100644
index 0000000000..cde193ca2e
--- /dev/null
+++ b/dom/ipc/tests/JSProcessActor/head.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provide infrastructure for JSProcessActor tests.
+ */
+
+const URL = "about:blank";
+const TEST_URL = "http://test2.example.org/";
+let processActorOptions = {
+ jsm: {
+ parent: {
+ moduleURI: "resource://testing-common/TestProcessActorParent.jsm",
+ },
+ child: {
+ moduleURI: "resource://testing-common/TestProcessActorChild.jsm",
+ observers: ["test-js-content-actor-child-observer"],
+ },
+ },
+ "sys.mjs": {
+ parent: {
+ esModuleURI: "resource://testing-common/TestProcessActorParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://testing-common/TestProcessActorChild.sys.mjs",
+ observers: ["test-js-content-actor-child-observer"],
+ },
+ },
+};
+
+function promiseNotification(aNotification) {
+ let notificationResolve;
+ let notificationObserver = function observer() {
+ notificationResolve();
+ Services.obs.removeObserver(notificationObserver, aNotification);
+ };
+ return new Promise(resolve => {
+ notificationResolve = resolve;
+ Services.obs.addObserver(notificationObserver, aNotification);
+ });
+}
+
+function declTest(name, cfg) {
+ declTestWithOptions(name, cfg, "jsm");
+ declTestWithOptions(name, cfg, "sys.mjs");
+}
+
+function declTestWithOptions(name, cfg, fileExt) {
+ let {
+ url = "about:blank",
+ includeParent = false,
+ remoteTypes,
+ loadInDevToolsLoader = false,
+ test,
+ } = cfg;
+
+ // Build the actor options object which will be used to register & unregister
+ // our process actor.
+ let actorOptions = {
+ parent: Object.assign({}, processActorOptions[fileExt].parent),
+ child: Object.assign({}, processActorOptions[fileExt].child),
+ };
+ actorOptions.includeParent = includeParent;
+ if (remoteTypes !== undefined) {
+ actorOptions.remoteTypes = remoteTypes;
+ }
+ if (loadInDevToolsLoader) {
+ actorOptions.loadInDevToolsLoader = true;
+ }
+
+ // 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 provided URL
+ ChromeUtils.registerProcessActor("TestProcessActor", 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.unregisterProcessActor("TestProcessActor");
+ info("Exiting test: " + name);
+ }
+ });
+}
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);
+ }
+ });
+}
diff --git a/dom/ipc/tests/blob_verify.sjs b/dom/ipc/tests/blob_verify.sjs
new file mode 100644
index 0000000000..c979192bf0
--- /dev/null
+++ b/dom/ipc/tests/blob_verify.sjs
@@ -0,0 +1,26 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+function handleRequest(request, response) {
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var bodyBytes = [];
+ let bodyAvail;
+ while ((bodyAvail = bodyStream.available()) > 0) {
+ Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail));
+ }
+
+ var bos = new BinaryOutputStream(response.bodyOutputStream);
+
+ response.processAsync();
+ bos.writeByteArray(bodyBytes);
+ response.finish();
+}
diff --git a/dom/ipc/tests/browser.toml b/dom/ipc/tests/browser.toml
new file mode 100644
index 0000000000..cd2129e9c1
--- /dev/null
+++ b/dom/ipc/tests/browser.toml
@@ -0,0 +1,90 @@
+[DEFAULT]
+support-files = [
+ "file_disableScript.html",
+ "file_domainPolicy_base.html",
+ "file_cancel_content_js.html",
+ "../../media/test/short.mp4",
+ "../../media/test/owl.mp3",
+]
+
+["browser_CrashService_crash.js"]
+skip-if = ["!crashreporter"]
+
+["browser_ProcessPriorityManager.js"]
+# The Process Priority Manager is only enabled for Windows, Linux, and MacOS so far.
+# Bug 1522879.
+# However, you can still run browser_ProcessPriorityManager.js locally on other
+# OSes. This will test the priority manager infrastructure but not actually
+# change the priority.
+skip-if = ["os == 'android'"]
+support-files = [
+ "file_cross_frame.html",
+ "file_dummy.html",
+ "../../tests/browser/file_coop_coep.html",
+ "../../tests/browser/file_coop_coep.html^headers^",
+]
+
+["browser_bug1646088.js"]
+support-files = ["file_dummy.html"]
+
+["browser_bug1686194.js"]
+support-files = ["file_dummy.html"]
+
+["browser_cancel_content_js.js"]
+
+["browser_child_clipboard_restricted.js"]
+
+["browser_content_shutdown_with_endless_js.js"]
+support-files = [
+ "file_endless_js.html",
+ "file_dummy.html",
+]
+
+["browser_crash_oopiframe.js"]
+skip-if = [
+ "!crashreporter",
+ "verify",
+ "os == 'win'", # Bug 1775837
+ "os == 'linux'", # Bug 1775837
+]
+
+["browser_domainPolicy.js"]
+
+["browser_gc_schedule.js"]
+# This test is timing sensitive, timing changes due to asan/tsan/debugging
+# can upset it.
+skip-if = [
+ "verify",
+ "asan",
+ "tsan",
+ "debug",
+ "os != 'linux'",
+ "bits != 64",
+]
+
+["browser_hide_tooltip.js"]
+
+["browser_layers_unloaded_while_interruptingJS.js"]
+
+["browser_memory_distribution_telemetry.js"]
+skip-if = ["true"]
+
+["browser_pbrowser_creation_failure.js"]
+
+["browser_subframesPreferUsed.js"]
+
+["browser_very_fission.js"]
+support-files = ["file_dummy.html"]
+run-if = ["widget == 'gtk'"]
+
+["browser_wpi_isolate_everything.js"]
+support-files = ["browser_wpi_base.js"]
+
+["browser_wpi_isolate_high_value.js"]
+skip-if = ["!fission"] # Only relevant for fission
+support-files = ["browser_wpi_base.js"]
+
+["browser_wpi_isolate_nothing.js"]
+skip-if = ["apple_catalina && debug"] # Bug 1741763; high frequency intermittent; leaked 2 windows
+
+support-files = ["browser_wpi_base.js"]
diff --git a/dom/ipc/tests/browser_CrashService_crash.js b/dom/ipc/tests/browser_CrashService_crash.js
new file mode 100644
index 0000000000..0bcbf95410
--- /dev/null
+++ b/dom/ipc/tests/browser_CrashService_crash.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Ensures that content crashes are reported to the crash service
+// (nsICrashService and CrashManager.sys.mjs).
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+SimpleTest.requestFlakyTimeout("untriaged");
+SimpleTest.requestCompleteLog();
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ forceNewProcess: true,
+ });
+
+ SimpleTest.expectChildProcessCrash();
+
+ let crashMan = Services.crashmanager;
+
+ // First, clear the crash record store.
+ info("Waiting for pruneOldCrashes");
+ var future = new Date(Date.now() + 1000 * 60 * 60 * 24);
+ await crashMan.pruneOldCrashes(future);
+
+ var crashDateMS = Date.now();
+
+ let crashPromise = BrowserTestUtils.crashFrame(tab.linkedBrowser);
+
+ // Finally, poll for the new crash record.
+ await new Promise((resolve, reject) => {
+ function tryGetCrash() {
+ info("Waiting for getCrashes");
+ crashMan.getCrashes().then(
+ function (crashes) {
+ if (crashes.length) {
+ is(crashes.length, 1, "There should be only one record");
+ var crash = crashes[0];
+ ok(
+ crash.isOfType(
+ crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT],
+ crashMan.CRASH_TYPE_CRASH
+ ),
+ "Record should be a content crash"
+ );
+ ok(!!crash.id, "Record should have an ID");
+ ok(!!crash.crashDate, "Record should have a crash date");
+ var dateMS = crash.crashDate.valueOf();
+ var twoMin = 1000 * 60 * 2;
+ ok(
+ crashDateMS - twoMin <= dateMS && dateMS <= crashDateMS + twoMin,
+ `Record's crash date should be nowish: ` +
+ `now=${crashDateMS} recordDate=${dateMS}`
+ );
+ resolve();
+ } else {
+ setTimeout(tryGetCrash, 1000);
+ }
+ },
+ function (err) {
+ reject(err);
+ }
+ );
+ }
+ setTimeout(tryGetCrash, 1000);
+ });
+
+ await crashPromise;
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/ipc/tests/browser_ProcessPriorityManager.js b/dom/ipc/tests/browser_ProcessPriorityManager.js
new file mode 100644
index 0000000000..e1532a53ca
--- /dev/null
+++ b/dom/ipc/tests/browser_ProcessPriorityManager.js
@@ -0,0 +1,963 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PRIORITY_SET_TOPIC =
+ "process-priority-manager:TEST-ONLY:process-priority-set";
+
+// Copied from Hal.cpp
+const PROCESS_PRIORITY_FOREGROUND = "FOREGROUND";
+const PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE = "BACKGROUND_PERCEIVABLE";
+const PROCESS_PRIORITY_BACKGROUND = "BACKGROUND";
+
+// This is how many milliseconds we'll wait for a process priority
+// change before we assume that it's just not happening.
+const WAIT_FOR_CHANGE_TIME_MS = 2000;
+
+// A convenience function for getting the child ID from a browsing context.
+function browsingContextChildID(bc) {
+ return bc.currentWindowGlobal?.domProcess.childID;
+}
+
+/**
+ * This class is responsible for watching process priority changes, and
+ * mapping them to tabs in a single window.
+ */
+class TabPriorityWatcher {
+ /**
+ * Constructing a TabPriorityWatcher should happen before any tests
+ * start when there's only a single tab in the window.
+ *
+ * Callers must call `destroy()` on any instance that is constructed
+ * when the test is completed.
+ *
+ * @param tabbrowser (<tabbrowser>)
+ * The tabbrowser (gBrowser) for the window to be tested.
+ */
+ constructor(tabbrowser) {
+ this.tabbrowser = tabbrowser;
+ Assert.equal(
+ tabbrowser.tabs.length,
+ 1,
+ "TabPriorityWatcher must be constructed in a window " +
+ "with a single tab to start."
+ );
+
+ // This maps from childIDs to process priorities.
+ this.priorityMap = new Map();
+
+ // The keys in this map are childIDs we're not expecting to change.
+ // Each value is an array of priorities we've seen the childID changed
+ // to since it was added to the map. If the array is empty, there
+ // have been no changes.
+ this.noChangeChildIDs = new Map();
+
+ Services.obs.addObserver(this, PRIORITY_SET_TOPIC);
+ }
+
+ /**
+ * Cleans up lingering references for an instance of
+ * TabPriorityWatcher to avoid leaks. This should be called when
+ * finishing the test.
+ */
+ destroy() {
+ Services.obs.removeObserver(this, PRIORITY_SET_TOPIC);
+ }
+
+ /**
+ * This returns a Promise that resolves when the process with
+ * the given childID reaches the given priority.
+ * This will eventually time out if that priority is never reached.
+ *
+ * @param childID
+ * The childID of the process to wait on.
+ * @param expectedPriority (String)
+ * One of the PROCESS_PRIORITY_ constants defined at the
+ * top of this file.
+ * @return Promise
+ * @resolves undefined
+ * Once the browser reaches the expected priority.
+ */
+ async waitForPriorityChange(childID, expectedPriority) {
+ await TestUtils.waitForCondition(() => {
+ let currentPriority = this.priorityMap.get(childID);
+ if (currentPriority == expectedPriority) {
+ Assert.ok(
+ true,
+ `Process with child ID ${childID} reached expected ` +
+ `priority: ${currentPriority}`
+ );
+ return true;
+ }
+ return false;
+ }, `Waiting for process with child ID ${childID} to reach priority ${expectedPriority}`);
+ }
+
+ /**
+ * Returns a Promise that resolves after a duration of
+ * WAIT_FOR_CHANGE_TIME_MS. During that time, if the process
+ * with the passed in child ID changes priority, a test
+ * failure will be registered.
+ *
+ * @param childID
+ * The childID of the process that we expect to not change priority.
+ * @return Promise
+ * @resolves undefined
+ * Once the WAIT_FOR_CHANGE_TIME_MS duration has passed.
+ */
+ async ensureNoPriorityChange(childID) {
+ this.noChangeChildIDs.set(childID, []);
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS));
+ let priorities = this.noChangeChildIDs.get(childID);
+ Assert.deepEqual(
+ priorities,
+ [],
+ `Should have seen no process priority changes for child ID ${childID}`
+ );
+ this.noChangeChildIDs.delete(childID);
+ }
+
+ /**
+ * This returns a Promise that resolves when all of the processes
+ * of the browsing contexts in the browsing context tree
+ * of a particular <browser> have reached a particular priority.
+ * This will eventually time out if that priority is never reached.
+ *
+ * @param browser (<browser>)
+ * The <browser> to get the BC tree from.
+ * @param expectedPriority (String)
+ * One of the PROCESS_PRIORITY_ constants defined at the
+ * top of this file.
+ * @return Promise
+ * @resolves undefined
+ * Once the browser reaches the expected priority.
+ */
+ async waitForBrowserTreePriority(browser, expectedPriority) {
+ let childIDs = new Set(
+ browser.browsingContext
+ .getAllBrowsingContextsInSubtree()
+ .map(browsingContextChildID)
+ );
+ let promises = [];
+ for (let childID of childIDs) {
+ let currentPriority = this.priorityMap.get(childID);
+
+ promises.push(
+ currentPriority == expectedPriority
+ ? this.ensureNoPriorityChange(childID)
+ : this.waitForPriorityChange(childID, expectedPriority)
+ );
+ }
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * Synchronously returns the priority of a particular child ID.
+ *
+ * @param childID
+ * The childID to get the content process priority for.
+ * @return String
+ * The priority of the child ID's process.
+ */
+ currentPriority(childID) {
+ return this.priorityMap.get(childID);
+ }
+
+ /**
+ * A utility function that takes a string passed via the
+ * PRIORITY_SET_TOPIC observer notification and extracts the
+ * childID and priority string.
+ *
+ * @param ppmDataString (String)
+ * The string data passed through the PRIORITY_SET_TOPIC observer
+ * notification.
+ * @return Object
+ * An object with the following properties:
+ *
+ * childID (Number)
+ * The ID of the content process that changed priority.
+ *
+ * priority (String)
+ * The priority that the content process was set to.
+ */
+ parsePPMData(ppmDataString) {
+ let [childIDStr, priority] = ppmDataString.split(":");
+ return {
+ childID: parseInt(childIDStr, 10),
+ priority,
+ };
+ }
+
+ /** nsIObserver **/
+ observe(subject, topic, data) {
+ if (topic != PRIORITY_SET_TOPIC) {
+ Assert.ok(false, "TabPriorityWatcher is observing the wrong topic");
+ return;
+ }
+
+ let { childID, priority } = this.parsePPMData(data);
+ if (this.noChangeChildIDs.has(childID)) {
+ this.noChangeChildIDs.get(childID).push(priority);
+ }
+ this.priorityMap.set(childID, priority);
+ }
+}
+
+let gTabPriorityWatcher;
+
+add_setup(async function () {
+ // We need to turn on testMode for the process priority manager in
+ // order to receive the observer notifications that this test relies on.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processPriorityManager.testMode", true],
+ ["dom.ipc.processPriorityManager.enabled", true],
+ ],
+ });
+ gTabPriorityWatcher = new TabPriorityWatcher(gBrowser);
+});
+
+registerCleanupFunction(() => {
+ gTabPriorityWatcher.destroy();
+ gTabPriorityWatcher = null;
+});
+
+/**
+ * Utility function that switches the current tabbrowser from one
+ * tab to another, and ensures that the tab that goes into the background
+ * has (or reaches) a particular content process priority.
+ *
+ * It is expected that the fromTab and toTab belong to two separate content
+ * processes.
+ *
+ * @param Object
+ * An object with the following properties:
+ *
+ * fromTab (<tab>)
+ * The tab that will be switched from to the toTab. The fromTab
+ * is the one that will be going into the background.
+ *
+ * toTab (<tab>)
+ * The tab that will be switched to from the fromTab. The toTab
+ * is presumed to start in the background, and will enter the
+ * foreground.
+ *
+ * fromTabExpectedPriority (String)
+ * The priority that the content process for the fromTab is
+ * expected to be (or reach) after the tab goes into the background.
+ * This should be one of the PROCESS_PRIORITY_ strings defined at the
+ * top of the file.
+ *
+ * @return Promise
+ * @resolves undefined
+ * Once the tab switch is complete, and the two content processes for the
+ * tabs have reached the expected priority levels.
+ */
+async function assertPriorityChangeOnBackground({
+ fromTab,
+ toTab,
+ fromTabExpectedPriority,
+}) {
+ let fromBrowser = fromTab.linkedBrowser;
+ let toBrowser = toTab.linkedBrowser;
+
+ // If the tabs aren't running in separate processes, none of the
+ // rest of this is going to work.
+ Assert.notEqual(
+ toBrowser.frameLoader.remoteTab.osPid,
+ fromBrowser.frameLoader.remoteTab.osPid,
+ "Tabs should be running in separate processes."
+ );
+
+ let fromPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
+ fromBrowser,
+ fromTabExpectedPriority
+ );
+ let toPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
+ toBrowser,
+ PROCESS_PRIORITY_FOREGROUND
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, toTab);
+ await Promise.all([fromPromise, toPromise]);
+}
+
+/**
+ * Test that if a normal tab goes into the background,
+ * it has its process priority lowered to PROCESS_PRIORITY_BACKGROUND.
+ * Additionally, test priorityHint flag sets the process priority
+ * appropriately to PROCESS_PRIORITY_BACKGROUND and PROCESS_PRIORITY_FOREGROUND.
+ */
+add_task(async function test_normal_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/browser/dom/ipc/tests/file_cross_frame.html",
+ async browser => {
+ let tab = gBrowser.getTabForBrowser(browser);
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ let origtabID = browsingContextChildID(
+ originalTab.linkedBrowser.browsingContext
+ );
+
+ Assert.equal(
+ originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint,
+ false,
+ "PriorityHint of the original tab should be false by default"
+ );
+
+ // Changing renderLayers doesn't change priority of the background tab.
+ originalTab.linkedBrowser.preserveLayers(true);
+ originalTab.linkedBrowser.renderLayers = true;
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
+ );
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(origtabID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Tab didn't get prioritized only due to renderLayers"
+ );
+
+ // Test when priorityHint is true, the original tab priority
+ // becomes PROCESS_PRIORITY_FOREGROUND.
+ originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(origtabID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Setting priorityHint to true should set the original tab to foreground priority"
+ );
+
+ // Test when priorityHint is false, the original tab priority
+ // becomes PROCESS_PRIORITY_BACKGROUND.
+ originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
+ );
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(origtabID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Setting priorityHint to false should set the original tab to background priority"
+ );
+
+ let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext);
+
+ // Test when priorityHint is true, the process priority of the
+ // active tab remains PROCESS_PRIORITY_FOREGROUND.
+ tab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(tabID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Setting priorityHint to true should maintain the new tab priority as foreground"
+ );
+
+ // Test when priorityHint is false, the process priority of the
+ // active tab remains PROCESS_PRIORITY_FOREGROUND.
+ tab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(tabID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Setting priorityHint to false should maintain the new tab priority as foreground"
+ );
+
+ originalTab.linkedBrowser.preserveLayers(false);
+ originalTab.linkedBrowser.renderLayers = false;
+ }
+ );
+});
+
+// Load a simple page on the given host into a new tab.
+async function loadKeepAliveTab(host) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ host + "/browser/dom/ipc/tests/file_dummy.html"
+ );
+ let childID = browsingContextChildID(
+ gBrowser.selectedBrowser.browsingContext
+ );
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new tab should make it prioritized"
+ );
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // There must be only one process with a remote type for the tab we loaded
+ // to ensure that when we load a new page into the iframe with that host
+ // that it will end up in the same process as the initial tab.
+ let remoteType = gBrowser.selectedBrowser.remoteType;
+ await TestUtils.waitForCondition(() => {
+ return (
+ ChromeUtils.getAllDOMProcesses().filter(
+ process => process.remoteType == remoteType
+ ).length == 1
+ );
+ }, `Waiting for there to be only one process with remote type ${remoteType}`);
+ }
+
+ return { tab, childID };
+}
+
+const AUDIO_WAKELOCK_NAME = "audio-playing";
+const VIDEO_WAKELOCK_NAME = "video-playing";
+
+// This function was copied from toolkit/content/tests/browser/head.js
+function wakeLockObserved(powerManager, observeTopic, checkFn) {
+ return new Promise(resolve => {
+ function wakeLockListener() {}
+ wakeLockListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMMozWakeLockListener"]),
+ callback(topic, state) {
+ if (topic == observeTopic && checkFn(state)) {
+ powerManager.removeWakeLockListener(wakeLockListener.prototype);
+ resolve();
+ }
+ },
+ };
+ powerManager.addWakeLockListener(wakeLockListener.prototype);
+ });
+}
+
+// This function was copied from toolkit/content/tests/browser/head.js
+async function waitForExpectedWakeLockState(
+ topic,
+ { needLock, isForegroundLock }
+) {
+ const powerManagerService = Cc["@mozilla.org/power/powermanagerservice;1"];
+ const powerManager = powerManagerService.getService(
+ Ci.nsIPowerManagerService
+ );
+ const wakelockState = powerManager.getWakeLockState(topic);
+ let expectedLockState = "unlocked";
+ if (needLock) {
+ expectedLockState = isForegroundLock
+ ? "locked-foreground"
+ : "locked-background";
+ }
+ if (wakelockState != expectedLockState) {
+ info(`wait until wakelock becomes ${expectedLockState}`);
+ await wakeLockObserved(
+ powerManager,
+ topic,
+ state => state == expectedLockState
+ );
+ }
+ is(
+ powerManager.getWakeLockState(topic),
+ expectedLockState,
+ `the wakelock state for '${topic}' is equal to '${expectedLockState}'`
+ );
+}
+
+/**
+ * If an iframe in a foreground tab is navigated to a new page for
+ * a different site, then the process of the new iframe page should
+ * have priority PROCESS_PRIORITY_FOREGROUND. Additionally, if Fission
+ * is enabled, then the old iframe page's process's priority should be
+ * lowered to PROCESS_PRIORITY_BACKGROUND.
+ */
+add_task(async function test_iframe_navigate() {
+ // This test (eventually) loads a page from the host topHost that has an
+ // iframe from iframe1Host. It then navigates the iframe to iframe2Host.
+ let topHost = "https://example.com";
+ let iframe1Host = "https://example.org";
+ let iframe2Host = "https://example.net";
+
+ // Before we load the final test page into a tab, we need to load pages
+ // from both iframe hosts into tabs. This is needed so that we are testing
+ // the "load a new page" part of prioritization and not the "initial
+ // process load" part. Additionally, it ensures that the process for the
+ // initial iframe page doesn't shut down once we navigate away from it,
+ // which will also affect its prioritization.
+ let { tab: iframe1Tab, childID: iframe1TabChildID } = await loadKeepAliveTab(
+ iframe1Host
+ );
+ let { tab: iframe2Tab, childID: iframe2TabChildID } = await loadKeepAliveTab(
+ iframe2Host
+ );
+
+ await BrowserTestUtils.withNewTab(
+ topHost + "/browser/dom/ipc/tests/file_cross_frame.html",
+ async browser => {
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(iframe2TabChildID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Switching to another new tab should deprioritize the old one"
+ );
+
+ let topChildID = browsingContextChildID(browser.browsingContext);
+ let iframe = browser.browsingContext.children[0];
+ let iframe1ChildID = browsingContextChildID(iframe);
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(topChildID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "The top level page in the new tab should be prioritized"
+ );
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(iframe1ChildID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "The iframe in the new tab should be prioritized"
+ );
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // Basic process uniqueness checks for the state after all three tabs
+ // are initially loaded.
+ Assert.notEqual(
+ topChildID,
+ iframe1ChildID,
+ "file_cross_frame.html should be loaded into a different process " +
+ "than its initial iframe"
+ );
+
+ Assert.notEqual(
+ topChildID,
+ iframe2TabChildID,
+ "file_cross_frame.html should be loaded into a different process " +
+ "than the tab containing iframe2Host"
+ );
+
+ Assert.notEqual(
+ iframe1ChildID,
+ iframe2TabChildID,
+ "The initial iframe loaded by file_cross_frame.html should be " +
+ "loaded into a different process than the tab containing " +
+ "iframe2Host"
+ );
+
+ // Note: this assertion depends on our process selection logic.
+ // Specifically, that we reuse an existing process for an iframe if
+ // possible.
+ Assert.equal(
+ iframe1TabChildID,
+ iframe1ChildID,
+ "Both pages loaded in iframe1Host should be in the same process"
+ );
+ }
+
+ // Do a cross-origin navigation in the iframe in the foreground tab.
+ let iframe2URI = iframe2Host + "/browser/dom/ipc/tests/file_dummy.html";
+ let loaded = BrowserTestUtils.browserLoaded(browser, true, iframe2URI);
+ await SpecialPowers.spawn(
+ iframe,
+ [iframe2URI],
+ async function (_iframe2URI) {
+ content.location = _iframe2URI;
+ }
+ );
+ await loaded;
+
+ let iframe2ChildID = browsingContextChildID(iframe);
+ let iframe1Priority = gTabPriorityWatcher.currentPriority(iframe1ChildID);
+ let iframe2Priority = gTabPriorityWatcher.currentPriority(iframe2ChildID);
+
+ if (SpecialPowers.useRemoteSubframes) {
+ // Basic process uniqueness check for the state after navigating the
+ // iframe. There's no need to check the top level pages because they
+ // have not navigated.
+ //
+ // iframe1ChildID != iframe2ChildID is implied by:
+ // iframe1ChildID != iframe2TabChildID
+ // iframe2TabChildID == iframe2ChildID
+ //
+ // iframe2ChildID != topChildID is implied by:
+ // topChildID != iframe2TabChildID
+ // iframe2TabChildID == iframe2ChildID
+
+ // Note: this assertion depends on our process selection logic.
+ // Specifically, that we reuse an existing process for an iframe if
+ // possible. If that changes, this test may need to be carefully
+ // rewritten, as the whole point of the test is to check what happens
+ // with the priority manager when an iframe shares a process with
+ // a page in another tab.
+ Assert.equal(
+ iframe2TabChildID,
+ iframe2ChildID,
+ "Both pages loaded in iframe2Host should be in the same process"
+ );
+
+ // Now that we've established the relationship between the various
+ // processes, we can finally check that the priority manager is doing
+ // the right thing.
+ Assert.equal(
+ iframe1Priority,
+ PROCESS_PRIORITY_BACKGROUND,
+ "The old iframe process should have been deprioritized"
+ );
+ } else {
+ Assert.equal(
+ iframe1ChildID,
+ iframe2ChildID,
+ "Navigation should not have switched processes"
+ );
+ }
+
+ Assert.equal(
+ iframe2Priority,
+ PROCESS_PRIORITY_FOREGROUND,
+ "The new iframe process should be prioritized"
+ );
+ }
+ );
+
+ await BrowserTestUtils.removeTab(iframe2Tab);
+ await BrowserTestUtils.removeTab(iframe1Tab);
+});
+
+/**
+ * Test that a cross-group navigation properly preserves the process priority.
+ * The goal of this test is to check that the code related to mPriorityActive in
+ * CanonicalBrowsingContext::ReplacedBy works correctly, but in practice the
+ * prioritization code in SetRenderLayers will also make this test pass, though
+ * that prioritization happens slightly later.
+ */
+add_task(async function test_cross_group_navigate() {
+ // This page is same-site with the page we're going to cross-group navigate to.
+ let coopPage =
+ "https://example.com/browser/dom/tests/browser/file_coop_coep.html";
+
+ // Load it as a top level tab so that we don't accidentally get the initial
+ // load prioritization.
+ let backgroundTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ coopPage
+ );
+ let backgroundTabChildID = browsingContextChildID(
+ gBrowser.selectedBrowser.browsingContext
+ );
+
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(backgroundTabChildID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new tab should make it prioritized"
+ );
+
+ await BrowserTestUtils.withNewTab(
+ "https://example.org/browser/dom/ipc/tests/file_cross_frame.html",
+ async browser => {
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(backgroundTabChildID),
+ PROCESS_PRIORITY_BACKGROUND,
+ "Switching to a new tab should deprioritize the old one"
+ );
+
+ let dotOrgChildID = browsingContextChildID(browser.browsingContext);
+
+ // Do a cross-group navigation.
+ BrowserTestUtils.startLoadingURIString(browser, coopPage);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let coopChildID = browsingContextChildID(browser.browsingContext);
+ let coopPriority = gTabPriorityWatcher.currentPriority(coopChildID);
+ let dotOrgPriority = gTabPriorityWatcher.currentPriority(dotOrgChildID);
+
+ Assert.equal(
+ backgroundTabChildID,
+ coopChildID,
+ "The same site should get loaded into the same process"
+ );
+ Assert.notEqual(
+ dotOrgChildID,
+ coopChildID,
+ "Navigation should have switched processes"
+ );
+ Assert.equal(
+ dotOrgPriority,
+ PROCESS_PRIORITY_BACKGROUND,
+ "The old page process should have been deprioritized"
+ );
+ Assert.equal(
+ coopPriority,
+ PROCESS_PRIORITY_FOREGROUND,
+ "The new page process should be prioritized"
+ );
+ }
+ );
+
+ await BrowserTestUtils.removeTab(backgroundTab);
+});
+
+/**
+ * Test that if a tab with video goes into the background,
+ * it has its process priority lowered to
+ * PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE if it has no audio,
+ * and that it has its priority remain at
+ * PROCESS_PRIORITY_FOREGROUND if it does have audio.
+ */
+add_task(async function test_video_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ // Let's load up a video in the tab, but mute it, so that this tab should
+ // reach PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE. We need to wait for the
+ // wakelock changes from the unmuting to get back up to the parent.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let video = content.document.createElement("video");
+ video.src = "https://example.net/browser/dom/ipc/tests/short.mp4";
+ video.muted = true;
+ content.document.body.appendChild(video);
+ // We'll loop the video to avoid it ending before the test is done.
+ video.loop = true;
+ await video.play();
+ });
+ await Promise.all([
+ waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: false,
+ }),
+ waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, {
+ needLock: true,
+ isForegroundLock: true,
+ }),
+ ]);
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // The tab with the muted video should reach
+ // PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Let's unmute the video now. We need to wait for the wakelock change from
+ // the unmuting to get back up to the parent.
+ await Promise.all([
+ waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, {
+ needLock: true,
+ isForegroundLock: true,
+ }),
+ SpecialPowers.spawn(browser, [], async () => {
+ let video = content.document.querySelector("video");
+ video.muted = false;
+ }),
+ ]);
+
+ // The tab with the unmuted video should stay at
+ // PROCESS_PRIORITY_FOREGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+ });
+});
+
+/**
+ * Test that if a tab with a playing <audio> element goes into
+ * the background, the process priority does not change, unless
+ * that audio is muted (in which case, it reaches
+ * PROCESS_PRIORITY_BACKGROUND).
+ */
+add_task(async function test_audio_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ // Let's load up some audio in the tab, but mute it, so that this tab should
+ // reach PROCESS_PRIORITY_BACKGROUND.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let audio = content.document.createElement("audio");
+ audio.src = "https://example.net/browser/dom/ipc/tests/owl.mp3";
+ audio.muted = true;
+ content.document.body.appendChild(audio);
+ // We'll loop the audio to avoid it ending before the test is done.
+ audio.loop = true;
+ await audio.play();
+ });
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // The tab with the muted audio should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now unmute the audio. Unfortuntely, there's a bit of a race here,
+ // since the wakelock on the audio element is released and then
+ // re-acquired if the audio reaches its end and loops around. This
+ // will cause an unexpected priority change on the content process.
+ //
+ // To avoid this race, we'll seek the audio back to the beginning,
+ // and lower its playback rate to the minimum to increase the
+ // likelihood that the check completes before the audio loops around.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let audio = content.document.querySelector("audio");
+ let seeked = ContentTaskUtils.waitForEvent(audio, "seeked");
+ audio.muted = false;
+ // 0.25 is the minimum playback rate that still keeps the audio audible.
+ audio.playbackRate = 0.25;
+ audio.currentTime = 0;
+ await seeked;
+ });
+
+ // The tab with the unmuted audio should stay at
+ // PROCESS_PRIORITY_FOREGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+ });
+});
+
+/**
+ * Test that if a tab with a WebAudio playing goes into the background,
+ * the process priority does not change, unless that WebAudio context is
+ * suspended.
+ */
+add_task(async function test_web_audio_background_tab() {
+ let originalTab = gBrowser.selectedTab;
+
+ await BrowserTestUtils.withNewTab("https://example.com", async browser => {
+ // Let's synthesize a basic square wave as WebAudio.
+ await SpecialPowers.spawn(browser, [], async () => {
+ let audioCtx = new content.AudioContext();
+ let oscillator = audioCtx.createOscillator();
+ oscillator.type = "square";
+ oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
+ oscillator.connect(audioCtx.destination);
+ oscillator.start();
+ while (audioCtx.state != "running") {
+ info(`wait until AudioContext starts running`);
+ await new Promise(r => (audioCtx.onstatechange = r));
+ }
+ // we'll stash the AudioContext away so that it's easier to access
+ // in the next SpecialPowers.spawn.
+ content.audioCtx = audioCtx;
+ });
+
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // The tab with the WebAudio should stay at
+ // PROCESS_PRIORITY_FOREGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now suspend the WebAudio. This will cause it to stop
+ // playing.
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.audioCtx.suspend();
+ });
+
+ // The tab with the suspended WebAudio should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: tab,
+ toTab: originalTab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+
+ // Now switch back. The initial blank tab should reach
+ // PROCESS_PRIORITY_BACKGROUND when backgrounded.
+ await assertPriorityChangeOnBackground({
+ fromTab: originalTab,
+ toTab: tab,
+ fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
+ });
+ });
+});
+
+/**
+ * Test that foreground tab's process priority isn't changed when going back to
+ * a bfcached session history entry.
+ */
+add_task(async function test_audio_background_tab() {
+ let page1 = "https://example.com";
+ let page2 = page1 + "/?2";
+
+ await BrowserTestUtils.withNewTab(page1, async browser => {
+ let childID = browsingContextChildID(browser.browsingContext);
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new tab should make it prioritized."
+ );
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, page2);
+ BrowserTestUtils.startLoadingURIString(browser, page2);
+ await loaded;
+
+ childID = browsingContextChildID(browser.browsingContext);
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a new page should keep the tab prioritized."
+ );
+
+ let pageShowPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "pageshow"
+ );
+ browser.goBack();
+ await pageShowPromise;
+
+ childID = browsingContextChildID(browser.browsingContext);
+ Assert.equal(
+ gTabPriorityWatcher.currentPriority(childID),
+ PROCESS_PRIORITY_FOREGROUND,
+ "Loading a page from the bfcache should keep the tab prioritized."
+ );
+ });
+});
diff --git a/dom/ipc/tests/browser_bug1646088.js b/dom/ipc/tests/browser_bug1646088.js
new file mode 100644
index 0000000000..18e9afb49e
--- /dev/null
+++ b/dom/ipc/tests/browser_bug1646088.js
@@ -0,0 +1,71 @@
+let dir = getChromeDir(getResolvedURI(gTestPath));
+dir.append("file_dummy.html");
+const uriString = Services.io.newFileURI(dir).spec;
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com",
+ async function (browser) {
+ // Override the browser's `prepareToChangeRemoteness` so that we can delay
+ // the process switch for an indefinite amount of time. This will allow us
+ // to control the timing of the resolve call to trigger the bug.
+ let prepareToChangeCalled = Promise.withResolvers();
+ let finishSwitch = Promise.withResolvers();
+ let oldPrepare = browser.prepareToChangeRemoteness;
+ browser.prepareToChangeRemoteness = async () => {
+ prepareToChangeCalled.resolve();
+ await oldPrepare.call(browser);
+ await finishSwitch.promise;
+ };
+
+ // Begin a process switch, which should cause `prepareToChangeRemoteness` to
+ // be called.
+ // NOTE: This used to avoid BrowserTestUtils.loadURI, as that call would
+ // previously eagerly perform a process switch meaning that the interesting
+ // codepath wouldn't be triggered. Nowadays the process switch codepath
+ // always happens during navigation as required by this test.
+ info("Beginning process switch into file URI process");
+ let browserLoaded = BrowserTestUtils.browserLoaded(browser);
+ BrowserTestUtils.startLoadingURIString(browser, uriString);
+ await prepareToChangeCalled.promise;
+
+ // The tab we opened is now midway through process switching. Open another
+ // browser within the same tab, and immediately close it after the load
+ // finishes.
+ info("Creating new tab loaded in file URI process");
+ let fileProcess;
+ let browserParentDestroyed = Promise.withResolvers();
+ await BrowserTestUtils.withNewTab(
+ uriString,
+ async function (otherBrowser) {
+ let remoteTab = otherBrowser.frameLoader.remoteTab;
+ fileProcess = remoteTab.contentProcessId;
+ info("Loaded test URI in pid: " + fileProcess);
+
+ browserParentDestroyed.resolve(
+ TestUtils.topicObserved(
+ "ipc:browser-destroyed",
+ subject => subject === remoteTab
+ )
+ );
+ }
+ );
+ await browserParentDestroyed.promise;
+
+ // This browser has now been closed, which could cause the file content
+ // process to begin shutting down, despite us process switching into it.
+ // We can now allow the process switch to finish, and wait for the load to
+ // finish as well.
+ info("BrowserParent has been destroyed, finishing process switch");
+ finishSwitch.resolve();
+ await browserLoaded;
+
+ info("Load complete");
+ is(
+ browser.frameLoader.remoteTab.contentProcessId,
+ fileProcess,
+ "Should have loaded in the same file URI process"
+ );
+ }
+ );
+});
diff --git a/dom/ipc/tests/browser_bug1686194.js b/dom/ipc/tests/browser_bug1686194.js
new file mode 100644
index 0000000000..f6acefa2f2
--- /dev/null
+++ b/dom/ipc/tests/browser_bug1686194.js
@@ -0,0 +1,47 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html";
+
+function untilPageTitleChanged() {
+ return new Promise(resolve =>
+ gBrowser.addEventListener("pagetitlechanged", resolve, { once: true })
+ );
+}
+
+add_task(async () => {
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PAGE,
+ });
+
+ const { linkedBrowser } = tab;
+ ok(
+ tab.getAttribute("label").includes("file_dummy.html"),
+ "The title should be the raw path"
+ );
+
+ await Promise.all([
+ SpecialPowers.spawn(linkedBrowser, [], function () {
+ content.document.title = "Title";
+ }),
+ untilPageTitleChanged(),
+ ]);
+
+ is(tab.getAttribute("label"), "Title", "The title should change");
+
+ linkedBrowser.reload();
+
+ await untilPageTitleChanged();
+
+ ok(
+ tab.getAttribute("label").includes("file_dummy.html"),
+ "The title should be the raw path again"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/ipc/tests/browser_cancel_content_js.js b/dom/ipc/tests/browser_cancel_content_js.js
new file mode 100644
index 0000000000..4cb00cd468
--- /dev/null
+++ b/dom/ipc/tests/browser_cancel_content_js.js
@@ -0,0 +1,65 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+requestLongerTimeout(10);
+
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_cancel_content_js.html";
+const NEXT_PAGE = "http://mochi.test:8888/browser/dom/ipc/tests/";
+const JS_URI = "javascript:void(document.title = 'foo')";
+
+async function test_navigation(nextPage, shouldCancel) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.max_script_run_time", 20],
+ // Force a single process so that the navigation will complete in the same
+ // process as the previous page which is running the long-running script.
+ ["dom.ipc.processCount", 1],
+ ["dom.ipc.processCount.webIsolated", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PAGE,
+ });
+
+ const loopEnded = ContentTask.spawn(tab.linkedBrowser, [], async function () {
+ await new Promise(resolve => {
+ content.addEventListener("LongLoopEnded", resolve, {
+ once: true,
+ });
+ });
+ });
+
+ // Wait for the test page's long-running JS loop to start.
+ await ContentTask.spawn(tab.linkedBrowser, [], function () {
+ content.dispatchEvent(new content.Event("StartLongLoop"));
+ });
+
+ info(`navigating to ${nextPage}`);
+ const nextPageLoaded = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "DOMTitleChanged"
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser, nextPage);
+
+ const result = await Promise.race([
+ nextPageLoaded,
+ loopEnded.then(() => "timeout"),
+ ]);
+
+ const timedOut = result === "timeout";
+ if (shouldCancel) {
+ Assert.strictEqual(timedOut, false, "expected next page to be loaded");
+ } else {
+ Assert.strictEqual(timedOut, true, "expected timeout");
+ }
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async () => test_navigation(NEXT_PAGE, true));
+add_task(async () => test_navigation(JS_URI, false));
diff --git a/dom/ipc/tests/browser_child_clipboard_restricted.js b/dom/ipc/tests/browser_child_clipboard_restricted.js
new file mode 100644
index 0000000000..be2d1bca9c
--- /dev/null
+++ b/dom/ipc/tests/browser_child_clipboard_restricted.js
@@ -0,0 +1,93 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+add_task(async function () {
+ // Create a new content tab to make sure the paste is cross-process.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<br>"
+ );
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function (arg) {
+ const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+
+ const string = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ string.data = "blablabla";
+
+ trans.addDataFlavor("text/unknown");
+ trans.setTransferData("text/unknown", string);
+
+ trans.addDataFlavor("text/plain");
+ trans.setTransferData("text/plain", string);
+
+ // Write to clipboard.
+ Services.clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
+ });
+
+ // Wait for known.
+ for (var i = 0; i < 20; i++) {
+ if (
+ Services.clipboard.hasDataMatchingFlavors(
+ ["text/plain"],
+ Services.clipboard.kGlobalClipboard
+ )
+ ) {
+ break;
+ }
+ }
+
+ function readClipboard(flavor) {
+ const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor(flavor);
+ Services.clipboard.getData(
+ trans,
+ Services.clipboard.kGlobalClipboard,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+
+ let data = {};
+ trans.getTransferData(flavor, data);
+ return data.value.QueryInterface(Ci.nsISupportsString).data;
+ }
+
+ ok(
+ Services.clipboard.hasDataMatchingFlavors(
+ ["text/plain"],
+ Services.clipboard.kGlobalClipboard
+ ),
+ "clipboard should have text/plain"
+ );
+
+ is(
+ readClipboard("text/plain"),
+ "blablabla",
+ "matching string for text/plain"
+ );
+
+ ok(
+ !Services.clipboard.hasDataMatchingFlavors(
+ ["text/unknown"],
+ Services.clipboard.kGlobalClipboard
+ ),
+ "clipboard should not have text/unknown"
+ );
+
+ let error = undefined;
+ try {
+ readClipboard("text/unknown");
+ } catch (e) {
+ error = e;
+ }
+ is(typeof error, "object", "reading text/unknown should fail");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/ipc/tests/browser_content_shutdown_with_endless_js.js b/dom/ipc/tests/browser_content_shutdown_with_endless_js.js
new file mode 100644
index 0000000000..bdec55a12c
--- /dev/null
+++ b/dom/ipc/tests/browser_content_shutdown_with_endless_js.js
@@ -0,0 +1,86 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EMPTY_PAGE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html";
+
+const HANG_PAGE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_endless_js.html";
+
+function pushPref(name, val) {
+ return SpecialPowers.pushPrefEnv({ set: [[name, val]] });
+}
+
+async function createAndShutdownContentProcess(url) {
+ info("Create and shutdown a content process for " + url);
+
+ // Launch a new process and load url. Sets up a promise that will resolve
+ // on shutdown.
+ let browserDestroyed = Promise.withResolvers();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ opening: url,
+ waitForLoad: true,
+ forceNewProcess: true,
+ },
+ async function (otherBrowser) {
+ let remoteTab = otherBrowser.frameLoader.remoteTab;
+
+ ok(true, "Content process created.");
+
+ browserDestroyed.resolve(
+ TestUtils.topicObserved(
+ "ipc:browser-destroyed",
+ subject => subject === remoteTab
+ )
+ );
+
+ // Trigger onmessage in the content browser
+ await SpecialPowers.spawn(otherBrowser, [], function () {
+ content.postMessage("LoadedMessage", "*");
+ });
+
+ // Give the content process some extra time before we start its shutdown.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+
+ // withNewTab will start the shutdown of the child process for us
+ }
+ );
+
+ // Now wait for it to really shut down.
+ // If the HANG_PAGE JS is not canceled we will hang here.
+ await browserDestroyed.promise;
+
+ // If we do not hang and get here, we are fine.
+ ok(true, "Shutdown of content process.");
+}
+
+add_task(async () => {
+ // This test is only relevant in e10s.
+ if (!gMultiProcessBrowser) {
+ ok(true, "We are not in multiprocess mode, skipping test.");
+ return;
+ }
+
+ await pushPref("dom.abort_script_on_child_shutdown", true);
+
+ // Ensure the process cache cannot interfere.
+ pushPref("dom.ipc.processPreload.enabled", false);
+ // Ensure we have no cached processes from previous tests.
+ Services.ppmm.releaseCachedProcesses();
+
+ // First let's do a dry run that should always succeed.
+ await createAndShutdownContentProcess(EMPTY_PAGE);
+
+ // Now we will start a shutdown of our content process while our content
+ // script is running an endless loop.
+ //
+ // If the JS does not get interrupted on shutdown, it will cause this test
+ // to hang.
+ await createAndShutdownContentProcess(HANG_PAGE);
+});
diff --git a/dom/ipc/tests/browser_crash_oopiframe.js b/dom/ipc/tests/browser_crash_oopiframe.js
new file mode 100644
index 0000000000..9161b874a9
--- /dev/null
+++ b/dom/ipc/tests/browser_crash_oopiframe.js
@@ -0,0 +1,245 @@
+"use strict";
+
+/**
+ * Opens a number of tabs containing an out-of-process iframe.
+ *
+ * @param numTabs the number of tabs to open.
+ * @returns the browsing context of the iframe in the last tab opened.
+ */
+async function openTestTabs(numTabs) {
+ let iframeBC = null;
+
+ for (let count = 0; count < numTabs; count++) {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:blank",
+ });
+
+ // If we load example.com in an injected subframe, we assume that this
+ // will load in its own subprocess, which we can then crash.
+ iframeBC = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", "http://example.com");
+
+ content.document.body.appendChild(iframe);
+ await ContentTaskUtils.waitForEvent(iframe, "load");
+ return iframe.frameLoader.browsingContext;
+ });
+ }
+
+ return iframeBC;
+}
+
+/**
+ * Helper function for testing frame crashing. Some tabs are opened
+ * containing frames from example.com and then the process for
+ * example.com is crashed. Notifications should apply to each tab
+ * and all should close when one of the notifications is closed.
+ *
+ * @param numTabs the number of tabs to open.
+ */
+async function testFrameCrash(numTabs) {
+ let iframeBC = await openTestTabs(numTabs);
+ let browser = gBrowser.selectedBrowser;
+ let rootBC = browser.browsingContext;
+
+ is(iframeBC.parent, rootBC, "oop frame has root as parent");
+
+ let eventFiredPromise = BrowserTestUtils.waitForEvent(
+ browser,
+ "oop-browser-crashed"
+ );
+
+ BrowserTestUtils.crashFrame(
+ browser,
+ true /* shouldShowTabCrashPage */,
+ true /* shouldClearMinidumps */,
+ iframeBC
+ );
+
+ let notificationPromise = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "subframe-crashed"
+ );
+
+ info("Waiting for oop-browser-crashed event.");
+ await eventFiredPromise.then(event => {
+ ok(!event.isTopFrame, "should not be reporting top-level frame crash");
+ Assert.notEqual(event.childID, 0, "childID is non-zero");
+
+ isnot(
+ event.browsingContextId,
+ rootBC,
+ "top frame browsing context id not expected."
+ );
+
+ is(
+ event.browsingContextId,
+ iframeBC.id,
+ "oop frame browsing context id expected."
+ );
+ });
+
+ if (numTabs == 1) {
+ // The BrowsingContext is re-used, but the window global might still be
+ // getting set up at this point, so wait until it's been initialized.
+ let { subject: windowGlobal } = await BrowserUtils.promiseObserved(
+ "window-global-created",
+ wgp => wgp.documentURI.spec.startsWith("about:framecrashed")
+ );
+
+ is(
+ windowGlobal,
+ iframeBC.currentWindowGlobal,
+ "Resolved on expected window global"
+ );
+
+ let newIframeURI = await SpecialPowers.spawn(iframeBC, [], async () => {
+ return content.document.documentURI;
+ });
+
+ ok(
+ newIframeURI.startsWith("about:framecrashed"),
+ "The iframe is now pointing at about:framecrashed"
+ );
+
+ let title = await SpecialPowers.spawn(iframeBC, [], async () => {
+ await content.document.l10n.ready;
+ return content.document.documentElement.getAttribute("title");
+ });
+ ok(title, "The iframe has a non-empty tooltip.");
+ }
+
+ // Next, check that the crash notification bar has appeared.
+ await notificationPromise;
+
+ for (let count = 1; count <= numTabs; count++) {
+ let notificationBox = gBrowser.getNotificationBox(gBrowser.browsers[count]);
+ let notification = notificationBox.currentNotification;
+ ok(notification, "Notification " + count + " should be visible");
+ is(
+ notification.getAttribute("value"),
+ "subframe-crashed",
+ "Should be showing the right notification" + count
+ );
+
+ let buttons = notification.buttonContainer.querySelectorAll(
+ ".notification-button"
+ );
+ is(
+ buttons.length,
+ 1,
+ "Notification " + count + " should have only one button."
+ );
+ let links = notification.supportLinkEls;
+ is(
+ links.length,
+ 1,
+ "Notification " + count + " should have only one link."
+ );
+ ok(
+ notification.messageText.textContent.length,
+ "Notification " + count + " should have a crash msg."
+ );
+ }
+
+ // Press the ignore button on the visible notification.
+ let notificationBox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
+ let notification = notificationBox.currentNotification;
+
+ // Make sure all of the notifications were closed when one of them was closed.
+ let closedPromises = [];
+ for (let count = 1; count <= numTabs; count++) {
+ let nb = gBrowser.getNotificationBox(gBrowser.browsers[count]);
+ closedPromises.push(
+ BrowserTestUtils.waitForMutationCondition(
+ nb.stack,
+ { childList: true },
+ () => !nb.currentNotification
+ )
+ );
+ }
+
+ notification.dismiss();
+ await Promise.all(closedPromises);
+
+ for (let count = 1; count <= numTabs; count++) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+}
+
+/**
+ * In this test, we crash an out-of-process iframe and
+ * verify that :
+ * 1. the "oop-browser-crashed" event is dispatched with
+ * the browsing context of the crashed oop subframe.
+ * 2. the crashed subframe is now pointing at "about:framecrashed"
+ * page.
+ */
+add_task(async function test_crashframe() {
+ // Open a new window with fission enabled.
+ ok(
+ SpecialPowers.useRemoteSubframes,
+ "This test only makes sense of we can use OOP iframes."
+ );
+
+ // Create the crash reporting directory if it doesn't yet exist, otherwise, a failure
+ // sometimes occurs. See bug 1687855 for fixing this.
+ const uAppDataPath = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ let path = PathUtils.join(uAppDataPath, "Crash Reports", "pending");
+ await IOUtils.makeDirectory(path, { ignoreExisting: true });
+
+ // Test both one tab and when four tabs are opened.
+ await testFrameCrash(1);
+ await testFrameCrash(4);
+});
+
+// This test checks that no notification shows when there is no minidump available. It
+// simulates the steps that occur during a crash, once with a dumpID and once without.
+add_task(async function test_nominidump() {
+ for (let dumpID of [null, "8888"]) {
+ let iframeBC = await openTestTabs(1);
+
+ let childID = iframeBC.currentWindowGlobal.domProcess.childID;
+
+ let notificationPromise;
+ if (dumpID) {
+ notificationPromise = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ gBrowser.selectedBrowser,
+ "subframe-crashed"
+ );
+ }
+
+ gBrowser.selectedBrowser.dispatchEvent(
+ new FrameCrashedEvent("oop-browser-crashed", {
+ browsingContextID: iframeBC,
+ childID,
+ isTopFrame: false,
+ bubbles: true,
+ })
+ );
+
+ let bag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+ );
+ bag.setProperty("abnormal", "true");
+ bag.setProperty("childID", iframeBC.currentWindowGlobal.domProcess.childID);
+ if (dumpID) {
+ bag.setProperty("dumpID", dumpID);
+ }
+
+ Services.obs.notifyObservers(bag, "ipc:content-shutdown");
+
+ await notificationPromise;
+ let notificationBox = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
+ let notification = notificationBox.currentNotification;
+ ok(
+ dumpID ? notification : !notification,
+ "notification shown for browser with no minidump"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
diff --git a/dom/ipc/tests/browser_domainPolicy.js b/dom/ipc/tests/browser_domainPolicy.js
new file mode 100644
index 0000000000..988288f950
--- /dev/null
+++ b/dom/ipc/tests/browser_domainPolicy.js
@@ -0,0 +1,187 @@
+// This test waits for a lot of subframe loads, causing it to take a long time,
+// especially with Fission enabled.
+requestLongerTimeout(2);
+
+const BASE_FILE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_domainPolicy_base.html";
+const SCRIPT_PATH = "/browser/dom/ipc/tests/file_disableScript.html";
+
+const TEST_POLICY = {
+ exceptions: ["http://test1.example.com", "http://example.com"],
+ superExceptions: ["http://test2.example.org", "https://test1.example.com"],
+ exempt: [
+ "http://test1.example.com",
+ "http://example.com",
+ "http://test2.example.org",
+ "http://sub1.test2.example.org",
+ "https://sub1.test1.example.com",
+ ],
+ notExempt: [
+ "http://test2.example.com",
+ "http://sub1.test1.example.com",
+ "http://www.example.com",
+ "https://test2.example.com",
+ "https://example.com",
+ "http://test1.example.org",
+ ],
+};
+
+// To make sure we never leave up an activated domain policy after a failed
+// test, let's make this global.
+var policy;
+
+function activateDomainPolicy(isBlock) {
+ policy = Services.scriptSecurityManager.activateDomainPolicy();
+
+ if (isBlock === undefined) {
+ return;
+ }
+
+ let set = isBlock ? policy.blocklist : policy.allowlist;
+ for (let e of TEST_POLICY.exceptions) {
+ set.add(makeURI(e));
+ }
+
+ let superSet = isBlock ? policy.superBlocklist : policy.superAllowlist;
+ for (let e of TEST_POLICY.superExceptions) {
+ superSet.add(makeURI(e));
+ }
+}
+
+function deactivateDomainPolicy() {
+ if (policy) {
+ policy.deactivate();
+ policy = null;
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.pagethumbnails.capturing_disabled", false]],
+ });
+
+ registerCleanupFunction(() => {
+ deactivateDomainPolicy();
+ });
+});
+
+add_task(async function test_domainPolicy() {
+ function test(testFunc, { activateFirst, isBlock }) {
+ if (activateFirst) {
+ activateDomainPolicy(isBlock);
+ }
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ opening: BASE_FILE,
+ forceNewProcess: true,
+ },
+ async browser => {
+ if (!activateFirst) {
+ activateDomainPolicy(isBlock);
+ }
+ await testFunc(browser);
+ deactivateDomainPolicy();
+ }
+ );
+ }
+
+ async function testDomain(browser, domain, expectEnabled = false) {
+ function navigateFrame() {
+ let url = domain + SCRIPT_PATH;
+ return SpecialPowers.spawn(browser, [url], async src => {
+ let iframe = content.document.getElementById("root");
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve, { once: true });
+ iframe.src = src;
+ });
+ return iframe.browsingContext;
+ });
+ }
+
+ function checkScriptEnabled(bc) {
+ return SpecialPowers.spawn(bc, [expectEnabled], enabled => {
+ content.wrappedJSObject.gFiredOnclick = false;
+ content.document.body.dispatchEvent(new content.Event("click"));
+ Assert.equal(
+ content.wrappedJSObject.gFiredOnclick,
+ enabled,
+ `Checking script-enabled for ${content.name} (${content.location})`
+ );
+ });
+ }
+
+ let browsingContext = await navigateFrame();
+ return checkScriptEnabled(browsingContext);
+ }
+
+ async function testList(browser, list, expectEnabled) {
+ // Run these sequentially to avoid navigating multiple domains at once.
+ for (let domain of list) {
+ await testDomain(browser, domain, expectEnabled);
+ }
+ }
+
+ info("1. Testing simple blocklist policy");
+
+ info("1A. Creating child process first, activating domainPolicy after");
+ await test(
+ async browser => {
+ policy.blocklist.add(Services.io.newURI("http://example.com"));
+ await testDomain(browser, "http://example.com");
+ },
+ { activateFirst: false }
+ );
+
+ info("1B. Activating domainPolicy first, creating child process after");
+ await test(
+ async browser => {
+ policy.blocklist.add(Services.io.newURI("http://example.com"));
+ await testDomain(browser, "http://example.com");
+ },
+ { activateFirst: true }
+ );
+
+ info("2. Testing Blocklist-style Domain Policy");
+
+ info("2A. Activating domainPolicy first, creating child process after");
+ await test(
+ async browser => {
+ await testList(browser, TEST_POLICY.notExempt, true);
+ await testList(browser, TEST_POLICY.exempt, false);
+ },
+ { activateFirst: true, isBlock: true }
+ );
+
+ info("2B. Creating child process first, activating domainPolicy after");
+ await test(
+ async browser => {
+ await testList(browser, TEST_POLICY.notExempt, true);
+ await testList(browser, TEST_POLICY.exempt, false);
+ },
+ { activateFirst: false, isBlock: true }
+ );
+
+ info("3. Testing Allowlist-style Domain Policy");
+ await SpecialPowers.pushPrefEnv({ set: [["javascript.enabled", false]] });
+
+ info("3A. Activating domainPolicy first, creating child process after");
+ await test(
+ async browser => {
+ await testList(browser, TEST_POLICY.notExempt, false);
+ await testList(browser, TEST_POLICY.exempt, true);
+ },
+ { activateFirst: true, isBlock: false }
+ );
+
+ info("3B. Creating child process first, activating domainPolicy after");
+ await test(
+ async browser => {
+ await testList(browser, TEST_POLICY.notExempt, false);
+ await testList(browser, TEST_POLICY.exempt, true);
+ },
+ { activateFirst: false, isBlock: false }
+ );
+
+ finish();
+});
diff --git a/dom/ipc/tests/browser_gc_schedule.js b/dom/ipc/tests/browser_gc_schedule.js
new file mode 100644
index 0000000000..8b44c98eae
--- /dev/null
+++ b/dom/ipc/tests/browser_gc_schedule.js
@@ -0,0 +1,379 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html";
+
+async function waitForGCBegin() {
+ var waitTopic = "garbage-collector-begin";
+ var observer = {};
+
+ info("Waiting for " + waitTopic);
+ // This fixes a ReferenceError for Date, it's weird.
+ ok(Date.now(), "Date.now()");
+ var when = await new Promise(resolve => {
+ observer.observe = function (subject, topic, data) {
+ resolve(Date.now());
+ };
+
+ Services.obs.addObserver(observer, waitTopic);
+ });
+
+ Services.obs.removeObserver(observer, waitTopic);
+
+ // This delay attempts to make the time stamps unique.
+ do {
+ var now = Date.now();
+ } while (when + 5 > now);
+
+ return when;
+}
+
+async function waitForGCEnd() {
+ var waitTopic = "garbage-collector-end";
+ var observer = {};
+
+ info("Waiting for " + waitTopic);
+ // This fixes a ReferenceError for Date, it's weird.
+ ok(Date.now(), "Date.now()");
+ let when = await new Promise(resolve => {
+ observer.observe = function (subject, topic, data) {
+ resolve(Date.now());
+ };
+
+ Services.obs.addObserver(observer, waitTopic);
+ });
+
+ Services.obs.removeObserver(observer, waitTopic);
+
+ do {
+ var now = Date.now();
+ } while (when + 5 > now);
+
+ return when;
+}
+
+function getProcessID() {
+ return Services.appinfo.processID;
+}
+
+async function resolveInOrder(promisesAndStates) {
+ var order = [];
+ var promises = [];
+
+ for (let p of promisesAndStates) {
+ promises.push(
+ p.promise.then(when => {
+ info(`Tab: ${p.tab} did ${p.state}`);
+ order.push({ tab: p.tab, state: p.state, when });
+ })
+ );
+ }
+
+ await Promise.all(promises);
+
+ return order;
+}
+
+// Check that the list of events returned by resolveInOrder are in a
+// sensible order.
+function checkOneAtATime(events) {
+ var cur = null;
+ var lastWhen = null;
+
+ info("Checking order of events");
+ for (const e of events) {
+ ok(e.state === "begin" || e.state === "end", "event.state is good");
+ Assert.notStrictEqual(e.tab, undefined, "event.tab exists");
+
+ if (lastWhen) {
+ // We need these in sorted order so that the other checks here make
+ // sense.
+ Assert.lessOrEqual(
+ lastWhen,
+ e.when,
+ `Unsorted events, last: ${lastWhen}, this: ${e.when}`
+ );
+ }
+ lastWhen = e.when;
+
+ if (e.state === "begin") {
+ is(cur, null, `GC can begin on tab ${e.tab}`);
+ cur = e.tab;
+ } else {
+ is(e.tab, cur, `GC can end on tab ${e.tab}`);
+ cur = null;
+ }
+ }
+
+ is(cur, null, "No GC left running");
+}
+
+function checkAllCompleted(events, expectTabsCompleted) {
+ var tabsCompleted = events.filter(e => e.state === "end").map(e => e.tab);
+
+ for (var t of expectTabsCompleted) {
+ ok(tabsCompleted.includes(t), `Tab ${t} did a GC`);
+ }
+}
+
+async function setupTabsAndOneForForeground(num_tabs) {
+ ++num_tabs;
+ var pids = [];
+
+ const parent_pid = getProcessID();
+ info("Parent process PID is " + parent_pid);
+
+ const tabs = await Promise.all(
+ Array(num_tabs)
+ .fill()
+ .map(_ => {
+ return BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PAGE,
+ forceNewProcess: true,
+ });
+ })
+ );
+
+ for (const [i, tab] of Object.entries(tabs)) {
+ const tab_pid = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ getProcessID
+ );
+
+ info(`Tab ${i} pid is ${tab_pid}`);
+ isnot(parent_pid, tab_pid, `Tab ${i} is in content process`);
+ ok(!pids.includes(tab_pid), `Tab ${i} is in a distinct process`);
+
+ pids.push(tab_pid);
+ }
+
+ // Since calling openNewForegroundTab several times in a row doesn't update
+ // process priorities correctly, we need to explicitly switch tabs.
+ for (let tab of tabs) {
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+ }
+
+ return tabs;
+}
+
+function doContentRunNextCollectionTimer() {
+ content.windowUtils.pokeGC("PAGE_HIDE");
+ content.windowUtils.runNextCollectorTimer("PAGE_HIDE");
+}
+
+function startNextCollection(
+ tab,
+ tab_num,
+ waits,
+ fn = doContentRunNextCollectionTimer
+) {
+ var browser = tab.linkedBrowser;
+
+ // Finish any currently running GC.
+ SpecialPowers.spawn(browser, [], () => {
+ SpecialPowers.Cu.getJSTestingFunctions().finishgc();
+ });
+
+ if (tab.selected) {
+ // One isn't expected to use the return value with foreground tab!
+ return {};
+ }
+
+ var waitBegin = SpecialPowers.spawn(browser, [], waitForGCBegin);
+ var waitEnd = SpecialPowers.spawn(browser, [], waitForGCEnd);
+ waits.push({ promise: waitBegin, tab: tab_num, state: "begin" });
+ waits.push({ promise: waitEnd, tab: tab_num, state: "end" });
+
+ SpecialPowers.spawn(browser, [], fn);
+
+ // Return these so that the abort GC test can wait for the begin.
+ return { waitBegin, waitEnd };
+}
+
+add_task(async function gcOneAtATime() {
+ SpecialPowers.pushPrefEnv({
+ set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
+ });
+
+ const num_tabs = 12;
+ var tabs = await setupTabsAndOneForForeground(num_tabs);
+
+ info("Tabs ready, Asking for GCs");
+ var waits = [];
+ for (var i = 0; i < num_tabs; i++) {
+ startNextCollection(tabs[i], i, waits);
+ }
+
+ let order = await resolveInOrder(waits);
+ // We need these in the order they actually occurred, so far that's how
+ // they're returned, but we'll sort them to be sure.
+ order.sort((e1, e2) => e1.when - e2.when);
+ checkOneAtATime(order);
+ checkAllCompleted(
+ order,
+ Array.from({ length: num_tabs }, (_, n) => n)
+ );
+
+ for (var tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function gcAbort() {
+ SpecialPowers.pushPrefEnv({
+ set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
+ });
+
+ const num_tabs = 2;
+ var tabs = await setupTabsAndOneForForeground(num_tabs);
+
+ info("Tabs ready, Asking for GCs");
+ var waits = [];
+
+ var tab0Waits = startNextCollection(tabs[0], 0, waits, () => {
+ SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
+ });
+ await tab0Waits.waitBegin;
+
+ // Tab 0 has started a GC. Now we schedule a GC in tab one. It must not
+ // begin yet (but we don't check that, gcOneAtATime is assumed to check
+ // this.
+ startNextCollection(tabs[1], 1, waits);
+
+ // Request that tab 0 abort, this test checks that tab 1 can now begin.
+ SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => {
+ SpecialPowers.Cu.getJSTestingFunctions().abortgc();
+ });
+
+ let order = await resolveInOrder(waits);
+ // We need these in the order they actually occurred, so far that's how
+ // they're returned, but we'll sort them to be sure.
+ order.sort((e1, e2) => e1.when - e2.when);
+ checkOneAtATime(order);
+ checkAllCompleted(
+ order,
+ Array.from({ length: num_tabs }, (_, n) => n)
+ );
+
+ for (var tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function gcJSInitiatedDuring() {
+ SpecialPowers.pushPrefEnv({
+ set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
+ });
+
+ const num_tabs = 3;
+ var tabs = await setupTabsAndOneForForeground(num_tabs);
+
+ info("Tabs ready, Asking for GCs");
+ var waits = [];
+
+ // Start a GC on tab 0 to consume the scheduler's "token". Zeal mode 10
+ // will cause it to run in many slices.
+ var tab0Waits = startNextCollection(tabs[0], 0, waits, () => {
+ if (SpecialPowers.Cu.getJSTestingFunctions().gczeal) {
+ SpecialPowers.Cu.getJSTestingFunctions().gczeal(10);
+ }
+ SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
+ });
+ await tab0Waits.waitBegin;
+ info("GC on tab 0 has begun");
+
+ // Request a GC in tab 1, this will be blocked by the ongoing GC in tab 0.
+ var tab1Waits = startNextCollection(tabs[1], 1, waits);
+
+ // Force a GC to start in tab 1. This won't wait for tab 0.
+ SpecialPowers.spawn(tabs[1].linkedBrowser, [], () => {
+ SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
+ });
+
+ await tab1Waits.waitBegin;
+ info("GC on tab 1 has begun");
+
+ // The GC in tab 0 should still be running.
+ var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => {
+ return SpecialPowers.Cu.getJSTestingFunctions().gcstate();
+ });
+ info("State of Tab 0 GC is " + state);
+ isnot(state, "NotActive", "GC is active in tab 0");
+
+ // Let the GCs complete, verify that a GC in a 3rd tab can acquire a token.
+ startNextCollection(tabs[2], 2, waits);
+
+ let order = await resolveInOrder(waits);
+ info("All GCs finished");
+ checkAllCompleted(
+ order,
+ Array.from({ length: num_tabs }, (_, n) => n)
+ );
+
+ for (var tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ SpecialPowers.popPrefEnv();
+});
+
+add_task(async function gcJSInitiatedBefore() {
+ SpecialPowers.pushPrefEnv({
+ set: [["javascript.options.concurrent_multiprocess_gcs.max", 1]],
+ });
+
+ const num_tabs = 8;
+ var tabs = await setupTabsAndOneForForeground(num_tabs);
+
+ info("Tabs ready");
+ var waits = [];
+
+ // Start a GC on tab 0 to consume the scheduler's first "token". Zeal mode 10
+ // will cause it to run in many slices.
+ info("Force a JS-initiated GC in tab 0");
+ var tab0Waits = startNextCollection(tabs[0], 0, waits, () => {
+ if (SpecialPowers.Cu.getJSTestingFunctions().gczeal) {
+ SpecialPowers.Cu.getJSTestingFunctions().gczeal(10);
+ }
+ SpecialPowers.Cu.getJSTestingFunctions().gcslice(1);
+ });
+ await tab0Waits.waitBegin;
+
+ info("Request GCs in remaining tabs");
+ for (var i = 1; i < num_tabs; i++) {
+ startNextCollection(tabs[i], i, waits);
+ }
+
+ // The GC in tab 0 should still be running.
+ var state = await SpecialPowers.spawn(tabs[0].linkedBrowser, [], () => {
+ return SpecialPowers.Cu.getJSTestingFunctions().gcstate();
+ });
+ info("State is " + state);
+ isnot(state, "NotActive", "GC is active in tab 0");
+
+ let order = await resolveInOrder(waits);
+ // We need these in the order they actually occurred, so far that's how
+ // they're returned, but we'll sort them to be sure.
+ order.sort((e1, e2) => e1.when - e2.when);
+ checkOneAtATime(order);
+ checkAllCompleted(
+ order,
+ Array.from({ length: num_tabs }, (_, n) => n)
+ );
+
+ for (var tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ SpecialPowers.popPrefEnv();
+});
diff --git a/dom/ipc/tests/browser_hide_tooltip.js b/dom/ipc/tests/browser_hide_tooltip.js
new file mode 100644
index 0000000000..1b7f7c56b9
--- /dev/null
+++ b/dom/ipc/tests/browser_hide_tooltip.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_hiding_tooltip() {
+ let page1 = "data:text/html,<html title='title'><body>page 1<body></html>";
+ let page2 = "data:text/html,<html><body>page 2</body></html>";
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: page1,
+ });
+
+ let popup = new Promise(function (resolve) {
+ window.addEventListener("popupshown", resolve, { once: true });
+ });
+ // Fire a mousemove to trigger the tooltip.
+ EventUtils.synthesizeMouseAtCenter(gBrowser.selectedBrowser, {
+ type: "mousemove",
+ });
+ await popup;
+
+ let hiding = new Promise(function (resolve) {
+ window.addEventListener("popuphiding", resolve, { once: true });
+ });
+ let loaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ page2
+ );
+ BrowserTestUtils.startLoadingURIString(gBrowser, page2);
+ await loaded;
+ await hiding;
+
+ ok(true, "Should have hidden the tooltip");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/ipc/tests/browser_layers_unloaded_while_interruptingJS.js b/dom/ipc/tests/browser_layers_unloaded_while_interruptingJS.js
new file mode 100644
index 0000000000..9903055858
--- /dev/null
+++ b/dom/ipc/tests/browser_layers_unloaded_while_interruptingJS.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_check_layers_cleared() {
+ let initialTab = gBrowser.selectedTab;
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await ContentTask.spawn(browser, null, () => {
+ return new Promise(resolve => {
+ content.requestAnimationFrame(() => {
+ content.setTimeout(
+ "let start = performance.now(); while (performance.now() < start + 5000);"
+ );
+ resolve();
+ });
+ });
+ });
+ let layersCleared = BrowserTestUtils.waitForEvent(
+ window,
+ "MozLayerTreeCleared"
+ );
+ let startWaiting = performance.now();
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+ await layersCleared;
+ Assert.less(
+ performance.now(),
+ startWaiting + 2000,
+ "MozLayerTreeCleared should be dispatched while the script is still running"
+ );
+ });
+});
diff --git a/dom/ipc/tests/browser_memory_distribution_telemetry.js b/dom/ipc/tests/browser_memory_distribution_telemetry.js
new file mode 100644
index 0000000000..f4568fe05f
--- /dev/null
+++ b/dom/ipc/tests/browser_memory_distribution_telemetry.js
@@ -0,0 +1,93 @@
+"use strict";
+
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+const DUMMY_PAGE_DATA_URI = `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Dummy</title>
+ </head>
+ <body>
+ <h1 id='header'>Just a regular everyday normal page.</h1>
+ </body>
+ </html>`;
+
+/**
+ * Tests the MEMORY_DISTRIBUTION_AMONG_CONTENT probe by opening a few tabs, then triggering
+ * the memory probes and waiting for the "gather-memory-telemetry-finished" notification.
+ */
+add_task(async function test_memory_distribution() {
+ waitForExplicitFinish();
+
+ if (SpecialPowers.getIntPref("dom.ipc.processCount", 1) < 2) {
+ ok(true, "Skip this test if e10s-multi is disabled.");
+ finish();
+ return;
+ }
+
+ Services.telemetry.canRecordExtended = true;
+
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "MEMORY_DISTRIBUTION_AMONG_CONTENT"
+ );
+ histogram.clear();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_PAGE_DATA_URI
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_PAGE_DATA_URI
+ );
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_PAGE_DATA_URI
+ );
+
+ let finishedGathering = new Promise(resolve => {
+ let obs = function () {
+ Services.obs.removeObserver(obs, "gather-memory-telemetry-finished");
+ resolve();
+ };
+ Services.obs.addObserver(obs, "gather-memory-telemetry-finished");
+ });
+
+ TelemetrySession.getPayload();
+
+ await finishedGathering;
+
+ let s = histogram.snapshot();
+ ok("0 - 10 tabs" in s, "We should have some samples by now in this bucket.");
+ for (var key in s) {
+ is(key, "0 - 10 tabs");
+ let fewTabsSnapshot = s[key];
+ Assert.greater(
+ fewTabsSnapshot.sum,
+ 0,
+ "Zero difference between all the content processes is unlikely, what happened?"
+ );
+ Assert.less(
+ fewTabsSnapshot.sum,
+ 80,
+ "20 percentage difference on average is unlikely, what happened?"
+ );
+ let values = fewTabsSnapshot.values;
+ for (let [bucket, value] of Object.entries(values)) {
+ if (bucket >= 10) {
+ // If this check fails it means that one of the content processes uses at least 20% more or 20% less than the mean.
+ is(value, 0, "All the buckets above 10 should be empty");
+ }
+ }
+ }
+
+ histogram.clear();
+
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab1);
+ finish();
+});
diff --git a/dom/ipc/tests/browser_pbrowser_creation_failure.js b/dom/ipc/tests/browser_pbrowser_creation_failure.js
new file mode 100644
index 0000000000..d4f3b8fdd5
--- /dev/null
+++ b/dom/ipc/tests/browser_pbrowser_creation_failure.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_subframe_pbrowser_creation_failure() {
+ await BrowserTestUtils.withNewTab(
+ "https://example.com/document-builder.sjs?html=<iframe></iframe>",
+ async browser => {
+ let bcid = await SpecialPowers.spawn(browser, [], () => {
+ return content.document.body.querySelector("iframe").browsingContext.id;
+ });
+
+ // We currently have no known way to trigger PBrowser creation failure,
+ // other than to use this custom pref for the purpose.
+ info(`enabling failPBrowserCreation for browsingContext: ${bcid}`);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.testOnly.failPBrowserCreation.enabled", true],
+ [
+ "browser.tabs.remote.testOnly.failPBrowserCreation.browsingContext",
+ `${bcid}`,
+ ],
+ ],
+ });
+
+ let eventFiredPromise = BrowserTestUtils.waitForEvent(
+ browser,
+ "oop-browser-crashed"
+ );
+
+ info("triggering navigation which will fail pbrowser creation");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.querySelector("iframe").src =
+ "https://example.org/document-builder.sjs?html=frame";
+ });
+
+ info("Waiting for oop-browser-crashed event.");
+ let event = await eventFiredPromise;
+ ok(!event.isTopFrame, "should be reporting subframe crash");
+ Assert.equal(
+ event.childID,
+ 0,
+ "childID should be zero, as no process actually crashed"
+ );
+ is(event.browsingContextId, bcid, "bcid should match");
+
+ let { subject: windowGlobal } = await BrowserUtils.promiseObserved(
+ "window-global-created",
+ wgp => wgp.documentURI.spec.startsWith("about:framecrashed")
+ );
+ is(windowGlobal.browsingContext.id, bcid, "bcid is correct");
+
+ await SpecialPowers.popPrefEnv();
+ }
+ );
+});
diff --git a/dom/ipc/tests/browser_subframesPreferUsed.js b/dom/ipc/tests/browser_subframesPreferUsed.js
new file mode 100644
index 0000000000..f2f9ed2593
--- /dev/null
+++ b/dom/ipc/tests/browser_subframesPreferUsed.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ok(
+ Services.appinfo.fissionAutostart,
+ "this test requires fission to function!"
+);
+
+function documentURL(origin, html) {
+ let params = new URLSearchParams();
+ params.append("html", html.trim());
+ return `${origin}/document-builder.sjs?${params.toString()}`;
+}
+
+async function singleTest(preferUsed) {
+ info(`running test with preferUsed=${preferUsed}`);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount.webIsolated", 4],
+ ["browser.tabs.remote.subframesPreferUsed", preferUsed],
+ ],
+ });
+
+ const TEST_URL = documentURL(
+ "https://example.com",
+ `<iframe src=${JSON.stringify(
+ documentURL("https://example.org", `<h1>iframe</h1>`)
+ )}></iframe>`
+ );
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser1 => {
+ is(browser1.browsingContext.children.length, 1);
+ let topProc1 = browser1.browsingContext.currentWindowGlobal.domProcess;
+ let frameProc1 =
+ browser1.browsingContext.children[0].currentWindowGlobal.domProcess;
+ isnot(
+ topProc1.childID,
+ frameProc1.childID,
+ "the frame should be in a separate process"
+ );
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser2 => {
+ is(browser2.browsingContext.children.length, 1);
+ let topProc2 = browser2.browsingContext.currentWindowGlobal.domProcess;
+ let frameProc2 =
+ browser2.browsingContext.children[0].currentWindowGlobal.domProcess;
+ isnot(
+ topProc2.childID,
+ frameProc2.childID,
+ "the frame should be in a separate process"
+ );
+
+ // Compare processes used for the two tabs.
+ isnot(
+ topProc1.childID,
+ topProc2.childID,
+ "the toplevel windows should be loaded in separate processes"
+ );
+ if (preferUsed) {
+ is(
+ frameProc1.childID,
+ frameProc2.childID,
+ "the iframes should load in the same process with subframesPreferUsed"
+ );
+ } else {
+ isnot(
+ frameProc1.childID,
+ frameProc2.childID,
+ "the iframes should load in different processes without subframesPreferUsed"
+ );
+ }
+ });
+ });
+}
+
+add_task(async function test_preferUsed() {
+ await singleTest(true);
+});
+
+add_task(async function test_noPreferUsed() {
+ await singleTest(false);
+});
diff --git a/dom/ipc/tests/browser_very_fission.js b/dom/ipc/tests/browser_very_fission.js
new file mode 100644
index 0000000000..582fe00133
--- /dev/null
+++ b/dom/ipc/tests/browser_very_fission.js
@@ -0,0 +1,38 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This test creates a large number of content processes as a
+// regression test for bug 1635451.
+
+const TEST_PAGE =
+ "http://mochi.test:8888/browser/dom/ipc/tests/file_dummy.html";
+
+const NUM_TABS = 256;
+
+add_task(async () => {
+ let promises = [];
+ for (let i = 0; i < NUM_TABS; ++i) {
+ promises.push(
+ BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PAGE,
+ waitForLoad: true,
+ forceNewProcess: true,
+ })
+ );
+ }
+
+ let tabs = [];
+ for (const p of promises) {
+ tabs.push(await p);
+ }
+
+ ok(true, "All of the tabs loaded");
+
+ for (const t of tabs) {
+ BrowserTestUtils.removeTab(t);
+ }
+});
diff --git a/dom/ipc/tests/browser_wpi_base.js b/dom/ipc/tests/browser_wpi_base.js
new file mode 100644
index 0000000000..7a01c9a161
--- /dev/null
+++ b/dom/ipc/tests/browser_wpi_base.js
@@ -0,0 +1,305 @@
+// This test is fission-only! Make that clear before continuing, to avoid
+// confusing failures.
+ok(
+ Services.appinfo.fissionAutostart,
+ "this test requires fission to function!"
+);
+
+requestLongerTimeout(2);
+
+const WebContentIsolationStrategy = {
+ IsolateNothing: 0,
+ IsolateEverything: 1,
+ IsolateHighValue: 2,
+};
+
+const COM_ORIGIN = "https://example.com";
+const ORG_ORIGIN = "https://example.org";
+const MOZ_ORIGIN = "https://www.mozilla.org";
+
+// Helper for building document-builder.sjs URLs which have specific headers &
+// HTML content.
+function documentURL(origin, headers, html) {
+ let params = new URLSearchParams();
+ params.append("html", html.trim());
+ for (const [key, value] of Object.entries(headers)) {
+ params.append("headers", `${key}:${value}`);
+ }
+ return `${origin}/document-builder.sjs?${params.toString()}`;
+}
+
+async function testTreeRemoteTypes(name, testpage) {
+ // Use document-builder.sjs to build up the expected document tree.
+ function buildURL(path, page) {
+ let html = `<h1>${path}</h1>`;
+ for (let i = 0; i < page.children.length; ++i) {
+ const inner = buildURL(`${path}[${i}]`, page.children[i]);
+ html += `<iframe src=${JSON.stringify(inner)}></iframe>`;
+ }
+ return documentURL(page.origin, page.headers, html);
+ }
+ const url = buildURL(name, testpage);
+
+ // Load the tab and confirm that properties of the loaded documents match
+ // expectation.
+ await BrowserTestUtils.withNewTab(url, async browser => {
+ let stack = [
+ {
+ path: name,
+ bc: browser.browsingContext,
+ ...testpage,
+ },
+ ];
+
+ while (stack.length) {
+ const { path, bc, remoteType, children, origin } = stack.pop();
+ is(
+ Services.scriptSecurityManager.createContentPrincipal(
+ bc.currentWindowGlobal.documentURI,
+ {}
+ ).originNoSuffix,
+ origin,
+ `Frame ${path} has expected originNoSuffix`
+ );
+ is(
+ bc.currentWindowGlobal.domProcess.remoteType,
+ remoteType,
+ `Frame ${path} has expected remote type`
+ );
+ is(
+ bc.children.length,
+ children.length,
+ `Frame ${path} has the expected number of children`
+ );
+ for (let i = 0; i < bc.children.length; ++i) {
+ stack.push({
+ path: `${path}[${i}]`,
+ bc: bc.children[i],
+ ...children[i],
+ });
+ }
+ }
+ });
+}
+
+function mkTestPage({
+ comRemoteType,
+ orgRemoteType,
+ mozRemoteType,
+ topOrigin,
+ topHeaders = {},
+ frameHeaders = {},
+}) {
+ const topRemoteType = {
+ [COM_ORIGIN]: comRemoteType,
+ [ORG_ORIGIN]: orgRemoteType,
+ [MOZ_ORIGIN]: mozRemoteType,
+ }[topOrigin];
+
+ const innerChildren = [
+ {
+ origin: COM_ORIGIN,
+ headers: frameHeaders,
+ remoteType: comRemoteType,
+ children: [],
+ },
+ {
+ origin: ORG_ORIGIN,
+ headers: frameHeaders,
+ remoteType: orgRemoteType,
+ children: [],
+ },
+ {
+ origin: MOZ_ORIGIN,
+ headers: frameHeaders,
+ remoteType: mozRemoteType,
+ children: [],
+ },
+ ];
+
+ return {
+ origin: topOrigin,
+ headers: topHeaders,
+ remoteType: topRemoteType,
+ children: [
+ {
+ origin: COM_ORIGIN,
+ headers: frameHeaders,
+ remoteType: comRemoteType,
+ children: [...innerChildren],
+ },
+ {
+ origin: ORG_ORIGIN,
+ headers: frameHeaders,
+ remoteType: orgRemoteType,
+ children: [...innerChildren],
+ },
+ {
+ origin: MOZ_ORIGIN,
+ headers: frameHeaders,
+ remoteType: mozRemoteType,
+ children: [...innerChildren],
+ },
+ ],
+ };
+}
+
+const heuristics = [
+ {
+ name: "coop",
+ setup_com: async expected => {
+ // Set the COOP header, and load
+ await testTreeRemoteTypes(
+ "com_set_coop",
+ mkTestPage({
+ topOrigin: COM_ORIGIN,
+ topHeaders: { "Cross-Origin-Opener-Policy": "same-origin" },
+ comRemoteType: expected.com_high,
+ orgRemoteType: expected.org_normal,
+ mozRemoteType: expected.moz_normal,
+ })
+ );
+ },
+ run_extra_test: async expected => {
+ // Load with both the COOP and COEP headers set.
+ await testTreeRemoteTypes(
+ "com_coop_coep",
+ mkTestPage({
+ topOrigin: COM_ORIGIN,
+ topHeaders: {
+ "Cross-Origin-Opener-Policy": "same-origin",
+ "Cross-Origin-Embedder-Policy": "require-corp",
+ },
+ frameHeaders: {
+ "Cross-Origin-Embedder-Policy": "require-corp",
+ "Cross-Origin-Resource-Policy": "cross-origin",
+ },
+ comRemoteType: expected.com_coop_coep,
+ orgRemoteType: expected.org_coop_coep,
+ mozRemoteType: expected.moz_coop_coep,
+ })
+ );
+ },
+ },
+ {
+ name: "hasSavedLogin",
+ setup_com: async expected => {
+ // add .com to the password manager
+ let LoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+ );
+ await Services.logins.addLoginAsync(
+ new LoginInfo(COM_ORIGIN, "", null, "username", "password", "", "")
+ );
+
+ // Init login detection service to trigger fetching logins
+ let loginDetection = Cc[
+ "@mozilla.org/login-detection-service;1"
+ ].createInstance(Ci.nsILoginDetectionService);
+ loginDetection.init();
+
+ await TestUtils.waitForCondition(() => {
+ let x = loginDetection.isLoginsLoaded();
+ return x;
+ }, "waiting for loading logins from the password manager");
+ },
+ },
+ {
+ name: "isLoggedIn",
+ setup_com: async expected => {
+ let p = new Promise(resolve => {
+ Services.obs.addObserver(function obs() {
+ Services.obs.removeObserver(
+ obs,
+ "passwordmgr-form-submission-detected"
+ );
+ resolve();
+ }, "passwordmgr-form-submission-detected");
+ });
+
+ const TEST_URL = documentURL(
+ COM_ORIGIN,
+ {},
+ `<form>
+ <input value="username">
+ <input type="password" value="password">
+ <input type="submit">
+ </form>`
+ );
+
+ // submit the form to simulate the login behavior
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], async () => {
+ content.document.querySelector("form").submit();
+ });
+ });
+ await p;
+ },
+ },
+];
+
+async function do_tests(expected) {
+ for (let heuristic of heuristics) {
+ info(`Starting ${heuristic.name} test`);
+ // Clear all site-specific data, as we don't want to have any high-value site
+ // permissions from any previous iterations.
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ // Loads for basic URLs with no special headers set.
+ await testTreeRemoteTypes(
+ "basic_com",
+ mkTestPage({
+ topOrigin: COM_ORIGIN,
+ comRemoteType: expected.com_normal,
+ orgRemoteType: expected.org_normal,
+ mozRemoteType: expected.moz_normal,
+ })
+ );
+
+ await testTreeRemoteTypes(
+ "basic_org",
+ mkTestPage({
+ topOrigin: ORG_ORIGIN,
+ comRemoteType: expected.com_normal,
+ orgRemoteType: expected.org_normal,
+ mozRemoteType: expected.moz_normal,
+ })
+ );
+
+ info(`Setting up ${heuristic.name} test`);
+ await heuristic.setup_com(expected);
+
+ // Load again after the heuristic is triggered
+ info(`Running ${heuristic.name} tests after setup`);
+ await testTreeRemoteTypes(
+ `com_after_${heuristic.name}`,
+ mkTestPage({
+ topOrigin: COM_ORIGIN,
+ comRemoteType: expected.com_high,
+ orgRemoteType: expected.org_normal,
+ mozRemoteType: expected.moz_normal,
+ })
+ );
+
+ // Load again with a .org toplevel
+ await testTreeRemoteTypes(
+ `org_after_${heuristic.name}`,
+ mkTestPage({
+ topOrigin: ORG_ORIGIN,
+ comRemoteType: expected.com_high,
+ orgRemoteType: expected.org_normal,
+ mozRemoteType: expected.moz_normal,
+ })
+ );
+
+ // Run heuristic dependent tests
+ if (heuristic.run_extra_test) {
+ info(`Running extra tests for ${heuristic.name}`);
+ await heuristic.run_extra_test(expected);
+ }
+ }
+}
diff --git a/dom/ipc/tests/browser_wpi_isolate_everything.js b/dom/ipc/tests/browser_wpi_isolate_everything.js
new file mode 100644
index 0000000000..e902fec9d0
--- /dev/null
+++ b/dom/ipc/tests/browser_wpi_isolate_everything.js
@@ -0,0 +1,27 @@
+// Import this in order to use `do_tests()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/dom/ipc/tests/browser_wpi_base.js",
+ this
+);
+
+add_task(async function test_isolate_everything() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.separatedMozillaDomains", "mozilla.org"],
+ [
+ "fission.webContentIsolationStrategy",
+ WebContentIsolationStrategy.IsolateEverything,
+ ],
+ ],
+ });
+
+ await do_tests({
+ com_normal: "webIsolated=https://example.com",
+ org_normal: "webIsolated=https://example.org",
+ moz_normal: "privilegedmozilla",
+ com_high: "webIsolated=https://example.com",
+ com_coop_coep: "webCOOP+COEP=https://example.com",
+ org_coop_coep: "webCOOP+COEP=https://example.org",
+ moz_coop_coep: "privilegedmozilla",
+ });
+});
diff --git a/dom/ipc/tests/browser_wpi_isolate_high_value.js b/dom/ipc/tests/browser_wpi_isolate_high_value.js
new file mode 100644
index 0000000000..bf6b99d5f5
--- /dev/null
+++ b/dom/ipc/tests/browser_wpi_isolate_high_value.js
@@ -0,0 +1,27 @@
+// Import this in order to use `do_tests()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/dom/ipc/tests/browser_wpi_base.js",
+ this
+);
+
+add_task(async function test_isolate_high_value() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.separatedMozillaDomains", "mozilla.org"],
+ [
+ "fission.webContentIsolationStrategy",
+ WebContentIsolationStrategy.IsolateHighValue,
+ ],
+ ],
+ });
+
+ await do_tests({
+ com_normal: "web",
+ org_normal: "web",
+ moz_normal: "privilegedmozilla",
+ com_high: "webIsolated=https://example.com",
+ com_coop_coep: "webCOOP+COEP=https://example.com",
+ org_coop_coep: "webCOOP+COEP=https://example.org",
+ moz_coop_coep: "privilegedmozilla",
+ });
+});
diff --git a/dom/ipc/tests/browser_wpi_isolate_nothing.js b/dom/ipc/tests/browser_wpi_isolate_nothing.js
new file mode 100644
index 0000000000..afd5e51640
--- /dev/null
+++ b/dom/ipc/tests/browser_wpi_isolate_nothing.js
@@ -0,0 +1,27 @@
+// Import this in order to use `do_tests()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/dom/ipc/tests/browser_wpi_base.js",
+ this
+);
+
+add_task(async function test_isolate_nothing() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.remote.separatedMozillaDomains", "mozilla.org"],
+ [
+ "fission.webContentIsolationStrategy",
+ WebContentIsolationStrategy.IsolateNothing,
+ ],
+ ],
+ });
+
+ await do_tests({
+ com_normal: "web",
+ org_normal: "web",
+ moz_normal: "privilegedmozilla",
+ com_high: "web",
+ com_coop_coep: "webCOOP+COEP=https://example.com",
+ org_coop_coep: "webCOOP+COEP=https://example.org",
+ moz_coop_coep: "privilegedmozilla",
+ });
+});
diff --git a/dom/ipc/tests/chrome.toml b/dom/ipc/tests/chrome.toml
new file mode 100644
index 0000000000..79f3e2fb22
--- /dev/null
+++ b/dom/ipc/tests/chrome.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = ["process_error.xhtml"]
+
+["test_process_error.xhtml"]
+skip-if = ["!crashreporter"]
diff --git a/dom/ipc/tests/file_broadcast_currenturi_onload.html b/dom/ipc/tests/file_broadcast_currenturi_onload.html
new file mode 100644
index 0000000000..b92c46c944
--- /dev/null
+++ b/dom/ipc/tests/file_broadcast_currenturi_onload.html
@@ -0,0 +1,66 @@
+
+<!doctype html>
+<body>
+<script>
+
+const url = new URL(location.href);
+
+// Create a popup to broadcast the load's completion to the test document.
+//
+// NOTE: We're using a popup to ensure that the new document has the same origin
+// (http://mochi.test:8888/) as the original test document, so that we can use a
+// BroadcastChannel to communicate with the test page. We can't use an iframe as
+// the mixed content blocker will prevent embedding this URL in https://
+// documents.
+function sendPayload(payload) {
+ let broadcastURL = new URL(url.pathname, "http://mochi.test:8888/");
+ broadcastURL.search = "?payload=" + encodeURIComponent(JSON.stringify(payload));
+ window.open(broadcastURL.href);
+}
+
+async function getURIs() {
+ // Run the test and fetch the relevant information.
+ const browsingContext = SpecialPowers.wrap(window).browsingContext;
+ let [docURI, curURI] = await SpecialPowers.spawnChrome(
+ [browsingContext.id], async id => {
+ let bc = BrowsingContext.get(id);
+ return [
+ bc.currentWindowGlobal.documentURI.spec,
+ bc.currentURI.spec,
+ ];
+ }
+ );
+ return { location: location.href, docURI, curURI };
+}
+
+addEventListener("load", async e => {
+ // If a payload parameter was included, just send the message.
+ const payloadStr = url.searchParams.get("payload");
+ if (payloadStr) {
+ const chan = new BroadcastChannel("test_broadcast_onload");
+ chan.postMessage(JSON.parse(payloadStr));
+ window.close();
+ return;
+ }
+
+ // collect the initial set of URIs
+ const result1 = await getURIs();
+
+ const pushstateURL = new URL("after_pushstate", url.href);
+ history.pushState({}, "After PushState!", pushstateURL.href);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ // Collect the set of URIs after pushstate
+ const result2 = await getURIs();
+
+ window.location.hash = "#after_hashchange";
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ // Collect the set of URIs after a hash change
+ const result3 = await getURIs();
+
+ sendPayload([result1, result2, result3]);
+ window.close();
+});
+</script>
+</body>
diff --git a/dom/ipc/tests/file_cancel_content_js.html b/dom/ipc/tests/file_cancel_content_js.html
new file mode 100644
index 0000000000..d2caf03c6a
--- /dev/null
+++ b/dom/ipc/tests/file_cancel_content_js.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Wait for it...</title>
+ </head>
+ <body>
+ Try to go to another page.
+ <script>
+ addEventListener("StartLongLoop", function() {
+ setTimeout(() => {
+ const start = Date.now();
+ while (Date.now() - start < 7500);
+ window.dispatchEvent(new CustomEvent("LongLoopEnded"));
+ });
+ });
+ </script>
+ </body>
+</html>
diff --git a/dom/ipc/tests/file_cross_frame.html b/dom/ipc/tests/file_cross_frame.html
new file mode 100644
index 0000000000..b52d920dd0
--- /dev/null
+++ b/dom/ipc/tests/file_cross_frame.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Different-origin iframe</title>
+</head>
+<body>
+<iframe id="testIFrame" src="https://example.org/browser/dom/ipc/tests/file_dummy.html"></iframe>
+<i>I am a web page</i>
+</div>
+</body>
+</html>
diff --git a/dom/ipc/tests/file_disableScript.html b/dom/ipc/tests/file_disableScript.html
new file mode 100644
index 0000000000..f4888cd586
--- /dev/null
+++ b/dom/ipc/tests/file_disableScript.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+var gFiredOnload = false;
+var gFiredOnclick = false;
+</script>
+</head>
+<body onload="gFiredOnload = true;" onclick="gFiredOnclick = true;">
+</body>
+</html>
diff --git a/dom/ipc/tests/file_domainPolicy_base.html b/dom/ipc/tests/file_domainPolicy_base.html
new file mode 100644
index 0000000000..6e3ec7aec4
--- /dev/null
+++ b/dom/ipc/tests/file_domainPolicy_base.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<iframe id="root" name="root"/>
+</body>
+</html>
diff --git a/dom/ipc/tests/file_dummy.html b/dom/ipc/tests/file_dummy.html
new file mode 100644
index 0000000000..c8701dae7d
--- /dev/null
+++ b/dom/ipc/tests/file_dummy.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<html>
+<head><meta charset="utf-8"></head>
+<body>
+ <h1>This is a dummy file</h1>
+</body>
+</html>
diff --git a/dom/ipc/tests/file_endless_js.html b/dom/ipc/tests/file_endless_js.html
new file mode 100644
index 0000000000..926fb1d8ab
--- /dev/null
+++ b/dom/ipc/tests/file_endless_js.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head><meta charset="utf-8"></head>
+<script>
+ function hang(m) {
+ let i = 1;
+ while (i > 0) {
+ i = Date.now();
+ }
+ }
+
+ onmessage = hang;
+</script>
+<body>
+ <h1>This is an endless JS loop</h1>
+</body>
+</html>
diff --git a/dom/ipc/tests/mochitest.toml b/dom/ipc/tests/mochitest.toml
new file mode 100644
index 0000000000..b1b347408d
--- /dev/null
+++ b/dom/ipc/tests/mochitest.toml
@@ -0,0 +1,30 @@
+[DEFAULT]
+
+["test_Preallocated.html"]
+skip-if = [
+ "os == 'android'",
+ "tsan", # Bug 1525959. tsan: Bug 1683730
+]
+
+["test_bcg_processes.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_browsingcontext_currenturi.html"]
+support-files = ["file_broadcast_currenturi_onload.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_temporaryfile_stream.html"]
+skip-if = ["os == 'android'"]
+support-files = [
+ "blob_verify.sjs",
+ "!/dom/canvas/test/captureStream_common.js",
+]
+
+["test_window_open_discarded_bc.html"]
+skip-if = ["os == 'android'"]
diff --git a/dom/ipc/tests/process_error.xhtml b/dom/ipc/tests/process_error.xhtml
new file mode 100644
index 0000000000..3d57a3f456
--- /dev/null
+++ b/dom/ipc/tests/process_error.xhtml
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ orient="vertical">
+
+ <browser id="thebrowser" type="content" remote="true" />
+ <script type="application/javascript"><![CDATA[
+ const {BrowserTestUtils} = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+ );
+
+ const ok = window.arguments[0].ok;
+ const is = window.arguments[0].is;
+ const done = window.arguments[0].done;
+ const SimpleTest = window.arguments[0].SimpleTest;
+
+ // Parse test options.
+ const url = new URL(document.location);
+ const crashType = url.searchParams.get("crashType");
+
+ // Allow the browser to get connected before using the messageManager to cause
+ // a crash:
+ addEventListener("DOMContentLoaded", () => {
+ let browser = document.getElementById('thebrowser');
+
+ let observerPromise = new Promise(resolve => {
+ let crashObserver = (subject, topic, data) => {
+ is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
+ ok(subject instanceof Ci.nsIPropertyBag2,
+ 'Subject implements nsIPropertyBag2.');
+
+ var dumpID;
+ if ('nsICrashReporter' in Ci) {
+ dumpID = subject.getPropertyAsAString('dumpID');
+ ok(dumpID, "dumpID is present and not an empty string");
+ }
+
+ Services.obs.removeObserver(crashObserver, 'ipc:content-shutdown');
+ resolve();
+ }
+
+ Services.obs.addObserver(crashObserver, 'ipc:content-shutdown');
+ });
+
+ let browsingContextId = browser.frameLoader.browsingContext.id;
+
+ let eventFiredPromise = BrowserTestUtils.waitForEvent(browser, "oop-browser-crashed");
+ let eventPromise = eventFiredPromise.then(event => {
+ is(event.browsingContextId, browsingContextId,
+ "Expected the right browsing context id on the oop-browser-crashed event.");
+ })
+
+ BrowserTestUtils.crashFrame(browser, true, false, /* Default browsing context */ null, { crashType });
+
+ Promise.all([observerPromise, eventPromise]).then(done);
+ });
+ ]]></script>
+
+</window>
diff --git a/dom/ipc/tests/test_Preallocated.html b/dom/ipc/tests/test_Preallocated.html
new file mode 100644
index 0000000000..fb719995f5
--- /dev/null
+++ b/dom/ipc/tests/test_Preallocated.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the preallocated process starts up.
+-->
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../browserElementTestHelpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+function expectProcessCreated() {
+ /* eslint-env mozilla/chrome-script */
+ return new Promise(resolve => {
+ function parentExpectProcessCreated() {
+ let topic = "ipc:content-initializing";
+ let obs = { observe() {
+ Services.obs.removeObserver(obs, topic);
+ sendAsyncMessage("process-created");
+ }};
+ Services.obs.addObserver(obs, topic);
+ }
+
+ let helper = SpecialPowers.loadChromeScript(parentExpectProcessCreated);
+ SimpleTest.registerCleanupFunction(function() { helper.destroy(); });
+ helper.addMessageListener("process-created", resolve);
+ });
+}
+
+expectProcessCreated().then(() => {
+ ok(true, "Process creation detected.");
+ SimpleTest.finish();
+});
+
+// Kill existing preallocated process.
+SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processPrelaunch.enabled", false]]}).then(() => {
+ // Make sure we have the capacity to launch preallocated process.
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processCount", 100]]}).then(() => {
+ // Enable preallocated process and run the test.
+ SpecialPowers.pushPrefEnv({"set": [["dom.ipc.processPrelaunch.enabled", true]]});
+ });
+});
+</script>
+</body>
+</html>
diff --git a/dom/ipc/tests/test_bcg_processes.html b/dom/ipc/tests/test_bcg_processes.html
new file mode 100644
index 0000000000..8f68aa4a89
--- /dev/null
+++ b/dom/ipc/tests/test_bcg_processes.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+"use strict";
+
+add_task(async function main_test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount.webIsolated", 10]],
+ });
+
+ let frame1 = document.createElement("iframe");
+ frame1.src = "http://example.com";
+ document.body.appendChild(frame1);
+ await new Promise(resolve => {
+ frame1.addEventListener("load", resolve, { once: true })
+ });
+ info("frame 1 loaded");
+
+ let frame2 = document.createElement("iframe");
+ frame2.src = "http://example.com";
+ document.body.appendChild(frame2);
+ await new Promise(resolve => {
+ frame2.addEventListener("load", resolve, { once: true })
+ });
+ info("frame 2 loaded");
+
+ let id1 = await SpecialPowers.spawn(frame1, [], () => {
+ return ChromeUtils.domProcessChild.childId;
+ });
+ let id2 = await SpecialPowers.spawn(frame2, [], () => {
+ return ChromeUtils.domProcessChild.childId;
+ });
+
+ is(id1, id2, "childID for example.com subframes should match");
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/ipc/tests/test_blob_sliced_from_child_process.js b/dom/ipc/tests/test_blob_sliced_from_child_process.js
new file mode 100644
index 0000000000..8a7a56088d
--- /dev/null
+++ b/dom/ipc/tests/test_blob_sliced_from_child_process.js
@@ -0,0 +1,140 @@
+"use strict";
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+function childFrameScript() {
+ /* eslint-env mozilla/frame-script */
+ "use strict";
+
+ const messageName = "test:blob-slice-test";
+ const blobData = ["So", " ", "many", " ", "blobs!"];
+ const blobType = "text/plain";
+
+ let blob = new Blob(blobData, { type: blobType });
+
+ let firstSliceStart = blobData[0].length + blobData[1].length;
+ let firstSliceEnd = firstSliceStart + blobData[2].length;
+
+ let slice = blob.slice(firstSliceStart, firstSliceEnd, blobType);
+
+ let secondSliceStart = blobData[2].indexOf("a");
+ let secondSliceEnd = secondSliceStart + 2;
+
+ slice = slice.slice(secondSliceStart, secondSliceEnd, blobType);
+
+ sendAsyncMessage(messageName, { blob });
+ sendAsyncMessage(messageName, { slice });
+}
+
+add_task(async function test() {
+ let page = await XPCShellContentUtils.loadContentPage(
+ "data:text/html,<!DOCTYPE HTML><html><body></body></html>",
+ {
+ remote: true,
+ }
+ );
+
+ page.loadFrameScript(childFrameScript);
+
+ const messageName = "test:blob-slice-test";
+ const blobData = ["So", " ", "many", " ", "blobs!"];
+ const blobText = blobData.join("");
+ const blobType = "text/plain";
+
+ const sliceText = "an";
+
+ let receivedBlob = false;
+ let receivedSlice = false;
+
+ let resolveBlob, resolveSlice;
+ let blobPromise = new Promise(resolve => {
+ resolveBlob = resolve;
+ });
+ let slicePromise = new Promise(resolve => {
+ resolveSlice = resolve;
+ });
+
+ let mm = page.browser.messageManager;
+ mm.addMessageListener(messageName, function (message) {
+ if ("blob" in message.data) {
+ equal(receivedBlob, false, "Have not yet received Blob");
+ equal(receivedSlice, false, "Have not yet received Slice");
+
+ receivedBlob = true;
+
+ let blob = message.data.blob;
+
+ ok(Blob.isInstance(blob), "Received a Blob");
+ equal(blob.size, blobText.length, "Blob has correct size");
+ equal(blob.type, blobType, "Blob has correct type");
+
+ let slice = blob.slice(
+ blobText.length - blobData[blobData.length - 1].length,
+ blob.size,
+ blobType
+ );
+
+ ok(Blob.isInstance(slice), "Slice returned a Blob");
+ equal(
+ slice.size,
+ blobData[blobData.length - 1].length,
+ "Slice has correct size"
+ );
+ equal(slice.type, blobType, "Slice has correct type");
+
+ let reader = new FileReader();
+ reader.onload = function () {
+ equal(
+ reader.result,
+ blobData[blobData.length - 1],
+ "Slice has correct data"
+ );
+
+ resolveBlob();
+ };
+ reader.readAsText(slice);
+ } else if ("slice" in message.data) {
+ equal(receivedBlob, true, "Already received Blob");
+ equal(receivedSlice, false, "Have not yet received Slice");
+
+ receivedSlice = true;
+
+ let slice = message.data.slice;
+
+ ok(Blob.isInstance(slice), "Received a Blob for slice");
+ equal(slice.size, sliceText.length, "Slice has correct size");
+ equal(slice.type, blobType, "Slice has correct type");
+
+ let reader = new FileReader();
+ reader.onload = function () {
+ equal(reader.result, sliceText, "Slice has correct data");
+
+ let slice2 = slice.slice(1, 2, blobType);
+
+ ok(Blob.isInstance(slice2), "Slice returned a Blob");
+ equal(slice2.size, 1, "Slice has correct size");
+ equal(slice2.type, blobType, "Slice has correct type");
+
+ let reader2 = new FileReader();
+ reader2.onload = function () {
+ equal(reader2.result, sliceText[1], "Slice has correct data");
+
+ resolveSlice();
+ };
+ reader2.readAsText(slice2);
+ };
+ reader.readAsText(slice);
+ } else {
+ ok(false, "Received a bad message: " + JSON.stringify(message.data));
+ }
+ });
+
+ await blobPromise;
+ await slicePromise;
+
+ await page.close();
+});
diff --git a/dom/ipc/tests/test_blob_sliced_from_parent_process.js b/dom/ipc/tests/test_blob_sliced_from_parent_process.js
new file mode 100644
index 0000000000..e196a6986c
--- /dev/null
+++ b/dom/ipc/tests/test_blob_sliced_from_parent_process.js
@@ -0,0 +1,167 @@
+"use strict";
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+function childFrameScript() {
+ /* eslint-env mozilla/frame-script */
+ const messageName = "test:blob-slice-test";
+ const blobData = ["So", " ", "many", " ", "blobs!"];
+ const blobText = blobData.join("");
+ const blobType = "text/plain";
+
+ const sliceText = "an";
+
+ function info(msg) {
+ sendAsyncMessage(messageName, { op: "info", msg });
+ }
+
+ function ok(condition, name, diag) {
+ sendAsyncMessage(messageName, { op: "ok", condition, name, diag });
+ }
+
+ function is(a, b, name) {
+ let pass = a == b;
+ let diag = pass ? "" : "got " + a + ", expected " + b;
+ ok(pass, name, diag);
+ }
+
+ function finish(result) {
+ sendAsyncMessage(messageName, { op: "done", result });
+ }
+
+ function grabAndContinue(arg) {
+ testGenerator.next(arg);
+ }
+
+ function* testSteps() {
+ addMessageListener(messageName, grabAndContinue);
+ let message = yield undefined;
+
+ let blob = message.data;
+
+ ok(Blob.isInstance(blob), "Received a Blob");
+ is(blob.size, blobText.length, "Blob has correct length");
+ is(blob.type, blobType, "Blob has correct type");
+
+ info("Reading blob");
+
+ let reader = new FileReader();
+ reader.addEventListener("load", grabAndContinue);
+ reader.readAsText(blob);
+
+ yield undefined;
+
+ is(reader.result, blobText, "Blob has correct data");
+
+ let firstSliceStart = blobData[0].length + blobData[1].length;
+ let firstSliceEnd = firstSliceStart + blobData[2].length;
+
+ let slice = blob.slice(firstSliceStart, firstSliceEnd, blobType);
+
+ ok(Blob.isInstance(slice), "Slice returned a Blob");
+ is(slice.size, blobData[2].length, "Slice has correct length");
+ is(slice.type, blobType, "Slice has correct type");
+
+ info("Reading slice");
+
+ reader = new FileReader();
+ reader.addEventListener("load", grabAndContinue);
+ reader.readAsText(slice);
+
+ yield undefined;
+
+ is(reader.result, blobData[2], "Slice has correct data");
+
+ let secondSliceStart = blobData[2].indexOf("a");
+ let secondSliceEnd = secondSliceStart + sliceText.length;
+
+ slice = slice.slice(secondSliceStart, secondSliceEnd, blobType);
+
+ ok(Blob.isInstance(slice), "Second slice returned a Blob");
+ is(slice.size, sliceText.length, "Second slice has correct length");
+ is(slice.type, blobType, "Second slice has correct type");
+
+ info("Sending second slice");
+ finish(slice);
+ }
+
+ let testGenerator = testSteps();
+ testGenerator.next();
+}
+
+add_task(async function test() {
+ let page = await XPCShellContentUtils.loadContentPage(
+ "data:text/html,<!DOCTYPE HTML><html><body></body></html>",
+ {
+ remote: true,
+ }
+ );
+
+ page.loadFrameScript(childFrameScript);
+
+ const messageName = "test:blob-slice-test";
+ const blobData = ["So", " ", "many", " ", "blobs!"];
+ const blobType = "text/plain";
+
+ const sliceText = "an";
+
+ await new Promise(resolve => {
+ function grabAndContinue(arg) {
+ testGenerator.next(arg);
+ }
+
+ function* testSteps() {
+ let slice = yield undefined;
+
+ ok(Blob.isInstance(slice), "Received a Blob");
+ equal(slice.size, sliceText.length, "Slice has correct size");
+ equal(slice.type, blobType, "Slice has correct type");
+
+ let reader = new FileReader();
+ reader.onload = grabAndContinue;
+ reader.readAsText(slice);
+ yield undefined;
+
+ equal(reader.result, sliceText, "Slice has correct data");
+ resolve();
+ }
+
+ let testGenerator = testSteps();
+ testGenerator.next();
+
+ let mm = page.browser.messageManager;
+ mm.addMessageListener(messageName, function (message) {
+ let data = message.data;
+ switch (data.op) {
+ case "info": {
+ info(data.msg);
+ break;
+ }
+
+ case "ok": {
+ ok(data.condition, data.name + " - " + data.diag);
+ break;
+ }
+
+ case "done": {
+ testGenerator.next(data.result);
+ break;
+ }
+
+ default: {
+ ok(false, "Unknown op: " + data.op);
+ resolve();
+ }
+ }
+ });
+
+ let blob = new Blob(blobData, { type: blobType });
+ mm.sendAsyncMessage(messageName, blob);
+ });
+
+ await page.close();
+});
diff --git a/dom/ipc/tests/test_browsingcontext_currenturi.html b/dom/ipc/tests/test_browsingcontext_currenturi.html
new file mode 100644
index 0000000000..b03af96b20
--- /dev/null
+++ b/dom/ipc/tests/test_browsingcontext_currenturi.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<iframe id="tls1frame" src="https://tls1.example.com/"></iframe>
+
+<script>
+"use strict";
+
+add_task(async function test_frame() {
+ let win = SpecialPowers.wrap(window);
+ info(`id=${win.browsingContext.id}`);
+ let [docURI, curURI] = await SpecialPowers.spawnChrome([win.browsingContext.id], async id => {
+ let bc = BrowsingContext.get(id);
+ return [
+ bc.currentWindowGlobal.documentURI.spec,
+ bc.currentURI.spec,
+ ];
+ });
+ info(`docURI=${docURI}, curURI=${curURI}`);
+ is(window.location.href, curURI, "curURI has the expected value");
+ is(window.location.href, docURI, "documentURI has the expected value");
+});
+
+add_task(async function test_tls1_frame() {
+ let expframe = SpecialPowers.wrap(document.getElementById("tls1frame"));
+ let [docURI, curURI] = await SpecialPowers.spawnChrome(
+ [expframe.browsingContext.id], async id => {
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+
+ let bc = BrowsingContext.get(id);
+
+ // awkwardly wait for the current window global to update to the error page.
+ // would be nice to do just about anything else here...
+ await TestUtils.waitForCondition(
+ () =>
+ bc.currentURI && bc.currentURI.spec != "about:blank" &&
+ bc.currentWindowGlobal && bc.currentWindowGlobal.documentURI.spec != "about:blank",
+ "waiting for current window global to be non-initial");
+
+ info(`currentWindowGlobal has updated in the parent!`);
+ return [
+ bc.currentWindowGlobal.documentURI.spec,
+ bc.currentURI.spec,
+ ];
+ });
+
+ info(`docURI=${docURI}, curURI=${curURI}`);
+ is(curURI, "https://tls1.example.com/", "curURI has expected value");
+ ok(docURI.startsWith("about:neterror"), "documentURI starts with about:neterror");
+});
+
+let BROADCAST_ONLOAD_URL =
+ new URL("file_broadcast_currenturi_onload.html", location.href);
+
+async function broadcastLoadTest(baseURI, callback) {
+ // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+ // Acquire storage access permission here so that the BroadcastChannel used to
+ // communicate with the opened windows works in xorigin tests. Otherwise,
+ // the iframe containing this page is isolated from first-party storage access,
+ // which isolates BroadcastChannel communication.
+ if (isXOrigin) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]],
+ });
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await SpecialPowers.addPermission(
+ "storageAccessAPI",
+ true,
+ window.location.href
+ );
+ await SpecialPowers.wrap(document).requestStorageAccess();
+ }
+ let loaded = new Promise(resolve => {
+ let chan = new BroadcastChannel("test_broadcast_onload");
+ chan.onmessage = event => {
+ resolve(event.data);
+ };
+ });
+ let srcURL = new URL(BROADCAST_ONLOAD_URL.pathname, baseURI);
+ callback(srcURL.href);
+
+ let results = await loaded;
+ for (let { location, curURI, docURI } of results) {
+ info(`location=${location}, docURI=${docURI}, curURI=${curURI}`);
+ is(location, curURI, "curURI has expected value");
+ is(location, docURI, "documentURI has expected value");
+ }
+}
+
+async function normalFrameLoadTest(base) {
+ await broadcastLoadTest(base, src => {
+ let frame = document.createElement("iframe");
+ frame.src = src;
+ document.body.appendChild(frame);
+ });
+}
+
+async function normalPopupLoadTest(base, flags = "") {
+ await broadcastLoadTest(base, src => {
+ window.open(src, null, flags);
+ });
+}
+
+add_task(async function test_sameorigin_frame() {
+ await normalFrameLoadTest(location.href);
+})
+
+add_task(async function test_crossorigin_frame() {
+ await normalFrameLoadTest("https://example.com");
+});
+
+add_task(async function test_sameorigin_popup() {
+ await normalPopupLoadTest(location.href);
+ await normalPopupLoadTest(location.href, "noopener");
+});
+
+add_task(async function test_crossorigin_popup() {
+ await normalPopupLoadTest("https://example.com");
+ await normalPopupLoadTest("https://example.com", "noopener");
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/ipc/tests/test_bug1086684.js b/dom/ipc/tests/test_bug1086684.js
new file mode 100644
index 0000000000..8a34906686
--- /dev/null
+++ b/dom/ipc/tests/test_bug1086684.js
@@ -0,0 +1,99 @@
+"use strict";
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+const childFramePath = "/file_bug1086684.html";
+const childFrameURL = "http://example.com" + childFramePath;
+
+const childFrameContents = `<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+</head>
+<body>
+<div id="content">
+ <input type="file" id="f">
+</div>
+</body>
+</html>`;
+
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["example.com"],
+});
+server.registerPathHandler(childFramePath, (request, response) => {
+ response.write(childFrameContents);
+});
+
+function childFrameScript() {
+ /* eslint-env mozilla/frame-script */
+ "use strict";
+
+ let { MockFilePicker } = ChromeUtils.importESModule(
+ "resource://testing-common/MockFilePicker.sys.mjs"
+ );
+
+ function parentReady(message) {
+ MockFilePicker.init(content);
+ MockFilePicker.setFiles([message.data.file]);
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ let input = content.document.getElementById("f");
+ input.addEventListener("change", () => {
+ MockFilePicker.cleanup();
+ let value = input.value;
+ message.target.sendAsyncMessage("testBug1086684:childDone", { value });
+ });
+
+ input.focus();
+ input.click();
+ }
+
+ addMessageListener("testBug1086684:parentReady", function (message) {
+ parentReady(message);
+ });
+}
+
+add_task(async function () {
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+ let page = await XPCShellContentUtils.loadContentPage(childFrameURL, {
+ remote: true,
+ });
+
+ page.loadFrameScript(childFrameScript);
+
+ await new Promise(resolve => {
+ let test;
+ function* testStructure(mm) {
+ let value;
+
+ function testDone(msg) {
+ test.next(msg.data.value);
+ }
+
+ mm.addMessageListener("testBug1086684:childDone", testDone);
+
+ let blob = new Blob([]);
+ let file = new File([blob], "helloworld.txt", { type: "text/plain" });
+
+ mm.sendAsyncMessage("testBug1086684:parentReady", { file });
+ value = yield;
+
+ // Note that the "helloworld.txt" passed in above doesn't affect the
+ // 'value' getter. Because we're mocking a file using a blob, we ask the
+ // blob for its path, which is the empty string.
+ equal(value, "", "got the right answer and didn't crash");
+
+ resolve();
+ }
+
+ test = testStructure(page.browser.messageManager);
+ test.next();
+ });
+
+ await page.close();
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/dom/ipc/tests/test_child_docshell.js b/dom/ipc/tests/test_child_docshell.js
new file mode 100644
index 0000000000..ee79a509dc
--- /dev/null
+++ b/dom/ipc/tests/test_child_docshell.js
@@ -0,0 +1,90 @@
+"use strict";
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+add_task(async function test() {
+ let page = await XPCShellContentUtils.loadContentPage("about:blank", {
+ remote: true,
+ });
+
+ await new Promise(resolve => {
+ let mm = page.browser.messageManager;
+ mm.addMessageListener("chromeEventHandler", function (msg) {
+ var result = msg.json;
+ equal(
+ result.processType,
+ Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT,
+ "The frame script is running in a real distinct child process"
+ );
+ ok(
+ result.hasCorrectInterface,
+ "docshell.chromeEventHandler has EventTarget interface"
+ );
+ });
+
+ mm.addMessageListener("DOMWindowCreatedReceived", function (msg) {
+ ok(true, "the chrome event handler looks functional");
+ var result = msg.json;
+ ok(
+ result.stableChromeEventHandler,
+ "docShell.chromeEventHandler is stable"
+ );
+ ok(result.iframeHasNewDocShell, "iframe spawns a new docShell");
+ ok(
+ result.iframeHasSameChromeEventHandler,
+ "but iframe has the same chrome event handler"
+ );
+ resolve();
+ });
+
+ // Inject a frame script in the child process:
+ page.loadFrameScript(async function () {
+ /* eslint-env mozilla/frame-script */
+ var chromeEventHandler = docShell.chromeEventHandler;
+ sendAsyncMessage("chromeEventHandler", {
+ processType: Services.appinfo.processType,
+ hasCorrectInterface:
+ chromeEventHandler && EventTarget.isInstance(chromeEventHandler),
+ });
+
+ /*
+ Ensure that this chromeEventHandler actually works,
+ by creating a new window and listening for its DOMWindowCreated event
+ */
+ chromeEventHandler.addEventListener(
+ "DOMWindowCreated",
+ function listener(evt) {
+ if (evt.target == content.document) {
+ return;
+ }
+ chromeEventHandler.removeEventListener("DOMWindowCreated", listener);
+ let new_win = evt.target.defaultView;
+ let new_docShell = new_win.docShell;
+ sendAsyncMessage("DOMWindowCreatedReceived", {
+ stableChromeEventHandler:
+ chromeEventHandler === docShell.chromeEventHandler,
+ iframeHasNewDocShell: new_docShell !== docShell,
+ iframeHasSameChromeEventHandler:
+ new_docShell.chromeEventHandler === chromeEventHandler,
+ });
+ }
+ );
+
+ if (content.document.readyState != "complete") {
+ await new Promise(res =>
+ addEventListener("load", res, { once: true, capture: true })
+ );
+ }
+
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", "data:text/html,foo");
+ content.document.documentElement.appendChild(iframe);
+ });
+ });
+
+ await page.close();
+});
diff --git a/dom/ipc/tests/test_process_error.xhtml b/dom/ipc/tests/test_process_error.xhtml
new file mode 100644
index 0000000000..d122e7fedd
--- /dev/null
+++ b/dom/ipc/tests/test_process_error.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.expectChildProcessCrash();
+
+ var w = window.browsingContext.topChromeWindow.openDialog('process_error.xhtml', '_blank', 'chrome,resizable=yes,width=400,height=600', window);
+
+ function done()
+ {
+ w.close();
+ SimpleTest.finish();
+ }
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" />
+</window>
diff --git a/dom/ipc/tests/test_sharedMap.js b/dom/ipc/tests/test_sharedMap.js
new file mode 100644
index 0000000000..2a6c3a7142
--- /dev/null
+++ b/dom/ipc/tests/test_sharedMap.js
@@ -0,0 +1,377 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+const PROCESS_COUNT_PREF = "dom.ipc.processCount";
+
+const remote = AppConstants.platform !== "android";
+
+XPCShellContentUtils.init(this);
+
+let contentPage;
+
+async function readBlob(key, sharedData = Services.cpmm.sharedData) {
+ const { ExtensionUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionUtils.sys.mjs"
+ );
+
+ let reader = new FileReader();
+ reader.readAsText(sharedData.get(key));
+ await ExtensionUtils.promiseEvent(reader, "loadend");
+ return reader.result;
+}
+
+function getKey(key, sharedData = Services.cpmm.sharedData) {
+ return sharedData.get(key);
+}
+
+function hasKey(key, sharedData = Services.cpmm.sharedData) {
+ return sharedData.has(key);
+}
+
+function getContents(sharedMap = Services.cpmm.sharedData) {
+ return {
+ keys: Array.from(sharedMap.keys()),
+ values: Array.from(sharedMap.values()),
+ entries: Array.from(sharedMap.entries()),
+ getValues: Array.from(sharedMap.keys(), key => sharedMap.get(key)),
+ };
+}
+
+function checkMap(contents, expected) {
+ expected = Array.from(expected);
+
+ equal(contents.keys.length, expected.length, "Got correct number of keys");
+ equal(
+ contents.values.length,
+ expected.length,
+ "Got correct number of values"
+ );
+ equal(
+ contents.entries.length,
+ expected.length,
+ "Got correct number of entries"
+ );
+
+ for (let [i, [key, val]] of contents.entries.entries()) {
+ equal(key, contents.keys[i], `keys()[${i}] matches entries()[${i}]`);
+ deepEqual(
+ val,
+ contents.values[i],
+ `values()[${i}] matches entries()[${i}]`
+ );
+ }
+
+ expected.sort(([a], [b]) => a.localeCompare(b));
+ contents.entries.sort(([a], [b]) => a.localeCompare(b));
+
+ for (let [i, [key, val]] of contents.entries.entries()) {
+ equal(
+ key,
+ expected[i][0],
+ `expected[${i}].key matches entries()[${i}].key`
+ );
+ deepEqual(
+ val,
+ expected[i][1],
+ `expected[${i}].value matches entries()[${i}].value`
+ );
+ }
+}
+
+function checkParentMap(expected) {
+ info("Checking parent map");
+ checkMap(getContents(Services.ppmm.sharedData), expected);
+}
+
+async function checkContentMaps(expected, parentOnly = false) {
+ info("Checking in-process content map");
+ checkMap(getContents(Services.cpmm.sharedData), expected);
+
+ if (!parentOnly) {
+ info("Checking out-of-process content map");
+ let contents = await contentPage.spawn([], getContents);
+ checkMap(contents, expected);
+ }
+}
+
+async function loadContentPage() {
+ let page = await XPCShellContentUtils.loadContentPage("data:text/html,", {
+ remote,
+ });
+ registerCleanupFunction(() => page.close());
+ return page;
+}
+
+add_setup(async function () {
+ // Start with one content process so that we can increase the number
+ // later and test the behavior of a fresh content process.
+ Services.prefs.setIntPref(PROCESS_COUNT_PREF, 1);
+
+ contentPage = await loadContentPage();
+});
+
+add_task(async function test_sharedMap() {
+ let { sharedData } = Services.ppmm;
+
+ info("Check that parent and child maps are both initially empty");
+
+ checkParentMap([]);
+ await checkContentMaps([]);
+
+ let expected = [
+ ["foo-a", { foo: "a" }],
+ ["foo-b", { foo: "b" }],
+ ["bar-c", null],
+ ["bar-d", 42],
+ ];
+
+ function setKey(key, val) {
+ sharedData.set(key, val);
+ expected = expected.filter(([k]) => k != key);
+ expected.push([key, val]);
+ }
+ function deleteKey(key) {
+ sharedData.delete(key);
+ expected = expected.filter(([k]) => k != key);
+ }
+
+ for (let [key, val] of expected) {
+ sharedData.set(key, val);
+ }
+
+ info(
+ "Add some entries, test that they are initially only available in the parent"
+ );
+
+ checkParentMap(expected);
+ await checkContentMaps([]);
+
+ info("Flush. Check that changes are visible in both parent and children");
+
+ sharedData.flush();
+
+ checkParentMap(expected);
+ await checkContentMaps(expected);
+
+ info(
+ "Add another entry. Check that it is initially only available in the parent"
+ );
+
+ let oldExpected = Array.from(expected);
+
+ setKey("baz-a", { meh: "meh" });
+
+ // When we do several checks in a row, we can't check the values in
+ // the content process, since the async checks may allow the idle
+ // flush task to run, and update it before we're ready.
+
+ checkParentMap(expected);
+ checkContentMaps(oldExpected, true);
+
+ info(
+ "Add another entry. Check that both new entries are only available in the parent"
+ );
+
+ setKey("baz-a", { meh: 12 });
+
+ checkParentMap(expected);
+ checkContentMaps(oldExpected, true);
+
+ info(
+ "Delete an entry. Check that all changes are only visible in the parent"
+ );
+
+ deleteKey("foo-b");
+
+ checkParentMap(expected);
+ checkContentMaps(oldExpected, true);
+
+ info(
+ "Flush. Check that all entries are available in both parent and children"
+ );
+
+ sharedData.flush();
+
+ checkParentMap(expected);
+ await checkContentMaps(expected);
+
+ info("Test that entries are automatically flushed on idle:");
+
+ info(
+ "Add a new entry. Check that it is initially only available in the parent"
+ );
+
+ // Test the idle flush task.
+ oldExpected = Array.from(expected);
+
+ setKey("thing", "stuff");
+
+ checkParentMap(expected);
+ checkContentMaps(oldExpected, true);
+
+ info(
+ "Wait for an idle timeout. Check that changes are now visible in all children"
+ );
+
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+
+ checkParentMap(expected);
+ await checkContentMaps(expected);
+
+ // Test that has() rebuilds map after a flush.
+ sharedData.set("grick", true);
+ sharedData.flush();
+ equal(
+ await contentPage.spawn(["grick"], hasKey),
+ true,
+ "has() should see key after flush"
+ );
+
+ sharedData.set("grack", true);
+ sharedData.flush();
+ equal(
+ await contentPage.spawn(["gruck"], hasKey),
+ false,
+ "has() should return false for nonexistent key"
+ );
+});
+
+add_task(async function test_blobs() {
+ let { sharedData } = Services.ppmm;
+
+ let text = [
+ "The quick brown fox jumps over the lazy dog",
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
+ "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ ];
+ let blobs = text.map(str => new Blob([str]));
+
+ let data = { foo: { bar: "baz" } };
+
+ sharedData.set("blob0", blobs[0]);
+ sharedData.set("blob1", blobs[1]);
+ sharedData.set("data", data);
+
+ equal(
+ await readBlob("blob0", sharedData),
+ text[0],
+ "Expected text for blob0 in parent ppmm"
+ );
+
+ sharedData.flush();
+
+ equal(
+ await readBlob("blob0", sharedData),
+ text[0],
+ "Expected text for blob0 in parent ppmm"
+ );
+ equal(
+ await readBlob("blob1", sharedData),
+ text[1],
+ "Expected text for blob1 in parent ppmm"
+ );
+
+ equal(
+ await readBlob("blob0"),
+ text[0],
+ "Expected text for blob0 in parent cpmm"
+ );
+ equal(
+ await readBlob("blob1"),
+ text[1],
+ "Expected text for blob1 in parent cpmm"
+ );
+
+ equal(
+ await contentPage.spawn(["blob0"], readBlob),
+ text[0],
+ "Expected text for blob0 in child 1 cpmm"
+ );
+ equal(
+ await contentPage.spawn(["blob1"], readBlob),
+ text[1],
+ "Expected text for blob1 in child 1 cpmm"
+ );
+
+ // Start a second child process
+ Services.prefs.setIntPref(PROCESS_COUNT_PREF, 2);
+
+ let page2 = await loadContentPage();
+
+ equal(
+ await page2.spawn(["blob0"], readBlob),
+ text[0],
+ "Expected text for blob0 in child 2 cpmm"
+ );
+ equal(
+ await page2.spawn(["blob1"], readBlob),
+ text[1],
+ "Expected text for blob1 in child 2 cpmm"
+ );
+
+ sharedData.set("blob0", blobs[2]);
+
+ equal(
+ await readBlob("blob0", sharedData),
+ text[2],
+ "Expected text for blob0 in parent ppmm"
+ );
+
+ sharedData.flush();
+
+ equal(
+ await readBlob("blob0", sharedData),
+ text[2],
+ "Expected text for blob0 in parent ppmm"
+ );
+ equal(
+ await readBlob("blob1", sharedData),
+ text[1],
+ "Expected text for blob1 in parent ppmm"
+ );
+
+ equal(
+ await readBlob("blob0"),
+ text[2],
+ "Expected text for blob0 in parent cpmm"
+ );
+ equal(
+ await readBlob("blob1"),
+ text[1],
+ "Expected text for blob1 in parent cpmm"
+ );
+
+ equal(
+ await contentPage.spawn(["blob0"], readBlob),
+ text[2],
+ "Expected text for blob0 in child 1 cpmm"
+ );
+ equal(
+ await contentPage.spawn(["blob1"], readBlob),
+ text[1],
+ "Expected text for blob1 in child 1 cpmm"
+ );
+
+ equal(
+ await page2.spawn(["blob0"], readBlob),
+ text[2],
+ "Expected text for blob0 in child 2 cpmm"
+ );
+ equal(
+ await page2.spawn(["blob1"], readBlob),
+ text[1],
+ "Expected text for blob1 in child 2 cpmm"
+ );
+
+ deepEqual(
+ await page2.spawn(["data"], getKey),
+ data,
+ "Expected data for data key in child 2 cpmm"
+ );
+});
diff --git a/dom/ipc/tests/test_temporaryfile_stream.html b/dom/ipc/tests/test_temporaryfile_stream.html
new file mode 100644
index 0000000000..9fa76a2155
--- /dev/null
+++ b/dom/ipc/tests/test_temporaryfile_stream.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Send an nsTemporaryFileInputStream cross-process</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+function startTest() {
+ var canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 100;
+ document.getElementById("content").appendChild(canvas);
+
+
+ // eslint-disable-next-line no-undef
+ var helper = new CaptureStreamTestHelper2D(100, 100);
+ helper.drawColor(canvas, helper.red);
+
+ var stream = canvas.captureStream(0);
+
+ var blob;
+
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the start of recording");
+
+ mediaRecorder.onwarning = () => ok(false, "warning unexpectedly fired");
+
+ mediaRecorder.onerror = () => ok(false, "Recording failed");
+
+ mediaRecorder.ondataavailable = ev => {
+ is(blob, undefined, "Should only get one dataavailable event");
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info("Got 'start' event");
+ // We just want one frame encoded, to see that the recorder produces something readable.
+ mediaRecorder.stop();
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ ok(blob, "Should have gotten a data blob");
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "blob_verify.sjs", true);
+ xhr.onload = () => {
+ var video = document.createElement("video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(xhr.response);
+ video.play();
+ video.onerror = err => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+ document.getElementById("content").appendChild(video);
+ helper.pixelMustBecome(video, helper.red, {
+ threshold: 128,
+ infoString: "Should become red",
+ }).then(SimpleTest.finish);
+ };
+ xhr.onerror = () => {
+ ok(false, "XHR error");
+ SimpleTest.finish();
+ };
+ xhr.responseType = "blob";
+ xhr.send(blob);
+ };
+
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder should be recording");
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({set: [["media.recorder.max_memory", 1]]}, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/ipc/tests/test_window_open_discarded_bc.html b/dom/ipc/tests/test_window_open_discarded_bc.html
new file mode 100644
index 0000000000..4cd81463e0
--- /dev/null
+++ b/dom/ipc/tests/test_window_open_discarded_bc.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Discard a new BrowsingContext during window.open nested event loop</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+add_task(async function() {
+ const TOPIC = "dangerous:test-only:new-browser-child-ready";
+
+ let found = false;
+ function observer(subject, topic, data) {
+ let win = SpecialPowers.wrap(subject);
+ if (SpecialPowers.compare(win.opener, window)) {
+ found = true;
+ SpecialPowers.removeObserver(observer, TOPIC);
+
+ win.close();
+ // window.close() is not synchronous, so we need to wait for the
+ // BrowsingContext to actually become discarded after we call it, to
+ // make sure that the window provider actually has a discarded BC at the
+ // end of its own nested event loop.
+ SpecialPowers.Services.tm.spinEventLoopUntil(
+ "Test(test_window_open_discarded_bc.html:add_task)",
+ () => !win.opener
+ );
+ }
+ }
+ SpecialPowers.addObserver(observer, TOPIC);
+
+ let win = window.open();
+
+ is(found, true, "Our observer should have fired for the new window");
+ is(win, null, "window.open() should return null when new window is already closed");
+});
+</script>
+</body>
+</html>
diff --git a/dom/ipc/tests/xpcshell.toml b/dom/ipc/tests/xpcshell.toml
new file mode 100644
index 0000000000..bc4c75e4b0
--- /dev/null
+++ b/dom/ipc/tests/xpcshell.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+
+["test_blob_sliced_from_child_process.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"]
+
+["test_blob_sliced_from_parent_process.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"]
+
+["test_bug1086684.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"]
+
+["test_child_docshell.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"]
+
+["test_sharedMap.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"]