summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/browser
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/browser')
-rw-r--r--toolkit/components/extensions/test/browser/.eslintrc.js11
-rw-r--r--toolkit/components/extensions/test/browser/browser-serviceworker.ini9
-rw-r--r--toolkit/components/extensions/test/browser/browser.ini56
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js285
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js126
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js138
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js91
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js83
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js226
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_management_themes.js177
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_process_crash_handling.js116
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_test_mock.js47
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js119
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js39
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js82
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js173
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js213
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js203
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js154
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js217
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js450
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js227
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js151
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js63
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js77
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js69
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js287
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js203
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js240
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js439
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js64
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_reset.js112
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js187
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_separators.js76
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js275
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js126
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js39
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js51
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js54
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js70
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js48
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js183
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js107
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js63
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js109
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js105
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js144
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js94
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js72
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js48
-rw-r--r--toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js133
-rw-r--r--toolkit/components/extensions/test/browser/data/test-download.txt1
-rw-r--r--toolkit/components/extensions/test/browser/data/test_downloads_referrer.html10
-rw-r--r--toolkit/components/extensions/test/browser/head.js126
-rw-r--r--toolkit/components/extensions/test/browser/head_serviceworker.js119
55 files changed, 7187 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/browser/.eslintrc.js b/toolkit/components/extensions/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..ef228570e3
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/.eslintrc.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+
+ rules: {
+ "no-shadow": "off",
+ },
+};
diff --git a/toolkit/components/extensions/test/browser/browser-serviceworker.ini b/toolkit/components/extensions/test/browser/browser-serviceworker.ini
new file mode 100644
index 0000000000..58e9082f7b
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser-serviceworker.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ head_serviceworker.js
+ data/**
+
+prefs =
+ extensions.backgroundServiceWorker.enabled=true
+
+[browser_ext_background_serviceworker.js]
diff --git a/toolkit/components/extensions/test/browser/browser.ini b/toolkit/components/extensions/test/browser/browser.ini
new file mode 100644
index 0000000000..a26e73ee37
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -0,0 +1,56 @@
+[DEFAULT]
+support-files =
+ head.js
+ data/**
+
+[browser_ext_background_serviceworker_pref_disabled.js]
+[browser_ext_downloads_filters.js]
+[browser_ext_downloads_referrer.js]
+https_first_disabled = true
+[browser_ext_eventpage_disableResetIdleForTest.js]
+[browser_ext_extension_page_tab_navigated.js]
+[browser_ext_management_themes.js]
+skip-if = verify
+[browser_ext_process_crash_handling.js]
+skip-if = !crashreporter
+[browser_ext_test_mock.js]
+[browser_ext_themes_additional_backgrounds_alignment.js]
+[browser_ext_themes_alpha_accentcolor.js]
+[browser_ext_themes_arrowpanels.js]
+[browser_ext_themes_autocomplete_popup.js]
+[browser_ext_themes_chromeparity.js]
+[browser_ext_themes_dynamic_getCurrent.js]
+[browser_ext_themes_dynamic_onUpdated.js]
+[browser_ext_themes_dynamic_updates.js]
+[browser_ext_themes_experiment.js]
+[browser_ext_themes_findbar.js]
+[browser_ext_themes_getCurrent_differentExt.js]
+[browser_ext_themes_highlight.js]
+[browser_ext_themes_incognito.js]
+[browser_ext_themes_lwtsupport.js]
+[browser_ext_themes_multiple_backgrounds.js]
+[browser_ext_themes_ntp_colors.js]
+[browser_ext_themes_ntp_colors_perwindow.js]
+[browser_ext_themes_pbm.js]
+[browser_ext_themes_persistence.js]
+[browser_ext_themes_reset.js]
+[browser_ext_themes_sanitization.js]
+[browser_ext_themes_separators.js]
+[browser_ext_themes_sidebars.js]
+[browser_ext_themes_static_onUpdated.js]
+[browser_ext_themes_tab_line.js]
+[browser_ext_themes_tab_loading.js]
+[browser_ext_themes_tab_selected.js]
+[browser_ext_themes_tab_text.js]
+[browser_ext_themes_theme_transition.js]
+[browser_ext_themes_toolbar_fields.js]
+[browser_ext_themes_toolbar_fields_focus.js]
+[browser_ext_themes_toolbarbutton_colors.js]
+[browser_ext_themes_toolbarbutton_icons.js]
+[browser_ext_themes_toolbars.js]
+[browser_ext_themes_warnings.js]
+[browser_ext_thumbnails_bg_extension.js]
+support-files = !/toolkit/components/thumbnails/test/head.js
+[browser_ext_webNavigation_eventpage.js]
+[browser_ext_webRequest_redirect_mozextension.js]
+[browser_ext_windows_popup_title.js]
diff --git a/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js
new file mode 100644
index 0000000000..153818f4de
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js
@@ -0,0 +1,285 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals getBackgroundServiceWorkerRegistration, waitForServiceWorkerTerminated */
+
+Services.scriptloader.loadSubScript(
+ new URL("head_serviceworker.js", gTestPath).href,
+ this
+);
+
+add_task(assert_background_serviceworker_pref_enabled);
+
+add_task(async function test_serviceWorker_register_guarded_by_pref() {
+ // Test with backgroundServiceWorkeEnable set to true and the
+ // extensions.serviceWorkerRegist.allowed pref set to false.
+ // NOTE: the scenario with backgroundServiceWorkeEnable set to false
+ // is part of "browser_ext_background_serviceworker_pref_disabled.js".
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.serviceWorkerRegister.allowed", false]],
+ });
+
+ let extensionData = {
+ files: {
+ "page.html": "<!DOCTYPE html><script src='page.js'></script>",
+ "page.js": async function () {
+ browser.test.assertEq(
+ undefined,
+ navigator.serviceWorker,
+ "navigator.serviceWorker should be undefined"
+ );
+ browser.test.sendMessage("test-serviceWorker-register-disallowed");
+ },
+ "sw.js": "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ // Verify that an extension page can't register a moz-extension url
+ // as a service worker.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `moz-extension://${extension.uuid}/page.html`,
+ },
+ async () => {
+ await extension.awaitMessage("test-serviceWorker-register-disallowed");
+ }
+ );
+
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+
+ // Test again with the pref set to true.
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.serviceWorkerRegister.allowed", true]],
+ });
+
+ extension = ExtensionTestUtils.loadExtension({
+ files: {
+ ...extensionData.files,
+ "page.js": async function () {
+ try {
+ await navigator.serviceWorker.register("sw.js");
+ } catch (err) {
+ browser.test.fail(
+ `Unexpected error on registering a service worker: ${err}`
+ );
+ throw err;
+ } finally {
+ browser.test.sendMessage("test-serviceworker-register-allowed");
+ }
+ },
+ },
+ });
+ await extension.startup();
+
+ // Verify that an extension page can register a moz-extension url
+ // as a service worker if enabled by the related pref.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `moz-extension://${extension.uuid}/page.html`,
+ },
+ async () => {
+ await extension.awaitMessage("test-serviceworker-register-allowed");
+ }
+ );
+
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_cache_api_allowed() {
+ // Verify that Cache API support for moz-extension url availability is
+ // conditioned only by the extensions.backgroundServiceWorker.enabled pref.
+ // NOTE: the scenario with backgroundServiceWorkeEnable set to false
+ // is part of "browser_ext_background_serviceworker_pref_disabled.js".
+ const extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ let cache = await window.caches.open("test-cache-api");
+ browser.test.assertTrue(
+ await window.caches.has("test-cache-api"),
+ "CacheStorage.has should resolve to true"
+ );
+
+ // Test that adding and requesting cached moz-extension urls
+ // works as well.
+ let url = browser.runtime.getURL("file.txt");
+ await cache.add(url);
+ const content = await cache.match(url).then(res => res.text());
+ browser.test.assertEq(
+ "file content",
+ content,
+ "Got the expected content from the cached moz-extension url"
+ );
+
+ // Test that deleting the cache storage works as expected.
+ browser.test.assertTrue(
+ await window.caches.delete("test-cache-api"),
+ "Cache deleted successfully"
+ );
+ browser.test.assertTrue(
+ !(await window.caches.has("test-cache-api")),
+ "CacheStorage.has should resolve to false"
+ );
+ } catch (err) {
+ browser.test.fail(`Unexpected error on using Cache API: ${err}`);
+ throw err;
+ } finally {
+ browser.test.sendMessage("test-cache-api-allowed");
+ }
+ },
+ files: {
+ "file.txt": "file content",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("test-cache-api-allowed");
+ await extension.unload();
+});
+
+function createTestSWScript({ postMessageReply }) {
+ return `
+ self.onmessage = msg => {
+ dump("Background ServiceWorker - onmessage handler\\n");
+ msg.ports[0].postMessage("${postMessageReply}");
+ dump("Background ServiceWorker - postMessage\\n");
+ };
+ dump("Background ServiceWorker - executed\\n");
+ `;
+}
+
+async function testServiceWorker({ extension, expectMessageReply }) {
+ // Verify that the WebExtensions framework has successfully registered the
+ // background service worker declared in the extension manifest.
+ const swRegInfo = getBackgroundServiceWorkerRegistration(extension);
+
+ // Activate the background service worker by exchanging a message
+ // with it.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `moz-extension://${extension.uuid}/page.html`,
+ },
+ async browser => {
+ let msgFromV1 = await SpecialPowers.spawn(
+ browser,
+ [swRegInfo.scriptURL],
+ async url => {
+ const { active } = await content.navigator.serviceWorker.ready;
+ const { port1, port2 } = new content.MessageChannel();
+
+ return new Promise(resolve => {
+ port1.onmessage = msg => resolve(msg.data);
+ active.postMessage("test", [port2]);
+ });
+ }
+ );
+
+ Assert.deepEqual(
+ msgFromV1,
+ expectMessageReply,
+ "Got the expected reply from the extension service worker"
+ );
+ }
+ );
+}
+
+function loadTestExtension({ version }) {
+ const postMessageReply = `reply:sw-v${version}`;
+
+ return ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ version,
+ background: {
+ service_worker: "sw.js",
+ },
+ browser_specific_settings: { gecko: { id: "test-bg-sw@mochi.test" } },
+ },
+ files: {
+ "page.html": "<!DOCTYPE html><body></body>",
+ "sw.js": createTestSWScript({ postMessageReply }),
+ },
+ });
+}
+
+async function assertWorkerIsRunningInExtensionProcess(extension) {
+ // Activate the background service worker by exchanging a message
+ // with it.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `moz-extension://${extension.uuid}/page.html`,
+ },
+ async browser => {
+ const workerScriptURL = `moz-extension://${extension.uuid}/sw.js`;
+ const workerDebuggerURLs = await SpecialPowers.spawn(
+ browser,
+ [workerScriptURL],
+ async url => {
+ await content.navigator.serviceWorker.ready;
+ const wdm = Cc[
+ "@mozilla.org/dom/workers/workerdebuggermanager;1"
+ ].getService(Ci.nsIWorkerDebuggerManager);
+
+ return Array.from(wdm.getWorkerDebuggerEnumerator())
+ .map(wd => {
+ return wd.url;
+ })
+ .filter(swURL => swURL == url);
+ }
+ );
+
+ Assert.deepEqual(
+ workerDebuggerURLs,
+ [workerScriptURL],
+ "The worker should be running in the extension child process"
+ );
+ }
+ );
+}
+
+add_task(async function test_background_serviceworker_with_no_ext_apis() {
+ const extensionV1 = loadTestExtension({ version: "1" });
+ await extensionV1.startup();
+
+ const swRegInfo = getBackgroundServiceWorkerRegistration(extensionV1);
+ const { uuid } = extensionV1;
+
+ await assertWorkerIsRunningInExtensionProcess(extensionV1);
+ await testServiceWorker({
+ extension: extensionV1,
+ expectMessageReply: "reply:sw-v1",
+ });
+
+ // Load a new version of the same addon and verify that the
+ // expected worker script is being executed.
+ const extensionV2 = loadTestExtension({ version: "2" });
+ await extensionV2.startup();
+ is(extensionV2.uuid, uuid, "The extension uuid did not change as expected");
+
+ await testServiceWorker({
+ extension: extensionV2,
+ expectMessageReply: "reply:sw-v2",
+ });
+
+ await Promise.all([
+ extensionV2.unload(),
+ // test extension v1 wrapper has to be unloaded explicitly, otherwise
+ // will be detected as a failure by the test harness.
+ extensionV1.unload(),
+ ]);
+ await waitForServiceWorkerTerminated(swRegInfo);
+ await waitForServiceWorkerRegistrationsRemoved(extensionV2);
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js
new file mode 100644
index 0000000000..0194cd237f
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js
@@ -0,0 +1,126 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function assert_background_serviceworker_pref_disabled() {
+ is(
+ WebExtensionPolicy.backgroundServiceWorkerEnabled,
+ false,
+ "Expect extensions.backgroundServiceWorker.enabled to be false"
+ );
+});
+
+add_task(async function test_background_serviceworker_disallowed() {
+ const id = "test-disallowed-worker@test";
+
+ const extensionData = {
+ manifest: {
+ background: {
+ service_worker: "sw.js",
+ },
+ applicantions: { gecko: { id } },
+ useAddonManager: "temporary",
+ },
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Reading manifest: Error processing background: background.service_worker is currently disabled/,
+ },
+ ]);
+ });
+
+ const extension = ExtensionTestUtils.loadExtension(extensionData);
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "Startup failed with background.service_worker while disabled by pref"
+ );
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
+
+add_task(async function test_serviceWorker_register_disallowed() {
+ // Verify that setting extensions.serviceWorkerRegist.allowed pref to false
+ // doesn't allow serviceWorker.register if backgroundServiceWorkeEnable is
+ // set to false
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.serviceWorkerRegister.allowed", true]],
+ });
+
+ let extensionData = {
+ files: {
+ "page.html": "<!DOCTYPE html><script src='page.js'></script>",
+ "page.js": async function () {
+ try {
+ await navigator.serviceWorker.register("sw.js");
+ browser.test.fail(
+ `An extension page should not be able to register a serviceworker successfully`
+ );
+ } catch (err) {
+ browser.test.assertEq(
+ String(err),
+ "SecurityError: The operation is insecure.",
+ "Got the expected error on registering a service worker from a script"
+ );
+ }
+ browser.test.sendMessage("test-serviceWorker-register-disallowed");
+ },
+ "sw.js": "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ // Verify that an extension page can't register a moz-extension url
+ // as a service worker.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `moz-extension://${extension.uuid}/page.html`,
+ },
+ async () => {
+ await extension.awaitMessage("test-serviceWorker-register-disallowed");
+ }
+ );
+
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_cache_api_disallowed() {
+ // Verify that Cache API support for moz-extension url availability is also
+ // conditioned by the extensions.backgroundServiceWorker.enabled pref.
+ const extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ const cache = await window.caches.open("test-cache-api");
+ let url = browser.runtime.getURL("file.txt");
+ await browser.test.assertRejects(
+ cache.add(url),
+ new RegExp(`Cache.add: Request URL ${url} must be either`),
+ "Got the expected rejections on calling cache.add with a moz-extension:// url"
+ );
+ } catch (err) {
+ browser.test.fail(`Unexpected error on using Cache API: ${err}`);
+ throw err;
+ } finally {
+ browser.test.sendMessage("test-cache-api-disallowed");
+ }
+ },
+ files: {
+ "file.txt": "file content",
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("test-cache-api-disallowed");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js b/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js
new file mode 100644
index 0000000000..18d68d2061
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+async function testAppliedFilters(ext, expectedFilter, expectedFilterCount) {
+ let tempDir = FileUtils.getDir(
+ "TmpD",
+ [`testDownloadDir-${Math.random()}`],
+ true
+ );
+
+ let filterCount = 0;
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ MockFilePicker.displayDirectory = tempDir;
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ MockFilePicker.appendFiltersCallback = function (fp, val) {
+ const hexstr = "0x" + ("000" + val.toString(16)).substr(-3);
+ filterCount++;
+ if (filterCount < expectedFilterCount) {
+ is(val, expectedFilter, "Got expected filter: " + hexstr);
+ } else if (filterCount == expectedFilterCount) {
+ is(val, MockFilePicker.filterAll, "Got all files filter: " + hexstr);
+ } else {
+ is(val, null, "Got unexpected filter: " + hexstr);
+ }
+ };
+ MockFilePicker.showCallback = function (fp) {
+ const filename = fp.defaultString;
+ info("MockFilePicker - save as: " + filename);
+ };
+
+ let manifest = {
+ description: ext,
+ permissions: ["downloads"],
+ };
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+
+ background: async function () {
+ let ext = chrome.runtime.getManifest().description;
+ await browser.test.assertRejects(
+ browser.downloads.download({
+ url: "http://any-origin/any-path/any-resource",
+ filename: "any-file" + ext,
+ saveAs: true,
+ }),
+ "Download canceled by the user",
+ "expected request to be canceled"
+ );
+ browser.test.sendMessage("canceled");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("canceled");
+ await extension.unload();
+
+ is(
+ filterCount,
+ expectedFilterCount,
+ "Got correct number of filters: " + filterCount
+ );
+
+ MockFilePicker.cleanup();
+
+ tempDir.remove(true);
+}
+
+// Missing extension
+add_task(async function testDownload_missing_All() {
+ await testAppliedFilters("", null, 1);
+});
+
+// Unrecognized extension
+add_task(async function testDownload_unrecognized_All() {
+ await testAppliedFilters(".xxx", null, 1);
+});
+
+// Recognized extensions
+add_task(async function testDownload_html_HTML() {
+ await testAppliedFilters(".html", Ci.nsIFilePicker.filterHTML, 2);
+});
+
+add_task(async function testDownload_xhtml_HTML() {
+ await testAppliedFilters(".xhtml", Ci.nsIFilePicker.filterHTML, 2);
+});
+
+add_task(async function testDownload_txt_Text() {
+ await testAppliedFilters(".txt", Ci.nsIFilePicker.filterText, 2);
+});
+
+add_task(async function testDownload_text_Text() {
+ await testAppliedFilters(".text", Ci.nsIFilePicker.filterText, 2);
+});
+
+add_task(async function testDownload_jpe_Images() {
+ await testAppliedFilters(".jpe", Ci.nsIFilePicker.filterImages, 2);
+});
+
+add_task(async function testDownload_tif_Images() {
+ await testAppliedFilters(".tif", Ci.nsIFilePicker.filterImages, 2);
+});
+
+add_task(async function testDownload_webp_Images() {
+ await testAppliedFilters(".webp", Ci.nsIFilePicker.filterImages, 2);
+});
+
+add_task(async function testDownload_xml_XML() {
+ await testAppliedFilters(".xml", Ci.nsIFilePicker.filterXML, 2);
+});
+
+add_task(async function testDownload_aac_Audio() {
+ await testAppliedFilters(".aac", Ci.nsIFilePicker.filterAudio, 2);
+});
+
+add_task(async function testDownload_mp3_Audio() {
+ await testAppliedFilters(".mp3", Ci.nsIFilePicker.filterAudio, 2);
+});
+
+add_task(async function testDownload_wma_Audio() {
+ await testAppliedFilters(".wma", Ci.nsIFilePicker.filterAudio, 2);
+});
+
+add_task(async function testDownload_avi_Video() {
+ await testAppliedFilters(".avi", Ci.nsIFilePicker.filterVideo, 2);
+});
+
+add_task(async function testDownload_mp4_Video() {
+ await testAppliedFilters(".mp4", Ci.nsIFilePicker.filterVideo, 2);
+});
+
+add_task(async function testDownload_xvid_Video() {
+ await testAppliedFilters(".xvid", Ci.nsIFilePicker.filterVideo, 2);
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js b/toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js
new file mode 100644
index 0000000000..9690df6376
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js
@@ -0,0 +1,91 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const URL_PATH = "browser/toolkit/components/extensions/test/browser/data";
+const TEST_URL = `http://example.com/${URL_PATH}/test_downloads_referrer.html`;
+const DOWNLOAD_URL = `http://example.com/${URL_PATH}/test-download.txt`;
+
+async function triggerSaveAs({ selector }) {
+ const contextMenu = window.document.getElementById("contentAreaContextMenu");
+ const popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupshown;
+ let saveLinkCommand = window.document.getElementById("context-savelink");
+ contextMenu.activateItem(saveLinkCommand);
+}
+
+add_setup(() => {
+ const tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append("test-download-dir");
+ if (!tempDir.exists()) {
+ tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+ registerCleanupFunction(function () {
+ MockFilePicker.cleanup();
+
+ if (tempDir.exists()) {
+ tempDir.remove(true);
+ }
+ });
+
+ MockFilePicker.displayDirectory = tempDir;
+ MockFilePicker.showCallback = function (fp) {
+ info("MockFilePicker: shown");
+ const filename = fp.defaultString;
+ info("MockFilePicker: save as " + filename);
+ const destFile = tempDir.clone();
+ destFile.append(filename);
+ MockFilePicker.setFiles([destFile]);
+ info("MockFilePicker: showCallback done");
+ };
+});
+
+add_task(async function test_download_item_referrer_info() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["downloads"],
+ },
+ async background() {
+ browser.downloads.onCreated.addListener(async downloadInfo => {
+ browser.test.sendMessage("download-on-created", downloadInfo);
+ });
+ browser.downloads.onChanged.addListener(async downloadInfo => {
+ // Wait download to be completed.
+ if (downloadInfo.state?.current !== "complete") {
+ return;
+ }
+ browser.test.sendMessage("download-completed");
+ });
+
+ // Call an API method implemented in the parent process to make sure
+ // registering the downloas.onCreated event listener has been completed.
+ await browser.runtime.getBrowserInfo();
+
+ browser.test.sendMessage("bg-page:ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bg-page:ready");
+
+ await BrowserTestUtils.withNewTab({ gBrowser, url: TEST_URL }, async () => {
+ await triggerSaveAs({ selector: "a.test-link" });
+ const downloadInfo = await extension.awaitMessage("download-on-created");
+ is(downloadInfo.url, DOWNLOAD_URL, "Got the expected download url");
+ is(downloadInfo.referrer, TEST_URL, "Got the expected referrer");
+ });
+
+ // Wait for the download to have been completed and removed.
+ await extension.awaitMessage("download-completed");
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js b/toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js
new file mode 100644
index 0000000000..8178411e80
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js
@@ -0,0 +1,83 @@
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+const { AppUiTestDelegate } = ChromeUtils.importESModule(
+ "resource://testing-common/AppUiTestDelegate.sys.mjs"
+);
+
+// Ignore error "Actor 'Conduits' destroyed before query 'RunListener' was resolved"
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Actor 'Conduits' destroyed before query 'RunListener'/
+);
+
+async function run_test_disableResetIdleForTest(options) {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ background() {
+ browser.action.onClicked.addListener(async () => {
+ browser.test.notifyPass("action-clicked");
+ // Deliberately keep this listener active to simulate a still active listener
+ // callback, while calling extension.terminateBackground().
+ await new Promise(() => {});
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+ // After startup, the listener should be persistent but not primed.
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: false,
+ });
+
+ // Terminating the background should prime the persistent listener.
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: true,
+ });
+
+ // Wake up the background, and verify the listener is no longer primed.
+ await AppUiTestDelegate.clickBrowserAction(window, extension.id);
+ await extension.awaitFinish("action-clicked");
+ await AppUiTestDelegate.closeBrowserAction(window, extension.id);
+ await extension.awaitMessage("background-ready");
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: false,
+ });
+
+ // Terminate the background again, while the onClicked listener is still
+ // being executed.
+ // With options.disableResetIdleForTest = true, the termination should NOT
+ // be skipped and the listener should become primed again.
+ // With options.disableResetIdleForTest = false or unset, the termination
+ // should be skipped and the listener should not become primed.
+ await extension.terminateBackground(options);
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: !!options?.disableResetIdleForTest,
+ });
+
+ await extension.unload();
+}
+
+// Verify default behaviour when terminating a background while a
+// listener is still running: The background should not be terminated
+// and the listener should not become primed. Not specifyiny a value
+// for disableResetIdleForTest defauls to disableResetIdleForTest:false.
+add_task(async function test_disableResetIdleForTest_default() {
+ await run_test_disableResetIdleForTest();
+});
+
+// Verify that disableResetIdleForTest:true is honoured and terminating
+// a background while a listener is still running is enforced: The
+// background should be terminated and the listener should become primed.
+add_task(async function test_disableResetIdleForTest_true() {
+ await run_test_disableResetIdleForTest({ disableResetIdleForTest: true });
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js b/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js
new file mode 100644
index 0000000000..b2fb9484c2
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+// The test tasks in this test file tends to trigger an intermittent
+// exception raised from JSActor::AfterDestroy, because of a race between
+// when the WebExtensions API event is being emitted from the parent process
+// and the navigation triggered on the test extension pages.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Actor 'Conduits' destroyed before query 'RunListener' was resolved/
+);
+
+AddonTestUtils.initMochitest(this);
+
+const server = AddonTestUtils.createHttpServer({
+ hosts: ["example.com", "anotherwebpage.org"],
+});
+
+server.registerPathHandler("/", (request, response) => {
+ response.write(`<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <title>test webpage</title>
+ </head>
+ </html>
+ `);
+});
+
+function createTestExtPage({ script }) {
+ return `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="${script}"></script>
+ </head>
+ </html>
+ `;
+}
+
+function createTestExtPageScript(name) {
+ return `(${function (pageName) {
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.log(
+ `Extension page "${pageName}" got a webRequest event: ${details.url}`
+ );
+ browser.test.sendMessage(`event-received:${pageName}`);
+ },
+ { types: ["main_frame"], urls: ["http://example.com/*"] }
+ );
+ /* eslint-disable mozilla/balanced-listeners */
+ window.addEventListener("pageshow", () => {
+ browser.test.log(`Extension page "${pageName}" got a pageshow event`);
+ browser.test.sendMessage(`pageshow:${pageName}`);
+ });
+ window.addEventListener("pagehide", () => {
+ browser.test.log(`Extension page "${pageName}" got a pagehide event`);
+ browser.test.sendMessage(`pagehide:${pageName}`);
+ });
+ /* eslint-enable mozilla/balanced-listeners */
+ }})("${name}");`;
+}
+
+// Triggers a WebRequest listener registered by the test extensions by
+// opening a tab on the given web page URL and then closing it after
+// it did load.
+async function triggerWebRequestListener(webPageURL, pause) {
+ let webPageTab = await BrowserTestUtils.openNewForegroundTab(
+ {
+ gBrowser,
+ url: webPageURL,
+ },
+ true /* waitForLoad */,
+ true /* waitForStop */
+ );
+ BrowserTestUtils.removeTab(webPageTab);
+}
+
+// The following tests tasks are testing the expected behaviors related to same-process and cross-process
+// navigations for an extension page, similarly to test_ext_extension_page_navigated.js, but unlike its
+// xpcshell counterpart this tests are only testing that after navigating back to an extension page
+// previously stored in the BFCache the WebExtensions events subscribed are being received as expected.
+
+add_task(async function test_extension_page_sameprocess_navigation() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "http://example.com/*"],
+ },
+ files: {
+ "extpage1.html": createTestExtPage({ script: "extpage1.js" }),
+ "extpage1.js": createTestExtPageScript("extpage1"),
+ "extpage2.html": createTestExtPage({ script: "extpage2.js" }),
+ "extpage2.js": createTestExtPageScript("extpage2"),
+ },
+ });
+
+ await extension.startup();
+
+ const policy = WebExtensionPolicy.getByID(extension.id);
+
+ const extPageURL1 = policy.extension.baseURI.resolve("extpage1.html");
+ const extPageURL2 = policy.extension.baseURI.resolve("extpage2.html");
+
+ info("Opening extension page in a new tab");
+ const extPageTab = await BrowserTestUtils.addTab(gBrowser, extPageURL1);
+ let browser = gBrowser.getBrowserForTab(extPageTab);
+ info("Wait for the extension page to be loaded");
+ await extension.awaitMessage("pageshow:extpage1");
+
+ await triggerWebRequestListener("http://example.com");
+ await extension.awaitMessage("event-received:extpage1");
+ ok(true, "extpage1 got a webRequest event as expected");
+
+ info("Load a second extension page in the same tab");
+ BrowserTestUtils.loadURIString(browser, extPageURL2);
+
+ info("Wait extpage1 to receive a pagehide event");
+ await extension.awaitMessage("pagehide:extpage1");
+ info("Wait extpage2 to receive a pageshow event");
+ await extension.awaitMessage("pageshow:extpage2");
+
+ info(
+ "Trigger a web request event and expect extpage2 to be the only one receiving it"
+ );
+ await triggerWebRequestListener("http://example.com");
+ await extension.awaitMessage("event-received:extpage2");
+ ok(true, "extpage2 got a webRequest event as expected");
+
+ info(
+ "Navigating back to extpage1 and expect extpage2 to be the only one receiving the webRequest event"
+ );
+
+ browser.goBack();
+ info("Wait for extpage1 to receive a pageshow event");
+ await extension.awaitMessage("pageshow:extpage1");
+ info("Wait for extpage2 to receive a pagehide event");
+ await extension.awaitMessage("pagehide:extpage2");
+
+ // We only expect extpage1 to be able to receive API events.
+ await triggerWebRequestListener("http://example.com");
+ await extension.awaitMessage("event-received:extpage1");
+ ok(true, "extpage1 got a webRequest event as expected");
+
+ BrowserTestUtils.removeTab(extPageTab);
+ await extension.awaitMessage("pagehide:extpage1");
+
+ await extension.unload();
+});
+
+add_task(async function test_extension_page_context_navigated_to_web_page() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "http://example.com/*"],
+ },
+ files: {
+ "extpage.html": createTestExtPage({ script: "extpage.js" }),
+ "extpage.js": createTestExtPageScript("extpage"),
+ },
+ });
+
+ await extension.startup();
+
+ const policy = WebExtensionPolicy.getByID(extension.id);
+
+ const extPageURL = policy.extension.baseURI.resolve("extpage.html");
+ // NOTE: this test will navigate the extension page to a webpage url that
+ // isn't matching the match pattern the test extension is going to use
+ // in its webRequest event listener, otherwise the extension page being
+ // navigated will be intermittently able to receive an event before it
+ // is navigated to the webpage url (and moved into the BFCache or destroyed)
+ // and trigger an intermittent failure of this test.
+ const webPageURL = "http://anotherwebpage.org/";
+ const triggerWebRequestURL = "http://example.com/";
+
+ info("Opening extension page in a new tab");
+ const extPageTab1 = await BrowserTestUtils.addTab(gBrowser, extPageURL);
+ let browserForTab1 = gBrowser.getBrowserForTab(extPageTab1);
+ info("Wait for the extension page to be loaded");
+ await extension.awaitMessage("pageshow:extpage");
+
+ info("Navigate the tab from the extension page to a web page");
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ browserForTab1,
+ false,
+ webPageURL
+ );
+ BrowserTestUtils.loadURIString(browserForTab1, webPageURL);
+ info("Wait the tab to have loaded the new webpage url");
+ await promiseLoaded;
+ info("Wait the extension page to receive a pagehide event");
+ await extension.awaitMessage("pagehide:extpage");
+
+ // Trigger a webRequest listener, the extension page is expected to
+ // not be active, if that isn't the case a test message will be queued
+ // and will trigger an explicit test failure.
+ await triggerWebRequestListener(triggerWebRequestURL);
+
+ info("Navigate back to the extension page");
+ browserForTab1.goBack();
+ info("Wait for extension page to receive a pageshow event");
+ await extension.awaitMessage("pageshow:extpage");
+
+ await triggerWebRequestListener(triggerWebRequestURL);
+ await extension.awaitMessage("event-received:extpage");
+ ok(
+ true,
+ "extpage got a webRequest event as expected after being restored from BFCache"
+ );
+
+ info("Cleanup and exit test");
+ BrowserTestUtils.removeTab(extPageTab1);
+
+ info("Wait the extension page to receive a pagehide event");
+ await extension.awaitMessage("pagehide:extpage");
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_management_themes.js b/toolkit/components/extensions/test/browser/browser_ext_management_themes.js
new file mode 100644
index 0000000000..d3cfa536b8
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_management_themes.js
@@ -0,0 +1,177 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+
+add_task(async function test_management_themes() {
+ await BuiltInThemes.ensureBuiltInThemes();
+
+ const TEST_ID = "test_management_themes@tests.mozilla.com";
+
+ let theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Simple theme test",
+ version: "1.0",
+ description: "test theme",
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ useAddonManager: "temporary",
+ });
+
+ async function background(TEST_ID) {
+ browser.management.onInstalled.addListener(info => {
+ if (info.name == TEST_ID) {
+ return;
+ }
+ browser.test.log(`${info.name} was installed`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onInstalled", info.name);
+ });
+ browser.management.onDisabled.addListener(info => {
+ browser.test.log(`${info.name} was disabled`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onDisabled", info.name);
+ });
+ browser.management.onEnabled.addListener(info => {
+ browser.test.log(`${info.name} was enabled`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onEnabled", info.name);
+ });
+ browser.management.onUninstalled.addListener(info => {
+ browser.test.log(`${info.name} was uninstalled`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onUninstalled", info.name);
+ });
+
+ async function getAddon(type) {
+ let addons = await browser.management.getAll();
+ let themes = addons.filter(addon => addon.type === "theme");
+ const STANDARD_BUILTIN_THEME_IDS = [
+ "default-theme@mozilla.org",
+ "firefox-compact-light@mozilla.org",
+ "firefox-compact-dark@mozilla.org",
+ "firefox-alpenglow@mozilla.org",
+ ];
+ // Check that management.getAll returns the built-in themes and our test
+ // extension.
+ for (let id of [...STANDARD_BUILTIN_THEME_IDS, TEST_ID]) {
+ let builtInExtension = addons.find(addon => {
+ return addon.id === id;
+ });
+ browser.test.assertTrue(
+ !!builtInExtension,
+ `The extension with id ${id} was returned by getAll.`
+ );
+ }
+ let found;
+ for (let addon of themes) {
+ browser.test.assertEq(addon.type, "theme", "addon is theme");
+ if (type == "theme" && addon.id.includes("temporary-addon")) {
+ found = addon;
+ } else if (type == "enabled" && addon.enabled) {
+ found = addon;
+ }
+ }
+ return found;
+ }
+
+ browser.test.onMessage.addListener(async msg => {
+ let theme = await getAddon("theme");
+ browser.test.assertEq(
+ theme.description,
+ "test theme",
+ "description is correct"
+ );
+ browser.test.assertTrue(theme.enabled, "theme is enabled");
+ await browser.management.setEnabled(theme.id, false);
+
+ theme = await getAddon("theme");
+
+ browser.test.assertTrue(!theme.enabled, "theme is disabled");
+ let addon = getAddon("enabled");
+ browser.test.assertTrue(addon, "another theme was enabled");
+
+ await browser.management.setEnabled(theme.id, true);
+ theme = await getAddon("theme");
+ addon = await getAddon("enabled");
+ browser.test.assertEq(theme.id, addon.id, "theme is enabled");
+
+ browser.test.sendMessage("done");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: TEST_ID,
+ },
+ },
+ name: TEST_ID,
+ permissions: ["management"],
+ },
+ background: `(${background})("${TEST_ID}")`,
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ await theme.startup();
+ is(
+ await extension.awaitMessage("onInstalled"),
+ "Simple theme test",
+ "webextension theme installed"
+ );
+ is(
+ await extension.awaitMessage("onDisabled"),
+ "System theme — auto",
+ "default disabled"
+ );
+
+ extension.sendMessage("test");
+ is(
+ await extension.awaitMessage("onEnabled"),
+ "System theme — auto",
+ "default enabled"
+ );
+ is(
+ await extension.awaitMessage("onDisabled"),
+ "Simple theme test",
+ "addon disabled"
+ );
+ is(
+ await extension.awaitMessage("onEnabled"),
+ "Simple theme test",
+ "addon enabled"
+ );
+ is(
+ await extension.awaitMessage("onDisabled"),
+ "System theme — auto",
+ "default disabled"
+ );
+ await extension.awaitMessage("done");
+
+ await Promise.all([theme.unload(), extension.awaitMessage("onUninstalled")]);
+
+ is(
+ await extension.awaitMessage("onEnabled"),
+ "System theme — auto",
+ "default enabled"
+ );
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_process_crash_handling.js b/toolkit/components/extensions/test/browser/browser_ext_process_crash_handling.js
new file mode 100644
index 0000000000..09ded0bcc1
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_process_crash_handling.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { ExtensionProcessCrashObserver, Management } =
+ ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+AddonTestUtils.initMochitest(this);
+
+add_task(async function test_ExtensionProcessCrashObserver() {
+ let mv2Extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ manifest_version: 2,
+ },
+ background() {
+ browser.test.sendMessage("background_running");
+ },
+ });
+
+ await mv2Extension.startup();
+ await mv2Extension.awaitMessage("background_running");
+
+ let { currentProcessChildID, lastCrashedProcessChildID } =
+ ExtensionProcessCrashObserver;
+
+ Assert.notEqual(
+ currentProcessChildID,
+ undefined,
+ "Expect ExtensionProcessCrashObserver.currentProcessChildID to be set"
+ );
+
+ Assert.equal(
+ ChromeUtils.getAllDOMProcesses().find(
+ pp => pp.childID == currentProcessChildID
+ )?.remoteType,
+ "extension",
+ "Expect a child process with remoteType extension to be found for the process childID set"
+ );
+
+ Assert.notEqual(
+ lastCrashedProcessChildID,
+ currentProcessChildID,
+ "Expect lastCrashedProcessChildID to not be set to the same value that currentProcessChildID is set"
+ );
+
+ let mv3Extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ manifest_version: 3,
+ },
+ background() {
+ browser.test.sendMessage("background_running");
+ },
+ });
+
+ const waitForExtensionBrowserInserted = () =>
+ new Promise(resolve => {
+ const listener = (_eventName, browser) => {
+ if (!browser.getAttribute("webextension-view-type") === "background") {
+ return;
+ }
+ Management.off("extension-browser-inserted", listener);
+ resolve(browser);
+ };
+ Management.on("extension-browser-inserted", listener);
+ });
+
+ const waitForExtensionProcessCrashNotified = () =>
+ new Promise(resolve => {
+ Management.once("extension-process-crash", (_evt, data) => resolve(data));
+ });
+
+ const promiseBackgroundBrowser = waitForExtensionBrowserInserted();
+
+ const promiseExtensionProcessCrashNotified =
+ waitForExtensionProcessCrashNotified();
+
+ await mv3Extension.startup();
+ await mv3Extension.awaitMessage("background_running");
+ const bgPageBrowser = await promiseBackgroundBrowser;
+
+ info("Force extension process crash");
+ // NOTE: shouldShowTabCrashPage option needs to be set to false
+ // to make sure crashFrame method resolves without waiting for a
+ // tab crash page (which is not going to be shown for a background
+ // page browser element).
+ await BrowserTestUtils.crashFrame(
+ bgPageBrowser,
+ /* shouldShowTabCrashPage */ false
+ );
+
+ info("Verify ExtensionProcessCrashObserver after extension process crash");
+ Assert.equal(
+ ExtensionProcessCrashObserver.lastCrashedProcessChildID,
+ currentProcessChildID,
+ "Expect ExtensionProcessCrashObserver.lastCrashedProcessChildID to be set to the expected childID"
+ );
+
+ info("Expect the same childID to have been notified as a Management event");
+ Assert.deepEqual(
+ await promiseExtensionProcessCrashNotified,
+ { childID: currentProcessChildID },
+ "Got the expected childID notified as part of the extension-process-crash Management event"
+ );
+
+ info("Wait for mv3 extension shutdown");
+ await mv3Extension.unload();
+ info("Wait for mv2 extension shutdown");
+ await mv2Extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_test_mock.js b/toolkit/components/extensions/test/browser/browser_ext_test_mock.js
new file mode 100644
index 0000000000..de496b7631
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_test_mock.js
@@ -0,0 +1,47 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// This test verifies that the extension mocks behave consistently, regardless
+// of test type (xpcshell vs browser test).
+// See also toolkit/components/extensions/test/xpcshell/test_ext_test_mock.js
+
+// Check the state of the extension object. This should be consistent between
+// browser tests and xpcshell tests.
+async function checkExtensionStartupAndUnload(ext) {
+ await ext.startup();
+ Assert.ok(ext.id, "Extension ID should be available");
+ Assert.ok(ext.uuid, "Extension UUID should be available");
+ await ext.unload();
+ // Once set nothing clears the UUID.
+ Assert.ok(ext.uuid, "Extension UUID exists after unload");
+}
+
+add_task(async function test_MockExtension() {
+ // When "useAddonManager" is set, a MockExtension is created in the main
+ // process, which does not necessarily behave identically to an Extension.
+ let ext = ExtensionTestUtils.loadExtension({
+ // xpcshell/test_ext_test_mock.js tests "temporary", so here we use
+ // "permanent" to have even more test coverage.
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "@permanent-mock-extension" } },
+ },
+ });
+
+ Assert.ok(!ext.id, "Extension ID is initially unavailable");
+ Assert.ok(!ext.uuid, "Extension UUID is initially unavailable");
+ await checkExtensionStartupAndUnload(ext);
+ Assert.ok(ext.id, "Extension ID exists after unload");
+});
+
+add_task(async function test_generated_Extension() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {},
+ });
+
+ Assert.ok(!ext.id, "Extension ID is initially unavailable");
+ Assert.ok(!ext.uuid, "Extension UUID is initially unavailable");
+ await checkExtensionStartupAndUnload(ext);
+ Assert.ok(ext.id, "Extension ID exists after unload");
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js b/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js
new file mode 100644
index 0000000000..cc814b4ae8
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js
@@ -0,0 +1,119 @@
+"use strict";
+
+// Case 1 - When there is a theme_frame image and additional_backgrounds_alignment is not specified.
+// So background-position should default to "right top"
+add_task(async function test_default_additional_backgrounds_alignment() {
+ const RIGHT_TOP = "100% 0%";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ additional_backgrounds: ["image1.png", "image1.png"],
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+ if (backgroundColorSetOnRoot()) {
+ let docEl = document.documentElement;
+ let rootCS = window.getComputedStyle(docEl);
+
+ Assert.equal(
+ rootCS.getPropertyValue("background-position"),
+ `${RIGHT_TOP}, ${RIGHT_TOP}`,
+ "root contains theme_frame and lwt-background-alignment properties"
+ );
+ Assert.equal(
+ toolboxCS.getPropertyValue("background-position"),
+ RIGHT_TOP,
+ toolbox.id + " contains lwt-background-alignment properties"
+ );
+ } else {
+ /**
+ * We expect duplicate background-position values because we apply `right top`
+ * once for theme_frame, and again as the default value of
+ * --lwt-background-alignment.
+ */
+ Assert.equal(
+ toolboxCS.getPropertyValue("background-position"),
+ `${RIGHT_TOP}, ${RIGHT_TOP}`,
+ toolbox.id +
+ " contains theme_frame and default lwt-background-alignment properties"
+ );
+ }
+
+ await extension.unload();
+});
+
+// Case 2 - When there is a theme_frame image and additional_backgrounds_alignment is specified.
+add_task(async function test_additional_backgrounds_alignment() {
+ const LEFT_BOTTOM = "0% 100%";
+ const CENTER_CENTER = "50% 50%";
+ const RIGHT_TOP = "100% 0%";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ additional_backgrounds: ["image1.png", "image1.png", "image1.png"],
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ properties: {
+ additional_backgrounds_alignment: [
+ "left bottom",
+ "center center",
+ "right top",
+ ],
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+ if (backgroundColorSetOnRoot()) {
+ let docEl = document.documentElement;
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.getPropertyValue("background-position"),
+ `${RIGHT_TOP}, ${LEFT_BOTTOM}, ${CENTER_CENTER}, ${RIGHT_TOP}`,
+ "root contains theme_frame and additional_backgrounds alignment properties"
+ );
+ Assert.equal(
+ toolboxCS.getPropertyValue("background-position"),
+ LEFT_BOTTOM + ", " + CENTER_CENTER + ", " + RIGHT_TOP,
+ toolbox.id + " contains additional_backgrounds alignment properties"
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.getPropertyValue("background-position"),
+ RIGHT_TOP + ", " + LEFT_BOTTOM + ", " + CENTER_CENTER + ", " + RIGHT_TOP,
+ toolbox.id +
+ " contains theme_frame and additional_backgrounds alignment properties"
+ );
+ }
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js b/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js
new file mode 100644
index 0000000000..2799d37551
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js
@@ -0,0 +1,39 @@
+"use strict";
+
+add_task(async function test_alpha_frame_color() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: "rgba(230, 128, 0, 0.1)",
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let computedStyle;
+ if (backgroundColorSetOnRoot()) {
+ let docEl = window.document.documentElement;
+ computedStyle = window.getComputedStyle(docEl);
+ } else {
+ let toolbox = document.querySelector("#navigator-toolbox");
+ computedStyle = window.getComputedStyle(toolbox);
+ }
+
+ Assert.equal(
+ computedStyle.backgroundColor,
+ "rgb(230, 128, 0)",
+ "Window background color should be opaque"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js b/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js
new file mode 100644
index 0000000000..6665fb3092
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js
@@ -0,0 +1,82 @@
+"use strict";
+
+function openIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gIdentityHandler._identityPopup
+ );
+ gIdentityHandler._identityIconBox.click();
+ return promise;
+}
+
+function closeIdentityPopup() {
+ let promise = BrowserTestUtils.waitForEvent(
+ gIdentityHandler._identityPopup,
+ "popuphidden"
+ );
+ gIdentityHandler._identityPopup.hidePopup();
+ return promise;
+}
+
+// This test checks applied WebExtension themes that attempt to change
+// popup properties
+
+add_task(async function test_popup_styling(browser, accDoc) {
+ const POPUP_BACKGROUND_COLOR = "#FF0000";
+ const POPUP_TEXT_COLOR = "#008000";
+ const POPUP_BORDER_COLOR = "#0000FF";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ popup: POPUP_BACKGROUND_COLOR,
+ popup_text: POPUP_TEXT_COLOR,
+ popup_border: POPUP_BORDER_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "https://example.com" },
+ async function (browser) {
+ await extension.startup();
+
+ // Open the information arrow panel
+ await openIdentityPopup();
+
+ let arrowContent = gIdentityHandler._identityPopup.panelContent;
+ let arrowContentComputedStyle = window.getComputedStyle(arrowContent);
+ // Ensure popup background color was set properly
+ Assert.equal(
+ arrowContentComputedStyle.getPropertyValue("background-color"),
+ `rgb(${hexToRGB(POPUP_BACKGROUND_COLOR).join(", ")})`,
+ "Popup background color should have been themed"
+ );
+
+ // Ensure popup text color was set properly
+ Assert.equal(
+ arrowContentComputedStyle.getPropertyValue("color"),
+ `rgb(${hexToRGB(POPUP_TEXT_COLOR).join(", ")})`,
+ "Popup text color should have been themed"
+ );
+
+ // Ensure popup border color was set properly
+ testBorderColor(arrowContent, POPUP_BORDER_COLOR);
+
+ await closeIdentityPopup();
+ await extension.unload();
+ }
+ );
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js b/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js
new file mode 100644
index 0000000000..d2baf6157b
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js
@@ -0,0 +1,173 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// popup properties are applied correctly to the autocomplete bar.
+const POPUP_COLOR_DARK = "#00A400";
+const POPUP_COLOR_BRIGHT = "#85A4FF";
+const POPUP_TEXT_COLOR_DARK = "#000000";
+const POPUP_TEXT_COLOR_BRIGHT = "#ffffff";
+const POPUP_SELECTED_COLOR = "#9400ff";
+const POPUP_SELECTED_TEXT_COLOR = "#09b9a6";
+
+const POPUP_URL_COLOR_DARK = "#0061e0";
+const POPUP_ACTION_COLOR_DARK = "#5b5b66";
+const POPUP_URL_COLOR_BRIGHT = "#00ddff";
+const POPUP_ACTION_COLOR_BRIGHT = "#bfbfc9";
+
+const SEARCH_TERM = "urlbar-reflows-" + Date.now();
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ const NUM_VISITS = 10;
+ let visits = [];
+
+ for (let i = 0; i < NUM_VISITS; ++i) {
+ visits.push({
+ uri: `http://example.com/urlbar-reflows-${i}`,
+ title: `Reflow test for URL bar entry #${i} - ${SEARCH_TERM}`,
+ });
+ }
+
+ await PlacesTestUtils.addVisits(visits);
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_popup_url() {
+ // Load a manifest with popup_text being dark (bright background). Test for
+ // dark text properties.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field_focus: POPUP_COLOR_BRIGHT,
+ toolbar_field_text_focus: POPUP_TEXT_COLOR_DARK,
+ popup_highlight: POPUP_SELECTED_COLOR,
+ popup_highlight_text: POPUP_SELECTED_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:mozilla"
+ );
+ registerCleanupFunction(async function () {
+ await PlacesUtils.history.clear();
+ await BrowserTestUtils.removeTab(tab);
+ });
+
+ let visits = [];
+
+ for (let i = 0; i < maxResults; i++) {
+ visits.push({ uri: makeURI("http://example.com/autocomplete/?" + i) });
+ }
+
+ await PlacesTestUtils.addVisits(visits);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example.com/autocomplete",
+ });
+ await UrlbarTestUtils.waitForAutocompleteResultAt(window, maxResults - 1);
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxResults,
+ "Should get maxResults=" + maxResults + " results"
+ );
+
+ // Set the selected attribute to true to test the highlight popup properties
+ UrlbarTestUtils.setSelectedRowIndex(window, 1);
+ let actionResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ let urlResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ let resultCS = window.getComputedStyle(urlResult.element.row);
+
+ Assert.equal(
+ resultCS.backgroundColor,
+ `rgb(${hexToRGB(POPUP_SELECTED_COLOR).join(", ")})`,
+ `Popup highlight background color should be set to ${POPUP_SELECTED_COLOR}`
+ );
+
+ Assert.equal(
+ resultCS.color,
+ `rgb(${hexToRGB(POPUP_SELECTED_TEXT_COLOR).join(", ")})`,
+ `Popup highlight color should be set to ${POPUP_SELECTED_TEXT_COLOR}`
+ );
+
+ // Now set the index to somewhere not on the first two, so that we can test both
+ // url and action text colors.
+ UrlbarTestUtils.setSelectedRowIndex(window, 2);
+
+ Assert.equal(
+ window.getComputedStyle(urlResult.element.url).color,
+ `rgb(${hexToRGB(POPUP_URL_COLOR_DARK).join(", ")})`,
+ `Urlbar popup url color should be set to ${POPUP_URL_COLOR_DARK}`
+ );
+
+ Assert.equal(
+ window.getComputedStyle(actionResult.element.action).color,
+ `rgb(${hexToRGB(POPUP_ACTION_COLOR_DARK).join(", ")})`,
+ `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_DARK}`
+ );
+
+ await extension.unload();
+
+ // Load a manifest with popup_text being bright (dark background). Test for
+ // bright text properties.
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field_focus: POPUP_COLOR_DARK,
+ toolbar_field_text_focus: POPUP_TEXT_COLOR_BRIGHT,
+ popup_highlight: POPUP_SELECTED_COLOR,
+ popup_highlight_text: POPUP_SELECTED_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ Assert.equal(
+ window.getComputedStyle(urlResult.element.url).color,
+ `rgb(${hexToRGB(POPUP_URL_COLOR_BRIGHT).join(", ")})`,
+ `Urlbar popup url color should be set to ${POPUP_URL_COLOR_BRIGHT}`
+ );
+
+ Assert.equal(
+ window.getComputedStyle(actionResult.element.action).color,
+ `rgb(${hexToRGB(POPUP_ACTION_COLOR_BRIGHT).join(", ")})`,
+ `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_BRIGHT}`
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js b/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js
new file mode 100644
index 0000000000..e88c78fa93
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js
@@ -0,0 +1,213 @@
+"use strict";
+
+add_task(async function test_support_theme_frame() {
+ const FRAME_COLOR = [71, 105, 91];
+ const TAB_TEXT_COLOR = [0, 0, 0];
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "face.png",
+ },
+ colors: {
+ frame: FRAME_COLOR,
+ tab_background_text: TAB_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "face.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+
+ Assert.ok(
+ docEl.hasAttribute("lwtheme-image"),
+ "LWT image attribute should be set"
+ );
+
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ null,
+ "LWT text color attribute should not be set"
+ );
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.ok(
+ rootCS.backgroundImage.includes("face.png"),
+ `The backgroundImage should use face.png. Actual value is: ${toolboxCS.backgroundImage}`
+ );
+ Assert.equal(
+ rootCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Expected correct background color"
+ );
+ } else {
+ Assert.ok(
+ toolboxCS.backgroundImage.includes("face.png"),
+ `The backgroundImage should use face.png. Actual value is: ${toolboxCS.backgroundImage}`
+ );
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Expected correct background color"
+ );
+ }
+ Assert.equal(
+ toolboxCS.color,
+ "rgb(" + TAB_TEXT_COLOR.join(", ") + ")",
+ "Expected correct text color"
+ );
+
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+
+ Assert.ok(
+ !docEl.hasAttribute("lwtheme-image"),
+ "LWT image attribute should not be set"
+ );
+
+ Assert.ok(
+ !docEl.hasAttribute("lwtheme-brighttext"),
+ "LWT text color attribute should not be set"
+ );
+});
+
+add_task(async function test_support_theme_frame_inactive() {
+ const FRAME_COLOR = [71, 105, 91];
+ const FRAME_COLOR_INACTIVE = [255, 0, 0];
+ const TAB_TEXT_COLOR = [207, 221, 192];
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: FRAME_COLOR,
+ frame_inactive: FRAME_COLOR_INACTIVE,
+ tab_background_text: TAB_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Window background is set to the colors.frame property"
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Window background is set to the colors.frame property"
+ );
+ }
+
+ // Now we'll open a new window to see if the inactive browser accent color changed
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.backgroundColor,
+ "rgb(" + FRAME_COLOR_INACTIVE.join(", ") + ")",
+ `Inactive window root background color should be ${FRAME_COLOR_INACTIVE}`
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ "rgb(" + FRAME_COLOR_INACTIVE.join(", ") + ")",
+ `Inactive window background color should be ${FRAME_COLOR_INACTIVE}`
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(window2);
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
+
+add_task(async function test_lack_of_theme_frame_inactive() {
+ const FRAME_COLOR = [71, 105, 91];
+ const TAB_TEXT_COLOR = [207, 221, 192];
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: FRAME_COLOR,
+ tab_background_text: TAB_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Window background is set to the colors.frame property"
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Window background is set to the colors.frame property"
+ );
+ }
+
+ // Now we'll open a new window to make sure the inactive browser accent color stayed the same
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Inactive window background should not change if colors.frame_inactive isn't set"
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ "rgb(" + FRAME_COLOR.join(", ") + ")",
+ "Inactive window background should not change if colors.frame_inactive isn't set"
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(window2);
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js
new file mode 100644
index 0000000000..4a379edfbf
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js
@@ -0,0 +1,203 @@
+"use strict";
+
+// This test checks whether browser.theme.getCurrent() works correctly in different
+// configurations and with different parameter.
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+// PNG image data for the Mozilla dino head.
+const BACKGROUND_2 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+add_task(async function test_get_current() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ const ACCENT_COLOR_1 = "#a14040";
+ const TEXT_COLOR_1 = "#fac96e";
+
+ const ACCENT_COLOR_2 = "#03fe03";
+ const TEXT_COLOR_2 = "#0ef325";
+
+ const theme1 = {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR_1,
+ tab_background_text: TEXT_COLOR_1,
+ },
+ };
+
+ const theme2 = {
+ images: {
+ theme_frame: "image2.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR_2,
+ tab_background_text: TEXT_COLOR_2,
+ },
+ };
+
+ function ensureWindowFocused(winId) {
+ browser.test.log("Waiting for focused window to be " + winId);
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async resolve => {
+ let listener = windowId => {
+ if (windowId === winId) {
+ browser.windows.onFocusChanged.removeListener(listener);
+ resolve();
+ }
+ };
+ // We first add a listener and then check whether the window is
+ // focused using .get(), because the .get() Promise resolving
+ // could race with the listener running, in which case we'd
+ // never be notified.
+ browser.windows.onFocusChanged.addListener(listener);
+ let { focused } = await browser.windows.get(winId);
+ if (focused) {
+ browser.windows.onFocusChanged.removeListener(listener);
+ resolve();
+ }
+ });
+ }
+
+ function testTheme1(returnedTheme) {
+ browser.test.assertTrue(
+ returnedTheme.images.theme_frame.includes("image1.png"),
+ "Theme 1 theme_frame image should be applied"
+ );
+ browser.test.assertEq(
+ ACCENT_COLOR_1,
+ returnedTheme.colors.frame,
+ "Theme 1 frame color should be applied"
+ );
+ browser.test.assertEq(
+ TEXT_COLOR_1,
+ returnedTheme.colors.tab_background_text,
+ "Theme 1 tab_background_text color should be applied"
+ );
+ }
+
+ function testTheme2(returnedTheme) {
+ browser.test.assertTrue(
+ returnedTheme.images.theme_frame.includes("image2.png"),
+ "Theme 2 theme_frame image should be applied"
+ );
+ browser.test.assertEq(
+ ACCENT_COLOR_2,
+ returnedTheme.colors.frame,
+ "Theme 2 frame color should be applied"
+ );
+ browser.test.assertEq(
+ TEXT_COLOR_2,
+ returnedTheme.colors.tab_background_text,
+ "Theme 2 tab_background_text color should be applied"
+ );
+ }
+
+ function testEmptyTheme(returnedTheme) {
+ browser.test.assertEq(
+ JSON.stringify({ colors: null, images: null, properties: null }),
+ JSON.stringify(returnedTheme),
+ JSON.stringify(returnedTheme, null, 2)
+ );
+ }
+
+ browser.test.log("Testing getCurrent() with initial unthemed window");
+ const firstWin = await browser.windows.getCurrent();
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+
+ browser.test.log("Testing getCurrent() with after theme.update()");
+ await browser.theme.update(theme1);
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+
+ browser.test.log(
+ "Testing getCurrent() with after theme.update(windowId)"
+ );
+ const secondWin = await browser.windows.create();
+ await ensureWindowFocused(secondWin.id);
+ await browser.theme.update(secondWin.id, theme2);
+ testTheme2(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after window focus change");
+ let focusChanged = ensureWindowFocused(firstWin.id);
+ await browser.windows.update(firstWin.id, { focused: true });
+ await focusChanged;
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log(
+ "Testing getCurrent() after another window focus change"
+ );
+ focusChanged = ensureWindowFocused(secondWin.id);
+ await browser.windows.update(secondWin.id, { focused: true });
+ await focusChanged;
+ testTheme2(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after theme.reset(windowId)");
+ await browser.theme.reset(firstWin.id);
+ testTheme2(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log(
+ "Testing getCurrent() after reset and window focus change"
+ );
+ focusChanged = ensureWindowFocused(firstWin.id);
+ await browser.windows.update(firstWin.id, { focused: true });
+ await focusChanged;
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after theme.update(windowId)");
+ await browser.theme.update(firstWin.id, theme1);
+ testTheme1(await browser.theme.getCurrent());
+ testTheme1(await browser.theme.getCurrent(firstWin.id));
+ testTheme2(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after theme.reset()");
+ await browser.theme.reset();
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+ testEmptyTheme(await browser.theme.getCurrent(secondWin.id));
+
+ browser.test.log("Testing getCurrent() after closing a window");
+ await browser.windows.remove(secondWin.id);
+ testEmptyTheme(await browser.theme.getCurrent());
+ testEmptyTheme(await browser.theme.getCurrent(firstWin.id));
+
+ browser.test.log("Testing update calls with invalid window ID");
+ await browser.test.assertRejects(
+ browser.theme.reset(secondWin.id),
+ /Invalid window/,
+ "Invalid window should throw"
+ );
+ await browser.test.assertRejects(
+ browser.theme.update(secondWin.id, theme2),
+ /Invalid window/,
+ "Invalid window should throw"
+ );
+ browser.test.notifyPass("get_current");
+ },
+ manifest: {
+ permissions: ["theme"],
+ },
+ files: {
+ "image1.png": BACKGROUND_1,
+ "image2.png": BACKGROUND_2,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("get_current");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js
new file mode 100644
index 0000000000..34c7162810
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js
@@ -0,0 +1,154 @@
+"use strict";
+
+// This test checks whether browser.theme.onUpdated works correctly with different
+// types of dynamic theme updates.
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+// PNG image data for the Mozilla dino head.
+const BACKGROUND_2 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+
+add_task(async function test_on_updated() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ const ACCENT_COLOR_1 = "#a14040";
+ const TEXT_COLOR_1 = "#fac96e";
+
+ const ACCENT_COLOR_2 = "#03fe03";
+ const TEXT_COLOR_2 = "#0ef325";
+
+ const theme1 = {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR_1,
+ tab_background_text: TEXT_COLOR_1,
+ },
+ };
+
+ const theme2 = {
+ images: {
+ theme_frame: "image2.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR_2,
+ tab_background_text: TEXT_COLOR_2,
+ },
+ };
+
+ function testTheme1(returnedTheme) {
+ browser.test.assertTrue(
+ returnedTheme.images.theme_frame.includes("image1.png"),
+ "Theme 1 theme_frame image should be applied"
+ );
+ browser.test.assertEq(
+ ACCENT_COLOR_1,
+ returnedTheme.colors.frame,
+ "Theme 1 frame color should be applied"
+ );
+ browser.test.assertEq(
+ TEXT_COLOR_1,
+ returnedTheme.colors.tab_background_text,
+ "Theme 1 tab_background_text color should be applied"
+ );
+ }
+
+ function testTheme2(returnedTheme) {
+ browser.test.assertTrue(
+ returnedTheme.images.theme_frame.includes("image2.png"),
+ "Theme 2 theme_frame image should be applied"
+ );
+ browser.test.assertEq(
+ ACCENT_COLOR_2,
+ returnedTheme.colors.frame,
+ "Theme 2 frame color should be applied"
+ );
+ browser.test.assertEq(
+ TEXT_COLOR_2,
+ returnedTheme.colors.tab_background_text,
+ "Theme 2 tab_background_text color should be applied"
+ );
+ }
+
+ const firstWin = await browser.windows.getCurrent();
+ const secondWin = await browser.windows.create();
+
+ const onceThemeUpdated = () =>
+ new Promise(resolve => {
+ const listener = updateInfo => {
+ browser.theme.onUpdated.removeListener(listener);
+ resolve(updateInfo);
+ };
+ browser.theme.onUpdated.addListener(listener);
+ });
+
+ browser.test.log("Testing update with no windowId parameter");
+ let updateInfo1 = onceThemeUpdated();
+ await browser.theme.update(theme1);
+ updateInfo1 = await updateInfo1;
+ testTheme1(updateInfo1.theme);
+ browser.test.assertTrue(
+ !updateInfo1.windowId,
+ "No window id on first update"
+ );
+
+ browser.test.log("Testing update with windowId parameter");
+ let updateInfo2 = onceThemeUpdated();
+ await browser.theme.update(secondWin.id, theme2);
+ updateInfo2 = await updateInfo2;
+ testTheme2(updateInfo2.theme);
+ browser.test.assertEq(
+ secondWin.id,
+ updateInfo2.windowId,
+ "window id on second update"
+ );
+
+ browser.test.log("Testing reset with windowId parameter");
+ let updateInfo3 = onceThemeUpdated();
+ await browser.theme.reset(firstWin.id);
+ updateInfo3 = await updateInfo3;
+ browser.test.assertEq(
+ 0,
+ Object.keys(updateInfo3.theme).length,
+ "Empty theme given on reset"
+ );
+ browser.test.assertEq(
+ firstWin.id,
+ updateInfo3.windowId,
+ "window id on third update"
+ );
+
+ browser.test.log("Testing reset with no windowId parameter");
+ let updateInfo4 = onceThemeUpdated();
+ await browser.theme.reset();
+ updateInfo4 = await updateInfo4;
+ browser.test.assertEq(
+ 0,
+ Object.keys(updateInfo4.theme).length,
+ "Empty theme given on reset"
+ );
+ browser.test.assertTrue(
+ !updateInfo4.windowId,
+ "no window id on fourth update"
+ );
+
+ browser.test.log("Cleaning up test");
+ await browser.windows.remove(secondWin.id);
+ browser.test.notifyPass("onUpdated");
+ },
+ manifest: {
+ permissions: ["theme"],
+ },
+ files: {
+ "image1.png": BACKGROUND_1,
+ "image2.png": BACKGROUND_2,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("onUpdated");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
new file mode 100644
index 0000000000..c6af66b5e5
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
@@ -0,0 +1,217 @@
+"use strict";
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const ACCENT_COLOR_1 = "#a14040";
+const TEXT_COLOR_1 = "#fac96e";
+
+// PNG image data for the Mozilla dino head.
+const BACKGROUND_2 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+const ACCENT_COLOR_2 = "#03fe03";
+const TEXT_COLOR_2 = "#0ef325";
+
+function hexToRGB(hex) {
+ hex = parseInt(hex.indexOf("#") > -1 ? hex.substring(1) : hex, 16);
+ return (
+ "rgb(" + [hex >> 16, (hex & 0x00ff00) >> 8, hex & 0x0000ff].join(", ") + ")"
+ );
+}
+
+function validateTheme(backgroundImage, accentColor, textColor, isLWT) {
+ let docEl = window.document.documentElement;
+ let rootCS = window.getComputedStyle(docEl);
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+
+ if (isLWT) {
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ "true",
+ "LWT text color attribute should be set"
+ );
+ }
+
+ if (accentColor.startsWith("#")) {
+ accentColor = hexToRGB(accentColor);
+ }
+ if (textColor.startsWith("#")) {
+ textColor = hexToRGB(textColor);
+ }
+ if (backgroundColorSetOnRoot()) {
+ Assert.ok(
+ rootCS.backgroundImage.includes(backgroundImage),
+ "Expected correct background image"
+ );
+ Assert.equal(
+ rootCS.backgroundColor,
+ accentColor,
+ "Expected correct accent color"
+ );
+ } else {
+ Assert.ok(
+ toolboxCS.backgroundImage.includes(backgroundImage),
+ "Expected correct background image"
+ );
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ accentColor,
+ "Expected correct accent color"
+ );
+ }
+
+ Assert.equal(rootCS.color, textColor, "Expected correct text color");
+}
+
+add_task(async function test_dynamic_theme_updates() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ files: {
+ "image1.png": BACKGROUND_1,
+ "image2.png": BACKGROUND_2,
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg === "update-theme") {
+ browser.theme.update(details).then(() => {
+ browser.test.sendMessage("theme-updated");
+ });
+ } else {
+ browser.theme.reset().then(() => {
+ browser.test.sendMessage("theme-reset");
+ });
+ }
+ });
+ },
+ });
+
+ let rootCS = window.getComputedStyle(window.document.documentElement);
+ let toolboxCS = window.getComputedStyle(
+ window.document.documentElement.querySelector("#navigator-toolbox")
+ );
+ await extension.startup();
+
+ extension.sendMessage("update-theme", {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR_1,
+ tab_background_text: TEXT_COLOR_1,
+ },
+ });
+
+ await extension.awaitMessage("theme-updated");
+
+ validateTheme("image1.png", ACCENT_COLOR_1, TEXT_COLOR_1, true);
+
+ // Check with the LWT aliases (to update on Firefox 69, because the
+ // LWT aliases are going to be removed).
+ extension.sendMessage("update-theme", {
+ images: {
+ theme_frame: "image2.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR_2,
+ tab_background_text: TEXT_COLOR_2,
+ },
+ });
+
+ await extension.awaitMessage("theme-updated");
+
+ validateTheme("image2.png", ACCENT_COLOR_2, TEXT_COLOR_2, true);
+
+ extension.sendMessage("reset-theme");
+
+ await extension.awaitMessage("theme-reset");
+
+ let { color } = rootCS;
+ let { backgroundImage, backgroundColor } = toolboxCS;
+ if (backgroundColorSetOnRoot()) {
+ backgroundImage = rootCS.backgroundImage;
+ backgroundColor = rootCS.backgroundColor;
+ }
+ validateTheme(backgroundImage, backgroundColor, color, false);
+
+ await extension.unload();
+
+ let docEl = window.document.documentElement;
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
+
+add_task(async function test_dynamic_theme_updates_with_data_url() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg === "update-theme") {
+ browser.theme.update(details).then(() => {
+ browser.test.sendMessage("theme-updated");
+ });
+ } else {
+ browser.theme.reset().then(() => {
+ browser.test.sendMessage("theme-reset");
+ });
+ }
+ });
+ },
+ });
+
+ let rootCS = window.getComputedStyle(window.document.documentElement);
+ let toolboxCS = window.getComputedStyle(
+ window.document.documentElement.querySelector("#navigator-toolbox")
+ );
+ await extension.startup();
+
+ extension.sendMessage("update-theme", {
+ images: {
+ theme_frame: BACKGROUND_1,
+ },
+ colors: {
+ frame: ACCENT_COLOR_1,
+ tab_background_text: TEXT_COLOR_1,
+ },
+ });
+
+ await extension.awaitMessage("theme-updated");
+
+ validateTheme(BACKGROUND_1, ACCENT_COLOR_1, TEXT_COLOR_1, true);
+
+ extension.sendMessage("update-theme", {
+ images: {
+ theme_frame: BACKGROUND_2,
+ },
+ colors: {
+ frame: ACCENT_COLOR_2,
+ tab_background_text: TEXT_COLOR_2,
+ },
+ });
+
+ await extension.awaitMessage("theme-updated");
+
+ validateTheme(BACKGROUND_2, ACCENT_COLOR_2, TEXT_COLOR_2, true);
+
+ extension.sendMessage("reset-theme");
+
+ await extension.awaitMessage("theme-reset");
+
+ let { color } = rootCS;
+ let { backgroundImage, backgroundColor } = toolboxCS;
+ if (backgroundColorSetOnRoot()) {
+ backgroundImage = rootCS.backgroundImage;
+ backgroundColor = rootCS.backgroundColor;
+ }
+ validateTheme(backgroundImage, backgroundColor, color, false);
+
+ await extension.unload();
+
+ let docEl = window.document.documentElement;
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js b/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js
new file mode 100644
index 0000000000..02156b6cd8
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js
@@ -0,0 +1,450 @@
+"use strict";
+
+const { AddonSettings } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/AddonSettings.sys.mjs"
+);
+
+// This test checks whether the theme experiments work
+add_task(async function test_experiment_static_theme() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ theme: {
+ colors: {
+ some_color_property: "#ff00ff",
+ },
+ images: {
+ some_image_property: "background.jpg",
+ },
+ properties: {
+ some_random_property: "no-repeat",
+ },
+ },
+ theme_experiment: {
+ colors: {
+ some_color_property: "--some-color-property",
+ },
+ images: {
+ some_image_property: "--some-image-property",
+ },
+ properties: {
+ some_random_property: "--some-random-property",
+ },
+ },
+ },
+ });
+
+ const root = window.document.documentElement;
+
+ is(
+ root.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ root.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ root.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+
+ await extension.startup();
+
+ const testExperimentApplied = rootEl => {
+ if (AddonSettings.EXPERIMENTS_ENABLED) {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ hexToCSS("#ff00ff"),
+ "Color property should be parsed and set."
+ );
+ ok(
+ rootEl.style
+ .getPropertyValue("--some-image-property")
+ .startsWith("url("),
+ "Image property should be parsed."
+ );
+ ok(
+ rootEl.style
+ .getPropertyValue("--some-image-property")
+ .endsWith("background.jpg)"),
+ "Image property should be set."
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "no-repeat",
+ "Generic Property should be set."
+ );
+ } else {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+ }
+ };
+
+ info("Testing that current window updated with the experiment applied");
+ testExperimentApplied(root);
+
+ info("Testing that new window initialized with the experiment applied");
+ const newWindow = await BrowserTestUtils.openNewBrowserWindow();
+ const newWindowRoot = newWindow.document.documentElement;
+ testExperimentApplied(newWindowRoot);
+
+ await extension.unload();
+
+ info("Testing that both windows unapplied the experiment");
+ for (const rootEl of [root, newWindowRoot]) {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+ }
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+add_task(async function test_experiment_dynamic_theme() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ permissions: ["theme"],
+ theme_experiment: {
+ colors: {
+ some_color_property: "--some-color-property",
+ },
+ images: {
+ some_image_property: "--some-image-property",
+ },
+ properties: {
+ some_random_property: "--some-random-property",
+ },
+ },
+ },
+ background() {
+ const theme = {
+ colors: {
+ some_color_property: "#ff00ff",
+ },
+ images: {
+ some_image_property: "background.jpg",
+ },
+ properties: {
+ some_random_property: "no-repeat",
+ },
+ };
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "update-theme") {
+ browser.theme.update(theme).then(() => {
+ browser.test.sendMessage("theme-updated");
+ });
+ } else {
+ browser.theme.reset().then(() => {
+ browser.test.sendMessage("theme-reset");
+ });
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+
+ const root = window.document.documentElement;
+
+ is(
+ root.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ root.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ root.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+
+ extension.sendMessage("update-theme");
+ await extension.awaitMessage("theme-updated");
+
+ const testExperimentApplied = rootEl => {
+ if (AddonSettings.EXPERIMENTS_ENABLED) {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ hexToCSS("#ff00ff"),
+ "Color property should be parsed and set."
+ );
+ ok(
+ rootEl.style
+ .getPropertyValue("--some-image-property")
+ .startsWith("url("),
+ "Image property should be parsed."
+ );
+ ok(
+ rootEl.style
+ .getPropertyValue("--some-image-property")
+ .endsWith("background.jpg)"),
+ "Image property should be set."
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "no-repeat",
+ "Generic Property should be set."
+ );
+ } else {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+ }
+ };
+ testExperimentApplied(root);
+
+ const newWindow = await BrowserTestUtils.openNewBrowserWindow();
+ const newWindowRoot = newWindow.document.documentElement;
+
+ testExperimentApplied(newWindowRoot);
+
+ extension.sendMessage("reset-theme");
+ await extension.awaitMessage("theme-reset");
+
+ for (const rootEl of [root, newWindowRoot]) {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+ }
+
+ extension.sendMessage("update-theme");
+ await extension.awaitMessage("theme-updated");
+
+ testExperimentApplied(root);
+ testExperimentApplied(newWindowRoot);
+
+ await extension.unload();
+
+ for (const rootEl of [root, newWindowRoot]) {
+ is(
+ rootEl.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-image-property"),
+ "",
+ "Image property should be unset"
+ );
+ is(
+ rootEl.style.getPropertyValue("--some-random-property"),
+ "",
+ "Generic Property should be unset."
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(newWindow);
+});
+
+add_task(async function test_experiment_stylesheet() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ theme: {
+ colors: {
+ menu_button_background: "#ff00ff",
+ },
+ },
+ theme_experiment: {
+ stylesheet: "experiment.css",
+ colors: {
+ menu_button_background: "--menu-button-background",
+ },
+ },
+ },
+ files: {
+ "experiment.css": `#PanelUI-menu-button {
+ background-color: var(--menu-button-background);
+ fill: white;
+ }`,
+ },
+ });
+
+ const root = window.document.documentElement;
+ const menuButton = document.getElementById("PanelUI-menu-button");
+ const computedStyle = window.getComputedStyle(menuButton);
+ const expectedColor = hexToCSS("#ff00ff");
+ const expectedFill = hexToCSS("#ffffff");
+
+ is(
+ root.style.getPropertyValue("--menu-button-background"),
+ "",
+ "Variable should be unset"
+ );
+ isnot(
+ computedStyle.backgroundColor,
+ expectedColor,
+ "Menu button should not have custom background"
+ );
+ isnot(
+ computedStyle.fill,
+ expectedFill,
+ "Menu button should not have stylesheet fill"
+ );
+
+ await extension.startup();
+
+ if (AddonSettings.EXPERIMENTS_ENABLED) {
+ // Wait for stylesheet load.
+ await BrowserTestUtils.waitForCondition(
+ () => computedStyle.fill === expectedFill
+ );
+
+ is(
+ root.style.getPropertyValue("--menu-button-background"),
+ expectedColor,
+ "Variable should be parsed and set."
+ );
+ is(
+ computedStyle.backgroundColor,
+ expectedColor,
+ "Menu button should be have correct background"
+ );
+ is(
+ computedStyle.fill,
+ expectedFill,
+ "Menu button should be have correct fill"
+ );
+ } else {
+ is(
+ root.style.getPropertyValue("--menu-button-background"),
+ "",
+ "Variable should be unset"
+ );
+ isnot(
+ computedStyle.backgroundColor,
+ expectedColor,
+ "Menu button should not have custom background"
+ );
+ isnot(
+ computedStyle.fill,
+ expectedFill,
+ "Menu button should not have stylesheet fill"
+ );
+ }
+
+ await extension.unload();
+
+ is(
+ root.style.getPropertyValue("--menu-button-background"),
+ "",
+ "Variable should be unset"
+ );
+ isnot(
+ computedStyle.backgroundColor,
+ expectedColor,
+ "Menu button should not have custom background"
+ );
+ isnot(
+ computedStyle.fill,
+ expectedFill,
+ "Menu button should not have stylesheet fill"
+ );
+});
+
+// This test checks whether the theme experiments are allowed for non privileged
+// theme installed non-temporarily if AddonSettings.EXPERIMENTS_ENABLED is true.
+add_task(async function test_experiment_installed_non_temporarily() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.experiments.enabled", true]],
+ });
+
+ if (!AddonSettings.EXPERIMENTS_ENABLED) {
+ info(
+ "Skipping test case on build where AddonSettings.EXPERIMENTS_ENABLED is false"
+ );
+ return;
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ some_color_property: "#ff00ff",
+ },
+ },
+ theme_experiment: {
+ colors: {
+ some_color_property: "--some-color-property",
+ },
+ },
+ },
+ });
+
+ const root = window.document.documentElement;
+
+ is(
+ root.style.getPropertyValue("--some-color-property"),
+ "",
+ "Color property should be unset"
+ );
+
+ await extension.startup();
+
+ is(
+ root.style.getPropertyValue("--some-color-property"),
+ hexToCSS("#ff00ff"),
+ "Color property should be parsed and set."
+ );
+
+ await extension.unload();
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js b/toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js
new file mode 100644
index 0000000000..a24c90615b
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js
@@ -0,0 +1,227 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the toolbar and toolbar_field properties also theme the findbar.
+
+function assertHasNoBorders(element) {
+ let cs = window.getComputedStyle(element);
+ Assert.equal(cs.borderTopWidth, "0px", "should have no top border");
+ Assert.equal(cs.borderRightWidth, "0px", "should have no right border");
+ Assert.equal(cs.borderBottomWidth, "0px", "should have no bottom border");
+ Assert.equal(cs.borderLeftWidth, "0px", "should have no left border");
+}
+
+add_task(async function test_support_toolbar_properties_on_findbar() {
+ const TOOLBAR_COLOR = "#ff00ff";
+ const TOOLBAR_TEXT_COLOR = "#9400ff";
+ const ACCENT_COLOR_INACTIVE = "#ffff00";
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ frame_inactive: ACCENT_COLOR_INACTIVE,
+ tab_background_text: TEXT_COLOR,
+ toolbar: TOOLBAR_COLOR,
+ bookmark_text: TOOLBAR_TEXT_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+ await gBrowser.getFindBar();
+
+ let findbar_button = gFindBar.getElement("highlight");
+
+ info("Checking findbar background is set as toolbar color");
+ Assert.equal(
+ window.getComputedStyle(gFindBar).backgroundColor,
+ hexToCSS(ACCENT_COLOR),
+ "Findbar background color should be the same as toolbar background color."
+ );
+
+ info("Checking findbar and checkbox text color use toolbar text color");
+ const rgbColor = hexToCSS(TOOLBAR_TEXT_COLOR);
+ Assert.equal(
+ window.getComputedStyle(gFindBar).color,
+ rgbColor,
+ "Findbar text color should be the same as toolbar text color."
+ );
+ Assert.equal(
+ window.getComputedStyle(findbar_button).color,
+ rgbColor,
+ "Findbar checkbox text color should be toolbar text color."
+ );
+
+ // Open a new window to check frame_inactive
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+ Assert.equal(
+ window.getComputedStyle(gFindBar).backgroundColor,
+ hexToCSS(ACCENT_COLOR_INACTIVE),
+ "Findbar background changed in inactive window."
+ );
+ await BrowserTestUtils.closeWindow(window2);
+
+ await extension.unload();
+});
+
+add_task(async function test_support_toolbar_field_properties_on_findbar() {
+ let findbar_prev_button = gFindBar.getElement("find-previous");
+ let findbar_next_button = gFindBar.getElement("find-next");
+
+ assertHasNoBorders(findbar_prev_button);
+ assertHasNoBorders(findbar_next_button);
+
+ const TOOLBAR_FIELD_COLOR = "#ff00ff";
+ const TOOLBAR_FIELD_TEXT_COLOR = "#9400ff";
+ const TOOLBAR_FIELD_BORDER_COLOR = "#ffffff";
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field: TOOLBAR_FIELD_COLOR,
+ toolbar_field_text: TOOLBAR_FIELD_TEXT_COLOR,
+ toolbar_field_border: TOOLBAR_FIELD_BORDER_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+ await gBrowser.getFindBar();
+
+ let findbar_textbox = gFindBar.getElement("findbar-textbox");
+
+ info(
+ "Checking findbar textbox background is set as toolbar field background color"
+ );
+ Assert.equal(
+ window.getComputedStyle(findbar_textbox).backgroundColor,
+ hexToCSS(TOOLBAR_FIELD_COLOR),
+ "Findbar textbox background color should be the same as toolbar field color."
+ );
+
+ info("Checking findbar textbox color is set as toolbar field text color");
+ Assert.equal(
+ window.getComputedStyle(findbar_textbox).color,
+ hexToCSS(TOOLBAR_FIELD_TEXT_COLOR),
+ "Findbar textbox text color should be the same as toolbar field text color."
+ );
+ testBorderColor(findbar_textbox, TOOLBAR_FIELD_BORDER_COLOR);
+
+ assertHasNoBorders(findbar_prev_button);
+ assertHasNoBorders(findbar_next_button);
+
+ await extension.unload();
+});
+
+// Test that theme properties are applied with a theme_frame
+add_task(async function test_toolbar_properties_on_findbar_with_theme_frame() {
+ const TOOLBAR_COLOR = "#ff00ff";
+ const TOOLBAR_TEXT_COLOR = "#9400ff";
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar: TOOLBAR_COLOR,
+ bookmark_text: TOOLBAR_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+ await gBrowser.getFindBar();
+
+ let findbar_button = gFindBar.getElement("highlight");
+
+ info("Checking findbar background is set as toolbar color");
+ Assert.equal(
+ window.getComputedStyle(gFindBar).backgroundColor,
+ hexToCSS(ACCENT_COLOR),
+ "Findbar background color should be set by theme."
+ );
+
+ info("Checking findbar and button text color is set as toolbar text color");
+ Assert.equal(
+ window.getComputedStyle(gFindBar).color,
+ hexToCSS(TOOLBAR_TEXT_COLOR),
+ "Findbar text color should be set by theme."
+ );
+ Assert.equal(
+ window.getComputedStyle(findbar_button).color,
+ hexToCSS(TOOLBAR_TEXT_COLOR),
+ "Findbar button text color should be set by theme."
+ );
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_toolbar_field_properties_on_findbar_with_theme_frame() {
+ const TOOLBAR_FIELD_COLOR = "#ff00ff";
+ const TOOLBAR_FIELD_TEXT_COLOR = "#9400ff";
+ const TOOLBAR_FIELD_BORDER_COLOR = "#ffffff";
+ // The TabContextMenu initializes its strings only on a focus or mouseover event.
+ // Calls focus event on the TabContextMenu early in the test.
+ gBrowser.selectedTab.focus();
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field: TOOLBAR_FIELD_COLOR,
+ toolbar_field_text: TOOLBAR_FIELD_TEXT_COLOR,
+ toolbar_field_border: TOOLBAR_FIELD_BORDER_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+ await gBrowser.getFindBar();
+
+ let findbar_textbox = gFindBar.getElement("findbar-textbox");
+
+ Assert.equal(
+ window.getComputedStyle(findbar_textbox).backgroundColor,
+ hexToCSS(TOOLBAR_FIELD_COLOR),
+ "Findbar textbox background color should be set by theme."
+ );
+
+ Assert.equal(
+ window.getComputedStyle(findbar_textbox).color,
+ hexToCSS(TOOLBAR_FIELD_TEXT_COLOR),
+ "Findbar textbox text color should be set by theme."
+ );
+
+ await extension.unload();
+ }
+);
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js b/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js
new file mode 100644
index 0000000000..587c5d4efe
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js
@@ -0,0 +1,151 @@
+"use strict";
+
+// This test checks whether browser.theme.getCurrent() works correctly when theme
+// does not originate from extension querying the theme.
+const THEME = {
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+};
+
+add_task(async function test_getcurrent() {
+ const theme = ExtensionTestUtils.loadExtension(THEME);
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.theme.onUpdated.addListener(() => {
+ browser.theme.getCurrent().then(theme => {
+ browser.test.sendMessage("theme-updated", theme);
+ if (!theme?.images) {
+ return;
+ }
+
+ // Try to access the theme_frame image
+ fetch(theme.images.theme_frame)
+ .then(() => {
+ browser.test.sendMessage("theme-image", { success: true });
+ })
+ .catch(e => {
+ browser.test.sendMessage("theme-image", {
+ success: false,
+ error: e.message,
+ });
+ });
+ });
+ });
+ },
+ });
+
+ await extension.startup();
+
+ info("Testing getCurrent after static theme startup");
+ let updatedPromise = extension.awaitMessage("theme-updated");
+ let imageLoaded = extension.awaitMessage("theme-image");
+ await theme.startup();
+ let receivedTheme = await updatedPromise;
+ Assert.ok(
+ receivedTheme.images.theme_frame.includes("image1.png"),
+ "getCurrent returns correct theme_frame image"
+ );
+ Assert.equal(
+ receivedTheme.colors.frame,
+ ACCENT_COLOR,
+ "getCurrent returns correct frame color"
+ );
+ Assert.equal(
+ receivedTheme.colors.tab_background_text,
+ TEXT_COLOR,
+ "getCurrent returns correct tab_background_text color"
+ );
+ Assert.deepEqual(await imageLoaded, { success: true }, "theme image loaded");
+
+ info("Testing getCurrent after static theme unload");
+ updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.unload();
+ receivedTheme = await updatedPromise;
+ Assert.equal(
+ JSON.stringify({ colors: null, images: null, properties: null }),
+ JSON.stringify(receivedTheme),
+ "getCurrent returns empty theme"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_getcurrent_privateBrowsing() {
+ const theme = ExtensionTestUtils.loadExtension(THEME);
+
+ const extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ // We don't want the sidebar to automatically open on extension startup.
+ startupReason: "APP_STARTUP",
+ files: {
+ "sidebar.html": `<!DOCTYPE html>
+ <html>
+ <body>
+ Test Extension Sidebar
+ <script src="sidebar.js"></script>
+ </body>
+ </html>
+ `,
+ "sidebar.js": function () {
+ browser.theme.getCurrent().then(theme => {
+ if (!theme?.images) {
+ browser.test.fail(
+ `Missing expected images from theme.getCurrent result`
+ );
+ return;
+ }
+
+ // Try to access the theme_frame image
+ fetch(theme.images.theme_frame)
+ .then(() => {
+ browser.test.sendMessage("theme-image", { success: true });
+ })
+ .catch(e => {
+ browser.test.sendMessage("theme-image", {
+ success: false,
+ error: e.message,
+ });
+ });
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await theme.startup();
+
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ const { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+ );
+ const { makeWidgetId } = ExtensionCommon;
+ privateWin.SidebarUI.show(`${makeWidgetId(extension.id)}-sidebar-action`);
+
+ let imageLoaded = extension.awaitMessage("theme-image");
+ Assert.deepEqual(await imageLoaded, { success: true }, "theme image loaded");
+
+ await extension.unload();
+ await theme.unload();
+
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js b/toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js
new file mode 100644
index 0000000000..5a0d1c7a8d
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js
@@ -0,0 +1,63 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the color of the font and background in a selection are applied properly.
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+add_setup(async function () {
+ await gCUITestUtils.addSearchBar();
+ await gFindBarPromise;
+ registerCleanupFunction(() => {
+ gFindBar.close();
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function test_support_selection() {
+ const HIGHLIGHT_TEXT_COLOR = "#9400FF";
+ const HIGHLIGHT_COLOR = "#F89919";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ toolbar_field_highlight: HIGHLIGHT_COLOR,
+ toolbar_field_highlight_text: HIGHLIGHT_TEXT_COLOR,
+ },
+ },
+ },
+ });
+ await extension.startup();
+
+ let fields = [
+ gURLBar.inputField,
+ document.querySelector("#searchbar .searchbar-textbox"),
+ document.querySelector(".findbar-textbox"),
+ ].filter(field => {
+ let bounds = field.getBoundingClientRect();
+ return bounds.width > 0 && bounds.height > 0;
+ });
+
+ Assert.equal(fields.length, 3, "Should be testing three elements");
+
+ info(
+ `Checking background colors and colors for ${fields.length} toolbar input fields.`
+ );
+ for (let field of fields) {
+ info(`Testing ${field.id || field.className}`);
+ field.focus();
+ Assert.equal(
+ window.getComputedStyle(field, "::selection").backgroundColor,
+ hexToCSS(HIGHLIGHT_COLOR),
+ "Input selection background should be set."
+ );
+ Assert.equal(
+ window.getComputedStyle(field, "::selection").color,
+ hexToCSS(HIGHLIGHT_TEXT_COLOR),
+ "Input selection color should be set."
+ );
+ }
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js b/toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js
new file mode 100644
index 0000000000..d9beb0f9a8
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js
@@ -0,0 +1,77 @@
+"use strict";
+
+add_task(async function test_theme_incognito_not_allowed() {
+ let windowExtension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ async background() {
+ const theme = {
+ colors: {
+ frame: "black",
+ tab_background_text: "black",
+ },
+ };
+ let window = await browser.windows.create({ incognito: true });
+ browser.test.onMessage.addListener(async message => {
+ if (message == "update") {
+ browser.theme.update(window.id, theme);
+ return;
+ }
+ await browser.windows.remove(window.id);
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("ready", window.id);
+ },
+ manifest: {
+ permissions: ["theme"],
+ },
+ });
+ await windowExtension.startup();
+ let wId = await windowExtension.awaitMessage("ready");
+
+ async function background(windowId) {
+ const theme = {
+ colors: {
+ frame: "black",
+ tab_background_text: "black",
+ },
+ };
+
+ browser.theme.onUpdated.addListener(info => {
+ browser.test.log("got theme onChanged");
+ browser.test.fail("theme");
+ });
+ await browser.test.assertRejects(
+ browser.theme.getCurrent(windowId),
+ /Invalid window ID/,
+ "API should reject getting window theme"
+ );
+ await browser.test.assertRejects(
+ browser.theme.update(windowId, theme),
+ /Invalid window ID/,
+ "API should reject updating theme"
+ );
+ await browser.test.assertRejects(
+ browser.theme.reset(windowId),
+ /Invalid window ID/,
+ "API should reject reseting theme on window"
+ );
+
+ browser.test.sendMessage("start");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})(${wId})`,
+ manifest: {
+ permissions: ["theme"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("start");
+ windowExtension.sendMessage("update");
+
+ windowExtension.sendMessage("close");
+ await windowExtension.awaitMessage("done");
+ await windowExtension.unload();
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js b/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
new file mode 100644
index 0000000000..4293e5eb19
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
@@ -0,0 +1,69 @@
+"use strict";
+
+const DEFAULT_THEME_BG_COLOR = "rgb(255, 255, 255)";
+const DEFAULT_THEME_TEXT_COLOR = "rgb(0, 0, 0)";
+
+add_task(async function test_deprecated_LWT_properties_ignored() {
+ // This test uses deprecated theme properties, so warnings are expected.
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ headerURL: "image1.png",
+ },
+ colors: {
+ accentcolor: ACCENT_COLOR,
+ textcolor: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let docStyle = window.getComputedStyle(docEl);
+ let navigatorStyle = window.getComputedStyle(
+ docEl.querySelector("#navigator-toolbox")
+ );
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.ok(
+ !docEl.hasAttribute("lwtheme-image"),
+ "LWT image attribute should not be set on deprecated headerURL alias"
+ );
+ Assert.ok(
+ !docEl.getAttribute("lwtheme-brighttext"),
+ "LWT text color attribute should not be set on deprecated textcolor alias"
+ );
+
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.backgroundColor,
+ DEFAULT_THEME_BG_COLOR,
+ "Expected default theme background color"
+ );
+ } else {
+ Assert.equal(
+ navigatorStyle.backgroundColor,
+ DEFAULT_THEME_BG_COLOR,
+ "Expected default theme background color"
+ );
+ }
+
+ Assert.equal(
+ docStyle.color,
+ DEFAULT_THEME_TEXT_COLOR,
+ "Expected default theme text color"
+ );
+
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js b/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js
new file mode 100644
index 0000000000..58088ce6a0
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js
@@ -0,0 +1,287 @@
+"use strict";
+
+add_task(async function test_support_backgrounds_position() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "face1.png",
+ additional_backgrounds: ["face2.png", "face2.png", "face2.png"],
+ },
+ colors: {
+ frame: `rgb(${FRAME_COLOR.join(",")})`,
+ tab_background_text: `rgb(${TAB_BACKGROUND_TEXT_COLOR.join(",")})`,
+ },
+ properties: {
+ additional_backgrounds_alignment: [
+ "left top",
+ "center top",
+ "right bottom",
+ ],
+ },
+ },
+ },
+ files: {
+ "face1.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ "face2.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let toolbox = document.querySelector("#navigator-toolbox");
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ "true",
+ "LWT text color attribute should be set"
+ );
+
+ let toolboxCS = window.getComputedStyle(toolbox);
+ let toolboxBgImage = toolboxCS.backgroundImage.split(",")[0].trim();
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ let rootBgImage = rootCS.backgroundImage.split(",")[0].trim();
+ Assert.ok(
+ rootBgImage.includes("face1.png"),
+ `The backgroundImage should use face1.png. Actual value is: ${rootBgImage}`
+ );
+ Assert.equal(
+ toolboxCS.backgroundImage,
+ Array(3).fill(toolboxBgImage).join(", "),
+ "The backgroundImage should use face2.png three times."
+ );
+ Assert.equal(
+ toolboxCS.backgroundPosition,
+ "0% 0%, 50% 0%, 100% 100%",
+ "The backgroundPosition should use the three values provided."
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.backgroundImage,
+ [1, 2, 2, 2]
+ .map(num => toolboxBgImage.replace(/face[\d]*/, `face${num}`))
+ .join(", "),
+ "The backgroundImage should use face1.png once and face2.png three times."
+ );
+ Assert.equal(
+ toolboxCS.backgroundPosition,
+ "100% 0%, 0% 0%, 50% 0%, 100% 100%",
+ "The backgroundPosition should use the three values provided, preceded by the default for theme_frame."
+ );
+ /**
+ * We expect duplicate background-repeat values because we apply `no-repeat`
+ * once for theme_frame, and again as the default value of
+ * --lwt-background-tiling.
+ */
+ Assert.equal(
+ toolboxCS.backgroundRepeat,
+ "no-repeat, no-repeat",
+ "The backgroundPosition should use the default value."
+ );
+ }
+
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+ toolboxCS = window.getComputedStyle(toolbox);
+
+ // Styles should've reverted to their initial values.
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(rootCS.backgroundImage, "none");
+ Assert.equal(rootCS.backgroundPosition, "0% 0%");
+ Assert.equal(rootCS.backgroundRepeat, "repeat");
+ }
+ Assert.equal(toolboxCS.backgroundImage, "none");
+ Assert.equal(toolboxCS.backgroundPosition, "0% 0%");
+ Assert.equal(toolboxCS.backgroundRepeat, "repeat");
+});
+
+add_task(async function test_support_backgrounds_repeat() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "face0.png",
+ additional_backgrounds: ["face1.png", "face2.png", "face3.png"],
+ },
+ colors: {
+ frame: FRAME_COLOR,
+ tab_background_text: TAB_BACKGROUND_TEXT_COLOR,
+ },
+ properties: {
+ additional_backgrounds_tiling: ["repeat-x", "repeat-y", "repeat"],
+ },
+ },
+ },
+ files: {
+ "face0.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ "face1.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ "face2.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ "face3.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let toolbox = document.querySelector("#navigator-toolbox");
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ "true",
+ "LWT text color attribute should be set"
+ );
+
+ let toolboxCS = window.getComputedStyle(toolbox);
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ let rootImage = rootCS.backgroundImage.split(",")[0].trim();
+ Assert.ok(
+ rootImage.includes("face0.png"),
+ `The backgroundImage should use face.png. Actual value is: ${rootImage}`
+ );
+ Assert.equal(
+ [1, 2, 3]
+ .map(num => rootImage.replace(/face[\d]*/, `face${num}`))
+ .join(", "),
+ toolboxCS.backgroundImage,
+ "The backgroundImage should use face.png three times."
+ );
+ Assert.equal(
+ rootCS.backgroundPosition,
+ "100% 0%, 100% 0%",
+ "The backgroundPosition should use the default value for root."
+ );
+ Assert.equal(
+ toolboxCS.backgroundPosition,
+ "100% 0%",
+ "The backgroundPosition should use the default value for navigator-toolbox."
+ );
+ Assert.equal(
+ rootCS.backgroundRepeat,
+ "no-repeat, repeat-x, repeat-y, repeat",
+ "The backgroundRepeat should use the default values for root."
+ );
+ Assert.equal(
+ toolboxCS.backgroundRepeat,
+ "repeat-x, repeat-y, repeat",
+ "The backgroundRepeat should use the three values provided for navigator-toolbox."
+ );
+ } else {
+ let toolboxImage = toolboxCS.backgroundImage.split(",")[0].trim();
+ Assert.equal(
+ [0, 1, 2, 3]
+ .map(num => toolboxImage.replace(/face[\d]*/, `face${num}`))
+ .join(", "),
+ toolboxCS.backgroundImage,
+ "The backgroundImage should use face.png four times."
+ );
+ /**
+ * We expect duplicate background-position values because we apply `right top`
+ * once for theme_frame, and again as the default value of
+ * --lwt-background-alignment.
+ */
+ Assert.equal(
+ toolboxCS.backgroundPosition,
+ "100% 0%, 100% 0%",
+ "The backgroundPosition should use the default value for navigator-toolbox."
+ );
+ Assert.equal(
+ toolboxCS.backgroundRepeat,
+ "no-repeat, repeat-x, repeat-y, repeat",
+ "The backgroundRepeat should use the three values provided for --lwt-background-tiling, preceeded by the default for theme_frame."
+ );
+ }
+
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
+
+add_task(async function test_additional_images_check() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "face.png",
+ },
+ colors: {
+ frame: FRAME_COLOR,
+ tab_background_text: TAB_BACKGROUND_TEXT_COLOR,
+ },
+ properties: {
+ additional_backgrounds_tiling: ["repeat-x", "repeat-y", "repeat"],
+ },
+ },
+ },
+ files: {
+ "face.png": imageBufferFromDataURI(ENCODED_IMAGE_DATA),
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let toolbox = document.querySelector("#navigator-toolbox");
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ "true",
+ "LWT text color attribute should be set"
+ );
+
+ let toolboxCS = window.getComputedStyle(toolbox);
+ if (backgroundColorSetOnRoot()) {
+ let rootCS = window.getComputedStyle(docEl);
+ let bgImage = rootCS.backgroundImage.split(",")[0].trim();
+ Assert.ok(
+ bgImage.includes("face.png"),
+ `The backgroundImage should use face.png. Actual value is: ${bgImage}`
+ );
+ Assert.equal(
+ "none",
+ toolboxCS.backgroundImage,
+ "The backgroundImage should not use face.png."
+ );
+ Assert.equal(
+ rootCS.backgroundPosition,
+ "100% 0%, 100% 0%",
+ "The backgroundPosition should use the default value."
+ );
+ Assert.equal(
+ rootCS.backgroundRepeat,
+ "no-repeat, no-repeat",
+ "The backgroundPosition should use only one (default) value for the header and the default additional images."
+ );
+ } else {
+ let bgImage = toolboxCS.backgroundImage.split(",")[0].trim();
+ Assert.ok(
+ bgImage.includes("face.png"),
+ `The backgroundImage should use face.png. Actual value is: ${bgImage}`
+ );
+ Assert.ok(
+ bgImage.includes("face.png"),
+ `The backgroundImage should use face.png. Actual value is: ${bgImage}`
+ );
+ Assert.equal(
+ toolboxCS.backgroundPosition,
+ "100% 0%, 100% 0%",
+ "The backgroundPosition should use the default value."
+ );
+ Assert.equal(
+ toolboxCS.backgroundRepeat,
+ "no-repeat, no-repeat",
+ "The backgroundRepeat should use the default value."
+ );
+ }
+
+ await extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js
new file mode 100644
index 0000000000..8e2f5446c9
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js
@@ -0,0 +1,203 @@
+"use strict";
+// This test checks whether the new tab page color properties work.
+
+function waitForAboutNewTabReady(browser, url) {
+ // Stop-gap fix for https://bugzilla.mozilla.org/show_bug.cgi?id=1697196#c24
+ return SpecialPowers.spawn(browser, [url], async url => {
+ let doc = content.document;
+ await ContentTaskUtils.waitForCondition(
+ () => doc.querySelector(".outer-wrapper"),
+ `Waiting for page wrapper to be initialized at ${url}`
+ );
+ });
+}
+
+/**
+ * Test whether the selected browser has the new tab page theme applied
+ *
+ * @param {object} theme that is applied
+ * @param {boolean} isBrightText whether the brighttext attribute should be set
+ */
+async function test_ntp_theme(theme, isBrightText) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme,
+ },
+ });
+
+ let browser = gBrowser.selectedBrowser;
+
+ let { originalBackground, originalCardBackground, originalColor } =
+ await SpecialPowers.spawn(browser, [], function () {
+ let doc = content.document;
+ ok(
+ !doc.documentElement.hasAttribute("lwt-newtab"),
+ "New tab page should not have lwt-newtab attribute"
+ );
+ ok(
+ !doc.documentElement.hasAttribute("lwt-newtab-brighttext"),
+ `New tab page should not have lwt-newtab-brighttext attribute`
+ );
+
+ return {
+ originalBackground: content.getComputedStyle(doc.body).backgroundColor,
+ originalCardBackground: content.getComputedStyle(
+ doc.querySelector(".top-site-outer .tile")
+ ).backgroundColor,
+ originalColor: content.getComputedStyle(
+ doc.querySelector(".outer-wrapper")
+ ).color,
+ // We check the value of --newtab-link-primary-color directly because the
+ // elements on which it is applied are hard to test. It is most visible in
+ // the "learn more" link in the Pocket section. We cannot show the Pocket
+ // section since it hits the network, and the usual workarounds to change
+ // its backend only work in browser/. This variable is also used in
+ // the Edit Top Site modal, but showing/hiding that is very verbose and
+ // would make this test almost unreadable.
+ originalLinks: content
+ .getComputedStyle(doc.documentElement)
+ .getPropertyValue("--newtab-link-primary-color"),
+ };
+ });
+
+ await extension.startup();
+
+ Services.ppmm.sharedData.flush();
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ isBrightText,
+ background: hexToCSS(theme.colors.ntp_background),
+ card_background: hexToCSS(theme.colors.ntp_card_background),
+ color: hexToCSS(theme.colors.ntp_text),
+ },
+ ],
+ async function ({ isBrightText, background, card_background, color }) {
+ let doc = content.document;
+ ok(
+ doc.documentElement.hasAttribute("lwt-newtab"),
+ "New tab page should have lwt-newtab attribute"
+ );
+ is(
+ doc.documentElement.hasAttribute("lwt-newtab-brighttext"),
+ isBrightText,
+ `New tab page should${
+ !isBrightText ? " not" : ""
+ } have lwt-newtab-brighttext attribute`
+ );
+
+ is(
+ content.getComputedStyle(doc.body).backgroundColor,
+ background,
+ "New tab page background should be set."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".top-site-outer .tile"))
+ .backgroundColor,
+ card_background,
+ "New tab page card background should be set."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".outer-wrapper")).color,
+ color,
+ "New tab page text color should be set."
+ );
+ }
+ );
+
+ await extension.unload();
+
+ Services.ppmm.sharedData.flush();
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ originalBackground,
+ originalCardBackground,
+ originalColor,
+ },
+ ],
+ function ({ originalBackground, originalCardBackground, originalColor }) {
+ let doc = content.document;
+ ok(
+ !doc.documentElement.hasAttribute("lwt-newtab"),
+ "New tab page should not have lwt-newtab attribute"
+ );
+ ok(
+ !doc.documentElement.hasAttribute("lwt-newtab-brighttext"),
+ `New tab page should not have lwt-newtab-brighttext attribute`
+ );
+
+ is(
+ content.getComputedStyle(doc.body).backgroundColor,
+ originalBackground,
+ "New tab page background should be reset."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".top-site-outer .tile"))
+ .backgroundColor,
+ originalCardBackground,
+ "New tab page card background should be reset."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".outer-wrapper")).color,
+ originalColor,
+ "New tab page text color should be reset."
+ );
+ }
+ );
+}
+
+add_task(async function test_support_ntp_colors() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // BrowserTestUtils.withNewTab waits for about:newtab to load
+ // so we disable preloading before running the test.
+ ["browser.newtab.preload", false],
+ // Force prefers-color-scheme to "light", as otherwise it might be
+ // derived from the theme, but we hard-code the light styles on this
+ // test.
+ ["layout.css.prefers-color-scheme.content-override", 1],
+ // Override the system color scheme to light so this test passes on
+ // machines with dark system color scheme.
+ ["ui.systemUsesDarkTheme", 0],
+ ],
+ });
+ NewTabPagePreloading.removePreloadedBrowser(window);
+ for (let url of ["about:newtab", "about:home"]) {
+ info("Opening url: " + url);
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => {
+ await waitForAboutNewTabReady(browser, url);
+ await test_ntp_theme(
+ {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ ntp_background: "#add8e6",
+ ntp_card_background: "#ffffff",
+ ntp_text: "#00008b",
+ },
+ },
+ false,
+ url
+ );
+
+ await test_ntp_theme(
+ {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ ntp_background: "#00008b",
+ ntp_card_background: "#000000",
+ ntp_text: "#add8e6",
+ },
+ },
+ true,
+ url
+ );
+ });
+ }
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js
new file mode 100644
index 0000000000..9d28cf50c8
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js
@@ -0,0 +1,240 @@
+"use strict";
+
+// This test checks whether the new tab page color properties work per-window.
+
+function waitForAboutNewTabReady(browser, url) {
+ // Stop-gap fix for https://bugzilla.mozilla.org/show_bug.cgi?id=1697196#c24
+ return SpecialPowers.spawn(browser, [url], async url => {
+ let doc = content.document;
+ await ContentTaskUtils.waitForCondition(
+ () => doc.querySelector(".outer-wrapper"),
+ `Waiting for page wrapper to be initialized at ${url}`
+ );
+ });
+}
+
+/**
+ * Test whether a given browser has the new tab page theme applied
+ *
+ * @param {object} browser to test against
+ * @param {object} theme that is applied
+ * @param {boolean} isBrightText whether the brighttext attribute should be set
+ * @returns {Promise} The task as a promise
+ */
+function test_ntp_theme(browser, theme, isBrightText) {
+ Services.ppmm.sharedData.flush();
+ return SpecialPowers.spawn(
+ browser,
+ [
+ {
+ isBrightText,
+ background: hexToCSS(theme.colors.ntp_background),
+ card_background: hexToCSS(theme.colors.ntp_card_background),
+ color: hexToCSS(theme.colors.ntp_text),
+ },
+ ],
+ function ({ isBrightText, background, card_background, color }) {
+ let doc = content.document;
+ ok(
+ doc.documentElement.hasAttribute("lwt-newtab"),
+ "New tab page should have lwt-newtab attribute"
+ );
+ is(
+ doc.documentElement.hasAttribute("lwt-newtab-brighttext"),
+ isBrightText,
+ `New tab page should${
+ !isBrightText ? " not" : ""
+ } have lwt-newtab-brighttext attribute`
+ );
+
+ is(
+ content.getComputedStyle(doc.body).backgroundColor,
+ background,
+ "New tab page background should be set."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".top-site-outer .tile"))
+ .backgroundColor,
+ card_background,
+ "New tab page card background should be set."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".outer-wrapper")).color,
+ color,
+ "New tab page text color should be set."
+ );
+ }
+ );
+}
+
+/**
+ * Test whether a given browser has the default theme applied
+ *
+ * @param {object} browser to test against
+ * @param {string} url being tested
+ * @returns {Promise} The task as a promise
+ */
+function test_ntp_default_theme(browser, url) {
+ Services.ppmm.sharedData.flush();
+ return SpecialPowers.spawn(
+ browser,
+ [
+ {
+ background: hexToCSS("#F9F9FB"),
+ color: hexToCSS("#15141A"),
+ },
+ ],
+ function ({ background, color }) {
+ let doc = content.document;
+ ok(
+ !doc.documentElement.hasAttribute("lwt-newtab"),
+ "New tab page should not have lwt-newtab attribute"
+ );
+ ok(
+ !doc.documentElement.hasAttribute("lwt-newtab-brighttext"),
+ `New tab page should not have lwt-newtab-brighttext attribute`
+ );
+
+ is(
+ content.getComputedStyle(doc.body).backgroundColor,
+ background,
+ "New tab page background should be reset."
+ );
+ is(
+ content.getComputedStyle(doc.querySelector(".outer-wrapper")).color,
+ color,
+ "New tab page text color should be reset."
+ );
+ }
+ );
+}
+
+add_task(async function test_per_window_ntp_theme() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ async background() {
+ function promiseWindowChecked() {
+ return new Promise(resolve => {
+ let listener = msg => {
+ if (msg == "checked-window") {
+ browser.test.onMessage.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.test.onMessage.addListener(listener);
+ });
+ }
+
+ function removeWindow(winId) {
+ return new Promise(resolve => {
+ let listener = removedWinId => {
+ if (removedWinId == winId) {
+ browser.windows.onRemoved.removeListener(listener);
+ resolve();
+ }
+ };
+ browser.windows.onRemoved.addListener(listener);
+ browser.windows.remove(winId);
+ });
+ }
+
+ async function checkWindow(theme, isBrightText, winId) {
+ let windowChecked = promiseWindowChecked();
+ browser.test.sendMessage("check-window", {
+ theme,
+ isBrightText,
+ winId,
+ });
+ await windowChecked;
+ }
+
+ const darkTextTheme = {
+ colors: {
+ frame: "#add8e6",
+ tab_background_text: "#000",
+ ntp_background: "#add8e6",
+ ntp_card_background: "#ff0000",
+ ntp_text: "#000",
+ },
+ };
+
+ const brightTextTheme = {
+ colors: {
+ frame: "#00008b",
+ tab_background_text: "#add8e6",
+ ntp_background: "#00008b",
+ ntp_card_background: "#00ff00",
+ ntp_text: "#add8e6",
+ },
+ };
+
+ let { id: winId } = await browser.windows.getCurrent();
+ // We are opening about:blank instead of the default homepage,
+ // because using the default homepage results in intermittent
+ // test failures on debug builds due to browser window leaks.
+ // A side effect of testing on about:blank is that
+ // test_ntp_default_theme cannot test properties used only on
+ // about:newtab, like ntp_card_background.
+ let { id: secondWinId } = await browser.windows.create({
+ url: "about:blank",
+ });
+
+ browser.test.log("Test that single window update works");
+ await browser.theme.update(winId, darkTextTheme);
+ await checkWindow(darkTextTheme, false, winId);
+ await checkWindow(null, false, secondWinId);
+
+ browser.test.log("Test that applying different themes on both windows");
+ await browser.theme.update(secondWinId, brightTextTheme);
+ await checkWindow(darkTextTheme, false, winId);
+ await checkWindow(brightTextTheme, true, secondWinId);
+
+ browser.test.log("Test resetting the theme on one window");
+ await browser.theme.reset(winId);
+ await checkWindow(null, false, winId);
+ await checkWindow(brightTextTheme, true, secondWinId);
+
+ await removeWindow(secondWinId);
+ await checkWindow(null, false, winId);
+ browser.test.notifyPass("perwindow-ntp-theme");
+ },
+ });
+
+ extension.onMessage(
+ "check-window",
+ async ({ theme, isBrightText, winId }) => {
+ let win = Services.wm.getOuterWindowWithId(winId);
+ win.NewTabPagePreloading.removePreloadedBrowser(win);
+ // These pages were initially chosen because LightweightThemeChild.sys.mjs
+ // treats them specially.
+ for (let url of ["about:newtab", "about:home"]) {
+ info("Opening url: " + url);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url },
+ async browser => {
+ await waitForAboutNewTabReady(browser, url);
+ if (theme) {
+ await test_ntp_theme(browser, theme, isBrightText);
+ } else {
+ await test_ntp_default_theme(browser, url);
+ }
+ }
+ );
+ }
+ extension.sendMessage("checked-window");
+ }
+ );
+
+ // BrowserTestUtils.withNewTab waits for about:newtab to load
+ // so we disable preloading before running the test.
+ await SpecialPowers.setBoolPref("browser.newtab.preload", false);
+ registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("browser.newtab.preload");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("perwindow-ntp-theme");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js
new file mode 100644
index 0000000000..f830e55294
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js
@@ -0,0 +1,439 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/**
+ * Tests that we apply dark theme variants to PBM windows where applicable.
+ */
+
+const { BuiltInThemes } = ChromeUtils.importESModule(
+ "resource:///modules/BuiltInThemes.sys.mjs"
+);
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+const IS_LINUX = AppConstants.platform == "linux";
+
+const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org";
+const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
+
+// This tests opens many chrome windows which is slow on debug builds.
+requestLongerTimeout(2);
+
+/**
+ * Test a window's theme color scheme.
+ *
+ * @param {*} options - Test options.
+ * @param {Window} options.win - Window object to test.
+ * @param {boolean} options.colorScheme - Whether expected chrome color scheme
+ * is dark (true) or light (false).
+ * @param {boolean} options.expectLWTAttributes - Whether the window should
+ * have the LWT attributes set matching the color scheme.
+ * @param {boolean} options.expectDefaultDarkAttribute - Whether the window
+ * should have the "lwt-default-theme-in-dark-mode" attribute.
+ */
+async function testWindowColorScheme({
+ win,
+ expectDark,
+ expectLWTAttributes,
+ expectDefaultDarkAttribute,
+}) {
+ let docEl = win.document.documentElement;
+
+ is(
+ docEl.hasAttribute("lwt-default-theme-in-dark-mode"),
+ expectDefaultDarkAttribute,
+ `Window should${
+ expectDefaultDarkAttribute ? "" : " not"
+ } have lwt-default-theme-in-dark-mode attribute.`
+ );
+
+ if (expectLWTAttributes) {
+ ok(docEl.hasAttribute("lwtheme"), "Window should have LWT attribute.");
+ is(
+ docEl.getAttribute("lwtheme-brighttext"),
+ expectDark ? "true" : null,
+ "LWT text color attribute should be set."
+ );
+ } else {
+ ok(!docEl.hasAttribute("lwtheme"), "Window should not have LWT attribute.");
+ ok(
+ !docEl.hasAttribute("lwtheme-brighttext"),
+ "LWT text color attribute should not be set."
+ );
+ }
+}
+
+/**
+ * Match the prefers-color-scheme media query and return the results.
+ *
+ * @param {object} options
+ * @param {Window} options.win - If chrome=true, window to test, otherwise
+ * parent window of the content window to test.
+ * @param {boolean} options.chrome - If true the media queries will be matched
+ * against the supplied chrome window. Otherwise they will be matched against
+ * the content window.
+ * @returns {Promise<{light: boolean, dark: boolean}>} - Resolves with an
+ * object of the media query results.
+ */
+function getPrefersColorSchemeInfo({ win, chrome = false }) {
+ let fn = async windowObj => {
+ // If called in the parent, we use the supplied win object. Otherwise use
+ // the content window global.
+ let win = windowObj || content;
+
+ // LookAndFeel updates are async.
+ await new Promise(resolve => {
+ win.requestAnimationFrame(() => win.requestAnimationFrame(resolve));
+ });
+ return {
+ light: win.matchMedia("(prefers-color-scheme: light)").matches,
+ dark: win.matchMedia("(prefers-color-scheme: dark)").matches,
+ };
+ };
+
+ if (chrome) {
+ return fn(win);
+ }
+
+ return SpecialPowers.spawn(win.gBrowser.selectedBrowser, [], fn);
+}
+
+add_setup(async function () {
+ // Set system theme to light to ensure consistency across test machines.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.theme.dark-private-windows", true],
+ ["ui.systemUsesDarkTheme", 0],
+ ],
+ });
+ // Ensure the built-in themes are initialized.
+ await BuiltInThemes.ensureBuiltInThemes();
+
+ // The previous test, browser_ext_themes_ntp_colors.js has side effects.
+ // Switch to a theme, then switch back to the default theme to reach a
+ // consistent themeData state. Without this, themeData in
+ // LightWeightConsumer#_update does not contain darkTheme data and PBM windows
+ // don't get themed correctly.
+ let lightTheme = await AddonManager.getAddonByID(LIGHT_THEME_ID);
+ await lightTheme.enable();
+ await lightTheme.disable();
+});
+
+// For the default theme with light color scheme, private browsing windows
+// should be themed dark.
+// The PBM window's content should not be themed dark.
+add_task(async function test_default_theme_light() {
+ info("Normal browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: false,
+ expectLWTAttributes: false,
+ expectDefaultDarkAttribute: false,
+ });
+
+ let windowB = await BrowserTestUtils.openNewBrowserWindow();
+
+ info("Additional normal browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: windowB,
+ expectDark: false,
+ expectLWTAttributes: false,
+ expectDefaultDarkAttribute: false,
+ });
+
+ let pbmWindowA = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info("Private browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindowA,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: true,
+ });
+
+ let prefersColorScheme = await getPrefersColorSchemeInfo({ win: pbmWindowA });
+ ok(
+ prefersColorScheme.light && !prefersColorScheme.dark,
+ "Content of dark themed PBM window should still be themed light"
+ );
+
+ let pbmWindowB = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ info("Additional private browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindowB,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: true,
+ });
+
+ await BrowserTestUtils.closeWindow(windowB);
+ await BrowserTestUtils.closeWindow(pbmWindowA);
+ await BrowserTestUtils.closeWindow(pbmWindowB);
+});
+
+// For the default theme with dark color scheme, normal and private browsing
+// windows should be themed dark.
+add_task(async function test_default_theme_dark() {
+ // Set the system theme to dark. The default theme will follow this color
+ // scheme.
+ await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] });
+
+ info("Normal browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: true,
+ expectLWTAttributes: !IS_LINUX,
+ expectDefaultDarkAttribute: !IS_LINUX,
+ });
+
+ let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info("Private browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindow,
+ expectDark: true,
+ expectLWTAttributes: !IS_LINUX,
+ expectDefaultDarkAttribute: !IS_LINUX,
+ });
+
+ await BrowserTestUtils.closeWindow(pbmWindow);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// For the light theme both normal and private browsing windows should have a
+// bright color scheme applied.
+add_task(async function test_light_theme_builtin() {
+ let lightTheme = await AddonManager.getAddonByID(LIGHT_THEME_ID);
+ await lightTheme.enable();
+
+ info("Normal browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: false,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ info("Private browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindow,
+ expectDark: false,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ await BrowserTestUtils.closeWindow(pbmWindow);
+ await lightTheme.disable();
+});
+
+// For the dark theme both normal and private browsing should have a dark color
+// scheme applied.
+add_task(async function test_dark_theme_builtin() {
+ let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID);
+ await darkTheme.enable();
+
+ info("Normal browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info("Private browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindow,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ await BrowserTestUtils.closeWindow(pbmWindow);
+ await darkTheme.disable();
+});
+
+// When switching between default, light and dark theme the private browsing
+// window color scheme should update accordingly.
+add_task(async function test_theme_switch_updates_existing_pbm_win() {
+ let windowB = await BrowserTestUtils.openNewBrowserWindow();
+ let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info("Normal browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: false,
+ expectLWTAttributes: false,
+ expectDefaultDarkAttribute: false,
+ });
+
+ info("Private browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindow,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: true,
+ });
+
+ info("Enabling light theme.");
+ let lightTheme = await AddonManager.getAddonByID(LIGHT_THEME_ID);
+ await lightTheme.enable();
+
+ info("Normal browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: false,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ info("Private browsing window should not be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindow,
+ expectDark: false,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ await lightTheme.disable();
+
+ info("Enabling dark theme.");
+ let darkTheme = await AddonManager.getAddonByID(DARK_THEME_ID);
+ await darkTheme.enable();
+
+ info("Normal browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: window,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ info("Private browsing window should be in dark mode.");
+ await testWindowColorScheme({
+ win: pbmWindow,
+ expectDark: true,
+ expectLWTAttributes: true,
+ expectDefaultDarkAttribute: false,
+ });
+
+ await darkTheme.disable();
+
+ await BrowserTestUtils.closeWindow(windowB);
+ await BrowserTestUtils.closeWindow(pbmWindow);
+});
+
+// pageInfo windows should inherit the PBM window dark theme.
+add_task(async function test_pbm_dark_page_info() {
+ for (let isPBM of [false, true]) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPBM,
+ });
+ let windowTypeStr = isPBM ? "private" : "normal";
+
+ info(`Opening pageInfo from ${windowTypeStr} browsing.`);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com" },
+ async () => {
+ let pageInfo = win.BrowserPageInfo(null, "securityTab");
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let prefersColorScheme = await getPrefersColorSchemeInfo({
+ win: pageInfo,
+ chrome: true,
+ });
+ if (isPBM) {
+ ok(
+ !prefersColorScheme.light && prefersColorScheme.dark,
+ "pageInfo from private window should be themed dark."
+ );
+ } else {
+ ok(
+ prefersColorScheme.light && !prefersColorScheme.dark,
+ "pageInfo from normal window should be themed light."
+ );
+ }
+
+ pageInfo.close();
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+// Prompts should inherit the PBM window dark theme.
+add_task(async function test_pbm_dark_prompts() {
+ const { MODAL_TYPE_TAB, MODAL_TYPE_CONTENT } = Services.prompt;
+
+ for (let isPBM of [false, true]) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPBM,
+ });
+
+ // TODO: Once Bug 1751953 has been fixed, we can also test MODAL_TYPE_WINDOW
+ // here.
+ for (let modalType of [MODAL_TYPE_TAB, MODAL_TYPE_CONTENT]) {
+ let windowTypeStr = isPBM ? "private" : "normal";
+ let modalTypeStr = modalType == MODAL_TYPE_TAB ? "tab" : "content";
+
+ info(`Opening ${modalTypeStr} prompt from ${windowTypeStr} browsing.`);
+
+ let openPromise = PromptTestUtils.waitForPrompt(
+ win.gBrowser.selectedBrowser,
+ {
+ modalType,
+ promptType: "alert",
+ }
+ );
+ let promptPromise = Services.prompt.asyncAlert(
+ win.gBrowser.selectedBrowser.browsingContext,
+ modalType,
+ "Hello",
+ "Hello, world!"
+ );
+
+ let dialog = await openPromise;
+
+ let prefersColorScheme = await getPrefersColorSchemeInfo({
+ win: dialog.ui.prompt,
+ chrome: true,
+ });
+
+ if (isPBM) {
+ ok(
+ !prefersColorScheme.light && prefersColorScheme.dark,
+ "Prompt from private window should be themed dark."
+ );
+ } else {
+ ok(
+ prefersColorScheme.light && !prefersColorScheme.dark,
+ "Prompt from normal window should be themed light."
+ );
+ }
+
+ await PromptTestUtils.handlePrompt(dialog);
+ await promptPromise;
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js b/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js
new file mode 100644
index 0000000000..526c3a0883
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js
@@ -0,0 +1,64 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes are persisted and applied
+// on newly opened windows.
+
+add_task(async function test_multiple_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = window.document.documentElement;
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let computedStyle = window.getComputedStyle(
+ backgroundColorSetOnRoot() ? docEl : toolbox
+ );
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ "true",
+ "LWT text color attribute should be set"
+ );
+ Assert.ok(
+ computedStyle.backgroundImage.includes("image1.png"),
+ "Expected background image"
+ );
+
+ // Now we'll open a new window to see if the theme is also applied there.
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+ docEl = window2.document.documentElement;
+ toolbox = window2.document.querySelector("#navigator-toolbox");
+ computedStyle = window.getComputedStyle(
+ backgroundColorSetOnRoot() ? docEl : toolbox
+ );
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(
+ docEl.getAttribute("lwtheme-brighttext"),
+ "true",
+ "LWT text color attribute should be set"
+ );
+ Assert.ok(
+ computedStyle.backgroundImage.includes("image1.png"),
+ "Expected background image"
+ );
+
+ await BrowserTestUtils.closeWindow(window2);
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_reset.js b/toolkit/components/extensions/test/browser/browser_ext_themes_reset.js
new file mode 100644
index 0000000000..d8b3b14073
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_reset.js
@@ -0,0 +1,112 @@
+"use strict";
+
+add_task(async function theme_reset_global_static_theme() {
+ let global_theme_extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: "#123456",
+ tab_background_text: "#fedcba",
+ },
+ },
+ },
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ async background() {
+ await browser.theme.reset();
+ let theme_after_reset = await browser.theme.getCurrent();
+
+ browser.test.assertEq(
+ "#123456",
+ theme_after_reset.colors.frame,
+ "Theme from other extension should not be cleared upon reset()"
+ );
+
+ let theme = {
+ colors: {
+ frame: "#CF723F",
+ },
+ };
+
+ await browser.theme.update(theme);
+ await browser.theme.reset();
+ let final_reset_theme = await browser.theme.getCurrent();
+
+ browser.test.assertEq(
+ JSON.stringify({ colors: null, images: null, properties: null }),
+ JSON.stringify(final_reset_theme),
+ "Should reset when extension had replaced the global theme"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await global_theme_extension.startup();
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ await global_theme_extension.unload();
+ await extension.unload();
+});
+
+add_task(async function theme_reset_by_windowId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ async background() {
+ let theme = {
+ colors: {
+ frame: "#CF723F",
+ },
+ };
+
+ let { id: winId } = await browser.windows.getCurrent();
+ await browser.theme.update(winId, theme);
+ let update_theme = await browser.theme.getCurrent(winId);
+
+ browser.test.onMessage.addListener(async () => {
+ let current_theme = await browser.theme.getCurrent(winId);
+ browser.test.assertEq(
+ update_theme.colors.frame,
+ current_theme.colors.frame,
+ "Should not be reset by a reset(windowId) call from another extension"
+ );
+
+ browser.test.sendMessage("done");
+ });
+
+ browser.test.sendMessage("ready", winId);
+ },
+ });
+
+ let anotherExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async winId => {
+ await browser.theme.reset(winId);
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ await extension.startup();
+ let winId = await extension.awaitMessage("ready");
+
+ await anotherExtension.startup();
+
+ // theme.reset should be ignored if the theme was set by another extension.
+ anotherExtension.sendMessage(winId);
+ await anotherExtension.awaitMessage("done");
+
+ extension.sendMessage();
+ await extension.awaitMessage("done");
+
+ await anotherExtension.unload();
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js
new file mode 100644
index 0000000000..2e7359ce29
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js
@@ -0,0 +1,187 @@
+"use strict";
+
+// This test checks color sanitization in various situations
+
+add_task(async function test_sanitization_invalid() {
+ // This test checks that invalid values are sanitized
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ bookmark_text: "ntimsfavoriteblue",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let navbar = document.querySelector("#nav-bar");
+ Assert.equal(
+ window.getComputedStyle(navbar).color,
+ "rgb(0, 0, 0)",
+ "All invalid values should always compute to black."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sanitization_css_variables() {
+ // This test checks that CSS variables are sanitized
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ bookmark_text: "var(--arrowpanel-dimmed)",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let navbar = document.querySelector("#nav-bar");
+ Assert.equal(
+ window.getComputedStyle(navbar).color,
+ "rgb(0, 0, 0)",
+ "All CSS variables should always compute to black."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sanitization_important() {
+ // This test checks that the sanitizer cannot be fooled with !important
+ let stylesheetAttr = `href="data:text/css,*{color:red!important}" type="text/css"`;
+ let stylesheet = document.createProcessingInstruction(
+ "xml-stylesheet",
+ stylesheetAttr
+ );
+ let load = BrowserTestUtils.waitForEvent(stylesheet, "load");
+ document.insertBefore(stylesheet, document.documentElement);
+ await load;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ bookmark_text: "green",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let navbar = document.querySelector("#nav-bar");
+ Assert.equal(
+ window.getComputedStyle(navbar).color,
+ "rgb(255, 0, 0)",
+ "Sheet applies"
+ );
+
+ stylesheet.remove();
+
+ Assert.equal(
+ window.getComputedStyle(navbar).color,
+ "rgb(0, 128, 0)",
+ "Shouldn't be able to fool the color sanitizer with !important"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sanitization_transparent() {
+ // This test checks whether transparent values are applied properly
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_top_separator: "transparent",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let navbar = document.querySelector("#nav-bar");
+ Assert.ok(
+ window.getComputedStyle(navbar).boxShadow.includes("rgba(0, 0, 0, 0)"),
+ "Top separator should be transparent"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sanitization_transparent_frame_color() {
+ // This test checks whether transparent frame color falls back to white.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: "transparent",
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolboxCS = window.getComputedStyle(toolbox);
+
+ if (backgroundColorSetOnRoot()) {
+ let docEl = document.documentElement;
+ let rootCS = window.getComputedStyle(docEl);
+ Assert.equal(
+ rootCS.backgroundColor,
+ "rgb(255, 255, 255)",
+ "Accent color should be white"
+ );
+ } else {
+ Assert.equal(
+ toolboxCS.backgroundColor,
+ "rgb(255, 255, 255)",
+ "Accent color should be white"
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_sanitization_transparent_tab_background_text_color() {
+ // This test checks whether transparent textcolor falls back to black.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: "transparent",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let docEl = document.documentElement;
+ Assert.equal(
+ window.getComputedStyle(docEl).color,
+ "rgb(0, 0, 0)",
+ "Text color should be black"
+ );
+
+ await extension.unload();
+ }
+);
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js b/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js
new file mode 100644
index 0000000000..4da4927ccf
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_separators.js
@@ -0,0 +1,76 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the separator colors are applied properly.
+
+add_task(async function test_support_separator_properties() {
+ const SEPARATOR_TOP_COLOR = "#ff00ff";
+ const SEPARATOR_VERTICAL_COLOR = "#f0000f";
+ const SEPARATOR_FIELD_COLOR = "#9400ff";
+ const SEPARATOR_BOTTOM_COLOR = "#3366cc";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_top_separator: SEPARATOR_TOP_COLOR,
+ toolbar_vertical_separator: SEPARATOR_VERTICAL_COLOR,
+ // This property is deprecated, but left in to check it doesn't
+ // unexpectedly break the theme installation.
+ toolbar_field_separator: SEPARATOR_FIELD_COLOR,
+ toolbar_bottom_separator: SEPARATOR_BOTTOM_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ // Test the deprecated color property.
+ let deprecatedMessagePromise = new Promise(resolve => {
+ Services.console.registerListener(function listener(msg) {
+ if (msg.message.includes("toolbar_field_separator")) {
+ resolve();
+ Services.console.unregisterListener(listener);
+ }
+ });
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ info("Wait for property deprecation message");
+ await deprecatedMessagePromise;
+
+ let navbar = document.querySelector("#nav-bar");
+ Assert.ok(
+ window
+ .getComputedStyle(navbar)
+ .boxShadow.includes(`rgb(${hexToRGB(SEPARATOR_TOP_COLOR).join(", ")})`),
+ "Top separator color properly set"
+ );
+
+ let panelUIButton = document.querySelector("#PanelUI-button");
+ // Bug 1712334: This should test bookmark item toolbar separators instead
+ Assert.equal(
+ window
+ .getComputedStyle(panelUIButton)
+ .getPropertyValue("border-image-source"),
+ "none",
+ "No vertical separator on app menu"
+ );
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ Assert.equal(
+ window.getComputedStyle(toolbox).borderBottomColor,
+ `rgb(${hexToRGB(SEPARATOR_BOTTOM_COLOR).join(", ")})`,
+ "Bottom separator color properly set"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
new file mode 100644
index 0000000000..35a7955f9f
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js
@@ -0,0 +1,275 @@
+"use strict";
+
+// This test checks whether the sidebar color properties work.
+
+/**
+ * Test whether the selected browser has the sidebar theme applied
+ *
+ * @param {object} theme that is applied
+ * @param {boolean} isBrightText whether the brighttext attribute should be set
+ */
+async function test_sidebar_theme(theme, isBrightText) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme,
+ },
+ });
+
+ const sidebarBox = document.getElementById("sidebar-box");
+ const content = SidebarUI.browser.contentWindow;
+ const root = content.document.documentElement;
+
+ ok(
+ !sidebarBox.hasAttribute("lwt-sidebar"),
+ "Sidebar box should not have lwt-sidebar attribute"
+ );
+ ok(
+ !root.hasAttribute("lwt-sidebar"),
+ "Sidebar should not have lwt-sidebar attribute"
+ );
+ ok(
+ !root.hasAttribute("lwt-sidebar-brighttext"),
+ "Sidebar should not have lwt-sidebar-brighttext attribute"
+ );
+ ok(
+ !root.hasAttribute("lwt-sidebar-highlight"),
+ "Sidebar should not have lwt-sidebar-highlight attribute"
+ );
+
+ const rootCS = content.getComputedStyle(root);
+ const originalBackground = rootCS.backgroundColor;
+ const originalColor = rootCS.color;
+
+ // ::-moz-tree-row(selected, focus) computed style can't be accessed, so we create a fake one.
+ const highlightCS = {
+ get backgroundColor() {
+ // Standardize to rgb like other computed style.
+ let color = rootCS.getPropertyValue(
+ "--lwt-sidebar-highlight-background-color"
+ );
+ let [r, g, b] = color
+ .replace("rgba(", "")
+ .split(",")
+ .map(channel => parseInt(channel, 10));
+ return `rgb(${r}, ${g}, ${b})`;
+ },
+
+ get color() {
+ let color = rootCS.getPropertyValue("--lwt-sidebar-highlight-text-color");
+ let [r, g, b] = color
+ .replace("rgba(", "")
+ .split(",")
+ .map(channel => parseInt(channel, 10));
+ return `rgb(${r}, ${g}, ${b})`;
+ },
+ };
+ const originalHighlightBackground = highlightCS.backgroundColor;
+ const originalHighlightColor = highlightCS.color;
+
+ await extension.startup();
+
+ Services.ppmm.sharedData.flush();
+
+ const actualBackground = hexToCSS(theme.colors.sidebar) || originalBackground;
+ const actualColor = hexToCSS(theme.colors.sidebar_text) || originalColor;
+ const actualHighlightBackground =
+ hexToCSS(theme.colors.sidebar_highlight) || originalHighlightBackground;
+ const actualHighlightColor =
+ hexToCSS(theme.colors.sidebar_highlight_text) || originalHighlightColor;
+ const isCustomHighlight = !!theme.colors.sidebar_highlight_text;
+ const isCustomSidebar = !!theme.colors.sidebar_text;
+
+ is(
+ sidebarBox.hasAttribute("lwt-sidebar"),
+ isCustomSidebar,
+ `Sidebar box should${
+ !isCustomSidebar ? " not" : ""
+ } have lwt-sidebar attribute`
+ );
+ is(
+ root.hasAttribute("lwt-sidebar"),
+ isCustomSidebar,
+ `Sidebar should${!isCustomSidebar ? " not" : ""} have lwt-sidebar attribute`
+ );
+ is(
+ root.hasAttribute("lwt-sidebar-brighttext"),
+ isBrightText,
+ `Sidebar should${
+ !isBrightText ? " not" : ""
+ } have lwt-sidebar-brighttext attribute`
+ );
+ is(
+ root.hasAttribute("lwt-sidebar-highlight"),
+ isCustomHighlight,
+ `Sidebar should${
+ !isCustomHighlight ? " not" : ""
+ } have lwt-sidebar-highlight attribute`
+ );
+
+ if (isCustomSidebar) {
+ const sidebarBoxCS = window.getComputedStyle(sidebarBox);
+ is(
+ sidebarBoxCS.backgroundColor,
+ actualBackground,
+ "Sidebar box background should be set."
+ );
+ is(
+ sidebarBoxCS.color,
+ actualColor,
+ "Sidebar box text color should be set."
+ );
+ }
+
+ is(
+ rootCS.backgroundColor,
+ actualBackground,
+ "Sidebar background should be set."
+ );
+ is(rootCS.color, actualColor, "Sidebar text color should be set.");
+
+ is(
+ highlightCS.backgroundColor,
+ actualHighlightBackground,
+ "Sidebar highlight background color should be set."
+ );
+ is(
+ highlightCS.color,
+ actualHighlightColor,
+ "Sidebar highlight text color should be set."
+ );
+
+ await extension.unload();
+
+ Services.ppmm.sharedData.flush();
+
+ ok(
+ !sidebarBox.hasAttribute("lwt-sidebar"),
+ "Sidebar box should not have lwt-sidebar attribute"
+ );
+ ok(
+ !root.hasAttribute("lwt-sidebar"),
+ "Sidebar should not have lwt-sidebar attribute"
+ );
+ ok(
+ !root.hasAttribute("lwt-sidebar-brighttext"),
+ "Sidebar should not have lwt-sidebar-brighttext attribute"
+ );
+ ok(
+ !root.hasAttribute("lwt-sidebar-highlight"),
+ "Sidebar should not have lwt-sidebar-highlight attribute"
+ );
+
+ is(
+ rootCS.backgroundColor,
+ originalBackground,
+ "Sidebar background should be reset."
+ );
+ is(rootCS.color, originalColor, "Sidebar text color should be reset.");
+ is(
+ highlightCS.backgroundColor,
+ originalHighlightBackground,
+ "Sidebar highlight background color should be reset."
+ );
+ is(
+ highlightCS.color,
+ originalHighlightColor,
+ "Sidebar highlight text color should be reset."
+ );
+}
+
+add_task(async function test_support_sidebar_colors() {
+ for (let command of ["viewBookmarksSidebar", "viewHistorySidebar"]) {
+ info("Executing command: " + command);
+
+ await SidebarUI.show(command);
+
+ await test_sidebar_theme(
+ {
+ colors: {
+ sidebar: "#fafad2", // lightgoldenrodyellow
+ sidebar_text: "#2f4f4f", // darkslategrey
+ },
+ },
+ false
+ );
+
+ await test_sidebar_theme(
+ {
+ colors: {
+ sidebar: "#8b4513", // saddlebrown
+ sidebar_text: "#ffa07a", // lightsalmon
+ },
+ },
+ true
+ );
+
+ await test_sidebar_theme(
+ {
+ colors: {
+ sidebar: "#fffafa", // snow
+ sidebar_text: "#663399", // rebeccapurple
+ sidebar_highlight: "#7cfc00", // lawngreen
+ sidebar_highlight_text: "#ffefd5", // papayawhip
+ },
+ },
+ false
+ );
+
+ await test_sidebar_theme(
+ {
+ colors: {
+ sidebar_highlight: "#a0522d", // sienna
+ sidebar_highlight_text: "#fff5ee", // seashell
+ },
+ },
+ false
+ );
+ }
+});
+
+add_task(async function test_support_sidebar_border_color() {
+ const LIGHT_SALMON = "#ffa07a";
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ sidebar_border: LIGHT_SALMON,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ const sidebarHeader = document.getElementById("sidebar-header");
+ const sidebarHeaderCS = window.getComputedStyle(sidebarHeader);
+
+ is(
+ sidebarHeaderCS.borderBottomColor,
+ hexToCSS(LIGHT_SALMON),
+ "Sidebar header border should be colored properly"
+ );
+
+ if (AppConstants.platform !== "linux") {
+ const sidebarSplitter = document.getElementById("sidebar-splitter");
+ const sidebarSplitterCS = window.getComputedStyle(sidebarSplitter);
+
+ is(
+ sidebarSplitterCS.borderInlineEndColor,
+ hexToCSS(LIGHT_SALMON),
+ "Sidebar splitter should be colored properly"
+ );
+
+ SidebarUI.reversePosition();
+
+ is(
+ sidebarSplitterCS.borderInlineStartColor,
+ hexToCSS(LIGHT_SALMON),
+ "Sidebar splitter should be colored properly after switching sides"
+ );
+
+ SidebarUI.reversePosition();
+ }
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js b/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js
new file mode 100644
index 0000000000..4a6d9a92f6
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js
@@ -0,0 +1,126 @@
+"use strict";
+
+// This test checks whether browser.theme.onUpdated works
+// when a static theme is applied
+
+add_task(async function test_on_updated() {
+ const theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.theme.onUpdated.addListener(updateInfo => {
+ browser.test.sendMessage("theme-updated", updateInfo);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ info("Testing update event on static theme startup");
+ let updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.startup();
+ const { theme: receivedTheme, windowId } = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event");
+ Assert.ok(
+ receivedTheme.images.theme_frame.includes("image1.png"),
+ "Theme theme_frame image should be applied"
+ );
+ Assert.equal(
+ receivedTheme.colors.frame,
+ ACCENT_COLOR,
+ "Theme frame color should be applied"
+ );
+ Assert.equal(
+ receivedTheme.colors.tab_background_text,
+ TEXT_COLOR,
+ "Theme tab_background_text color should be applied"
+ );
+
+ info("Testing update event on static theme unload");
+ updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.unload();
+ const updateInfo = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event on unload");
+ Assert.equal(
+ Object.keys(updateInfo.theme),
+ 0,
+ "unloading theme sends empty theme in update event"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_on_updated_eventpage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+ const theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "watcher@mochitest" } },
+ background: { persistent: false },
+ },
+ background() {
+ browser.theme.onUpdated.addListener(updateInfo => {
+ browser.test.sendMessage("theme-updated", updateInfo);
+ });
+ },
+ });
+
+ await extension.startup();
+ assertPersistentListeners(extension, "theme", "onUpdated", {
+ primed: false,
+ });
+
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "theme", "onUpdated", {
+ primed: true,
+ });
+
+ info("Testing update event on static theme startup");
+ let updatedPromise = extension.awaitMessage("theme-updated");
+ await theme.startup();
+ const { theme: receivedTheme, windowId } = await updatedPromise;
+ Assert.ok(!windowId, "No window id in static theme update event");
+ Assert.ok(
+ receivedTheme.images.theme_frame.includes("image1.png"),
+ "Theme theme_frame image should be applied"
+ );
+ await theme.unload();
+ await extension.awaitMessage("theme-updated");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js
new file mode 100644
index 0000000000..e4bd8cb99b
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js
@@ -0,0 +1,39 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the color of the tab line are applied properly.
+
+add_task(async function test_support_tab_line() {
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ const TAB_LINE_COLOR = "#ff0000";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: "#000",
+ tab_line: TAB_LINE_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ info("Checking selected tab line color");
+ let selectedTab = newWin.document.querySelector(".tabbrowser-tab[selected]");
+ let tab = selectedTab.querySelector(".tab-background");
+ let element = tab;
+ let property = "outline-color";
+ let computedValue = newWin.getComputedStyle(element)[property];
+ let expectedColor = `rgb(${hexToRGB(TAB_LINE_COLOR).join(", ")})`;
+
+ Assert.ok(
+ computedValue.includes(expectedColor),
+ `Tab line should be displayed in the box shadow of the tab: ${computedValue}`
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(newWin);
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js
new file mode 100644
index 0000000000..1e402dbcc6
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js
@@ -0,0 +1,51 @@
+"use strict";
+
+add_task(async function test_support_tab_loading_filling() {
+ const TAB_LOADING_COLOR = "#FF0000";
+
+ // Make sure we use the animating loading icon
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.prefersReducedMotion", 0]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: "#000",
+ toolbar: "#124455",
+ tab_background_text: "#9400ff",
+ tab_loading: TAB_LOADING_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ info("Checking selected tab loading indicator colors");
+
+ let selectedTab = document.querySelector(
+ ".tabbrowser-tab[visuallyselected=true]"
+ );
+
+ selectedTab.setAttribute("busy", "true");
+ selectedTab.setAttribute("progress", "true");
+
+ let throbber = selectedTab.throbber;
+ Assert.equal(
+ window.getComputedStyle(throbber, "::before").fill,
+ `rgb(${hexToRGB(TAB_LOADING_COLOR).join(", ")})`,
+ "Throbber is filled with theme color"
+ );
+
+ selectedTab.removeAttribute("busy");
+ selectedTab.removeAttribute("progress");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js
new file mode 100644
index 0000000000..21f3c6d38b
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js
@@ -0,0 +1,54 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the background color of selected tab are applied correctly.
+
+add_task(async function test_tab_background_color_property() {
+ const TAB_BACKGROUND_COLOR = "#9400ff";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ tab_selected: TAB_BACKGROUND_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ info("Checking selected tab color");
+
+ let openTab = document.querySelector(
+ ".tabbrowser-tab[visuallyselected=true]"
+ );
+ let openTabBackground = openTab.querySelector(".tab-background");
+
+ let selectedTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ let selectedTabBackground = selectedTab.querySelector(".tab-background");
+
+ let openTabGradient = window
+ .getComputedStyle(openTabBackground)
+ .getPropertyValue("background-image");
+ let selectedTabGradient = window
+ .getComputedStyle(selectedTabBackground)
+ .getPropertyValue("background-image");
+
+ let rgbRegex = /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/g;
+ let selectedTabColors = selectedTabGradient.match(rgbRegex);
+
+ Assert.equal(
+ selectedTabColors[0],
+ "rgb(" + hexToRGB(TAB_BACKGROUND_COLOR).join(", ") + ")",
+ "Selected tab background color should be set."
+ );
+ Assert.equal(openTabGradient, "none");
+
+ gBrowser.removeTab(selectedTab);
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js
new file mode 100644
index 0000000000..d819f3a5f1
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js
@@ -0,0 +1,70 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the text color of the selected tab are applied properly.
+
+add_task(async function test_support_tab_text_property_css_color() {
+ const TAB_TEXT_COLOR = "#9400ff";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ tab_text: TAB_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ info("Checking selected tab colors");
+ let selectedTab = document.querySelector(".tabbrowser-tab[selected]");
+ Assert.equal(
+ window.getComputedStyle(selectedTab).color,
+ "rgb(" + hexToRGB(TAB_TEXT_COLOR).join(", ") + ")",
+ "Selected tab text color should be set."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_support_tab_text_chrome_array() {
+ const TAB_TEXT_COLOR = [148, 0, 255];
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: FRAME_COLOR,
+ tab_background_text: TAB_BACKGROUND_TEXT_COLOR,
+ tab_text: TAB_TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ info("Checking selected tab colors");
+ let selectedTab = document.querySelector(".tabbrowser-tab[selected]");
+ Assert.equal(
+ window.getComputedStyle(selectedTab).color,
+ "rgb(" + TAB_TEXT_COLOR.join(", ") + ")",
+ "Selected tab text color should be set."
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js b/toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js
new file mode 100644
index 0000000000..39934200ac
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js
@@ -0,0 +1,48 @@
+"use strict";
+
+// This test checks whether the applied theme transition effects are applied
+// correctly.
+
+add_task(async function test_theme_transition_effects() {
+ const TOOLBAR = "#f27489";
+ const TEXT_COLOR = "#000000";
+ const TRANSITION_PROPERTY = "background-color";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ tab_background_text: TEXT_COLOR,
+ toolbar: TOOLBAR,
+ bookmark_text: TEXT_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ // check transition effect for toolbars
+ let navbar = document.querySelector("#nav-bar");
+ let navbarCS = window.getComputedStyle(navbar);
+
+ Assert.ok(
+ navbarCS
+ .getPropertyValue("transition-property")
+ .includes(TRANSITION_PROPERTY),
+ "Transition property set for #nav-bar"
+ );
+
+ let bookmarksBar = document.querySelector("#PersonalToolbar");
+ setToolbarVisibility(bookmarksBar, true, false, true);
+ let bookmarksBarCS = window.getComputedStyle(bookmarksBar);
+
+ Assert.ok(
+ bookmarksBarCS
+ .getPropertyValue("transition-property")
+ .includes(TRANSITION_PROPERTY),
+ "Transition property set for #PersonalToolbar"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
new file mode 100644
index 0000000000..aa0446c453
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
@@ -0,0 +1,183 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the background color and the color of the navbar text fields are applied properly.
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+add_setup(async function () {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function test_support_toolbar_field_properties() {
+ const TOOLBAR_FIELD_BACKGROUND = "#ff00ff";
+ const TOOLBAR_FIELD_COLOR = "#00ff00";
+ const TOOLBAR_FIELD_BORDER = "#aaaaff";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field: TOOLBAR_FIELD_BACKGROUND,
+ toolbar_field_text: TOOLBAR_FIELD_COLOR,
+ toolbar_field_border: TOOLBAR_FIELD_BORDER,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let root = document.documentElement;
+ // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
+ root.removeAttribute("remotecontrol");
+ registerCleanupFunction(() => {
+ root.setAttribute("remotecontrol", "true");
+ });
+
+ let fields = [
+ document.querySelector("#urlbar-background"),
+ BrowserSearch.searchBar,
+ ].filter(field => {
+ let bounds = field.getBoundingClientRect();
+ return bounds.width > 0 && bounds.height > 0;
+ });
+
+ Assert.equal(fields.length, 2, "Should be testing two elements");
+
+ info(
+ `Checking toolbar background colors and colors for ${fields.length} toolbar fields.`
+ );
+ for (let field of fields) {
+ info(`Testing ${field.id || field.className}`);
+ Assert.equal(
+ window.getComputedStyle(field).backgroundColor,
+ hexToCSS(TOOLBAR_FIELD_BACKGROUND),
+ "Field background should be set."
+ );
+ Assert.equal(
+ window.getComputedStyle(field).color,
+ hexToCSS(TOOLBAR_FIELD_COLOR),
+ "Field color should be set."
+ );
+ testBorderColor(field, TOOLBAR_FIELD_BORDER);
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_support_toolbar_field_brighttext() {
+ let root = document.documentElement;
+ // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
+ root.removeAttribute("remotecontrol");
+ registerCleanupFunction(() => {
+ root.setAttribute("remotecontrol", "true");
+ });
+ let urlbar = gURLBar.textbox;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field: "#fff",
+ toolbar_field_text: "#000",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ Assert.equal(
+ window.getComputedStyle(urlbar).color,
+ hexToCSS("#000000"),
+ "Color has been set"
+ );
+ Assert.ok(
+ !root.hasAttribute("lwt-toolbar-field-brighttext"),
+ "Brighttext attribute should not be set"
+ );
+
+ await extension.unload();
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field: "#000",
+ toolbar_field_text: "#fff",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ Assert.equal(
+ window.getComputedStyle(urlbar).color,
+ hexToCSS("#ffffff"),
+ "Color has been set"
+ );
+ Assert.ok(
+ root.hasAttribute("lwt-toolbar-field-brighttext"),
+ "Brighttext attribute should be set"
+ );
+
+ await extension.unload();
+});
+
+// Verifies that we apply the lwt-toolbar-field-brighttext attribute when
+// toolbar fields are dark text on a dark background.
+add_task(async function test_support_toolbar_field_brighttext_dark_on_dark() {
+ let root = document.documentElement;
+ // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
+ root.removeAttribute("remotecontrol");
+ registerCleanupFunction(() => {
+ root.setAttribute("remotecontrol", "true");
+ });
+ let urlbar = gURLBar.textbox;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar_field: "#000",
+ toolbar_field_text: "#111111",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ Assert.equal(
+ window.getComputedStyle(urlbar).color,
+ hexToCSS("#111111"),
+ "Color has been set"
+ );
+ Assert.ok(
+ root.hasAttribute("lwt-toolbar-field-brighttext"),
+ "Brighttext attribute should be set"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js
new file mode 100644
index 0000000000..ff6af3ade7
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js
@@ -0,0 +1,107 @@
+"use strict";
+
+add_setup(async function () {
+ // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
+ document.documentElement.removeAttribute("remotecontrol");
+ registerCleanupFunction(() => {
+ document.documentElement.setAttribute("remotecontrol", "true");
+ });
+});
+
+add_task(async function test_toolbar_field_focus() {
+ const TOOLBAR_FIELD_BACKGROUND = "#FF00FF";
+ const TOOLBAR_FIELD_COLOR = "#00FF00";
+ const TOOLBAR_FOCUS_BACKGROUND = "#FF0000";
+ const TOOLBAR_FOCUS_TEXT = "#9400FF";
+ const TOOLBAR_FOCUS_BORDER = "#FFFFFF";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: "#FF0000",
+ tab_background_color: "#ffffff",
+ toolbar_field: TOOLBAR_FIELD_BACKGROUND,
+ toolbar_field_text: TOOLBAR_FIELD_COLOR,
+ toolbar_field_focus: TOOLBAR_FOCUS_BACKGROUND,
+ toolbar_field_text_focus: TOOLBAR_FOCUS_TEXT,
+ toolbar_field_border_focus: TOOLBAR_FOCUS_BORDER,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ info("Checking toolbar field's focus color");
+
+ let urlBar = document.querySelector("#urlbar-background");
+ gURLBar.textbox.setAttribute("focused", "true");
+ let style = window.getComputedStyle(urlBar);
+
+ Assert.equal(
+ style.backgroundColor,
+ `rgb(${hexToRGB(TOOLBAR_FOCUS_BACKGROUND).join(", ")})`,
+ "Background Color is changed"
+ );
+ Assert.equal(
+ style.color,
+ `rgb(${hexToRGB(TOOLBAR_FOCUS_TEXT).join(", ")})`,
+ "Text Color is changed"
+ );
+ Assert.equal(
+ style.outlineColor,
+ `rgb(${hexToRGB(TOOLBAR_FOCUS_BORDER).join(", ")})`,
+ "Focus ring color"
+ );
+
+ gURLBar.textbox.removeAttribute("focused");
+
+ Assert.equal(
+ style.backgroundColor,
+ `rgb(${hexToRGB(TOOLBAR_FIELD_BACKGROUND).join(", ")})`,
+ "Background Color is set back to initial"
+ );
+ Assert.equal(
+ style.color,
+ `rgb(${hexToRGB(TOOLBAR_FIELD_COLOR).join(", ")})`,
+ "Text Color is set back to initial"
+ );
+ await extension.unload();
+});
+
+add_task(async function test_toolbar_field_focus_low_alpha() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: "#FF0000",
+ tab_background_color: "#ffffff",
+ toolbar_field: "#FF00FF",
+ toolbar_field_text: "#00FF00",
+ toolbar_field_focus: "rgba(0, 0, 255, 0.4)",
+ toolbar_field_text_focus: "red",
+ toolbar_field_border_focus: "#FFFFFF",
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+ gURLBar.textbox.setAttribute("focused", "true");
+
+ let urlBar = document.querySelector("#urlbar-background");
+ Assert.equal(
+ window.getComputedStyle(urlBar).backgroundColor,
+ `rgba(0, 0, 255, 0.9)`,
+ "Background color has minimum opacity enforced"
+ );
+ Assert.equal(
+ window.getComputedStyle(urlBar).color,
+ `rgb(255, 255, 255)`,
+ "Text color has been overridden to match background"
+ );
+
+ gURLBar.textbox.removeAttribute("focused");
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js
new file mode 100644
index 0000000000..37c082b36f
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js
@@ -0,0 +1,63 @@
+"use strict";
+
+/* globals InspectorUtils */
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the button background color properties are applied correctly.
+
+add_task(async function setup_home_button() {
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("home-button")
+ );
+});
+
+add_task(async function test_button_background_properties() {
+ const BUTTON_BACKGROUND_ACTIVE = "#FFFFFF";
+ const BUTTON_BACKGROUND_HOVER = "#59CBE8";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ button_background_active: BUTTON_BACKGROUND_ACTIVE,
+ button_background_hover: BUTTON_BACKGROUND_HOVER,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbarButton = document.querySelector("#home-button");
+ let toolbarButtonIcon = toolbarButton.icon;
+ let toolbarButtonIconCS = window.getComputedStyle(toolbarButtonIcon);
+
+ InspectorUtils.addPseudoClassLock(toolbarButton, ":hover");
+
+ Assert.equal(
+ toolbarButtonIconCS.getPropertyValue("background-color"),
+ `rgb(${hexToRGB(BUTTON_BACKGROUND_HOVER).join(", ")})`,
+ "Toolbar button hover background is set."
+ );
+
+ InspectorUtils.addPseudoClassLock(toolbarButton, ":active");
+
+ Assert.equal(
+ toolbarButtonIconCS.getPropertyValue("background-color"),
+ `rgb(${hexToRGB(BUTTON_BACKGROUND_ACTIVE).join(", ")})`,
+ "Toolbar button active background is set!"
+ );
+
+ InspectorUtils.clearPseudoClassLocks(toolbarButton);
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js
new file mode 100644
index 0000000000..2802c6ac33
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js
@@ -0,0 +1,109 @@
+"use strict";
+
+// This test checks applied WebExtension themes that attempt to change
+// icon color properties
+
+add_task(async function setup_home_button() {
+ CustomizableUI.addWidgetToArea("home-button", "nav-bar");
+ registerCleanupFunction(() =>
+ CustomizableUI.removeWidgetFromArea("home-button")
+ );
+});
+
+add_task(async function test_icons_properties() {
+ const ICONS_COLOR = "#001b47";
+ const ICONS_ATTENTION_COLOR = "#44ba77";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ icons: ICONS_COLOR,
+ icons_attention: ICONS_ATTENTION_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbarbutton = document.querySelector("#home-button");
+ Assert.equal(
+ window.getComputedStyle(toolbarbutton).getPropertyValue("fill"),
+ `rgb(${hexToRGB(ICONS_COLOR).join(", ")})`,
+ "Buttons fill color set!"
+ );
+
+ let starButton = document.querySelector("#star-button");
+ starButton.setAttribute("starred", "true");
+
+ let starComputedStyle = window.getComputedStyle(starButton);
+ Assert.equal(
+ starComputedStyle.getPropertyValue(
+ "--lwt-toolbarbutton-icon-fill-attention"
+ ),
+ `rgb(${hexToRGB(ICONS_ATTENTION_COLOR).join(", ")})`,
+ "Variable is properly set"
+ );
+ Assert.equal(
+ starComputedStyle.getPropertyValue("fill"),
+ `rgb(${hexToRGB(ICONS_ATTENTION_COLOR).join(", ")})`,
+ "Starred icon fill is properly set"
+ );
+
+ starButton.removeAttribute("starred");
+
+ await extension.unload();
+});
+
+add_task(async function test_no_icons_properties() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ images: {
+ theme_frame: "image1.png",
+ },
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ });
+
+ await extension.startup();
+
+ let toolbarbutton = document.querySelector("#home-button");
+ let toolbarbuttonCS = window.getComputedStyle(toolbarbutton);
+ let currentColor = toolbarbuttonCS.getPropertyValue("color");
+ Assert.equal(
+ window.getComputedStyle(toolbarbutton).getPropertyValue("fill"),
+ currentColor,
+ "Button fill color should be currentColor when no icon color specified."
+ );
+
+ let starButton = document.querySelector("#star-button");
+ starButton.setAttribute("starred", "true");
+ let starComputedStyle = window.getComputedStyle(starButton);
+ Assert.equal(
+ starComputedStyle.getPropertyValue(
+ "--lwt-toolbarbutton-icon-fill-attention"
+ ),
+ "",
+ "Icon attention fill should not be set when the value is not specified in the manifest."
+ );
+ starButton.removeAttribute("starred");
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js
new file mode 100644
index 0000000000..ee31d80888
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js
@@ -0,0 +1,105 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// the background color of toolbars are applied properly.
+
+add_task(async function test_support_toolbar_property() {
+ const TOOLBAR_COLOR = "#ff00ff";
+ const TOOLBAR_TEXT_COLOR = "#9400ff";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar: TOOLBAR_COLOR,
+ toolbar_text: TOOLBAR_TEXT_COLOR,
+ },
+ },
+ },
+ });
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolbars = [
+ ...toolbox.querySelectorAll("toolbar:not(#TabsToolbar)"),
+ ].filter(toolbar => {
+ let bounds = toolbar.getBoundingClientRect();
+ return bounds.width > 0 && bounds.height > 0;
+ });
+
+ let transitionPromise = waitForTransition(toolbars[0], "background-color");
+ await extension.startup();
+ await transitionPromise;
+
+ info(`Checking toolbar colors for ${toolbars.length} toolbars.`);
+ for (let toolbar of toolbars) {
+ info(`Testing ${toolbar.id}`);
+ Assert.equal(
+ window.getComputedStyle(toolbar).backgroundColor,
+ hexToCSS(TOOLBAR_COLOR),
+ "Toolbar background color should be set."
+ );
+ Assert.equal(
+ window.getComputedStyle(toolbar).color,
+ hexToCSS(TOOLBAR_TEXT_COLOR),
+ "Toolbar text color should be set."
+ );
+ }
+
+ info("Checking selected tab colors");
+ let selectedTab = document.querySelector(".tabbrowser-tab[selected]");
+ Assert.equal(
+ window.getComputedStyle(selectedTab).color,
+ hexToCSS(TOOLBAR_TEXT_COLOR),
+ "Selected tab text color should be set."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_bookmark_text_property() {
+ const TOOLBAR_COLOR = [255, 0, 255];
+ const TOOLBAR_TEXT_COLOR = [48, 0, 255];
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ colors: {
+ frame: ACCENT_COLOR,
+ tab_background_text: TEXT_COLOR,
+ toolbar: TOOLBAR_COLOR,
+ bookmark_text: TOOLBAR_TEXT_COLOR,
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let toolbox = document.querySelector("#navigator-toolbox");
+ let toolbars = [
+ ...toolbox.querySelectorAll("toolbar:not(#TabsToolbar)"),
+ ].filter(toolbar => {
+ let bounds = toolbar.getBoundingClientRect();
+ return bounds.width > 0 && bounds.height > 0;
+ });
+
+ info(`Checking toolbar colors for ${toolbars.length} toolbars.`);
+ for (let toolbar of toolbars) {
+ info(`Testing ${toolbar.id}`);
+ Assert.equal(
+ window.getComputedStyle(toolbar).color,
+ rgbToCSS(TOOLBAR_TEXT_COLOR),
+ "bookmark_text should be an alias for toolbar_text"
+ );
+ }
+
+ info("Checking selected tab colors");
+ let selectedTab = document.querySelector(".tabbrowser-tab[selected]");
+ Assert.equal(
+ window.getComputedStyle(selectedTab).color,
+ rgbToCSS(TOOLBAR_TEXT_COLOR),
+ "Selected tab text color should be set."
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js b/toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js
new file mode 100644
index 0000000000..025a4073dd
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js
@@ -0,0 +1,144 @@
+"use strict";
+
+const { AddonSettings } = ChromeUtils.importESModule(
+ "resource://gre/modules/addons/AddonSettings.sys.mjs"
+);
+
+// This test checks that theme warnings are properly emitted.
+
+function waitForConsole(task, message) {
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message: new RegExp(message),
+ },
+ ]);
+ await task();
+ SimpleTest.endMonitorConsole();
+ });
+}
+
+add_setup(async function () {
+ SimpleTest.waitForExplicitFinish();
+});
+
+add_task(async function test_static_theme() {
+ for (const property of ["colors", "images", "properties"]) {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ [property]: {
+ such_property: "much_wow",
+ },
+ },
+ },
+ });
+ await waitForConsole(
+ extension.startup,
+ `Unrecognized theme property found: ${property}.such_property`
+ );
+ await extension.unload();
+ }
+});
+
+add_task(async function test_dynamic_theme() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["theme"],
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg === "update-theme") {
+ browser.theme.update(details).then(() => {
+ browser.test.sendMessage("theme-updated");
+ });
+ } else {
+ browser.theme.reset().then(() => {
+ browser.test.sendMessage("theme-reset");
+ });
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+
+ for (const property of ["colors", "images", "properties"]) {
+ extension.sendMessage("update-theme", {
+ [property]: {
+ such_property: "much_wow",
+ },
+ });
+ await waitForConsole(
+ () => extension.awaitMessage("theme-updated"),
+ `Unrecognized theme property found: ${property}.such_property`
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_experiments_enabled() {
+ info("Testing that experiments are handled correctly on nightly and deved");
+
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ theme: {
+ properties: {
+ such_property: "much_wow",
+ unknown_property: "very_unknown",
+ },
+ },
+ theme_experiment: {
+ properties: {
+ such_property: "--such-property",
+ },
+ },
+ },
+ });
+ if (!AddonSettings.EXPERIMENTS_ENABLED) {
+ await waitForConsole(
+ extension.startup,
+ "This extension is not allowed to run theme experiments"
+ );
+ } else {
+ await waitForConsole(
+ extension.startup,
+ "Unrecognized theme property found: properties.unknown_property"
+ );
+ }
+ await extension.unload();
+});
+
+add_task(async function test_experiments_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.experiments.enabled", false]],
+ });
+
+ info(
+ "Testing that experiments are handled correctly when experiements pref is disabled"
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ theme: {
+ properties: {
+ such_property: "much_wow",
+ },
+ },
+ theme_experiment: {
+ properties: {
+ such_property: "--such-property",
+ },
+ },
+ },
+ });
+ await waitForConsole(
+ extension.startup,
+ "This extension is not allowed to run theme experiments"
+ );
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js b/toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js
new file mode 100644
index 0000000000..96a2216067
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js
@@ -0,0 +1,94 @@
+"use strict";
+
+/* import-globals-from ../../../thumbnails/test/head.js */
+loadTestSubscript("../../../thumbnails/test/head.js");
+
+// The service that creates thumbnails of webpages in the background loads a
+// web page in the background (with several features disabled). Extensions
+// should be able to observe requests, but not run content scripts.
+add_task(async function test_thumbnails_background_visibility_to_extensions() {
+ const iframeUrl = "http://example.com/?iframe";
+ const testPageUrl = bgTestPageURL({ iframe: iframeUrl });
+ // ^ testPageUrl is http://mochi.test:8888/.../thumbnails_background.sjs?...
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ // ":8888" omitted due to bug 1362809.
+ matches: [
+ "http://mochi.test/*/thumbnails_background.sjs*",
+ "http://example.com/?iframe*",
+ ],
+ js: ["contentscript.js"],
+ run_at: "document_start",
+ all_frames: true,
+ },
+ ],
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "http://example.com/*",
+ "http://mochi.test/*",
+ ],
+ },
+ files: {
+ "contentscript.js": () => {
+ // Content scripts are not expected to be run in the page of the
+ // thumbnail service, so this should never execute.
+ new Image().src = "http://example.com/?unexpected-content-script";
+ browser.test.fail("Content script ran in thumbs, unexpectedly.");
+ },
+ },
+ background() {
+ let requests = [];
+ browser.webRequest.onBeforeRequest.addListener(
+ ({ url, tabId, frameId, type }) => {
+ browser.test.assertEq(-1, tabId, "Thumb page is not a tab");
+ // We want to know if frameId is 0 or non-negative (or possibly -1).
+ if (type === "sub_frame") {
+ browser.test.assertTrue(frameId > 0, `frame ${frameId} for ${url}`);
+ } else {
+ browser.test.assertEq(0, frameId, `frameId for ${type} ${url}`);
+ }
+ requests.push({ type, url });
+ },
+ {
+ types: ["main_frame", "sub_frame", "image"],
+ urls: ["*://*/*"],
+ },
+ ["blocking"]
+ );
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq("get-results", msg, "expected message");
+ browser.test.sendMessage("webRequest-results", requests);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ ok(!thumbnailExists(testPageUrl), "Thumbnail should not be cached yet.");
+
+ await bgCapture(testPageUrl);
+ ok(thumbnailExists(testPageUrl), "Thumbnail should be cached after capture");
+ removeThumbnail(testPageUrl);
+
+ extension.sendMessage("get-results");
+ Assert.deepEqual(
+ await extension.awaitMessage("webRequest-results"),
+ [
+ {
+ type: "main_frame",
+ url: testPageUrl,
+ },
+ {
+ type: "sub_frame",
+ url: iframeUrl,
+ },
+ ],
+ "Expected requests via webRequest"
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js b/toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js
new file mode 100644
index 0000000000..d898cb96a4
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js
@@ -0,0 +1,72 @@
+"use strict";
+
+add_task(async function webnav_test_eventpage() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation", "*://mochi.test/*"],
+ background: { persistent: false },
+ },
+ background() {
+ const EVENTS = [
+ "onTabReplaced",
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ ];
+
+ for (let event of EVENTS) {
+ browser.webNavigation[event].addListener(() => {});
+ }
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ // onTabReplaced is never persisted, it is an empty event handler.
+ const EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ ];
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "webNavigation", event, {
+ primed: false,
+ });
+ }
+
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "webNavigation", event, {
+ primed: true,
+ });
+ }
+
+ // wake up the background, we don't really care which event does it,
+ // we're just verifying the state after.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "webNavigation", event, {
+ primed: false,
+ });
+ }
+
+ await BrowserTestUtils.closeWindow(newWin);
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js b/toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js
new file mode 100644
index 0000000000..674a10a5ef
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js
@@ -0,0 +1,48 @@
+"use strict";
+
+// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1573456
+add_task(async function test_mozextension_page_loaded_in_extension_process() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "https://example.com/*",
+ ],
+ web_accessible_resources: ["test.html"],
+ },
+ files: {
+ "test.html": '<!DOCTYPE html><script src="test.js"></script>',
+ "test.js": () => {
+ browser.test.assertTrue(
+ browser.webRequest,
+ "webRequest API should be available"
+ );
+
+ browser.test.sendMessage("test_done");
+ },
+ },
+ background: () => {
+ browser.webRequest.onBeforeRequest.addListener(
+ () => {
+ return {
+ redirectUrl: browser.runtime.getURL("test.html"),
+ };
+ },
+ { urls: ["*://*/redir"] },
+ ["blocking"]
+ );
+ },
+ });
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/redir"
+ );
+
+ await extension.awaitMessage("test_done");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js b/toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js
new file mode 100644
index 0000000000..666d4f324f
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js
@@ -0,0 +1,133 @@
+"use strict";
+
+// Check that extension popup windows contain the name of the extension
+// as well as the title of the loaded document, but not the URL.
+add_task(async function test_popup_title() {
+ const name = "custom_title_number_9_please";
+ const docTitle = "popup-test-title";
+
+ const extensionWithImplicitHostPermission = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name,
+ },
+ async background() {
+ let popup;
+
+ // Called after the popup loads
+ browser.runtime.onMessage.addListener(async ({ docTitle }) => {
+ const name = browser.runtime.getManifest().name;
+ const { id } = await popup;
+ const { title } = await browser.windows.get(id);
+
+ browser.test.assertTrue(
+ title.includes(name),
+ "popup title must include extension name"
+ );
+ browser.test.assertTrue(
+ title.includes(docTitle),
+ "popup title must include extension document title"
+ );
+ browser.test.assertFalse(
+ title.includes("moz-extension:"),
+ "popup title must not include extension URL"
+ );
+
+ // share window data with other extensions
+ browser.test.sendMessage("windowData", {
+ id: id,
+ fullTitle: title,
+ });
+
+ browser.test.onMessage.addListener(async message => {
+ if (message === "cleanup") {
+ await browser.windows.remove(id);
+ browser.test.sendMessage("finishedCleanup");
+ }
+ });
+
+ browser.test.sendMessage("done");
+ });
+
+ popup = browser.windows.create({
+ url: "/index.html",
+ type: "popup",
+ });
+ },
+ files: {
+ "index.html": `<!doctype html>
+ <meta charset="utf-8">
+ <title>${docTitle}</title>,
+ <script src="index.js"></script>
+ `,
+ "index.js": `addEventListener(
+ "load",
+ () => browser.runtime.sendMessage({docTitle: document.title})
+ );`,
+ },
+ });
+
+ const extensionWithoutPermissions = ExtensionTestUtils.loadExtension({
+ async background() {
+ const { id } = await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => {
+ resolve(message);
+ });
+ });
+
+ const { title } = await browser.windows.get(id);
+
+ browser.test.assertEq(
+ title,
+ undefined,
+ "popup window must not include title"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ const extensionWithTabsPermission = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ const { id, fullTitle } = await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => {
+ resolve(message);
+ });
+ });
+
+ const { title } = await browser.windows.get(id);
+
+ browser.test.assertEq(
+ title,
+ fullTitle,
+ "popup title equals expected title"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extensionWithoutPermissions.startup();
+ await extensionWithTabsPermission.startup();
+ await extensionWithImplicitHostPermission.startup();
+
+ const windowData = await extensionWithImplicitHostPermission.awaitMessage(
+ "windowData"
+ );
+
+ extensionWithoutPermissions.sendMessage(windowData);
+ extensionWithTabsPermission.sendMessage(windowData);
+
+ await extensionWithoutPermissions.awaitMessage("done");
+ await extensionWithTabsPermission.awaitMessage("done");
+ await extensionWithImplicitHostPermission.awaitMessage("done");
+
+ extensionWithImplicitHostPermission.sendMessage("cleanup");
+ await extensionWithImplicitHostPermission.awaitMessage("finishedCleanup");
+
+ await extensionWithoutPermissions.unload();
+ await extensionWithTabsPermission.unload();
+ await extensionWithImplicitHostPermission.unload();
+});
diff --git a/toolkit/components/extensions/test/browser/data/test-download.txt b/toolkit/components/extensions/test/browser/data/test-download.txt
new file mode 100644
index 0000000000..f416e0e291
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/data/test-download.txt
@@ -0,0 +1 @@
+test download content
diff --git a/toolkit/components/extensions/test/browser/data/test_downloads_referrer.html b/toolkit/components/extensions/test/browser/data/test_downloads_referrer.html
new file mode 100644
index 0000000000..85410abfcd
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/data/test_downloads_referrer.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test downloads referrer</title>
+ </head>
+ <body>
+ <a href="test-download.txt" class="test-link">test link</a>
+ </body>
+</html>
diff --git a/toolkit/components/extensions/test/browser/head.js b/toolkit/components/extensions/test/browser/head.js
new file mode 100644
index 0000000000..e25d4cb594
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/head.js
@@ -0,0 +1,126 @@
+/* exported ACCENT_COLOR, BACKGROUND, ENCODED_IMAGE_DATA, FRAME_COLOR, TAB_TEXT_COLOR,
+ TEXT_COLOR, TAB_BACKGROUND_TEXT_COLOR, imageBufferFromDataURI, hexToCSS, hexToRGB, testBorderColor,
+ waitForTransition, loadTestSubscript, backgroundColorSetOnRoot, assertPersistentListeners */
+
+"use strict";
+
+const { ClientEnvironmentBase } = ChromeUtils.importESModule(
+ "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs"
+);
+
+const BACKGROUND =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0" +
+ "DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const ENCODED_IMAGE_DATA =
+ "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0h" +
+ "STQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAdhwAAHYcBj+XxZQAAB5dJREFUSMd" +
+ "91vmTlEcZB/Bvd7/vO+/ce83O3gfLDUsC4VgIghBUEo2GM9GCFTaQBEISA1qIEVNQ4aggJDGIgAGTlFUKKcqKQpVHaQyny7FrCMiywp4ze+/Mzs67M/P" +
+ "O+3a3v5jdWo32H/B86vv0U083weecV3+0C8lkEh6PhzS3tuLkieMSAKo3fW9Mb1eoUtM0jemerukLllzrbGlKheovUpeqkmt113hPfx/27tyFF7+/bbg" +
+ "e+U9g20s7kEwmMXXGNLrp2fWi4V5z/tFjJ3fWX726INbfU2xx0yelkJAKdJf3Xl5+2QcPTpv2U0JZR+u92+xvly5ygKDm20/hlX17/jvB6VNnIKXEOyd" +
+ "O0iFh4PLVy0XV1U83Vk54QI7JK+bl+UE5vjRfTCzJ5eWBTFEayBLjisvljKmzwmtWrVkEAPNmVrEZkyfh+fU1n59k//7X4Fbz8MK2DRSAWLNq/Yc36y9" +
+ "+3UVMsyAYVPMy/MTvdBKvriJhphDq6xa9vf0i1GMwPVhM5s9bsLw/EvtN2kywwnw/nzBuLDZs2z4auXGjHuvWbmBQdT5v7qytn165fLCyyGtXTR6j5GV" +
+ "kIsvlBCwTVNgQhMKCRDQ2iIbmJv7BpU+Ykl02UFOzdt6gkbzTEQ5Rl2KL3W8eGUE+/ssFXK+rJQ8vWigLgjk5z9ZsvpOniJzVi+ZKTUhCuATTKCjhoLA" +
+ "hhQAsjrSZBJcm7rZ22O+ev6mMmTLj55eu1T+jU8GOH/kJf2TZCiifIQsXfwEbN2yktxoaeYbf93DKSORMnTOZE0aZaVlQGYVKJCgjEJSCcgLB0xDERjI" +
+ "NFBUEaXmuB20t95eEutr0xrufpo4eepMAkMPIxx+dx9at25EWQNXsh77q0Bzwen0ShEF32HCrCpjksAWHFAKqokFhgEJt2DKJeFoQv8eDuz3duaseXZY" +
+ "dixthaQ+NRlRCcKO+FgCweP68wswMF/yZWcTkNpLJFAZEGi6XC07NCUIIoqaNSLQfFALCEpCSEL/bK/wuw+12sKlDQzKs6k5yZt+rI+2aNKUSNdUbSSQ" +
+ "Wh2mJP46rGPeYrjtkY0M7jFgciUQCiqqgrCAfBTle3G9rR1NHN3SnDq9Lg49QlBQEcbfbQCKZlhQEDkXBih27RpDOrmacfP8YB4CfHT7uNXrCMFM2FdD" +
+ "BVQ5TE/A5HbDSJoSpQXAbXm8A4b5+gKrwulU4KKEBnwuzHpiQu+n1jQoQsM+9cYQMT9fvf/FLBYTaDqdzbfgft95PKzbPyQqwnlAXGkJtGIgNYnJpMfw" +
+ "OghLG0GJE0ZdiaOnsQ16OD6XZLkiRROdAgud5sxk8ridsy/pQU1VlOIkZN6QtAGnx0FA0AtXvIA4C5OX4kOWbiLRhQBDApTmgJuLwEonMgBvjgpmgjIE" +
+ "hhX7DAIVKNeqE05/dJbgEgRy5eOJ1ieXr1gJA7ZNLTrVVlAZLyopLJAUlHsrAMrwwrRQ4t6E5VHgSBExjcGpO0JQNizCE05a41dhOi+cXXVm144e1AHD" +
+ "1vXfFMOLy+KSHEDoEJLZ8s+ZWKpUusWwpFKiMUQ4jbiAaj8Hp9oExBsMCUpEIfD6JLKZjKJVGV3RIZGdm0qxA5qmz+/cgMhBVuuMRewRRGF7fe4BYHMg" +
+ "N5LxdV3vhy1EjrrjA5GAyTuKpFHricfS0dSDNCQRPoSyQgSSPI+UBEtwShiWUQEHw5mMvbz4JRcXvDr3B3dBG1sq5X53GlMcX4JWVTyvRQcOumDD2vfK" +
+ "cjOqiQDZPGBF2ryUEnjRhJlP4d6/BiQ1TABPKiyQhgtzvjPCJlQ/OGRwauqESSUPX68U3Vi4fGeH83Hwc3bYHBWUV0m0k4HB6z7aGu6sznDos00R3exg" +
+ "l5ZMwc+FMaJoKKxHFnbo6DMYiELBlqLOXDBq8dsvuPTfKALpwdbX42iMLsHjLd0Zv4RNvvY1wZxdZunyVDGZm6D/47sv12RqbmOPVhG5LGnAH4S8sgu7" +
+ "1oK/pn2BWAoYw0dDbaTd19iqlZROejwzEjqgMSuXUifak8jF49JnNI0kAoGrBfET7+uXOrS+y5ta21JzZsw7faW45XJaXxSvyAtTpkOi483fwtAWP1wt" +
+ "vrhvd/VFx+26zojr9Les2PnfaTNu4cuGvvKe9BVv3/RgARiNTpk/Hod17MWikxcqzzfhK/+1jL2xc+YQAX1ISDHLV7WTpQQaLcASzPEiB41ZrmEeHkrT" +
+ "Q49uz/aXn+iilLKXq/MmlS0e/jFcuX4SmaQAAKSXlnIvVy1aQ6EBMFgRyCznDpfGFwdKqirF2tu5SdIeGrkiP+KS5yb7dHtIKsnI++kP9rS8RQvjmxxe" +
+ "jePxD2HHwwP9FdCllurGhUbx14CAbiMc4Y2qVJqwLbo0qfpdLSilILB4Xg0mT6h7vnSWzZn9RoaynobWF3K6rk1NmzMWZ83/+37+V4a1cVg5JACYF45b" +
+ "FGVVWOFS2V1HUCjOdBqW0Q9fYb7N9/tcSptnldjpott8rFEXBO+f+NKrWMHL9Wu1nSUAIAaUUa59aAyE43E4X3bD8W6K5K6x1h1snRaMDJDuQf7+vrzf" +
+ "eG+mgfrcLHh3C79bx6wttGEqERiH/AjPohWMouv2ZAAAAAElFTkSuQmCC";
+const ACCENT_COLOR = "#a14040";
+const TEXT_COLOR = "#fac96e";
+// For testing aliases of the colors above:
+const FRAME_COLOR = [71, 105, 91];
+const TAB_BACKGROUND_TEXT_COLOR = [207, 221, 192, 0.9];
+
+function hexToRGB(hex) {
+ if (!hex) {
+ return null;
+ }
+ hex = parseInt(hex.indexOf("#") > -1 ? hex.substring(1) : hex, 16);
+ return [hex >> 16, (hex & 0x00ff00) >> 8, hex & 0x0000ff];
+}
+
+function rgbToCSS(rgb) {
+ return `rgb(${rgb.join(", ")})`;
+}
+
+function hexToCSS(hex) {
+ if (!hex) {
+ return null;
+ }
+ return rgbToCSS(hexToRGB(hex));
+}
+
+function imageBufferFromDataURI(encodedImageData) {
+ let decodedImageData = atob(encodedImageData);
+ return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
+}
+
+function waitForTransition(element, propertyName) {
+ return BrowserTestUtils.waitForEvent(
+ element,
+ "transitionend",
+ false,
+ event => {
+ return event.target == element && event.propertyName == propertyName;
+ }
+ );
+}
+
+function testBorderColor(element, expected) {
+ let computedStyle = window.getComputedStyle(element);
+ Assert.equal(
+ computedStyle.borderLeftColor,
+ hexToCSS(expected),
+ "Element left border color should be set."
+ );
+ Assert.equal(
+ computedStyle.borderRightColor,
+ hexToCSS(expected),
+ "Element right border color should be set."
+ );
+ Assert.equal(
+ computedStyle.borderTopColor,
+ hexToCSS(expected),
+ "Element top border color should be set."
+ );
+ Assert.equal(
+ computedStyle.borderBottomColor,
+ hexToCSS(expected),
+ "Element bottom border color should be set."
+ );
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+/**
+ * Windows 7 and 8 set the window's background-color on :root instead of
+ * #navigator-toolbox to avoid bug 1695280. When that bug is fixed, this
+ * function and the assertions it gates can be removed.
+ *
+ * @returns {boolean} True if the window's background-color is set on :root
+ * rather than #navigator-toolbox.
+ */
+function backgroundColorSetOnRoot() {
+ const os = ClientEnvironmentBase.os;
+ if (!os.isWindows) {
+ return false;
+ }
+ return os.windowsVersion < 10;
+}
+
+// Persistent Listener test functionality
+const { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
diff --git a/toolkit/components/extensions/test/browser/head_serviceworker.js b/toolkit/components/extensions/test/browser/head_serviceworker.js
new file mode 100644
index 0000000000..b2f9512e5a
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/head_serviceworker.js
@@ -0,0 +1,119 @@
+"use strict";
+
+/* exported assert_background_serviceworker_pref_enabled,
+ * getBackgroundServiceWorkerRegistration,
+ * getServiceWorkerInfo, getServiceWorkerState,
+ * waitForServiceWorkerRegistrationsRemoved, waitForServiceWorkerTerminated
+ */
+
+async function assert_background_serviceworker_pref_enabled() {
+ is(
+ WebExtensionPolicy.backgroundServiceWorkerEnabled,
+ true,
+ "Expect extensions.backgroundServiceWorker.enabled to be true"
+ );
+}
+
+// Return the name of the enum corresponding to the worker's state (ex: "STATE_ACTIVATED")
+// because nsIServiceWorkerInfo doesn't currently provide a comparable string-returning getter.
+function getServiceWorkerState(workerInfo) {
+ const map = Object.keys(workerInfo)
+ .filter(k => k.startsWith("STATE_"))
+ .reduce((map, name) => {
+ map.set(workerInfo[name], name);
+ return map;
+ }, new Map());
+ return map.has(workerInfo.state)
+ ? map.get(workerInfo.state)
+ : "state: ${workerInfo.state}";
+}
+
+function getServiceWorkerInfo(swRegInfo) {
+ const { evaluatingWorker, installingWorker, waitingWorker, activeWorker } =
+ swRegInfo;
+ return evaluatingWorker || installingWorker || waitingWorker || activeWorker;
+}
+
+async function waitForServiceWorkerTerminated(swRegInfo) {
+ info(`Wait all ${swRegInfo.scope} workers to be terminated`);
+
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => !getServiceWorkerInfo(swRegInfo)
+ );
+ } catch (err) {
+ const workerInfo = getServiceWorkerInfo(swRegInfo);
+ if (workerInfo) {
+ ok(
+ false,
+ `Error while waiting for workers for scope ${swRegInfo.scope} to be terminated. ` +
+ `Found a worker in state: ${getServiceWorkerState(workerInfo)}`
+ );
+ return;
+ }
+
+ throw err;
+ }
+}
+
+function getBackgroundServiceWorkerRegistration(extension) {
+ const policy = WebExtensionPolicy.getByHostname(extension.uuid);
+ const expectedSWScope = policy.getURL("/");
+ const expectedScriptURL = policy.extension.backgroundWorkerScript || "";
+
+ ok(
+ expectedScriptURL.startsWith(expectedSWScope),
+ `Extension does include a valid background.service_worker: ${expectedScriptURL}`
+ );
+
+ const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+
+ let swReg;
+ let regs = swm.getAllRegistrations();
+
+ for (let i = 0; i < regs.length; i++) {
+ let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+ if (reg.scriptSpec === expectedScriptURL) {
+ swReg = reg;
+ break;
+ }
+ }
+
+ ok(swReg, `Found service worker registration for ${expectedScriptURL}`);
+
+ is(
+ swReg.scope,
+ expectedSWScope,
+ "The extension background worker registration has the expected scope URL"
+ );
+
+ return swReg;
+}
+
+async function waitForServiceWorkerRegistrationsRemoved(extension) {
+ info(`Wait ${extension.id} service worker registration to be deleted`);
+ const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+ let baseURI = Services.io.newURI(`moz-extension://${extension.uuid}/`);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ baseURI,
+ {}
+ );
+
+ await BrowserTestUtils.waitForCondition(() => {
+ let regs = swm.getAllRegistrations();
+
+ for (let i = 0; i < regs.length; i++) {
+ let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+ if (principal.equals(reg.principal)) {
+ return false;
+ }
+ }
+
+ info(`All ${extension.id} service worker registrations are gone`);
+ return true;
+ }, `All ${extension.id} service worker registrations should be deleted`);
+}