From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../extensions/test/browser/.eslintrc.js | 11 + .../test/browser/browser-serviceworker.toml | 9 + .../extensions/test/browser/browser.toml | 103 +++++ .../browser_ext_background_serviceworker.js | 285 +++++++++++++ ...r_ext_background_serviceworker_pref_disabled.js | 126 ++++++ .../test/browser/browser_ext_downloads_filters.js | 139 +++++++ .../test/browser/browser_ext_downloads_referrer.js | 91 +++++ ...rowser_ext_eventpage_disableResetIdleForTest.js | 83 ++++ .../browser_ext_extension_page_tab_navigated.js | 226 +++++++++++ .../test/browser/browser_ext_management_themes.js | 177 ++++++++ .../browser/browser_ext_process_crash_handling.js | 180 +++++++++ .../test/browser/browser_ext_test_mock.js | 47 +++ ..._ext_themes_additional_backgrounds_alignment.js | 88 ++++ .../browser_ext_themes_alpha_accentcolor.js | 30 ++ .../test/browser/browser_ext_themes_arrowpanels.js | 82 ++++ .../browser_ext_themes_autocomplete_popup.js | 173 ++++++++ .../browser/browser_ext_themes_chromeparity.js | 159 ++++++++ .../browser_ext_themes_dynamic_getCurrent.js | 203 ++++++++++ .../browser_ext_themes_dynamic_onUpdated.js | 154 +++++++ .../browser/browser_ext_themes_dynamic_updates.js | 199 +++++++++ .../test/browser/browser_ext_themes_experiment.js | 450 +++++++++++++++++++++ .../test/browser/browser_ext_themes_findbar.js | 227 +++++++++++ .../browser_ext_themes_getCurrent_differentExt.js | 151 +++++++ .../test/browser/browser_ext_themes_highlight.js | 63 +++ .../test/browser/browser_ext_themes_incognito.js | 77 ++++ .../test/browser/browser_ext_themes_lwtsupport.js | 56 +++ .../browser_ext_themes_multiple_backgrounds.js | 202 +++++++++ .../test/browser/browser_ext_themes_ntp_colors.js | 203 ++++++++++ .../browser_ext_themes_ntp_colors_perwindow.js | 240 +++++++++++ .../test/browser/browser_ext_themes_pbm.js | 422 +++++++++++++++++++ .../test/browser/browser_ext_themes_persistence.js | 60 +++ .../test/browser/browser_ext_themes_reset.js | 112 +++++ .../browser/browser_ext_themes_sanitization.js | 174 ++++++++ .../test/browser/browser_ext_themes_separators.js | 76 ++++ .../test/browser/browser_ext_themes_sidebars.js | 278 +++++++++++++ .../browser/browser_ext_themes_static_onUpdated.js | 126 ++++++ .../test/browser/browser_ext_themes_tab_line.js | 39 ++ .../test/browser/browser_ext_themes_tab_loading.js | 49 +++ .../browser/browser_ext_themes_tab_selected.js | 49 +++ .../test/browser/browser_ext_themes_tab_text.js | 70 ++++ .../browser/browser_ext_themes_theme_transition.js | 48 +++ .../browser/browser_ext_themes_toolbar_fields.js | 212 ++++++++++ .../browser_ext_themes_toolbar_fields_focus.js | 107 +++++ .../browser_ext_themes_toolbarbutton_colors.js | 63 +++ .../browser_ext_themes_toolbarbutton_icons.js | 109 +++++ .../test/browser/browser_ext_themes_toolbars.js | 105 +++++ .../test/browser/browser_ext_themes_warnings.js | 144 +++++++ .../browser/browser_ext_thumbnails_bg_extension.js | 94 +++++ .../browser/browser_ext_webNavigation_eventpage.js | 72 ++++ ...browser_ext_webRequest_redirect_mozextension.js | 48 +++ .../browser/browser_ext_windows_popup_title.js | 133 ++++++ .../extensions/test/browser/data/test-download.txt | 1 + .../test/browser/data/test_downloads_referrer.html | 10 + toolkit/components/extensions/test/browser/head.js | 115 ++++++ .../extensions/test/browser/head_serviceworker.js | 119 ++++++ 55 files changed, 7069 insertions(+) create mode 100644 toolkit/components/extensions/test/browser/.eslintrc.js create mode 100644 toolkit/components/extensions/test/browser/browser-serviceworker.toml create mode 100644 toolkit/components/extensions/test/browser/browser.toml create mode 100644 toolkit/components/extensions/test/browser/browser_ext_background_serviceworker.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_background_serviceworker_pref_disabled.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_downloads_referrer.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_eventpage_disableResetIdleForTest.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_extension_page_tab_navigated.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_management_themes.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_process_crash_handling.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_test_mock.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_getCurrent.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_onUpdated.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_experiment.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_findbar.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_getCurrent_differentExt.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_highlight.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_incognito.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_ntp_colors_perwindow.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_reset.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_separators.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_static_onUpdated.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_line.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_tab_text.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_theme_transition.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields_focus.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_colors.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbarbutton_icons.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_toolbars.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_themes_warnings.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_thumbnails_bg_extension.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_webNavigation_eventpage.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_webRequest_redirect_mozextension.js create mode 100644 toolkit/components/extensions/test/browser/browser_ext_windows_popup_title.js create mode 100644 toolkit/components/extensions/test/browser/data/test-download.txt create mode 100644 toolkit/components/extensions/test/browser/data/test_downloads_referrer.html create mode 100644 toolkit/components/extensions/test/browser/head.js create mode 100644 toolkit/components/extensions/test/browser/head_serviceworker.js (limited to 'toolkit/components/extensions/test/browser') 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.toml b/toolkit/components/extensions/test/browser/browser-serviceworker.toml new file mode 100644 index 0000000000..1d7c7c3ffe --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser-serviceworker.toml @@ -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.toml b/toolkit/components/extensions/test/browser/browser.toml new file mode 100644 index 0000000000..33d54bddc2 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser.toml @@ -0,0 +1,103 @@ +[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": "", + "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": "", + "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": "", + "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..48dd6b88ee --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_downloads_filters.js @@ -0,0 +1,139 @@ +/* -*- 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()}`]); + tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + + 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_heic_Images() { + await testAppliedFilters(".heic", 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..9742d42b2e --- /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(` + + + + test webpage + + + `); +}); + +function createTestExtPage({ script }) { + return ` + + + + + + + `; +} + +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.startLoadingURIString(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.startLoadingURIString(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..c33727b96b --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_process_crash_handling.js @@ -0,0 +1,180 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + // This test triggers a crash and so it will be restarting all builtin + // extensions persistent background pages as a side effect (and that would + // be make this test to hit failures due to the builtin background pages + // still in the process of being restarted being detected as shutdown leaks). + set: [["extensions.background.disableRestartPersistentAfterCrash", true]], + }); + 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, + processSpawningDisabled, + lastCrashTimestamps, + } = 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" + ); + + Assert.equal( + processSpawningDisabled, + false, + "Expect process spawning to be enabled" + ); + + Assert.deepEqual(lastCrashTimestamps, [], "Expect no crash timestamps"); + + 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; + + Assert.ok( + Glean.extensions.processEvent.created_fg.testGetValue() > 0, + "Expect glean processEvent.created_fg to be set." + ); + Assert.equal( + undefined, + Glean.extensions.processEvent.created_bg.testGetValue(), + "Creating in the background is not expected on desktop." + ); + + info("Force extension process crash"); + // Clear any existing telemetry data, so that we can be sure we can + // assert the glean process_event metric labels values to be strictly + // equal to 1 after the extension process crashed. + Services.fog.testResetFOG(); + // 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" + ); + + Assert.equal( + ExtensionProcessCrashObserver.processSpawningDisabled, + false, + "Expect process spawning to still be enabled" + ); + Assert.equal( + ExtensionProcessCrashObserver.lastCrashTimestamps.length, + 1, + "Expect a crash timestamp" + ); + + info("Expect the same childID to have been notified as a Management event"); + Assert.deepEqual( + await promiseExtensionProcessCrashNotified, + { + childID: currentProcessChildID, + processSpawningDisabled: false, + // This boolean flag is expected to be always true on Desktop builds. + appInForeground: true, + }, + "Got the expected childID notified as part of the extension-process-crash Management event" + ); + + Assert.ok( + Glean.extensions.processEvent.crashed_fg.testGetValue() > 0, + "Expect glean processEvent.crashed_fg to be set" + ); + Assert.equal( + undefined, + Glean.extensions.processEvent.crashed_bg.testGetValue(), + "Crashing in the background is not expected on desktop." + ); + + info("Wait for mv3 extension shutdown"); + await mv3Extension.unload(); + info("Wait for mv2 extension shutdown"); + await mv2Extension.unload(); + + // Reset this array to prevent TV failures. + ExtensionProcessCrashObserver.lastCrashTimestamps = []; + + await SpecialPowers.popPrefEnv(); +}); 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..b02e552ecb --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_additional_backgrounds_alignment.js @@ -0,0 +1,88 @@ +"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); + /** + * 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); + 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..761e89561f --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_alpha_accentcolor.js @@ -0,0 +1,30 @@ +"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(); + + Assert.equal( + getToolboxBackgroundColor(), + "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..086ea39653 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_chromeparity.js @@ -0,0 +1,159 @@ +"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); + + Assert.ok( + toolboxCS.backgroundImage.includes("face.png"), + `The backgroundImage should use face.png. Actual value is: ${toolboxCS.backgroundImage}` + ); + Assert.equal( + getToolboxBackgroundColor(), + "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; + + Assert.equal( + getToolboxBackgroundColor(), + "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(); + Assert.equal( + getToolboxBackgroundColor(), + "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; + Assert.equal( + getToolboxBackgroundColor(), + "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(); + Assert.equal( + getToolboxBackgroundColor(), + "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..5fae5150b6 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js @@ -0,0 +1,199 @@ +"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); + } + Assert.ok( + toolboxCS.backgroundImage.includes(backgroundImage), + "Expected correct background image" + ); + Assert.equal( + getToolboxBackgroundColor(), + 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 = toolboxCS.backgroundImage; + let backgroundColor = getToolboxBackgroundColor(); + 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 = toolboxCS.backgroundImage; + let backgroundColor = getToolboxBackgroundColor(); + 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": ` + + + Test Extension Sidebar + + + + `, + "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..0458558ee4 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_lwtsupport.js @@ -0,0 +1,56 @@ +"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); + 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" + ); + + Assert.equal( + getToolboxBackgroundColor(), + 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..2115d8d23c --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_multiple_backgrounds.js @@ -0,0 +1,202 @@ +"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(); + 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. + 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); + 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); + 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..3b36a256d0 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_pbm.js @@ -0,0 +1,422 @@ +/* -*- 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 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); + +async function testIsDark(win, expectDark) { + let mql = win.matchMedia("(prefers-color-scheme: dark)"); + if (mql.matches != expectDark) { + // The color scheme change might not have been processed yet, since that + // happens on a refresh driver tick. + await new Promise(r => mql.addEventListener("change", r, { once: true })); + } + is( + mql.matches, + expectDark, + `Window should${expectDark ? "" : " not"} be dark.` + ); +} + +/** + * 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. + */ +async function testWindowColorScheme({ win, expectDark, expectLWTAttributes }) { + let docEl = win.document.documentElement; + + await testIsDark(win, expectDark); + + 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, + }); + + let windowB = await BrowserTestUtils.openNewBrowserWindow(); + + info("Additional normal browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: windowB, + expectDark: false, + expectLWTAttributes: 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, + }); + + 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, + }); + + 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: false, + }); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: false, + }); + + 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, + }); + + 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, + }); + + 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, + }); + + let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: true, + }); + + 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, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: 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, + }); + + info("Private browsing window should not be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: false, + expectLWTAttributes: true, + }); + + 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, + }); + + info("Private browsing window should be in dark mode."); + await testWindowColorScheme({ + win: pbmWindow, + expectDark: true, + expectLWTAttributes: true, + }); + + 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..b71ff572cc --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_persistence.js @@ -0,0 +1,60 @@ +"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(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(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..89ebd3ae68 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sanitization.js @@ -0,0 +1,174 @@ +"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(); + + Assert.equal( + getToolboxBackgroundColor(), + "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..0d2e69716d --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_sidebars.js @@ -0,0 +1,278 @@ +"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 text color is light + */ +async function test_sidebar_theme(theme, isBrightText) { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme, + }, + }); + + const sidebarBox = document.getElementById("sidebar-box"); + const browserRoot = document.documentElement; + const content = SidebarUI.browser.contentWindow; + const root = content.document.documentElement; + + ok( + !browserRoot.hasAttribute("lwt-sidebar"), + "Browser should not have lwt-sidebar attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar"), + "Root should not have lwt-sidebar attribute" + ); + ok( + !browserRoot.hasAttribute("lwt-sidebar-highlight"), + "Browser 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( + browserRoot.hasAttribute("lwt-sidebar"), + isCustomSidebar, + `Browser should${!isCustomSidebar ? " not" : ""} have lwt-sidebar attribute` + ); + is( + root.hasAttribute("lwt-sidebar"), + isCustomSidebar, + `Sidebar should${!isCustomSidebar ? " not" : ""} have lwt-sidebar attribute` + ); + if (isCustomSidebar) { + // Quite confusingly, getAttribute() on XUL elements for attributes that + // are not present has different behavior to HTML (empty string vs. null). + is( + root.getAttribute("lwt-sidebar"), + browserRoot.getAttribute("lwt-sidebar"), + `Sidebar lwt-sidebar attribute should match browser` + ); + } + is( + browserRoot.getAttribute("lwt-sidebar") == "dark", + isBrightText, + `Browser should${ + !isBrightText ? " not" : "" + } have lwt-sidebar="dark" 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( + !browserRoot.hasAttribute("lwt-sidebar"), + "Browser should not have lwt-sidebar attribute" + ); + ok( + !root.hasAttribute("lwt-sidebar"), + "Sidebar should not have lwt-sidebar 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..10ce77db11 --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_loading.js @@ -0,0 +1,49 @@ +"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]"); + + 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..3dd77ac92c --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_tab_selected.js @@ -0,0 +1,49 @@ +"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]"); + let openTabBackground = openTab.querySelector(".tab-background"); + + let selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + let selectedTabBackground = selectedTab.querySelector(".tab-background"); + + let openTabColor = window + .getComputedStyle(openTabBackground) + .getPropertyValue("background-color"); + let selectedTabColor = window + .getComputedStyle(selectedTabBackground) + .getPropertyValue("background-color"); + + Assert.equal( + selectedTabColor, + "rgb(" + hexToRGB(TAB_BACKGROUND_COLOR).join(", ") + ")", + "Selected tab background color should be set." + ); + Assert.notEqual(openTabColor, selectedTabColor); + + 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..3fd7899cdc --- /dev/null +++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js @@ -0,0 +1,212 @@ +"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.equal( + root.getAttribute("lwt-toolbar-field"), + "light", + "Should be light" + ); + + 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.equal( + root.getAttribute("lwt-toolbar-field"), + "dark", + "Should be dark" + ); + + await extension.unload(); +}); + +// Verifies that we apply the lwt-toolbar-field="dark" 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.equal( + root.getAttribute("lwt-toolbar-field"), + "dark", + "toolbar-field color-scheme should be dark" + ); + + await extension.unload(); +}); + +add_task(async function test_no_explicit_toolbar_field_on_dark_toolbar() { + let root = document.documentElement; + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + theme: { + colors: { + frame: "#000", + tab_background_text: "#fff", + // Explicitly unset toolbar fields, but they default to light. + }, + }, + }, + }); + + await extension.startup(); + + Assert.equal( + root.getAttribute("lwt-toolbar-field"), + "light", + "toolbar-field color-scheme should be set and light" + ); + + 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": '', + "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": ` + + ${docTitle}, + + `, + "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 @@ + + + + + Test downloads referrer + + + test link + + diff --git a/toolkit/components/extensions/test/browser/head.js b/toolkit/components/extensions/test/browser/head.js new file mode 100644 index 0000000000..fc17bd5a51 --- /dev/null +++ b/toolkit/components/extensions/test/browser/head.js @@ -0,0 +1,115 @@ +/* exported ACCENT_COLOR, BACKGROUND, ENCODED_IMAGE_DATA, FRAME_COLOR, TAB_TEXT_COLOR, + TEXT_COLOR, TAB_BACKGROUND_TEXT_COLOR, imageBufferFromDataURI, hexToCSS, hexToRGB, testBorderColor, + waitForTransition, loadTestSubscript, assertPersistentListeners, getToolboxBackgroundColor */ + +"use strict"; + +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 getToolboxBackgroundColor() { + let toolbox = document.getElementById("navigator-toolbox"); + // Ignore any potentially ongoing transition. + toolbox.style.transitionProperty = "none"; + let color = window.getComputedStyle(toolbox).backgroundColor; + toolbox.style.transitionProperty = ""; + return color; +} + +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); +} + +// 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`); +} -- cgit v1.2.3