diff options
Diffstat (limited to 'dom/ipc/tests')
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 Binary files differnew file mode 100644 index 0000000000..bed764fbf1 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/audio.ogg 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'"] |