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