summaryrefslogtreecommitdiffstats
path: root/toolkit/components/remotebrowserutils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/components/remotebrowserutils
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/remotebrowserutils')
-rw-r--r--toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs209
-rw-r--r--toolkit/components/remotebrowserutils/moz.build14
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs6
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser.ini16
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js238
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js280
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js79
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js410
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js115
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js161
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs58
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/dummy_page.html13
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html5
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/head.js17
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs28
15 files changed, 1649 insertions, 0 deletions
diff --git a/toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs b/toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs
new file mode 100644
index 0000000000..2d33706427
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.sys.mjs
@@ -0,0 +1,209 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+// 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/.
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+// This object implements the JS parts of nsIWebNavigation.
+export class RemoteWebNavigation {
+ constructor(aBrowser) {
+ this._browser = aBrowser;
+ this._cancelContentJSEpoch = 1;
+ this._currentURI = null;
+ this._canGoBack = false;
+ this._canGoForward = false;
+ this.referringURI = null;
+ }
+
+ swapBrowser(aBrowser) {
+ this._browser = aBrowser;
+ }
+
+ maybeCancelContentJSExecution(aNavigationType, aOptions = {}) {
+ const epoch = this._cancelContentJSEpoch++;
+ this._browser.frameLoader.remoteTab.maybeCancelContentJSExecution(
+ aNavigationType,
+ { ...aOptions, epoch }
+ );
+ return epoch;
+ }
+
+ get canGoBack() {
+ if (Services.appinfo.sessionHistoryInParent) {
+ return this._browser.browsingContext.sessionHistory?.index > 0;
+ }
+ return this._canGoBack;
+ }
+
+ get canGoForward() {
+ if (Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory = this._browser.browsingContext.sessionHistory;
+ return sessionHistory?.index < sessionHistory?.count - 1;
+ }
+ return this._canGoForward;
+ }
+
+ goBack(requireUserInteraction = false) {
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_BACK
+ );
+ this._browser.browsingContext.goBack(
+ cancelContentJSEpoch,
+ requireUserInteraction,
+ true
+ );
+ }
+ goForward(requireUserInteraction = false) {
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_FORWARD
+ );
+ this._browser.browsingContext.goForward(
+ cancelContentJSEpoch,
+ requireUserInteraction,
+ true
+ );
+ }
+ gotoIndex(aIndex) {
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_INDEX,
+ { index: aIndex }
+ );
+ this._browser.browsingContext.goToIndex(aIndex, cancelContentJSEpoch, true);
+ }
+
+ _speculativeConnect(uri, loadURIOptions) {
+ try {
+ // Let's start a network connection before the content process asks.
+ // Note that we might have already set up the speculative connection in
+ // some cases, especially when the url is from location bar or its popup
+ // menu.
+ if (uri.schemeIs("http") || uri.schemeIs("https")) {
+ let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
+ this._browser
+ );
+ let principal = loadURIOptions.triggeringPrincipal;
+ // We usually have a triggeringPrincipal assigned, but in case we
+ // don't have one or if it's a SystemPrincipal, let's create it with OA
+ // inferred from the current context.
+ if (!principal || principal.isSystemPrincipal) {
+ let attrs = {
+ userContextId: this._browser.getAttribute("usercontextid") || 0,
+ privateBrowsingId: isBrowserPrivate ? 1 : 0,
+ };
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ attrs
+ );
+ }
+ Services.io.speculativeConnect(uri, principal, null, false);
+ }
+ } catch (ex) {
+ // Can't setup speculative connection for this uri for some
+ // reason, just ignore it.
+ }
+ }
+
+ loadURI(uri, loadURIOptions) {
+ this._speculativeConnect(uri, loadURIOptions);
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_URL,
+ { uri }
+ );
+ this._browser.browsingContext.loadURI(uri, {
+ ...loadURIOptions,
+ cancelContentJSEpoch,
+ });
+ }
+
+ fixupAndLoadURIString(uriString, loadURIOptions) {
+ let uri;
+ try {
+ let fixupFlags = Services.uriFixup.webNavigationFlagsToFixupFlags(
+ uriString,
+ loadURIOptions.loadFlags
+ );
+ let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
+ this._browser
+ );
+ if (isBrowserPrivate) {
+ fixupFlags |= Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+
+ uri = Services.uriFixup.getFixupURIInfo(
+ uriString,
+ fixupFlags
+ ).preferredURI;
+ } catch (ex) {
+ // In rare cases `uriFixup` can throw. We ignore this here, but it's
+ // likely that the fixupAndLoadURIString call below will still throw,
+ // hopefully with more details.
+ }
+ if (uri) {
+ this._speculativeConnect(uri, loadURIOptions);
+ }
+
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_URL,
+ { uri }
+ );
+ // For now, continue to use fixup here, but note that ideally we should be
+ // doing fixup only once and reusing the URI we created above. Addressing
+ // this is bug 1815509.
+ this._browser.browsingContext.fixupAndLoadURIString(uriString, {
+ ...loadURIOptions,
+ cancelContentJSEpoch,
+ });
+ }
+
+ reload(aReloadFlags) {
+ this._browser.browsingContext.reload(aReloadFlags);
+ }
+ stop(aStopFlags) {
+ this._browser.browsingContext.stop(aStopFlags);
+ }
+
+ get document() {
+ return this._browser.contentDocument;
+ }
+
+ get currentURI() {
+ if (!this._currentURI) {
+ this._currentURI = Services.io.newURI("about:blank");
+ }
+ return this._currentURI;
+ }
+ set currentURI(aURI) {
+ // Bug 1498600 verify usages of systemPrincipal here
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ this.loadURI(aURI.spec, loadURIOptions);
+ }
+
+ // Bug 1233803 - accessing the sessionHistory of remote browsers should be
+ // done in content scripts.
+ get sessionHistory() {
+ throw new Components.Exception(
+ "Not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ set sessionHistory(aValue) {
+ throw new Components.Exception(
+ "Not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ _sendMessage(aMessage, aData) {
+ try {
+ this._browser.sendMessageToActor(aMessage, aData, "WebNavigation");
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
diff --git a/toolkit/components/remotebrowserutils/moz.build b/toolkit/components/remotebrowserutils/moz.build
new file mode 100644
index 0000000000..0723c716ff
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Navigation")
+
+EXTRA_JS_MODULES += [
+ "RemoteWebNavigation.sys.mjs",
+]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
diff --git a/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs b/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs
new file mode 100644
index 0000000000..1a28b8c5c3
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 307, "Temporary Redirect");
+ let location = request.queryString;
+ response.setHeader("Location", location, false);
+ response.write("Hello world!");
+}
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser.ini b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
new file mode 100644
index 0000000000..a7d0a4786d
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ dummy_page.html
+ print_postdata.sjs
+ 307redirect.sjs
+ head.js
+ coop_header.sjs
+ file_postmsg_parent.html
+
+[browser_RemoteWebNavigation.js]
+https_first_disabled = true
+[browser_documentChannel.js]
+[browser_httpCrossOriginOpenerPolicy.js]
+[browser_httpToFileHistory.js]
+[browser_oopProcessSwap.js]
+[browser_externalLinkBlanksPage.js]
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js
new file mode 100644
index 0000000000..22952e85f5
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js
@@ -0,0 +1,238 @@
+const SYSTEMPRINCIPAL = Services.scriptSecurityManager.getSystemPrincipal();
+const DUMMY1 =
+ "http://test1.example.org/browser/toolkit/modules/tests/browser/dummy_page.html";
+const DUMMY2 =
+ "http://test2.example.org/browser/toolkit/modules/tests/browser/dummy_page.html";
+const LOAD_URI_OPTIONS = { triggeringPrincipal: SYSTEMPRINCIPAL };
+
+function waitForLoad(uri) {
+ return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
+}
+
+function waitForPageShow(browser = gBrowser.selectedBrowser) {
+ return BrowserTestUtils.waitForContentEvent(browser, "pageshow", true);
+}
+
+// Tests that loadURI accepts a referrer and it is included in the load.
+add_task(async function test_referrer() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ let loadURIOptionsWithReferrer = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ referrerInfo: new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(DUMMY2)
+ ),
+ };
+ browser.webNavigation.loadURI(
+ Services.io.newURI(DUMMY1),
+ loadURIOptionsWithReferrer
+ );
+ await waitForLoad(DUMMY1);
+
+ await SpecialPowers.spawn(
+ browser,
+ [[DUMMY1, DUMMY2]],
+ function ([dummy1, dummy2]) {
+ function getExpectedReferrer(referrer) {
+ let defaultPolicy = Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy"
+ );
+ ok(
+ [2, 3].indexOf(defaultPolicy) > -1,
+ "default referrer policy should be either strict-origin-when-cross-origin(2) or no-referrer-when-downgrade(3)"
+ );
+ if (defaultPolicy == 2) {
+ return referrer.match(/https?:\/\/[^\/]+\/?/i)[0];
+ }
+ return referrer;
+ }
+
+ is(content.location.href, dummy1, "Should have loaded the right URL");
+ is(
+ content.document.referrer,
+ getExpectedReferrer(dummy2),
+ "Should have the right referrer"
+ );
+ }
+ );
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that remote access to webnavigation.sessionHistory works.
+add_task(async function test_history() {
+ async function checkHistoryIndex(browser, n) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return SpecialPowers.spawn(browser, [n], function (n) {
+ let history =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+
+ is(history.index, n, "Should be at the right place in history");
+ });
+ }
+
+ let history = browser.browsingContext.sessionHistory;
+ is(history.index, n, "Should be at the right place in history");
+ return null;
+ }
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(Services.io.newURI(DUMMY1), LOAD_URI_OPTIONS);
+ await waitForLoad(DUMMY1);
+
+ browser.webNavigation.loadURI(Services.io.newURI(DUMMY2), LOAD_URI_OPTIONS);
+ await waitForLoad(DUMMY2);
+
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(
+ browser,
+ [[DUMMY1, DUMMY2]],
+ function ([dummy1, dummy2]) {
+ let history =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+
+ is(history.count, 2, "Should be two history items");
+ is(history.index, 1, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(0);
+ is(entry.URI.spec, dummy1, "Should have the right history entry");
+ entry = history.getEntryAtIndex(1);
+ is(entry.URI.spec, dummy2, "Should have the right history entry");
+ }
+ );
+ } else {
+ let history = browser.browsingContext.sessionHistory;
+
+ is(history.count, 2, "Should be two history items");
+ is(history.index, 1, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(0);
+ is(entry.URI.spec, DUMMY1, "Should have the right history entry");
+ entry = history.getEntryAtIndex(1);
+ is(entry.URI.spec, DUMMY2, "Should have the right history entry");
+ }
+
+ let promise = waitForPageShow();
+ browser.webNavigation.goBack();
+ await promise;
+ await checkHistoryIndex(browser, 0);
+
+ promise = waitForPageShow();
+ browser.webNavigation.goForward();
+ await promise;
+ await checkHistoryIndex(browser, 1);
+
+ promise = waitForPageShow();
+ browser.webNavigation.gotoIndex(0);
+ await promise;
+ await checkHistoryIndex(browser, 0);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that load flags are passed through to the content process.
+add_task(async function test_flags() {
+ async function checkHistory(browser, { count, index }) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return SpecialPowers.spawn(
+ browser,
+ [[DUMMY2, count, index]],
+ function ([dummy2, count, index]) {
+ let history =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+ is(history.count, count, "Should be one history item");
+ is(history.index, index, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(index);
+ is(entry.URI.spec, dummy2, "Should have the right history entry");
+ }
+ );
+ }
+
+ let history = browser.browsingContext.sessionHistory;
+ is(history.count, count, "Should be one history item");
+ is(history.index, index, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(index);
+ is(entry.URI.spec, DUMMY2, "Should have the right history entry");
+
+ return null;
+ }
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(Services.io.newURI(DUMMY1), LOAD_URI_OPTIONS);
+ await waitForLoad(DUMMY1);
+ let loadURIOptionsReplaceHistory = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ };
+ browser.webNavigation.loadURI(
+ Services.io.newURI(DUMMY2),
+ loadURIOptionsReplaceHistory
+ );
+ await waitForLoad(DUMMY2);
+ await checkHistory(browser, { count: 1, index: 0 });
+ let loadURIOptionsBypassHistory = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ };
+ browser.webNavigation.loadURI(
+ Services.io.newURI(DUMMY1),
+ loadURIOptionsBypassHistory
+ );
+ await waitForLoad(DUMMY1);
+ await checkHistory(browser, { count: 1, index: 0 });
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that attempts to use unsupported arguments throw an exception.
+add_task(async function test_badarguments() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+
+ try {
+ let loadURIOptionsBadPostData = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ postData: {},
+ };
+ browser.webNavigation.loadURI(
+ Services.io.newURI(DUMMY1),
+ loadURIOptionsBadPostData
+ );
+ ok(
+ false,
+ "Should have seen an exception from trying to pass some postdata"
+ );
+ } catch (e) {
+ ok(true, "Should have seen an exception from trying to pass some postdata");
+ }
+
+ try {
+ let loadURIOptionsBadHeader = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ headers: {},
+ };
+ browser.webNavigation.loadURI(
+ Services.io.newURI(DUMMY1),
+ loadURIOptionsBadHeader
+ );
+ ok(false, "Should have seen an exception from trying to pass some headers");
+ } catch (e) {
+ ok(true, "Should have seen an exception from trying to pass some headers");
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js
new file mode 100644
index 0000000000..362bc9ee6d
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js
@@ -0,0 +1,280 @@
+/* eslint-env webextensions */
+"use strict";
+
+const PRINT_POSTDATA = httpURL("print_postdata.sjs");
+const FILE_DUMMY = fileURL("dummy_page.html");
+const DATA_URL = "data:text/html,Hello%2C World!";
+const DATA_STRING = "Hello, World!";
+
+async function performLoad(browser, opts, action) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ opts.url,
+ opts.maybeErrorPage
+ );
+ await action();
+ await loadedPromise;
+}
+
+const EXTENSION_DATA = {
+ manifest: {
+ name: "Simple extension test",
+ version: "1.0",
+ manifest_version: 2,
+ description: "",
+
+ permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ files: {
+ "dummy.html": "<html>webext dummy</html>",
+ "redirect.html": "<html>webext redirect</html>",
+ },
+
+ extUrl: "",
+
+ async background() {
+ browser.test.log("background script running");
+ browser.webRequest.onAuthRequired.addListener(
+ async details => {
+ browser.test.log("webRequest onAuthRequired");
+
+ // A blocking request that returns a promise exercises a codepath that
+ // sets the notificationCallbacks on the channel to a JS object that we
+ // can't do directly QueryObject on with expected results.
+ // This triggered a crash which was fixed in bug 1528188.
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, 0);
+ });
+ },
+ { urls: ["*://*/*"] },
+ ["blocking"]
+ );
+ browser.webRequest.onBeforeRequest.addListener(
+ async details => {
+ browser.test.log("webRequest onBeforeRequest");
+ let isRedirect =
+ details.originUrl == browser.runtime.getURL("redirect.html") &&
+ details.url.endsWith("print_postdata.sjs");
+ let url = this.extUrl ? this.extUrl : details.url + "?redirected";
+ return isRedirect ? { redirectUrl: url } : {};
+ },
+ { urls: ["*://*/*"] },
+ ["blocking"]
+ );
+ browser.test.onMessage.addListener(async ({ method, url }) => {
+ if (method == "setRedirectUrl") {
+ this.extUrl = url;
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+};
+
+async function withExtensionDummy(callback) {
+ let extension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await extension.startup();
+ let rv = await callback(`moz-extension://${extension.uuid}/`, extension);
+ await extension.unload();
+ return rv;
+}
+
+async function postFrom(start, target) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: start,
+ },
+ async function (browser) {
+ info("Test tab ready: postFrom " + start);
+
+ // Create the form element in our loaded URI.
+ await SpecialPowers.spawn(browser, [{ target }], function ({ target }) {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.body.innerHTML = `
+ <form method="post" action="${target}">
+ <input type="text" name="initialRemoteType" value="${Services.appinfo.remoteType}">
+ <input type="submit" id="submit">
+ </form>`;
+ });
+
+ // Perform a form POST submit load.
+ info("Performing POST submission");
+ await performLoad(
+ browser,
+ {
+ url(url) {
+ let enable =
+ url.startsWith(PRINT_POSTDATA) ||
+ url == target ||
+ url == DATA_URL;
+ if (!enable) {
+ info(`url ${url} is invalid to perform load`);
+ }
+ return enable;
+ },
+ maybeErrorPage: true,
+ },
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("#submit").click();
+ });
+ }
+ );
+
+ // Check that the POST data was submitted.
+ info("Fetching results");
+ return SpecialPowers.spawn(browser, [], () => {
+ return {
+ remoteType: Services.appinfo.remoteType,
+ location: "" + content.location.href,
+ body: content.document.body.textContent,
+ };
+ });
+ }
+ );
+}
+
+async function loadAndGetProcessID(browser, target) {
+ info(`Performing GET load: ${target}`);
+ await performLoad(
+ browser,
+ {
+ maybeErrorPage: true,
+ },
+ () => {
+ BrowserTestUtils.loadURIString(browser, target);
+ }
+ );
+
+ info(`Navigated to: ${target}`);
+ browser = gBrowser.selectedBrowser;
+ let processID = await SpecialPowers.spawn(browser, [], () => {
+ return Services.appinfo.processID;
+ });
+ return processID;
+}
+
+async function testLoadAndRedirect(
+ target,
+ expectedProcessSwitch,
+ testRedirect
+) {
+ let start = httpURL(`dummy_page.html`);
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: start,
+ },
+ async function (_browser) {
+ info("Test tab ready: getFrom " + start);
+
+ let browser = gBrowser.selectedBrowser;
+ let firstProcessID = await SpecialPowers.spawn(browser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ info(`firstProcessID: ${firstProcessID}`);
+
+ let secondProcessID = await loadAndGetProcessID(browser, target);
+
+ info(`secondProcessID: ${secondProcessID}`);
+ Assert.equal(firstProcessID != secondProcessID, expectedProcessSwitch);
+
+ if (!testRedirect) {
+ return;
+ }
+
+ let thirdProcessID = await loadAndGetProcessID(browser, add307(target));
+
+ info(`thirdProcessID: ${thirdProcessID}`);
+ Assert.equal(firstProcessID != thirdProcessID, expectedProcessSwitch);
+ Assert.ok(secondProcessID == thirdProcessID);
+ }
+ );
+}
+
+add_task(async function test_enabled() {
+ // Force only one webIsolated content process to ensure same-origin loads
+ // always end in the same process.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount.webIsolated", 1]],
+ });
+
+ // URIs should correctly switch processes & the POST
+ // should succeed.
+ info("ENABLED -- FILE -- raw URI load");
+ let resp = await postFrom(FILE_DUMMY, PRINT_POSTDATA);
+ ok(E10SUtils.isWebRemoteType(resp.remoteType), "process switch");
+ is(resp.location, PRINT_POSTDATA, "correct location");
+ is(resp.body, "initialRemoteType=file", "correct POST body");
+
+ info("ENABLED -- FILE -- 307-redirect URI load");
+ let resp307 = await postFrom(FILE_DUMMY, add307(PRINT_POSTDATA));
+ ok(E10SUtils.isWebRemoteType(resp307.remoteType), "process switch");
+ is(resp307.location, PRINT_POSTDATA, "correct location");
+ is(resp307.body, "initialRemoteType=file", "correct POST body");
+
+ // Same with extensions
+ await withExtensionDummy(async extOrigin => {
+ info("ENABLED -- EXTENSION -- raw URI load");
+ let respExt = await postFrom(extOrigin + "dummy.html", PRINT_POSTDATA);
+ ok(E10SUtils.isWebRemoteType(respExt.remoteType), "process switch");
+ is(respExt.location, PRINT_POSTDATA, "correct location");
+ is(respExt.body, "initialRemoteType=extension", "correct POST body");
+
+ info("ENABLED -- EXTENSION -- extension-redirect URI load");
+ let respExtRedirect = await postFrom(
+ extOrigin + "redirect.html",
+ PRINT_POSTDATA
+ );
+ ok(E10SUtils.isWebRemoteType(respExtRedirect.remoteType), "process switch");
+ is(
+ respExtRedirect.location,
+ PRINT_POSTDATA + "?redirected",
+ "correct location"
+ );
+ is(
+ respExtRedirect.body,
+ "initialRemoteType=extension?redirected",
+ "correct POST body"
+ );
+
+ info("ENABLED -- EXTENSION -- 307-redirect URI load");
+ let respExt307 = await postFrom(
+ extOrigin + "dummy.html",
+ add307(PRINT_POSTDATA)
+ );
+ ok(E10SUtils.isWebRemoteType(respExt307.remoteType), "process switch");
+ is(respExt307.location, PRINT_POSTDATA, "correct location");
+ is(respExt307.body, "initialRemoteType=extension", "correct POST body");
+ });
+});
+
+async function sendMessage(ext, method, url) {
+ ext.sendMessage({ method, url });
+ await ext.awaitMessage("done");
+}
+
+// TODO: Currently no test framework for ftp://.
+add_task(async function test_protocol() {
+ // TODO: Processes should be switched due to navigation of different origins.
+ await testLoadAndRedirect("data:,foo", false, true);
+
+ // Redirecting to file:// is not allowed.
+ await testLoadAndRedirect(FILE_DUMMY, true, false);
+
+ await withExtensionDummy(async (extOrigin, extension) => {
+ await sendMessage(extension, "setRedirectUrl", DATA_URL);
+
+ let respExtRedirect = await postFrom(
+ extOrigin + "redirect.html",
+ PRINT_POSTDATA
+ );
+
+ ok(E10SUtils.isWebRemoteType(respExtRedirect.remoteType), "process switch");
+ is(respExtRedirect.location, DATA_URL, "correct location");
+ is(respExtRedirect.body, DATA_STRING, "correct POST body");
+ });
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js b/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js
new file mode 100644
index 0000000000..3cdfed48cc
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js
@@ -0,0 +1,79 @@
+/*
+ * Test that following a link with a scheme that opens externally (like
+ * irc:) does not blank the page (Bug 1630757).
+ */
+
+const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+let Pages = [httpURL("dummy_page.html"), fileURL("dummy_page.html")];
+
+/**
+ * Creates dummy protocol handler
+ */
+function initTestHandlers() {
+ let handlerInfo =
+ HandlerServiceTestUtils.getBlankHandlerInfo("test-proto://");
+
+ let appHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ // This is a dir and not executable, but that's enough for here.
+ appHandler.executable = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ handlerInfo.possibleApplicationHandlers.appendElement(appHandler);
+ handlerInfo.preferredApplicationHandler = appHandler;
+ handlerInfo.preferredAction = handlerInfo.useHelperApp;
+ handlerInfo.alwaysAskBeforeHandling = false;
+ gHandlerService.store(handlerInfo);
+
+ registerCleanupFunction(() => {
+ gHandlerService.remove(handlerInfo);
+ });
+}
+
+async function runTest() {
+ initTestHandlers();
+
+ for (let page of Pages) {
+ await BrowserTestUtils.withNewTab(page, async function (aBrowser) {
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ let h = content.document.createElement("h1");
+ ok(h);
+ h.innerHTML = "My heading";
+ h.id = "my-heading";
+ content.document.body.append(h);
+ is(content.document.getElementById("my-heading"), h, "h exists");
+
+ let a = content.document.createElement("a");
+ ok(a);
+ a.innerHTML = "my link";
+ a.id = "my-link";
+ content.document.body.append(a);
+ });
+
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ let url = "test-proto://some-thing";
+
+ let a = content.document.getElementById("my-link");
+ ok(a);
+ a.href = url;
+ a.click();
+ });
+
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ ok(
+ content.document.getElementById("my-heading"),
+ "Page contents not erased"
+ );
+ });
+ });
+ }
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(runTest);
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
new file mode 100644
index 0000000000..a99db3b289
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
@@ -0,0 +1,410 @@
+"use strict";
+
+const COOP_PREF = "browser.tabs.remote.useCrossOriginOpenerPolicy";
+
+async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[COOP_PREF, true]],
+ });
+}
+
+async function unsetPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[COOP_PREF, false]],
+ });
+}
+
+function httpURL(filename, host = "https://example.com") {
+ let root = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ host
+ );
+ return root + filename;
+}
+
+async function performLoad(browser, opts, action) {
+ let loadedPromise = BrowserTestUtils.browserStopped(
+ browser,
+ opts.url,
+ opts.maybeErrorPage
+ );
+ await action();
+ await loadedPromise;
+}
+
+async function test_coop(
+ start,
+ target,
+ expectedProcessSwitch,
+ startRemoteTypeCheck,
+ targetRemoteTypeCheck
+) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: start,
+ waitForStateStop: true,
+ },
+ async function (_browser) {
+ info(`test_coop: Test tab ready: ${start}`);
+
+ let browser = gBrowser.selectedBrowser;
+ let firstRemoteType = browser.remoteType;
+ let firstBC = browser.browsingContext;
+
+ info(`firstBC: ${firstBC.id} remoteType: ${firstRemoteType}`);
+
+ if (startRemoteTypeCheck) {
+ startRemoteTypeCheck(firstRemoteType);
+ }
+
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURIString(browser, target)
+ );
+
+ info(`Navigated to: ${target}`);
+ browser = gBrowser.selectedBrowser;
+ let secondRemoteType = browser.remoteType;
+ let secondBC = browser.browsingContext;
+
+ info(`secondBC: ${secondBC.id} remoteType: ${secondRemoteType}`);
+ if (targetRemoteTypeCheck) {
+ targetRemoteTypeCheck(secondRemoteType);
+ }
+ if (expectedProcessSwitch) {
+ Assert.notEqual(firstBC.id, secondBC.id, `from: ${start} to ${target}`);
+ } else {
+ Assert.equal(firstBC.id, secondBC.id, `from: ${start} to ${target}`);
+ }
+ }
+ );
+}
+
+function waitForDownloadWindow() {
+ return new Promise(resolve => {
+ var listener = {
+ onOpenWindow: aXULWindow => {
+ info("Download window shown...");
+ Services.wm.removeListener(listener);
+
+ function downloadOnLoad() {
+ domwindow.removeEventListener("load", downloadOnLoad, true);
+
+ is(
+ domwindow.document.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Download page appeared"
+ );
+ resolve(domwindow);
+ }
+
+ var domwindow = aXULWindow.docShell.domWindow;
+ domwindow.addEventListener("load", downloadOnLoad, true);
+ },
+ onCloseWindow: aXULWindow => {},
+ };
+
+ Services.wm.addListener(listener);
+ });
+}
+
+async function waitForDownloadUI() {
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+async function cleanupDownloads(downloadList) {
+ info("cleaning up downloads");
+ let [download] = await downloadList.getAll();
+ await downloadList.remove(download);
+ await download.finalize(true);
+
+ try {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (error) {
+ info("The file " + download.target.path + " is not removed, " + error);
+ }
+
+ if (DownloadsPanel.panel.state !== "closed") {
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+ }
+ is(
+ DownloadsPanel.panel.state,
+ "closed",
+ "Check that the download panel is closed"
+ );
+}
+
+async function test_download_from(initCoop, downloadCoop) {
+ return BrowserTestUtils.withNewTab("about:blank", async function (_browser) {
+ info(`test_download: Test tab ready`);
+
+ let start = httpURL(
+ "coop_header.sjs?downloadPage&coop=" + initCoop,
+ "https://example.com"
+ );
+ await performLoad(
+ _browser,
+ {
+ url: start,
+ maybeErrorPage: false,
+ },
+ async () => {
+ info(`test_download: Loading download page ${start}`);
+ return BrowserTestUtils.loadURIString(_browser, start);
+ }
+ );
+
+ info(`test_download: Download page ready ${start}`);
+ info(`Downloading ${downloadCoop}`);
+
+ let expectDialog = Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ );
+ let resultPromise = expectDialog
+ ? waitForDownloadWindow()
+ : waitForDownloadUI();
+ let browser = gBrowser.selectedBrowser;
+ SpecialPowers.spawn(browser, [downloadCoop], downloadCoop => {
+ content.document.getElementById(downloadCoop).click();
+ });
+
+ // if the download page doesn't appear, the promise leads a timeout.
+ if (expectDialog) {
+ let win = await resultPromise;
+ await BrowserTestUtils.closeWindow(win);
+ } else {
+ // verify link target will get automatically downloaded
+ await resultPromise;
+ let downloadList = await Downloads.getList(Downloads.PUBLIC);
+ is((await downloadList.getAll()).length, 1, "Target was downloaded");
+ await cleanupDownloads(downloadList);
+ is((await downloadList.getAll()).length, 0, "Downloads were cleaned up");
+ }
+ });
+}
+
+// Check that multiple navigations of the same tab will only switch processes
+// when it's expected.
+add_task(async function test_multiple_nav_process_switches() {
+ await setPref();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: httpURL("coop_header.sjs", "https://example.org"),
+ waitForStateStop: true,
+ },
+ async function (browser) {
+ let prevBC = browser.browsingContext;
+
+ let target = httpURL("coop_header.sjs?.", "https://example.org");
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURIString(browser, target)
+ );
+
+ Assert.equal(prevBC, browser.browsingContext);
+ prevBC = browser.browsingContext;
+
+ target = httpURL(
+ "coop_header.sjs?coop=same-origin",
+ "https://example.org"
+ );
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURIString(browser, target)
+ );
+
+ Assert.notEqual(prevBC, browser.browsingContext);
+ prevBC = browser.browsingContext;
+
+ target = httpURL(
+ "coop_header.sjs?coop=same-origin",
+ "https://example.com"
+ );
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURIString(browser, target)
+ );
+
+ Assert.notEqual(prevBC, browser.browsingContext);
+ prevBC = browser.browsingContext;
+
+ target = httpURL(
+ "coop_header.sjs?coop=same-origin&index=4",
+ "https://example.com"
+ );
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURIString(browser, target)
+ );
+
+ Assert.equal(prevBC, browser.browsingContext);
+ }
+ );
+});
+
+add_task(async function test_disabled() {
+ await unsetPref();
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL("coop_header.sjs", "https://example.com"),
+ false
+ );
+ await test_coop(
+ httpURL("coop_header.sjs?coop=same-origin", "http://example.com"),
+ httpURL("coop_header.sjs", "http://example.com"),
+ false
+ );
+ await test_coop(
+ httpURL("coop_header.sjs", "http://example.com"),
+ httpURL("coop_header.sjs?coop=same-origin", "http://example.com"),
+ false
+ );
+});
+
+add_task(async function test_enabled() {
+ await setPref();
+
+ function checkIsCoopRemoteType(remoteType) {
+ Assert.ok(
+ remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX),
+ `${remoteType} expected to be coop`
+ );
+ }
+
+ function checkIsNotCoopRemoteType(remoteType) {
+ if (gFissionBrowser) {
+ Assert.ok(
+ remoteType.startsWith("webIsolated="),
+ `${remoteType} expected to start with webIsolated=`
+ );
+ } else {
+ Assert.equal(
+ remoteType,
+ E10SUtils.WEB_REMOTE_TYPE,
+ `${remoteType} expected to be web`
+ );
+ }
+ }
+
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL("coop_header.sjs", "https://example.com"),
+ false,
+ checkIsNotCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL("coop_header.sjs?coop=same-origin", "https://example.org"),
+ true,
+ checkIsNotCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL("coop_header.sjs?coop=same-origin&index=1", "https://example.com"),
+ httpURL("coop_header.sjs?coop=same-origin&index=1", "https://example.org"),
+ true,
+ checkIsNotCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp",
+ "https://example.com"
+ ),
+ true,
+ checkIsNotCoopRemoteType,
+ checkIsCoopRemoteType
+ );
+ await test_coop(
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=2",
+ "https://example.com"
+ ),
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=3",
+ "https://example.com"
+ ),
+ false,
+ checkIsCoopRemoteType,
+ checkIsCoopRemoteType
+ );
+ await test_coop(
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=4",
+ "https://example.com"
+ ),
+ httpURL("coop_header.sjs", "https://example.com"),
+ true,
+ checkIsCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=5",
+ "https://example.com"
+ ),
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=6",
+ "https://example.org"
+ ),
+ true,
+ checkIsCoopRemoteType,
+ checkIsCoopRemoteType
+ );
+});
+
+add_task(async function test_download() {
+ requestLongerTimeout(4);
+ await setPref();
+
+ let initCoopArray = ["", "same-origin"];
+
+ let downloadCoopArray = [
+ "no-coop",
+ "same-origin",
+ "same-origin-allow-popups",
+ ];
+
+ // If the coop mismatch between current page and download link, clicking the
+ // download link will make the page empty and popup the download window. That
+ // forces us to reload the page every time.
+ for (var initCoop of initCoopArray) {
+ for (var downloadCoop of downloadCoopArray) {
+ await test_download_from(initCoop, downloadCoop);
+ }
+ }
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js
new file mode 100644
index 0000000000..56151ae94e
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js
@@ -0,0 +1,115 @@
+const HISTORY = [
+ { url: httpURL("dummy_page.html") },
+ { url: fileURL("dummy_page.html") },
+ { url: httpURL("dummy_page.html") },
+];
+
+function reversed(list) {
+ let copy = list.slice();
+ copy.reverse();
+ return copy;
+}
+
+function butLast(list) {
+ return list.slice(0, -1);
+}
+
+async function runTest() {
+ await BrowserTestUtils.withNewTab({ gBrowser }, async function (aBrowser) {
+ // Perform initial load of each URL in the history.
+ let count = 0;
+ let index = -1;
+ for (let { url } of HISTORY) {
+ BrowserTestUtils.loadURIString(aBrowser, url);
+
+ await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => {
+ return (
+ Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme
+ );
+ });
+
+ count++;
+ index++;
+ await SpecialPowers.spawn(
+ aBrowser,
+ [{ count, index, url }],
+ async function ({ count, index, url }) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ is(
+ docShell.sessionHistory.count,
+ count,
+ "Initial Navigation Count Match"
+ );
+ is(
+ docShell.sessionHistory.index,
+ index,
+ "Initial Navigation Index Match"
+ );
+
+ let real = Services.io.newURI(content.location.href);
+ let expect = Services.io.newURI(url);
+ is(real.scheme, expect.scheme, "Initial Navigation URL Scheme");
+ }
+ );
+ }
+
+ // Go back to the first entry.
+ for (let { url } of reversed(HISTORY).slice(1)) {
+ SpecialPowers.spawn(aBrowser, [], () => {
+ content.history.back();
+ });
+ await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => {
+ return (
+ Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme
+ );
+ });
+
+ index--;
+ await SpecialPowers.spawn(
+ aBrowser,
+ [{ count, index, url }],
+ async function ({ count, index, url }) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ is(docShell.sessionHistory.count, count, "Go Back Count Match");
+ is(docShell.sessionHistory.index, index, "Go Back Index Match");
+
+ let real = Services.io.newURI(content.location.href);
+ let expect = Services.io.newURI(url);
+ is(real.scheme, expect.scheme, "Go Back URL Scheme");
+ }
+ );
+ }
+
+ // Go forward to the last entry.
+ for (let { url } of HISTORY.slice(1)) {
+ SpecialPowers.spawn(aBrowser, [], () => {
+ content.history.forward();
+ });
+ await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => {
+ return (
+ Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme
+ );
+ });
+
+ index++;
+ await SpecialPowers.spawn(
+ aBrowser,
+ [{ count, index, url }],
+ async function ({ count, index, url }) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ is(docShell.sessionHistory.count, count, "Go Forward Count Match");
+ is(docShell.sessionHistory.index, index, "Go Forward Index Match");
+
+ let real = Services.io.newURI(content.location.href);
+ let expect = Services.io.newURI(url);
+ is(real.scheme, expect.scheme, "Go Forward URL Scheme");
+ }
+ );
+ }
+ });
+}
+
+add_task(runTest);
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js
new file mode 100644
index 0000000000..3286227d37
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js
@@ -0,0 +1,161 @@
+add_task(async function oopProcessSwap() {
+ const FILE = fileURL("dummy_page.html");
+ const WEB = httpURL("file_postmsg_parent.html");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: FILE },
+ async browser => {
+ is(browser.browsingContext.children.length, 0);
+
+ info("creating an in-process frame");
+ let frameId = await SpecialPowers.spawn(
+ browser,
+ [{ FILE }],
+ async ({ FILE }) => {
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", FILE);
+ content.document.body.appendChild(iframe);
+
+ // The nested URI should be same-process
+ ok(iframe.browsingContext.docShell, "Should be in-process");
+
+ return iframe.browsingContext.id;
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+
+ info("navigating to x-process frame");
+ let oopinfo = await SpecialPowers.spawn(
+ browser,
+ [{ WEB }],
+ async ({ WEB }) => {
+ let iframe = content.document.querySelector("iframe");
+
+ iframe.contentWindow.location = WEB;
+
+ let data = await new Promise(resolve => {
+ content.window.addEventListener(
+ "message",
+ function (evt) {
+ info("oop iframe loaded");
+ is(evt.source, iframe.contentWindow);
+ resolve(evt.data);
+ },
+ { once: true }
+ );
+ });
+
+ is(iframe.browsingContext.docShell, null, "Should be out-of-process");
+ is(
+ iframe.browsingContext.embedderElement,
+ iframe,
+ "correct embedder"
+ );
+
+ return {
+ location: data.location,
+ browsingContextId: iframe.browsingContext.id,
+ };
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+
+ if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) {
+ is(
+ frameId,
+ oopinfo.browsingContextId,
+ `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})`
+ );
+ }
+ is(oopinfo.location, WEB, "correct location");
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function oopOriginProcessSwap() {
+ const COM_DUMMY = httpURL("dummy_page.html", "https://example.com/");
+ const ORG_POSTMSG = httpURL(
+ "file_postmsg_parent.html",
+ "https://example.org/"
+ );
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: COM_DUMMY },
+ async browser => {
+ is(browser.browsingContext.children.length, 0);
+
+ info("creating an in-process frame");
+ let frameId = await SpecialPowers.spawn(
+ browser,
+ [{ COM_DUMMY }],
+ async ({ COM_DUMMY }) => {
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", COM_DUMMY);
+ content.document.body.appendChild(iframe);
+
+ // The nested URI should be same-process
+ ok(iframe.browsingContext.docShell, "Should be in-process");
+
+ return iframe.browsingContext.id;
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+
+ info("navigating to x-process frame");
+ let oopinfo = await SpecialPowers.spawn(
+ browser,
+ [{ ORG_POSTMSG }],
+ async ({ ORG_POSTMSG }) => {
+ let iframe = content.document.querySelector("iframe");
+
+ iframe.contentWindow.location = ORG_POSTMSG;
+
+ let data = await new Promise(resolve => {
+ content.window.addEventListener(
+ "message",
+ function (evt) {
+ info("oop iframe loaded");
+ is(evt.source, iframe.contentWindow);
+ resolve(evt.data);
+ },
+ { once: true }
+ );
+ });
+
+ is(iframe.browsingContext.docShell, null, "Should be out-of-process");
+ is(
+ iframe.browsingContext.embedderElement,
+ iframe,
+ "correct embedder"
+ );
+
+ return {
+ location: data.location,
+ browsingContextId: iframe.browsingContext.id,
+ };
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+ if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) {
+ is(
+ frameId,
+ oopinfo.browsingContextId,
+ `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})`
+ );
+ }
+ is(oopinfo.location, ORG_POSTMSG, "correct location");
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs
new file mode 100644
index 0000000000..c6b537d770
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs
@@ -0,0 +1,58 @@
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ // The tests for Cross-Origin-Opener-Policy unfortunately depend on
+ // BFCacheInParent not kicking in, as with that enabled, it is not possible to
+ // tell whether the BrowsingContext switch was caused by the BFCache
+ // navigation or by the COOP mismatch. This header disables BFCache for the
+ // coop documents, and should avoid the issue.
+ response.setHeader("Cache-Control", "no-store", false);
+
+ let isDownloadPage = false;
+ let isDownloadFile = false;
+
+ query.forEach((value, name) => {
+ if (name === "downloadPage") {
+ isDownloadPage = true;
+ } else if (name === "downloadFile") {
+ isDownloadFile = true;
+ } else if (name == "coop") {
+ response.setHeader("Cross-Origin-Opener-Policy", unescape(value), false);
+ } else if (name == "coep") {
+ response.setHeader(
+ "Cross-Origin-Embedder-Policy",
+ unescape(value),
+ false
+ );
+ }
+ });
+
+ let downloadHTML = "";
+ if (isDownloadPage) {
+ ["no-coop", "same-origin", "same-origin-allow-popups"].forEach(coop => {
+ downloadHTML +=
+ '<a href="https://example.com/browser/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs?downloadFile&' +
+ (coop === "no-coop" ? "" : coop) +
+ '" id="' +
+ coop +
+ '" download>' +
+ unescape(coop) +
+ "</a> <br>";
+ });
+ }
+
+ if (isDownloadFile) {
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ response.write("BINARY_DATA");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write(
+ "<!DOCTYPE html><html><body><p>Hello world</p> " +
+ downloadHTML +
+ "</body></html>"
+ );
+ }
+}
diff --git a/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html
new file mode 100644
index 0000000000..8205e90d5d
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<html>
+<body>
+<p>Page</p>
+ <script>
+ // Prevent this page from being stored in the bfcache for the
+ // browser_httpToFileHistory.js test
+ window.blockBFCache = new RTCPeerConnection();
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html b/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html
new file mode 100644
index 0000000000..d5f640775b
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<body onload="parent.postMessage({location: window.location.href}, '*')">
+</body>
+</html>
diff --git a/toolkit/components/remotebrowserutils/tests/browser/head.js b/toolkit/components/remotebrowserutils/tests/browser/head.js
new file mode 100644
index 0000000000..ffdd9375cf
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/head.js
@@ -0,0 +1,17 @@
+function fileURL(filename) {
+ let ifile = getChromeDir(getResolvedURI(gTestPath));
+ ifile.append(filename);
+ return Services.io.newFileURI(ifile).spec;
+}
+
+function httpURL(filename, host = "https://example.com/") {
+ let root = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ host
+ );
+ return root + filename;
+}
+
+function add307(url, host = "https://example.com/") {
+ return httpURL("307redirect.sjs?" + url, host);
+}
diff --git a/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs b/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs
new file mode 100644
index 0000000000..bd9c18f7ea
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs
@@ -0,0 +1,28 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+ if (request.queryString) {
+ data = data + "?" + request.queryString;
+ }
+ response.bodyOutputStream.write(data, data.length);
+ }
+}