diff options
Diffstat (limited to 'dom/ipc/tests')
52 files changed, 4003 insertions, 0 deletions
diff --git a/dom/ipc/tests/.eslintrc.js b/dom/ipc/tests/.eslintrc.js new file mode 100644 index 0000000000..4e9d6a74a8 --- /dev/null +++ b/dom/ipc/tests/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + extends: [ + "plugin:mozilla/browser-test", + "plugin:mozilla/mochitest-test", + "plugin:mozilla/xpcshell-test", + ], +}; diff --git a/dom/ipc/tests/JSProcessActor/browser.ini b/dom/ipc/tests/JSProcessActor/browser.ini new file mode 100644 index 0000000000..c3d8645a33 --- /dev/null +++ b/dom/ipc/tests/JSProcessActor/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + head.js + +[browser_getActor.js] +[browser_getActor_filter.js] +[browser_observer_notification.js] +[browser_registerProcessActor.js] +[browser_sendAsyncMessage.js] +[browser_sendQuery.js] + diff --git a/dom/ipc/tests/JSProcessActor/browser_getActor.js b/dom/ipc/tests/JSProcessActor/browser_getActor.js new file mode 100644 index 0000000000..e972eaaac9 --- /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..3d5c620d16 --- /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..dfe92ad240 --- /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..e57f762744 --- /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() { + SimpleTest.doesThrow( + () => + ChromeUtils.registerContentActor( + "TestProcessActor", + processActorOptions + ), + "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..f81d4ddbee --- /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..69fe881367 --- /dev/null +++ b/dom/ipc/tests/JSProcessActor/browser_sendQuery.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +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) { + 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.jsm:33:31\n" + + asyncStack, + "Error should have the correct stack" + ); + }, +}); + +declTest("sendQuery Exception", { + async test(browser) { + 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.jsm:36:22\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/head.js b/dom/ipc/tests/JSProcessActor/head.js new file mode 100644 index 0000000000..86f35f4c57 --- /dev/null +++ b/dom/ipc/tests/JSProcessActor/head.js @@ -0,0 +1,66 @@ +/* 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 = { + parent: { + moduleURI: "resource://testing-common/TestProcessActorParent.jsm", + }, + child: { + moduleURI: "resource://testing-common/TestProcessActorChild.jsm", + observers: ["test-js-content-actor-child-observer"], + }, +}; + +function promiseNotification(aNotification) { + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + 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) { + let { url = "about:blank", includeParent = false, remoteTypes, test } = cfg; + + // Build the actor options object which will be used to register & unregister + // our process actor. + let actorOptions = { + parent: Object.assign({}, processActorOptions.parent), + child: Object.assign({}, processActorOptions.child), + }; + actorOptions.includeParent = includeParent; + if (remoteTypes !== undefined) { + actorOptions.remoteTypes = remoteTypes; + } + + // 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)); + }); + } 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.ini b/dom/ipc/tests/JSWindowActor/browser.ini new file mode 100644 index 0000000000..fe07d9a97d --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser.ini @@ -0,0 +1,19 @@ +[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] +[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] diff --git a/dom/ipc/tests/JSWindowActor/browser_contentWindow.js b/dom/ipc/tests/JSWindowActor/browser_contentWindow.js new file mode 100644 index 0000000000..cfdc5b114f --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_contentWindow.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +declTest("contentWindow null when inner window inactive", { + matches: [TEST_URL + "*"], + url: TEST_URL + "?1", + + async test(browser) { + { + let parent = browser.browsingContext.currentWindowGlobal; + let actorParent = parent.getActor("TestWindow"); + + await actorParent.sendQuery("storeActor"); + } + + { + let url = TEST_URL + "?2"; + let loaded = BrowserTestUtils.browserLoaded(browser, false, url); + await BrowserTestUtils.loadURI(browser, url); + await loaded; + } + + let parent = browser.browsingContext.currentWindowGlobal; + let actorParent = parent.getActor("TestWindow"); + + let result = await actorParent.sendQuery("checkActor"); + if (SpecialPowers.useRemoteSubframes) { + is( + result.status, + "error", + "Should get an error when bfcache is disabled for Fission" + ); + is( + result.errorType, + "InvalidStateError", + "Should get an InvalidStateError without bfcache" + ); + } else { + 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..a1ef293cc0 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_crash_report.js @@ -0,0 +1,112 @@ +/* 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..25feb47179 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_destroy_callbacks.js @@ -0,0 +1,194 @@ +/* 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..1874a0a174 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_event_listener.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +declTest("test event triggering actor creation", { + async test(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>`; + }); + + // Wait for the observer notification. + let observePromise = new Promise(resolve => { + const TOPIC = "test-js-window-actor-parent-event"; + Services.obs.addObserver(function obs(subject, topic, data) { + is(topic, TOPIC, "topic matches"); + + Services.obs.removeObserver(obs, TOPIC); + resolve({ subject, data }); + }, TOPIC); + }); + + // Click on the select to show the dropdown. + await BrowserTestUtils.synthesizeMouseAtCenter("#testSelect", {}, 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" + ); + }, +}); diff --git a/dom/ipc/tests/JSWindowActor/browser_getActor.js b/dom/ipc/tests/JSWindowActor/browser_getActor.js new file mode 100644 index 0000000000..53205fc7d9 --- /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..7ee938dddb --- /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..ae8c40f781 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_observer_notification.js @@ -0,0 +1,111 @@ +/* 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-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", { + 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", { + 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", { + 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..95e1a0c422 --- /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..838fa653b4 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_registerWindowActor.js @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +declTest("double register", { + async test() { + SimpleTest.doesThrow( + () => ChromeUtils.registerWindowActor("TestWindow", windowActorOptions), + "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..077732c45e --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/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; + 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; + }); + }, +}); diff --git a/dom/ipc/tests/JSWindowActor/browser_sendQuery.js b/dom/ipc/tests/JSWindowActor/browser_sendQuery.js new file mode 100644 index 0000000000..149744a2fc --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/browser_sendQuery.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const ERROR_LINE_NUMBER = 41; +const EXCEPTION_LINE_NUMBER = ERROR_LINE_NUMBER + 3; + +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) { + 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.jsm:${ERROR_LINE_NUMBER}:31\n` + + asyncStack, + "Error should have the correct stack" + ); + }, +}); + +declTest("sendQuery Exception", { + async test(browser) { + 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.jsm:${EXCEPTION_LINE_NUMBER}:22\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/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..a1a98f66e0 --- /dev/null +++ b/dom/ipc/tests/JSWindowActor/head.js @@ -0,0 +1,71 @@ +/* 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 = { + parent: { + moduleURI: "resource://testing-common/TestWindowParent.jsm", + }, + child: { + moduleURI: "resource://testing-common/TestWindowChild.jsm", + + events: { + mozshowdropdown: {}, + }, + + observers: ["test-js-window-actor-child-observer", "audio-playback"], + }, +}; + +function declTest(name, cfg) { + let { + url = "about:blank", + allFrames = false, + includeChrome = false, + matches, + remoteTypes, + messageManagerGroups, + test, + } = cfg; + + // Build the actor options object which will be used to register & unregister + // our window actor. + let actorOptions = { + parent: Object.assign({}, windowActorOptions.parent), + child: Object.assign({}, windowActorOptions.child), + }; + 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)); + }); + } 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..cf50371c8a --- /dev/null +++ b/dom/ipc/tests/blob_verify.sjs @@ -0,0 +1,20 @@ +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 = []; + 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.ini b/dom/ipc/tests/browser.ini new file mode 100644 index 0000000000..aa60226764 --- /dev/null +++ b/dom/ipc/tests/browser.ini @@ -0,0 +1,22 @@ +[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] +skip-if = os != "win" # The Process Priority Manager is only enabled for Windows so far. Bug 1522879. +[browser_crash_oopiframe.js] +skip-if = !fission || !crashreporter || verify +[browser_domainPolicy.js] +[browser_memory_distribution_telemetry.js] +skip-if = true || !e10s # This is an e10s only probe, but the test is currently broken. See Bug 1449991 +[browser_cancel_content_js.js] +skip-if = !e10s +[browser_bug1646088.js] +support-files = file_dummy.html +skip-if = !e10s diff --git a/dom/ipc/tests/browser_CrashService_crash.js b/dom/ipc/tests/browser_CrashService_crash.js new file mode 100644 index 0000000000..9504d68ae7 --- /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.jsm). + +/* 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.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..b74b2b84cd --- /dev/null +++ b/dom/ipc/tests/browser_ProcessPriorityManager.js @@ -0,0 +1,520 @@ +/* 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; + +/** + * 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.priorityMap = new WeakMap(); + this.priorityMap.set( + this.tabbrowser.selectedBrowser, + PROCESS_PRIORITY_FOREGROUND + ); + this.noChangeBrowsers = new WeakMap(); + 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.window = null; + } + + /** + * Returns a Promise that resolves when a particular <browser> + * has its content process reach a particular priority. Will + * eventually time out if that priority is never reached. + * + * @param browser (<browser>) + * The <browser> that we expect to change priority. + * @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(browser, expectedPriority) { + return TestUtils.waitForCondition(() => { + let currentPriority = this.priorityMap.get(browser); + if (currentPriority == expectedPriority) { + Assert.ok( + true, + `Browser at ${browser.currentURI.spec} reached expected ` + + `priority: ${currentPriority}` + ); + return true; + } + return false; + }, `Waiting for browser at ${browser.currentURI.spec} to reach priority ` + expectedPriority); + } + + /** + * Returns a Promise that resolves after a duration of + * WAIT_FOR_CHANGE_TIME_MS. During that time, if the passed browser + * changes priority, a test failure will be registered. + * + * @param browser (<browser>) + * The <browser> that we expect to change priority. + * @return Promise + * @resolves undefined + * Once the WAIT_FOR_CHANGE_TIME_MS duration has passed. + */ + async ensureNoPriorityChange(browser) { + this.noChangeBrowsers.set(browser, null); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)); + let priority = this.noChangeBrowsers.get(browser); + Assert.equal( + priority, + null, + `Should have seen no process priority change for a browser at ${browser.currentURI.spec}` + ); + this.noChangeBrowsers.delete(browser); + } + + /** + * Makes sure that a particular foreground browser has been + * registered in the priority map. This is needed because browsers are + * only registered when their priorities change - and if a browser's + * priority never changes during a test, then they wouldn't be registered. + * + * The passed browser must be a foreground browser, since it's assumed that + * the associated content process is running with foreground priority. + * + * @param browser (browser) + * A _foreground_ browser. + */ + ensureForegroundRegistered(browser) { + if (!this.priorityMap.has(browser)) { + this.priorityMap.set(browser, PROCESS_PRIORITY_FOREGROUND); + } + } + + /** + * Synchronously returns the priority of a particular browser's + * content process. + * + * @param browser (browser) + * The browser to get the content process priority for. + * @return String + * The priority that the browser's content process is at. + */ + currentPriority(browser) { + return this.priorityMap.get(browser); + } + + /** + * 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); + for (let browser of this.tabbrowser.browsers) { + if (browser.frameLoader.childID == childID) { + info( + `Browser at: ${browser.currentURI.spec} transitioning to ${priority}` + ); + if (this.noChangeBrowsers.has(browser)) { + this.noChangeBrowsers.set(browser, priority); + } + this.priorityMap.set(browser, priority); + } + } + } +} + +let gTabPriorityWatcher; + +add_task(async function setup() { + // 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." + ); + + gTabPriorityWatcher.ensureForegroundRegistered(fromBrowser); + + let fromPromise; + if ( + gTabPriorityWatcher.currentPriority(fromBrowser) == fromTabExpectedPriority + ) { + fromPromise = gTabPriorityWatcher.ensureNoPriorityChange(fromBrowser); + } else { + fromPromise = gTabPriorityWatcher.waitForPriorityChange( + fromBrowser, + fromTabExpectedPriority + ); + } + + let toPromise; + if ( + gTabPriorityWatcher.currentPriority(toBrowser) == + PROCESS_PRIORITY_FOREGROUND + ) { + toPromise = gTabPriorityWatcher.ensureNoPriorityChange(toBrowser); + } else { + toPromise = gTabPriorityWatcher.waitForPriorityChange( + 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. + */ +add_task(async function test_normal_background_tab() { + let originalTab = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab("http://example.com", 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, + }); + }); +}); + +/** + * 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("http://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. + await SpecialPowers.spawn(browser, [], async () => { + let video = content.document.createElement("video"); + video.src = "http://mochi.test:8888/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(); + }); + + 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. + await 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("http://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 = "http://mochi.test:8888/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("http://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, + }); + }); +}); diff --git a/dom/ipc/tests/browser_bug1646088.js b/dom/ipc/tests/browser_bug1646088.js new file mode 100644 index 0000000000..4466c67129 --- /dev/null +++ b/dom/ipc/tests/browser_bug1646088.js @@ -0,0 +1,70 @@ +const { PromiseUtils } = ChromeUtils.import( + "resource://gre/modules/PromiseUtils.jsm" +); + +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 = PromiseUtils.defer(); + let finishSwitch = PromiseUtils.defer(); + 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. We do this from the content process to make sure the frontend + // has no chance to trigger an eager process switch. + info("Beginning process switch into file URI process"); + let browserLoaded = BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn(browser, [uriString], uri => { + content.location = uri; + }); + 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 = PromiseUtils.defer(); + 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_cancel_content_js.js b/dom/ipc/tests/browser_cancel_content_js.js new file mode 100644 index 0000000000..d319de87e4 --- /dev/null +++ b/dom/ipc/tests/browser_cancel_content_js.js @@ -0,0 +1,68 @@ +/* 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, cancelContentJSPref, shouldCancel) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.cancel_content_js_when_navigating", cancelContentJSPref], + ["dom.max_script_run_time", 20], + ], + }); + 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} with cancel content JS ${ + cancelContentJSPref ? "enabled" : "disabled" + }` + ); + const nextPageLoaded = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "DOMTitleChanged" + ); + BrowserTestUtils.loadURI(gBrowser, nextPage); + + const result = await Promise.race([ + nextPageLoaded, + loopEnded.then(() => "timeout"), + ]); + + const timedOut = result === "timeout"; + if (shouldCancel) { + ok(timedOut === false, "expected next page to be loaded"); + } else { + ok(timedOut === true, "expected timeout"); + } + + BrowserTestUtils.removeTab(tab); +} + +add_task(async () => test_navigation(NEXT_PAGE, true, true)); +add_task(async () => test_navigation(NEXT_PAGE, false, false)); +add_task(async () => test_navigation(JS_URI, true, false)); +add_task(async () => test_navigation(JS_URI, false, false)); diff --git a/dom/ipc/tests/browser_crash_oopiframe.js b/dom/ipc/tests/browser_crash_oopiframe.js new file mode 100644 index 0000000000..2cf133e5e3 --- /dev/null +++ b/dom/ipc/tests/browser_crash_oopiframe.js @@ -0,0 +1,167 @@ +"use strict"; + +/** + * 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 browser, rootBC, iframeBC; + + for (let count = 0; count < numTabs; count++) { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:blank", + }); + + browser = tab.linkedBrowser; + rootBC = browser.browsingContext; + + // 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(browser, [], 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; + }); + } + + 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"); + ok(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" + ); + } + + // 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.querySelectorAll(".notification-button"); + is( + buttons.length, + 2, + "Notification " + count + " should have only two buttons." + ); + } + + // Press the ignore button on the visible notification. + let notificationBox = gBrowser.getNotificationBox(gBrowser.selectedBrowser); + let notification = notificationBox.currentNotification; + notification.dismiss(); + + for (let count = 1; count <= numTabs; count++) { + let nb = gBrowser.getNotificationBox(gBrowser.browsers[count]); + + await TestUtils.waitForCondition( + () => !nb.currentNotification, + "notification closed" + ); + + ok( + !nb.currentNotification, + "notification " + count + " closed when dismiss button is pressed" + ); + } + + 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() { + // 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); +}); diff --git a/dom/ipc/tests/browser_domainPolicy.js b/dom/ipc/tests/browser_domainPolicy.js new file mode 100644 index 0000000000..cec360f2e1 --- /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_task(async function setup() { + 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_memory_distribution_telemetry.js b/dom/ipc/tests/browser_memory_distribution_telemetry.js new file mode 100644 index 0000000000..bec51fa1cd --- /dev/null +++ b/dom/ipc/tests/browser_memory_distribution_telemetry.js @@ -0,0 +1,92 @@ +"use strict"; + +var session = ChromeUtils.import( + "resource://gre/modules/TelemetrySession.jsm", + null +); + +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"); + }); + + session.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]; + ok( + fewTabsSnapshot.sum > 0, + "Zero difference between all the content processes is unlikely, what happened?" + ); + ok( + 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/chrome.ini b/dom/ipc/tests/chrome.ini new file mode 100644 index 0000000000..206f0a1501 --- /dev/null +++ b/dom/ipc/tests/chrome.ini @@ -0,0 +1,11 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + process_error.xhtml + +[test_process_error.xhtml] +skip-if = !crashreporter + + +[test_process_error_oom.xhtml] +skip-if = !crashreporter 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_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..1a481c4163 --- /dev/null +++ b/dom/ipc/tests/file_dummy.html @@ -0,0 +1,4 @@ +<!doctype html> +<body> + <h1>This is a dummy file</h1> +</body> diff --git a/dom/ipc/tests/mochitest.ini b/dom/ipc/tests/mochitest.ini new file mode 100644 index 0000000000..263a1cfb1f --- /dev/null +++ b/dom/ipc/tests/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] + +[test_temporaryfile_stream.html] +skip-if = !e10s || toolkit == 'android' || (os == "win" && processor == "aarch64") # Bug 1525959, aarch64 due to 1531150 +support-files = + blob_verify.sjs + !/dom/canvas/test/captureStream_common.js +[test_Preallocated.html] +skip-if = !e10s || toolkit == 'android' || tsan # Bug 1525959. tsan: Bug 1683730 +[test_window_open_discarded_bc.html] +skip-if = toolkit == 'android' +[test_bcg_processes.html] diff --git a/dom/ipc/tests/process_error.xhtml b/dom/ipc/tests/process_error.xhtml new file mode 100644 index 0000000000..7f0a5e20dd --- /dev/null +++ b/dom/ipc/tests/process_error.xhtml @@ -0,0 +1,63 @@ +<?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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + + 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"); + + // Let's check whether we have correctly reported OOM. + var isLikelyOOM = subject.getPropertyAsBool('isLikelyOOM'); + is(isLikelyOOM, crashType == 'CRASH_OOM', 'isLikelyOOM is correct'); + } + + 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..5d2b2bd3a6 --- /dev/null +++ b/dom/ipc/tests/test_Preallocated.html @@ -0,0 +1,52 @@ +<!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"; +/* eslint-env mozilla/frame-script */ + +SimpleTest.waitForExplicitFinish(); + +function expectProcessCreated() { + return new Promise(resolve => { + function parentExpectProcessCreated() { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + 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..8109b59a42 --- /dev/null +++ b/dom/ipc/tests/test_bcg_processes.html @@ -0,0 +1,46 @@ +<!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"; +/* eslint-env mozilla/frame-script */ + +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..e3498320ee --- /dev/null +++ b/dom/ipc/tests/test_blob_sliced_from_child_process.js @@ -0,0 +1,140 @@ +"use strict"; +/* eslint-env mozilla/frame-script */ + +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +ExtensionTestUtils.init(this); + +function childFrameScript() { + "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 ExtensionTestUtils.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..4f3b10d7a2 --- /dev/null +++ b/dom/ipc/tests/test_blob_sliced_from_parent_process.js @@ -0,0 +1,167 @@ +"use strict"; +/* eslint-env mozilla/frame-script */ + +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +ExtensionTestUtils.init(this); + +function childFrameScript() { + 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 ExtensionTestUtils.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_bug1086684.js b/dom/ipc/tests/test_bug1086684.js new file mode 100644 index 0000000000..c5d4836d98 --- /dev/null +++ b/dom/ipc/tests/test_bug1086684.js @@ -0,0 +1,99 @@ +"use strict"; +/* eslint-env mozilla/frame-script */ + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +AddonTestUtils.init(this); +ExtensionTestUtils.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 = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler(childFramePath, (request, response) => { + response.write(childFrameContents); +}); + +function childFrameScript() { + "use strict"; + + let { MockFilePicker } = ChromeUtils.import( + "resource://testing-common/MockFilePicker.jsm" + ); + + 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() { + let page = await ExtensionTestUtils.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(); +}); diff --git a/dom/ipc/tests/test_child_docshell.js b/dom/ipc/tests/test_child_docshell.js new file mode 100644 index 0000000000..3871c35496 --- /dev/null +++ b/dom/ipc/tests/test_child_docshell.js @@ -0,0 +1,93 @@ +"use strict"; +/* eslint-env mozilla/frame-script */ + +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +ExtensionTestUtils.init(this); + +add_task(async function test() { + let page = await ExtensionTestUtils.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() { + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + 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_process_error_oom.xhtml b/dom/ipc/tests/test_process_error_oom.xhtml new file mode 100644 index 0000000000..03de3b07eb --- /dev/null +++ b/dom/ipc/tests/test_process_error_oom.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?crashType=CRASH_OOM', '_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..112093e44c --- /dev/null +++ b/dom/ipc/tests/test_sharedMap.js @@ -0,0 +1,382 @@ +"use strict"; + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +const PROCESS_COUNT_PREF = "dom.ipc.processCount"; + +const remote = AppConstants.platform !== "android"; + +ExtensionTestUtils.init(this); + +let contentPage; + +async function readBlob(key, sharedData = Services.cpmm.sharedData) { + 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(undefined, getContents); + checkMap(contents, expected); + } +} + +async function loadContentPage() { + let page = await ExtensionTestUtils.loadContentPage("about:blank", { + remote, + }); + registerCleanupFunction(() => page.close()); + + page.addFrameScriptHelper(` + var {ExtensionUtils} = ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); + Cu.importGlobalProperties(["FileReader"]); + `); + return page; +} + +add_task(async function setup() { + // 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..f101264eeb --- /dev/null +++ b/dom/ipc/tests/test_window_open_discarded_bc.html @@ -0,0 +1,37 @@ +<!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(() => !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.ini b/dom/ipc/tests/xpcshell.ini new file mode 100644 index 0000000000..a93537c9b6 --- /dev/null +++ b/dom/ipc/tests/xpcshell.ini @@ -0,0 +1,10 @@ +[test_sharedMap.js] +skip-if = os == "android" && processor == "x86_64" +[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" |