summaryrefslogtreecommitdiffstats
path: root/dom/workers/test/xpcshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/workers/test/xpcshell
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.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/workers/test/xpcshell')
-rw-r--r--dom/workers/test/xpcshell/data/base_uri_module.mjs23
-rw-r--r--dom/workers/test/xpcshell/data/base_uri_module2.mjs1
-rw-r--r--dom/workers/test/xpcshell/data/base_uri_worker.js27
-rw-r--r--dom/workers/test/xpcshell/data/chrome.manifest1
-rw-r--r--dom/workers/test/xpcshell/data/worker.js6
-rw-r--r--dom/workers/test/xpcshell/data/worker_fileReader.js7
-rw-r--r--dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js558
-rw-r--r--dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js112
-rw-r--r--dom/workers/test/xpcshell/test_fileReader.js33
-rw-r--r--dom/workers/test/xpcshell/test_import_base_uri.js35
-rw-r--r--dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js88
-rw-r--r--dom/workers/test/xpcshell/test_workers.js37
-rw-r--r--dom/workers/test/xpcshell/test_workers_clone_error.js43
-rw-r--r--dom/workers/test/xpcshell/xpcshell.toml38
14 files changed, 1009 insertions, 0 deletions
diff --git a/dom/workers/test/xpcshell/data/base_uri_module.mjs b/dom/workers/test/xpcshell/data/base_uri_module.mjs
new file mode 100644
index 0000000000..7604baed82
--- /dev/null
+++ b/dom/workers/test/xpcshell/data/base_uri_module.mjs
@@ -0,0 +1,23 @@
+// This file is for testing the module loader's path handling.
+// ESLint rules that modifies path shouldn't be applied.
+
+export const obj = {};
+
+export async function doImport() {
+ // This file is loaded as resource://test/data/base_uri_module.mjs
+ // Relative/absolute paths should be resolved based on the URI, instead of
+ // file: path.
+
+ const namespaceWithURI = await import(
+ "resource://test/data/base_uri_module2.mjs"
+ );
+ const namespaceWithCurrentDir = await import("./base_uri_module2.mjs");
+ const namespaceWithParentDir = await import("../data/base_uri_module2.mjs");
+ const namespaceWithAbsoluteDir = await import("/data/base_uri_module2.mjs");
+
+ return {
+ equal1: namespaceWithURI.obj2 == namespaceWithCurrentDir.obj2,
+ equal2: namespaceWithURI.obj2 == namespaceWithParentDir.obj2,
+ equal3: namespaceWithURI.obj2 == namespaceWithAbsoluteDir.obj2,
+ };
+}
diff --git a/dom/workers/test/xpcshell/data/base_uri_module2.mjs b/dom/workers/test/xpcshell/data/base_uri_module2.mjs
new file mode 100644
index 0000000000..2358d27a83
--- /dev/null
+++ b/dom/workers/test/xpcshell/data/base_uri_module2.mjs
@@ -0,0 +1 @@
+export const obj2 = {};
diff --git a/dom/workers/test/xpcshell/data/base_uri_worker.js b/dom/workers/test/xpcshell/data/base_uri_worker.js
new file mode 100644
index 0000000000..74137cc20b
--- /dev/null
+++ b/dom/workers/test/xpcshell/data/base_uri_worker.js
@@ -0,0 +1,27 @@
+// This file is for testing the module loader's path handling.
+// ESLint rules that modifies path shouldn't be applied.
+
+onmessage = async event => {
+ // This file is loaded as resource://test/data/base_uri_worker.js
+ // Relative/absolute paths should be resolved based on the URI, instead of
+ // file: path.
+
+ const namespaceWithURI = await import(
+ "resource://test/data/base_uri_module.mjs"
+ );
+ const namespaceWithCurrentDir = await import("./base_uri_module.mjs");
+ const namespaceWithParentDir = await import("../data/base_uri_module.mjs");
+ const namespaceWithAbsoluteDir = await import("/data/base_uri_module.mjs");
+
+ postMessage({
+ scriptToModule: {
+ equal1: namespaceWithURI.obj == namespaceWithCurrentDir.obj,
+ equal2: namespaceWithURI.obj == namespaceWithParentDir.obj,
+ equal3: namespaceWithURI.obj == namespaceWithAbsoluteDir.obj,
+ },
+ moduleToModuleURI: await namespaceWithURI.doImport(),
+ moduleToModuleCurrent: await namespaceWithCurrentDir.doImport(),
+ moduleToModuleParent: await namespaceWithParentDir.doImport(),
+ moduleToModuleAbsolute: await namespaceWithAbsoluteDir.doImport(),
+ });
+};
diff --git a/dom/workers/test/xpcshell/data/chrome.manifest b/dom/workers/test/xpcshell/data/chrome.manifest
new file mode 100644
index 0000000000..611e81fd4e
--- /dev/null
+++ b/dom/workers/test/xpcshell/data/chrome.manifest
@@ -0,0 +1 @@
+content workers ./
diff --git a/dom/workers/test/xpcshell/data/worker.js b/dom/workers/test/xpcshell/data/worker.js
new file mode 100644
index 0000000000..0a455f51c3
--- /dev/null
+++ b/dom/workers/test/xpcshell/data/worker.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+self.onmessage = function (msg) {
+ self.postMessage("OK");
+};
diff --git a/dom/workers/test/xpcshell/data/worker_fileReader.js b/dom/workers/test/xpcshell/data/worker_fileReader.js
new file mode 100644
index 0000000000..44e7e6499b
--- /dev/null
+++ b/dom/workers/test/xpcshell/data/worker_fileReader.js
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+self.onmessage = function (msg) {
+ var fr = new FileReader();
+ self.postMessage("OK");
+};
diff --git a/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js
new file mode 100644
index 0000000000..3028e5d539
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js
@@ -0,0 +1,558 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const { createHttpServer } = AddonTestUtils;
+
+// Force ServiceWorkerRegistrar to init by calling do_get_profile.
+// (This has to be called before AddonTestUtils.init, because it does
+// also call do_get_profile internally but it doesn't notify
+// profile-after-change).
+do_get_profile(true);
+
+AddonTestUtils.init(this);
+ExtensionTestUtils.init(this);
+
+const server = createHttpServer({ hosts: ["localhost"] });
+
+server.registerPathHandler("/page.html", (request, response) => {
+ info(`/page.html is being requested: ${JSON.stringify(request)}`);
+ response.write(`<!DOCTYPE html>`);
+});
+
+server.registerPathHandler("/sw.js", (request, response) => {
+ info(`/sw.js is being requested: ${JSON.stringify(request)}`);
+ response.setHeader("Content-Type", "application/javascript");
+ response.write(`
+ dump('Executing http://localhost/sw.js\\n');
+ importScripts('sw-imported.js');
+ dump('Executed importScripts from http://localhost/sw.js\\n');
+ `);
+});
+
+server.registerPathHandler("/sw-imported.js", (request, response) => {
+ info(`/sw-imported.js is being requested: ${JSON.stringify(request)}`);
+ response.setHeader("Content-Type", "application/javascript");
+ response.write(`
+ dump('importScript loaded from http://localhost/sw-imported.js\\n');
+ self.onmessage = evt => evt.ports[0].postMessage('original-imported-script');
+ `);
+});
+
+Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true);
+// Make sure this test file doesn't run with the legacy behavior by
+// setting explicitly the expected default value.
+Services.prefs.setBoolPref(
+ "extensions.filterResponseServiceWorkerScript.disabled",
+ false
+);
+Services.prefs.setBoolPref("extensions.dnr.enabled", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled");
+ Services.prefs.clearUserPref(
+ "extensions.filterResponseServiceWorkerScript.disabled"
+ );
+ Services.prefs.clearUserPref("extensions.dnr.enabled");
+});
+
+// Helper function used to be sure to clear any data that a previous test case
+// may have left (e.g. service worker registration, cached service worker
+// scripts).
+//
+// NOTE: Given that xpcshell test are running isolated from each other (unlike
+// mochitests), we can just clear every storage type supported by clear data
+// (instead of cherry picking what we want to clear based on the test cases
+// part of this test file).
+async function ensureDataCleanup() {
+ info("Clear any service worker or data previous test cases may have left");
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+}
+
+// Note that the update algorithm (https://w3c.github.io/ServiceWorker/#update-algorithm)
+// builds an "updatedResourceMap" as part of its check process. This means that only a
+// single fetch will be performed for "sw-imported.js" as part of the update check and its
+// resulting install invocation. The installation's call to importScripts when evaluated
+// will load the script directly out of the Cache API.
+function testSWUpdate(contentPage) {
+ return contentPage.spawn([], async () => {
+ const oldReg = await this.content.navigator.serviceWorker.ready;
+ const reg = await oldReg.update();
+ const sw = reg.installing || reg.waiting || reg.active;
+ return new Promise(resolve => {
+ const { MessageChannel } = this.content;
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+}
+
+add_task(async function test_extension_invalid_sw_scripts_redirect_ignored() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["<all_urls>", "webRequest", "webRequestBlocking"],
+ // In this test task, the extension resource is not expected to be
+ // requested at all, so it does not really matter whether the file is
+ // listed in web_accessible_resources. Regardless, add it to make sure
+ // that any load failure is not caused by the lack of being listed here.
+ web_accessible_resources: ["sw-unexpected-redirect.js"],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ req => {
+ if (req.url == "http://localhost/sw.js") {
+ const filter = browser.webRequest.filterResponseData(req.requestId);
+ filter.ondata = event => filter.write(event.data);
+ filter.onstop = event => filter.disconnect();
+ filter.onerror = () => {
+ browser.test.sendMessage(
+ "filter-response-error:mainscript",
+ filter.error
+ );
+ };
+ return {
+ redirectUrl: browser.runtime.getURL("sw-unexpected-redirect.js"),
+ };
+ }
+
+ if (req.url == "http://localhost/sw-imported.js") {
+ const filter = browser.webRequest.filterResponseData(req.requestId);
+ filter.ondata = event => filter.write(event.data);
+ filter.onstop = event => filter.disconnect();
+ filter.onerror = () => {
+ browser.test.sendMessage(
+ "filter-response-error:importscript",
+ filter.error
+ );
+ };
+ return { redirectUrl: "about:blank" };
+ }
+
+ return {};
+ },
+ { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] },
+ ["blocking"]
+ );
+ },
+ files: {
+ "sw-unexpected-redirect.js": `
+ dump('main worker redirected to moz-extension://UUID/sw-unexpected-redirect.js\\n');
+ self.onmessage = evt => evt.ports[0].postMessage('sw-unexpected-redirect');
+ `,
+ },
+ });
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+
+ function awaitConsoleMessage(regexp) {
+ return new Promise(resolve => {
+ Services.console.registerListener(function listener(message) {
+ if (regexp.test(message.message)) {
+ Services.console.unregisterListener(listener);
+ resolve(message);
+ }
+ });
+ });
+ }
+
+ const awaitIgnoredMainScriptRedirect = awaitConsoleMessage(
+ /Invalid redirectUrl .* on service worker main script/
+ );
+ const awaitIgnoredImportScriptRedirect = awaitConsoleMessage(
+ /Invalid redirectUrl .* on service worker imported script/
+ );
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ // Register the worker after loading the test extension, which should not be
+ // able to intercept and redirect the importedScripts requests because of
+ // invalid destinations.
+ info("Register service worker from a content webpage");
+ let workerMessage = await contentPage.spawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { MessageChannel } = this.content;
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (importScripts not intercepted)"
+ );
+
+ info("Wait for the expected error message on main script redirect");
+ const errorMsg = await awaitIgnoredMainScriptRedirect;
+ ok(errorMsg?.message, `Got error message: ${errorMsg?.message}`);
+ ok(
+ errorMsg?.message?.includes(extension.id),
+ "error message should include the addon id"
+ );
+ ok(
+ errorMsg?.message?.includes("http://localhost/sw.js"),
+ "error message should include the sw main script url"
+ );
+
+ info("Wait for the expected error message on import script redirect");
+ const errorMsg2 = await awaitIgnoredImportScriptRedirect;
+ ok(errorMsg2?.message, `Got error message: ${errorMsg2?.message}`);
+ ok(
+ errorMsg2?.message?.includes(extension.id),
+ "error message should include the addon id"
+ );
+ ok(
+ errorMsg2?.message?.includes("http://localhost/sw-imported.js"),
+ "error message should include the sw main script url"
+ );
+
+ info("Wait filterResponse error on main script");
+ equal(
+ await extension.awaitMessage("filter-response-error:mainscript"),
+ "Invalid request ID",
+ "Got expected error on main script"
+ );
+ info("Wait filterResponse error on import script");
+ equal(
+ await extension.awaitMessage("filter-response-error:importscript"),
+ "Invalid request ID",
+ "Got expected error on import script"
+ );
+
+ await extension.unload();
+ await contentPage.close();
+});
+
+add_task(async function test_filter_sw_script() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "<all_urls>",
+ "webRequest",
+ "webRequestBlocking",
+ "webRequestFilterResponse.serviceWorkerScript",
+ ],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ req => {
+ if (req.url == "http://localhost/sw.js") {
+ const filter = browser.webRequest.filterResponseData(req.requestId);
+ let decoder = new TextDecoder("utf-8");
+ let encoder = new TextEncoder();
+ filter.ondata = event => {
+ let str = decoder.decode(event.data, { stream: true });
+ browser.test.log(`Got filter ondata event: ${str}\n`);
+ str = `
+ dump('Executing filterResponse script for http://localhost/sw.js\\n');
+ self.onmessage = evt => evt.ports[0].postMessage('filter-response-script');
+ dump('Executed firlterResponse script for http://localhost/sw.js\\n');
+ `;
+ filter.write(encoder.encode(str));
+ filter.disconnect();
+ };
+ }
+
+ return {};
+ },
+ { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] },
+ ["blocking"]
+ );
+ },
+ });
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+
+ await ensureDataCleanup();
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ let workerMessage = await contentPage.spawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { MessageChannel } = this.content;
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "filter-response-script",
+ "Got expected worker reply (filterResponse script)"
+ );
+
+ await extension.unload();
+ workerMessage = await testSWUpdate(contentPage);
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (original script)"
+ );
+
+ await contentPage.close();
+});
+
+add_task(async function test_extension_redirect_sw_imported_script() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["<all_urls>", "webRequest", "webRequestBlocking"],
+ web_accessible_resources: ["sw-imported-1.js", "sw-imported-2.js"],
+ },
+ background() {
+ let i = 1;
+ browser.webRequest.onBeforeRequest.addListener(
+ req => {
+ browser.test.log(
+ "Extension is redirecting http://localhost/sw-imported.js"
+ );
+ browser.test.sendMessage("request-redirected");
+ return {
+ redirectUrl: browser.runtime.getURL(`sw-imported-${i++}.js`),
+ };
+ },
+ { urls: ["http://localhost/sw-imported.js"] },
+ ["blocking"]
+ );
+ },
+ files: {
+ "sw-imported-1.js": `
+ dump('importScript redirected to moz-extension://UUID/sw-imported1.js \\n');
+ self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-1');
+ `,
+ "sw-imported-2.js": `
+ dump('importScript redirected to moz-extension://UUID/sw-imported2.js \\n');
+ self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-2');
+ `,
+ },
+ });
+
+ await ensureDataCleanup();
+ const contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ // Register the worker while the test extension isn't loaded and cannot
+ // intercept and redirect the importedScripts requests.
+ let workerMessage = await contentPage.spawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { MessageChannel } = this.content;
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (importScripts not intercepted)"
+ );
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+
+ // Trigger an update on the registered service worker, then assert that the
+ // reply got is coming from the script where the extension is redirecting the
+ // request.
+ info("Update service worker and expect extension script to reply");
+ workerMessage = await testSWUpdate(contentPage);
+ await extension.awaitMessage("request-redirected");
+ equal(
+ workerMessage,
+ "redirected-imported-script-1",
+ "Got expected worker reply (importScripts redirected to moz-extension url)"
+ );
+
+ // Trigger a new update of the registered service worker, then assert that the
+ // reply got is coming from a different script where the extension is
+ // redirecting the second request (this confirms that the extension can
+ // intercept and can change the redirected imported scripts on new service
+ // worker updates).
+ info("Update service worker and expect new extension script to reply");
+ workerMessage = await testSWUpdate(contentPage);
+ await extension.awaitMessage("request-redirected");
+ equal(
+ workerMessage,
+ "redirected-imported-script-2",
+ "Got expected worker reply (importScripts redirected to moz-extension url again)"
+ );
+
+ // Uninstall the extension and trigger one more update of the registered
+ // service worker, then assert that the reply got is the one coming from the
+ // server (because difference from the one got from the cache).
+ // This verify that the service worker are updated as expected after the
+ // extension is uninstalled and the worker is not stuck on the script where
+ // the extension did redirect the request the last time.
+ info(
+ "Unload extension, update service worker and expect original script to reply"
+ );
+ await extension.unload();
+ workerMessage = await testSWUpdate(contentPage);
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (importScripts not intercepted)"
+ );
+
+ await contentPage.close();
+});
+
+// Test cases for redirects with declarativeNetRequest instead of webRequest,
+// testing the same scenarios as:
+// - test_extension_invalid_sw_scripts_redirect_ignored
+// i.e. fail to redirect SW main script, fail to redirect SW to about:blank.
+// - test_extension_redirect_sw_imported_script
+// i.e. allowed to redirect importScripts to moz-extension.
+add_task(async function test_dnr_redirect_sw_script_or_import() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ permissions: ["declarativeNetRequest"],
+ host_permissions: ["<all_urls>"],
+ granted_host_permissions: true,
+ web_accessible_resources: [
+ {
+ resources: ["sw-bad-redirect.js", "sw-dnr-redirect.js", "sw-nest.js"],
+ matches: ["*://*/*"],
+ },
+ ],
+ },
+ temporarilyInstalled: true, // <-- for granted_host_permissions
+ background: async () => {
+ await browser.declarativeNetRequest.updateSessionRules({
+ addRules: [
+ {
+ id: 1,
+ condition: { urlFilter: "|http://localhost/sw.js?dnr_redir_bad" },
+ action: {
+ type: "redirect",
+ redirect: { extensionPath: "/sw-bad-redirect.js" },
+ },
+ },
+ {
+ id: 2,
+ condition: { urlFilter: "|http://localhost/sw-imported.js|" },
+ action: {
+ type: "redirect",
+ redirect: { extensionPath: "/sw-dnr-redirect.js" },
+ },
+ },
+ {
+ id: 3,
+ condition: { urlFilter: "|http://localhost/sw-nest.js|" },
+ action: {
+ type: "redirect",
+ redirect: { extensionPath: "/sw-nest.js" },
+ },
+ },
+ {
+ id: 4,
+ condition: { urlFilter: "|http://localhost/sw-imported.js?about|" },
+ action: {
+ type: "redirect",
+ redirect: { url: "about:blank" },
+ },
+ },
+ ],
+ });
+ browser.test.sendMessage("dnr_registered");
+ },
+ files: {
+ "sw-bad-redirect.js": String.raw`
+ dump('main worker redirected to moz-extension://UUID/sw-bad-redirect.js\n');
+ self.onmessage = evt => evt.ports[0].postMessage('sw-bad-redirect');
+ `,
+ "sw-dnr-redirect.js": String.raw`
+ dump('importScript redirected to moz-extension://UUID/sw-dnr-redirect.js\n');
+ self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-before-nest');
+
+ importScripts("/sw-nest.js");
+ // ^ sw-nest.js does not exist on the server, so if importScripts()
+ // succeeded, then that means that the DNR-triggered redirect worked.
+
+ self.onmessage = evt => evt.ports[0].postMessage('sw-before-about');
+ try {
+ importScripts("/sw-imported.js?about");
+ // ^ DNR redirects to about:blank, which should throw here.
+ self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-about-bad');
+ } catch (e) {
+ // All is good.
+ self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-redirect');
+ }
+ `,
+ "sw-nest.js": String.raw`
+ dump('importScript redirected to moz-extension://UUID/sw-nest.js\n');
+ // No other code here. The caller verifies success by confirming that
+ // the importScripts() call did not throw.
+ `,
+ },
+ });
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+ await extension.awaitMessage("dnr_registered");
+
+ await ensureDataCleanup();
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ // Register the worker after loading the test extension, which should not be
+ // able to intercept and redirect the importedScripts requests because of
+ // invalid destinations.
+ info("Register service worker from a content webpage (disallowed redirects)");
+ await contentPage.spawn([], async () => {
+ await Assert.rejects(
+ this.content.navigator.serviceWorker.register("/sw.js?dnr_redir_bad1"),
+ /SecurityError: The operation is insecure/,
+ "Redirect of main service worker script is not allowed"
+ );
+ });
+ info("Register service worker from a content webpage (with import redirect)");
+ let workerMessage = await contentPage.spawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { MessageChannel } = this.content;
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "sw-dnr-redirect",
+ "Got expected worker reply (importScripts redirected to moz-extension:-URL)"
+ );
+
+ await extension.unload();
+ await contentPage.close();
+});
diff --git a/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js b/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js
new file mode 100644
index 0000000000..718093422b
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js
@@ -0,0 +1,112 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const { createHttpServer } = AddonTestUtils;
+
+// Force ServiceWorkerRegistrar to init by calling do_get_profile.
+// (This has to be called before AddonTestUtils.init, because it does
+// also call do_get_profile internally but it doesn't notify
+// profile-after-change).
+do_get_profile(true);
+
+AddonTestUtils.init(this);
+ExtensionTestUtils.init(this);
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerPathHandler("/dummy", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("dummy page");
+});
+
+add_setup(() => {
+ info("Making sure Services.io.offline is true");
+ // Explicitly setting Services.io.offline to true makes this test able
+ // to hit on Desktop builds the same issue that test_ext_cache_api.js
+ // was hitting on Android builds (Bug 1844825).
+ Services.io.offline = true;
+});
+
+// Regression test derived from Bug 1844825.
+add_task(async function test_fetch_request_from_ext_shared_worker() {
+ if (!WebExtensionPolicy.useRemoteWebExtensions) {
+ // Ensure RemoteWorkerService has been initialized in the main
+ // process.
+ Services.obs.notifyObservers(null, "profile-after-change");
+ }
+
+ const background = async function () {
+ const testUrl = `http://example.com/dummy`;
+ const worker = new SharedWorker("worker.js");
+ const { data: result } = await new Promise(resolve => {
+ worker.port.onmessage = resolve;
+ worker.port.postMessage(["worker-fetch-test", testUrl]);
+ });
+
+ browser.test.sendMessage("test-sharedworker-fetch:done", result);
+ };
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: { permissions: ["http://example.com/*"] },
+ files: {
+ "worker.js": function () {
+ self.onconnect = evt => {
+ const port = evt.ports[0];
+ port.onmessage = async evt => {
+ let result = {};
+ let message;
+ try {
+ const [msg, url] = evt.data;
+ message = msg;
+ const response = await fetch(url);
+ dump(`fetch call resolved: ${response}\n`);
+ result.fetchResolvesTo = `${response}`;
+ } catch (err) {
+ dump(`fetch call rejected: ${err}\n`);
+ result.error = err.name;
+ throw err;
+ } finally {
+ port.postMessage([`${message}:result`, result]);
+ }
+ };
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+ const result = await extension.awaitMessage("test-sharedworker-fetch:done");
+ if (Services.io.offline && WebExtensionPolicy.useRemoteWebExtensions) {
+ // If the network is offline and the extensions are running in the
+ // child extension process, expect the fetch call to be rejected
+ // with an TypeError.
+ Assert.deepEqual(
+ ["worker-fetch-test:result", { error: "TypeError" }],
+ result,
+ "fetch should have been rejected with an TypeError"
+ );
+ } else {
+ // If the network is not offline or the extension are running in the
+ // parent process, we expect the fetch call to resolve to a Response.
+ Assert.deepEqual(
+ ["worker-fetch-test:result", { fetchResolvesTo: "[object Response]" }],
+ result,
+ "fetch should have been resolved to a Response instance"
+ );
+ }
+ await extension.unload();
+});
diff --git a/dom/workers/test/xpcshell/test_fileReader.js b/dom/workers/test/xpcshell/test_fileReader.js
new file mode 100644
index 0000000000..02bc3fa667
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_fileReader.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Worker must be loaded from a chrome:// uri, not a file://
+// uri, so we first need to load it.
+var WORKER_SOURCE_URI = "chrome://workers/content/worker_fileReader.js";
+do_load_manifest("data/chrome.manifest");
+
+function talk_with_worker(worker) {
+ return new Promise((resolve, reject) => {
+ worker.onmessage = function (event) {
+ let success = true;
+ if (event.data == "OK") {
+ resolve();
+ } else {
+ success = false;
+ reject(event);
+ }
+ Assert.ok(success);
+ worker.terminate();
+ };
+ worker.onerror = function (event) {
+ let error = new Error(event.message, event.filename, event.lineno);
+ worker.terminate();
+ reject(error);
+ };
+ worker.postMessage("START");
+ });
+}
+
+add_task(function test_chrome_worker() {
+ return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI));
+});
diff --git a/dom/workers/test/xpcshell/test_import_base_uri.js b/dom/workers/test/xpcshell/test_import_base_uri.js
new file mode 100644
index 0000000000..7c88a946f4
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_import_base_uri.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_task(async function testSyncImportBeforeAsyncImportDependencyInWorker() {
+ const worker = new ChromeWorker("resource://test/data/base_uri_worker.js");
+
+ const { promise, resolve } = Promise.withResolvers();
+ worker.onmessage = event => {
+ resolve(event.data);
+ };
+ worker.postMessage("");
+
+ const result = await promise;
+
+ Assert.ok(result.scriptToModule.equal1);
+ Assert.ok(result.scriptToModule.equal2);
+ Assert.ok(result.scriptToModule.equal3);
+
+ Assert.ok(result.moduleToModuleURI.equal1);
+ Assert.ok(result.moduleToModuleURI.equal2);
+ Assert.ok(result.moduleToModuleURI.equal3);
+
+ Assert.ok(result.moduleToModuleCurrent.equal1);
+ Assert.ok(result.moduleToModuleCurrent.equal2);
+ Assert.ok(result.moduleToModuleCurrent.equal3);
+
+ Assert.ok(result.moduleToModuleParent.equal1);
+ Assert.ok(result.moduleToModuleParent.equal2);
+ Assert.ok(result.moduleToModuleParent.equal3);
+
+ Assert.ok(result.moduleToModuleAbsolute.equal1);
+ Assert.ok(result.moduleToModuleAbsolute.equal2);
+ Assert.ok(result.moduleToModuleAbsolute.equal3);
+});
diff --git a/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js
new file mode 100644
index 0000000000..b95ad4bcaf
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js
@@ -0,0 +1,88 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { createHttpServer } = AddonTestUtils;
+
+// Force ServiceWorkerRegistrar to init by calling do_get_profile.
+// (This has to be called before AddonTestUtils.init, because it does
+// also call do_get_profile internally but it doesn't notify
+// profile-after-change).
+do_get_profile(true);
+
+AddonTestUtils.init(this);
+
+const server = createHttpServer({ hosts: ["localhost"] });
+
+server.registerPathHandler("/sw.js", (request, response) => {
+ info(`/sw.js is being requested: ${JSON.stringify(request)}`);
+ response.setHeader("Content-Type", "application/javascript");
+ response.write("");
+});
+
+add_task(async function setup_prefs() {
+ // Enable nsIServiceWorkerManager.registerForTest.
+ Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled");
+ });
+});
+
+/**
+ * This test installs a ServiceWorker via test API and verify that the install
+ * process spawns a new process. (Normally ServiceWorker installation won't
+ * cause a new content process to be spawned because the call to register must
+ * be coming from within an existing content process, but the registerForTest
+ * API allows us to bypass this restriction.)
+ *
+ * This models the real-world situation of a push notification being received
+ * from the network which results in a ServiceWorker being spawned without their
+ * necessarily being an existing content process to host it (especially under Fission).
+ */
+add_task(async function launch_remoteworkers_in_new_processes() {
+ const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+
+ const ssm = Services.scriptSecurityManager;
+
+ const initialChildCount = Services.ppmm.childCount;
+
+ // A test service worker that should spawn a regular web content child process.
+ const swRegInfoWeb = await swm.registerForTest(
+ ssm.createContentPrincipal(Services.io.newURI("http://localhost"), {}),
+ "http://localhost/scope",
+ "http://localhost/sw.js"
+ );
+ swRegInfoWeb.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo);
+
+ info(
+ `web content service worker registered: ${JSON.stringify({
+ principal: swRegInfoWeb.principal.spec,
+ scope: swRegInfoWeb.scope,
+ })}`
+ );
+
+ info("Wait new process to be launched");
+ await TestUtils.waitForCondition(() => {
+ return Services.ppmm.childCount - initialChildCount >= 1;
+ }, "wait for a new child processes to be started");
+
+ // Wait both workers to become active to be sure that. besides spawning
+ // the new child processes as expected, the two remote worker have been
+ // able to run successfully (in other word their remote worker data did
+ // pass successfull the IsRemoteTypeAllowed check in RemoteworkerChild).
+ info("Wait for webcontent worker to become active");
+ await TestUtils.waitForCondition(
+ () => swRegInfoWeb.activeWorker,
+ `wait workers for scope ${swRegInfoWeb.scope} to be active`
+ );
+});
diff --git a/dom/workers/test/xpcshell/test_workers.js b/dom/workers/test/xpcshell/test_workers.js
new file mode 100644
index 0000000000..5b768f69bb
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_workers.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Worker must be loaded from a chrome:// uri, not a file://
+// uri, so we first need to load it.
+var WORKER_SOURCE_URI = "chrome://workers/content/worker.js";
+do_load_manifest("data/chrome.manifest");
+
+function talk_with_worker(worker) {
+ return new Promise((resolve, reject) => {
+ worker.onmessage = function (event) {
+ let success = true;
+ if (event.data == "OK") {
+ resolve();
+ } else {
+ success = false;
+ reject(event);
+ }
+ Assert.ok(success);
+ worker.terminate();
+ };
+ worker.onerror = function (event) {
+ let error = new Error(event.message, event.filename, event.lineno);
+ worker.terminate();
+ reject(error);
+ };
+ worker.postMessage("START");
+ });
+}
+
+add_task(function test_chrome_worker() {
+ return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI));
+});
+
+add_task(function test_worker() {
+ return talk_with_worker(new Worker(WORKER_SOURCE_URI));
+});
diff --git a/dom/workers/test/xpcshell/test_workers_clone_error.js b/dom/workers/test/xpcshell/test_workers_clone_error.js
new file mode 100644
index 0000000000..f3fd430457
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_workers_clone_error.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Worker must be loaded from a chrome:// uri, not a file://
+// uri, so we first need to load it.
+var WORKER_SOURCE_URI = "chrome://workers/content/worker.js";
+do_load_manifest("data/chrome.manifest");
+
+function talk_with_worker(worker) {
+ return new Promise((resolve, reject) => {
+ worker.onmessage = function (event) {
+ let success = true;
+ if (event.data == "OK") {
+ resolve();
+ } else {
+ success = false;
+ reject(event);
+ }
+ Assert.ok(success);
+ worker.terminate();
+ };
+ worker.onerror = function (event) {
+ let error = new Error(event.message, event.filename, event.lineno);
+ worker.terminate();
+ reject(error);
+ };
+
+ try {
+ // eslint-disable-next-line no-eval
+ eval("/");
+ } catch (e) {
+ worker.postMessage(new ClonedErrorHolder(e));
+ }
+ });
+}
+
+add_task(function test_chrome_worker() {
+ return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI));
+});
+
+add_task(function test_worker() {
+ return talk_with_worker(new Worker(WORKER_SOURCE_URI));
+});
diff --git a/dom/workers/test/xpcshell/xpcshell.toml b/dom/workers/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..61b1eef0c2
--- /dev/null
+++ b/dom/workers/test/xpcshell/xpcshell.toml
@@ -0,0 +1,38 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+support-files = [
+ "data/worker.js",
+ "data/worker_fileReader.js",
+ "data/chrome.manifest",
+ "data/base_uri_worker.js",
+ "data/base_uri_module.mjs",
+ "data/base_uri_module2.mjs",
+]
+
+["test_ext_redirects_sw_scripts.js"]
+# The following firefox-appdir make sure that ExtensionTestUtils.loadExtension
+# will be able to successfully start the background page (it does fail without
+# it because there wouldn't be a global.tabTracker implementation as we would
+# expect in a real Firefox, Fenix or Thunderbird instance).
+firefox-appdir = "browser"
+
+["test_ext_worker_offline_fetch.js"]
+firefox-appdir = "browser"
+
+["test_fileReader.js"]
+
+["test_import_base_uri.js"]
+
+["test_remoteworker_launch_new_process.js"]
+# The following firefox-appdir make sure that this xpcshell test will run
+# with e10s enabled (which is needed to make sure that the test case is
+# going to launch the expected new processes)
+firefox-appdir = "browser"
+# Disable plugin loading to make it rr able to record and replay this test.
+prefs = ["plugin.disable=true"]
+skip-if = ["socketprocess_networking"] # Bug 1759035
+
+
+["test_workers.js"]
+
+["test_workers_clone_error.js"]