diff --git a/caps/tests/unit/test_precursor_principal.js b/caps/tests/unit/test_precursor_principal.js
new file mode 100644
index 000000000000..9d1491b2e050
--- /dev/null
+++ b/caps/tests/unit/test_precursor_principal.js
@@ -0,0 +1,256 @@
+"use strict";
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+const { XPCShellContentUtils } = ChromeUtils.import(
+ "resource://testing-common/XPCShellContentUtils.jsm"
+);
+const { ExtensionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/ExtensionXPCShellUtils.jsm"
+);
+
+XPCShellContentUtils.init(this);
+ExtensionTestUtils.init(this);
+
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["example.com", "example.org"],
+});
+
+server.registerPathHandler("/static_frames", (request, response) => {
+ response.setHeader("Content-Type", "text/html");
+ response.write(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+});
+
+server.registerPathHandler("/frame", (request, response) => {
+ response.setHeader("Content-Type", "text/html");
+ response.write(`
HTTP Subframe
`);
+});
+
+server.registerPathHandler("/redirect", (request, response) => {
+ let redirect;
+ if (request.queryString == "com") {
+ redirect = "http://example.com/frame";
+ } else if (request.queryString == "org") {
+ redirect = "http://example.org/frame";
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not found");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", redirect);
+});
+
+server.registerPathHandler("/client_replace", (request, response) => {
+ let redirect;
+ if (request.queryString == "blank") {
+ redirect = "about:blank";
+ } else if (request.queryString == "data") {
+ redirect = "data:text/html,Data Subframe
";
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not found");
+ return;
+ }
+
+ response.setHeader("Content-Type", "text/html");
+ response.write(`
+
+ `);
+});
+
+add_task(async function sandboxed_precursor() {
+ let extension = await ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", ""],
+ },
+ background() {
+ // eslint-disable-next-line no-undef
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ let url = new URL(details.url);
+ if (!url.pathname.includes("ext_redirect")) {
+ return {};
+ }
+
+ let redirectUrl;
+ if (url.search == "?com") {
+ redirectUrl = "http://example.com/frame";
+ } else if (url.search == "?org") {
+ redirectUrl = "http://example.org/frame";
+ } else if (url.search == "?data") {
+ redirectUrl = "data:text/html,Data Subframe
";
+ }
+ return { redirectUrl };
+ },
+ { urls: [""] },
+ ["blocking"]
+ );
+ },
+ });
+ await extension.startup();
+
+ registerCleanupFunction(async function() {
+ await extension.unload();
+ });
+
+ for (let userContextId of [undefined, 1]) {
+ let comURI = Services.io.newURI("http://example.com");
+ let comPrin = Services.scriptSecurityManager.createContentPrincipal(
+ comURI,
+ { userContextId }
+ );
+ let orgURI = Services.io.newURI("http://example.org");
+ let orgPrin = Services.scriptSecurityManager.createContentPrincipal(
+ orgURI,
+ { userContextId }
+ );
+
+ let page = await XPCShellContentUtils.loadContentPage(
+ "http://example.com/static_frames",
+ {
+ remote: true,
+ remoteSubframes: true,
+ userContextId,
+ }
+ );
+ let bc = page.browsingContext;
+
+ ok(
+ bc.currentWindowGlobal.documentPrincipal.equals(comPrin),
+ "toplevel principal matches"
+ );
+
+ // XXX: This is sketchy as heck, but it's also the easiest way to wait for
+ // the `window.location.replace` loads to finish.
+ await TestUtils.waitForCondition(
+ () =>
+ bc.children.every(
+ child =>
+ !child.currentWindowGlobal.documentURI.spec.includes(
+ "/client_replace"
+ )
+ ),
+ "wait for every client_replace global to be replaced"
+ );
+
+ let principals = {};
+ for (let child of bc.children) {
+ notEqual(child.name, "", "child frames must have names");
+ ok(!(child.name in principals), "duplicate child frame name");
+ principals[child.name] = child.currentWindowGlobal.documentPrincipal;
+ }
+
+ function principal_is(name, expected) {
+ let principal = principals[name];
+ info(`${name} = ${principal.origin}`);
+ ok(principal.equals(expected), `${name} is correct`);
+ }
+ function precursor_is(name, precursor) {
+ let principal = principals[name];
+ info(`${name} = ${principal.origin}`);
+ ok(principal.isNullPrincipal, `${name} is null`);
+ ok(
+ principal.precursorPrincipal.equals(precursor),
+ `${name} has the correct precursor`
+ );
+ }
+
+ // Basic loads should have the principals or precursor principals for the
+ // document being loaded.
+ principal_is("same_origin", comPrin);
+ precursor_is("same_origin_sandbox", comPrin);
+
+ principal_is("cross_origin", orgPrin);
+ precursor_is("cross_origin_sandbox", orgPrin);
+
+ // Loads of a data: URI should complete with a sandboxed principal based on
+ // the principal which tried to perform the load.
+ precursor_is("data_uri", comPrin);
+ precursor_is("data_uri_sandbox", comPrin);
+
+ // Loads which inherit principals, such as srcdoc an about:blank loads,
+ // should also inherit sandboxed precursor principals.
+ principal_is("srcdoc", comPrin);
+ precursor_is("srcdoc_sandbox", comPrin);
+
+ principal_is("blank", comPrin);
+ precursor_is("blank_sandbox", comPrin);
+
+ // Redirects shouldn't interfere with the final principal, and it should be
+ // based only on the final URI.
+ principal_is("redirect_com", comPrin);
+ precursor_is("redirect_com_sandbox", comPrin);
+
+ principal_is("redirect_org", orgPrin);
+ precursor_is("redirect_org_sandbox", orgPrin);
+
+ // Extension redirects should act like normal redirects, and still resolve
+ // with the principal or sandboxed principal of the final URI.
+ principal_is("ext_redirect_com_com", comPrin);
+ precursor_is("ext_redirect_com_com_sandbox", comPrin);
+
+ principal_is("ext_redirect_com_org", orgPrin);
+ precursor_is("ext_redirect_com_org_sandbox", orgPrin);
+
+ principal_is("ext_redirect_org_com", comPrin);
+ precursor_is("ext_redirect_org_com_sandbox", comPrin);
+
+ principal_is("ext_redirect_org_org", orgPrin);
+ precursor_is("ext_redirect_org_org_sandbox", orgPrin);
+
+ // When an extension redirects to a data: URI, we use the last non-data: URI
+ // in the chain as the precursor principal.
+ // FIXME: This should perhaps use the extension's principal instead?
+ precursor_is("ext_redirect_com_data", comPrin);
+ precursor_is("ext_redirect_com_data_sandbox", comPrin);
+
+ precursor_is("ext_redirect_org_data", orgPrin);
+ precursor_is("ext_redirect_org_data_sandbox", orgPrin);
+
+ // Check that navigations triggred by script within the frames will have the
+ // correct behaviour when navigating to blank and data URIs.
+ principal_is("client_replace_org_blank", orgPrin);
+ precursor_is("client_replace_org_blank_sandbox", orgPrin);
+
+ precursor_is("client_replace_org_data", orgPrin);
+ precursor_is("client_replace_org_data_sandbox", orgPrin);
+
+ await page.close();
+ }
+});
diff --git a/caps/tests/unit/xpcshell.ini b/caps/tests/unit/xpcshell.ini
index fe6e9f0b3b45..9ff2b22617fc 100644
--- a/caps/tests/unit/xpcshell.ini
+++ b/caps/tests/unit/xpcshell.ini
@@ -6,3 +6,6 @@ head =
[test_ipv6_host_literal.js]
[test_oa_partitionKey_pattern.js]
[test_site_origin.js]
+[test_precursor_principal.js]
+firefox-appdir = browser
+skip-if = toolkit == 'android'