diff options
Diffstat (limited to '')
134 files changed, 23302 insertions, 0 deletions
diff --git a/browser/components/preferences/tests/addons/pl-dictionary.xpi b/browser/components/preferences/tests/addons/pl-dictionary.xpi Binary files differnew file mode 100644 index 0000000000..cc4da1fa83 --- /dev/null +++ b/browser/components/preferences/tests/addons/pl-dictionary.xpi diff --git a/browser/components/preferences/tests/addons/set_homepage.xpi b/browser/components/preferences/tests/addons/set_homepage.xpi Binary files differnew file mode 100644 index 0000000000..9aff671021 --- /dev/null +++ b/browser/components/preferences/tests/addons/set_homepage.xpi diff --git a/browser/components/preferences/tests/addons/set_newtab.xpi b/browser/components/preferences/tests/addons/set_newtab.xpi Binary files differnew file mode 100644 index 0000000000..f11db0b6a8 --- /dev/null +++ b/browser/components/preferences/tests/addons/set_newtab.xpi diff --git a/browser/components/preferences/tests/browser.ini b/browser/components/preferences/tests/browser.ini new file mode 100644 index 0000000000..8203bceb90 --- /dev/null +++ b/browser/components/preferences/tests/browser.ini @@ -0,0 +1,152 @@ +[DEFAULT] +prefs = + extensions.formautofill.addresses.available='on' + extensions.formautofill.creditCards.available='on' + signon.management.page.os-auth.enabled=true +support-files = + head.js + privacypane_tests_perwindow.js + addons/pl-dictionary.xpi + addons/set_homepage.xpi + addons/set_newtab.xpi + +[browser_advanced_update.js] +skip-if = !updater +[browser_application_xml_handle_internally.js] +[browser_applications_selection.js] +[browser_basic_rebuild_fonts_test.js] +[browser_browser_languages_subdialog.js] +skip-if = + tsan + (!debug && os == 'win') # Bug 1518370 +[browser_bug1018066_resetScrollPosition.js] +[browser_bug1020245_openPreferences_to_paneContent.js] +[browser_bug1184989_prevent_scrolling_when_preferences_flipped.js] +skip-if = os == "mac" # 1664576 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +support-files = + browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml +[browser_bug1547020_lockedDownloadDir.js] +[browser_bug1579418.js] +[browser_bug410900.js] +[browser_bug731866.js] +[browser_bug795764_cachedisabled.js] +[browser_cert_export.js] +[browser_change_app_handler.js] +skip-if = os != "win" # Windows-specific handler application selection dialog +[browser_checkspelling.js] +[browser_connection.js] +[browser_connection_bug1445991.js] +[browser_connection_bug1505330.js] +skip-if = (verify && debug && (os == 'linux' || os == 'mac')) +[browser_connection_bug388287.js] +[browser_containers_name_input.js] +[browser_contentblocking.js] +skip-if = socketprocess_networking +[browser_contentblocking_categories.js] +[browser_contentblocking_standard_tcp_section.js] +[browser_cookie_exceptions_addRemove.js] +[browser_cookies_exceptions.js] +[browser_defaultbrowser_alwayscheck.js] +[browser_engines.js] +[browser_etp_exceptions_dialog.js] +[browser_experimental_features.js] +[browser_experimental_features_filter.js] +[browser_experimental_features_hidden_when_not_public.js] +skip-if = + os == "mac" && debug # Bug 1723854 + os == "linux" && os_version == "18.04" && debug # Bug 1723854 +[browser_experimental_features_resetall.js] +[browser_extension_controlled.js] +skip-if = + tsan + ccov && (os == 'linux' || os == 'win') # Linux: bug 1613530, Windows: bug 1437051 +[browser_filetype_dialog.js] +[browser_fluent.js] +[browser_homepage_default.js] +[browser_homepages_filter_aboutpreferences.js] +[browser_homepages_use_bookmark.js] +[browser_hometab_restore_defaults.js] +https_first_disabled = true +[browser_https_only_exceptions.js] +[browser_https_only_section.js] +[browser_ignore_invalid_capability.js] +[browser_languages_subdialog.js] +[browser_layersacceleration.js] +[browser_localSearchShortcuts.js] +[browser_moreFromMozilla.js] +[browser_moreFromMozilla_locales.js] +[browser_newtab_menu.js] +[browser_notifications_do_not_disturb.js] +[browser_open_download_preferences.js] +support-files = empty_pdf_file.pdf +[browser_open_migration_wizard.js] +[browser_password_management.js] +[browser_pdf_disabled.js] +[browser_performance.js] +[browser_performance_content_process_limit.js] +[browser_performance_e10srollout.js] +[browser_performance_non_e10s.js] +skip-if = true +[browser_permissions_checkPermissionsWereAdded.js] +[browser_permissions_dialog.js] +[browser_permissions_dialog_default_perm.js] +[browser_permissions_urlFieldHidden.js] +[browser_primaryPassword.js] +[browser_privacy_cookieBannerHandling.js] +[browser_privacy_dnsoverhttps.js] +[browser_privacy_firefoxSuggest.js] +[browser_privacy_passwordGenerationAndAutofill.js] +[browser_privacy_quickactions.js] +[browser_privacy_relayIntegration.js] +[browser_privacy_segmentation_pref.js] +[browser_privacy_syncDataClearing.js] +[browser_privacypane_2.js] +[browser_privacypane_3.js] +[browser_proxy_backup.js] +[browser_sanitizeOnShutdown_prefLocked.js] +[browser_searchChangedEngine.js] +[browser_searchDefaultEngine.js] +support-files = + engine1/manifest.json + engine2/manifest.json +[browser_searchFindMoreLink.js] +[browser_searchRestoreDefaults.js] +[browser_searchScroll.js] +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +[browser_searchShowSuggestionsFirst.js] +[browser_search_no_results_change_category.js] +[browser_search_searchTerms.js] +[browser_search_subdialog_tooltip_saved_addresses.js] +[browser_search_subdialogs_within_preferences_1.js] +skip-if = tsan # Bug 1678829 +[browser_search_subdialogs_within_preferences_2.js] +[browser_search_subdialogs_within_preferences_3.js] +[browser_search_subdialogs_within_preferences_4.js] +[browser_search_subdialogs_within_preferences_5.js] +[browser_search_subdialogs_within_preferences_6.js] +[browser_search_subdialogs_within_preferences_7.js] +[browser_search_subdialogs_within_preferences_8.js] +[browser_search_subdialogs_within_preferences_site_data.js] +[browser_search_within_preferences_1.js] +skip-if = (os == 'win' && processor == "aarch64") # Bug 1536560 +[browser_search_within_preferences_2.js] +[browser_search_within_preferences_command.js] +[browser_searchsuggestions.js] +[browser_security-1.js] +[browser_security-2.js] +[browser_security-3.js] +[browser_site_login_exceptions.js] +[browser_site_login_exceptions_policy.js] +[browser_spotlight.js] +[browser_statePartitioning_PBM_strings.js] +[browser_statePartitioning_strings.js] +[browser_subdialogs.js] +support-files = + subdialog.xhtml + subdialog2.xhtml +[browser_sync_chooseWhatToSync.js] +[browser_sync_disabled.js] +[browser_sync_pairing.js] +[browser_warning_permanent_private_browsing.js] diff --git a/browser/components/preferences/tests/browser_advanced_update.js b/browser/components/preferences/tests/browser_advanced_update.js new file mode 100644 index 0000000000..95da7a1c7a --- /dev/null +++ b/browser/components/preferences/tests/browser_advanced_update.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cm = Components.manager; + +const uuidGenerator = Services.uuid; + +const mockUpdateManager = { + contractId: "@mozilla.org/updates/update-manager;1", + + _mockClassId: uuidGenerator.generateUUID(), + + _originalClassId: "", + + QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager"]), + + createInstance(iiD) { + return this.QueryInterface(iiD); + }, + + register() { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (!registrar.isCIDRegistered(this._mockClassId)) { + this._originalClassId = registrar.contractIDToCID(this.contractId); + registrar.registerFactory( + this._mockClassId, + "Unregister after testing", + this.contractId, + this + ); + } + }, + + unregister() { + let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(this._mockClassId, this); + registrar.registerFactory(this._originalClassId, "", this.contractId, null); + }, + + getUpdateCount() { + return this._updates.length; + }, + + getUpdateAt(index) { + return this._updates[index]; + }, + + _updates: [ + { + name: "Firefox Developer Edition 49.0a2", + statusText: "The Update was successfully installed", + buildID: "20160728004010", + installDate: 1469763105156, + detailsURL: "https://www.mozilla.org/firefox/aurora/", + }, + { + name: "Firefox Developer Edition 43.0a2", + statusText: "The Update was successfully installed", + buildID: "20150929004011", + installDate: 1443585886224, + detailsURL: "https://www.mozilla.org/firefox/aurora/", + }, + { + name: "Firefox Developer Edition 42.0a2", + statusText: "The Update was successfully installed", + buildID: "20150920004018", + installDate: 1442818147544, + detailsURL: "https://www.mozilla.org/firefox/aurora/", + }, + ], +}; + +function formatInstallDate(sec) { + var date = new Date(sec); + const dtOptions = { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }; + return date.toLocaleString(undefined, dtOptions); +} + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let showBtn = doc.getElementById("showUpdateHistory"); + let dialogOverlay = content.gSubDialog._preloadDialog._overlay; + + // XXX: For unknown reasons, this mock cannot be loaded by + // XPCOMUtils.defineLazyServiceGetter() called in aboutDialog-appUpdater.js. + // It is registered here so that we could assert update history subdialog + // without stopping the preferences advanced pane from loading. + // See bug 1361929. + mockUpdateManager.register(); + + // Test the dialog window opens + ok( + BrowserTestUtils.is_hidden(dialogOverlay), + "The dialog should be invisible" + ); + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://mozapps/content/update/history.xhtml" + ); + showBtn.doCommand(); + await promiseSubDialogLoaded; + ok( + !BrowserTestUtils.is_hidden(dialogOverlay), + "The dialog should be visible" + ); + + let dialogFrame = dialogOverlay.querySelector(".dialogFrame"); + let frameDoc = dialogFrame.contentDocument; + let updates = frameDoc.querySelectorAll("richlistitem.update"); + + // Test the update history numbers are correct + is( + updates.length, + mockUpdateManager.getUpdateCount(), + "The update count is incorrect." + ); + + // Test the updates are displayed correctly + let update = null; + let updateData = null; + for (let i = 0; i < updates.length; ++i) { + update = updates[i]; + updateData = mockUpdateManager.getUpdateAt(i); + + let testcases = [ + { + selector: ".update-name", + id: "update-full-build-name", + args: { name: updateData.name, buildID: updateData.buildID }, + }, + { + selector: ".update-installedOn-label", + id: "update-installed-on", + args: { date: formatInstallDate(updateData.installDate) }, + }, + { + selector: ".update-status-label", + id: "update-status", + args: { status: updateData.statusText }, + }, + ]; + + for (let { selector, id, args } of testcases) { + const element = update.querySelector(selector); + const l10nAttrs = frameDoc.l10n.getAttributes(element); + Assert.deepEqual( + l10nAttrs, + { + id, + args, + }, + "Wrong " + id + ); + } + + if (update.detailsURL) { + is( + update.detailsURL, + update.querySelector(".text-link").href, + "Wrong detailsURL" + ); + } + } + + // Test the dialog window closes + let closeBtn = dialogOverlay.querySelector(".dialogClose"); + closeBtn.doCommand(); + ok( + BrowserTestUtils.is_hidden(dialogOverlay), + "The dialog should be invisible" + ); + + mockUpdateManager.unregister(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_application_xml_handle_internally.js b/browser/components/preferences/tests/browser_application_xml_handle_internally.js new file mode 100644 index 0000000000..edb4a4c0ec --- /dev/null +++ b/browser/components/preferences/tests/browser_application_xml_handle_internally.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const HandlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" +].getService(Ci.nsIHandlerService); + +const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +// This test checks that application/xml has the handle internally option. +add_task(async function applicationXmlHandleInternally() { + const mimeInfo = MIMEService.getFromTypeAndExtension( + "application/xml", + "xml" + ); + HandlerService.store(mimeInfo); + registerCleanupFunction(() => { + HandlerService.remove(mimeInfo); + }); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First, find the application/xml item. + let xmlItem = container.querySelector("richlistitem[type='application/xml']"); + Assert.ok(xmlItem, "application/xml is present in handlersView"); + if (xmlItem) { + xmlItem.scrollIntoView({ block: "center" }); + xmlItem.closest("richlistbox").selectItem(xmlItem); + + // Open its menu + let list = xmlItem.querySelector(".actionsMenu"); + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(list, {}, win); + await popupShown; + + let handleInternallyItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']` + ); + + ok(!!handleInternallyItem, "handle internally is present"); + } + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_applications_selection.js b/browser/components/preferences/tests/browser_applications_selection.js new file mode 100644 index 0000000000..683ce76a89 --- /dev/null +++ b/browser/components/preferences/tests/browser_applications_selection.js @@ -0,0 +1,403 @@ +SimpleTest.requestCompleteLog(); +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +let gOldMailHandlers = []; +let gDummyHandlers = []; +let gOriginalPreferredMailHandler; +let gOriginalPreferredPDFHandler; + +registerCleanupFunction(function () { + function removeDummyHandlers(handlers) { + // Remove any of the dummy handlers we created. + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + if ( + gDummyHandlers.some( + h => + h.uriTemplate == + handlers.queryElementAt(i, Ci.nsIWebHandlerApp).uriTemplate + ) + ) { + handlers.removeElementAt(i); + } + } catch (ex) { + /* ignore non-web-app handlers */ + } + } + } + // Re-add the original protocol handlers: + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + removeDummyHandlers(mailHandlers); + mailHandlerInfo.preferredApplicationHandler = gOriginalPreferredMailHandler; + gHandlerService.store(mailHandlerInfo); + + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + removeDummyHandlers(pdfHandlerInfo.possibleApplicationHandlers); + pdfHandlerInfo.preferredApplicationHandler = gOriginalPreferredPDFHandler; + gHandlerService.store(pdfHandlerInfo); + + gBrowser.removeCurrentTab(); +}); + +function scrubMailtoHandlers(handlerInfo) { + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = handlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } +} + +add_setup(async function () { + // Create our dummy handlers + let handler1 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler1.name = "Handler 1"; + handler1.uriTemplate = "https://example.com/first/%s"; + + let handler2 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler2.name = "Handler 2"; + handler2.uriTemplate = "http://example.org/second/%s"; + gDummyHandlers.push(handler1, handler2); + + function substituteWebHandlers(handlerInfo) { + // Append the dummy handlers to replace them: + let handlers = handlerInfo.possibleApplicationHandlers; + handlers.appendElement(handler1); + handlers.appendElement(handler2); + gHandlerService.store(handlerInfo); + } + // Set up our mailto handler test infrastructure. + let mailtoHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + scrubMailtoHandlers(mailtoHandlerInfo); + gOriginalPreferredMailHandler = mailtoHandlerInfo.preferredApplicationHandler; + substituteWebHandlers(mailtoHandlerInfo); + + // Now do the same for pdf handler: + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + // PDF doesn't have built-in web handlers, so no need to scrub. + gOriginalPreferredPDFHandler = pdfHandlerInfo.preferredApplicationHandler; + substituteWebHandlers(pdfHandlerInfo); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + info("Preferences page opened on the general pane."); + + await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList; + info("Apps list loaded."); +}); + +async function selectStandardOptions(itemToUse) { + async function selectItemInPopup(item) { + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + // Synthesizing the mouse on the .actionsMenu menulist somehow just selects + // the top row. Probably something to do with the multiple layers of anon + // content - workaround by using the `.open` setter instead. + list.open = true; + await popupShown; + let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + if (typeof item == "function") { + item = item(); + } + popup.activateItem(item); + await popupHidden; + return item; + } + + let itemType = itemToUse.getAttribute("type"); + // Center the item. Center rather than top so it doesn't get blocked by + // the search header. + itemToUse.scrollIntoView({ block: "center" }); + itemToUse.closest("richlistbox").selectItem(itemToUse); + Assert.ok(itemToUse.selected, "Should be able to select our item."); + // Force reflow to make sure it's visible and the container dropdown isn't + // hidden. + itemToUse.getBoundingClientRect().top; + let list = itemToUse.querySelector(".actionsMenu"); + let popup = list.menupopup; + + // select one of our test cases: + let handlerItem = list.querySelector("menuitem[data-l10n-args*='Handler 1']"); + await selectItemInPopup(handlerItem); + let { preferredAction, alwaysAskBeforeHandling } = + HandlerServiceTestUtils.getHandlerInfo(itemType); + Assert.notEqual( + preferredAction, + Ci.nsIHandlerInfo.alwaysAsk, + "Should have selected something other than 'always ask' (" + itemType + ")" + ); + Assert.ok( + !alwaysAskBeforeHandling, + "Should have turned off asking before handling (" + itemType + ")" + ); + + // Test the alwaysAsk option + let alwaysAskItem = list.getElementsByAttribute( + "action", + Ci.nsIHandlerInfo.alwaysAsk + )[0]; + await selectItemInPopup(alwaysAskItem); + Assert.equal( + list.selectedItem, + alwaysAskItem, + "Should have selected always ask item (" + itemType + ")" + ); + alwaysAskBeforeHandling = + HandlerServiceTestUtils.getHandlerInfo(itemType).alwaysAskBeforeHandling; + Assert.ok( + alwaysAskBeforeHandling, + "Should have turned on asking before handling (" + itemType + ")" + ); + + let useDefaultItem = list.getElementsByAttribute( + "action", + Ci.nsIHandlerInfo.useSystemDefault + ); + useDefaultItem = useDefaultItem && useDefaultItem[0]; + if (useDefaultItem) { + await selectItemInPopup(useDefaultItem); + Assert.equal( + list.selectedItem, + useDefaultItem, + "Should have selected 'use default' item (" + itemType + ")" + ); + preferredAction = + HandlerServiceTestUtils.getHandlerInfo(itemType).preferredAction; + Assert.equal( + preferredAction, + Ci.nsIHandlerInfo.useSystemDefault, + "Should have selected 'use default' (" + itemType + ")" + ); + } else { + // Whether there's a "use default" item depends on the OS, so it's not + // possible to rely on it being the case or not. + info("No 'Use default' item, so not testing (" + itemType + ")"); + } + + // Select a web app item. + let webAppItems = Array.from( + popup.getElementsByAttribute("action", Ci.nsIHandlerInfo.useHelperApp) + ); + webAppItems = webAppItems.filter( + item => item.handlerApp instanceof Ci.nsIWebHandlerApp + ); + Assert.equal( + webAppItems.length, + 2, + "Should have 2 web application handler. (" + itemType + ")" + ); + Assert.notEqual( + webAppItems[0].label, + webAppItems[1].label, + "Should have 2 different web app handlers" + ); + let selectedItem = await selectItemInPopup(webAppItems[0]); + + // Test that the selected item label is the same as the label + // of the menu item. + let win = gBrowser.selectedBrowser.contentWindow; + await win.document.l10n.translateFragment(selectedItem); + await win.document.l10n.translateFragment(itemToUse); + Assert.equal( + selectedItem.label, + itemToUse.querySelector(".actionContainer label").value, + "Should have selected correct item (" + itemType + ")" + ); + let { preferredApplicationHandler } = + HandlerServiceTestUtils.getHandlerInfo(itemType); + preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp); + Assert.equal( + selectedItem.handlerApp.uriTemplate, + preferredApplicationHandler.uriTemplate, + "App should actually be selected in the backend. (" + itemType + ")" + ); + + // select the other web app item + selectedItem = await selectItemInPopup(webAppItems[1]); + + // Test that the selected item label is the same as the label + // of the menu item + await win.document.l10n.translateFragment(selectedItem); + await win.document.l10n.translateFragment(itemToUse); + Assert.equal( + selectedItem.label, + itemToUse.querySelector(".actionContainer label").value, + "Should have selected correct item (" + itemType + ")" + ); + preferredApplicationHandler = + HandlerServiceTestUtils.getHandlerInfo( + itemType + ).preferredApplicationHandler; + preferredApplicationHandler.QueryInterface(Ci.nsIWebHandlerApp); + Assert.equal( + selectedItem.handlerApp.uriTemplate, + preferredApplicationHandler.uriTemplate, + "App should actually be selected in the backend. (" + itemType + ")" + ); +} + +add_task(async function checkDropdownBehavior() { + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First check a protocol handler item. + let mailItem = container.querySelector("richlistitem[type='mailto']"); + Assert.ok(mailItem, "mailItem is present in handlersView."); + await selectStandardOptions(mailItem); + + // Then check a content menu item. + let pdfItem = container.querySelector("richlistitem[type='application/pdf']"); + Assert.ok(pdfItem, "pdfItem is present in handlersView."); + await selectStandardOptions(pdfItem); +}); + +add_task(async function sortingCheck() { + let win = gBrowser.selectedBrowser.contentWindow; + const handlerView = win.document.getElementById("handlersView"); + const typeColumn = win.document.getElementById("typeColumn"); + Assert.ok(typeColumn, "typeColumn is present in handlersView."); + + let expectedNumberOfItems = + handlerView.querySelectorAll("richlistitem").length; + + // Test default sorting + assertSortByType("ascending"); + + const oldDir = typeColumn.getAttribute("sortDirection"); + + // click on an item and sort again: + let itemToUse = handlerView.querySelector("richlistitem[type=mailto]"); + itemToUse.scrollIntoView({ block: "center" }); + itemToUse.closest("richlistbox").selectItem(itemToUse); + + // Test sorting on the type column + typeColumn.click(); + assertSortByType("descending"); + Assert.notEqual( + oldDir, + typeColumn.getAttribute("sortDirection"), + "Sort direction should change" + ); + + typeColumn.click(); + assertSortByType("ascending"); + + const actionColumn = win.document.getElementById("actionColumn"); + Assert.ok(actionColumn, "actionColumn is present in handlersView."); + + // Test sorting on the action column + const oldActionDir = actionColumn.getAttribute("sortDirection"); + actionColumn.click(); + assertSortByAction("ascending"); + Assert.notEqual( + oldActionDir, + actionColumn.getAttribute("sortDirection"), + "Sort direction should change" + ); + + actionColumn.click(); + assertSortByAction("descending"); + + // Restore the default sort order + typeColumn.click(); + assertSortByType("ascending"); + + function assertSortByAction(order) { + Assert.equal( + actionColumn.getAttribute("sortDirection"), + order, + `Sort direction should be ${order}` + ); + let siteItems = handlerView.getElementsByTagName("richlistitem"); + Assert.equal( + siteItems.length, + expectedNumberOfItems, + "Number of items should not change." + ); + for (let i = 0; i < siteItems.length - 1; ++i) { + let aType = siteItems[i].getAttribute("actionDescription").toLowerCase(); + let bType = siteItems[i + 1] + .getAttribute("actionDescription") + .toLowerCase(); + let result = 0; + if (aType > bType) { + result = 1; + } else if (bType > aType) { + result = -1; + } + if (order == "ascending") { + Assert.lessOrEqual( + result, + 0, + "Should sort applications in the ascending order by action" + ); + } else { + Assert.greaterOrEqual( + result, + 0, + "Should sort applications in the descending order by action" + ); + } + } + } + + function assertSortByType(order) { + Assert.equal( + typeColumn.getAttribute("sortDirection"), + order, + `Sort direction should be ${order}` + ); + + let siteItems = handlerView.getElementsByTagName("richlistitem"); + Assert.equal( + siteItems.length, + expectedNumberOfItems, + "Number of items should not change." + ); + for (let i = 0; i < siteItems.length - 1; ++i) { + let aType = siteItems[i].getAttribute("typeDescription").toLowerCase(); + let bType = siteItems[i + 1] + .getAttribute("typeDescription") + .toLowerCase(); + let result = 0; + if (aType > bType) { + result = 1; + } else if (bType > aType) { + result = -1; + } + if (order == "ascending") { + Assert.lessOrEqual( + result, + 0, + "Should sort applications in the ascending order by type" + ); + } else { + Assert.greaterOrEqual( + result, + 0, + "Should sort applications in the descending order by type" + ); + } + } + } +}); diff --git a/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js new file mode 100644 index 0000000000..bacda8a6b4 --- /dev/null +++ b/browser/components/preferences/tests/browser_basic_rebuild_fonts_test.js @@ -0,0 +1,235 @@ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + await gBrowser.contentWindow.gMainPane._selectDefaultLanguageGroupPromise; + await TestUtils.waitForCondition( + () => !gBrowser.contentWindow.Preferences.updateQueued + ); + + let doc = gBrowser.contentDocument; + let contentWindow = gBrowser.contentWindow; + var langGroup = Services.prefs.getComplexValue( + "font.language.group", + Ci.nsIPrefLocalizedString + ).data; + is( + contentWindow.Preferences.get("font.language.group").value, + langGroup, + "Language group should be set correctly." + ); + + let defaultFontType = Services.prefs.getCharPref("font.default." + langGroup); + let fontFamilyPref = "font.name." + defaultFontType + "." + langGroup; + let fontFamily = Services.prefs.getCharPref(fontFamilyPref); + let fontFamilyField = doc.getElementById("defaultFont"); + is(fontFamilyField.value, fontFamily, "Font family should be set correctly."); + + function dispatchMenuItemCommand(menuItem) { + const cmdEvent = doc.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + contentWindow, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + menuItem.dispatchEvent(cmdEvent); + } + + /** + * Return a promise that resolves when the fontFamilyPref changes. + * + * Font prefs are the only ones whose form controls set "delayprefsave", + * which delays the pref change when a user specifies a new value + * for the pref. Thus, in order to confirm that the pref gets changed + * when the test selects a new value in a font field, we need to await + * the change. Awaiting this function does so for fontFamilyPref. + */ + function fontFamilyPrefChanged() { + return new Promise(resolve => { + const observer = { + observe(aSubject, aTopic, aData) { + // Check for an exact match to avoid the ambiguity of nsIPrefBranch's + // prefix-matching algorithm for notifying pref observers. + if (aData == fontFamilyPref) { + Services.prefs.removeObserver(fontFamilyPref, observer); + resolve(); + } + }, + }; + Services.prefs.addObserver(fontFamilyPref, observer); + }); + } + + const menuItems = fontFamilyField.querySelectorAll("menuitem"); + ok(menuItems.length > 1, "There are multiple font menuitems."); + ok(menuItems[0].selected, "The first (default) font menuitem is selected."); + + dispatchMenuItemCommand(menuItems[1]); + ok(menuItems[1].selected, "The second font menuitem is selected."); + + await fontFamilyPrefChanged(); + fontFamily = Services.prefs.getCharPref(fontFamilyPref); + is(fontFamilyField.value, fontFamily, "The font family has been updated."); + + dispatchMenuItemCommand(menuItems[0]); + ok( + menuItems[0].selected, + "The first (default) font menuitem is selected again." + ); + + await fontFamilyPrefChanged(); + fontFamily = Services.prefs.getCharPref(fontFamilyPref); + is(fontFamilyField.value, fontFamily, "The font family has been updated."); + + let defaultFontSize = Services.prefs.getIntPref( + "font.size.variable." + langGroup + ); + let fontSizeField = doc.getElementById("defaultFontSize"); + is( + fontSizeField.value, + "" + defaultFontSize, + "Font size should be set correctly." + ); + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/fonts.xhtml" + ); + doc.getElementById("advancedFonts").click(); + let win = await promiseSubDialogLoaded; + doc = win.document; + + // Simulate a dumb font backend. + win.FontBuilder._enumerator = { + _list: ["MockedFont1", "MockedFont2", "MockedFont3"], + _defaultFont: null, + EnumerateFontsAsync(lang, type) { + return Promise.resolve(this._list); + }, + EnumerateAllFontsAsync() { + return Promise.resolve(this._list); + }, + getDefaultFont() { + return this._defaultFont; + }, + getStandardFamilyName(name) { + return name; + }, + }; + win.FontBuilder._allFonts = null; + win.FontBuilder._langGroupSupported = false; + + let langGroupElement = win.Preferences.get("font.language.group"); + let selectLangsField = doc.getElementById("selectLangs"); + let serifField = doc.getElementById("serif"); + let armenian = "x-armn"; + let western = "x-western"; + + // Await rebuilding of the font lists, which happens asynchronously in + // gFontsDialog._selectLanguageGroup. Testing code needs to call this + // function and await its resolution after changing langGroupElement's value + // (or doing anything else that triggers a call to _selectLanguageGroup). + function fontListsRebuilt() { + return win.gFontsDialog._selectLanguageGroupPromise; + } + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is(serifField.value, "", "Font family should not be set."); + + let armenianSerifElement = win.Preferences.get("font.name.serif.x-armn"); + + langGroupElement.value = western; + await fontListsRebuilt(); + selectLangsField.value = western; + + // Simulate a font backend supporting language-specific enumeration. + // NB: FontBuilder has cached the return value from EnumerateAllFonts(), + // so _allFonts will always have 3 elements regardless of subsequent + // _list changes. + win.FontBuilder._enumerator._list = ["MockedFont2"]; + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "", + "Font family should still be empty for indicating using 'default' font." + ); + + langGroupElement.value = western; + await fontListsRebuilt(); + selectLangsField.value = western; + + // Simulate a system that has no fonts for the specified language. + win.FontBuilder._enumerator._list = []; + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is(serifField.value, "", "Font family should not be set."); + + // Setting default font to "MockedFont3". Then, when serifField.value is + // empty, it should indicate using "MockedFont3" but it shouldn't be saved + // to "MockedFont3" in the pref. It should be resolved at runtime. + win.FontBuilder._enumerator._list = [ + "MockedFont1", + "MockedFont2", + "MockedFont3", + ]; + win.FontBuilder._enumerator._defaultFont = "MockedFont3"; + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "", + "Font family should be empty even if there is a default font." + ); + + armenianSerifElement.value = "MockedFont2"; + serifField.value = "MockedFont2"; + is( + serifField.value, + "MockedFont2", + 'Font family should be "MockedFont2" for now.' + ); + + langGroupElement.value = western; + await fontListsRebuilt(); + selectLangsField.value = western; + is(serifField.value, "", "Font family of other language should not be set."); + + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "MockedFont2", + "Font family should not be changed even after switching the language." + ); + + // If MochedFont2 is removed from the system, the value should be treated + // as empty (i.e., 'default' font) after rebuilding the font list. + win.FontBuilder._enumerator._list = ["MockedFont1", "MockedFont3"]; + win.FontBuilder._enumerator._allFonts = ["MockedFont1", "MockedFont3"]; + serifField.removeAllItems(); // This will cause rebuilding the font list from available fonts. + langGroupElement.value = armenian; + await fontListsRebuilt(); + selectLangsField.value = armenian; + is( + serifField.value, + "", + "Font family should become empty due to the font uninstalled." + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_browser_languages_subdialog.js b/browser/components/preferences/tests/browser_browser_languages_subdialog.js new file mode 100644 index 0000000000..8b57bf08a8 --- /dev/null +++ b/browser/components/preferences/tests/browser_browser_languages_subdialog.js @@ -0,0 +1,1058 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const BROWSER_LANGUAGES_URL = + "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml"; +const DICTIONARY_ID_PL = "pl@dictionaries.addons.mozilla.org"; +const TELEMETRY_CATEGORY = "intl.ui.browserLanguage"; + +function langpackId(locale) { + return `langpack-${locale}@firefox.mozilla.org`; +} + +function getManifestData(locale, version = "2.0") { + return { + langpack_id: locale, + name: `${locale} Language Pack`, + description: `${locale} Language pack`, + languages: { + [locale]: { + chrome_resources: { + branding: `browser/chrome/${locale}/locale/branding/`, + }, + version: "1", + }, + }, + browser_specific_settings: { + gecko: { + id: langpackId(locale), + strict_min_version: AppConstants.MOZ_APP_VERSION, + strict_max_version: AppConstants.MOZ_APP_VERSION, + }, + }, + version, + manifest_version: 2, + sources: { + browser: { + base_path: "browser/", + }, + }, + author: "Mozilla", + }; +} + +let testLocales = ["fr", "pl", "he"]; +let testLangpacks; + +function createLangpack(locale, version) { + return AddonTestUtils.createTempXPIFile({ + "manifest.json": getManifestData(locale, version), + [`browser/${locale}/branding/brand.ftl`]: "-brand-short-name = Firefox", + }); +} + +function createTestLangpacks() { + if (!testLangpacks) { + testLangpacks = Promise.all( + testLocales.map(async locale => [locale, await createLangpack(locale)]) + ); + } + return testLangpacks; +} + +function createLocaleResult(target_locale, url) { + return { + guid: langpackId(target_locale), + type: "language", + target_locale, + current_compatible_version: { + files: [ + { + platform: "all", + url, + }, + ], + }, + }; +} + +async function createLanguageToolsFile() { + let langpacks = await createTestLangpacks(); + let results = langpacks.map(([locale, file]) => + createLocaleResult(locale, Services.io.newFileURI(file).spec) + ); + + let filename = "language-tools.json"; + let files = { [filename]: { results } }; + let tempdir = AddonTestUtils.tempDir.clone(); + let dir = await AddonTestUtils.promiseWriteFilesToDir(tempdir.path, files); + dir.append(filename); + + return dir; +} + +async function createDictionaryBrowseResults() { + let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/")); + let dictionaryPath = testDir + "/addons/pl-dictionary.xpi"; + let filename = "dictionaries.json"; + let response = { + page_size: 25, + page_count: 1, + count: 1, + results: [ + { + current_version: { + id: 1823648, + compatibility: { + firefox: { max: "9999", min: "4.0" }, + }, + files: [ + { + platform: "all", + url: dictionaryPath, + }, + ], + version: "1.0.20160228", + }, + default_locale: "pl", + description: "Polish spell-check", + guid: DICTIONARY_ID_PL, + name: "Polish Dictionary", + slug: "polish-spellchecker-dictionary", + status: "public", + summary: "Polish dictionary", + type: "dictionary", + }, + ], + }; + + let files = { [filename]: response }; + let dir = await AddonTestUtils.promiseWriteFilesToDir( + AddonTestUtils.tempDir.path, + files + ); + dir.append(filename); + + return dir; +} + +function assertLocaleOrder(list, locales) { + is( + list.itemCount, + locales.split(",").length, + "The right number of locales are selected" + ); + is( + Array.from(list.children) + .map(child => child.value) + .join(","), + locales, + "The selected locales are in order" + ); +} + +function assertAvailableLocales(list, locales) { + let items = Array.from(list.menupopup.children); + let listLocales = items.filter(item => item.value && item.value != "search"); + is( + listLocales.length, + locales.length, + "The right number of locales are available" + ); + is( + listLocales + .map(item => item.value) + .sort() + .join(","), + locales.sort().join(","), + "The available locales match" + ); + is(items[0].getAttribute("class"), "label-item", "The first row is a label"); +} + +function getDialogId(dialogDoc) { + return dialogDoc.ownerGlobal.arguments[0].telemetryId; +} + +function assertTelemetryRecorded(events) { + let snapshot = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + true + ); + + // Make sure we got some data. + ok( + snapshot.parent && !!snapshot.parent.length, + "Got parent telemetry events in the snapshot" + ); + + // Only look at the related events after stripping the timestamp and category. + let relatedEvents = snapshot.parent + .filter(([timestamp, category]) => category == TELEMETRY_CATEGORY) + .map(relatedEvent => relatedEvent.slice(2, 6)); + + // Events are now an array of: method, object[, value[, extra]] as expected. + Assert.deepEqual(relatedEvents, events, "The events are recorded correctly"); +} + +async function selectLocale(localeCode, available, selected, dialogDoc) { + let [locale] = Array.from(available.menupopup.children).filter( + item => item.value == localeCode + ); + available.selectedItem = locale; + + // Get ready for the selected list to change. + let added = waitForMutation(selected, { childList: true }, target => + Array.from(target.children).some(el => el.value == localeCode) + ); + + // Add the locale. + dialogDoc.getElementById("add").doCommand(); + + // Wait for the list to update. + await added; +} + +async function openDialog(doc, search = false) { + let dialogLoaded = promiseLoadSubDialog(BROWSER_LANGUAGES_URL); + if (search) { + doc.getElementById("primaryBrowserLocaleSearch").doCommand(); + doc.getElementById("primaryBrowserLocale").menupopup.hidePopup(); + } else { + doc.getElementById("manageBrowserLanguagesButton").doCommand(); + } + let dialogWin = await dialogLoaded; + let dialogDoc = dialogWin.document; + return { + dialog: dialogDoc.getElementById("BrowserLanguagesDialog"), + dialogDoc, + available: dialogDoc.getElementById("availableLocales"), + selected: dialogDoc.getElementById("selectedLocales"), + }; +} + +add_task(async function testDisabledBrowserLanguages() { + let langpacksFile = await createLanguageToolsFile(); + let langpacksUrl = Services.io.newFileURI(langpacksFile).spec; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,pl,he,de"], + ["extensions.langpacks.signatures.required", false], + ["extensions.getAddons.langpacks.url", langpacksUrl], + ], + }); + + // Install an old pl langpack. + let oldLangpack = await createLangpack("pl", "1.0"); + await AddonTestUtils.promiseInstallFile(oldLangpack); + + // Install all the other available langpacks. + let pl; + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + if (locale == "pl") { + pl = await AddonManager.getAddonByID(langpackId("pl")); + // Disable pl so it's removed from selected. + await pl.disable(); + return pl; + } + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let { dialogDoc, available, selected } = await openDialog(doc); + + // pl is not selected since it's disabled. + is(pl.userDisabled, true, "pl is disabled"); + is(pl.version, "1.0", "pl is the old 1.0 version"); + assertLocaleOrder(selected, "en-US,he"); + + // Wait for the children menu to be populated. + await BrowserTestUtils.waitForCondition( + () => !!available.children.length, + "Children list populated" + ); + + // Only fr is enabled and not selected, so it's the only locale available. + assertAvailableLocales(available, ["fr"]); + + // Search for more languages. + available.menupopup.lastElementChild.doCommand(); + available.menupopup.hidePopup(); + await waitForMutation(available.menupopup, { childList: true }, target => + Array.from(available.menupopup.children).some( + locale => locale.value == "pl" + ) + ); + + // pl is now available since it is available remotely. + assertAvailableLocales(available, ["fr", "pl"]); + + let installId = null; + AddonTestUtils.promiseInstallEvent("onInstallEnded").then(([install]) => { + installId = install.installId; + }); + + // Add pl. + await selectLocale("pl", available, selected, dialogDoc); + assertLocaleOrder(selected, "pl,en-US,he"); + + // Find pl again since it's been upgraded. + pl = await AddonManager.getAddonByID(langpackId("pl")); + is(pl.userDisabled, false, "pl is now enabled"); + is(pl.version, "2.0", "pl is upgraded to version 2.0"); + + let dialogId = getDialogId(dialogDoc); + ok(dialogId, "There's a dialogId"); + ok(installId, "There's an installId"); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + ["manage", "main", dialogId], + ["search", "dialog", dialogId], + ["add", "dialog", dialogId, { installId }], + + // Cancel is recorded when the tab is closed. + ["cancel", "dialog", dialogId], + ]); +}); + +add_task(async function testReorderingBrowserLanguages() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,pl,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Install all the available langpacks. + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // Open the dialog. + let { dialog, dialogDoc, selected } = await openDialog(doc); + let firstDialogId = getDialogId(dialogDoc); + + // The initial order is set by the pref, filtered by available. + assertLocaleOrder(selected, "en-US,pl,he"); + + // Moving pl down changes the order. + selected.selectedItem = selected.querySelector("[value='pl']"); + dialogDoc.getElementById("down").doCommand(); + assertLocaleOrder(selected, "en-US,he,pl"); + + // Accepting the change shows the confirm message bar. + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + + // The message bar uses async `formatValues` and that may resolve + // after the dialog is closed. + await BrowserTestUtils.waitForMutationCondition( + messageBar, + { attributes: true }, + () => !messageBar.hidden + ); + is( + messageBar.querySelector("button").getAttribute("locales"), + "en-US,he,pl", + "The locales are set on the message bar button" + ); + + // Open the dialog again. + let newDialog = await openDialog(doc); + dialog = newDialog.dialog; + dialogDoc = newDialog.dialogDoc; + let secondDialogId = getDialogId(dialogDoc); + selected = newDialog.selected; + + // The initial order comes from the previous settings. + assertLocaleOrder(selected, "en-US,he,pl"); + + // Select pl in the list. + selected.selectedItem = selected.querySelector("[value='pl']"); + // Move pl back up. + dialogDoc.getElementById("up").doCommand(); + assertLocaleOrder(selected, "en-US,pl,he"); + + // Accepting the change hides the confirm message bar. + dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + is(messageBar.hidden, true, "The message bar is hidden again"); + + ok(firstDialogId, "There was an id on the first dialog"); + ok(secondDialogId, "There was an id on the second dialog"); + ok(firstDialogId != secondDialogId, "The dialog ids are different"); + ok( + parseInt(firstDialogId) < parseInt(secondDialogId), + "The second dialog id is larger than the first" + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + ["manage", "main", firstDialogId], + ["reorder", "dialog", firstDialogId], + ["accept", "dialog", firstDialogId], + ["manage", "main", secondDialogId], + ["reorder", "dialog", secondDialogId], + ["accept", "dialog", secondDialogId], + ]); +}); + +add_task(async function testAddAndRemoveSelectedLanguages() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // Open the dialog. + let { dialog, dialogDoc, available, selected } = await openDialog(doc); + let dialogId = getDialogId(dialogDoc); + + // loadLocalesFromAMO is async but `initAvailableLocales` doesn't wait + // for it to be resolved, so we have to wait for the list to be populated + // before we test for its values. + await BrowserTestUtils.waitForMutationCondition( + available.menupopup, + { attributes: true, childList: true }, + () => { + let listLocales = Array.from(available.menupopup.children).filter( + item => item.value && item.value != "search" + ); + return listLocales.length == 3; + } + ); + // The initial order is set by the pref. + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "pl", "he"]); + + // Add pl and fr to selected. + await selectLocale("pl", available, selected, dialogDoc); + await selectLocale("fr", available, selected, dialogDoc); + + assertLocaleOrder(selected, "fr,pl,en-US"); + assertAvailableLocales(available, ["he"]); + + // Remove pl and fr from selected. + dialogDoc.getElementById("remove").doCommand(); + dialogDoc.getElementById("remove").doCommand(); + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "pl", "he"]); + + // Add he to selected. + await selectLocale("he", available, selected, dialogDoc); + assertLocaleOrder(selected, "he,en-US"); + assertAvailableLocales(available, ["pl", "fr"]); + + // Accepting the change shows the confirm message bar. + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + + await waitForMutation( + messageBar, + { attributes: true, attributeFilter: ["hidden"] }, + target => !target.hidden + ); + + is(messageBar.hidden, false, "The message bar is now visible"); + is( + messageBar.querySelector("button").getAttribute("locales"), + "he,en-US", + "The locales are set on the message bar button" + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + ["manage", "main", dialogId], + + // Install id is not recorded since it was already installed. + ["add", "dialog", dialogId], + ["add", "dialog", dialogId], + + ["remove", "dialog", dialogId], + ["remove", "dialog", dialogId], + + ["add", "dialog", dialogId], + ["accept", "dialog", dialogId], + ]); +}); + +add_task(async function testInstallFromAMO() { + let langpacks = await AddonManager.getAddonsByTypes(["locale"]); + is(langpacks.length, 0, "There are no langpacks installed"); + + let langpacksFile = await createLanguageToolsFile(); + let langpacksUrl = Services.io.newFileURI(langpacksFile).spec; + let dictionaryBrowseFile = await createDictionaryBrowseResults(); + let browseApiEndpoint = Services.io.newFileURI(dictionaryBrowseFile).spec; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US"], + ["extensions.getAddons.langpacks.url", langpacksUrl], + ["extensions.langpacks.signatures.required", false], + ["extensions.getAddons.get.url", browseApiEndpoint], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // Verify only en-US is listed on the main pane. + let getMainPaneLocales = () => { + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + return availableLocales + .map(item => item.value) + .sort() + .join(","); + }; + is(getMainPaneLocales(), "en-US,search", "Only en-US installed to start"); + + // Open the dialog. + let { dialog, dialogDoc, available, selected } = await openDialog(doc, true); + let firstDialogId = getDialogId(dialogDoc); + + // Make sure the message bar is still hidden. + is( + messageBar.hidden, + true, + "The message bar is still hidden after searching" + ); + + if (available.itemCount == 1) { + await waitForMutation( + available.menupopup, + { childList: true }, + target => available.itemCount > 1 + ); + } + + // The initial order is set by the pref. + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "he", "pl"]); + is( + Services.locale.availableLocales.join(","), + "en-US", + "There is only one installed locale" + ); + + // Verify that there are no extra dictionaries. + let dicts = await AddonManager.getAddonsByTypes(["dictionary"]); + is(dicts.length, 0, "There are no installed dictionaries"); + + let installId = null; + AddonTestUtils.promiseInstallEvent("onInstallEnded").then(([install]) => { + installId = install.installId; + }); + + // Add Polish, this will install the langpack. + await selectLocale("pl", available, selected, dialogDoc); + + ok(installId, "We got an installId for the langpack installation"); + + let langpack = await AddonManager.getAddonByID(langpackId("pl")); + Assert.deepEqual( + langpack.installTelemetryInfo, + { source: "about:preferences" }, + "The source is set to preferences" + ); + + // Verify the list is correct. + assertLocaleOrder(selected, "pl,en-US"); + assertAvailableLocales(available, ["fr", "he"]); + is( + Services.locale.availableLocales.sort().join(","), + "en-US,pl", + "Polish is now installed" + ); + + await BrowserTestUtils.waitForCondition(async () => { + let newDicts = await AddonManager.getAddonsByTypes(["dictionary"]); + let done = !!newDicts.length; + + if (done) { + is( + newDicts[0].id, + DICTIONARY_ID_PL, + "The polish dictionary was installed" + ); + } + + return done; + }); + + // Move pl down the list, which prevents an error since it isn't valid. + dialogDoc.getElementById("down").doCommand(); + assertLocaleOrder(selected, "en-US,pl"); + + // Test that disabling the langpack removes it from the list. + let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "dialogclosing"); + dialog.acceptDialog(); + await dialogClosed; + + // Verify pl is now available to select. + is(getMainPaneLocales(), "en-US,pl,search", "en-US and pl now available"); + + // Disable the Polish langpack. + langpack = await AddonManager.getAddonByID("langpack-pl@firefox.mozilla.org"); + await langpack.disable(); + + ({ dialogDoc, available, selected } = await openDialog(doc, true)); + let secondDialogId = getDialogId(dialogDoc); + + // Wait for the available langpacks to load. + if (available.itemCount == 1) { + await waitForMutation( + available.menupopup, + { childList: true }, + target => available.itemCount > 1 + ); + } + assertLocaleOrder(selected, "en-US"); + assertAvailableLocales(available, ["fr", "he", "pl"]); + + // Uninstall the langpack and dictionary. + let installs = await AddonManager.getAddonsByTypes(["locale", "dictionary"]); + is(installs.length, 2, "There is one langpack and one dictionary installed"); + await Promise.all(installs.map(item => item.uninstall())); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([ + // First dialog installs a locale and accepts. + ["search", "main", firstDialogId], + // It has an installId since it was downloaded. + ["add", "dialog", firstDialogId, { installId }], + // It got moved down to avoid errors with finding translations. + ["reorder", "dialog", firstDialogId], + ["accept", "dialog", firstDialogId], + + // The second dialog just checks the state and is closed with the tab. + ["search", "main", secondDialogId], + ["cancel", "dialog", secondDialogId], + ]); +}); + +let hasSearchOption = popup => + Array.from(popup.children).some(el => el.value == "search"); + +add_task(async function testDownloadEnabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let defaultMenulist = doc.getElementById("primaryBrowserLocale"); + ok( + hasSearchOption(defaultMenulist.menupopup), + "There's a search option in the General pane" + ); + + let { available } = await openDialog(doc, false); + ok( + hasSearchOption(available.menupopup), + "There's a search option in the dialog" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testDownloadDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", false], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let defaultMenulist = doc.getElementById("primaryBrowserLocale"); + ok( + !hasSearchOption(defaultMenulist.menupopup), + "There's no search option in the General pane" + ); + + let { available } = await openDialog(doc, false); + ok( + !hasSearchOption(available.menupopup), + "There's no search option in the dialog" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testReorderMainPane() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", false], + ["intl.multilingual.liveReload", false], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + let availableCodes = availableLocales + .map(item => item.value) + .sort() + .join(","); + is( + availableCodes, + "en-US,fr,he,pl", + "All of the available locales are listed" + ); + + is(available.selectedItem.value, "en-US", "English is selected"); + + let hebrew = availableLocales.find(item => item.value == "he"); + hebrew.click(); + available.menupopup.hidePopup(); + + await BrowserTestUtils.waitForCondition( + () => !messageBar.hidden, + "Wait for message bar to show" + ); + + is(messageBar.hidden, false, "The message bar is now shown"); + is( + messageBar.querySelector("button").getAttribute("locales"), + "he,en-US", + "The locales are set on the message bar button" + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); + +add_task(async function testLiveLanguageReloading() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", true], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,fr,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale starts as English." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order starts as what was initially requested." + ); + + // French and English are both LTR languages. + let french = availableLocales.find(item => item.value == "fr"); + + french.click(); + available.menupopup.hidePopup(); + + is( + Services.locale.appLocaleAsBCP47, + "fr", + "The app locale was changed to French" + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["fr", "en-US", "he", "de"], + "The locale order is switched to french first." + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); + +add_task(async function testLiveLanguageReloadingBidiOff() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", true], + ["intl.multilingual.liveReloadBidirectional", false], + ["intl.locale.requested", "en-US,fr,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale starts as English." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order starts as what was initially requested." + ); + + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // English is LTR and Hebrew is RTL. + let hebrew = availableLocales.find(item => item.value == "he"); + + hebrew.click(); + available.menupopup.hidePopup(); + + await BrowserTestUtils.waitForCondition( + () => !messageBar.hidden, + "Wait for message bar to show" + ); + + is(messageBar.hidden, false, "The message bar is now shown"); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale remains in English" + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order did not change." + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); + +add_task(async function testLiveLanguageReloadingBidiOn() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["intl.multilingual.enabled", true], + ["intl.multilingual.downloadEnabled", true], + ["intl.multilingual.liveReload", true], + ["intl.multilingual.liveReloadBidirectional", true], + ["intl.locale.requested", "en-US,fr,he,de"], + ["extensions.langpacks.signatures.required", false], + ], + }); + + // Clear the telemetry from other tests. + Services.telemetry.clearEvents(); + + let langpacks = await createTestLangpacks(); + let addons = await Promise.all( + langpacks.map(async ([locale, file]) => { + let install = await AddonTestUtils.promiseInstallFile(file); + return install.addon; + }) + ); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + + let available = doc.getElementById("primaryBrowserLocale"); + let availableLocales = Array.from(available.menupopup.children); + + is( + Services.locale.appLocaleAsBCP47, + "en-US", + "The app locale starts as English." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["en-US", "fr", "he", "de"], + "The locale order starts as what was initially requested." + ); + + let messageBar = doc.getElementById("confirmBrowserLanguage"); + is(messageBar.hidden, true, "The message bar is hidden at first"); + + // English is LTR and Hebrew is RTL. + let hebrew = availableLocales.find(item => item.value == "he"); + + hebrew.click(); + available.menupopup.hidePopup(); + + is(messageBar.hidden, true, "The message bar is still hidden"); + + is( + Services.locale.appLocaleAsBCP47, + "he", + "The app locale was changed to Hebrew." + ); + + Assert.deepEqual( + Services.locale.requestedLocales, + ["he", "en-US", "fr", "de"], + "The locale changed with Hebrew first." + ); + + await Promise.all(addons.map(addon => addon.uninstall())); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + assertTelemetryRecorded([["reorder", "main"]]); +}); diff --git a/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js b/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js new file mode 100644 index 0000000000..bc928656ab --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1018066_resetScrollPosition.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var originalWindowHeight; +registerCleanupFunction(function () { + window.resizeTo(window.outerWidth, originalWindowHeight); + while (gBrowser.tabs[1]) { + gBrowser.removeTab(gBrowser.tabs[1]); + } +}); + +add_task(async function () { + originalWindowHeight = window.outerHeight; + window.resizeTo(window.outerWidth, 300); + let prefs = await openPreferencesViaOpenPreferencesAPI("paneSearch", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneSearch", "Search pane was selected"); + let mainContent = gBrowser.contentDocument.querySelector(".main-content"); + mainContent.scrollTop = 50; + is(mainContent.scrollTop, 50, "main-content should be scrolled 50 pixels"); + + await gBrowser.contentWindow.gotoPref("paneGeneral"); + + is( + mainContent.scrollTop, + 0, + "Switching to a different category should reset the scroll position" + ); +}); diff --git a/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js new file mode 100644 index 0000000000..26e79b648d --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1020245_openPreferences_to_paneContent.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test opening to the differerent panes and subcategories in Preferences +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy"); + is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected"); + prefs = await openPreferencesViaHash("privacy"); + is( + prefs.selectedPane, + "panePrivacy", + "Privacy pane is selected when hash is 'privacy'" + ); + prefs = await openPreferencesViaOpenPreferencesAPI("nonexistant-category"); + is( + prefs.selectedPane, + "paneGeneral", + "General pane is selected by default when a nonexistant-category is requested" + ); + prefs = await openPreferencesViaHash("nonexistant-category"); + is( + prefs.selectedPane, + "paneGeneral", + "General pane is selected when hash is a nonexistant-category" + ); + prefs = await openPreferencesViaHash(); + is(prefs.selectedPane, "paneGeneral", "General pane is selected by default"); + prefs = await openPreferencesViaOpenPreferencesAPI("privacy-reports", { + leaveOpen: true, + }); + is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default"); + let doc = gBrowser.contentDocument; + is( + doc.location.hash, + "#privacy", + "The subcategory should be removed from the URI" + ); + await TestUtils.waitForCondition( + () => doc.querySelector(".spotlight"), + "Wait for the reports section is spotlighted." + ); + is( + doc.querySelector(".spotlight").getAttribute("data-subcategory"), + "reports", + "The reports section is spotlighted." + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test opening Preferences with subcategory on an existing Preferences tab. See bug 1358475. +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("general", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane is selected by default"); + let doc = gBrowser.contentDocument; + is( + doc.location.hash, + "#general", + "The subcategory should be removed from the URI" + ); + // The reasons that here just call the `openPreferences` API without the helping function are + // - already opened one about:preferences tab up there and + // - the goal is to test on the existing tab and + // - using `openPreferencesViaOpenPreferencesAPI` would introduce more handling of additional about:blank and unneccessary event + await openPreferences("privacy-reports"); + let selectedPane = gBrowser.contentWindow.history.state; + is(selectedPane, "panePrivacy", "Privacy pane should be selected"); + is( + doc.location.hash, + "#privacy", + "The subcategory should be removed from the URI" + ); + await TestUtils.waitForCondition( + () => doc.querySelector(".spotlight"), + "Wait for the reports section is spotlighted." + ); + is( + doc.querySelector(".spotlight").getAttribute("data-subcategory"), + "reports", + "The reports section is spotlighted." + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test opening to a subcategory displays the correct values for preferences +add_task(async function () { + // Skip if crash reporting isn't enabled since the checkbox will be missing. + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", true]], + }); + await openPreferencesViaOpenPreferencesAPI("privacy-reports", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + ok( + doc.querySelector("#automaticallySubmitCrashesBox").checked, + "Checkbox for automatically submitting crashes should be checked when the pref is true and only Reports are requested" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function () { + // Skip if crash reporting isn't enabled since the checkbox will be missing. + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", false]], + }); + await openPreferencesViaOpenPreferencesAPI("privacy-reports", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + ok( + !doc.querySelector("#automaticallySubmitCrashesBox").checked, + "Checkbox for automatically submitting crashes should not be checked when the pref is false only Reports are requested" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +function openPreferencesViaHash(aPane) { + return new Promise(resolve => { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:preferences" + (aPane ? "#" + aPane : "") + ); + let newTabBrowser = gBrowser.selectedBrowser; + + newTabBrowser.addEventListener( + "Initialized", + function () { + newTabBrowser.contentWindow.addEventListener( + "load", + async function () { + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + await finalPrefPaneLoaded; + gBrowser.removeCurrentTab(); + resolve({ selectedPane }); + }, + { once: true } + ); + }, + { capture: true, once: true } + ); + }); +} diff --git a/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js new file mode 100644 index 0000000000..ef387f9a9d --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.js @@ -0,0 +1,116 @@ +const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; + +add_task(async function () { + waitForExplicitFinish(); + + const tabURL = + getRootDirectory(gTestPath) + + "browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml"; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: tabURL }, + async function (browser) { + let doc = browser.contentDocument; + let container = doc.getElementById("container"); + + // Test button + let button = doc.getElementById("button"); + button.focus(); + let initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + await checkPageScrolling(container, "button", initialScrollTop); + + // Test checkbox + let checkbox = doc.getElementById("checkbox"); + checkbox.focus(); + initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + ok(checkbox.checked, "Checkbox is checked"); + await checkPageScrolling(container, "checkbox", initialScrollTop); + + // Test radio + let radiogroup = doc.getElementById("radiogroup"); + radiogroup.focus(); + initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + await checkPageScrolling(container, "radio", initialScrollTop); + } + ); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#search" }, + async function (browser) { + let doc = browser.contentDocument; + let container = doc.getElementsByClassName("main-content")[0]; + + // Test search + let engineList = doc.getElementById("engineList"); + engineList.focus(); + let initialScrollTop = container.scrollTop; + EventUtils.sendString(" "); + is( + engineList.view.selection.currentIndex, + 0, + "Search engineList is selected" + ); + EventUtils.sendString(" "); + await checkPageScrolling( + container, + "search engineList", + initialScrollTop + ); + } + ); + + // Test session restore + const CRASH_URL = "about:mozilla"; + const CRASH_FAVICON = "chrome://branding/content/icon32.png"; + const CRASH_SHENTRY = { url: CRASH_URL }; + const CRASH_TAB = { entries: [CRASH_SHENTRY], image: CRASH_FAVICON }; + const CRASH_STATE = { windows: [{ tabs: [CRASH_TAB] }] }; + + const TAB_URL = "about:sessionrestore"; + const TAB_FORMDATA = { url: TAB_URL, id: { sessionData: CRASH_STATE } }; + const TAB_SHENTRY = { url: TAB_URL, triggeringPrincipal_base64 }; + const TAB_STATE = { entries: [TAB_SHENTRY], formdata: TAB_FORMDATA }; + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + + // Fake a post-crash tab + SessionStore.setTabState(tab, JSON.stringify(TAB_STATE)); + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let doc = tab.linkedBrowser.contentDocument; + + // Make body scrollable + doc.body.style.height = doc.body.clientHeight + 100 + "px"; + + let tabsToggle = doc.getElementById("tabsToggle"); + tabsToggle.focus(); + let initialScrollTop = doc.documentElement.scrollTop; + EventUtils.sendString(" "); + await checkPageScrolling( + doc.documentElement, + "session restore", + initialScrollTop + ); + + gBrowser.removeCurrentTab(); + finish(); +}); + +function checkPageScrolling(container, type, initialScrollTop = 0) { + return new Promise(resolve => { + setTimeout(() => { + is( + container.scrollTop, + initialScrollTop, + "Page should not scroll when " + type + " flipped" + ); + resolve(); + }, 0); + }); +} diff --git a/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml new file mode 100644 index 0000000000..8b3e39a39c --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1184989_prevent_scrolling_when_preferences_flipped.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- + XUL Widget Test for Bug 1184989 + --> +<window title="Bug 1184989 Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<vbox id="container" style="height: 200px; overflow: auto;"> + <vbox style="height: 500px;"> + <hbox> + <button id="button" label="button" /> + </hbox> + + <hbox> + <checkbox id="checkbox" label="checkbox" /> + </hbox> + + <hbox> + <radiogroup id="radiogroup"> + <radio id="radio" label="radio" /> + </radiogroup> + </hbox> + </vbox> +</vbox> + +</window> diff --git a/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js b/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js new file mode 100644 index 0000000000..cd5b00b5f2 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1547020_lockedDownloadDir.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + Services.prefs.lockPref("browser.download.useDownloadDir"); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + var downloadFolder = doc.getElementById("downloadFolder"); + var chooseFolder = doc.getElementById("chooseFolder"); + is( + downloadFolder.disabled, + false, + "Download folder field should not be disabled." + ); + is(chooseFolder.disabled, false, "Choose folder should not be disabled."); + + gBrowser.removeCurrentTab(); + + Services.prefs.unlockPref("browser.download.useDownloadDir"); +}); diff --git a/browser/components/preferences/tests/browser_bug1579418.js b/browser/components/preferences/tests/browser_bug1579418.js new file mode 100644 index 0000000000..a179ae9936 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug1579418.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function default_homepage_test() { + let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage"); + let oldStartpagePref = Services.prefs.getIntPref("browser.startup.page"); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + let homeMode = doc.getElementById("homeMode"); + let customSettings = doc.getElementById("customSettings"); + + // HOME_MODE_FIREFOX_HOME + homeMode.value = 0; + + homeMode.dispatchEvent(new Event("command")); + + is(Services.prefs.getCharPref("browser.startup.homepage"), "about:home"); + + // HOME_MODE_BLANK + homeMode.value = 1; + + homeMode.dispatchEvent(new Event("command")); + + await TestUtils.waitForCondition( + () => customSettings.hidden === true, + "Wait for customSettings to be hidden." + ); + + is( + Services.prefs.getCharPref("browser.startup.homepage"), + "chrome://browser/content/blanktab.html" + ); + + // HOME_MODE_CUSTOM + homeMode.value = 2; + + homeMode.dispatchEvent(new Event("command")); + + await TestUtils.waitForCondition( + () => customSettings.hidden === false, + "Wait for customSettings to be shown." + ); + + is(customSettings.hidden, false, "homePageURL should be visible"); + + registerCleanupFunction(async () => { + Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref); + Services.prefs.setIntPref("browser.startup.page", oldStartpagePref); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); diff --git a/browser/components/preferences/tests/browser_bug410900.js b/browser/components/preferences/tests/browser_bug410900.js new file mode 100644 index 0000000000..4fd4bb21f5 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug410900.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + // Setup a phony handler to ensure the app pane will be populated. + var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler.name = "App pane alive test"; + handler.uriTemplate = "http://test.mozilla.org/%s"; + + var extps = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + var info = extps.getProtocolHandlerInfo("apppanetest"); + info.possibleApplicationHandlers.appendElement(handler); + + var hserv = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + hserv.store(info); + + openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }) + .then(() => gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList) + .then(() => runTest(gBrowser.selectedBrowser.contentWindow)); +} + +function runTest(win) { + var rbox = win.document.getElementById("handlersView"); + ok(rbox, "handlersView is present"); + + var items = rbox && rbox.getElementsByTagName("richlistitem"); + ok(items && !!items.length, "App handler list populated"); + + var handlerAdded = false; + for (let i = 0; i < items.length; i++) { + if (items[i].getAttribute("type") == "apppanetest") { + handlerAdded = true; + } + } + ok(handlerAdded, "apppanetest protocol handler was successfully added"); + + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/components/preferences/tests/browser_bug731866.js b/browser/components/preferences/tests/browser_bug731866.js new file mode 100644 index 0000000000..b090535a49 --- /dev/null +++ b/browser/components/preferences/tests/browser_bug731866.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const browserContainersGroupDisabled = !SpecialPowers.getBoolPref( + "privacy.userContext.ui.enabled" +); +const cookieBannerHandlingDisabled = !SpecialPowers.getBoolPref( + "cookiebanners.ui.desktop.enabled" +); +const updatePrefContainers = ["updatesCategory", "updateApp"]; +const updateContainersGroupDisabled = + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId"); + +function test() { + waitForExplicitFinish(); + open_preferences(runTest); +} + +var gElements; + +function checkElements(expectedPane) { + for (let element of gElements) { + // keyset elements fail is_element_visible checks because they are never visible. + // special-case the drmGroup item because its visibility depends on pref + OS version + if (element.nodeName == "keyset" || element.id === "drmGroup") { + continue; + } + + // The browserContainersGroup is still only pref-on on Nightly + if ( + element.id == "browserContainersGroup" && + browserContainersGroupDisabled + ) { + is_element_hidden( + element, + "Disabled browserContainersGroup should be hidden" + ); + continue; + } + + // Cookie Banner Handling is currently disabled by default (bug 1800679) + if ( + element.id == "cookieBannerHandlingGroup" && + cookieBannerHandlingDisabled + ) { + is_element_hidden( + element, + "Disabled cookieBannerHandlingGroup should be hidden" + ); + continue; + } + + // Update prefs are hidden when running an MSIX build + if ( + updatePrefContainers.includes(element.id) && + updateContainersGroupDisabled + ) { + is_element_hidden(element, "Disabled " + element + " should be hidden"); + continue; + } + + let attributeValue = element.getAttribute("data-category"); + let suffix = " (id=" + element.id + ")"; + if (attributeValue == "pane" + expectedPane) { + is_element_visible( + element, + expectedPane + " elements should be visible" + suffix + ); + } else { + is_element_hidden( + element, + "Elements not in " + expectedPane + " should be hidden" + suffix + ); + } + } +} + +async function runTest(win) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + + let tab = win.document; + gElements = tab.getElementById("mainPrefPane").children; + + let panes = ["General", "Search", "Privacy", "Sync"]; + + for (let pane of panes) { + await win.gotoPref("pane" + pane); + checkElements(pane); + } + + gBrowser.removeCurrentTab(); + win.close(); + finish(); +} diff --git a/browser/components/preferences/tests/browser_bug795764_cachedisabled.js b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js new file mode 100644 index 0000000000..97f5aaf48f --- /dev/null +++ b/browser/components/preferences/tests/browser_bug795764_cachedisabled.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + // Adding one fake site so that the SiteDataManager would run. + // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://www.foo.com" + ); + Services.perms.addFromPrincipal( + principal, + "persistent-storage", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + registerCleanupFunction(function () { + Services.perms.removeFromPrincipal(principal, "persistent-storage"); + }); + + SpecialPowers.pushPrefEnv({ + set: [["privacy.userContext.ui.enabled", true]], + }).then(() => open_preferences(runTest)); +} + +async function runTest(win) { + is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded"); + + let tab = win.document; + let elements = tab.getElementById("mainPrefPane").children; + + // Test if privacy pane is opened correctly + await win.gotoPref("panePrivacy"); + for (let element of elements) { + let attributeValue = element.getAttribute("data-category"); + + // Ignore the cookie banner handling section, as it is currently preffed + // off by default (bug 1800679). + if (element.id === "cookieBannerHandlingGroup") { + continue; + } + + if (attributeValue == "panePrivacy") { + is_element_visible(element, "HTTPSOnly should be visible"); + + is_element_visible( + element, + `Privacy element of id=${element.id} should be visible` + ); + } else { + is_element_hidden( + element, + `Non-Privacy element of id=${element.id} should be hidden` + ); + } + } + + gBrowser.removeCurrentTab(); + win.close(); + finish(); +} diff --git a/browser/components/preferences/tests/browser_cert_export.js b/browser/components/preferences/tests/browser_cert_export.js new file mode 100644 index 0000000000..48769f84e6 --- /dev/null +++ b/browser/components/preferences/tests/browser_cert_export.js @@ -0,0 +1,161 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var MockFilePicker = SpecialPowers.MockFilePicker; + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + info("create testsavedir!"); + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + info("return from createTempSaveDir: " + saveDir.path); + return saveDir; +} + +// Create the folder the certificates will be saved into. +var destDir = createTemporarySaveDirectory(); +registerCleanupFunction(function () { + destDir.remove(true); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +function stringOrArrayEquals(actual, expected, message) { + is( + typeof actual, + typeof expected, + "actual, expected should have the same type" + ); + if (typeof expected == "string") { + is(actual, expected, message); + } else { + is(actual.toString(), expected.toString(), message); + } +} + +var dialogWin; +var exportButton; +var expectedCert; + +async function setupTest() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let certButton = gBrowser.selectedBrowser.contentDocument.getElementById( + "viewCertificatesButton" + ); + certButton.scrollIntoView(); + let certDialogLoaded = promiseLoadSubDialog( + "chrome://pippki/content/certManager.xhtml" + ); + certButton.click(); + dialogWin = await certDialogLoaded; + let doc = dialogWin.document; + doc.getElementById("certmanagertabs").selectedTab = + doc.getElementById("ca_tab"); + let treeView = doc.getElementById("ca-tree").view; + // Select any which cert. Ignore parent rows (ie rows without certs): + for (let i = 0; i < treeView.rowCount; i++) { + treeView.selection.select(i); + dialogWin.getSelectedCerts(); + let certs = dialogWin.selected_certs; // yuck... but this is how the dialog works. + if (certs && certs.length == 1 && certs[0]) { + expectedCert = certs[0]; + // OK, we managed to select a cert! + break; + } + } + + exportButton = doc.getElementById("ca_exportButton"); + is(exportButton.disabled, false, "Should enable export button"); +} + +async function checkCertExportWorks( + exportType, + encoding, + expectedFileContents +) { + MockFilePicker.displayDirectory = destDir; + var destFile = destDir.clone(); + MockFilePicker.init(window); + MockFilePicker.filterIndex = exportType; + MockFilePicker.showCallback = function (fp) { + info("showCallback"); + let fileName = fp.defaultString; + info("fileName: " + fileName); + destFile.append(fileName); + MockFilePicker.setFiles([destFile]); + info("done showCallback"); + }; + let finishedExporting = TestUtils.topicObserved("cert-export-finished"); + exportButton.click(); + await finishedExporting; + MockFilePicker.cleanup(); + if (destFile && destFile.exists()) { + let contents; + if (encoding === "utf-8") { + contents = await IOUtils.readUTF8(destFile.path); + } else { + is(encoding, "", "expected either utf-8 or empty string for encoding"); + contents = await IOUtils.read(destFile.path); + } + stringOrArrayEquals( + contents, + expectedFileContents, + "Should have written correct contents" + ); + destFile.remove(false); + } else { + ok(false, "No cert saved!"); + } +} + +add_task(setupTest); + +add_task(async function checkCertPEMExportWorks() { + let expectedContents = dialogWin.getPEMString(expectedCert); + await checkCertExportWorks(0, /* 0 = PEM */ "utf-8", expectedContents); +}); + +add_task(async function checkCertPEMChainExportWorks() { + let expectedContents = dialogWin.getPEMString(expectedCert); + await checkCertExportWorks( + 1, // 1 = PEM chain, but the chain is of length 1 + "utf-8", + expectedContents + ); +}); + +add_task(async function checkCertDERExportWorks() { + let expectedContents = Uint8Array.from(expectedCert.getRawDER()); + await checkCertExportWorks(2, /* 2 = DER */ "", expectedContents); +}); + +function stringToTypedArray(str) { + let arr = new Uint8Array(str.length); + for (let i = 0; i < arr.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; +} + +add_task(async function checkCertPKCS7ExportWorks() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + let expectedContents = stringToTypedArray(certdb.asPKCS7Blob([expectedCert])); + await checkCertExportWorks(3, /* 3 = PKCS7 */ "", expectedContents); +}); + +add_task(async function checkCertPKCS7ChainExportWorks() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + let expectedContents = stringToTypedArray(certdb.asPKCS7Blob([expectedCert])); + await checkCertExportWorks( + 4, // 4 = PKCS7 chain, but the chain is of length 1 + "", + expectedContents + ); +}); diff --git a/browser/components/preferences/tests/browser_change_app_handler.js b/browser/components/preferences/tests/browser_change_app_handler.js new file mode 100644 index 0000000000..b4ca92592f --- /dev/null +++ b/browser/components/preferences/tests/browser_change_app_handler.js @@ -0,0 +1,155 @@ +var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); +var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +SimpleTest.requestCompleteLog(); + +function setupFakeHandler() { + let info = gMimeSvc.getFromTypeAndExtension("text/plain", "foo.txt"); + ok( + info.possibleLocalHandlers.length, + "Should have at least one known handler" + ); + let handler = info.possibleLocalHandlers.queryElementAt( + 0, + Ci.nsILocalHandlerApp + ); + + let infoToModify = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + infoToModify.possibleApplicationHandlers.appendElement(handler); + + gHandlerSvc.store(infoToModify); +} + +add_task(async function () { + setupFakeHandler(); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + let ourItem = container.querySelector( + "richlistitem[type='text/x-test-handler']" + ); + ok(ourItem, "handlersView is present"); + ourItem.scrollIntoView(); + container.selectItem(ourItem); + ok(ourItem.selected, "Should be able to select our item."); + + let list = ourItem.querySelector(".actionsMenu"); + + let chooseItem = list.menupopup.querySelector(".choose-app-item"); + let dialogLoadedPromise = promiseLoadSubDialog( + "chrome://global/content/appPicker.xhtml" + ); + let cmdEvent = win.document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + win, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + chooseItem.dispatchEvent(cmdEvent); + + let dialog = await dialogLoadedPromise; + info("Dialog loaded"); + + let dialogDoc = dialog.document; + let dialogElement = dialogDoc.getElementById("app-picker"); + let dialogList = dialogDoc.getElementById("app-picker-listbox"); + dialogList.selectItem(dialogList.firstElementChild); + let selectedApp = dialogList.firstElementChild.handlerApp; + dialogElement.acceptDialog(); + + // Verify results are correct in mime service: + let mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null); + ok( + mimeInfo.preferredApplicationHandler.equals(selectedApp), + "App should be set as preferred." + ); + + // Check that we display this result: + ok(list.selectedItem, "Should have a selected item"); + ok( + mimeInfo.preferredApplicationHandler.equals(list.selectedItem.handlerApp), + "App should be visible as preferred item." + ); + + // Now try to 'manage' this list: + dialogLoadedPromise = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/applicationManager.xhtml" + ); + + let manageItem = list.menupopup.querySelector(".manage-app-item"); + cmdEvent = win.document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + win, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + manageItem.dispatchEvent(cmdEvent); + + dialog = await dialogLoadedPromise; + info("Dialog loaded the second time"); + + dialogDoc = dialog.document; + dialogElement = dialogDoc.getElementById("appManager"); + dialogList = dialogDoc.getElementById("appList"); + let itemToRemove = dialogList.querySelector( + 'richlistitem > label[value="' + selectedApp.name + '"]' + ).parentNode; + dialogList.selectItem(itemToRemove); + let itemsBefore = dialogList.children.length; + dialogDoc.getElementById("remove").click(); + ok(!itemToRemove.parentNode, "Item got removed from DOM"); + is(dialogList.children.length, itemsBefore - 1, "Item got removed"); + dialogElement.acceptDialog(); + + // Verify results are correct in mime service: + mimeInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null); + ok( + !mimeInfo.preferredApplicationHandler, + "App should no longer be set as preferred." + ); + + // Check that we display this result: + ok(list.selectedItem, "Should have a selected item"); + ok( + !list.selectedItem.handlerApp, + "No app should be visible as preferred item." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +registerCleanupFunction(function () { + let infoToModify = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + gHandlerSvc.remove(infoToModify); +}); diff --git a/browser/components/preferences/tests/browser_checkspelling.js b/browser/components/preferences/tests/browser_checkspelling.js new file mode 100644 index 0000000000..a7895b4201 --- /dev/null +++ b/browser/components/preferences/tests/browser_checkspelling.js @@ -0,0 +1,34 @@ +add_task(async function () { + SpecialPowers.pushPrefEnv({ set: [["layout.spellcheckDefault", 2]] }); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let checkbox = doc.querySelector("#checkSpelling"); + is( + checkbox.checked, + Services.prefs.getIntPref("layout.spellcheckDefault") == 2, + "checkbox should represent pref value before clicking on checkbox" + ); + ok( + checkbox.checked, + "checkbox should be checked before clicking on checkbox" + ); + + checkbox.click(); + + is( + checkbox.checked, + Services.prefs.getIntPref("layout.spellcheckDefault") == 2, + "checkbox should represent pref value after clicking on checkbox" + ); + ok( + !checkbox.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_connection.js b/browser/components/preferences/tests/browser_connection.js new file mode 100644 index 0000000000..01cdf571f2 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection.js @@ -0,0 +1,145 @@ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // network.proxy.type needs to be backed up and restored because mochitest + // changes this setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function () { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + // On accepting the dialog, we also write TRR values, so we need to clear + // them. They are tested separately in browser_privacy_dnsoverhttps.js. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + }); + + let connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(async function tabOpened(aContentWindow) { + is( + gBrowser.currentURI.spec, + "about:preferences", + "about:preferences loaded" + ); + let dialog = await openAndLoadSubDialog(connectionURL); + let dialogElement = dialog.document.getElementById("ConnectionsDialog"); + let dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + + ok(dialog, "connection window opened"); + runConnectionTests(dialog); + dialogElement.acceptDialog(); + + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "connection window closed"); + // runConnectionTests will have changed this pref - make sure it was + // sanitized correctly when the dialog was accepted + is( + Services.prefs.getCharPref("network.proxy.no_proxies_on"), + ".a.com,.b.com,.c.com", + "no_proxies_on pref has correct value" + ); + gBrowser.removeCurrentTab(); + finish(); + }); +} + +// run a bunch of tests on the window containing connection.xul +function runConnectionTests(win) { + let doc = win.document; + let networkProxyNone = doc.getElementById("networkProxyNone"); + let networkProxyNonePref = win.Preferences.get("network.proxy.no_proxies_on"); + let networkProxyTypePref = win.Preferences.get("network.proxy.type"); + + // make sure the networkProxyNone textbox is formatted properly + is(networkProxyNone.localName, "textarea", "networkProxyNone is a textarea"); + is( + networkProxyNone.getAttribute("rows"), + "2", + "networkProxyNone textbox has two rows" + ); + + // make sure manual proxy controls are disabled when the window is opened + let networkProxyHTTP = doc.getElementById("networkProxyHTTP"); + is(networkProxyHTTP.disabled, true, "networkProxyHTTP textbox is disabled"); + + // check if sanitizing the given input for the no_proxies_on pref results in + // expected string + function testSanitize(input, expected, errorMessage) { + networkProxyNonePref.value = input; + win.gConnectionsDialog.sanitizeNoProxiesPref(); + is(networkProxyNonePref.value, expected, errorMessage); + } + + // change this pref so proxy exceptions are actually configurable + networkProxyTypePref.value = 1; + is(networkProxyNone.disabled, false, "networkProxyNone textbox is enabled"); + + testSanitize(".a.com", ".a.com", "sanitize doesn't mess up single filter"); + testSanitize( + ".a.com, .b.com, .c.com", + ".a.com, .b.com, .c.com", + "sanitize doesn't mess up multiple comma/space sep filters" + ); + testSanitize( + ".a.com\n.b.com", + ".a.com,.b.com", + "sanitize turns line break into comma" + ); + testSanitize( + ".a.com,\n.b.com", + ".a.com,.b.com", + "sanitize doesn't add duplicate comma after comma" + ); + testSanitize( + ".a.com\n,.b.com", + ".a.com,.b.com", + "sanitize doesn't add duplicate comma before comma" + ); + testSanitize( + ".a.com,\n,.b.com", + ".a.com,,.b.com", + "sanitize doesn't add duplicate comma surrounded by commas" + ); + testSanitize( + ".a.com, \n.b.com", + ".a.com, .b.com", + "sanitize doesn't add comma after comma/space" + ); + testSanitize( + ".a.com\n .b.com", + ".a.com, .b.com", + "sanitize adds comma before space" + ); + testSanitize( + ".a.com\n\n\n;;\n;\n.b.com", + ".a.com,.b.com", + "sanitize only adds one comma per substring of bad chars" + ); + testSanitize( + ".a.com,,.b.com", + ".a.com,,.b.com", + "duplicate commas from user are untouched" + ); + testSanitize( + ".a.com\n.b.com\n.c.com,\n.d.com,\n.e.com", + ".a.com,.b.com,.c.com,.d.com,.e.com", + "sanitize replaces things globally" + ); + + // will check that this was sanitized properly after window closes + networkProxyNonePref.value = ".a.com;.b.com\n.c.com"; +} diff --git a/browser/components/preferences/tests/browser_connection_bug1445991.js b/browser/components/preferences/tests/browser_connection_bug1445991.js new file mode 100644 index 0000000000..ecb5068a26 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug1445991.js @@ -0,0 +1,31 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the disabled status of the autoconfig Reload button when the proxy type +// is autoconfig (network.proxy.type == 2). +add_task(async function testAutoconfigReloadButton() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.proxy.type", 2], + ["network.proxy.autoconfig_url", "file:///nonexistent.pac"], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + const promiseDialogLoaded = promiseLoadSubDialog(connectionURL); + gBrowser.contentDocument.getElementById("connectionSettings").click(); + const dialog = await promiseDialogLoaded; + + ok( + !dialog.document.getElementById("autoReload").disabled, + "Reload button is enabled when proxy type is autoconfig" + ); + + dialog.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_connection_bug1505330.js b/browser/components/preferences/tests/browser_connection_bug1505330.js new file mode 100644 index 0000000000..94dfd4706e --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug1505330.js @@ -0,0 +1,31 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the disabled status of the autoconfig Reload button when the proxy type +// is autoconfig (network.proxy.type == 2). +add_task(async function testAutoconfigReloadButton() { + Services.prefs.lockPref("signon.autologin.proxy"); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + const promiseDialogLoaded = promiseLoadSubDialog(connectionURL); + gBrowser.contentDocument.getElementById("connectionSettings").click(); + const dialog = await promiseDialogLoaded; + + ok( + !dialog.document.getElementById("networkProxyType").firstChild.disabled, + "Connection options should not be disabled" + ); + ok( + dialog.document.getElementById("autologinProxy").disabled, + "Proxy autologin should be disabled" + ); + + dialog.close(); + Services.prefs.unlockPref("signon.autologin.proxy"); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_connection_bug388287.js b/browser/components/preferences/tests/browser_connection_bug388287.js new file mode 100644 index 0000000000..d6f0c3c9d0 --- /dev/null +++ b/browser/components/preferences/tests/browser_connection_bug388287.js @@ -0,0 +1,124 @@ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + const connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + let closeable = false; + let finalTest = false; + + // The changed preferences need to be backed up and restored because this mochitest + // changes them setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function () { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "socks"]) { + Services.prefs.clearUserPref("network.proxy." + proxyType); + Services.prefs.clearUserPref("network.proxy." + proxyType + "_port"); + if (proxyType == "http") { + continue; + } + Services.prefs.clearUserPref("network.proxy.backup." + proxyType); + Services.prefs.clearUserPref( + "network.proxy.backup." + proxyType + "_port" + ); + } + // On accepting the dialog, we also write TRR values, so we need to clear + // them. They are tested separately in browser_privacy_dnsoverhttps.js. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + }); + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(async function tabOpened(aContentWindow) { + let dialog, dialogClosingPromise, dialogElement; + let proxyTypePref, sharePref, httpPref, httpPortPref; + + // Convenient function to reset the variables for the new window + async function setDoc() { + if (closeable) { + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "Connection dialog closed"); + } + + if (finalTest) { + gBrowser.removeCurrentTab(); + finish(); + return; + } + + dialog = await openAndLoadSubDialog(connectionURL); + dialogElement = dialog.document.getElementById("ConnectionsDialog"); + dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + + proxyTypePref = dialog.Preferences.get("network.proxy.type"); + sharePref = dialog.Preferences.get("network.proxy.share_proxy_settings"); + httpPref = dialog.Preferences.get("network.proxy.http"); + httpPortPref = dialog.Preferences.get("network.proxy.http_port"); + } + + // This batch of tests should not close the dialog + await setDoc(); + + // Testing HTTP port 0 with share on + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 0; + dialogElement.acceptDialog(); + + // Testing HTTP port 0 + FTP port 80 with share off + sharePref.value = false; + dialogElement.acceptDialog(); + + // Testing HTTP port 80 + FTP port 0 with share off + httpPortPref.value = 80; + dialogElement.acceptDialog(); + + // From now on, the dialog should close since we are giving it legitimate inputs. + // The test will timeout if the onbeforeaccept kicks in erroneously. + closeable = true; + + // Both ports 80, share on + httpPortPref.value = 80; + dialogElement.acceptDialog(); + + // HTTP 80, FTP 0, with share on + await setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 80; + dialogElement.acceptDialog(); + + // HTTP host empty, port 0 with share on + await setDoc(); + proxyTypePref.value = 1; + sharePref.value = true; + httpPref.value = ""; + httpPortPref.value = 0; + dialogElement.acceptDialog(); + + // HTTP 0, but in no proxy mode + await setDoc(); + proxyTypePref.value = 0; + sharePref.value = true; + httpPref.value = "localhost"; + httpPortPref.value = 0; + + // This is the final test, don't spawn another connection window + finalTest = true; + dialogElement.acceptDialog(); + await setDoc(); + }); +} diff --git a/browser/components/preferences/tests/browser_containers_name_input.js b/browser/components/preferences/tests/browser_containers_name_input.js new file mode 100644 index 0000000000..38785d3cb0 --- /dev/null +++ b/browser/components/preferences/tests/browser_containers_name_input.js @@ -0,0 +1,72 @@ +const CONTAINERS_URL = + "chrome://browser/content/preferences/dialogs/containers.xhtml"; + +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("containers", { leaveOpen: true }); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +add_task(async function () { + async function openDialog() { + let doc = gBrowser.selectedBrowser.contentDocument; + + let dialogPromise = promiseLoadSubDialog(CONTAINERS_URL); + + let addButton = doc.getElementById("containersAdd"); + addButton.doCommand(); + + let dialog = await dialogPromise; + + return dialog.document; + } + + let { contentDocument } = gBrowser.selectedBrowser; + let containerNodes = Array.from( + contentDocument.querySelectorAll("[data-category=paneContainers]") + ); + ok( + containerNodes.find(node => node.getBoundingClientRect().width > 0), + "Should actually be showing the container nodes." + ); + + let doc = await openDialog(); + + let name = doc.getElementById("name"); + let btnApplyChanges = doc.querySelector("dialog").getButton("accept"); + + Assert.equal(name.value, "", "The name textbox should initlally be empty"); + Assert.ok( + btnApplyChanges.disabled, + "The done button should initially be disabled" + ); + + function setName(value) { + name.value = value; + + let event = new doc.defaultView.InputEvent("input", { data: value }); + SpecialPowers.dispatchEvent(doc.defaultView, name, event); + } + + setName("test"); + + Assert.ok( + !btnApplyChanges.disabled, + "The done button should be enabled when the value is not empty" + ); + + setName(""); + + Assert.ok( + btnApplyChanges.disabled, + "The done button should be disabled when the value is empty" + ); + + setName("\u0009\u000B\u000C\u0020\u00A0\uFEFF\u000A\u000D\u2028\u2029"); + + Assert.ok( + btnApplyChanges.disabled, + "The done button should be disabled when the value contains only whitespaces" + ); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking.js b/browser/components/preferences/tests/browser_contentblocking.js new file mode 100644 index 0000000000..19bd45153b --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking.js @@ -0,0 +1,1382 @@ +/* eslint-env webextensions */ + +"use strict"; + +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const TP_PREF = "privacy.trackingprotection.enabled"; +const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled"; +const NCB_PREF = "network.cookie.cookieBehavior"; +const NCBP_PREF = "network.cookie.cookieBehavior.pbmode"; +const CAT_PREF = "browser.contentblocking.category"; +const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled"; +const STP_PREF = "privacy.trackingprotection.socialtracking.enabled"; +const CM_PREF = "privacy.trackingprotection.cryptomining.enabled"; +const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled"; +const EMAIL_TP_PBM_PREF = + "privacy.trackingprotection.emailtracking.pbmode.enabled"; +const LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled"; +const REFERRER_PREF = "network.http.referer.disallowCrossSiteRelaxingDefault"; +const REFERRER_TOP_PREF = + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation"; +const OCSP_PREF = "privacy.partition.network_state.ocsp_cache"; +const QUERY_PARAM_STRIP_PREF = "privacy.query_stripping.enabled"; +const QUERY_PARAM_STRIP_PBM_PREF = "privacy.query_stripping.enabled.pbmode"; +const PREF_TEST_NOTIFICATIONS = + "browser.safebrowsing.test-notifications.enabled"; +const STRICT_PREF = "browser.contentblocking.features.strict"; +const PRIVACY_PAGE = "about:preferences#privacy"; +const ISOLATE_UI_PREF = + "browser.contentblocking.reject-and-isolate-cookies.preferences.ui.enabled"; +const FPI_PREF = "privacy.firstparty.isolate"; + +const { EnterprisePolicyTesting, PoliciesPrefTracker } = + ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + +requestLongerTimeout(2); + +add_task(async function testListUpdate() { + SpecialPowers.pushPrefEnv({ set: [[PREF_TEST_NOTIFICATIONS, true]] }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let fingerprintersCheckbox = doc.getElementById( + "contentBlockingFingerprintersCheckbox" + ); + let updateObserved = TestUtils.topicObserved("safebrowsing-update-attempt"); + fingerprintersCheckbox.click(); + let url = (await updateObserved)[1]; + + ok(true, "Has tried to update after the fingerprinting checkbox was toggled"); + is( + url, + "http://127.0.0.1:8888/safebrowsing-dummy/update", + "Using the correct list url to update" + ); + + let cryptominersCheckbox = doc.getElementById( + "contentBlockingCryptominersCheckbox" + ); + updateObserved = TestUtils.topicObserved("safebrowsing-update-attempt"); + cryptominersCheckbox.click(); + url = (await updateObserved)[1]; + + ok(true, "Has tried to update after the cryptomining checkbox was toggled"); + is( + url, + "http://127.0.0.1:8888/safebrowsing-dummy/update", + "Using the correct list url to update" + ); + + gBrowser.removeCurrentTab(); +}); + +// Tests that the content blocking main category checkboxes have the correct default state. +add_task(async function testContentBlockingMainCategory() { + let prefs = [ + [TP_PREF, false], + [TP_PBM_PREF, true], + [STP_PREF, false], + [NCB_PREF, Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER], + [ + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ISOLATE_UI_PREF, true], + [FPI_PREF, false], + ]; + + for (let pref of prefs) { + switch (typeof pref[1]) { + case "boolean": + SpecialPowers.setBoolPref(pref[0], pref[1]); + break; + case "number": + SpecialPowers.setIntPref(pref[0], pref[1]); + break; + } + } + + let checkboxes = [ + "#contentBlockingTrackingProtectionCheckbox", + "#contentBlockingBlockCookiesCheckbox", + ]; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + for (let selector of checkboxes) { + let element = doc.querySelector(selector); + ok(element, "checkbox " + selector + " exists"); + is( + element.getAttribute("checked"), + "true", + "checkbox " + selector + " is checked" + ); + } + + // Ensure the dependent controls of the tracking protection subsection behave properly. + let tpCheckbox = doc.querySelector(checkboxes[0]); + + let dependentControls = ["#trackingProtectionMenu"]; + let alwaysEnabledControls = [ + "#trackingProtectionMenuDesc", + ".content-blocking-category-name", + "#changeBlockListLink", + ]; + + tpCheckbox.checked = true; + + // Select "Always" under "All Detected Trackers". + let menu = doc.querySelector("#trackingProtectionMenu"); + let always = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=always]" + ); + let privateElement = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=private]" + ); + menu.selectedItem = always; + ok( + !privateElement.selected, + "The Only in private windows item should not be selected" + ); + ok(always.selected, "The Always item should be selected"); + + // The first time, privacy-pane-tp-ui-updated won't be dispatched since the + // assignment above is a no-op. + + // Ensure the dependent controls are enabled + checkControlState(doc, dependentControls, true); + checkControlState(doc, alwaysEnabledControls, true); + + let promise = TestUtils.topicObserved("privacy-pane-tp-ui-updated"); + tpCheckbox.click(); + + await promise; + ok(!tpCheckbox.checked, "The checkbox should now be unchecked"); + + // Ensure the dependent controls are disabled + checkControlState(doc, dependentControls, false); + checkControlState(doc, alwaysEnabledControls, true); + + // Make sure the selection in the tracking protection submenu persists after + // a few times of checking and unchecking All Detected Trackers. + // Doing this in a loop in order to avoid typing in the unrolled version manually. + // We need to go from the checked state of the checkbox to unchecked back to + // checked again... + for (let i = 0; i < 3; ++i) { + promise = TestUtils.topicObserved("privacy-pane-tp-ui-updated"); + tpCheckbox.click(); + + await promise; + is(tpCheckbox.checked, i % 2 == 0, "The checkbox should now be unchecked"); + is( + privateElement.selected, + i % 2 == 0, + "The Only in private windows item should be selected by default, when the checkbox is checked" + ); + ok(!always.selected, "The Always item should no longer be selected"); + } + + let cookieMenu = doc.querySelector("#blockCookiesMenu"); + let cookieMenuTrackers = cookieMenu.querySelector( + "menupopup > menuitem[value=trackers]" + ); + let cookieMenuTrackersPlusIsolate = cookieMenu.querySelector( + "menupopup > menuitem[value=trackers-plus-isolate]" + ); + let cookieMenuUnvisited = cookieMenu.querySelector( + "menupopup > menuitem[value=unvisited]" + ); + let cookieMenuAllThirdParties = doc.querySelector( + "menupopup > menuitem[value=all-third-parties]" + ); + let cookieMenuAll = cookieMenu.querySelector( + "menupopup > menuitem[value=always]" + ); + // Select block trackers + cookieMenuTrackers.click(); + ok(cookieMenuTrackers.selected, "The trackers item should be selected"); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block trackers and isolate + cookieMenuTrackersPlusIsolate.click(); + ok( + cookieMenuTrackersPlusIsolate.selected, + "The trackers plus isolate item should be selected" + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block unvisited + cookieMenuUnvisited.click(); + ok(cookieMenuUnvisited.selected, "The unvisited item should be selected"); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block all third party + cookieMenuAllThirdParties.click(); + ok( + cookieMenuAllThirdParties.selected, + "The all-third-parties item should be selected" + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + // Select block all third party + cookieMenuAll.click(); + ok(cookieMenuAll.selected, "The all cookies item should be selected"); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + + gBrowser.removeCurrentTab(); + + // Ensure the block-trackers-plus-isolate option only shows in the dropdown if the UI pref is set. + Services.prefs.setBoolPref(ISOLATE_UI_PREF, false); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + doc = gBrowser.contentDocument; + cookieMenuTrackersPlusIsolate = doc.querySelector( + "#blockCookiesMenu menupopup > menuitem[value=trackers-plus-isolate]" + ); + ok( + cookieMenuTrackersPlusIsolate.hidden, + "Trackers plus isolate option is hidden from the dropdown if the ui pref is not set." + ); + + gBrowser.removeCurrentTab(); + + // Ensure the block-trackers-plus-isolate option only shows in the dropdown if FPI is disabled. + SpecialPowers.setIntPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ); + SpecialPowers.setBoolPref(FPI_PREF, true); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + doc = gBrowser.contentDocument; + cookieMenuTrackers = doc.querySelector( + "#blockCookiesMenu menupopup > menuitem[value=trackers]" + ); + cookieMenuTrackersPlusIsolate = doc.querySelector( + "#blockCookiesMenu menupopup > menuitem[value=trackers-plus-isolate]" + ); + ok(cookieMenuTrackers.selected, "The trackers item should be selected"); + ok( + cookieMenuTrackersPlusIsolate.hidden, + "Trackers plus isolate option is hidden from the dropdown if the FPI pref is set." + ); + gBrowser.removeCurrentTab(); + + for (let pref of prefs) { + SpecialPowers.clearUserPref(pref[0]); + } +}); + +// Tests that the content blocking "Standard" category radio sets the prefs to their default values. +add_task(async function testContentBlockingStandardCategory() { + let prefs = { + [TP_PREF]: null, + [TP_PBM_PREF]: null, + [NCB_PREF]: null, + [NCBP_PREF]: null, + [FP_PREF]: null, + [STP_PREF]: null, + [CM_PREF]: null, + [EMAIL_TP_PREF]: null, + [EMAIL_TP_PBM_PREF]: null, + [LEVEL2_PREF]: null, + [REFERRER_PREF]: null, + [REFERRER_TOP_PREF]: null, + [OCSP_PREF]: null, + [QUERY_PARAM_STRIP_PREF]: null, + [QUERY_PARAM_STRIP_PBM_PREF]: null, + }; + + for (let pref in prefs) { + Services.prefs.clearUserPref(pref); + switch (Services.prefs.getPrefType(pref)) { + case Services.prefs.PREF_BOOL: + prefs[pref] = Services.prefs.getBoolPref(pref); + break; + case Services.prefs.PREF_INT: + prefs[pref] = Services.prefs.getIntPref(pref); + break; + case Services.prefs.PREF_STRING: + prefs[pref] = Services.prefs.getCharPref(pref); + break; + default: + ok(false, `Unknown pref type for ${pref}`); + } + } + + Services.prefs.setBoolPref(TP_PREF, true); + Services.prefs.setBoolPref(TP_PBM_PREF, false); + Services.prefs.setIntPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setIntPref( + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setBoolPref(STP_PREF, !Services.prefs.getBoolPref(STP_PREF)); + Services.prefs.setBoolPref(FP_PREF, !Services.prefs.getBoolPref(FP_PREF)); + Services.prefs.setBoolPref(CM_PREF, !Services.prefs.getBoolPref(CM_PREF)); + Services.prefs.setBoolPref( + EMAIL_TP_PREF, + !Services.prefs.getBoolPref(EMAIL_TP_PREF) + ); + Services.prefs.setBoolPref( + EMAIL_TP_PBM_PREF, + !Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF) + ); + Services.prefs.setBoolPref( + LEVEL2_PREF, + !Services.prefs.getBoolPref(LEVEL2_PREF) + ); + Services.prefs.setBoolPref( + REFERRER_PREF, + !Services.prefs.getBoolPref(REFERRER_PREF) + ); + Services.prefs.setBoolPref( + REFERRER_TOP_PREF, + !Services.prefs.getBoolPref(REFERRER_TOP_PREF) + ); + Services.prefs.setBoolPref(OCSP_PREF, !Services.prefs.getBoolPref(OCSP_PREF)); + Services.prefs.setBoolPref( + QUERY_PARAM_STRIP_PREF, + !Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF) + ); + Services.prefs.setBoolPref( + QUERY_PARAM_STRIP_PBM_PREF, + !Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF) + ); + + for (let pref in prefs) { + switch (Services.prefs.getPrefType(pref)) { + case Services.prefs.PREF_BOOL: + // Account for prefs that may have retained their default value + if (Services.prefs.getBoolPref(pref) != prefs[pref]) { + ok( + Services.prefs.prefHasUserValue(pref), + `modified the pref ${pref}` + ); + } + break; + case Services.prefs.PREF_INT: + if (Services.prefs.getIntPref(pref) != prefs[pref]) { + ok( + Services.prefs.prefHasUserValue(pref), + `modified the pref ${pref}` + ); + } + break; + case Services.prefs.PREF_STRING: + if (Services.prefs.getCharPref(pref) != prefs[pref]) { + ok( + Services.prefs.prefHasUserValue(pref), + `modified the pref ${pref}` + ); + } + break; + default: + ok(false, `Unknown pref type for ${pref}`); + } + } + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let standardRadioOption = doc.getElementById("standardRadio"); + standardRadioOption.click(); + + // TP prefs are reset async to check for extensions controlling them. + await TestUtils.waitForCondition( + () => !Services.prefs.prefHasUserValue(TP_PREF) + ); + + for (let pref in prefs) { + ok(!Services.prefs.prefHasUserValue(pref), `reset the pref ${pref}`); + } + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} has been set to standard` + ); + + gBrowser.removeCurrentTab(); +}); + +// Tests that the content blocking "Strict" category radio sets the prefs to the expected values. +add_task(async function testContentBlockingStrictCategory() { + Services.prefs.setBoolPref(TP_PREF, false); + Services.prefs.setBoolPref(TP_PBM_PREF, false); + Services.prefs.setBoolPref(EMAIL_TP_PREF, false); + Services.prefs.setBoolPref(EMAIL_TP_PBM_PREF, false); + Services.prefs.setBoolPref(LEVEL2_PREF, false); + Services.prefs.setBoolPref(REFERRER_PREF, false); + Services.prefs.setBoolPref(REFERRER_TOP_PREF, false); + Services.prefs.setBoolPref(OCSP_PREF, false); + Services.prefs.setBoolPref(QUERY_PARAM_STRIP_PREF, false); + Services.prefs.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, false); + Services.prefs.setIntPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN + ); + Services.prefs.setIntPref( + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN + ); + let strict_pref = Services.prefs.getStringPref(STRICT_PREF).split(","); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let strictRadioOption = doc.getElementById("strictRadio"); + strictRadioOption.click(); + + // TP prefs are reset async to check for extensions controlling them. + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + // Depending on the definition of the STRICT_PREF, the dependant prefs may have been + // set to varying values. Ensure they have been set according to this definition. + for (let pref of strict_pref) { + switch (pref) { + case "tp": + is( + Services.prefs.getBoolPref(TP_PREF), + true, + `${TP_PREF} has been set to true` + ); + break; + case "-tp": + is( + Services.prefs.getBoolPref(TP_PREF), + false, + `${TP_PREF} has been set to false` + ); + break; + case "tpPrivate": + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + true, + `${TP_PBM_PREF} has been set to true` + ); + break; + case "-tpPrivate": + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + false, + `${TP_PBM_PREF} has been set to false` + ); + break; + case "fp": + is( + Services.prefs.getBoolPref(FP_PREF), + true, + `${FP_PREF} has been set to true` + ); + break; + case "-fp": + is( + Services.prefs.getBoolPref(FP_PREF), + false, + `${FP_PREF} has been set to false` + ); + break; + case "stp": + is( + Services.prefs.getBoolPref(STP_PREF), + true, + `${STP_PREF} has been set to true` + ); + break; + case "-stp": + is( + Services.prefs.getBoolPref(STP_PREF), + false, + `${STP_PREF} has been set to false` + ); + break; + case "cm": + is( + Services.prefs.getBoolPref(CM_PREF), + true, + `${CM_PREF} has been set to true` + ); + break; + case "-cm": + is( + Services.prefs.getBoolPref(CM_PREF), + false, + `${CM_PREF} has been set to false` + ); + break; + case "emailTP": + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + true, + `${EMAIL_TP_PREF} has been set to true` + ); + break; + case "-emailTP": + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} has been set to false` + ); + break; + case "emailTPPrivate": + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} has been set to true` + ); + break; + case "-emailTPPrivate": + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + false, + `${EMAIL_TP_PBM_PREF} has been set to false` + ); + break; + case "lvl2": + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + true, + `${CM_PREF} has been set to true` + ); + break; + case "-lvl2": + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + false, + `${CM_PREF} has been set to false` + ); + break; + case "rp": + is( + Services.prefs.getBoolPref(REFERRER_PREF), + true, + `${REFERRER_PREF} has been set to true` + ); + break; + case "-rp": + is( + Services.prefs.getBoolPref(REFERRER_PREF), + false, + `${REFERRER_PREF} has been set to false` + ); + break; + case "rpTop": + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + true, + `${REFERRER_TOP_PREF} has been set to true` + ); + break; + case "-rpTop": + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + false, + `${REFERRER_TOP_PREF} has been set to false` + ); + break; + case "ocsp": + is( + Services.prefs.getBoolPref(OCSP_PREF), + true, + `${OCSP_PREF} has been set to true` + ); + break; + case "-ocsp": + is( + Services.prefs.getBoolPref(OCSP_PREF), + false, + `${OCSP_PREF} has been set to false` + ); + break; + case "qps": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + true, + `${QUERY_PARAM_STRIP_PREF} has been set to true` + ); + break; + case "-qps": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + false, + `${QUERY_PARAM_STRIP_PREF} has been set to false` + ); + break; + case "qpsPBM": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + true, + `${QUERY_PARAM_STRIP_PBM_PREF} has been set to true` + ); + break; + case "-qpsPBM": + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + false, + `${QUERY_PARAM_STRIP_PBM_PREF} has been set to false` + ); + break; + case "cookieBehavior0": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_ACCEPT}` + ); + break; + case "cookieBehavior1": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}` + ); + break; + case "cookieBehavior2": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}` + ); + break; + case "cookieBehavior3": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}` + ); + break; + case "cookieBehavior4": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}` + ); + break; + case "cookieBehavior5": + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCB_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + break; + case "cookieBehaviorPBM0": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_ACCEPT}` + ); + break; + case "cookieBehaviorPBM1": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN}` + ); + break; + case "cookieBehaviorPBM2": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT}` + ); + break; + case "cookieBehaviorPBM3": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN}` + ); + break; + case "cookieBehaviorPBM4": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER}` + ); + break; + case "cookieBehaviorPBM5": + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + `${NCBP_PREF} has been set to ${Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN}` + ); + break; + default: + ok(false, "unknown option was added to the strict pref"); + break; + } + } + + gBrowser.removeCurrentTab(); +}); + +// Tests that the content blocking "Custom" category behaves as expected. +add_task(async function testContentBlockingCustomCategory() { + let untouchedPrefs = [ + TP_PREF, + TP_PBM_PREF, + NCB_PREF, + NCBP_PREF, + FP_PREF, + STP_PREF, + CM_PREF, + REFERRER_PREF, + REFERRER_TOP_PREF, + OCSP_PREF, + QUERY_PARAM_STRIP_PREF, + QUERY_PARAM_STRIP_PBM_PREF, + ]; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + let strictRadioOption = doc.getElementById("strictRadio"); + let standardRadioOption = doc.getElementById("standardRadio"); + let customRadioOption = doc.getElementById("customRadio"); + let defaults = new Preferences({ defaultBranch: true }); + + standardRadioOption.click(); + await TestUtils.waitForCondition( + () => !Services.prefs.prefHasUserValue(TP_PREF) + ); + + customRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "custom" + ); + + // The custom option will only force change of some prefs, like CAT_PREF. All + // other prefs should remain as they were for standard. + for (let pref of untouchedPrefs) { + ok( + !Services.prefs.prefHasUserValue(pref), + `the pref ${pref} remains as default value` + ); + } + + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + strictRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + + // Changing the following prefs should necessarily set CAT_PREF to "custom" + for (let pref of [ + FP_PREF, + STP_PREF, + CM_PREF, + TP_PREF, + TP_PBM_PREF, + REFERRER_PREF, + REFERRER_TOP_PREF, + OCSP_PREF, + QUERY_PARAM_STRIP_PREF, + QUERY_PARAM_STRIP_PBM_PREF, + ]) { + Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref)); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "custom" + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + strictRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + } + + // Changing the NCB_PREF should necessarily set CAT_PREF to "custom" + let defaultNCB = defaults.get(NCB_PREF); + let nonDefaultNCB; + switch (defaultNCB) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + default: + ok( + false, + "Unexpected default value found for " + NCB_PREF + ": " + defaultNCB + ); + break; + } + Services.prefs.setIntPref(NCB_PREF, nonDefaultNCB); + await TestUtils.waitForCondition(() => + Services.prefs.prefHasUserValue(NCB_PREF) + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + strictRadioOption.click(); + await TestUtils.waitForCondition( + () => Services.prefs.getStringPref(CAT_PREF) == "strict" + ); + + // Changing the NCBP_PREF should necessarily set CAT_PREF to "custom" + let defaultNCBP = defaults.get(NCBP_PREF); + let nonDefaultNCBP; + switch (defaultNCBP) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER: + case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + default: + ok( + false, + "Unexpected default value found for " + NCBP_PREF + ": " + defaultNCBP + ); + break; + } + Services.prefs.setIntPref(NCBP_PREF, nonDefaultNCBP); + await TestUtils.waitForCondition(() => + Services.prefs.prefHasUserValue(NCBP_PREF) + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + for (let pref of untouchedPrefs) { + SpecialPowers.clearUserPref(pref); + } + + gBrowser.removeCurrentTab(); +}); + +function checkControlState(doc, controls, enabled) { + for (let selector of controls) { + for (let control of doc.querySelectorAll(selector)) { + if (enabled) { + ok(!control.hasAttribute("disabled"), `${selector} is enabled.`); + } else { + is( + control.getAttribute("disabled"), + "true", + `${selector} is disabled.` + ); + } + } + } +} + +// Checks that the menulists for tracking protection and cookie blocking are disabled when all TP prefs are off. +add_task(async function testContentBlockingDependentTPControls() { + SpecialPowers.pushPrefEnv({ + set: [ + [TP_PREF, false], + [TP_PBM_PREF, false], + [NCB_PREF, Ci.nsICookieService.BEHAVIOR_ACCEPT], + [CAT_PREF, "custom"], + ], + }); + + let disabledControls = ["#trackingProtectionMenu", "#blockCookiesMenu"]; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + checkControlState(doc, disabledControls, false); + + gBrowser.removeCurrentTab(); +}); + +// Checks that disabling tracking protection also disables email tracking protection. +add_task(async function testDisableTPCheckBoxDisablesEmailTP() { + SpecialPowers.pushPrefEnv({ + set: [ + [TP_PREF, false], + [TP_PBM_PREF, true], + [EMAIL_TP_PREF, false], + [EMAIL_TP_PBM_PREF, true], + [CAT_PREF, "custom"], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + // Click the checkbox to disable TP and check if this disables Email TP. + let tpCheckbox = doc.getElementById( + "contentBlockingTrackingProtectionCheckbox" + ); + + // Verify the initial check state of the tracking protection checkbox. + is( + tpCheckbox.getAttribute("checked"), + "true", + "Tracking protection checkbox is checked initially" + ); + + tpCheckbox.click(); + + // Verify the checkbox is unchecked after clicking. + is( + tpCheckbox.getAttribute("checked"), + "", + "Tracking protection checkbox is unchecked" + ); + + // Verify the pref states. + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} has been set to false` + ); + + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + false, + `${EMAIL_TP_PBM_PREF} has been set to false` + ); + + gBrowser.removeCurrentTab(); +}); + +// Checks that the email tracking prefs set properly with tracking protection +// drop downs. +add_task(async function testTPMenuForEmailTP() { + SpecialPowers.pushPrefEnv({ + set: [ + [TP_PREF, false], + [TP_PBM_PREF, true], + [EMAIL_TP_PREF, false], + [EMAIL_TP_PBM_PREF, true], + [CAT_PREF, "custom"], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let menu = doc.querySelector("#trackingProtectionMenu"); + let always = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=always]" + ); + let privateElement = doc.querySelector( + "#trackingProtectionMenu > menupopup > menuitem[value=private]" + ); + + // Click the always option on the tracking protection drop down. + menu.selectedItem = always; + always.click(); + + // Verify the pref states. + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + true, + `${EMAIL_TP_PREF} has been set to true` + ); + + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} has been set to true` + ); + + // Click the private-only option on the tracking protection drop down. + menu.selectedItem = privateElement; + privateElement.click(); + + // Verify the pref states. + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} has been set to false` + ); + + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} has been set to true` + ); + + gBrowser.removeCurrentTab(); +}); + +// Checks that social media trackers, cryptomining and fingerprinting visibility +// can be controlled via pref. +add_task(async function testCustomOptionsVisibility() { + Services.prefs.setBoolPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled", + false + ); + Services.prefs.setBoolPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled", + false + ); + Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + false + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + let cryptominersOption = doc.getElementById( + "contentBlockingCryptominersOption" + ); + let fingerprintersOption = doc.getElementById( + "contentBlockingFingerprintersOption" + ); + + ok(cryptominersOption.hidden, "Cryptomining is hidden"); + ok(fingerprintersOption.hidden, "Fingerprinting is hidden"); + + gBrowser.removeCurrentTab(); + + Services.prefs.setBoolPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + cryptominersOption = doc.getElementById("contentBlockingCryptominersOption"); + fingerprintersOption = doc.getElementById( + "contentBlockingFingerprintersOption" + ); + + ok(!cryptominersOption.hidden, "Cryptomining is shown"); + ok(fingerprintersOption.hidden, "Fingerprinting is hidden"); + + gBrowser.removeCurrentTab(); + + Services.prefs.setBoolPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + cryptominersOption = doc.getElementById("contentBlockingCryptominersOption"); + fingerprintersOption = doc.getElementById( + "contentBlockingFingerprintersOption" + ); + + ok(!cryptominersOption.hidden, "Cryptomining is shown"); + ok(!fingerprintersOption.hidden, "Fingerprinting is shown"); + + gBrowser.removeCurrentTab(); + + // Social media trackers UI should be hidden + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + let socialTrackingUI = [...doc.querySelectorAll(".social-media-option")]; + + ok( + socialTrackingUI.every(el => el.hidden), + "All Social media tracker UI instances are hidden" + ); + + gBrowser.removeCurrentTab(); + + // Social media trackers UI should be visible + Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + doc = gBrowser.contentDocument; + socialTrackingUI = [...doc.querySelectorAll(".social-media-option")]; + + ok( + !socialTrackingUI.every(el => el.hidden), + "All Social media tracker UI instances are visible" + ); + + gBrowser.removeCurrentTab(); + + Services.prefs.clearUserPref( + "browser.contentblocking.cryptomining.preferences.ui.enabled" + ); + Services.prefs.clearUserPref( + "browser.contentblocking.fingerprinting.preferences.ui.enabled" + ); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); +}); + +// Checks that adding a custom enterprise policy will put the user in the custom category. +// Other categories will be disabled. +add_task(async function testPolicyCategorization() { + Services.prefs.setStringPref(CAT_PREF, "standard"); + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} starts on standard` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} starts with the default value` + ); + PoliciesPrefTracker.start(); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + EnableTrackingProtection: { + Value: true, + }, + }, + }); + EnterprisePolicyTesting.checkPolicyPref(TP_PREF, true, false); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + Services.prefs.setStringPref(CAT_PREF, "standard"); + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} starts on standard` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} starts with the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} starts with the default value` + ); + + let uiUpdatedPromise = TestUtils.topicObserved("privacy-pane-tp-ui-updated"); + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + Cookies: { + AcceptThirdParty: "never", + Locked: true, + }, + }, + }); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await uiUpdatedPromise; + + EnterprisePolicyTesting.checkPolicyPref( + NCB_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + true + ); + EnterprisePolicyTesting.checkPolicyPref( + NCBP_PREF, + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + true + ); + is( + Services.prefs.getStringPref(CAT_PREF), + "custom", + `${CAT_PREF} has been set to custom` + ); + + let doc = gBrowser.contentDocument; + let strictRadioOption = doc.getElementById("strictRadio"); + let standardRadioOption = doc.getElementById("standardRadio"); + is(strictRadioOption.disabled, true, "the strict option is disabled"); + is(standardRadioOption.disabled, true, "the standard option is disabled"); + + gBrowser.removeCurrentTab(); + + // Cleanup after this particular test. + if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) { + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + Cookies: { + Locked: false, + }, + }, + }); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); + } + is( + Services.policies.status, + Ci.nsIEnterprisePolicies.INACTIVE, + "Engine is inactive at the end of the test" + ); + + EnterprisePolicyTesting.resetRunOnceState(); + PoliciesPrefTracker.stop(); +}); + +// Tests that changing a content blocking pref shows the content blocking warning +// to reload tabs to apply changes. +add_task(async function testContentBlockingReloadWarning() { + Services.prefs.setStringPref(CAT_PREF, "standard"); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + let reloadWarnings = [ + ...doc.querySelectorAll(".content-blocking-warning.reload-tabs"), + ]; + let allHidden = reloadWarnings.every(el => el.hidden); + ok(allHidden, "all of the warnings to reload tabs are initially hidden"); + + Services.prefs.setStringPref(CAT_PREF, "strict"); + + let strictWarning = doc.querySelector( + "#contentBlockingOptionStrict .content-blocking-warning.reload-tabs" + ); + ok( + !BrowserTestUtils.is_hidden(strictWarning), + "The warning in the strict section should be showing" + ); + + Services.prefs.setStringPref(CAT_PREF, "standard"); + gBrowser.removeCurrentTab(); +}); + +// Tests that changing a content blocking pref does not show the content blocking warning +// if it is the only tab. +add_task(async function testContentBlockingReloadWarningSingleTab() { + Services.prefs.setStringPref(CAT_PREF, "standard"); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, PRIVACY_PAGE); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + PRIVACY_PAGE + ); + + let reloadWarnings = [ + ...gBrowser.contentDocument.querySelectorAll( + ".content-blocking-warning.reload-tabs" + ), + ]; + ok(reloadWarnings.length, "must have at least one reload warning"); + ok( + reloadWarnings.every(el => el.hidden), + "all of the warnings to reload tabs are initially hidden" + ); + + is(BrowserWindowTracker.windowCount, 1, "There is only one window open"); + is(gBrowser.tabs.length, 1, "There is only one tab open"); + Services.prefs.setStringPref(CAT_PREF, "strict"); + + ok( + reloadWarnings.every(el => el.hidden), + "all of the warnings to reload tabs are still hidden" + ); + Services.prefs.setStringPref(CAT_PREF, "standard"); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:newtab"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); +}); + +// Checks that the reload tabs message reloads all tabs except the active tab. +add_task(async function testReloadTabsMessage() { + Services.prefs.setStringPref(CAT_PREF, "strict"); + let exampleTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com" + ); + let examplePinnedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com" + ); + gBrowser.pinTab(examplePinnedTab); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + let standardWarning = doc.querySelector( + "#contentBlockingOptionStandard .content-blocking-warning.reload-tabs" + ); + let standardReloadButton = doc.querySelector( + "#contentBlockingOptionStandard .reload-tabs-button" + ); + + Services.prefs.setStringPref(CAT_PREF, "standard"); + ok( + !BrowserTestUtils.is_hidden(standardWarning), + "The warning in the standard section should be showing" + ); + + let exampleTabBrowserDiscardedPromise = BrowserTestUtils.waitForEvent( + exampleTab, + "TabBrowserDiscarded" + ); + let examplePinnedTabLoadPromise = BrowserTestUtils.browserLoaded( + examplePinnedTab.linkedBrowser + ); + standardReloadButton.click(); + // The pinned example page had a load event + await examplePinnedTabLoadPromise; + // The other one had its browser discarded + await exampleTabBrowserDiscardedPromise; + + ok( + BrowserTestUtils.is_hidden(standardWarning), + "The warning in the standard section should have hidden after being clicked" + ); + + // cleanup + Services.prefs.setStringPref(CAT_PREF, "standard"); + gBrowser.removeTab(exampleTab); + gBrowser.removeTab(examplePinnedTab); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking_categories.js b/browser/components/preferences/tests/browser_contentblocking_categories.js new file mode 100644 index 0000000000..3b9e16a2fe --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking_categories.js @@ -0,0 +1,487 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env webextensions */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const TP_PREF = "privacy.trackingprotection.enabled"; +const TP_PBM_PREF = "privacy.trackingprotection.pbmode.enabled"; +const NCB_PREF = "network.cookie.cookieBehavior"; +const NCBP_PREF = "network.cookie.cookieBehavior.pbmode"; +const CAT_PREF = "browser.contentblocking.category"; +const FP_PREF = "privacy.trackingprotection.fingerprinting.enabled"; +const CM_PREF = "privacy.trackingprotection.cryptomining.enabled"; +const STP_PREF = "privacy.trackingprotection.socialtracking.enabled"; +const EMAIL_TP_PREF = "privacy.trackingprotection.emailtracking.enabled"; +const EMAIL_TP_PBM_PREF = + "privacy.trackingprotection.emailtracking.pbmode.enabled"; +const LEVEL2_PREF = "privacy.annotate_channels.strict_list.enabled"; +const REFERRER_PREF = "network.http.referer.disallowCrossSiteRelaxingDefault"; +const REFERRER_TOP_PREF = + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation"; +const OCSP_PREF = "privacy.partition.network_state.ocsp_cache"; +const QUERY_PARAM_STRIP_PREF = "privacy.query_stripping.enabled"; +const QUERY_PARAM_STRIP_PBM_PREF = "privacy.query_stripping.enabled.pbmode"; +const STRICT_DEF_PREF = "browser.contentblocking.features.strict"; + +// Tests that the content blocking standard category definition is based on the default settings of +// the content blocking prefs. +// Changing the definition does not remove the user from the category. +add_task(async function testContentBlockingStandardDefinition() { + Services.prefs.setStringPref(CAT_PREF, "strict"); + Services.prefs.setStringPref(CAT_PREF, "standard"); + is( + Services.prefs.getStringPref(CAT_PREF), + "standard", + `${CAT_PREF} starts on standard` + ); + + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PBM_PREF), + `${TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(FP_PREF), + `${FP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(CM_PREF), + `${CM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(STP_PREF), + `${STP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PREF), + `${EMAIL_TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF), + `${EMAIL_TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_PREF), + `${REFERRER_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF), + `${REFERRER_TOP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(OCSP_PREF), + `${OCSP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF), + `${QUERY_PARAM_STRIP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF), + `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value` + ); + + let defaults = Services.prefs.getDefaultBranch(""); + let originalTP = defaults.getBoolPref(TP_PREF); + let originalTPPBM = defaults.getBoolPref(TP_PBM_PREF); + let originalFP = defaults.getBoolPref(FP_PREF); + let originalCM = defaults.getBoolPref(CM_PREF); + let originalSTP = defaults.getBoolPref(STP_PREF); + let originalEmailTP = defaults.getBoolPref(EMAIL_TP_PREF); + let originalEmailTPPBM = defaults.getBoolPref(EMAIL_TP_PBM_PREF); + let originalNCB = defaults.getIntPref(NCB_PREF); + let originalNCBP = defaults.getIntPref(NCBP_PREF); + let originalLEVEL2 = defaults.getBoolPref(LEVEL2_PREF); + let originalREFERRER = defaults.getBoolPref(REFERRER_PREF); + let originalREFERRERTOP = defaults.getBoolPref(REFERRER_TOP_PREF); + let originalOCSP = defaults.getBoolPref(OCSP_PREF); + let originalQueryParamStrip = defaults.getBoolPref(QUERY_PARAM_STRIP_PREF); + let originalQueryParamStripPBM = defaults.getBoolPref( + QUERY_PARAM_STRIP_PBM_PREF + ); + + let nonDefaultNCB; + switch (originalNCB) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + default: + nonDefaultNCB = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + } + let nonDefaultNCBP; + switch (originalNCBP) { + case Ci.nsICookieService.BEHAVIOR_ACCEPT: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_REJECT; + break; + default: + nonDefaultNCBP = Ci.nsICookieService.BEHAVIOR_ACCEPT; + break; + } + defaults.setIntPref(NCB_PREF, nonDefaultNCB); + defaults.setIntPref(NCBP_PREF, nonDefaultNCBP); + defaults.setBoolPref(TP_PREF, !originalTP); + defaults.setBoolPref(TP_PBM_PREF, !originalTPPBM); + defaults.setBoolPref(FP_PREF, !originalFP); + defaults.setBoolPref(CM_PREF, !originalCM); + defaults.setBoolPref(CM_PREF, !originalSTP); + defaults.setBoolPref(EMAIL_TP_PREF, !originalEmailTP); + defaults.setBoolPref(EMAIL_TP_PBM_PREF, !originalEmailTPPBM); + defaults.setIntPref(NCB_PREF, !originalNCB); + defaults.setBoolPref(LEVEL2_PREF, !originalLEVEL2); + defaults.setBoolPref(REFERRER_PREF, !originalREFERRER); + defaults.setBoolPref(REFERRER_TOP_PREF, !originalREFERRERTOP); + defaults.setBoolPref(OCSP_PREF, !originalOCSP); + defaults.setBoolPref(QUERY_PARAM_STRIP_PREF, !originalQueryParamStrip); + defaults.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, !originalQueryParamStripPBM); + + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PBM_PREF), + `${TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(FP_PREF), + `${FP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(CM_PREF), + `${CM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(STP_PREF), + `${STP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PREF), + `${EMAIL_TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF), + `${EMAIL_TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_PREF), + `${REFERRER_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF), + `${REFERRER_TOP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(OCSP_PREF), + `${OCSP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF), + `${QUERY_PARAM_STRIP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF), + `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value` + ); + + // cleanup + defaults.setIntPref(NCB_PREF, originalNCB); + defaults.setBoolPref(TP_PREF, originalTP); + defaults.setBoolPref(TP_PBM_PREF, originalTPPBM); + defaults.setBoolPref(FP_PREF, originalFP); + defaults.setBoolPref(CM_PREF, originalCM); + defaults.setBoolPref(STP_PREF, originalSTP); + defaults.setBoolPref(EMAIL_TP_PREF, originalEmailTP); + defaults.setBoolPref(EMAIL_TP_PBM_PREF, originalEmailTPPBM); + defaults.setIntPref(NCB_PREF, originalNCB); + defaults.setIntPref(NCBP_PREF, originalNCBP); + defaults.setBoolPref(LEVEL2_PREF, originalLEVEL2); + defaults.setBoolPref(REFERRER_PREF, originalREFERRER); + defaults.setBoolPref(REFERRER_TOP_PREF, originalREFERRERTOP); + defaults.setBoolPref(OCSP_PREF, originalOCSP); + defaults.setBoolPref(QUERY_PARAM_STRIP_PREF, originalQueryParamStrip); + defaults.setBoolPref(QUERY_PARAM_STRIP_PBM_PREF, originalQueryParamStripPBM); +}); + +// Tests that the content blocking strict category definition changes the behavior +// of the strict category pref and all prefs it controls. +// Changing the definition does not remove the user from the category. +add_task(async function testContentBlockingStrictDefinition() { + let defaults = Services.prefs.getDefaultBranch(""); + let originalStrictPref = defaults.getStringPref(STRICT_DEF_PREF); + defaults.setStringPref( + STRICT_DEF_PREF, + "tp,tpPrivate,fp,cm,cookieBehavior0,cookieBehaviorPBM0,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp,qps,qpsPBM" + ); + Services.prefs.setStringPref(CAT_PREF, "strict"); + is( + Services.prefs.getStringPref(CAT_PREF), + "strict", + `${CAT_PREF} has changed to strict` + ); + + ok( + !Services.prefs.prefHasUserValue(STRICT_DEF_PREF), + `We changed the default value of ${STRICT_DEF_PREF}` + ); + is( + Services.prefs.getStringPref(STRICT_DEF_PREF), + "tp,tpPrivate,fp,cm,cookieBehavior0,cookieBehaviorPBM0,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp,qps,qpsPBM", + `${STRICT_DEF_PREF} changed to what we set.` + ); + + is( + Services.prefs.getBoolPref(TP_PREF), + true, + `${TP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + true, + `${TP_PBM_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(FP_PREF), + true, + `${CM_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(CM_PREF), + true, + `${CM_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(STP_PREF), + true, + `${STP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + true, + `${EMAIL_TP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + true, + `${EMAIL_TP_PBM_PREF} pref has been set to true` + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCB_PREF} has been set to BEHAVIOR_ACCEPT` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_ACCEPT, + `${NCBP_PREF} has been set to BEHAVIOR_ACCEPT` + ); + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + true, + `${LEVEL2_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(REFERRER_PREF), + true, + `${REFERRER_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + true, + `${REFERRER_TOP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(OCSP_PREF), + true, + `${OCSP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + true, + `${QUERY_PARAM_STRIP_PREF} pref has been set to true` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + true, + `${QUERY_PARAM_STRIP_PBM_PREF} pref has been set to true` + ); + + // Note, if a pref is not listed it will use the default value, however this is only meant as a + // backup if a mistake is made. The UI will not respond correctly. + defaults.setStringPref(STRICT_DEF_PREF, ""); + ok( + !Services.prefs.prefHasUserValue(TP_PREF), + `${TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(TP_PBM_PREF), + `${TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(FP_PREF), + `${FP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(CM_PREF), + `${CM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(STP_PREF), + `${STP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PREF), + `${EMAIL_TP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(EMAIL_TP_PBM_PREF), + `${EMAIL_TP_PBM_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCB_PREF), + `${NCB_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(NCBP_PREF), + `${NCBP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(LEVEL2_PREF), + `${LEVEL2_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_PREF), + `${REFERRER_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(REFERRER_TOP_PREF), + `${REFERRER_TOP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(OCSP_PREF), + `${OCSP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PREF), + `${QUERY_PARAM_STRIP_PREF} pref has the default value` + ); + ok( + !Services.prefs.prefHasUserValue(QUERY_PARAM_STRIP_PBM_PREF), + `${QUERY_PARAM_STRIP_PBM_PREF} pref has the default value` + ); + + defaults.setStringPref( + STRICT_DEF_PREF, + "-tpPrivate,-fp,-cm,-tp,cookieBehavior3,cookieBehaviorPBM2,-stp,-emailTP,-emailTPPrivate,-lvl2,-rp,-ocsp,-qps,-qpsPBM" + ); + is( + Services.prefs.getBoolPref(TP_PREF), + false, + `${TP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(TP_PBM_PREF), + false, + `${TP_PBM_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(FP_PREF), + false, + `${FP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(CM_PREF), + false, + `${CM_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(STP_PREF), + false, + `${STP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PREF), + false, + `${EMAIL_TP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(EMAIL_TP_PBM_PREF), + false, + `${EMAIL_TP_PBM_PREF} pref has been set to false` + ); + is( + Services.prefs.getIntPref(NCB_PREF), + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + `${NCB_PREF} has been set to BEHAVIOR_REJECT_TRACKER` + ); + is( + Services.prefs.getIntPref(NCBP_PREF), + Ci.nsICookieService.BEHAVIOR_REJECT, + `${NCBP_PREF} has been set to BEHAVIOR_REJECT` + ); + is( + Services.prefs.getBoolPref(LEVEL2_PREF), + false, + `${LEVEL2_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(REFERRER_PREF), + false, + `${REFERRER_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(REFERRER_TOP_PREF), + false, + `${REFERRER_TOP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(OCSP_PREF), + false, + `${OCSP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PREF), + false, + `${QUERY_PARAM_STRIP_PREF} pref has been set to false` + ); + is( + Services.prefs.getBoolPref(QUERY_PARAM_STRIP_PBM_PREF), + false, + `${QUERY_PARAM_STRIP_PBM_PREF} pref has been set to false` + ); + + // cleanup + defaults.setStringPref(STRICT_DEF_PREF, originalStrictPref); + Services.prefs.setStringPref(CAT_PREF, "standard"); +}); diff --git a/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js b/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js new file mode 100644 index 0000000000..249a42317a --- /dev/null +++ b/browser/components/preferences/tests/browser_contentblocking_standard_tcp_section.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the TCP info box in the ETP standard section of about:preferences#privacy. + */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const CAT_PREF = "browser.contentblocking.category"; + +const LEARN_MORE_URL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "total-cookie-protection"; + +const { + BEHAVIOR_REJECT_TRACKER, + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, +} = Ci.nsICookieService; + +async function testTCPSection({ dFPIEnabled }) { + info( + "Testing TCP preferences section in standard " + + JSON.stringify({ dFPIEnabled }) + ); + + // In order to test the "standard" category we need to set the default value + // for the cookie behavior pref. A user value would get cleared as soon as we + // switch to "standard". + Services.prefs + .getDefaultBranch("") + .setIntPref( + COOKIE_BEHAVIOR_PREF, + dFPIEnabled + ? BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + : BEHAVIOR_REJECT_TRACKER + ); + + // Setting to standard category explicitly, since changing the default cookie + // behavior still switches us to custom initially. + await SpecialPowers.pushPrefEnv({ + set: [[CAT_PREF, "standard"]], + }); + + const uiEnabled = + Services.prefs.getIntPref(COOKIE_BEHAVIOR_PREF) == + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let standardRadioOption = doc.getElementById("standardRadio"); + let strictRadioOption = doc.getElementById("strictRadio"); + let customRadioOption = doc.getElementById("customRadio"); + + ok(standardRadioOption.selected, "Standard category is selected"); + + let etpStandardTCPBox = doc.getElementById("etpStandardTCPBox"); + is( + BrowserTestUtils.is_visible(etpStandardTCPBox), + uiEnabled, + `TCP section in standard is ${uiEnabled ? " " : "not "}visible.` + ); + + if (uiEnabled) { + // If visible, test the TCP section elements. + let learnMoreLink = etpStandardTCPBox.querySelector("#tcp-learn-more-link"); + ok(learnMoreLink, "Should have a learn more link"); + BrowserTestUtils.is_visible( + learnMoreLink, + "Learn more link should be visible." + ); + ok( + learnMoreLink.href && !learnMoreLink.href.startsWith("about:blank"), + "Learn more link should be valid." + ); + is( + learnMoreLink.href, + LEARN_MORE_URL, + "Learn more link should have the correct target." + ); + + let description = etpStandardTCPBox.querySelector(".tail-with-learn-more"); + ok(description, "Should have a description element."); + BrowserTestUtils.is_visible(description, "Description should be visible."); + + let title = etpStandardTCPBox.querySelector( + ".content-blocking-warning-title" + ); + ok(title, "Should have a title element."); + BrowserTestUtils.is_visible(title, "Title should be visible."); + } + + info("Switch to ETP strict."); + let categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "strict"); + strictRadioOption.click(); + await categoryPrefChange; + ok( + !BrowserTestUtils.is_visible(etpStandardTCPBox), + "When strict is selected TCP UI is not visible." + ); + + info("Switch to ETP custom."); + categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "custom"); + customRadioOption.click(); + await categoryPrefChange; + ok( + !BrowserTestUtils.is_visible(etpStandardTCPBox), + "When custom is selected TCP UI is not visible." + ); + + info("Switch back to standard and ensure we show the TCP UI again."); + categoryPrefChange = waitForAndAssertPrefState(CAT_PREF, "standard"); + standardRadioOption.click(); + await categoryPrefChange; + is( + BrowserTestUtils.is_visible(etpStandardTCPBox), + uiEnabled, + `TCP section in standard is ${uiEnabled ? " " : "not "}visible.` + ); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); + Services.prefs.setStringPref(CAT_PREF, "standard"); +} + +add_setup(async function () { + // Register cleanup function to restore default cookie behavior. + const defaultPrefs = Services.prefs.getDefaultBranch(""); + const previousDefaultCB = defaultPrefs.getIntPref(COOKIE_BEHAVIOR_PREF); + + registerCleanupFunction(function () { + defaultPrefs.setIntPref(COOKIE_BEHAVIOR_PREF, previousDefaultCB); + }); +}); + +// Clients which don't have dFPI enabled should not see the +// preferences section. +add_task(async function test_dfpi_disabled() { + await testTCPSection({ dFPIEnabled: false }); +}); + +add_task(async function test_dfpi_enabled() { + await testTCPSection({ dFPIEnabled: true }); +}); diff --git a/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js new file mode 100644 index 0000000000..1b70170ddc --- /dev/null +++ b/browser/components/preferences/tests/browser_cookie_exceptions_addRemove.js @@ -0,0 +1,299 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +if (AppConstants.TSAN || AppConstants.DEBUG) { + requestLongerTimeout(2); +} + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +async function openCookiesDialog(doc) { + let cookieExceptionsButton = doc.getElementById("cookieExceptions"); + ok(cookieExceptionsButton, "cookieExceptionsButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + cookieExceptionsButton.click(); + let dialog = await dialogPromise; + return dialog; +} + +function checkCookiesDialog(dialog) { + ok(dialog, "dialog loaded"); + let buttonIds = ["removePermission", "removeAllPermissions"]; + + for (let buttonId of buttonIds) { + let button = dialog.document.getElementById(buttonId); + ok(button, `${buttonId} found`); + } + + let dialogEl = dialog.document.querySelector("dialog"); + let acceptBtn = dialogEl.getButton("accept"); + let cancelBtn = dialogEl.getButton("cancel"); + + ok(!acceptBtn.hidden, "acceptButton found"); + ok(!cancelBtn.hidden, "cancelButton found"); +} + +function addNewPermission(websiteAddress, dialog) { + let url = dialog.document.getElementById("url"); + let buttonDialog = dialog.document.getElementById("btnBlock"); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let currentPermissions = permissionsBox.itemCount; + + url.value = websiteAddress; + url.dispatchEvent(new Event("input", { bubbles: true })); + is( + buttonDialog.hasAttribute("disabled"), + false, + "When the user add an url the button should be clickable" + ); + buttonDialog.click(); + + is( + permissionsBox.itemCount, + currentPermissions + 1, + "Website added in url should be in the list" + ); +} + +async function cleanList(dialog) { + let removeAllButton = dialog.document.getElementById("removeAllPermissions"); + if (!removeAllButton.hasAttribute("disabled")) { + removeAllButton.click(); + } +} + +function addData(websites, dialog) { + for (let website of websites) { + addNewPermission(website, dialog); + } +} + +function deletePermission(permission, dialog) { + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let elements = permissionsBox.getElementsByAttribute("origin", permission); + is(elements.length, 1, "It should find only one entry"); + permissionsBox.selectItem(elements[0]); + let removePermissionButton = + dialog.document.getElementById("removePermission"); + is( + removePermissionButton.hasAttribute("disabled"), + false, + "The button should be clickable to remove selected item" + ); + removePermissionButton.click(); +} + +function save(dialog) { + let saveButton = dialog.document.querySelector("dialog").getButton("accept"); + saveButton.click(); +} + +function cancel(dialog) { + let cancelButton = dialog.document + .querySelector("dialog") + .getButton("cancel"); + ok(!cancelButton.hidden, "cancelButton found"); + cancelButton.click(); +} + +async function checkExpected(expected, doc) { + let dialog = await openCookiesDialog(doc); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + + is( + permissionsBox.itemCount, + expected.length, + `There should be ${expected.length} elements in the list` + ); + + for (let website of expected) { + let elements = permissionsBox.getElementsByAttribute("origin", website); + is(elements.length, 1, "It should find only one entry"); + } + return dialog; +} + +async function runTest(test, websites, doc) { + let dialog = await openCookiesDialog(doc); + checkCookiesDialog(dialog); + + if (test.needPreviousData) { + addData(websites, dialog); + save(dialog); + dialog = await openCookiesDialog(doc); + } + + for (let step of test.steps) { + switch (step) { + case "addNewPermission": + addNewPermission(test.newData, dialog); + break; + case "deletePermission": + deletePermission(test.newData, dialog); + break; + case "deleteAllPermission": + await cleanList(dialog); + break; + case "save": + save(dialog); + break; + case "cancel": + cancel(dialog); + break; + case "openPane": + dialog = await openCookiesDialog(doc); + break; + default: + // code block + } + } + dialog = await checkExpected(test.expected, doc); + await cleanList(dialog); + save(dialog); +} + +add_task(async function checkPermissions() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let websites = ["http://test1.com", "http://test2.com"]; + + let tests = [ + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "save"], + expected: ["https://mytest.com"], // when open the pane again it should find this in the list + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "cancel"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deletePermission", "save"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deletePermission", "cancel"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deletePermission", + "save", + ], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deletePermission", + "cancel", + ], + expected: ["https://mytest.com"], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deleteAllPermission", "save"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: ["addNewPermission", "deleteAllPermission", "cancel"], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deleteAllPermission", + "save", + ], + expected: [], + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deleteAllPermission", + "cancel", + ], + expected: ["https://mytest.com"], + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["deleteAllPermission", "save"], + expected: [], + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["deleteAllPermission", "cancel"], + expected: websites, + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["addNewPermission", "save"], + expected: (function () { + let result = websites.slice(); + result.push("https://mytest.com"); + return result; + })(), + }, + { + needPreviousData: true, + newData: "https://mytest.com", + steps: ["addNewPermission", "cancel"], + expected: websites, + }, + { + needPreviousData: false, + newData: "https://mytest.com", + steps: [ + "addNewPermission", + "save", + "openPane", + "deleteAllPermission", + "addNewPermission", + "save", + ], + expected: ["https://mytest.com"], + }, + ]; + + for (let test of tests) { + await runTest(test, websites, doc); + } + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_cookies_exceptions.js b/browser/components/preferences/tests/browser_cookies_exceptions.js new file mode 100644 index 0000000000..d2d538a48a --- /dev/null +++ b/browser/components/preferences/tests/browser_cookies_exceptions.js @@ -0,0 +1,568 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +requestLongerTimeout(3); + +add_task(async function testAllow() { + await runTest( + async (params, observeAllPromise, apply) => { + assertListContents(params, []); + + params.url.value = "test.com"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://test.com", params.allowL10nId], + ["https://test.com", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testBlock() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "test.com"; + params.btnBlock.doCommand(); + + assertListContents(params, [ + ["http://test.com", params.denyL10nId], + ["https://test.com", params.denyL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + { + type: "cookie", + origin: "https://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowAgain() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "test.com"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://test.com", params.allowL10nId], + ["https://test.com", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://test.com", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testRemove() { + await runTest( + async (params, observeAllPromise, apply) => { + while (params.richlistbox.itemCount) { + params.richlistbox.selectedIndex = 0; + params.btnRemove.doCommand(); + } + assertListContents(params, []); + + apply(); + await observeAllPromise; + }, + params => { + let richlistItems = params.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "cookie", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); +}); + +add_task(async function testAdd() { + await runTest( + async (params, observeAllPromise, apply) => { + let uri = Services.io.newURI("http://test.com"); + PermissionTestUtils.add( + uri, + "popup", + Ci.nsIPermissionManager.DENY_ACTION + ); + + info("Adding unrelated permission should not change display."); + assertListContents(params, []); + + apply(); + await observeAllPromise; + + PermissionTestUtils.remove(uri, "popup"); + }, + params => { + return [ + { + type: "popup", + origin: "http://test.com", + data: "added", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "https://test.com:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["https://test.com:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "https://test.com:12345", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testBlockHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "https://test.com:12345"; + params.btnBlock.doCommand(); + + assertListContents(params, [ + ["https://test.com:12345", params.denyL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "https://test.com:12345", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowAgainHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "https://test.com:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["https://test.com:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "https://test.com:12345", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testRemoveHTTPSWithPort() { + await runTest( + async (params, observeAllPromise, apply) => { + while (params.richlistbox.itemCount) { + params.richlistbox.selectedIndex = 0; + params.btnRemove.doCommand(); + } + + assertListContents(params, []); + + apply(); + await observeAllPromise; + }, + params => { + let richlistItems = params.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "cookie", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); +}); + +add_task(async function testAllowPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "localhost:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://localhost:12345", params.allowL10nId], + ["https://localhost:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://localhost:12345", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://localhost:12345", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testBlockPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "localhost:12345"; + params.btnBlock.doCommand(); + + assertListContents(params, [ + ["http://localhost:12345", params.denyL10nId], + ["https://localhost:12345", params.denyL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + { + type: "cookie", + origin: "https://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.DENY_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testAllowAgainPort() { + await runTest( + async (params, observeAllPromise, apply) => { + params.url.value = "localhost:12345"; + params.btnAllow.doCommand(); + + assertListContents(params, [ + ["http://localhost:12345", params.allowL10nId], + ["https://localhost:12345", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + }, + params => { + return [ + { + type: "cookie", + origin: "http://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "https://localhost:12345", + data: "changed", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testRemovePort() { + await runTest( + async (params, observeAllPromise, apply) => { + while (params.richlistbox.itemCount) { + params.richlistbox.selectedIndex = 0; + params.btnRemove.doCommand(); + } + + assertListContents(params, []); + + apply(); + await observeAllPromise; + }, + params => { + let richlistItems = params.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "cookie", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); +}); + +add_task(async function testSort() { + await runTest( + async (params, observeAllPromise, apply) => { + // Sort by site name. + EventUtils.synthesizeMouseAtCenter( + params.doc.getElementById("siteCol"), + {}, + params.doc.defaultView + ); + + for (let URL of ["http://a", "http://z", "http://b"]) { + let URI = Services.io.newURI(URL); + PermissionTestUtils.add( + URI, + "cookie", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + } + + assertListContents(params, [ + ["http://a", params.allowL10nId], + ["http://b", params.allowL10nId], + ["http://z", params.allowL10nId], + ]); + + // Sort by site name in descending order. + EventUtils.synthesizeMouseAtCenter( + params.doc.getElementById("siteCol"), + {}, + params.doc.defaultView + ); + + assertListContents(params, [ + ["http://z", params.allowL10nId], + ["http://b", params.allowL10nId], + ["http://a", params.allowL10nId], + ]); + + apply(); + await observeAllPromise; + + for (let URL of ["http://a", "http://z", "http://b"]) { + let uri = Services.io.newURI(URL); + PermissionTestUtils.remove(uri, "cookie"); + } + }, + params => { + return [ + { + type: "cookie", + origin: "http://a", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "http://z", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + { + type: "cookie", + origin: "http://b", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + }, + ]; + } + ); +}); + +add_task(async function testPrivateBrowsingSessionPermissionsAreHidden() { + await runTest( + async (params, observeAllPromise, apply) => { + assertListContents(params, []); + + let uri = Services.io.newURI("http://test.com"); + let privateBrowsingPrincipal = + Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: 1, + }); + + // Add a session permission for private browsing. + PermissionTestUtils.add( + privateBrowsingPrincipal, + "cookie", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_SESSION + ); + + assertListContents(params, []); + + PermissionTestUtils.remove(uri, "cookie"); + }, + params => { + return []; + } + ); +}); + +function assertListContents(params, expected) { + Assert.equal(params.richlistbox.itemCount, expected.length); + + for (let i = 0; i < expected.length; i++) { + let website = expected[i][0]; + let elements = params.richlistbox.getElementsByAttribute("origin", website); + Assert.equal(elements.length, 1); // "It should find only one coincidence" + Assert.equal( + elements[0] + .querySelector(".website-capability-value") + .getAttribute("data-l10n-id"), + expected[i][1] + ); + } +} + +async function runTest(test, getObservances) { + registerCleanupFunction(function () { + Services.prefs.clearUserPref("privacy.history.custom"); + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let historyMode = doc.getElementById("historyMode"); + historyMode.value = "custom"; + historyMode.doCommand(); + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + doc.getElementById("cookieExceptions").doCommand(); + + let win = await promiseSubDialogLoaded; + + doc = win.document; + let params = { + doc, + richlistbox: doc.getElementById("permissionsBox"), + url: doc.getElementById("url"), + btnAllow: doc.getElementById("btnAllow"), + btnBlock: doc.getElementById("btnBlock"), + btnRemove: doc.getElementById("removePermission"), + allowL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIPermissionManager.ALLOW_ACTION + ), + denyL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIPermissionManager.DENY_ACTION + ), + allow: Ci.nsIPermissionManager.ALLOW_ACTION, + deny: Ci.nsIPermissionManager.DENY_ACTION, + }; + let btnApplyChanges = doc.querySelector("dialog").getButton("accept"); + let observances = getObservances(params); + let observeAllPromise = createObserveAllPromise(observances); + + await test(params, observeAllPromise, () => btnApplyChanges.doCommand()); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} diff --git a/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js new file mode 100644 index 0000000000..dc88e36cef --- /dev/null +++ b/browser/components/preferences/tests/browser_defaultbrowser_alwayscheck.js @@ -0,0 +1,185 @@ +"use strict"; + +const CHECK_DEFAULT_INITIAL = Services.prefs.getBoolPref( + "browser.shell.checkDefaultBrowser" +); + +add_task(async function clicking_make_default_checks_alwaysCheck_checkbox() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + await test_with_mock_shellservice({ isDefault: false }, async function () { + let checkDefaultBrowserState = isDefault => { + let isDefaultPane = content.document.getElementById("isDefaultPane"); + let isNotDefaultPane = + content.document.getElementById("isNotDefaultPane"); + Assert.equal( + ContentTaskUtils.is_hidden(isDefaultPane), + !isDefault, + "The 'browser is default' pane should be hidden when browser is not default" + ); + Assert.equal( + ContentTaskUtils.is_hidden(isNotDefaultPane), + isDefault, + "The 'make default' pane should be hidden when browser is default" + ); + }; + + checkDefaultBrowserState(false); + + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok(!alwaysCheck.checked, "Always Check is unchecked by default"); + Assert.ok( + !Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "alwaysCheck pref should be false by default in test runs" + ); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + await ContentTaskUtils.waitForCondition( + () => alwaysCheck.checked, + "'Always Check' checkbox should get checked after clicking the 'Set Default' button" + ); + + Assert.ok( + alwaysCheck.checked, + "Clicking 'Make Default' checks the 'Always Check' checkbox" + ); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "Checking the checkbox should set the pref to true" + ); + Assert.ok( + alwaysCheck.disabled, + "'Always Check' checkbox is locked with default browser and alwaysCheck=true" + ); + checkDefaultBrowserState(true); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "checkDefaultBrowser pref is now enabled" + ); + }); + + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser"); +}); + +add_task(async function clicking_make_default_checks_alwaysCheck_checkbox() { + Services.prefs.lockPref("browser.shell.checkDefaultBrowser"); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + await test_with_mock_shellservice({ isDefault: false }, async function () { + let isDefaultPane = content.document.getElementById("isDefaultPane"); + let isNotDefaultPane = content.document.getElementById("isNotDefaultPane"); + Assert.ok( + ContentTaskUtils.is_hidden(isDefaultPane), + "The 'browser is default' pane should be hidden when not default" + ); + Assert.ok( + ContentTaskUtils.is_visible(isNotDefaultPane), + "The 'make default' pane should be visible when not default" + ); + + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok(alwaysCheck.disabled, "Always Check is disabled when locked"); + Assert.ok( + alwaysCheck.checked, + "Always Check is checked because defaultPref is true and pref is locked" + ); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "alwaysCheck pref should ship with 'true' by default" + ); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + await ContentTaskUtils.waitForCondition( + () => ContentTaskUtils.is_visible(isDefaultPane), + "Browser is now default" + ); + + Assert.ok( + alwaysCheck.checked, + "'Always Check' is still checked because it's locked" + ); + Assert.ok( + alwaysCheck.disabled, + "'Always Check is disabled because it's locked" + ); + Assert.ok( + Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), + "The pref is locked and so doesn't get changed" + ); + }); + + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + gBrowser.removeCurrentTab(); +}); + +add_task(async function make_default_disabled_until_prefs_are_loaded() { + // Testcase with Firefox not set as the default browser + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + await test_with_mock_shellservice({ isDefault: false }, async function () { + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok( + !alwaysCheck.disabled, + "'Always Check' is enabled after default browser updated" + ); + }); + gBrowser.removeCurrentTab(); + + // Testcase with Firefox set as the default browser + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + await test_with_mock_shellservice({ isDefault: true }, async function () { + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + Assert.ok( + alwaysCheck.disabled, + "'Always Check' is still disabled after default browser updated" + ); + }); + gBrowser.removeCurrentTab(); +}); + +registerCleanupFunction(function () { + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + Services.prefs.setBoolPref( + "browser.shell.checkDefaultBrowser", + CHECK_DEFAULT_INITIAL + ); +}); + +async function test_with_mock_shellservice(options, testFn) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [options], + async function (contentOptions) { + let doc = content.document; + let win = doc.defaultView; + win.oldShellService = win.getShellService(); + let mockShellService = { + _isDefault: false, + isDefaultBrowser() { + return this._isDefault; + }, + setDefaultBrowser() { + this._isDefault = true; + }, + }; + win.getShellService = function () { + return mockShellService; + }; + mockShellService._isDefault = contentOptions.isDefault; + win.gMainPane.updateSetDefaultBrowser(); + } + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], testFn); + + Services.prefs.setBoolPref( + "browser.shell.checkDefaultBrowser", + CHECK_DEFAULT_INITIAL + ); +} diff --git a/browser/components/preferences/tests/browser_engines.js b/browser/components/preferences/tests/browser_engines.js new file mode 100644 index 0000000000..aa681e7039 --- /dev/null +++ b/browser/components/preferences/tests/browser_engines.js @@ -0,0 +1,141 @@ +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +SearchTestUtils.init(this); + +function getCellText(tree, i, cellName) { + return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName)); +} + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + keyword: ["testing", "customkeyword"], + search_url: "https://example.com/engine1", + search_url_get_params: "search={searchTerms}", + }); +}); + +add_task(async function test_engine_list() { + let prefs = await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneSearch", "Search pane is selected by default"); + let doc = gBrowser.contentDocument; + + let tree = doc.querySelector("#engineList"); + ok( + !tree.hidden, + "The search engine list should be visible when Search is requested" + ); + + // Check for default search engines to be displayed in the engineList + let defaultEngines = await Services.search.getAppProvidedEngines(); + for (let i = 0; i < defaultEngines.length; i++) { + let engine = defaultEngines[i]; + is( + getCellText(tree, i, "engineName"), + engine.name, + "Default search engine " + engine.name + " displayed correctly" + ); + } + + let customEngineIndex = defaultEngines.length; + is( + getCellText(tree, customEngineIndex, "engineKeyword"), + "testing, customkeyword", + "Show internal aliases" + ); + + // Scroll the treeview into view since mouse operations + // off screen can act confusingly. + tree.scrollIntoView(); + let rect = tree.getCoordsForCellItem( + customEngineIndex, + tree.columns.getNamedColumn("engineKeyword"), + "text" + ); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + let win = tree.ownerGlobal; + + let promise = BrowserTestUtils.waitForEvent(tree, "dblclick"); + EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win); + EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win); + await promise; + + EventUtils.sendString("newkeyword"); + EventUtils.sendKey("RETURN"); + + await TestUtils.waitForCondition(() => { + return ( + getCellText(tree, customEngineIndex, "engineKeyword") === + "newkeyword, testing, customkeyword" + ); + }); + + // Avoid duplicated keywords + tree.view.setCellText( + 0, + tree.columns.getNamedColumn("engineKeyword"), + "keyword" + ); + tree.view.setCellText( + 1, + tree.columns.getNamedColumn("engineKeyword"), + "keyword" + ); + isnot( + getCellText(tree, 1, "engineKeyword"), + "keyword", + "Do not allow duplicated keywords" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_remove_button_disabled_state() { + let prefs = await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneSearch", "Search pane is selected by default"); + let doc = gBrowser.contentDocument; + + let tree = doc.querySelector("#engineList"); + ok( + !tree.hidden, + "The search engine list should be visible when Search is requested" + ); + + let defaultEngines = await Services.search.getAppProvidedEngines(); + for (let i = 0; i < defaultEngines.length; i++) { + let engine = defaultEngines[i]; + + let isDefaultSearchEngine = + engine.name == Services.search.defaultEngine.name || + engine.name == Services.search.defaultPrivateEngine.name; + + tree.scrollIntoView(); + let rect = tree.getCoordsForCellItem( + i, + tree.columns.getNamedColumn("engineName"), + "text" + ); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + let win = tree.ownerGlobal; + + let promise = BrowserTestUtils.waitForEvent(tree, "click"); + EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win); + await promise; + + let removeButton = doc.querySelector("#removeEngineButton"); + is( + removeButton.disabled, + isDefaultSearchEngine, + "Remove button is in correct disable state" + ); + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_etp_exceptions_dialog.js b/browser/components/preferences/tests/browser_etp_exceptions_dialog.js new file mode 100644 index 0000000000..349223995c --- /dev/null +++ b/browser/components/preferences/tests/browser_etp_exceptions_dialog.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +const TRACKING_URL = "https://example.com"; + +async function openETPExceptionsDialog(doc) { + let exceptionsButton = doc.getElementById("trackingProtectionExceptions"); + ok(exceptionsButton, "trackingProtectionExceptions button found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + exceptionsButton.click(); + let dialog = await dialogPromise; + return dialog; +} + +async function addETPPermission(doc) { + let dialog = await openETPExceptionsDialog(doc); + let url = dialog.document.getElementById("url"); + let buttonDisableETP = dialog.document.getElementById("btnDisableETP"); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let currentPermissions = permissionsBox.itemCount; + + url.value = TRACKING_URL; + url.dispatchEvent(new Event("input", { bubbles: true })); + is( + buttonDisableETP.hasAttribute("disabled"), + false, + "Disable ETP button is selectable after url is entered" + ); + buttonDisableETP.click(); + + // Website is listed + is( + permissionsBox.itemCount, + currentPermissions + 1, + "Website added in url should be in the list" + ); + let saveButton = dialog.document.querySelector("dialog").getButton("accept"); + saveButton.click(); + BrowserTestUtils.waitForEvent(dialog, "unload"); +} + +async function removeETPPermission(doc) { + let dialog = await openETPExceptionsDialog(doc); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let elements = permissionsBox.getElementsByAttribute("origin", TRACKING_URL); + // Website is listed + ok(permissionsBox.itemCount, "List is not empty"); + permissionsBox.selectItem(elements[0]); + let removePermissionButton = + dialog.document.getElementById("removePermission"); + is( + removePermissionButton.hasAttribute("disabled"), + false, + "The button should be clickable to remove selected item" + ); + removePermissionButton.click(); + + let saveButton = dialog.document.querySelector("dialog").getButton("accept"); + saveButton.click(); + BrowserTestUtils.waitForEvent(dialog, "unload"); +} + +async function checkShieldIcon(shieldIcon) { + // Open the website and check that the tracking protection icon is enabled/disabled + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TRACKING_URL); + let icon = document.getElementById("tracking-protection-icon"); + is( + gBrowser.ownerGlobal + .getComputedStyle(icon) + .getPropertyValue("list-style-image"), + shieldIcon, + `The tracking protection icon shows the icon ${shieldIcon}` + ); + BrowserTestUtils.removeTab(tab); +} + +// test adds and removes an ETP permission via the about:preferences#privacy and checks if the ProtectionsUI shield icon resembles the state +add_task(async function ETPPermissionSyncedFromPrivacyPane() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + await addETPPermission(doc); + await checkShieldIcon( + `url("chrome://browser/skin/tracking-protection-disabled.svg")` + ); + await removeETPPermission(doc); + await checkShieldIcon(`url("chrome://browser/skin/tracking-protection.svg")`); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_experimental_features.js b/browser/components/preferences/tests/browser_experimental_features.js new file mode 100644 index 0000000000..cecbb60893 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testPrefRequired() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", false]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let experimentalCategory = doc.getElementById("category-experimental"); + ok(experimentalCategory, "The category exists"); + ok(experimentalCategory.hidden, "The category is hidden"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testCanOpenWithPref() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let experimentalCategory = doc.getElementById("category-experimental"); + ok(experimentalCategory, "The category exists"); + ok(!experimentalCategory.hidden, "The category is not hidden"); + + let categoryHeader = await TestUtils.waitForCondition( + () => doc.getElementById("firefoxExperimentalCategory"), + "Waiting for experimental features category to get initialized" + ); + ok( + categoryHeader.hidden, + "The category header should be hidden when Home is selected" + ); + + EventUtils.synthesizeMouseAtCenter(experimentalCategory, {}, doc.ownerGlobal); + await TestUtils.waitForCondition( + () => !categoryHeader.hidden, + "Waiting until category is visible" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testSearchFindsExperiments() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + + let experimentalCategory = doc.getElementById("category-experimental"); + ok(experimentalCategory, "The category exists"); + ok(!experimentalCategory.hidden, "The category is not hidden"); + + await TestUtils.waitForCondition( + () => doc.getElementById("firefoxExperimentalCategory"), + "Waiting for experimental features category to get initialized" + ); + await evaluateSearchResults( + "advanced configuration", + ["pane-experimental-featureGates"], + /* include experiments */ true + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_experimental_features_filter.js b/browser/components/preferences/tests/browser_experimental_features_filter.js new file mode 100644 index 0000000000..6bd66db555 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_filter.js @@ -0,0 +1,183 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test verifies that searching filters the features to just that subset that +// contains the search terms. +add_task(async function testFilterFeatures() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + // Add a number of test features. + const server = new DefinitionServer(); + let definitions = [ + { + id: "test-featureA", + preference: "test.featureA", + title: "Experimental Feature 1", + description: "This is a fun experimental feature you can enable", + result: true, + }, + { + id: "test-featureB", + preference: "test.featureB", + title: "Experimental Thing 2", + description: "This is a very boring experimental tool", + result: false, + }, + { + id: "test-featureC", + preference: "test.featureC", + title: "Experimental Thing 3", + description: "This is a fun experimental feature for you can enable", + result: true, + }, + { + id: "test-featureD", + preference: "test.featureD", + title: "Experimental Thing 4", + description: "This is a not a checkbox that you should be enabling", + result: false, + }, + ]; + for (let { id, preference } of definitions) { + server.addDefinition({ id, preference, isPublic: true }); + } + + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById(definitions[definitions.length - 1].id), + "wait for the first public feature to get added to the DOM" + ); + + // Manually modify the labels of the features that were just added, so that the test + // can rely on consistent search terms. + for (let definition of definitions) { + let mainItem = doc.getElementById(definition.id); + mainItem.label = definition.title; + mainItem.removeAttribute("data-l10n-id"); + let descItem = doc.getElementById(definition.id + "-description"); + descItem.textContent = definition.description; + descItem.removeAttribute("data-l10n-id"); + } + + // First, check that all of the items are visible by default. + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + `${definition.id} should be initially visible` + ); + } + + // After searching, only a subset should be visible. + await enterSearch(doc, "feature"); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + definition.result, + `${definition.id} should be ${ + definition.result ? "visible" : "hidden" + } after first search` + ); + info("Text for item was: " + doc.getElementById(definition.id).textContent); + } + + // Further restrict the search to only a single item. + await enterSearch(doc, " you"); + + let shouldBeVisible = true; + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + shouldBeVisible, + `${definition.id} should be ${ + shouldBeVisible ? "visible" : "hidden" + } after further search` + ); + shouldBeVisible = false; + } + + // Reset the search entirely. + let searchInput = doc.getElementById("searchInput"); + searchInput.value = ""; + searchInput.doCommand(); + + // Clearing the search will go to the general pane so switch back to the experimental pane. + EventUtils.synthesizeMouseAtCenter( + doc.getElementById("category-experimental"), + {}, + gBrowser.contentWindow + ); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + `${definition.id} should be visible after search cleared` + ); + } + + // Simulate entering a search and then clicking one of the category labels. The search + // should reset each time. + for (let category of ["category-search", "category-experimental"]) { + await enterSearch(doc, "feature"); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + definition.result, + `${definition.id} should be ${ + definition.result ? "visible" : "hidden" + } after next search` + ); + } + + EventUtils.synthesizeMouseAtCenter( + doc.getElementById(category), + {}, + gBrowser.contentWindow + ); + + for (let definition of definitions) { + checkVisibility( + doc.getElementById(definition.id), + true, + `${definition.id} should be visible after category change to ${category}` + ); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +function checkVisibility(element, expected, desc) { + return expected + ? is_element_visible(element, desc) + : is_element_hidden(element, desc); +} + +function enterSearch(doc, query) { + let searchInput = doc.getElementById("searchInput"); + searchInput.focus(); + + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + + EventUtils.sendString(query); + + return searchCompletedPromise; +} diff --git a/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js b/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js new file mode 100644 index 0000000000..e1e2adced9 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_hidden_when_not_public.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function testNonPublicFeaturesShouldntGetDisplayed() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.experimental", true]], + }); + + const server = new DefinitionServer(); + let definitions = [ + { id: "test-featureA", isPublic: true, preference: "test.feature.a" }, + { id: "test-featureB", isPublic: false, preference: "test.feature.b" }, + { id: "test-featureC", isPublic: true, preference: "test.feature.c" }, + ]; + for (let { id, isPublic, preference } of definitions) { + server.addDefinition({ id, isPublic, preference }); + } + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById(definitions.find(d => d.isPublic).id), + "wait for the first public feature to get added to the DOM" + ); + + for (let definition of definitions) { + is( + !!doc.getElementById(definition.id), + definition.isPublic, + "feature should only be in DOM if it's public: " + definition.id + ); + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testNonPublicFeaturesShouldntGetDisplayed() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.experimental", true], + ["browser.preferences.experimental.hidden", false], + ], + }); + + const server = new DefinitionServer(); + server.addDefinition({ + id: "test-hidden", + isPublic: false, + preference: "test.feature.hidden", + }); + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById("category-experimental").hidden, + "Wait for Experimental Features section to get hidden" + ); + + ok( + doc.getElementById("category-experimental").hidden, + "Experimental Features section should be hidden when all features are hidden" + ); + ok( + !doc.getElementById("firefoxExperimentalCategory"), + "Experimental Features header should not exist when all features are hidden" + ); + is( + doc.querySelector(".category[selected]").id, + "category-general", + "When the experimental features section is hidden, navigating to #experimental should redirect to #general" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_experimental_features_resetall.js b/browser/components/preferences/tests/browser_experimental_features_resetall.js new file mode 100644 index 0000000000..636374c057 --- /dev/null +++ b/browser/components/preferences/tests/browser_experimental_features_resetall.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// It doesn't matter what two preferences are used here, as long as the first is a built-in +// one that defaults to false and the second defaults to true. +const KNOWN_PREF_1 = "browser.display.use_system_colors"; +const KNOWN_PREF_2 = "browser.underline_anchors"; + +// This test verifies that pressing the reset all button for experimental features +// resets all of the checkboxes to their default state. +add_task(async function testResetAll() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.experimental", true], + ["test.featureA", false], + ["test.featureB", true], + [KNOWN_PREF_1, false], + [KNOWN_PREF_2, true], + ], + }); + + // Add a number of test features. + const server = new DefinitionServer(); + let definitions = [ + { + id: "test-featureA", + preference: "test.featureA", + defaultValue: false, + }, + { + id: "test-featureB", + preference: "test.featureB", + defaultValue: true, + }, + { + id: "test-featureC", + preference: KNOWN_PREF_1, + defaultValue: false, + }, + { + id: "test-featureD", + preference: KNOWN_PREF_2, + defaultValue: true, + }, + ]; + for (let { id, preference, defaultValue } of definitions) { + server.addDefinition({ id, preference, defaultValue, isPublic: true }); + } + + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + `about:preferences?definitionsUrl=${encodeURIComponent( + server.definitionsUrl + )}#paneExperimental` + ); + let doc = gBrowser.contentDocument; + + await TestUtils.waitForCondition( + () => doc.getElementById(definitions[definitions.length - 1].id), + "wait for the first public feature to get added to the DOM" + ); + + // Check the initial state of each feature. + ok(!Services.prefs.getBoolPref("test.featureA"), "initial state A"); + ok(Services.prefs.getBoolPref("test.featureB"), "initial state B"); + ok(!Services.prefs.getBoolPref(KNOWN_PREF_1), "initial state C"); + ok(Services.prefs.getBoolPref(KNOWN_PREF_2), "initial state D"); + + // Modify the state of some of the features. + doc.getElementById("test-featureC").click(); + doc.getElementById("test-featureD").click(); + ok(!Services.prefs.getBoolPref("test.featureA"), "modified state A"); + ok(Services.prefs.getBoolPref("test.featureB"), "modified state B"); + ok(Services.prefs.getBoolPref(KNOWN_PREF_1), "modified state C"); + ok(!Services.prefs.getBoolPref(KNOWN_PREF_2), "modified state D"); + + // State after reset. + let prefChangedPromise = new Promise(resolve => { + Services.prefs.addObserver(KNOWN_PREF_2, function observer() { + Services.prefs.removeObserver(KNOWN_PREF_2, observer); + resolve(); + }); + }); + doc.getElementById("experimentalCategory-reset").click(); + await prefChangedPromise; + + // The preferences will be reset to the default value for the feature. + ok(!Services.prefs.getBoolPref("test.featureA"), "after reset state A"); + ok(Services.prefs.getBoolPref("test.featureB"), "after reset state B"); + ok(!Services.prefs.getBoolPref(KNOWN_PREF_1), "after reset state C"); + ok(Services.prefs.getBoolPref(KNOWN_PREF_2), "after reset state D"); + ok( + !doc.getElementById("test-featureA").checked, + "after reset checkbox state A" + ); + ok( + doc.getElementById("test-featureB").checked, + "after reset checkbox state B" + ); + ok( + !doc.getElementById("test-featureC").checked, + "after reset checkbox state C" + ); + ok( + doc.getElementById("test-featureD").checked, + "after reset checkbox state D" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_extension_controlled.js b/browser/components/preferences/tests/browser_extension_controlled.js new file mode 100644 index 0000000000..6cec9ba93a --- /dev/null +++ b/browser/components/preferences/tests/browser_extension_controlled.js @@ -0,0 +1,1447 @@ +/* eslint-env webextensions */ + +const PROXY_PREF = "network.proxy.type"; +const HOMEPAGE_URL_PREF = "browser.startup.homepage"; +const HOMEPAGE_OVERRIDE_KEY = "homepage_override"; +const URL_OVERRIDES_TYPE = "url_overrides"; +const NEW_TAB_KEY = "newTabURL"; +const PREF_SETTING_TYPE = "prefs"; + +ChromeUtils.defineESModuleGetters(this, { + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", +}); + +ChromeUtils.defineModuleGetter( + this, + "AboutNewTab", + "resource:///modules/AboutNewTab.jsm" +); + +XPCOMUtils.defineLazyPreferenceGetter(this, "proxyType", PROXY_PREF); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +AddonTestUtils.initMochitest(this); + +const { ExtensionPreferencesManager } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPreferencesManager.sys.mjs" +); + +const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +const CHROME_URL_ROOT = TEST_DIR + "/"; +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml"; +let sitePermissionsDialog; + +function getSupportsFile(path) { + let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIChromeRegistry + ); + let uri = Services.io.newURI(CHROME_URL_ROOT + path); + let fileurl = cr.convertChromeURL(uri); + return fileurl.QueryInterface(Ci.nsIFileURL); +} + +function waitForMessageChange( + element, + cb, + opts = { attributes: true, attributeFilter: ["hidden"] } +) { + return waitForMutation(element, opts, cb); +} + +function getElement(id, doc = gBrowser.contentDocument) { + return doc.getElementById(id); +} + +function waitForMessageHidden(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => target.hidden + ); +} + +function waitForMessageShown(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => !target.hidden + ); +} + +function waitForEnableMessage(messageId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => target.classList.contains("extension-controlled-disabled"), + { attributeFilter: ["class"], attributes: true } + ); +} + +function waitForMessageContent(messageId, l10nId, doc) { + return waitForMessageChange( + getElement(messageId, doc), + target => doc.l10n.getAttributes(target).id === l10nId, + { childList: true } + ); +} + +async function openNotificationsPermissionDialog() { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let settingsButton = doc.getElementById("notificationSettingsButton"); + settingsButton.click(); + }); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +async function disableExtensionViaClick(labelId, disableButtonId, doc) { + let controlledLabel = doc.getElementById(labelId); + + let enableMessageShown = waitForEnableMessage(labelId, doc); + doc.getElementById(disableButtonId).click(); + await enableMessageShown; + + let controlledDescription = controlledLabel.querySelector("description"); + is( + doc.l10n.getAttributes(controlledDescription.querySelector("label")).id, + "extension-controlled-enable", + "The user is notified of how to enable the extension again." + ); + + // The user can dismiss the enable instructions. + let hidden = waitForMessageHidden(labelId, doc); + controlledLabel.querySelector("image:last-of-type").click(); + await hidden; +} + +async function reEnableExtension(addon, labelId) { + let controlledMessageShown = waitForMessageShown(labelId); + await addon.enable(); + await controlledMessageShown; +} + +add_task(async function testExtensionControlledHomepage() { + const ADDON_ID = "@set_homepage"; + const SECOND_ADDON_ID = "@second_set_homepage"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let homepagePref = () => Services.prefs.getCharPref(HOMEPAGE_URL_PREF); + let originalHomepagePref = homepagePref(); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + let doc = gBrowser.contentDocument; + let homeModeEl = doc.getElementById("homeMode"); + let customSettingsSection = doc.getElementById("customSettings"); + + is(homeModeEl.itemCount, 3, "The menu list starts with 3 options"); + + let promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount === 4, + "wait for the addon option to be added as an option in the menu list" + ); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_homepage", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "/home.html" }, + }, + }); + await extension.startup(); + await promise; + + // The homepage is set to the default and the custom settings section is hidden + is(homeModeEl.disabled, false, "The homepage menulist is enabled"); + is( + customSettingsSection.hidden, + true, + "The custom settings element is hidden" + ); + + let addon = await AddonManager.getAddonByID(ADDON_ID); + is( + homeModeEl.value, + addon.id, + "the home select menu's value is set to the addon" + ); + + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + // Set the Menu to the default value + homeModeEl.value = "0"; + homeModeEl.dispatchEvent(new Event("command")); + await promise; + is(homepagePref(), originalHomepagePref, "homepage is set back to default"); + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable." + ); + let setting = await ExtensionPreferencesManager.getSetting( + HOMEPAGE_OVERRIDE_KEY + ); + ok(!setting.value, "the setting is not set."); + + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + // Set the menu to the addon value + homeModeEl.value = ADDON_ID; + homeModeEl.dispatchEvent(new Event("command")); + await promise; + ok( + homepagePref().startsWith("moz-extension") && + homepagePref().endsWith("home.html"), + "Home url should be provided by the extension." + ); + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("home.html"), + "The setting value is the same as the extension." + ); + + // Add a second extension, ensure it is added to the menulist and selected. + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added as an option in the menu list" + ); + let secondExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "second_set_homepage", + browser_specific_settings: { + gecko: { + id: SECOND_ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "/home2.html" }, + }, + }); + await secondExtension.startup(); + await promise; + + let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID); + is(homeModeEl.value, SECOND_ADDON_ID, "home menulist is set to the add-on"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("home2.html"), + "The setting value is the same as the extension." + ); + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 4, + "addon option is no longer an option in the menu list after disable, even if it was not selected" + ); + await addon.disable(); + await promise; + + // Ensure that re-enabling an addon adds it back to the menulist + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added again to the menulist when enabled" + ); + await addon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 4, + "addon option is no longer an option in the menu list after disable" + ); + await secondAddon.disable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 5, + "addon option is added again to the menulist when enabled" + ); + await secondAddon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => homeModeEl.itemCount == 3, + "addon options are no longer an option in the menu list after disabling all addons" + ); + await secondAddon.disable(); + await addon.disable(); + await promise; + + is(homeModeEl.value, "0", "addon option is not selected in the menu list"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controllable_by_this_extension", + "getLevelOfControl returns controllable_by_this_extension." + ); + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok(!setting.value, "The setting value is back to default."); + + // The homepage elements are reset to their original state. + is(homepagePref(), originalHomepagePref, "homepage is set back to default"); + is(homeModeEl.disabled, false, "The homepage menulist is enabled"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await extension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testPrefLockedHomepage() { + const ADDON_ID = "@set_homepage"; + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let homePagePref = "browser.startup.homepage"; + let buttonPrefs = [ + "pref.browser.homepage.disable_button.current_page", + "pref.browser.homepage.disable_button.bookmark_page", + "pref.browser.homepage.disable_button.restore_default", + ]; + let homeModeEl = doc.getElementById("homeMode"); + let homePageInput = doc.getElementById("homePageUrl"); + let prefs = Services.prefs.getDefaultBranch(null); + let mutationOpts = { attributes: true, attributeFilter: ["disabled"] }; + + // Helper functions. + let getButton = pref => + doc.querySelector(`.homepage-button[preference="${pref}"`); + let waitForAllMutations = () => + Promise.all( + buttonPrefs + .map(pref => waitForMutation(getButton(pref), mutationOpts)) + .concat([ + waitForMutation(homeModeEl, mutationOpts), + waitForMutation(homePageInput, mutationOpts), + ]) + ); + let getHomepage = () => + Services.prefs.getCharPref("browser.startup.homepage"); + + let originalHomepage = getHomepage(); + let extensionHomepage = "https://developer.mozilla.org/"; + let lockedHomepage = "http://www.yahoo.com"; + + let lockPrefs = () => { + buttonPrefs.forEach(pref => { + prefs.setBoolPref(pref, true); + prefs.lockPref(pref); + }); + // Do the homepage last since that's the only pref that triggers a UI update. + prefs.setCharPref(homePagePref, lockedHomepage); + prefs.lockPref(homePagePref); + }; + let unlockPrefs = () => { + buttonPrefs.forEach(pref => { + prefs.unlockPref(pref); + prefs.setBoolPref(pref, false); + }); + // Do the homepage last since that's the only pref that triggers a UI update. + prefs.unlockPref(homePagePref); + prefs.setCharPref(homePagePref, originalHomepage); + }; + + // Lock or unlock prefs then wait for all mutations to finish. + // Expects a bool indicating if we should lock or unlock. + let waitForLockMutations = lock => { + let mutationsDone = waitForAllMutations(); + if (lock) { + lockPrefs(); + } else { + unlockPrefs(); + } + return mutationsDone; + }; + + ok( + originalHomepage != extensionHomepage, + "The extension will change the homepage" + ); + + // Install an extension that sets the homepage to MDN. + let promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_homepage", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_settings_overrides: { homepage: "https://developer.mozilla.org/" }, + }, + }); + await extension.startup(); + await promise; + + // Check that everything is still disabled, homepage didn't change. + is( + getHomepage(), + extensionHomepage, + "The reported homepage is set by the extension" + ); + is( + homePageInput.value, + extensionHomepage, + "The homepage is set by the extension" + ); + + // Lock all of the prefs, wait for the UI to update. + await waitForLockMutations(true); + + // Check that everything is now disabled. + is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref"); + is(homePageInput.value, lockedHomepage, "The homepage is set by the pref"); + is( + homePageInput.disabled, + true, + "The homepage is disabed when the pref is locked" + ); + + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + true, + `The ${pref} button is disabled when locked` + ); + }); + + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + ADDON_ID, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable, the pref is locked." + ); + + // Verify that the UI is selecting the extension's Id in the menulist + let unlockedPromise = TestUtils.waitForCondition( + () => homeModeEl.value == ADDON_ID, + "Homepage menulist value is equal to the extension ID" + ); + // Unlock the prefs, wait for the UI to update. + unlockPrefs(); + await unlockedPromise; + + is( + homeModeEl.disabled, + false, + "the home select element is not disabled when the pref is not locked" + ); + is( + homePageInput.disabled, + false, + "The homepage is enabled when the pref is unlocked" + ); + is( + getHomepage(), + extensionHomepage, + "The homepage is reset to extension page" + ); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + ADDON_ID, + HOMEPAGE_OVERRIDE_KEY, + PREF_SETTING_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension after prefs are unlocked." + ); + let setting = await ExtensionPreferencesManager.getSetting( + HOMEPAGE_OVERRIDE_KEY + ); + is( + setting.value, + extensionHomepage, + "The setting value is equal to the extensionHomepage." + ); + + // Uninstall the add-on. + promise = TestUtils.waitForPrefChange(HOMEPAGE_URL_PREF); + await extension.unload(); + await promise; + + setting = await ExtensionPreferencesManager.getSetting(HOMEPAGE_OVERRIDE_KEY); + ok(!setting, "The setting is gone after the addon is uninstalled."); + + // Check that everything is now enabled again. + is( + getHomepage(), + originalHomepage, + "The reported homepage is reset to original value" + ); + is(homePageInput.value, "", "The homepage is empty"); + is( + homePageInput.disabled, + false, + "The homepage is enabled after clearing lock" + ); + is( + homeModeEl.disabled, + false, + "Homepage menulist is enabled after clearing lock" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + false, + `The ${pref} button is enabled when unlocked` + ); + }); + + // Lock the prefs without an extension. + await waitForLockMutations(true); + + // Check that everything is now disabled. + is(getHomepage(), lockedHomepage, "The reported homepage is set by the pref"); + is(homePageInput.value, lockedHomepage, "The homepage is set by the pref"); + is( + homePageInput.disabled, + true, + "The homepage is disabed when the pref is locked" + ); + is( + homeModeEl.disabled, + true, + "Homepage menulist is disabled when pref is locked" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + true, + `The ${pref} button is disabled when locked` + ); + }); + + // Unlock the prefs without an extension. + await waitForLockMutations(false); + + // Check that everything is enabled again. + is( + getHomepage(), + originalHomepage, + "The homepage is reset to the original value" + ); + is(homePageInput.value, "", "The homepage is clear after being unlocked"); + is( + homePageInput.disabled, + false, + "The homepage is enabled after clearing lock" + ); + is( + homeModeEl.disabled, + false, + "Homepage menulist is enabled after clearing lock" + ); + buttonPrefs.forEach(pref => { + is( + getButton(pref).disabled, + false, + `The ${pref} button is enabled when unlocked` + ); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledNewTab() { + const ADDON_ID = "@set_newtab"; + const SECOND_ADDON_ID = "@second_set_newtab"; + const DEFAULT_NEWTAB = "about:newtab"; + const NEWTAB_CONTROLLED_PREF = "browser.newtab.extensionControlled"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let doc = gBrowser.contentDocument; + let newTabMenuList = doc.getElementById("newTabMode"); + // The new tab page is set to the default. + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + + let promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is added as an option in the menu list" + ); + // Install an extension that will set the new tab page. + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "set_newtab", + browser_specific_settings: { + gecko: { + id: ADDON_ID, + }, + }, + chrome_url_overrides: { newtab: "/newtab.html" }, + }, + }); + await extension.startup(); + + await promise; + let addon = await AddonManager.getAddonByID(ADDON_ID); + + is(newTabMenuList.value, ADDON_ID, "New tab menulist is set to the add-on"); + + let levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + let setting = ExtensionSettingsStore.getSetting( + URL_OVERRIDES_TYPE, + NEW_TAB_KEY + ); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("newtab.html"), + "The url_overrides is set by this extension" + ); + + promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF); + // Set the menu to the default value + newTabMenuList.value = "0"; + newTabMenuList.dispatchEvent(new Event("command")); + await promise; + let newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false); + is(newTabPref, false, "the new tab is not controlled"); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + addon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "not_controllable", + "getLevelOfControl returns not_controllable." + ); + setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY); + ok(!setting.value, "The url_overrides is not set by this extension"); + + promise = TestUtils.waitForPrefChange(NEWTAB_CONTROLLED_PREF); + // Set the menu to a the addon value + newTabMenuList.value = ADDON_ID; + newTabMenuList.dispatchEvent(new Event("command")); + await promise; + newTabPref = Services.prefs.getBoolPref(NEWTAB_CONTROLLED_PREF, false); + is(newTabPref, true, "the new tab is controlled"); + + // Add a second extension, ensure it is added to the menulist and selected. + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added as an option in the menu list" + ); + let secondExtension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + version: "1.0", + name: "second_set_newtab", + browser_specific_settings: { + gecko: { + id: SECOND_ADDON_ID, + }, + }, + chrome_url_overrides: { newtab: "/newtab2.html" }, + }, + }); + await secondExtension.startup(); + await promise; + let secondAddon = await AddonManager.getAddonByID(SECOND_ADDON_ID); + is( + newTabMenuList.value, + SECOND_ADDON_ID, + "New tab menulist is set to the add-on" + ); + + levelOfControl = await ExtensionPreferencesManager.getLevelOfControl( + secondAddon.id, + NEW_TAB_KEY, + URL_OVERRIDES_TYPE + ); + is( + levelOfControl, + "controlled_by_this_extension", + "getLevelOfControl returns controlled_by_this_extension." + ); + setting = ExtensionSettingsStore.getSetting(URL_OVERRIDES_TYPE, NEW_TAB_KEY); + ok( + setting.value.startsWith("moz-extension") && + setting.value.endsWith("newtab2.html"), + "The url_overrides is set by the second extension" + ); + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is no longer an option in the menu list after disable, even if it was not selected" + ); + await addon.disable(); + await promise; + + // Ensure that re-enabling an addon adds it back to the menulist + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added again to the menulist when enabled" + ); + await addon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 3, + "addon option is no longer an option in the menu list after disable" + ); + await secondAddon.disable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 4, + "addon option is added again to the menulist when enabled" + ); + await secondAddon.enable(); + await promise; + + promise = TestUtils.waitForCondition( + () => newTabMenuList.itemCount == 2, + "addon options are all removed after disabling all" + ); + await addon.disable(); + await secondAddon.disable(); + await promise; + is( + AboutNewTab.newTabURL, + DEFAULT_NEWTAB, + "new tab page is set back to default" + ); + + // Cleanup the tabs and add-on. + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await extension.unload(); + await secondExtension.unload(); +}); + +add_task(async function testExtensionControlledWebNotificationsPermission() { + let manifest = { + manifest_version: 2, + name: "TestExtension", + version: "1.0", + description: "Testing WebNotificationsDisable", + browser_specific_settings: { gecko: { id: "@web_notifications_disable" } }, + permissions: ["browserSettings"], + browser_action: { + default_title: "Testing", + }, + }; + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openNotificationsPermissionDialog(); + + let doc = sitePermissionsDialog.document; + let extensionControlledContent = doc.getElementById( + "browserNotificationsPermissionExtensionContent" + ); + + // Test that extension content is initially hidden. + ok( + extensionControlledContent.hidden, + "Extension content is initially hidden" + ); + + // Install an extension that will disable web notifications permission. + let messageShown = waitForMessageShown( + "browserNotificationsPermissionExtensionContent", + doc + ); + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + background() { + browser.browserSettings.webNotificationsDisabled.set({ value: true }); + browser.test.sendMessage("load-extension"); + }, + }); + await extension.startup(); + await extension.awaitMessage("load-extension"); + await messageShown; + + let controlledDesc = extensionControlledContent.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-web-notifications", + args: { + name: "TestExtension", + }, + }, + "The user is notified that an extension is controlling the web notifications permission" + ); + is( + extensionControlledContent.hidden, + false, + "The extension controlled row is not hidden" + ); + + // Disable the extension. + doc.getElementById("disableNotificationsPermissionExtension").click(); + + // Verify the user is notified how to enable the extension. + await waitForEnableMessage(extensionControlledContent.id, doc); + is( + doc.l10n.getAttributes(controlledDesc.querySelector("label")).id, + "extension-controlled-enable", + "The user is notified of how to enable the extension again" + ); + + // Verify the enable message can be dismissed. + let hidden = waitForMessageHidden(extensionControlledContent.id, doc); + let dismissButton = controlledDesc.querySelector("image:last-of-type"); + dismissButton.click(); + await hidden; + + // Verify that the extension controlled content in hidden again. + is( + extensionControlledContent.hidden, + true, + "The extension controlled row is now hidden" + ); + + await extension.unload(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledHomepageUninstalledAddon() { + async function checkHomepageEnabled() { + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + // The homepage is enabled. + let homepageInput = doc.getElementById("homePageUrl"); + is(homepageInput.disabled, false, "The homepage input is enabled"); + is(homepageInput.value, "", "The homepage input is empty"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + await ExtensionSettingsStore.initialize(); + + // Verify the setting isn't reported as controlled and the inputs are enabled. + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override"), + null, + "The homepage_override is not set" + ); + await checkHomepageEnabled(); + + // Disarm any pending writes before we modify the JSONFile directly. + await ExtensionSettingsStore._reloadFile(false); + + // Write out a bad store file. + let storeData = { + prefs: { + homepage_override: { + initialValue: "", + precedenceList: [ + { + id: "bad@mochi.test", + installDate: 1508802672, + value: "https://developer.mozilla.org", + enabled: true, + }, + ], + }, + }, + }; + let jsonFileName = "extension-settings.json"; + let storePath = PathUtils.join(PathUtils.profileDir, jsonFileName); + + await IOUtils.writeUTF8(storePath, JSON.stringify(storeData)); + + // Reload the ExtensionSettingsStore so it will read the file on disk. Don't + // finalize the current store since it will overwrite our file. + await ExtensionSettingsStore._reloadFile(false); + + // Verify that the setting is reported as set, but the homepage is still enabled + // since there is no matching installed extension. + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override").value, + "https://developer.mozilla.org", + "The homepage_override appears to be set" + ); + await checkHomepageEnabled(); + + // Remove the bad store file that we used. + await IOUtils.remove(storePath); + + // Reload the ExtensionSettingsStore again so it clears the data we added. + // Don't finalize the current store since it will write out the bad data. + await ExtensionSettingsStore._reloadFile(false); + + is( + ExtensionSettingsStore.getSetting("prefs", "homepage_override"), + null, + "The ExtensionSettingsStore is left empty." + ); +}); + +add_task(async function testExtensionControlledTrackingProtection() { + const TP_PREF = "privacy.trackingprotection.enabled"; + const TP_DEFAULT = false; + const EXTENSION_ID = "@set_tp"; + const CONTROLLED_LABEL_ID = + "contentBlockingTrackingProtectionExtensionContentLabel"; + const CONTROLLED_BUTTON_ID = + "contentBlockingDisableTrackingProtectionExtension"; + + let tpEnabledPref = () => Services.prefs.getBoolPref(TP_PREF); + + await SpecialPowers.pushPrefEnv({ set: [[TP_PREF, TP_DEFAULT]] }); + + function background() { + browser.privacy.websites.trackingProtectionMode.set({ value: "always" }); + } + + function verifyState(isControlled) { + is(tpEnabledPref(), isControlled, "TP pref is set to the expected value."); + + let controlledLabel = doc.getElementById(CONTROLLED_LABEL_ID); + let controlledButton = doc.getElementById(CONTROLLED_BUTTON_ID); + + is( + controlledLabel.hidden, + !isControlled, + "The extension controlled row's visibility is as expected." + ); + is( + controlledButton.hidden, + !isControlled, + "The disable extension button's visibility is as expected." + ); + if (isControlled) { + let controlledDesc = controlledLabel.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-websites-content-blocking-all-trackers", + args: { + name: "set_tp", + }, + }, + "The user is notified that an extension is controlling TP." + ); + } + + is( + doc.getElementById("trackingProtectionMenu").disabled, + isControlled, + "TP control is enabled." + ); + } + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + is( + gBrowser.currentURI.spec, + "about:preferences#privacy", + "#privacy should be in the URI for about:preferences" + ); + + verifyState(false); + + // Install an extension that sets Tracking Protection. + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "set_tp", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["privacy"], + }, + background, + }); + + let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID); + await extension.startup(); + await messageShown; + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + verifyState(true); + + await disableExtensionViaClick( + CONTROLLED_LABEL_ID, + CONTROLLED_BUTTON_ID, + doc + ); + + verifyState(false); + + // Enable the extension so we get the UNINSTALL event, which is needed by + // ExtensionPreferencesManager to clean up properly. + // TODO: BUG 1408226 + await reEnableExtension(addon, CONTROLLED_LABEL_ID); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledPasswordManager() { + const PASSWORD_MANAGER_ENABLED_PREF = "signon.rememberSignons"; + const PASSWORD_MANAGER_ENABLED_DEFAULT = true; + const CONTROLLED_BUTTON_ID = "disablePasswordManagerExtension"; + const CONTROLLED_LABEL_ID = "passwordManagerExtensionContent"; + const EXTENSION_ID = "@remember_signons"; + let manifest = { + manifest_version: 2, + name: "testPasswordManagerExtension", + version: "1.0", + description: "Testing rememberSignons", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["privacy"], + browser_action: { + default_title: "Testing rememberSignons", + }, + }; + + let passwordManagerEnabledPref = () => + Services.prefs.getBoolPref(PASSWORD_MANAGER_ENABLED_PREF); + + await SpecialPowers.pushPrefEnv({ + set: [[PASSWORD_MANAGER_ENABLED_PREF, PASSWORD_MANAGER_ENABLED_DEFAULT]], + }); + is( + passwordManagerEnabledPref(), + true, + "Password manager is enabled by default." + ); + + function verifyState(isControlled) { + is( + passwordManagerEnabledPref(), + !isControlled, + "Password manager pref is set to the expected value." + ); + let controlledLabel = + gBrowser.contentDocument.getElementById(CONTROLLED_LABEL_ID); + let controlledButton = + gBrowser.contentDocument.getElementById(CONTROLLED_BUTTON_ID); + is( + controlledLabel.hidden, + !isControlled, + "The extension's controlled row visibility is as expected." + ); + is( + controlledButton.hidden, + !isControlled, + "The extension's controlled button visibility is as expected." + ); + if (isControlled) { + let controlledDesc = controlledLabel.querySelector("description"); + Assert.deepEqual( + gBrowser.contentDocument.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-password-saving", + args: { + name: "testPasswordManagerExtension", + }, + }, + "The user is notified that an extension is controlling the remember signons pref." + ); + } + } + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + info("Verify that no extension is controlling the password manager pref."); + verifyState(false); + + let extension = ExtensionTestUtils.loadExtension({ + manifest, + useAddonManager: "permanent", + background() { + browser.privacy.services.passwordSavingEnabled.set({ value: false }); + }, + }); + let messageShown = waitForMessageShown(CONTROLLED_LABEL_ID); + await extension.startup(); + await messageShown; + + info( + "Verify that the test extension is controlling the password manager pref." + ); + verifyState(true); + + info("Verify that the extension shows as controlled when loaded again."); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + verifyState(true); + + await disableExtensionViaClick( + CONTROLLED_LABEL_ID, + CONTROLLED_BUTTON_ID, + gBrowser.contentDocument + ); + + info( + "Verify that disabling the test extension removes the lock on the password manager pref." + ); + verifyState(false); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function testExtensionControlledProxyConfig() { + const proxySvc = Ci.nsIProtocolProxyService; + const PROXY_DEFAULT = proxySvc.PROXYCONFIG_SYSTEM; + const EXTENSION_ID = "@set_proxy"; + const CONTROLLED_SECTION_ID = "proxyExtensionContent"; + const CONTROLLED_BUTTON_ID = "disableProxyExtension"; + const CONNECTION_SETTINGS_DESC_ID = "connectionSettingsDescription"; + const PANEL_URL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + await SpecialPowers.pushPrefEnv({ set: [[PROXY_PREF, PROXY_DEFAULT]] }); + + function background() { + browser.proxy.settings.set({ value: { proxyType: "none" } }); + } + + function expectedConnectionSettingsMessage(doc, isControlled) { + return isControlled + ? "extension-controlling-proxy-config" + : "network-proxy-connection-description"; + } + + function connectionSettingsMessagePromise(doc, isControlled) { + return waitForMessageContent( + CONNECTION_SETTINGS_DESC_ID, + expectedConnectionSettingsMessage(doc, isControlled), + doc + ); + } + + function verifyProxyState(doc, isControlled) { + let isPanel = doc.getElementById(CONTROLLED_BUTTON_ID); + is( + proxyType === proxySvc.PROXYCONFIG_DIRECT, + isControlled, + "Proxy pref is set to the expected value." + ); + + if (isPanel) { + let controlledSection = doc.getElementById(CONTROLLED_SECTION_ID); + + is( + controlledSection.hidden, + !isControlled, + "The extension controlled row's visibility is as expected." + ); + if (isPanel) { + is( + doc.getElementById(CONTROLLED_BUTTON_ID).hidden, + !isControlled, + "The disable extension button's visibility is as expected." + ); + } + if (isControlled) { + let controlledDesc = controlledSection.querySelector("description"); + Assert.deepEqual( + doc.l10n.getAttributes(controlledDesc), + { + id: "extension-controlling-proxy-config", + args: { + name: "set_proxy", + }, + }, + "The user is notified that an extension is controlling proxy settings." + ); + } + function getProxyControls() { + let controlGroup = doc.getElementById("networkProxyType"); + let manualControlContainer = controlGroup.querySelector("#proxy-grid"); + return { + manualControls: [ + ...manualControlContainer.querySelectorAll( + "label[data-l10n-id]:not([control=networkProxyNone])" + ), + ...manualControlContainer.querySelectorAll("input"), + ...manualControlContainer.querySelectorAll("checkbox"), + ...doc.querySelectorAll("#networkProxySOCKSVersion > radio"), + ], + pacControls: [doc.getElementById("networkProxyAutoconfigURL")], + otherControls: [ + doc.querySelector("label[control=networkProxyNone]"), + doc.getElementById("networkProxyNone"), + ...controlGroup.querySelectorAll(":scope > radio"), + ...doc.querySelectorAll("#ConnectionsDialogPane > checkbox"), + ], + }; + } + let controlState = isControlled ? "disabled" : "enabled"; + let controls = getProxyControls(); + for (let element of controls.manualControls) { + let disabled = + isControlled || proxyType !== proxySvc.PROXYCONFIG_MANUAL; + is( + element.disabled, + disabled, + `Manual proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + for (let element of controls.pacControls) { + let disabled = isControlled || proxyType !== proxySvc.PROXYCONFIG_PAC; + is( + element.disabled, + disabled, + `PAC proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + for (let element of controls.otherControls) { + is( + element.disabled, + isControlled, + `Other proxy controls should be ${controlState} - control: ${element.outerHTML}.` + ); + } + } else { + let elem = doc.getElementById(CONNECTION_SETTINGS_DESC_ID); + is( + doc.l10n.getAttributes(elem).id, + expectedConnectionSettingsMessage(doc, isControlled), + "The connection settings description is as expected." + ); + } + } + + async function reEnableProxyExtension(addon) { + let messageChanged = connectionSettingsMessagePromise(mainDoc, true); + await addon.enable(); + await messageChanged; + } + + async function openProxyPanel() { + let panel = await openAndLoadSubDialog(PANEL_URL); + let closingPromise = BrowserTestUtils.waitForEvent( + panel.document.getElementById("ConnectionsDialog"), + "dialogclosing" + ); + ok(panel, "Proxy panel opened."); + return { panel, closingPromise }; + } + + async function closeProxyPanel(panelObj) { + let dialog = panelObj.panel.document.getElementById("ConnectionsDialog"); + dialog.cancelDialog(); + let panelClosingEvent = await panelObj.closingPromise; + ok(panelClosingEvent, "Proxy panel closed."); + } + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let mainDoc = gBrowser.contentDocument; + + is( + gBrowser.currentURI.spec, + "about:preferences#general", + "#general should be in the URI for about:preferences" + ); + + verifyProxyState(mainDoc, false); + + // Open the connections panel. + let panelObj = await openProxyPanel(); + let panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + + verifyProxyState(mainDoc, false); + + // Install an extension that controls proxy settings. The extension needs + // incognitoOverride because controlling the proxy.settings requires private + // browsing access. + let extension = ExtensionTestUtils.loadExtension({ + incognitoOverride: "spanning", + useAddonManager: "permanent", + manifest: { + name: "set_proxy", + browser_specific_settings: { gecko: { id: EXTENSION_ID } }, + permissions: ["proxy"], + }, + background, + }); + + let messageChanged = connectionSettingsMessagePromise(mainDoc, true); + await extension.startup(); + await messageChanged; + let addon = await AddonManager.getAddonByID(EXTENSION_ID); + + verifyProxyState(mainDoc, true); + messageChanged = connectionSettingsMessagePromise(mainDoc, false); + + panelObj = await openProxyPanel(); + panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, true); + + await disableExtensionViaClick( + CONTROLLED_SECTION_ID, + CONTROLLED_BUTTON_ID, + panelDoc + ); + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + await messageChanged; + + verifyProxyState(mainDoc, false); + + await reEnableProxyExtension(addon); + + verifyProxyState(mainDoc, true); + messageChanged = connectionSettingsMessagePromise(mainDoc, false); + + panelObj = await openProxyPanel(); + panelDoc = panelObj.panel.document; + + verifyProxyState(panelDoc, true); + + await disableExtensionViaClick( + CONTROLLED_SECTION_ID, + CONTROLLED_BUTTON_ID, + panelDoc + ); + + verifyProxyState(panelDoc, false); + + await closeProxyPanel(panelObj); + await messageChanged; + + verifyProxyState(mainDoc, false); + + // Enable the extension so we get the UNINSTALL event, which is needed by + // ExtensionPreferencesManager to clean up properly. + // TODO: BUG 1408226 + await reEnableProxyExtension(addon); + + await extension.unload(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test that the newtab menu selection is correct when loading about:preferences +add_task(async function testMenuSyncFromPrefs() { + const DEFAULT_NEWTAB = "about:newtab"; + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + let doc = gBrowser.contentDocument; + let newTabMenuList = doc.getElementById("newTabMode"); + // The new tab page is set to the default. + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + + is(newTabMenuList.value, "0", "New tab menulist is set to the default"); + + newTabMenuList.value = "1"; + newTabMenuList.dispatchEvent(new Event("command")); + is(newTabMenuList.value, "1", "New tab menulist is set to blank"); + + gBrowser.reloadTab(gBrowser.selectedTab); + + await TestUtils.waitForCondition( + () => gBrowser.contentDocument.getElementById("newTabMode"), + "wait until element exists in new contentDoc" + ); + + is( + gBrowser.contentDocument.getElementById("newTabMode").value, + "1", + "New tab menulist is still set to blank" + ); + + // Cleanup + newTabMenuList.value = "0"; + newTabMenuList.dispatchEvent(new Event("command")); + is(AboutNewTab.newTabURL, DEFAULT_NEWTAB, "new tab is set to default"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_filetype_dialog.js b/browser/components/preferences/tests/browser_filetype_dialog.js new file mode 100644 index 0000000000..0e5ac036c4 --- /dev/null +++ b/browser/components/preferences/tests/browser_filetype_dialog.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +SimpleTest.requestCompleteLog(); +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +let gOldMailHandlers = []; +let gDummyHandlers = []; +let gOriginalPreferredMailHandler; +let gOriginalPreferredPDFHandler; + +registerCleanupFunction(function () { + function removeDummyHandlers(handlers) { + // Remove any of the dummy handlers we created. + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + if ( + gDummyHandlers.some( + h => + h.uriTemplate == + handlers.queryElementAt(i, Ci.nsIWebHandlerApp).uriTemplate + ) + ) { + handlers.removeElementAt(i); + } + } catch (ex) { + /* ignore non-web-app handlers */ + } + } + } + // Re-add the original protocol handlers: + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + removeDummyHandlers(mailHandlers); + mailHandlerInfo.preferredApplicationHandler = gOriginalPreferredMailHandler; + gHandlerService.store(mailHandlerInfo); + + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally; + pdfHandlerInfo.preferredApplicationHandler = gOriginalPreferredPDFHandler; + let handlers = pdfHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + let app = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (app.name == "Foopydoopydoo") { + handlers.removeElementAt(i); + } + } + gHandlerService.store(pdfHandlerInfo); + + gBrowser.removeCurrentTab(); +}); + +function scrubMailtoHandlers(handlerInfo) { + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = handlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } +} + +("use strict"); + +add_setup(async function () { + // Create our dummy handlers + let handler1 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler1.name = "Handler 1"; + handler1.uriTemplate = "https://example.com/first/%s"; + + let handler2 = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + handler2.name = "Handler 2"; + handler2.uriTemplate = "http://example.org/second/%s"; + gDummyHandlers.push(handler1, handler2); + + function substituteWebHandlers(handlerInfo) { + // Append the dummy handlers to replace them: + let handlers = handlerInfo.possibleApplicationHandlers; + handlers.appendElement(handler1); + handlers.appendElement(handler2); + gHandlerService.store(handlerInfo); + } + // Set up our mailto handler test infrastructure. + let mailtoHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + scrubMailtoHandlers(mailtoHandlerInfo); + gOriginalPreferredMailHandler = mailtoHandlerInfo.preferredApplicationHandler; + substituteWebHandlers(mailtoHandlerInfo); + + // Now add a pdf handler: + let pdfHandlerInfo = + HandlerServiceTestUtils.getHandlerInfo("application/pdf"); + // PDF doesn't have built-in web handlers, so no need to scrub. + gOriginalPreferredPDFHandler = pdfHandlerInfo.preferredApplicationHandler; + let handlers = pdfHandlerInfo.possibleApplicationHandlers; + let appHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + appHandler.name = "Foopydoopydoo"; + appHandler.executable = Services.dirsvc.get("ProfD", Ci.nsIFile); + appHandler.executable.append("dummy.exe"); + // Prefs are picky and want this to exist and be executable (bug 1626009): + if (!appHandler.executable.exists()) { + appHandler.executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o777); + } + + handlers.appendElement(appHandler); + + pdfHandlerInfo.preferredApplicationHandler = appHandler; + pdfHandlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + gHandlerService.store(pdfHandlerInfo); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + info("Preferences page opened on the general pane."); + + await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList; + info("Apps list loaded."); +}); + +add_task(async function dialogShowsCorrectContent() { + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First, find the PDF item. + let pdfItem = container.querySelector("richlistitem[type='application/pdf']"); + Assert.ok(pdfItem, "pdfItem is present in handlersView."); + pdfItem.scrollIntoView({ block: "center" }); + pdfItem.closest("richlistbox").selectItem(pdfItem); + + // Open its menu + let list = pdfItem.querySelector(".actionsMenu"); + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(list, {}, win); + await popupShown; + + // Then open the dialog + const promiseDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/applicationManager.xhtml" + ); + EventUtils.synthesizeMouseAtCenter( + popup.querySelector(".manage-app-item"), + {}, + win + ); + let dialogWin = await promiseDialogLoaded; + + // Then verify that the description is correct. + let desc = dialogWin.document.getElementById("appDescription"); + let descL10n = dialogWin.document.l10n.getAttributes(desc); + is(descL10n.id, "app-manager-handle-file", "Should have right string"); + let stringBundle = Services.strings.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + is( + descL10n.args.type, + stringBundle.GetStringFromName("pdfExtHandlerDescription"), + "Should have PDF string bits." + ); + + // And that there's one item in the list, with the correct name: + let appList = dialogWin.document.getElementById("appList"); + is(appList.itemCount, 1, "Should have 1 item in the list"); + is( + appList.selectedItem.querySelector("label").getAttribute("value"), + "Foopydoopydoo", + "Should have the right executable label" + ); + + dialogWin.close(); +}); diff --git a/browser/components/preferences/tests/browser_fluent.js b/browser/components/preferences/tests/browser_fluent.js new file mode 100644 index 0000000000..db2daecc4f --- /dev/null +++ b/browser/components/preferences/tests/browser_fluent.js @@ -0,0 +1,40 @@ +function whenMainPaneLoadedFinished() { + return new Promise(function (resolve, reject) { + const topic = "main-pane-loaded"; + Services.obs.addObserver(function observer(aSubject) { + Services.obs.removeObserver(observer, topic); + resolve(); + }, topic); + }); +} + +// Temporary test for an experimental new localization API. +// See bug 1402069 for details. +add_task(async function () { + // The string is used only when `browserTabsRemoteAutostart` is true + if (!Services.appinfo.browserTabsRemoteAutostart) { + ok(true, "fake test to avoid harness complaining"); + return; + } + + await Promise.all([ + openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }), + whenMainPaneLoadedFinished(), + ]); + + let doc = gBrowser.contentDocument; + await doc.l10n.ready; + + let [msg] = await doc.l10n.formatMessages([{ id: "category-general" }]); + + let elem = doc.querySelector(`#category-general`); + + Assert.deepEqual(msg, { + value: null, + attributes: [ + { name: "tooltiptext", value: elem.getAttribute("tooltiptext") }, + ], + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_homepage_default.js b/browser/components/preferences/tests/browser_homepage_default.js new file mode 100644 index 0000000000..e3fcdcec5e --- /dev/null +++ b/browser/components/preferences/tests/browser_homepage_default.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function default_homepage_test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.startup.page", 1]], + }); + let defaults = Services.prefs.getDefaultBranch(""); + // Simulate a homepage set via policy or a distribution. + defaults.setStringPref("browser.startup.homepage", "https://example.com"); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + let homeMode = doc.getElementById("homeMode"); + Assert.equal(homeMode.value, 2, "homeMode should be 2 (Custom URL)"); + + let homePageUrl = doc.getElementById("homePageUrl"); + Assert.equal( + homePageUrl.value, + "https://example.com", + "homePageUrl should be example.com" + ); + + registerCleanupFunction(async () => { + defaults.setStringPref("browser.startup.homepage", "about:home"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); diff --git a/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js new file mode 100644 index 0000000000..f54d0edaaa --- /dev/null +++ b/browser/components/preferences/tests/browser_homepages_filter_aboutpreferences.js @@ -0,0 +1,33 @@ +add_task(async function testSetHomepageUseCurrent() { + is( + gBrowser.currentURI.spec, + "about:blank", + "Test starts with about:blank open" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home"); + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + let doc = gBrowser.contentDocument; + is( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + let oldHomepage = HomePage.get(); + + let useCurrent = doc.getElementById("useCurrentBtn"); + useCurrent.click(); + + is(gBrowser.tabs.length, 3, "Three tabs should be open"); + await TestUtils.waitForCondition( + () => HomePage.get() == "about:blank|about:home" + ); + is( + HomePage.get(), + "about:blank|about:home", + "about:blank and about:home should be the only homepages set" + ); + + HomePage.safeSet(oldHomepage); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_homepages_use_bookmark.js b/browser/components/preferences/tests/browser_homepages_use_bookmark.js new file mode 100644 index 0000000000..572783481d --- /dev/null +++ b/browser/components/preferences/tests/browser_homepages_use_bookmark.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL1 = "http://example.com/1"; +const TEST_URL2 = "http://example.com/2"; + +add_setup(async function () { + let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage"); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + Assert.equal( + gBrowser.currentURI.spec, + "about:preferences#home", + "#home should be in the URI for about:preferences" + ); + + registerCleanupFunction(async () => { + Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function testSetHomepageFromBookmark() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "TestHomepage", + url: TEST_URL1, + }); + + let doc = gBrowser.contentDocument; + // Select the custom URLs option. + doc.getElementById("homeMode").value = 2; + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml" + ); + doc.getElementById("useBookmarkBtn").click(); + + let dialog = await promiseSubDialogLoaded; + dialog.document.getElementById("bookmarks").selectItems([bm.guid]); + dialog.document + .getElementById("selectBookmarkDialog") + .getButton("accept") + .click(); + + await TestUtils.waitForCondition(() => HomePage.get() == TEST_URL1); + + Assert.equal( + HomePage.get(), + TEST_URL1, + "Should have set the homepage to the same as the bookmark." + ); +}); + +add_task(async function testSetHomepageFromTopLevelFolder() { + // Insert a second item into the menu folder + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "TestHomepage", + url: TEST_URL2, + }); + + let doc = gBrowser.contentDocument; + // Select the custom URLs option. + doc.getElementById("homeMode").value = 2; + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/selectBookmark.xhtml" + ); + doc.getElementById("useBookmarkBtn").click(); + + let dialog = await promiseSubDialogLoaded; + dialog.document + .getElementById("bookmarks") + .selectItems([PlacesUtils.bookmarks.menuGuid]); + dialog.document + .getElementById("selectBookmarkDialog") + .getButton("accept") + .click(); + + await TestUtils.waitForCondition( + () => HomePage.get() == `${TEST_URL1}|${TEST_URL2}` + ); + + Assert.equal( + HomePage.get(), + `${TEST_URL1}|${TEST_URL2}`, + "Should have set the homepage to the same as the bookmark." + ); +}); diff --git a/browser/components/preferences/tests/browser_hometab_restore_defaults.js b/browser/components/preferences/tests/browser_hometab_restore_defaults.js new file mode 100644 index 0000000000..55a9a974b2 --- /dev/null +++ b/browser/components/preferences/tests/browser_hometab_restore_defaults.js @@ -0,0 +1,220 @@ +add_task(async function testRestoreDefaultsBtn_visible() { + const before = SpecialPowers.Services.prefs.getStringPref( + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + "" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Hide Pocket prefs so we don't trigger network requests when we reset all preferences + [ + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + JSON.stringify(Object.assign({}, JSON.parse(before), { hidden: true })), + ], + [ + "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear", + "", + ], + // Set a user pref to false to force the Restore Defaults button to be visible + ["browser.newtabpage.activity-stream.feeds.topsites", false], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#home", + false + ); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn") !== null + ), + "Wait for the button to be added to the page" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ) !== null + ), + "Wait for the preference checkbox to load" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn") + .hidden === false + ), + "Should show the Restore Defaults btn because pref is changed" + ); + + await SpecialPowers.spawn(browser, [], () => + content.document.getElementById("restoreDefaultHomePageBtn").click() + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ).checked + ), + "Should have checked preference" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn").style + .visibility === "hidden" + ), + "Should not show the Restore Defaults btn if prefs were reset" + ); + + const topsitesPref = await SpecialPowers.Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + Assert.ok(topsitesPref, "Topsites pref should have the default value"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testRestoreDefaultsBtn_hidden() { + const before = SpecialPowers.Services.prefs.getStringPref( + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + "" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Hide Pocket pref so we don't trigger network requests when we reset all preferences + [ + "browser.newtabpage.activity-stream.feeds.section.topstories.options", + JSON.stringify(Object.assign({}, JSON.parse(before), { hidden: true })), + ], + [ + "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear", + "", + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#home", + false + ); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn") !== null + ), + "Wait for the button to be added to the page" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ) !== null + ), + "Wait for the preference checkbox to load" + ); + + const btnDefault = await SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn").style + .visibility + ); + Assert.equal( + btnDefault, + "hidden", + "When no prefs are changed button should not show up" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ).checked + ), + "Should have checked preference" + ); + + // Uncheck a pref + await SpecialPowers.spawn(browser, [], () => + content.document + .querySelector("[data-subcategory='topsites'] checkbox") + .click() + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + !content.document.querySelector( + "[data-subcategory='topsites'] checkbox" + ).checked + ), + "Should have unchecked preference" + ); + + await BrowserTestUtils.waitForCondition( + () => + SpecialPowers.spawn( + browser, + [], + () => + content.document.getElementById("restoreDefaultHomePageBtn").style + .visibility === "" + ), + "Should show the Restore Defaults btn if prefs were changed" + ); + + // Reset the pref + await SpecialPowers.Services.prefs.clearUserPref( + "browser.newtabpage.activity-stream.feeds.topsites" + ); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_https_only_exceptions.js b/browser/components/preferences/tests/browser_https_only_exceptions.js new file mode 100644 index 0000000000..ad7cd34571 --- /dev/null +++ b/browser/components/preferences/tests/browser_https_only_exceptions.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * First Test + * Checks if buttons are disabled/enabled and visible/hidden correctly. + */ +add_task(async function testButtons() { + // Let's make sure HTTPS-Only Mode is off. + await setHttpsOnlyPref("off"); + + // Open the privacy-pane in about:preferences + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + // Get button-element to open the exceptions-dialog + const exceptionButton = gBrowser.contentDocument.getElementById( + "httpsOnlyExceptionButton" + ); + + is( + exceptionButton.disabled, + true, + "HTTPS-Only exception button should be disabled when HTTPS-Only Mode is disabled." + ); + + await setHttpsOnlyPref("private"); + is( + exceptionButton.disabled, + true, + "HTTPS-Only exception button should be disabled when HTTPS-Only Mode is only enabled in private browsing." + ); + + await setHttpsOnlyPref("everywhere"); + is( + exceptionButton.disabled, + false, + "HTTPS-Only exception button should be enabled when HTTPS-Only Mode enabled everywhere." + ); + + // Now that the button is clickable, we open the dialog + // to check if the correct buttons are visible + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + exceptionButton.doCommand(); + + let win = await promiseSubDialogLoaded; + + const dialogDoc = win.document; + + is( + dialogDoc.getElementById("btnBlock").hidden, + true, + "Block button should not be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnCookieSession").hidden, + true, + "Cookie specific allow button should not be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnAllow").hidden, + true, + "Allow button should not be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnHttpsOnlyOff").hidden, + false, + "HTTPS-Only off button should be visible in HTTPS-Only Dialog." + ); + is( + dialogDoc.getElementById("btnHttpsOnlyOffTmp").hidden, + false, + "HTTPS-Only temporary off button should be visible in HTTPS-Only Dialog." + ); + + // Reset prefs and close the tab + await SpecialPowers.flushPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Second Test + * Checks permissions are added and removed correctly. + * + * Each test opens a new dialog, performs an action (second argument), + * then closes the dialog and checks if the changes were made (third argument). + */ +add_task(async function checkDialogFunctionality() { + // Enable HTTPS-Only Mode for every window, so the exceptions dialog is accessible. + await setHttpsOnlyPref("everywhere"); + + // Open the privacy-pane in about:preferences + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + const preferencesDoc = gBrowser.contentDocument; + + // Test if we can add permanent exceptions + await runTest( + preferencesDoc, + elements => { + assertListContents(elements, []); + + elements.url.value = "test.com"; + elements.btnAllow.doCommand(); + + assertListContents(elements, [ + ["http://test.com", elements.allowL10nId], + ["https://test.com", elements.allowL10nId], + ]); + }, + () => [ + { + type: "https-only-load-insecure", + origin: "http://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: Ci.nsIPermissionManager.EXPIRE_NEVER, + }, + { + type: "https-only-load-insecure", + origin: "https://test.com", + data: "added", + capability: Ci.nsIPermissionManager.ALLOW_ACTION, + expireType: Ci.nsIPermissionManager.EXPIRE_NEVER, + }, + ] + ); + + // Test if items are retained, and if temporary exceptions are added correctly + await runTest( + preferencesDoc, + elements => { + assertListContents(elements, [ + ["http://test.com", elements.allowL10nId], + ["https://test.com", elements.allowL10nId], + ]); + + elements.url.value = "1.1.1.1:8080"; + elements.btnAllowSession.doCommand(); + + assertListContents(elements, [ + ["http://test.com", elements.allowL10nId], + ["https://test.com", elements.allowL10nId], + ["http://1.1.1.1:8080", elements.allowSessionL10nId], + ["https://1.1.1.1:8080", elements.allowSessionL10nId], + ]); + }, + () => [ + { + type: "https-only-load-insecure", + origin: "http://1.1.1.1:8080", + data: "added", + capability: Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, + expireType: Ci.nsIPermissionManager.EXPIRE_SESSION, + }, + { + type: "https-only-load-insecure", + origin: "https://1.1.1.1:8080", + data: "added", + capability: Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, + expireType: Ci.nsIPermissionManager.EXPIRE_SESSION, + }, + ] + ); + + await runTest( + preferencesDoc, + elements => { + while (elements.richlistbox.itemCount) { + elements.richlistbox.selectedIndex = 0; + elements.btnRemove.doCommand(); + } + assertListContents(elements, []); + }, + elements => { + let richlistItems = elements.richlistbox.getElementsByAttribute( + "origin", + "*" + ); + let observances = []; + for (let item of richlistItems) { + observances.push({ + type: "https-only-load-insecure", + origin: item.getAttribute("origin"), + data: "deleted", + }); + } + return observances; + } + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Changes HTTPS-Only Mode pref + * @param {string} state "everywhere", "private", "off" + */ +async function setHttpsOnlyPref(state) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", state === "everywhere"], + ["dom.security.https_only_mode_pbm", state === "private"], + ], + }); +} + +/** + * Opens new exceptions dialog, runs test function + * @param {HTMLElement} preferencesDoc document of about:preferences tab + * @param {function} test function to call when dialog is open + * @param {Array} observances permission changes to observe (order is important) + */ +async function runTest(preferencesDoc, test, observancesFn) { + // Click on exception-button and wait for dialog to open + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + preferencesDoc.getElementById("httpsOnlyExceptionButton").doCommand(); + + let win = await promiseSubDialogLoaded; + + // Create a bunch of references to UI-elements for the test-function + const doc = win.document; + let elements = { + richlistbox: doc.getElementById("permissionsBox"), + url: doc.getElementById("url"), + btnAllow: doc.getElementById("btnHttpsOnlyOff"), + btnAllowSession: doc.getElementById("btnHttpsOnlyOffTmp"), + btnRemove: doc.getElementById("removePermission"), + allowL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIPermissionManager.ALLOW_ACTION + ), + allowSessionL10nId: win.gPermissionManager._getCapabilityL10nId( + Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION + ), + }; + + // Some observances need to be computed based on the current state. + const observances = observancesFn(elements); + + // Run test function + await test(elements); + + // Click on "Save changes" and wait for permission changes. + let btnApplyChanges = doc.querySelector("dialog").getButton("accept"); + let observeAllPromise = createObserveAllPromise(observances); + + btnApplyChanges.doCommand(); + await observeAllPromise; +} + +function assertListContents(elements, expected) { + is( + elements.richlistbox.itemCount, + expected.length, + "Richlistbox should match the expected amount of exceptions." + ); + + for (let i = 0; i < expected.length; i++) { + let website = expected[i][0]; + let listItem = elements.richlistbox.getElementsByAttribute( + "origin", + website + ); + is(listItem.length, 1, "Each origin should be unique"); + is( + listItem[0] + .querySelector(".website-capability-value") + .getAttribute("data-l10n-id"), + expected[i][1], + "List item capability should match expected l10n-id" + ); + } +} diff --git a/browser/components/preferences/tests/browser_https_only_section.js b/browser/components/preferences/tests/browser_https_only_section.js new file mode 100644 index 0000000000..a43448adf5 --- /dev/null +++ b/browser/components/preferences/tests/browser_https_only_section.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 1671122 - Fixed bug where second click on HTTPS-Only Mode enable-checkbox disables it again. +// https://bugzilla.mozilla.org/bug/1671122 +"use strict"; + +const HTTPS_ONLY_ENABLED = "enabled"; +const HTTPS_ONLY_PBM_ONLY = "privateOnly"; +const HTTPS_ONLY_DISABLED = "disabled"; + +add_task(async function httpsOnlyRadioGroupIsWorking() { + // Make sure HTTPS-Only mode is only enabled for PBM + + registerCleanupFunction(async function () { + Services.prefs.clearUserPref("dom.security.https_only_mode"); + Services.prefs.clearUserPref("dom.security.https_only_mode_pbm"); + }); + + await SpecialPowers.setBoolPref("dom.security.https_only_mode", false); + await SpecialPowers.setBoolPref("dom.security.https_only_mode_pbm", true); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const radioGroup = doc.getElementById("httpsOnlyRadioGroup"); + const enableAllRadio = doc.getElementById("httpsOnlyRadioEnabled"); + const enablePbmRadio = doc.getElementById("httpsOnlyRadioEnabledPBM"); + const disableRadio = doc.getElementById("httpsOnlyRadioDisabled"); + + // Check if UI + check(radioGroup, HTTPS_ONLY_PBM_ONLY); + + // Check if UI updated on pref-change + await SpecialPowers.setBoolPref("dom.security.https_only_mode_pbm", false); + check(radioGroup, HTTPS_ONLY_DISABLED); + + // Check if prefs change if clicked on radio button + enableAllRadio.click(); + check(radioGroup, HTTPS_ONLY_ENABLED); + + // Check if prefs stay the same if clicked on same + // radio button again (see bug 1671122) + enableAllRadio.click(); + check(radioGroup, HTTPS_ONLY_ENABLED); + + // Check if prefs are set correctly for PBM-only mode. + enablePbmRadio.click(); + check(radioGroup, HTTPS_ONLY_PBM_ONLY); + + // Check if prefs are set correctly when disabled again. + disableRadio.click(); + check(radioGroup, HTTPS_ONLY_DISABLED); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +function check(radioGroupElement, expectedValue) { + is( + radioGroupElement.value, + expectedValue, + "Radio Group value should match expected value" + ); + is( + SpecialPowers.getBoolPref("dom.security.https_only_mode"), + expectedValue === HTTPS_ONLY_ENABLED, + "HTTPS-Only pref should match expected value." + ); + is( + SpecialPowers.getBoolPref("dom.security.https_only_mode_pbm"), + expectedValue === HTTPS_ONLY_PBM_ONLY, + "HTTPS-Only PBM pref should match expected value." + ); +} diff --git a/browser/components/preferences/tests/browser_ignore_invalid_capability.js b/browser/components/preferences/tests/browser_ignore_invalid_capability.js new file mode 100644 index 0000000000..35b2679ffc --- /dev/null +++ b/browser/components/preferences/tests/browser_ignore_invalid_capability.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function testInvalidCapabilityIgnored() { + info( + "Test to make sure that invalid combinations of type and capability are ignored \ + so the cookieExceptions management popup does not crash" + ); + PermissionTestUtils.add( + "https://mozilla.org", + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + // This is an invalid combination of type & capability and should be ignored + PermissionTestUtils.add( + "https://foobar.org", + "cookie", + Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION + ); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/permissions.xhtml" + ); + doc.getElementById("cookieExceptions").doCommand(); + + let win = await promiseSubDialogLoaded; + doc = win.document; + + is( + doc.getElementById("permissionsBox").itemCount, + 1, + "We only display the permission that is valid for the type cookie" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_languages_subdialog.js b/browser/components/preferences/tests/browser_languages_subdialog.js new file mode 100644 index 0000000000..e85ce44ca3 --- /dev/null +++ b/browser/components/preferences/tests/browser_languages_subdialog.js @@ -0,0 +1,139 @@ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const contentDocument = gBrowser.contentDocument; + let dialogOverlay = content.gSubDialog._preloadDialog._overlay; + + async function languagesSubdialogOpened() { + const promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/languages.xhtml" + ); + contentDocument.getElementById("chooseLanguage").click(); + const win = await promiseSubDialogLoaded; + dialogOverlay = content.gSubDialog._topDialog._overlay; + ok(!BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is visible."); + return win; + } + + function acceptLanguagesSubdialog(win) { + const button = win.document.querySelector("dialog").getButton("accept"); + button.doCommand(); + } + + ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible."); + let win = await languagesSubdialogOpened(); + ok( + win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox is hidden." + ); + acceptLanguagesSubdialog(win); + ok(BrowserTestUtils.is_hidden(dialogOverlay), "The dialog is invisible."); + + await SpecialPowers.pushPrefEnv({ + set: [["intl.accept_languages", "en-US,en-XX,foo"]], + }); + win = await languagesSubdialogOpened(); + let activeLanguages = win.document.getElementById("activeLanguages").children; + ok( + activeLanguages[0].id == "en-us", + "The ID for 'en-US' locale code is correctly set." + ); + ok( + activeLanguages[0].firstChild.value == "English (United States) [en-us]", + "The name for known 'en-US' locale code is correctly resolved." + ); + ok( + activeLanguages[1].id == "en-xx", + "The ID for 'en-XX' locale code is correctly set." + ); + ok( + activeLanguages[1].firstChild.value == "English [en-xx]", + "The name for unknown 'en-XX' locale code is resolved using 'en'." + ); + ok( + activeLanguages[2].firstChild.value == " [foo]", + "The name for unknown 'foo' locale code is empty." + ); + acceptLanguagesSubdialog(win); + await SpecialPowers.popPrefEnv(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting", true], + ["privacy.spoof_english", 0], + ], + }); + + win = await languagesSubdialogOpened(); + ok( + !win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox isn't hidden." + ); + ok( + !win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox isn't checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 0, + "The privacy.spoof_english pref is set to 0." + ); + + win.document.getElementById("spoofEnglish").checked = true; + win.document.getElementById("spoofEnglish").doCommand(); + ok( + win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox is checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 2, + "The privacy.spoof_english pref is set to 2." + ); + acceptLanguagesSubdialog(win); + + win = await languagesSubdialogOpened(); + ok( + !win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox isn't hidden." + ); + ok( + win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox is checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 2, + "The privacy.spoof_english pref is set to 2." + ); + + win.document.getElementById("spoofEnglish").checked = false; + win.document.getElementById("spoofEnglish").doCommand(); + ok( + !win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox isn't checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 1, + "The privacy.spoof_english pref is set to 1." + ); + acceptLanguagesSubdialog(win); + + win = await languagesSubdialogOpened(); + ok( + !win.document.getElementById("spoofEnglish").hidden, + "The 'Request English' checkbox isn't hidden." + ); + ok( + !win.document.getElementById("spoofEnglish").checked, + "The 'Request English' checkbox isn't checked." + ); + is( + win.Preferences.get("privacy.spoof_english").value, + 1, + "The privacy.spoof_english pref is set to 1." + ); + acceptLanguagesSubdialog(win); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_layersacceleration.js b/browser/components/preferences/tests/browser_layersacceleration.js new file mode 100644 index 0000000000..982a32c94d --- /dev/null +++ b/browser/components/preferences/tests/browser_layersacceleration.js @@ -0,0 +1,36 @@ +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["gfx.direct2d.disabled", false], + ["layers.acceleration.disabled", false], + ], + }); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let checkbox = doc.querySelector("#allowHWAccel"); + is( + !checkbox.checked, + Services.prefs.getBoolPref("layers.acceleration.disabled"), + "checkbox should represent inverted pref value before clicking on checkbox" + ); + + checkbox.click(); + + is( + !checkbox.checked, + Services.prefs.getBoolPref("layers.acceleration.disabled"), + "checkbox should represent inverted pref value after clicking on checkbox" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_localSearchShortcuts.js b/browser/components/preferences/tests/browser_localSearchShortcuts.js new file mode 100644 index 0000000000..0b8e170cc1 --- /dev/null +++ b/browser/components/preferences/tests/browser_localSearchShortcuts.js @@ -0,0 +1,309 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Checks the local shortcut rows in the engines list of the search pane. + */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +let gTree; + +add_setup(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); + + Assert.equal( + prefs.selectedPane, + "paneSearch", + "Sanity check: Search pane is selected by default" + ); + + gTree = gBrowser.contentDocument.querySelector("#engineList"); + gTree.scrollIntoView(); + gTree.focus(); +}); + +// The rows should be visible and checked by default. +add_task(async function visible() { + await checkRowVisibility(true); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.equal( + gTree.view.getCellValue(row, gTree.columns.getNamedColumn("engineShown")), + "true", + "Row is checked initially" + ); + }); +}); + +// Toggling the browser.urlbar.shortcuts.* prefs should toggle the corresponding +// checkboxes in the rows. +add_task(async function syncFromPrefs() { + let col = gTree.columns.getNamedColumn("engineShown"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked initially" + ); + await SpecialPowers.pushPrefEnv({ + set: [[getUrlbarPrefName(shortcut.pref), false]], + }); + Assert.equal( + gTree.view.getCellValue(row, col), + "false", + "Row is unchecked after disabling pref" + ); + await SpecialPowers.popPrefEnv(); + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked after re-enabling pref" + ); + }); +}); + +// Pressing the space key while a row is selected should toggle its checkbox +// and pref. +add_task(async function syncToPrefs_spaceKey() { + let col = gTree.columns.getNamedColumn("engineShown"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + UrlbarPrefs.get(shortcut.pref), + "Sanity check: Pref is enabled initially" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked initially" + ); + gTree.view.selection.select(row); + EventUtils.synthesizeKey(" ", {}, gTree.ownerGlobal); + Assert.ok( + !UrlbarPrefs.get(shortcut.pref), + "Pref is disabled after pressing space key" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "false", + "Row is unchecked after pressing space key" + ); + Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref)); + }); +}); + +// Clicking the checkbox in a local shortcut row should toggle the checkbox and +// pref. +add_task(async function syncToPrefs_click() { + let col = gTree.columns.getNamedColumn("engineShown"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + UrlbarPrefs.get(shortcut.pref), + "Sanity check: Pref is enabled initially" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "true", + "Row is checked initially" + ); + + let rect = gTree.getCoordsForCellItem(row, col, "cell"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + EventUtils.synthesizeMouse(gTree.body, x, y, {}, gTree.ownerGlobal); + + Assert.ok( + !UrlbarPrefs.get(shortcut.pref), + "Pref is disabled after clicking checkbox" + ); + Assert.equal( + gTree.view.getCellValue(row, col), + "false", + "Row is unchecked after clicking checkbox" + ); + Services.prefs.clearUserPref(getUrlbarPrefName(shortcut.pref)); + }); +}); + +// The keyword column should not be editable according to isEditable(). +add_task(async function keywordNotEditable_isEditable() { + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + !gTree.view.isEditable( + row, + gTree.columns.getNamedColumn("engineKeyword") + ), + "Keyword column is not editable" + ); + }); +}); + +// Pressing the enter key while a row is selected shouldn't allow the keyword to +// be edited. +add_task(async function keywordNotEditable_enterKey() { + let col = gTree.columns.getNamedColumn("engineKeyword"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + shortcut.restrict, + "Sanity check: Shortcut restriction char is non-empty" + ); + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Sanity check: Keyword column has correct restriction char initially" + ); + + gTree.view.selection.select(row); + EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); + EventUtils.sendString("newkeyword"); + EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); + + // Wait a moment to allow for any possible asynchronicity. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Keyword column is still restriction char" + ); + }); +}); + +// Double-clicking the keyword column shouldn't allow the keyword to be edited. +add_task(async function keywordNotEditable_click() { + let col = gTree.columns.getNamedColumn("engineKeyword"); + await forEachLocalShortcutRow(async (row, shortcut) => { + Assert.ok( + shortcut.restrict, + "Sanity check: Shortcut restriction char is non-empty" + ); + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Sanity check: Keyword column has correct restriction char initially" + ); + + let rect = gTree.getCoordsForCellItem(row, col, "text"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + + let promise = BrowserTestUtils.waitForEvent(gTree, "dblclick"); + + // Click once to select the row. + EventUtils.synthesizeMouse( + gTree.body, + x, + y, + { clickCount: 1 }, + gTree.ownerGlobal + ); + + // Now double-click the keyword column. + EventUtils.synthesizeMouse( + gTree.body, + x, + y, + { clickCount: 2 }, + gTree.ownerGlobal + ); + + await promise; + + EventUtils.sendString("newkeyword"); + EventUtils.synthesizeKey("KEY_Enter", {}, gTree.ownerGlobal); + + // Wait a moment to allow for any possible asynchronicity. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + + Assert.equal( + gTree.view.getCellText(row, col), + shortcut.restrict, + "Keyword column is still restriction char" + ); + }); +}); + +/** + * Asserts that the engine and local shortcut rows are present in the tree. + */ +async function checkRowVisibility() { + let engines = await Services.search.getVisibleEngines(); + + Assert.equal( + gTree.view.rowCount, + engines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "Expected number of tree rows" + ); + + // Check the engine rows. + for (let row = 0; row < engines.length; row++) { + let engine = engines[row]; + let text = gTree.view.getCellText( + row, + gTree.columns.getNamedColumn("engineName") + ); + Assert.equal( + text, + engine.name, + `Sanity check: Tree row ${row} has expected engine name` + ); + } + + // Check the shortcut rows. + await forEachLocalShortcutRow(async (row, shortcut) => { + let text = gTree.view.getCellText( + row, + gTree.columns.getNamedColumn("engineName") + ); + let name = UrlbarUtils.getResultSourceName(shortcut.source); + let l10nName = await gTree.ownerDocument.l10n.formatValue( + `urlbar-search-mode-${name}` + ); + Assert.ok(l10nName, "Sanity check: l10n name is non-empty"); + Assert.equal(text, l10nName, `Tree row ${row} has expected shortcut name`); + }); +} + +/** + * Calls a callback for each local shortcut row in the tree. + * + * @param {function} callback + * Called for each local shortcut row like: callback(rowIndex, shortcutObject) + */ +async function forEachLocalShortcutRow(callback) { + let engines = await Services.search.getVisibleEngines(); + for (let i = 0; i < UrlbarUtils.LOCAL_SEARCH_MODES.length; i++) { + let shortcut = UrlbarUtils.LOCAL_SEARCH_MODES[i]; + let row = engines.length + i; + // These tests assume LOCAL_SEARCH_MODES are enabled, this can be removed + // when we enable QuickActions. We cant just enable the pref in browser.ini + // as this test calls clearUserPref. + if (shortcut.pref == "shortcuts.quickactions") { + continue; + } + await callback(row, shortcut); + } +} + +/** + * Prepends the `browser.urlbar.` branch to the given relative pref. + * + * @param {string} relativePref + * A pref name relative to the `browser.urlbar.`. + * @returns {string} + * The full pref name with `browser.urlbar.` prepended. + */ +function getUrlbarPrefName(relativePref) { + return `browser.urlbar.${relativePref}`; +} diff --git a/browser/components/preferences/tests/browser_moreFromMozilla.js b/browser/components/preferences/tests/browser_moreFromMozilla.js new file mode 100644 index 0000000000..0c9b6e2b88 --- /dev/null +++ b/browser/components/preferences/tests/browser_moreFromMozilla.js @@ -0,0 +1,380 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +let { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +async function clearPolicies() { + // Ensure no active policies are set + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); +} + +// The Relay promo is only shown if the default FxA instance is detected, and +// tests override it to a dummy address, so we need to make the dummy address +// appear like it's the default (using the actual default instance might cause a +// remote connection, crashing the test harness). +add_setup(mockDefaultFxAInstance); + +add_task(async function testDefaultUIWithoutTemplatePref() { + await clearPolicies(); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + ok(moreFromMozillaCategory, "The category exists"); + ok(!moreFromMozillaCategory.hidden, "The category is not hidden"); + + moreFromMozillaCategory.click(); + + let productCards = doc.querySelectorAll(".mozilla-product-item.simple"); + Assert.ok(productCards, "Default UI uses simple template"); + Assert.equal(productCards.length, 3, "3 product cards displayed"); + + const expectedUrl = "https://www.mozilla.org/firefox/browsers/mobile/"; + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, url => + url.startsWith(expectedUrl) + ); + let mobileLink = doc.getElementById("default-fxMobile"); + mobileLink.click(); + let openedTab = await tabOpened; + Assert.ok(gBrowser.selectedBrowser.documentURI.spec.startsWith(expectedUrl)); + + let searchParams = new URL(gBrowser.selectedBrowser.documentURI.spec) + .searchParams; + Assert.equal( + searchParams.get("utm_source"), + "about-prefs", + "expected utm_source sent" + ); + Assert.equal( + searchParams.get("utm_campaign"), + "morefrommozilla", + "utm_campaign set" + ); + Assert.equal( + searchParams.get("utm_medium"), + "firefox-desktop", + "utm_medium set" + ); + Assert.equal( + searchParams.get("utm_content"), + "default-global", + "default utm_content set" + ); + Assert.ok( + !searchParams.has("entrypoint_variation"), + "entrypoint_variation should not be set" + ); + Assert.ok( + !searchParams.has("entrypoint_experiment"), + "entrypoint_experiment should not be set" + ); + BrowserTestUtils.removeTab(openedTab); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testDefaulEmailClick() { + await clearPolicies(); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + moreFromMozillaCategory.click(); + + const expectedUrl = "https://www.mozilla.org/firefox/mobile/get-app/?v=mfm"; + let sendEmailLink = doc.getElementById("default-qr-code-send-email"); + + Assert.ok( + sendEmailLink.href.startsWith(expectedUrl), + `Expected URL ${sendEmailLink.href}` + ); + + let searchParams = new URL(sendEmailLink.href).searchParams; + Assert.equal(searchParams.get("v"), "mfm", "expected send email param set"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Test that we don't show moreFromMozilla pane when it's disabled. + */ +add_task(async function testwhenPrefDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", false]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + ok(moreFromMozillaCategory, "The category exists"); + ok(moreFromMozillaCategory.hidden, "The category is hidden"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_event_telemetry() { + Services.telemetry.clearEvents(); + Services.telemetry.setEventRecordingEnabled("aboutpreferences", true); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", true]], + }); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + + let clickedPromise = BrowserTestUtils.waitForEvent( + moreFromMozillaCategory, + "click" + ); + moreFromMozillaCategory.click(); + await clickedPromise; + + TelemetryTestUtils.assertEvents( + [["aboutpreferences", "show", "initial", "paneGeneral"]], + { category: "aboutpreferences", method: "show", object: "initial" }, + { clear: false } + ); + TelemetryTestUtils.assertEvents( + [["aboutpreferences", "show", "click", "paneMoreFromMozilla"]], + { category: "aboutpreferences", method: "show", object: "click" }, + { clear: false } + ); + TelemetryTestUtils.assertNumberOfEvents(2); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_simple_template() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let moreFromMozillaCategory = doc.getElementById( + "category-more-from-mozilla" + ); + + moreFromMozillaCategory.click(); + + let productCards = doc.querySelectorAll(".mozilla-product-item"); + Assert.ok(productCards, "The product cards from simple template found"); + Assert.equal(productCards.length, 3, "3 product cards displayed"); + + let qrCodeButtons = doc.querySelectorAll('.qr-code-box[hidden="false"]'); + Assert.equal(qrCodeButtons.length, 1, "1 qr-code box displayed"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_clickBtnVPN() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll(".mozilla-product-item.simple"); + Assert.ok(productCards, "Simple template loaded"); + + const expectedUrl = "https://www.mozilla.org/products/vpn/"; + let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, url => + url.startsWith(expectedUrl) + ); + + let vpnButton = doc.getElementById("simple-mozillaVPN"); + vpnButton.click(); + + let openedTab = await tabOpened; + Assert.ok(gBrowser.selectedBrowser.documentURI.spec.startsWith(expectedUrl)); + + let searchParams = new URL(gBrowser.selectedBrowser.documentURI.spec) + .searchParams; + Assert.equal( + searchParams.get("utm_source"), + "about-prefs", + "expected utm_source sent" + ); + Assert.equal( + searchParams.get("utm_campaign"), + "morefrommozilla", + "utm_campaign set" + ); + Assert.equal( + searchParams.get("utm_medium"), + "firefox-desktop", + "utm_medium set" + ); + Assert.equal( + searchParams.get("utm_content"), + "fxvt-113-a-global", + "utm_content set" + ); + Assert.equal( + searchParams.get("entrypoint_experiment"), + "morefrommozilla-experiment-1846", + "entrypoint_experiment set" + ); + Assert.equal( + searchParams.get("entrypoint_variation"), + "treatment-simple", + "entrypoint_variation set" + ); + BrowserTestUtils.removeTab(openedTab); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_aboutpreferences_clickBtnMobile() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll("vbox.simple"); + Assert.ok(productCards, "Simple template loaded"); + + const expectedUrl = "https://www.mozilla.org/firefox/browsers/mobile/"; + + let mobileUrl = new URL(doc.getElementById("simple-fxMobile").href); + + Assert.ok(mobileUrl.href.startsWith(expectedUrl)); + + let searchParams = mobileUrl.searchParams; + Assert.equal( + searchParams.get("utm_source"), + "about-prefs", + "expected utm_source sent" + ); + Assert.equal( + searchParams.get("utm_campaign"), + "morefrommozilla", + "utm_campaign set" + ); + Assert.equal( + searchParams.get("utm_medium"), + "firefox-desktop", + "utm_medium set" + ); + Assert.equal( + searchParams.get("utm_content"), + "fxvt-113-a-global", + "default-global", + "utm_content set" + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_aboutpreferences_search() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", true]], + }); + + await openPreferencesViaOpenPreferencesAPI(null, { + leaveOpen: true, + }); + + await runSearchInput("Relay"); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll(".mozilla-product-item"); + Assert.equal(productCards.length, 3, "All products in the group are found"); + let [mobile, vpn, relay] = productCards; + Assert.ok(BrowserTestUtils.is_hidden(mobile), "Mobile hidden"); + Assert.ok(BrowserTestUtils.is_hidden(vpn), "VPN hidden"); + Assert.ok(BrowserTestUtils.is_visible(relay), "Relay shown"); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_aboutpreferences_clickBtnRelay() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla", true]], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let expectedUrl = new URL("https://relay.firefox.com"); + expectedUrl.searchParams.set("utm_source", "about-prefs"); + expectedUrl.searchParams.set("utm_campaign", "morefrommozilla"); + expectedUrl.searchParams.set("utm_medium", "firefox-desktop"); + expectedUrl.searchParams.set("utm_content", "fxvt-113-a-global"); + expectedUrl.searchParams.set( + "entrypoint_experiment", + "morefrommozilla-experiment-1846" + ); + expectedUrl.searchParams.set("entrypoint_variation", "treatment-simple"); + + let tabOpened = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedUrl.toString(), + gBrowser, + channel => { + Assert.equal( + channel.originalURI.spec, + expectedUrl.toString(), + "URL matched" + ); + return true; + } + ); + doc.getElementById("simple-firefoxRelay").click(); + + await tabOpened; + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_moreFromMozilla_locales.js b/browser/components/preferences/tests/browser_moreFromMozilla_locales.js new file mode 100644 index 0000000000..404e22b3ea --- /dev/null +++ b/browser/components/preferences/tests/browser_moreFromMozilla_locales.js @@ -0,0 +1,331 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +let { Region } = ChromeUtils.importESModule( + "resource://gre/modules/Region.sys.mjs" +); + +const initialHomeRegion = Region._home; +const initialCurrentRegion = Region._current; + +// Helper to run tests for specific regions +async function setupRegions(home, current) { + Region._setHomeRegion(home || ""); + Region._setCurrentRegion(current || ""); +} + +function setLocale(language) { + Services.locale.availableLocales = [language]; + Services.locale.requestedLocales = [language]; +} + +async function clearPolicies() { + // Ensure no active policies are set + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); +} + +async function getPromoCards() { + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let vpnPromoCard = doc.getElementById("mozilla-vpn"); + let mobileCard = doc.getElementById("firefox-mobile"); + let relayPromoCard = doc.getElementById("firefox-relay"); + + return { + vpnPromoCard, + mobileCard, + relayPromoCard, + }; +} + +let mockFxA, unmockFxA; + +// The Relay promo is only shown if the default FxA instance is detected, and +// tests override it to a dummy address, so we need to make the dummy address +// appear like it's the default (using the actual default instance might cause a +// remote connection, crashing the test harness). +add_setup(async function () { + let { mock, unmock } = await mockDefaultFxAInstance(); + mockFxA = mock; + unmockFxA = unmock; +}); + +add_task(async function test_VPN_promo_enabled() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.vpn_promo.enabled", true], + ], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(vpnPromoCard, "The VPN promo is visible"); + ok(mobileCard, "The Mobile promo is visible"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_disabled() { + await clearPolicies(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", false]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + Services.prefs.clearUserPref("browser.vpn_promo.enabled"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_disallowed_home_region() { + await clearPolicies(); + const disallowedRegion = "SY"; + + setupRegions(disallowedRegion); + + // Promo should not show in disallowed regions even when vpn_promo pref is enabled + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", true]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_illegal_home_region() { + await clearPolicies(); + const illegalRegion = "CN"; + + setupRegions(illegalRegion); + + // Promo should not show in illegal regions even if the list of disallowed regions is somehow altered (though changing this preference is blocked) + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.disallowedRegions", "SY, CU"]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_disallowed_current_region() { + await clearPolicies(); + const allowedRegion = "US"; + const disallowedRegion = "SY"; + + setupRegions(allowedRegion, disallowedRegion); + + // Promo should not show in disallowed regions even when vpn_promo pref is enabled + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.enabled", true]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_VPN_promo_in_illegal_current_region() { + await clearPolicies(); + const allowedRegion = "US"; + const illegalRegion = "CN"; + + setupRegions(allowedRegion, illegalRegion); + + // Promo should not show in illegal regions even if the list of disallowed regions is somehow altered (though changing this preference is blocked) + await SpecialPowers.pushPrefEnv({ + set: [["browser.vpn_promo.disallowedRegions", "SY, CU"]], + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_aboutpreferences_partnerCNRepack() { + let defaultBranch = Services.prefs.getDefaultBranch(null); + defaultBranch.setCharPref("distribution.id", "MozillaOnline"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.preferences.moreFromMozilla", true], + ["browser.preferences.moreFromMozilla.template", "simple"], + ], + }); + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let tab = gBrowser.selectedTab; + + let productCards = doc.querySelectorAll("vbox.simple"); + Assert.ok(productCards, "Simple template loaded"); + + const expectedUrl = "https://www.firefox.com.cn/browsers/mobile/"; + + let link = doc.getElementById("simple-fxMobile"); + Assert.ok(link.getAttribute("href").startsWith(expectedUrl)); + + defaultBranch.setCharPref("distribution.id", ""); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_send_to_device_email_link_for_supported_locale() { + // Email is supported for Brazilian Portuguese + const supportedLocale = "pt-BR"; + const initialLocale = Services.locale.appLocaleAsBCP47; + setLocale(supportedLocale); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla.template", "simple"]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let emailLink = doc.getElementById("simple-qr-code-send-email"); + + ok(!BrowserTestUtils.is_hidden(emailLink), "Email link should be visible"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + setLocale(initialLocale); // revert changes to language +}); + +add_task( + async function test_send_to_device_email_link_for_unsupported_locale() { + // Email is not supported for Afrikaans + const unsupportedLocale = "af"; + const initialLocale = Services.locale.appLocaleAsBCP47; + setLocale(unsupportedLocale); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.moreFromMozilla.template", "simple"]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneMoreFromMozilla", { + leaveOpen: true, + }); + + let doc = gBrowser.contentDocument; + let emailLink = doc.getElementById("simple-qr-code-send-email"); + + ok(BrowserTestUtils.is_hidden(emailLink), "Email link should be hidden"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + setLocale(initialLocale); // revert changes to language + } +); + +add_task( + async function test_VPN_promo_in_unsupported_current_region_with_supported_home_region() { + await clearPolicies(); + const supportedRegion = "US"; + const unsupportedRegion = "LY"; + + setupRegions(supportedRegion, unsupportedRegion); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(vpnPromoCard, "The VPN promo is visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task( + async function test_VPN_promo_in_supported_current_region_with_unsupported_home_region() { + await clearPolicies(); + const supportedRegion = "US"; + const unsupportedRegion = "LY"; + + setupRegions(unsupportedRegion, supportedRegion); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + + ok(vpnPromoCard, "The VPN promo is visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task(async function test_VPN_promo_with_active_enterprise_policy() { + // set up an arbitrary enterprise policy + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + EnableTrackingProtection: { + Value: true, + }, + }, + }); + + let { vpnPromoCard, mobileCard } = await getPromoCards(); + ok(!vpnPromoCard, "The VPN promo is not visible"); + ok(mobileCard, "The Mobile promo is visible"); + + setupRegions(initialHomeRegion, initialCurrentRegion); // revert changes to regions + await clearPolicies(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_relay_promo_with_supported_fxa_server() { + await clearPolicies(); + + let { relayPromoCard } = await getPromoCards(); + ok(relayPromoCard, "The Relay promo is visible"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function test_relay_promo_with_unsupported_fxa_server() { + await clearPolicies(); + // Set the default pref value to something other than the current value so it + // will appear to be user-set and treated as invalid (actually setting the + // pref would cause a remote connection and crash the test harness) + unmockFxA(); + + let { relayPromoCard } = await getPromoCards(); + ok(!relayPromoCard, "The Relay promo is not visible"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + mockFxA(); +}); diff --git a/browser/components/preferences/tests/browser_newtab_menu.js b/browser/components/preferences/tests/browser_newtab_menu.js new file mode 100644 index 0000000000..774c9dd756 --- /dev/null +++ b/browser/components/preferences/tests/browser_newtab_menu.js @@ -0,0 +1,38 @@ +add_task(async function newtabPreloaded() { + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + const { contentDocument: doc, contentWindow } = gBrowser; + function dispatchMenuItemCommand(menuItem) { + const cmdEvent = doc.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent( + "command", + true, + true, + contentWindow, + 0, + false, + false, + false, + false, + 0, + null, + 0 + ); + menuItem.dispatchEvent(cmdEvent); + } + + const menuHome = doc.querySelector(`#newTabMode menuitem[value="0"]`); + const menuBlank = doc.querySelector(`#newTabMode menuitem[value="1"]`); + ok(menuHome.selected, "The first item, Home (default), is selected."); + ok(NewTabPagePreloading.enabled, "Default Home allows preloading."); + + dispatchMenuItemCommand(menuBlank); + ok(menuBlank.selected, "The second item, Blank, is selected."); + ok(!NewTabPagePreloading.enabled, "Non-Home prevents preloading."); + + dispatchMenuItemCommand(menuHome); + ok(menuHome.selected, "The first item, Home, is selected again."); + ok(NewTabPagePreloading.enabled, "Default Home allows preloading again."); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_notifications_do_not_disturb.js b/browser/components/preferences/tests/browser_notifications_do_not_disturb.js new file mode 100644 index 0000000000..afc31b9041 --- /dev/null +++ b/browser/components/preferences/tests/browser_notifications_do_not_disturb.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +registerCleanupFunction(function () { + while (gBrowser.tabs[1]) { + gBrowser.removeTab(gBrowser.tabs[1]); + } +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected"); + + let doc = gBrowser.contentDocument; + let notificationsDoNotDisturbBox = doc.getElementById( + "notificationsDoNotDisturbBox" + ); + if (notificationsDoNotDisturbBox.hidden) { + todo(false, "Do not disturb is not available on this platform"); + return; + } + + let alertService; + try { + alertService = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIAlertsDoNotDisturb); + } catch (ex) { + ok(true, "Do not disturb is not available on this platform: " + ex.message); + return; + } + + let checkbox = doc.getElementById("notificationsDoNotDisturb"); + ok(!checkbox.checked, "Checkbox should not be checked by default"); + ok( + !alertService.manualDoNotDisturb, + "Do not disturb should be off by default" + ); + + let checkboxChanged = BrowserTestUtils.waitForEvent(checkbox, "command"); + checkbox.click(); + await checkboxChanged; + ok( + alertService.manualDoNotDisturb, + "Do not disturb should be enabled when checked" + ); + + checkboxChanged = BrowserTestUtils.waitForEvent(checkbox, "command"); + checkbox.click(); + await checkboxChanged; + ok( + !alertService.manualDoNotDisturb, + "Do not disturb should be disabled when unchecked" + ); +}); diff --git a/browser/components/preferences/tests/browser_open_download_preferences.js b/browser/components/preferences/tests/browser_open_download_preferences.js new file mode 100644 index 0000000000..794d2ebb05 --- /dev/null +++ b/browser/components/preferences/tests/browser_open_download_preferences.js @@ -0,0 +1,288 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HandlerServiceTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/HandlerServiceTestUtils.sys.mjs" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +async function selectPdfCategoryItem() { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + info("Preferences page opened on the general pane."); + + await gBrowser.selectedBrowser.contentWindow.promiseLoadHandlersList; + info("Apps list loaded."); + + let win = gBrowser.selectedBrowser.contentWindow; + let container = win.document.getElementById("handlersView"); + let pdfCategory = container.querySelector( + "richlistitem[type='application/pdf']" + ); + + pdfCategory.closest("richlistbox").selectItem(pdfCategory); + Assert.ok(pdfCategory.selected, "Should be able to select our item."); + + return pdfCategory; +} + +async function selectItemInPopup(item, list) { + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + // Synthesizing the mouse on the .actionsMenu menulist somehow just selects + // the top row. Probably something to do with the multiple layers of anon + // content - workaround by using the `.open` setter instead. + list.open = true; + await popupShown; + let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + + item.click(); + popup.hidePopup(); + await popupHidden; + return item; +} + +function downloadHadFinished(publicList) { + return new Promise(resolve => { + publicList.addView({ + onDownloadChanged(download) { + if (download.succeeded || download.error) { + publicList.removeView(this); + resolve(download); + } + }, + }); + }); +} + +async function removeTheFile(download) { + Assert.ok( + await IOUtils.exists(download.target.path), + "The file should have been downloaded." + ); + + try { + info("removing " + download.target.path); + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (ex) { + info("The file " + download.target.path + " is not removed, " + ex); + } +} + +add_task(async function alwaysAskPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let alwaysAskItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.alwaysAsk}']` + ); + + await selectItemInPopup(alwaysAskItem, list); + Assert.equal( + list.selectedItem, + alwaysAskItem, + "Should have selected 'always ask' for pdf" + ); + let alwaysAskBeforeHandling = HandlerServiceTestUtils.getHandlerInfo( + pdfCategory.getAttribute("type") + ).alwaysAskBeforeHandling; + Assert.ok( + alwaysAskBeforeHandling, + "Should have turned on 'always asking before handling'" + ); + + let domWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + let domWindow = await domWindowPromise; + let dialog = domWindow.document.querySelector("#unknownContentType"); + let button = dialog.getButton("cancel"); + + await TestUtils.waitForCondition( + () => !button.disabled, + "Wait for Cancel button to get enabled" + ); + Assert.ok(dialog, "Dialog should be shown"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function handleInternallyPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let handleInternallyItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']` + ); + + await selectItemInPopup(handleInternallyItem, list); + Assert.equal( + list.selectedItem, + handleInternallyItem, + "Should have selected 'handle internally' for pdf" + ); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + await ContentTask.spawn(loadingTab.linkedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.readyState == "complete" + ); + Assert.ok( + content.document.querySelector("div#viewer"), + "document content has viewer UI" + ); + }); + + BrowserTestUtils.removeTab(loadingTab); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function saveToDiskPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let saveToDiskItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.saveToDisk}']` + ); + + await selectItemInPopup(saveToDiskItem, list); + Assert.equal( + list.selectedItem, + saveToDiskItem, + "Should have selected 'save to disk' for pdf" + ); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + + let downloadFinishedPromise = downloadHadFinished(publicList); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + let download = await downloadFinishedPromise; + BrowserTestUtils.removeTab(loadingTab); + + await removeTheFile(download); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function useSystemDefaultPreferenceWorks() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.useDownloadDir", true], + ], + }); + + let pdfCategory = await selectPdfCategoryItem(); + let list = pdfCategory.querySelector(".actionsMenu"); + + let useSystemDefaultItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.useSystemDefault}']` + ); + + // Whether there's a "use default" item depends on the OS, there might not be a system default viewer. + if (!useSystemDefaultItem) { + info( + "No 'Use default' item, so no testing for setting 'use system default' preference" + ); + gBrowser.removeCurrentTab(); + return; + } + + await selectItemInPopup(useSystemDefaultItem, list); + Assert.equal( + list.selectedItem, + useSystemDefaultItem, + "Should have selected 'use system default' for pdf" + ); + + let oldLaunchFile = DownloadIntegration.launchFile; + + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + + let downloadFinishedPromise = downloadHadFinished(publicList); + + let loadingTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "empty_pdf_file.pdf", + waitForLoad: false, + waitForStateStop: true, + }); + + info("Downloading had finished"); + let download = await downloadFinishedPromise; + + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + + DownloadIntegration.launchFile = oldLaunchFile; + + await removeTheFile(download); + + BrowserTestUtils.removeTab(loadingTab); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_open_migration_wizard.js b/browser/components/preferences/tests/browser_open_migration_wizard.js new file mode 100644 index 0000000000..c2c18b35ef --- /dev/null +++ b/browser/components/preferences/tests/browser_open_migration_wizard.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the "Import Data" button in the "Import Browser Data" section of + * the General pane of about:preferences launches the Migration Wizard. + */ +add_task(async function test_open_migration_wizard() { + const BUTTON_ID = "data-migration"; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#general" }, + async function (browser) { + let button = browser.contentDocument.getElementById(BUTTON_ID); + + // First, we'll test the legacy Migration Wizard. + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.content-modal.enabled", false]], + }); + + let migrationWizardWindow = BrowserTestUtils.domWindowOpenedAndLoaded( + null, + win => { + let type = win.document.documentElement.getAttribute("windowtype"); + if (type == "Browser:MigrationWizard") { + Assert.ok(true, "Saw legacy Migration Wizard window open."); + return true; + } + + return false; + } + ); + + button.click(); + let win = await migrationWizardWindow; + await BrowserTestUtils.closeWindow(win); + + // Next, we'll test the new Migration Wizard. + await SpecialPowers.pushPrefEnv({ + set: [["browser.migrate.content-modal.enabled", true]], + }); + + let wizardReady = BrowserTestUtils.waitForEvent( + browser.contentWindow, + "MigrationWizard:Ready" + ); + button.click(); + await wizardReady; + Assert.ok(true, "Saw the new Migration Wizard dialog open."); + } + ); +}); diff --git a/browser/components/preferences/tests/browser_password_management.js b/browser/components/preferences/tests/browser_password_management.js new file mode 100644 index 0000000000..d84c88dc08 --- /dev/null +++ b/browser/components/preferences/tests/browser_password_management.js @@ -0,0 +1,43 @@ +"use strict"; + +const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +var passwordsDialog; + +add_task(async function test_openPasswordManagement() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let tabOpenPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:logins"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + + let savePasswordCheckBox = doc.getElementById("savePasswords"); + Assert.ok( + !savePasswordCheckBox.checked, + "Save Password CheckBox should be unchecked by default" + ); + + let showPasswordsButton = doc.getElementById("showPasswords"); + showPasswordsButton.click(); + }); + + let tab = await tabOpenPromise; + ok(tab, "Tab opened"); + + // check telemetry events while we are in here + await LoginTestUtils.telemetry.waitForEventCount(1); + TelemetryTestUtils.assertEvents( + [["pwmgr", "open_management", "preferences"]], + { category: "pwmgr", method: "open_management" }, + { clear: true, process: "content" } + ); + + BrowserTestUtils.removeTab(tab); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_pdf_disabled.js b/browser/components/preferences/tests/browser_pdf_disabled.js new file mode 100644 index 0000000000..5b814b39e9 --- /dev/null +++ b/browser/components/preferences/tests/browser_pdf_disabled.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks that pdf always appears in the applications list even +// both a customized handler doesn't exist and when the internal viewer is +// not enabled. +add_task(async function pdfIsAlwaysPresent() { + // Try again with the pdf viewer enabled and disabled. + for (let test of ["enabled", "disabled"]) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.disabled", test == "disabled"]], + }); + + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + + let win = gBrowser.selectedBrowser.contentWindow; + + let container = win.document.getElementById("handlersView"); + + // First, find the PDF item. + let pdfItem = container.querySelector( + "richlistitem[type='application/pdf']" + ); + Assert.ok(pdfItem, "pdfItem is present in handlersView when " + test); + if (pdfItem) { + pdfItem.scrollIntoView({ block: "center" }); + pdfItem.closest("richlistbox").selectItem(pdfItem); + + // Open its menu + let list = pdfItem.querySelector(".actionsMenu"); + let popup = list.menupopup; + let popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter(list, {}, win); + await popupShown; + + let handleInternallyItem = list.querySelector( + `menuitem[action='${Ci.nsIHandlerInfo.handleInternally}']` + ); + + is( + test == "enabled", + !!handleInternallyItem, + "handle internally is present when " + test + ); + } + + gBrowser.removeCurrentTab(); + } +}); diff --git a/browser/components/preferences/tests/browser_performance.js b/browser/components/preferences/tests/browser_performance.js new file mode 100644 index 0000000000..bd7ff70c51 --- /dev/null +++ b/browser/components/preferences/tests/browser_performance.js @@ -0,0 +1,300 @@ +const DEFAULT_HW_ACCEL_PREF = Services.prefs + .getDefaultBranch(null) + .getBoolPref("layers.acceleration.disabled"); +const DEFAULT_PROCESS_COUNT = Services.prefs + .getDefaultBranch(null) + .getIntPref("dom.ipc.processCount"); + +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF], + ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT], + ["browser.preferences.defaultPerformanceSettings.enabled", true], + ], + }); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + + useRecommendedPerformanceSettings.click(); + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + false, + "pref value should be false after clicking on checkbox" + ); + ok( + !useRecommendedPerformanceSettings.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + let allowHWAccelPref = Services.prefs.getBoolPref( + "layers.acceleration.disabled" + ); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value before clicking on checkbox" + ); + is( + allowHWAccel.checked, + !DEFAULT_HW_ACCEL_PREF, + "checkbox should show the invert of the default value" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + false, + "process count control should be enabled" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "default pref value should be default value" + ); + is( + contentProcessCount.selectedItem.value, + "" + DEFAULT_PROCESS_COUNT, + "selected item should be the default one" + ); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + !DEFAULT_HW_ACCEL_PREF, + "pref value should be opposite of the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + contentProcessCount.value = 7; + contentProcessCount.doCommand(); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + 7, + "pref value should be 7" + ); + is(contentProcessCount.selectedItem.value, "7", "selected item should be 7"); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + contentProcessCount.value = DEFAULT_PROCESS_COUNT; + contentProcessCount.doCommand(); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "pref value should be default value" + ); + is( + contentProcessCount.selectedItem.value, + "" + DEFAULT_PROCESS_COUNT, + "selected item should be default one" + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be still shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + let allowHWAccel = doc.querySelector("#allowHWAccel"); + let contentProcessCount = doc.querySelector("#contentProcessCount"); + let performanceSettings = doc.querySelector("#performanceSettings"); + + useRecommendedPerformanceSettings.click(); + allowHWAccel.click(); + contentProcessCount.value = 7; + contentProcessCount.doCommand(); + useRecommendedPerformanceSettings.click(); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + is( + performanceSettings.hidden, + true, + "performance settings section should be still shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let performanceSettings = doc.querySelector("#performanceSettings"); + + is( + performanceSettings.hidden, + true, + "performance settings section should not be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + false + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + Services.prefs.setIntPref("dom.ipc.processCount", 7); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + 7, + "pref value should be 7" + ); + is(contentProcessCount.selectedItem.value, "7", "selected item should be 7"); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + Services.prefs.setBoolPref("layers.acceleration.disabled", true); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + is( + Services.prefs.getBoolPref("layers.acceleration.disabled"), + true, + "pref value is false" + ); + ok(!allowHWAccel.checked, "checkbox should not be checked"); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_performance_content_process_limit.js b/browser/components/preferences/tests/browser_performance_content_process_limit.js new file mode 100644 index 0000000000..7ac5c354bd --- /dev/null +++ b/browser/components/preferences/tests/browser_performance_content_process_limit.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.defaultPerformanceSettings.enabled", false]], + }); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let limitContentProcess = doc.querySelector("#limitContentProcess"); + is( + limitContentProcess.hidden, + Services.appinfo.fissionAutostart, + "Limit Content Process should be hidden if fission is enabled and shown if it is not." + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.hidden, + Services.appinfo.fissionAutostart, + "Limit Content Count should be hidden if fission is enabled and shown if it is not." + ); + + let contentProcessCountEnabledDescription = doc.querySelector( + "#contentProcessCountEnabledDescription" + ); + is( + contentProcessCountEnabledDescription.hidden, + Services.appinfo.fissionAutostart, + "Limit Content Process Enabled Description should be hidden if fission is enabled and shown if it is not." + ); + + let contentProcessCountDisabledDescription = doc.querySelector( + "#contentProcessCountDisabledDescription" + ); + is( + contentProcessCountDisabledDescription.hidden, + Services.appinfo.fissionAutostart || + Services.appinfo.browserTabsRemoteAutostart, + "Limit Content Process Disabled Description should be shown if e10s is disabled, and hidden otherwise." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_performance_e10srollout.js b/browser/components/preferences/tests/browser_performance_e10srollout.js new file mode 100644 index 0000000000..1c2c57b6e7 --- /dev/null +++ b/browser/components/preferences/tests/browser_performance_e10srollout.js @@ -0,0 +1,164 @@ +const DEFAULT_HW_ACCEL_PREF = Services.prefs + .getDefaultBranch(null) + .getBoolPref("layers.acceleration.disabled"); +const DEFAULT_PROCESS_COUNT = Services.prefs + .getDefaultBranch(null) + .getIntPref("dom.ipc.processCount"); + +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF], + ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT], + ["browser.preferences.defaultPerformanceSettings.enabled", true], + ], + }); +}); + +add_task(async function testPrefsAreDefault() { + Services.prefs.setIntPref("dom.ipc.processCount", DEFAULT_PROCESS_COUNT); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + + useRecommendedPerformanceSettings.click(); + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + false, + "pref value should be false after clicking on checkbox" + ); + ok( + !useRecommendedPerformanceSettings.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + false, + "process count control should be enabled" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "default pref should be default value" + ); + is( + contentProcessCount.selectedItem.value, + "" + DEFAULT_PROCESS_COUNT, + "selected item should be the default one" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + Services.prefs.clearUserPref("dom.ipc.processCount"); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); +}); + +add_task(async function testPrefsSetByUser() { + const kNewCount = DEFAULT_PROCESS_COUNT - 2; + + Services.prefs.setIntPref("dom.ipc.processCount", kNewCount); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + false + ); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + false, + "process count control should be enabled" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + kNewCount, + "process count should be the set value" + ); + is( + contentProcessCount.selectedItem.value, + "" + kNewCount, + "selected item should be the set one" + ); + + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + useRecommendedPerformanceSettings.click(); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true after clicking on checkbox" + ); + is( + Services.prefs.getIntPref("dom.ipc.processCount"), + DEFAULT_PROCESS_COUNT, + "process count should be default value" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + Services.prefs.clearUserPref("dom.ipc.processCount"); + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); +}); diff --git a/browser/components/preferences/tests/browser_performance_non_e10s.js b/browser/components/preferences/tests/browser_performance_non_e10s.js new file mode 100644 index 0000000000..169afcaaa6 --- /dev/null +++ b/browser/components/preferences/tests/browser_performance_non_e10s.js @@ -0,0 +1,210 @@ +const DEFAULT_HW_ACCEL_PREF = Services.prefs + .getDefaultBranch(null) + .getBoolPref("layers.acceleration.disabled"); +const DEFAULT_PROCESS_COUNT = Services.prefs + .getDefaultBranch(null) + .getIntPref("dom.ipc.processCount"); + +add_task(async function () { + // We must temporarily disable `Once` StaticPrefs check for the duration of + // this test (see bug 1556131). We must do so in a separate operation as + // pushPrefEnv doesn't set the preferences in the order one could expect. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["layers.acceleration.disabled", DEFAULT_HW_ACCEL_PREF], + ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT], + ["browser.preferences.defaultPerformanceSettings.enabled", true], + ], + }); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let useRecommendedPerformanceSettings = doc.querySelector( + "#useRecommendedPerformanceSettings" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + true, + "pref value should be true before clicking on checkbox" + ); + ok( + useRecommendedPerformanceSettings.checked, + "checkbox should be checked before clicking on checkbox" + ); + + useRecommendedPerformanceSettings.click(); + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section is shown" + ); + + is( + Services.prefs.getBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled" + ), + false, + "pref value should be false after clicking on checkbox" + ); + ok( + !useRecommendedPerformanceSettings.checked, + "checkbox should not be checked after clicking on checkbox" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + let allowHWAccelPref = Services.prefs.getBoolPref( + "layers.acceleration.disabled" + ); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value before clicking on checkbox" + ); + is( + allowHWAccel.checked, + !DEFAULT_HW_ACCEL_PREF, + "checkbox should show the invert of the default value" + ); + + let contentProcessCount = doc.querySelector("#contentProcessCount"); + is( + contentProcessCount.disabled, + true, + "process count control should be disabled" + ); + + let contentProcessCountEnabledDescription = doc.querySelector( + "#contentProcessCountEnabledDescription" + ); + is( + contentProcessCountEnabledDescription.hidden, + true, + "process count enabled description should be hidden" + ); + + let contentProcessCountDisabledDescription = doc.querySelector( + "#contentProcessCountDisabledDescription" + ); + is( + contentProcessCountDisabledDescription.hidden, + false, + "process count enabled description should be shown" + ); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + !DEFAULT_HW_ACCEL_PREF, + "pref value should be opposite of the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + allowHWAccel.click(); + allowHWAccelPref = Services.prefs.getBoolPref("layers.acceleration.disabled"); + is( + allowHWAccelPref, + DEFAULT_HW_ACCEL_PREF, + "pref value should be the default value after clicking on checkbox" + ); + is( + allowHWAccel.checked, + !allowHWAccelPref, + "checkbox should show the invert of the current value" + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be still shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + let performanceSettings = doc.querySelector("#performanceSettings"); + + is( + performanceSettings.hidden, + true, + "performance settings section should not be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + false + ); + + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + Services.prefs.setBoolPref("layers.acceleration.disabled", true); + + let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + is(prefs.selectedPane, "paneGeneral", "General pane was selected"); + + let doc = gBrowser.contentDocument; + + let performanceSettings = doc.querySelector("#performanceSettings"); + is( + performanceSettings.hidden, + false, + "performance settings section should be shown" + ); + + let allowHWAccel = doc.querySelector("#allowHWAccel"); + is( + Services.prefs.getBoolPref("layers.acceleration.disabled"), + true, + "pref value is false" + ); + ok(!allowHWAccel.checked, "checkbox should not be checked"); + + Services.prefs.setBoolPref( + "browser.preferences.defaultPerformanceSettings.enabled", + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js b/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js new file mode 100644 index 0000000000..829f897b72 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_checkPermissionsWereAdded.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +const _checkAndOpenCookiesDialog = async doc => { + let cookieExceptionsButton = doc.getElementById("cookieExceptions"); + ok(cookieExceptionsButton, "cookieExceptionsButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + cookieExceptionsButton.click(); + let dialog = await dialogPromise; + return dialog; +}; + +const _checkCookiesDialog = (dialog, buttonIds) => { + ok(dialog, "dialog loaded"); + let urlLabel = dialog.document.getElementById("urlLabel"); + ok(!urlLabel.hidden, "urlLabel should be visible"); + let url = dialog.document.getElementById("url"); + ok(!url.hidden, "url should be visible"); + for (let buttonId of buttonIds) { + let buttonDialog = dialog.document.getElementById(buttonId); + ok(buttonDialog, "blockButtonDialog found"); + is( + buttonDialog.hasAttribute("disabled"), + true, + "If the user hasn't added an url the button shouldn't be clickable" + ); + } + return dialog; +}; + +const _addWebsiteAddressToPermissionBox = ( + websiteAddress, + dialog, + buttonId +) => { + let url = dialog.document.getElementById("url"); + let buttonDialog = dialog.document.getElementById(buttonId); + url.value = websiteAddress; + url.dispatchEvent(new Event("input", { bubbles: true })); + is( + buttonDialog.hasAttribute("disabled"), + false, + "When the user add an url the button should be clickable" + ); + buttonDialog.click(); + let permissionsBox = dialog.document.getElementById("permissionsBox"); + let children = permissionsBox.getElementsByAttribute("origin", "*"); + is(!children.length, false, "Website added in url should be in the list"); +}; + +const _checkIfPermissionsWereAdded = (dialog, expectedResult) => { + let permissionsBox = dialog.document.getElementById("permissionsBox"); + for (let website of expectedResult) { + let elements = permissionsBox.getElementsByAttribute("origin", website); + is(elements.length, 1, "It should find only one coincidence"); + } +}; + +const _removesAllSitesInPermissionBox = dialog => { + let removeAllWebsitesButton = dialog.document.getElementById( + "removeAllPermissions" + ); + ok(removeAllWebsitesButton, "removeAllWebsitesButton found"); + is( + removeAllWebsitesButton.hasAttribute("disabled"), + false, + "There should be websites in the list" + ); + removeAllWebsitesButton.click(); +}; + +add_task(async function checkCookiePermissions() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let buttonIds = ["btnBlock", "btnCookieSession", "btnAllow"]; + + let dialog = await _checkAndOpenCookiesDialog(doc); + _checkCookiesDialog(dialog, buttonIds); + + let tests = [ + { + inputWebsite: "google.com", + expectedResult: ["http://google.com", "https://google.com"], + }, + { + inputWebsite: "https://google.com", + expectedResult: ["https://google.com"], + }, + { + inputWebsite: "http://", + expectedResult: ["http://http", "https://http"], + }, + { + inputWebsite: "s3.eu-central-1.amazonaws.com", + expectedResult: [ + "http://s3.eu-central-1.amazonaws.com", + "https://s3.eu-central-1.amazonaws.com", + ], + }, + { + inputWebsite: "file://", + expectedResult: ["file:///"], + }, + { + inputWebsite: "about:config", + expectedResult: ["about:config"], + }, + ]; + + for (let buttonId of buttonIds) { + for (let test of tests) { + _addWebsiteAddressToPermissionBox(test.inputWebsite, dialog, buttonId); + _checkIfPermissionsWereAdded(dialog, test.expectedResult); + _removesAllSitesInPermissionBox(dialog); + } + } + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_permissions_dialog.js b/browser/components/preferences/tests/browser_permissions_dialog.js new file mode 100644 index 0000000000..3a5e0f95c2 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_dialog.js @@ -0,0 +1,642 @@ +"use strict"; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/sitePermissions.xhtml"; +const URL = "http://www.example.com"; +const URI = Services.io.newURI(URL); +var sitePermissionsDialog; +let settingsButtonMap = { + "desktop-notification": "notificationSettingsButton", + speaker: "speakerSettingsButton", +}; + +function checkMenulistPermissionItem(origin, state) { + let doc = sitePermissionsDialog.document; + + let label = doc.getElementsByTagName("label")[3]; + Assert.equal(label.value, origin); + + let menulist = doc.getElementsByTagName("menulist")[0]; + Assert.equal(menulist.value, state); +} + +async function openPermissionsDialog(permissionType) { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [settingsButtonMap[permissionType]], + function (settingsButtonId) { + let doc = content.document; + let settingsButton = doc.getElementById(settingsButtonId); + settingsButton.click(); + } + ); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +add_task(async function openSitePermissionsDialog() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openPermissionsDialog("desktop-notification"); +}); + +add_task(async function addPermission() { + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + // First item in the richlistbox contains column headers. + Assert.equal( + richlistbox.itemCount, + 0, + "Number of permission items is 0 initially" + ); + + // Add notification permission for a website. + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Observe the added permission changes in the dialog UI. + Assert.equal(richlistbox.itemCount, 1); + checkMenulistPermissionItem(URL, Services.perms.ALLOW_ACTION); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function addPermissionPrivateBrowsing() { + let privateBrowsingPrincipal = + Services.scriptSecurityManager.createContentPrincipal(URI, { + privateBrowsingId: 1, + }); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + Assert.equal( + richlistbox.itemCount, + 0, + "Number of permission items is 0 initially" + ); + + // Add a session permission for private browsing. + PermissionTestUtils.add( + privateBrowsingPrincipal, + "desktop-notification", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_SESSION + ); + + // The permission should not show in the dialog UI. + Assert.equal(richlistbox.itemCount, 0); + + PermissionTestUtils.remove(privateBrowsingPrincipal, "desktop-notification"); + + // Add a permanent permission for private browsing + // The permission manager will store it as EXPIRE_SESSION + PermissionTestUtils.add( + privateBrowsingPrincipal, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // The permission should not show in the dialog UI. + Assert.equal(richlistbox.itemCount, 0); + + PermissionTestUtils.remove(privateBrowsingPrincipal, "desktop-notification"); +}); + +add_task(async function observePermissionChange() { + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission. + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.DENY_ACTION + ); + + checkMenulistPermissionItem(URL, Services.perms.DENY_ACTION); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function observePermissionDelete() { + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + richlistbox.itemCount, + 1, + "The box contains one permission item initially" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + + Assert.equal(richlistbox.itemCount, 0); +}); + +add_task(async function onPermissionChange() { + let doc = sitePermissionsDialog.document; + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission state in the UI. + doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click(); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission state does not change before saving changes" + ); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability == Services.perms.DENY_ACTION + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function onPermissionDelete() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + richlistbox.selectItem(richlistbox.getItemAtIndex(0)); + doc.getElementById("removePermission").click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission is not deleted before saving changes" + ); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") == + null + ); +}); + +add_task(async function onAllPermissionsDelete() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + doc.getElementById("removeAllPermissions").click(); + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject(u, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permissions are not deleted before saving changes" + ); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") == + null && + PermissionTestUtils.getPermissionObject(u, "desktop-notification") == null + ); +}); + +add_task(async function onPermissionChangeAndDelete() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission state in the UI. + doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click(); + + // Remove that permission by clicking the "Remove" button. + richlistbox.selectItem(richlistbox.getItemAtIndex(0)); + doc.getElementById("removePermission").click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + doc.querySelector("dialog").getButton("accept").click(); + + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") == + null + ); +}); + +add_task(async function onPermissionChangeCancel() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Change the permission state in the UI. + doc.getElementsByAttribute("value", SitePermissions.BLOCK)[0].click(); + + doc.querySelector("dialog").getButton("cancel").click(); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission state does not change on clicking cancel" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function onPermissionDeleteCancel() { + await openPermissionsDialog("desktop-notification"); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + // Remove that permission by clicking the "Remove" button. + richlistbox.selectItem(richlistbox.getItemAtIndex(0)); + doc.getElementById("removePermission").click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 0); + + doc.querySelector("dialog").getButton("cancel").click(); + + Assert.equal( + PermissionTestUtils.getPermissionObject(URI, "desktop-notification") + .capability, + Services.perms.ALLOW_ACTION, + "Permission state does not change on clicking cancel" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); +}); + +add_task(async function onSearch() { + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + let searchBox = doc.getElementById("searchBox"); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + searchBox.value = "www.example.com"; + + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + doc.getElementsByAttribute("origin", "http://www.test.com")[0], + null + ); + Assert.equal( + doc.getElementsByAttribute("origin", "http://www.example.com")[0], + richlistbox.getItemAtIndex(0) + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + PermissionTestUtils.remove(u, "desktop-notification"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function onPermissionsSort() { + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.DENY_ACTION + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + // Test default arrangement(Allow followed by Block). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.example.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.test.com" + ); + + doc.getElementById("statusCol").click(); + + // Test the rearrangement(Block followed by Allow). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.test.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.example.com" + ); + + doc.getElementById("siteCol").click(); + + // Test the rearrangement(Website names arranged in alphabhetical order). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.example.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.test.com" + ); + + doc.getElementById("siteCol").click(); + + // Test the rearrangement(Website names arranged in reverse alphabhetical order). + Assert.equal( + richlistbox.getItemAtIndex(0).getAttribute("origin"), + "http://www.test.com" + ); + Assert.equal( + richlistbox.getItemAtIndex(1).getAttribute("origin"), + "http://www.example.com" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + PermissionTestUtils.remove(u, "desktop-notification"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function onPermissionDisable() { + // Enable desktop-notification permission prompts. + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.UNKNOWN + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + + // Check if the enabled state is reflected in the checkbox. + let checkbox = doc.getElementById("permissionsDisableCheckbox"); + Assert.equal(checkbox.checked, false); + + // Disable permission and click on "Cancel". + checkbox.checked = true; + doc.querySelector("dialog").getButton("cancel").click(); + + // Check that the permission is not disabled yet. + let perm = Services.prefs.getIntPref( + "permissions.default.desktop-notification" + ); + Assert.equal(perm, SitePermissions.UNKNOWN); + + // Open the dialog once again. + await openPermissionsDialog("desktop-notification"); + doc = sitePermissionsDialog.document; + + // Disable permission and save changes. + checkbox = doc.getElementById("permissionsDisableCheckbox"); + checkbox.checked = true; + doc.querySelector("dialog").getButton("accept").click(); + + // Check if the permission is now disabled. + perm = Services.prefs.getIntPref("permissions.default.desktop-notification"); + Assert.equal(perm, SitePermissions.BLOCK); + + // Open the dialog once again and check if the disabled state is still reflected in the checkbox. + await openPermissionsDialog("desktop-notification"); + doc = sitePermissionsDialog.document; + checkbox = doc.getElementById("permissionsDisableCheckbox"); + Assert.equal(checkbox.checked, true); + + // Close the dialog and clean up. + doc.querySelector("dialog").getButton("cancel").click(); + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.UNKNOWN + ); +}); + +add_task(async function checkDefaultPermissionState() { + // Set default permission state to ALLOW. + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.ALLOW + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + + // Check if the enabled state is reflected in the checkbox. + let checkbox = doc.getElementById("permissionsDisableCheckbox"); + Assert.equal(checkbox.checked, false); + + // Check the checkbox and then uncheck it. + checkbox.checked = true; + checkbox.checked = false; + + // Save changes. + doc.querySelector("dialog").getButton("accept").click(); + + // Check if the default permission state is retained (and not automatically set to SitePermissions.UNKNOWN). + let state = Services.prefs.getIntPref( + "permissions.default.desktop-notification" + ); + Assert.equal(state, SitePermissions.ALLOW); + + // Clean up. + Services.prefs.setIntPref( + "permissions.default.desktop-notification", + SitePermissions.UNKNOWN + ); +}); + +add_task(async function testTabBehaviour() { + // Test tab behaviour inside the permissions setting dialog when site permissions are selected. + // Only selected items in the richlistbox should be tabable for accessibility reasons. + + // Force tabfocus for all elements on OSX. + SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] }); + + PermissionTestUtils.add( + URI, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + let u = Services.io.newURI("http://www.test.com"); + PermissionTestUtils.add( + u, + "desktop-notification", + Services.perms.ALLOW_ACTION + ); + + await openPermissionsDialog("desktop-notification"); + let doc = sitePermissionsDialog.document; + + EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog); + let richlistbox = doc.getElementById("permissionsBox"); + is( + richlistbox, + doc.activeElement.closest("#permissionsBox"), + "The richlistbox is focused after pressing tab once." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown", {}, sitePermissionsDialog); + EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog); + let menulist = doc + .getElementById("permissionsBox") + .itemChildren[1].getElementsByTagName("menulist")[0]; + is( + menulist, + doc.activeElement, + "The menulist inside the selected richlistitem is focused now" + ); + + EventUtils.synthesizeKey("KEY_Tab", {}, sitePermissionsDialog); + let removeButton = doc.getElementById("removePermission"); + is( + removeButton, + doc.activeElement, + "The focus moves outside the richlistbox and onto the remove button" + ); + + PermissionTestUtils.remove(URI, "desktop-notification"); + PermissionTestUtils.remove(u, "desktop-notification"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function addSpeakerPermission() { + let enabled = Services.prefs.getBoolPref("media.setsinkid.enabled", false); + let speakerRow = + gBrowser.contentDocument.getElementById("speakerSettingsRow"); + Assert.equal( + BrowserTestUtils.is_visible(speakerRow), + enabled, + "speakerRow visible" + ); + if (!enabled) { + return; + } + + await openPermissionsDialog("speaker"); + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + Assert.equal( + richlistbox.itemCount, + 0, + "Number of permission items is 0 initially" + ); + // Add an allow permission for a device. + let deviceId = "DEVICE-ID"; + let devicePermissionId = `speaker^${deviceId}`; + PermissionTestUtils.add(URI, devicePermissionId, Services.perms.ALLOW_ACTION); + + // Observe the added permission changes in the dialog UI. + Assert.equal(richlistbox.itemCount, 1, "itemCount with allow"); + checkMenulistPermissionItem(URL, Services.perms.ALLOW_ACTION); + + // Check that an all-device deny permission overrides the device-specific + // allow permission. + PermissionTestUtils.add(URI, "speaker", Services.perms.DENY_ACTION); + + Assert.equal(richlistbox.itemCount, 1, "itemCount with deny and allow"); + let richlistitem = richlistbox.itemChildren[0]; + let siteStatus = richlistitem.querySelector(".website-status"); + Assert.equal( + siteStatus.value, + Services.perms.DENY_ACTION, + "website status with deny and allow" + ); + // The website status element is not a menulist because all-device allow is + // not an option. + Assert.equal(siteStatus.tagName, "hbox"); + Assert.equal(siteStatus.firstElementChild.tagName, "label"); + + PermissionTestUtils.remove(URI, devicePermissionId); + PermissionTestUtils.remove(URI, "speaker"); + + doc.querySelector("dialog").getButton("cancel").click(); +}); + +add_task(async function removeTab() { + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js b/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js new file mode 100644 index 0000000000..37bde1a275 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_dialog_default_perm.js @@ -0,0 +1,145 @@ +"use strict"; + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +let sitePermissionsDialog; + +let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("http://www.example.com"), + {} +); +let pbPrincipal = Services.scriptSecurityManager.principalWithOA(principal, { + privateBrowsingId: 1, +}); +let principalB = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://example.org"), + {} +); + +/** + * Replaces the default permissions defined in browser/app/permissions with our + * own test-only permissions and instructs the permission manager to import + * them. + */ +async function addDefaultTestPermissions() { + // create a file in the temp directory with the defaults. + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("test_default_permissions"); + + await IOUtils.writeUTF8( + file.path, + `origin\tinstall\t1\t${principal.origin}\norigin\tinstall\t1\t${pbPrincipal.origin}\n` + ); + + // Change the default permission file path. + await SpecialPowers.pushPrefEnv({ + set: [ + ["permissions.manager.defaultsUrl", Services.io.newFileURI(file).spec], + ], + }); + + // Call the permission manager to reload default permissions from file. + Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); + + registerCleanupFunction(async () => { + // Clean up temporary default permission file. + await IOUtils.remove(file.path); + + // Restore non-test default permissions. + await SpecialPowers.popPrefEnv(); + Services.obs.notifyObservers(null, "testonly-reload-permissions-from-disk"); + }); +} + +async function openPermissionsDialog() { + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let settingsButton = doc.getElementById("addonExceptions"); + settingsButton.click(); + }); + + sitePermissionsDialog = await dialogOpened; + await sitePermissionsDialog.document.mozSubdialogReady; +} + +add_setup(async function () { + await addDefaultTestPermissions(); +}); + +/** + * Tests that default (persistent) private browsing permissions can be removed. + */ +add_task(async function removeAll() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openPermissionsDialog(); + + let doc = sitePermissionsDialog.document; + let richlistbox = doc.getElementById("permissionsBox"); + + // First item in the richlistbox contains column headers. + Assert.equal( + richlistbox.itemCount, + 2, + "Should have the two default permission entries initially." + ); + + info("Adding a new non-default install permission"); + PermissionTestUtils.add(principalB, "install", Services.perms.ALLOW_ACTION); + + info("Waiting for the permission to appear in the list."); + await BrowserTestUtils.waitForMutationCondition( + richlistbox, + { childList: true }, + () => richlistbox.itemCount == 3 + ); + + info("Clicking remove all."); + doc.getElementById("removeAllPermissions").click(); + + info("Waiting for all list items to be cleared."); + await BrowserTestUtils.waitForMutationCondition( + richlistbox, + { childList: true }, + () => richlistbox.itemCount == 0 + ); + + let dialogClosePromise = BrowserTestUtils.waitForEvent( + sitePermissionsDialog, + "dialogclosing", + true + ); + + info("Accepting dialog to apply the changes."); + doc.querySelector("dialog").getButton("accept").click(); + + info("Waiting for dialog to close."); + await dialogClosePromise; + + info("Waiting for all permissions to be removed."); + await TestUtils.waitForCondition( + () => + PermissionTestUtils.getPermissionObject(principal, "install") == null && + PermissionTestUtils.getPermissionObject(pbPrincipal, "install") == null && + PermissionTestUtils.getPermissionObject(principalB, "install") == null + ); + + info("Opening the permissions dialog again."); + await openPermissionsDialog(); + + Assert.equal( + richlistbox.itemCount, + 0, + "Permission list should still be empty." + ); + + // Cleanup + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.perms.removeAll(); +}); diff --git a/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js b/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js new file mode 100644 index 0000000000..537ee3db72 --- /dev/null +++ b/browser/components/preferences/tests/browser_permissions_urlFieldHidden.js @@ -0,0 +1,38 @@ +"use strict"; + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +add_task(async function urlFieldVisibleForPopupPermissions(finish) { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let win = gBrowser.selectedBrowser.contentWindow; + let doc = win.document; + let popupPolicyCheckbox = doc.getElementById("popupPolicy"); + ok( + !popupPolicyCheckbox.checked, + "popupPolicyCheckbox should be unchecked by default" + ); + popupPolicyCheckbox.click(); + let popupPolicyButton = doc.getElementById("popupPolicyButton"); + ok(popupPolicyButton, "popupPolicyButton found"); + let dialogPromise = promiseLoadSubDialog(PERMISSIONS_URL); + popupPolicyButton.click(); + let dialog = await dialogPromise; + ok(dialog, "dialog loaded"); + + let urlLabel = dialog.document.getElementById("urlLabel"); + ok( + !urlLabel.hidden, + "urlLabel should be visible when one of block/session/allow visible" + ); + let url = dialog.document.getElementById("url"); + ok( + !url.hidden, + "url should be visible when one of block/session/allow visible" + ); + + popupPolicyCheckbox.click(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_primaryPassword.js b/browser/components/preferences/tests/browser_primaryPassword.js new file mode 100644 index 0000000000..4de28a1fdb --- /dev/null +++ b/browser/components/preferences/tests/browser_primaryPassword.js @@ -0,0 +1,130 @@ +const { OSKeyStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/OSKeyStoreTestUtils.sys.mjs" +); +const { OSKeyStore } = ChromeUtils.importESModule( + "resource://gre/modules/OSKeyStore.sys.mjs" +); + +add_task(async function () { + let prefs = await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected"); + + let doc = gBrowser.contentDocument; + // Fake the subdialog and LoginHelper + let win = doc.defaultView; + let dialogURL = ""; + let dialogOpened = false; + XPCOMUtils.defineLazyGetter(win, "gSubDialog", () => ({ + open(aDialogURL, { closingCallback: aCallback }) { + dialogOpened = true; + dialogURL = aDialogURL; + primaryPasswordSet = primaryPasswordNextState; + aCallback(); + }, + })); + + let primaryPasswordSet = false; + win.LoginHelper = { + isPrimaryPasswordSet() { + return primaryPasswordSet; + }, + }; + + let checkbox = doc.querySelector("#useMasterPassword"); + checkbox.scrollIntoView(); + ok( + !checkbox.checked, + "primary password checkbox should be unchecked by default" + ); + let button = doc.getElementById("changeMasterPassword"); + ok(button.disabled, "primary password button should be disabled by default"); + + let primaryPasswordNextState = false; + if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) { + let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); + checkbox.click(); + info("waiting for os auth dialog to appear and get canceled"); + await osAuthDialogShown; + await TestUtils.waitForCondition( + () => !checkbox.checked, + "wait for checkbox to get unchecked" + ); + ok(!dialogOpened, "the dialog should not have opened"); + ok( + !dialogURL, + "the changemp dialog should not have been opened when the os auth dialog is canceled" + ); + ok( + !checkbox.checked, + "primary password checkbox should be unchecked after canceling os auth dialog" + ); + ok(button.disabled, "button should be disabled after canceling os auth"); + } + + primaryPasswordNextState = true; + if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) { + let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + checkbox.click(); + info("waiting for os auth dialog to appear"); + await osAuthDialogShown; + info("waiting for dialogURL to get set"); + await TestUtils.waitForCondition( + () => dialogURL, + "wait for open to get called asynchronously" + ); + is( + dialogURL, + "chrome://mozapps/content/preferences/changemp.xhtml", + "clicking on the checkbox should open the primary password dialog" + ); + } else { + primaryPasswordSet = true; + doc.defaultView.gPrivacyPane._initMasterPasswordUI(); + await TestUtils.waitForCondition( + () => !button.disabled, + "waiting for primary password button to get enabled" + ); + } + ok(!button.disabled, "primary password button should now be enabled"); + ok(checkbox.checked, "primary password checkbox should be checked now"); + + dialogURL = ""; + button.doCommand(); + await TestUtils.waitForCondition( + () => dialogURL, + "wait for open to get called asynchronously" + ); + is( + dialogURL, + "chrome://mozapps/content/preferences/changemp.xhtml", + "clicking on the button should open the primary password dialog" + ); + ok(!button.disabled, "primary password button should still be enabled"); + ok(checkbox.checked, "primary password checkbox should be checked still"); + + // Confirm that we won't automatically respond to the dialog, + // since we don't expect a dialog here, we want the test to fail if one appears. + is( + Services.prefs.getStringPref( + "toolkit.osKeyStore.unofficialBuildOnlyLogin", + "" + ), + "", + "Pref should be set to an empty string" + ); + + primaryPasswordNextState = false; + dialogURL = ""; + checkbox.click(); + is( + dialogURL, + "chrome://mozapps/content/preferences/removemp.xhtml", + "clicking on the checkbox to uncheck primary password should show the removal dialog" + ); + ok(button.disabled, "primary password button should now be disabled"); + ok(!checkbox.checked, "primary password checkbox should now be unchecked"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js b/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js new file mode 100644 index 0000000000..722fe9a215 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_cookieBannerHandling.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests the Privacy pane's Cookie Banner Handling UI. + +"use strict"; + +const FEATURE_PREF = "cookiebanners.ui.desktop.enabled"; +const MODE_PREF = "cookiebanners.service.mode"; +const PBM_MODE_PREF = "cookiebanners.service.mode.privateBrowsing"; +const DETECT_ONLY_PREF = "cookiebanners.service.detectOnly"; + +const GROUPBOX_ID = "cookieBannerHandlingGroup"; +const CHECKBOX_ID = "handleCookieBanners"; + +// Test the section is hidden on page load if the feature pref is disabled. +add_task(async function test_section_hidden_when_feature_flag_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, false], + [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let groupbox = browser.contentDocument.getElementById(GROUPBOX_ID); + is_element_hidden(groupbox, "#cookieBannerHandlingGroup is hidden"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the section is shown on page load if the feature pref is enabled. +add_task(async function test_section_shown_when_feature_flag_enabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let groupbox = browser.contentDocument.getElementById(GROUPBOX_ID); + is_element_visible(groupbox, "#cookieBannerHandlingGroup is visible"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is unchecked in DISABLED mode. +add_task(async function test_checkbox_unchecked_disabled_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_DISABLED], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(!checkbox.checked, "checkbox is not checked in DISABLED mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is unchecked in detect-only mode. +add_task(async function test_checkbox_unchecked_detect_only_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT], + [DETECT_ONLY_PREF, true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(!checkbox.checked, "checkbox is not checked in detect-only mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is checked in REJECT_OR_ACCEPT mode. +add_task(async function test_checkbox_checked_reject_or_accept_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(checkbox.checked, "checkbox is checked in REJECT_OR_ACCEPT mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the checkbox is checked in REJECT mode. +add_task(async function test_checkbox_checked_reject_mode() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_REJECT], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkbox = browser.contentDocument.getElementById(CHECKBOX_ID); + ok(checkbox.checked, "checkbox is checked in REJECT mode"); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test that toggling the checkbox toggles the mode pref value as expected, +// and also disables detect only mode, as expected. +add_task(async function test_checkbox_modifies_prefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, true], + [MODE_PREF, Ci.nsICookieBannerService.MODE_UNSET], + [PBM_MODE_PREF, Ci.nsICookieBannerService.MODE_UNSET], + [DETECT_ONLY_PREF, true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let checkboxSelector = "#" + CHECKBOX_ID; + let checkbox = browser.contentDocument.querySelector(checkboxSelector); + let section = browser.contentDocument.getElementById(GROUPBOX_ID); + + section.scrollIntoView(); + + Assert.ok( + !checkbox.checked, + "initially, the checkbox should be unchecked" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + checkboxSelector, + {}, + browser + ); + Assert.ok(checkbox.checked, "checkbox should be checked"); + Assert.equal( + Ci.nsICookieBannerService.MODE_REJECT, + Services.prefs.getIntPref(MODE_PREF), + "cookie banner handling mode should be set to REJECT mode after checking the checkbox" + ); + Assert.equal( + Ci.nsICookieBannerService.MODE_REJECT, + Services.prefs.getIntPref(PBM_MODE_PREF), + "cookie banner handling mode for PBM should be set to REJECT mode after checking the checkbox" + ); + Assert.equal( + false, + Services.prefs.getBoolPref(DETECT_ONLY_PREF), + "cookie banner handling detect-only mode should be disabled after checking the checkbox" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + checkboxSelector, + {}, + browser + ); + Assert.ok(!checkbox.checked, "checkbox should be unchecked"); + Assert.equal( + Ci.nsICookieBannerService.MODE_DISABLED, + Services.prefs.getIntPref(MODE_PREF), + "cookie banner handling mode should be set to DISABLED mode after unchecking the checkbox" + ); + Assert.equal( + Ci.nsICookieBannerService.MODE_DISABLED, + Services.prefs.getIntPref(PBM_MODE_PREF), + "cookie banner handling mode for PBM should be set to DISABLED mode after unchecking the checkbox" + ); + Assert.equal( + false, + Services.prefs.getBoolPref(DETECT_ONLY_PREF), + "cookie banner handling detect-only mode should still be disabled after unchecking the checkbox" + ); + } + ); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js new file mode 100644 index 0000000000..48469cfce4 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js @@ -0,0 +1,844 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(4); + +const { EnterprisePolicyTesting, PoliciesPrefTracker } = + ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + +ChromeUtils.defineESModuleGetters(this, { + DoHConfigController: "resource:///modules/DoHConfig.sys.mjs", + DoHController: "resource:///modules/DoHController.sys.mjs", + DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs", +}); + +const TRR_MODE_PREF = "network.trr.mode"; +const TRR_URI_PREF = "network.trr.uri"; +const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri"; +const ROLLOUT_ENABLED_PREF = "doh-rollout.enabled"; +const ROLLOUT_SELF_ENABLED_PREF = "doh-rollout.self-enabled"; +const HEURISTICS_DISABLED_PREF = "doh-rollout.disable-heuristics"; +const FIRST_RESOLVER_VALUE = DoHTestUtils.providers[0].uri; +const SECOND_RESOLVER_VALUE = DoHTestUtils.providers[1].uri; +const DEFAULT_RESOLVER_VALUE = FIRST_RESOLVER_VALUE; + +const defaultPrefValues = Object.freeze({ + [TRR_MODE_PREF]: 0, + [TRR_CUSTOM_URI_PREF]: "", +}); + +// See bug 1741554. This test should not actually try to create a connection to +// the real DoH endpoint. But a background request could do that while the test +// is in progress, before we've actually disabled TRR, and would cause a crash +// due to connecting to a non-local IP. +// To prevent that we override the IP to a local address. +Cc["@mozilla.org/network/native-dns-override;1"] + .getService(Ci.nsINativeDNSResolverOverride) + .addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1"); + +async function clearEvents() { + Services.telemetry.clearEvents(); + await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_ALL_CHANNELS, + true + ).parent; + return !events || !events.length; + }); +} + +async function getEvent(filter1, filter2) { + let event = await TestUtils.waitForCondition(() => { + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_ALL_CHANNELS, + true + ).parent; + return events?.find(e => e[1] == filter1 && e[2] == filter2); + }, "recorded telemetry for the load"); + event.shift(); + return event; +} + +async function resetPrefs() { + await DoHTestUtils.resetRemoteSettingsConfig(); + await DoHController._uninit(); + Services.prefs.clearUserPref(TRR_MODE_PREF); + Services.prefs.clearUserPref(TRR_URI_PREF); + Services.prefs.clearUserPref(TRR_CUSTOM_URI_PREF); + Services.prefs.getChildList("doh-rollout.").forEach(pref => { + Services.prefs.clearUserPref(pref); + }); + // Clear out any telemetry events generated by DoHController so that we don't + // confuse tests running after this one that are looking at those. + Services.telemetry.clearEvents(); + await DoHController.init(); +} +Services.prefs.setStringPref("network.trr.confirmationNS", "skip"); + +registerCleanupFunction(async () => { + await resetPrefs(); + Services.prefs.clearUserPref("network.trr.confirmationNS"); +}); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["toolkit.telemetry.testing.overrideProductsCheck", true]], + }); + + await DoHTestUtils.resetRemoteSettingsConfig(); +}); + +function waitForPrefObserver(name) { + return new Promise(resolve => { + const observer = { + observe(aSubject, aTopic, aData) { + if (aData == name) { + Services.prefs.removeObserver(name, observer); + resolve(); + } + }, + }; + Services.prefs.addObserver(name, observer); + }); +} + +async function testWithProperties(props, startTime) { + info( + Date.now() - + startTime + + ": testWithProperties: testing with " + + JSON.stringify(props) + ); + + // There are two different signals that the DoHController is ready, depending + // on the config being tested. If we're setting the TRR mode pref, we should + // expect the disable-heuristics pref to be set as the signal. Else, we can + // expect the self-enabled pref as the signal. + let rolloutReadyPromise; + if (props.hasOwnProperty(TRR_MODE_PREF)) { + if ( + [2, 3, 5].includes(props[TRR_MODE_PREF]) && + props.hasOwnProperty(ROLLOUT_ENABLED_PREF) + ) { + // Only initialize the promise if we're going to enable the rollout - + // otherwise we will never await it, which could cause a leak if it doesn't + // end up resolving. + rolloutReadyPromise = waitForPrefObserver(HEURISTICS_DISABLED_PREF); + } + Services.prefs.setIntPref(TRR_MODE_PREF, props[TRR_MODE_PREF]); + } + if (props.hasOwnProperty(ROLLOUT_ENABLED_PREF)) { + if (!rolloutReadyPromise) { + rolloutReadyPromise = waitForPrefObserver(ROLLOUT_SELF_ENABLED_PREF); + } + Services.prefs.setBoolPref( + ROLLOUT_ENABLED_PREF, + props[ROLLOUT_ENABLED_PREF] + ); + await rolloutReadyPromise; + } + if (props.hasOwnProperty(TRR_CUSTOM_URI_PREF)) { + Services.prefs.setStringPref( + TRR_CUSTOM_URI_PREF, + props[TRR_CUSTOM_URI_PREF] + ); + } + if (props.hasOwnProperty(TRR_URI_PREF)) { + Services.prefs.setStringPref(TRR_URI_PREF, props[TRR_URI_PREF]); + } + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + info(Date.now() - startTime + ": testWithProperties: tab now open"); + let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup"); + let uriTextbox = doc.getElementById("dohEnabledInputField"); + let resolverMenulist = doc.getElementById("dohStrictResolverChoices"); + let modePrefChangedPromise; + let uriPrefChangedPromise; + let disableHeuristicsPrefChangedPromise; + + modeRadioGroup.scrollIntoView(); + + if (props.hasOwnProperty("expectedSelectedIndex")) { + await TestUtils.waitForCondition( + () => modeRadioGroup.selectedIndex === props.expectedSelectedIndex + ); + is( + modeRadioGroup.selectedIndex, + props.expectedSelectedIndex, + "dohCategoryRadioGroup has expected selected index" + ); + } + if (props.hasOwnProperty("expectedUriValue")) { + await TestUtils.waitForCondition( + () => uriTextbox.value === props.expectedUriValue + ); + is( + uriTextbox.value, + props.expectedUriValue, + "URI textbox has expected value" + ); + } + if (props.hasOwnProperty("expectedResolverListValue")) { + await TestUtils.waitForCondition( + () => resolverMenulist.value === props.expectedResolverListValue + ); + is( + resolverMenulist.value, + props.expectedResolverListValue, + "resolver menulist has expected value" + ); + } + + if (props.clickMode) { + await clearEvents(); + info( + Date.now() - + startTime + + ": testWithProperties: clickMode, waiting for the pref observer" + ); + modePrefChangedPromise = waitForPrefObserver(TRR_MODE_PREF); + if (props.hasOwnProperty("expectedDisabledHeuristics")) { + disableHeuristicsPrefChangedPromise = waitForPrefObserver( + HEURISTICS_DISABLED_PREF + ); + } + info( + Date.now() - startTime + ": testWithProperties: clickMode, pref changed" + ); + let option = doc.getElementById(props.clickMode); + option.scrollIntoView(); + let win = doc.ownerGlobal; + EventUtils.synthesizeMouseAtCenter(option, {}, win); + info( + `${Date.now() - startTime} : testWithProperties: clickMode=${ + props.clickMode + }, mouse click synthesized` + ); + let clickEvent = await getEvent("security.doh.settings", "mode_changed"); + Assert.deepEqual(clickEvent, [ + "security.doh.settings", + "mode_changed", + "button", + props.clickMode, + ]); + } + if (props.hasOwnProperty("selectResolver")) { + await clearEvents(); + info( + Date.now() - + startTime + + ": testWithProperties: selectResolver, creating change event" + ); + resolverMenulist.focus(); + resolverMenulist.value = props.selectResolver; + resolverMenulist.dispatchEvent(new Event("input", { bubbles: true })); + resolverMenulist.dispatchEvent(new Event("command", { bubbles: true })); + info( + Date.now() - + startTime + + ": testWithProperties: selectResolver, item value set and events dispatched" + ); + let choiceEvent = await getEvent( + "security.doh.settings", + "provider_choice" + ); + Assert.deepEqual(choiceEvent, [ + "security.doh.settings", + "provider_choice", + "value", + props.selectResolver, + ]); + } + if (props.hasOwnProperty("inputUriKeys")) { + info( + Date.now() - + startTime + + ": testWithProperties: inputUriKeys, waiting for the pref observer" + ); + uriPrefChangedPromise = waitForPrefObserver(TRR_CUSTOM_URI_PREF); + info( + Date.now() - + startTime + + ": testWithProperties: inputUriKeys, pref changed, now enter the new value" + ); + let win = doc.ownerGlobal; + uriTextbox.focus(); + uriTextbox.value = props.inputUriKeys; + uriTextbox.dispatchEvent(new win.Event("input", { bubbles: true })); + uriTextbox.dispatchEvent(new win.Event("change", { bubbles: true })); + info( + Date.now() - + startTime + + ": testWithProperties: inputUriKeys, input and change events dispatched" + ); + } + + info( + Date.now() - + startTime + + ": testWithProperties: waiting for any of uri and mode prefs to change" + ); + await Promise.all([ + uriPrefChangedPromise, + modePrefChangedPromise, + disableHeuristicsPrefChangedPromise, + ]); + info(Date.now() - startTime + ": testWithProperties: prefs changed"); + + if (props.hasOwnProperty("expectedFinalUriPref")) { + if (props.expectedFinalUriPref) { + let uriPref = Services.prefs.getStringPref(TRR_URI_PREF); + is( + uriPref, + props.expectedFinalUriPref, + "uri pref ended up with the expected value" + ); + } else { + ok( + !Services.prefs.prefHasUserValue(TRR_URI_PREF), + `uri pref ended up with the expected value (unset) got ${Services.prefs.getStringPref( + TRR_URI_PREF + )}` + ); + } + } + + if (props.hasOwnProperty("expectedModePref")) { + let modePref = Services.prefs.getIntPref(TRR_MODE_PREF); + is( + modePref, + props.expectedModePref, + "mode pref ended up with the expected value" + ); + } + + if (props.hasOwnProperty("expectedDisabledHeuristics")) { + let disabledHeuristicsPref = Services.prefs.getBoolPref( + HEURISTICS_DISABLED_PREF + ); + is( + disabledHeuristicsPref, + props.expectedDisabledHeuristics, + "disable-heuristics pref ended up with the expected value" + ); + } + + if (props.hasOwnProperty("expectedFinalCustomUriPref")) { + let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); + is( + customUriPref, + props.expectedFinalCustomUriPref, + "custom_uri pref ended up with the expected value" + ); + } + + if (props.hasOwnProperty("expectedModeValue")) { + let modeValue = Services.prefs.getIntPref(TRR_MODE_PREF); + is(modeValue, props.expectedModeValue, "mode pref has expected value"); + } + + gBrowser.removeCurrentTab(); + info(Date.now() - startTime + ": testWithProperties: fin"); +} + +add_task(async function default_values() { + let customUriPref = Services.prefs.getStringPref(TRR_CUSTOM_URI_PREF); + let uriPrefHasUserValue = Services.prefs.prefHasUserValue(TRR_URI_PREF); + let modePref = Services.prefs.getIntPref(TRR_MODE_PREF); + is( + modePref, + defaultPrefValues[TRR_MODE_PREF], + `Actual value of ${TRR_MODE_PREF} matches expected default value` + ); + ok( + !uriPrefHasUserValue, + `Actual value of ${TRR_URI_PREF} matches expected default value (unset)` + ); + is( + customUriPref, + defaultPrefValues[TRR_CUSTOM_URI_PREF], + `Actual value of ${TRR_CUSTOM_URI_PREF} matches expected default value` + ); +}); + +const DEFAULT_OPTION_INDEX = 0; +const ENABLED_OPTION_INDEX = 1; +const STRICT_OPTION_INDEX = 2; +const OFF_OPTION_INDEX = 3; + +let testVariations = [ + // verify state with defaults + { + name: "default", + expectedModePref: 0, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + expectedUriValue: "", + }, + + // verify each of the modes maps to the correct checked state + { + name: "mode 0", + [TRR_MODE_PREF]: 0, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + }, + { + name: "mode 1", + [TRR_MODE_PREF]: 1, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + { + name: "mode 2", + [TRR_MODE_PREF]: 2, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + expectedFinalUriPref: "", + }, + { + name: "mode 3", + [TRR_MODE_PREF]: 3, + expectedSelectedIndex: STRICT_OPTION_INDEX, + expectedFinalUriPref: "", + }, + { + name: "mode 4", + [TRR_MODE_PREF]: 4, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + { + name: "mode 5", + [TRR_MODE_PREF]: 5, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + // verify an out of bounds mode value maps to the correct checked state + { + name: "mode out-of-bounds", + [TRR_MODE_PREF]: 77, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + + // verify automatic heuristics states + { + name: "heuristics on and mode unset", + [TRR_MODE_PREF]: 0, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + }, + { + name: "heuristics on and mode set to 2", + [TRR_MODE_PREF]: 2, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + }, + { + name: "heuristics on but disabled, mode unset", + [TRR_MODE_PREF]: 5, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: OFF_OPTION_INDEX, + }, + { + name: "heuristics on but disabled, mode set to 2", + [TRR_MODE_PREF]: 2, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + }, + + // verify picking each radio button option gives the right outcomes + { + name: "toggle mode on", + clickMode: "dohEnabledRadio", + expectedModeValue: 2, + expectedUriValue: "", + expectedFinalUriPref: "", + }, + { + name: "toggle mode off", + [TRR_MODE_PREF]: 2, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + clickMode: "dohOffRadio", + expectedModePref: 5, + }, + { + name: "toggle mode off when on due to heuristics", + [TRR_MODE_PREF]: 0, + [ROLLOUT_ENABLED_PREF]: true, + expectedSelectedIndex: DEFAULT_OPTION_INDEX, + clickMode: "dohOffRadio", + expectedModePref: 5, + expectedDisabledHeuristics: true, + }, + // Test selecting non-default, non-custom TRR provider, NextDNS. + { + name: "Select NextDNS as TRR provider", + [TRR_MODE_PREF]: 2, + selectResolver: SECOND_RESOLVER_VALUE, + expectedFinalUriPref: SECOND_RESOLVER_VALUE, + }, + // Test selecting non-default, non-custom TRR provider, NextDNS, + // with DoH not enabled. The provider selection should stick. + { + name: "Select NextDNS as TRR provider in mode 0", + [TRR_MODE_PREF]: 0, + selectResolver: SECOND_RESOLVER_VALUE, + expectedFinalUriPref: SECOND_RESOLVER_VALUE, + }, + { + name: "return to default from NextDNS", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: SECOND_RESOLVER_VALUE, + expectedResolverListValue: SECOND_RESOLVER_VALUE, + selectResolver: DEFAULT_RESOLVER_VALUE, + expectedFinalUriPref: FIRST_RESOLVER_VALUE, + }, + // test that selecting Custom, when we have a TRR_CUSTOM_URI_PREF subsequently changes TRR_URI_PREF + { + name: "select custom with existing custom_uri pref value", + [TRR_MODE_PREF]: 2, + [TRR_CUSTOM_URI_PREF]: "https://example.com", + expectedModeValue: 2, + expectedSelectedIndex: ENABLED_OPTION_INDEX, + selectResolver: "custom", + expectedUriValue: "https://example.com", + expectedFinalUriPref: "https://example.com", + expectedFinalCustomUriPref: "https://example.com", + }, + { + name: "select custom and enter new custom_uri pref value", + [TRR_URI_PREF]: "", + [TRR_CUSTOM_URI_PREF]: "", + clickMode: "dohEnabledRadio", + selectResolver: "custom", + inputUriKeys: "https://custom.com", + expectedModePref: 2, + expectedFinalUriPref: "https://custom.com", + expectedFinalCustomUriPref: "https://custom.com", + }, + + { + name: "return to default from custom", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "https://custom.com", + expectedUriValue: "https://example.com", + expectedResolverListValue: "custom", + selectResolver: DEFAULT_RESOLVER_VALUE, + expectedFinalUriPref: DEFAULT_RESOLVER_VALUE, + expectedFinalCustomUriPref: "https://example.com", + }, + { + name: "clear the custom uri", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "https://example.com", + expectedUriValue: "https://example.com", + expectedResolverListValue: "custom", + inputUriKeys: "", + expectedFinalUriPref: " ", + expectedFinalCustomUriPref: "", + }, + { + name: "empty default resolver list", + [TRR_MODE_PREF]: 2, + [TRR_URI_PREF]: "https://example.com", + [TRR_CUSTOM_URI_PREF]: "", + expectedUriValue: "https://example.com", + expectedResolverListValue: "custom", + expectedFinalUriPref: "https://example.com", + expectedFinalCustomUriPref: "https://example.com", + }, +]; + +for (let props of testVariations) { + add_task(async function testVariation() { + let startTime = Date.now(); + info("starting test: " + props.name); + await testWithProperties(props, startTime); + await resetPrefs(); + }); +} + +add_task(async function testRemoteSettingsEnable() { + let startTime = Date.now(); + // Enable the rollout. + await DoHTestUtils.loadRemoteSettingsConfig({ + providers: "example-1, example-2", + rolloutEnabled: true, + steeringEnabled: false, + steeringProviders: "", + autoDefaultEnabled: false, + autoDefaultProviders: "", + id: "global", + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + info(Date.now() - startTime + ": testWithProperties: tab now open"); + let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup"); + + is(modeRadioGroup.value, "0", "expecting default mode"); + + let status = doc.getElementById("dohStatus"); + await TestUtils.waitForCondition( + () => document.l10n.getAttributes(status).args.status == "Active" + ); + is( + document.l10n.getAttributes(status).args.status, + "Active", + "expecting status active" + ); + + let provider = doc.getElementById("dohResolver"); + is( + provider.hidden, + false, + "Provider should not be hidden when status is active" + ); + await TestUtils.waitForCondition( + () => + document.l10n.getAttributes(provider).args.name == + DoHConfigController.currentConfig.providerList[0].UIName + ); + is( + document.l10n.getAttributes(provider).args.name, + DoHConfigController.currentConfig.providerList[0].UIName, + "expecting the right provider name" + ); + + let option = doc.getElementById("dohEnabledRadio"); + option.scrollIntoView(); + let win = doc.ownerGlobal; + EventUtils.synthesizeMouseAtCenter(option, {}, win); + + await TestUtils.waitForCondition(() => + Services.prefs.prefHasUserValue("doh-rollout.disable-heuristics") + ); + is(provider.hidden, false); + await TestUtils.waitForCondition( + () => + document.l10n.getAttributes(provider).args.name == + DoHConfigController.currentConfig.providerList[0].UIName + ); + is( + document.l10n.getAttributes(provider).args.name, + DoHConfigController.currentConfig.providerList[0].UIName, + "expecting the right provider name" + ); + is( + Services.prefs.getIntPref("network.trr.mode"), + Ci.nsIDNSService.MODE_TRRFIRST + ); + + option = doc.getElementById("dohOffRadio"); + option.scrollIntoView(); + win = doc.ownerGlobal; + EventUtils.synthesizeMouseAtCenter(option, {}, win); + await TestUtils.waitForCondition(() => status.innerHTML == "Status: Off"); + is( + Services.prefs.getIntPref("network.trr.mode"), + Ci.nsIDNSService.MODE_TRROFF + ); + is(provider.hidden, true, "Expecting provider to be hidden when DoH is off"); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function testEnterprisePolicy() { + async function withPolicy(policy, fn, preFn = () => {}) { + await resetPrefs(); + PoliciesPrefTracker.start(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(policy); + await preFn(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let modeRadioGroup = doc.getElementById("dohCategoryRadioGroup"); + let resolverMenulist = doc.getElementById("dohEnabledResolverChoices"); + let uriTextbox = doc.getElementById("dohEnabledInputField"); + + await fn({ + modeRadioGroup, + resolverMenulist, + doc, + uriTextbox, + }); + + gBrowser.removeCurrentTab(); + EnterprisePolicyTesting.resetRunOnceState(); + PoliciesPrefTracker.stop(); + } + + info("Check that a locked policy does not allow any changes in the UI"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://examplelocked.com/provider", + ExcludedDomains: ["examplelocked.com", "example.org"], + Locked: true, + }, + }, + }, + async res => { + is(res.modeRadioGroup.disabled, true, "The mode menu should be locked."); + is(res.modeRadioGroup.value, "2", "Should be enabled"); + is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); + is( + res.uriTextbox.value, + "https://examplelocked.com/provider", + "Custom URI should be set" + ); + } + ); + + info("Check that an unlocked policy has editable fields in the dialog"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }, + async res => { + is( + res.modeRadioGroup.disabled, + false, + "The mode menu should not be locked." + ); + is(res.modeRadioGroup.value, "2", "Should be enabled"); + is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); + is( + res.uriTextbox.value, + "https://example.com/provider", + "Expected custom resolver" + ); + } + ); + + info("Check that a locked disabled policy disables the buttons"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: false, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + Locked: true, + }, + }, + }, + async res => { + is(res.modeRadioGroup.disabled, true, "The mode menu should be locked."); + is(res.modeRadioGroup.value, "5", "Should be disabled"); + } + ); + + info("Check that an unlocked disabled policy has editable fields"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: false, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }, + async res => { + is( + res.modeRadioGroup.disabled, + false, + "The mode menu should not be locked." + ); + is(res.modeRadioGroup.value, "5", "Should be disabled"); + } + ); + + info("Check that the remote settings config doesn't override the policy"); + await withPolicy( + { + policies: { + DNSOverHTTPS: { + Enabled: true, + ProviderURL: "https://example.com/provider", + ExcludedDomains: ["example.com", "example.org"], + }, + }, + }, + async res => { + is( + res.modeRadioGroup.disabled, + false, + "The mode menu should not be locked." + ); + is(res.resolverMenulist.value, "custom", "Resolver list shows custom"); + is( + res.uriTextbox.value, + "https://example.com/provider", + "Expected custom resolver" + ); + }, + async function runAfterSettingPolicy() { + await DoHTestUtils.loadRemoteSettingsConfig({ + providers: "example-1, example-2", + rolloutEnabled: true, + steeringEnabled: false, + steeringProviders: "", + autoDefaultEnabled: false, + autoDefaultProviders: "", + id: "global", + }); + } + ); +}); + +add_task(async function clickWarnButton() { + Services.prefs.setBoolPref( + "network.trr_ui.show_fallback_warning_option", + true + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + await clearEvents(); + let checkbox = doc.getElementById("dohWarnCheckbox1"); + checkbox.click(); + + let event = await getEvent("security.doh.settings", "warn_checkbox"); + Assert.deepEqual(event, [ + "security.doh.settings", + "warn_checkbox", + "checkbox", + "true", + ]); + Assert.equal( + Services.prefs.getBoolPref("network.trr.display_fallback_warning"), + true, + "Clicking the checkbox should change the pref" + ); + + checkbox.click(); + event = await getEvent("security.doh.settings", "warn_checkbox"); + Assert.deepEqual(event, [ + "security.doh.settings", + "warn_checkbox", + "checkbox", + "false", + ]); + Assert.equal( + Services.prefs.getBoolPref("network.trr.display_fallback_warning"), + false, + "Clicking the checkbox should change the pref" + ); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js b/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js new file mode 100644 index 0000000000..883e19acc2 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_firefoxSuggest.js @@ -0,0 +1,855 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This tests the Privacy pane's Firefox Suggest UI. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +const CONTAINER_ID = "firefoxSuggestContainer"; +const NONSPONSORED_TOGGLE_ID = "firefoxSuggestNonsponsoredToggle"; +const SPONSORED_TOGGLE_ID = "firefoxSuggestSponsoredToggle"; +const DATA_COLLECTION_TOGGLE_ID = "firefoxSuggestDataCollectionToggle"; +const INFO_BOX_ID = "firefoxSuggestInfoBox"; +const INFO_TEXT_ID = "firefoxSuggestInfoText"; +const LEARN_MORE_CLASS = "firefoxSuggestLearnMore"; +const BEST_MATCH_CONTAINER_ID = "firefoxSuggestBestMatchContainer"; +const BEST_MATCH_CHECKBOX_ID = "firefoxSuggestBestMatch"; +const BUTTON_RESTORE_DISMISSED_ID = "restoreDismissedSuggestions"; +const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST = + "browser.urlbar.quicksuggest.blockedDigests"; +const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather"; + +// Maps text element IDs to `{ enabled, disabled }`, where `enabled` is the +// expected l10n ID when the Firefox Suggest feature is enabled, and `disabled` +// is when disabled. +const EXPECTED_L10N_IDS = { + locationBarGroupHeader: { + enabled: "addressbar-header-firefox-suggest", + disabled: "addressbar-header", + }, + locationBarSuggestionLabel: { + enabled: "addressbar-suggest-firefox-suggest", + disabled: "addressbar-suggest", + }, +}; + +// This test can take a while due to the many permutations some of these tasks +// run through, so request a longer timeout. +requestLongerTimeout(10); + +// The following tasks check the visibility of the Firefox Suggest UI based on +// the value of the feature pref. See doVisibilityTest(). + +add_task(async function historyToOffline() { + await doVisibilityTest({ + initialScenario: "history", + initialExpectedVisibility: false, + newScenario: "offline", + newExpectedVisibility: true, + }); +}); + +add_task(async function historyToOnline() { + await doVisibilityTest({ + initialScenario: "history", + initialExpectedVisibility: false, + newScenario: "online", + newExpectedVisibility: true, + }); +}); + +add_task(async function offlineToHistory() { + await doVisibilityTest({ + initialScenario: "offline", + initialExpectedVisibility: true, + newScenario: "history", + newExpectedVisibility: false, + }); +}); + +add_task(async function offlineToOnline() { + await doVisibilityTest({ + initialScenario: "offline", + initialExpectedVisibility: true, + newScenario: "online", + newExpectedVisibility: true, + }); +}); + +add_task(async function onlineToHistory() { + await doVisibilityTest({ + initialScenario: "online", + initialExpectedVisibility: true, + newScenario: "history", + newExpectedVisibility: false, + }); +}); + +add_task(async function onlineToOffline() { + await doVisibilityTest({ + initialScenario: "online", + initialExpectedVisibility: true, + newScenario: "offline", + newExpectedVisibility: true, + }); +}); + +/** + * Runs a test that checks the visibility of the Firefox Suggest preferences UI + * based on scenario pref. + * + * @param {string} initialScenario + * The initial scenario. + * @param {boolean} initialExpectedVisibility + * Whether the UI should be visible with the initial scenario. + * @param {string} newScenario + * The updated scenario. + * @param {boolean} newExpectedVisibility + * Whether the UI should be visible after setting the new scenario. + */ +async function doVisibilityTest({ + initialScenario, + initialExpectedVisibility, + newScenario, + newExpectedVisibility, +}) { + info( + "Running visibility test: " + + JSON.stringify( + { + initialScenario, + initialExpectedVisibility, + newScenario, + newExpectedVisibility, + }, + null, + 2 + ) + ); + + // Set the initial scenario. + await QuickSuggestTestUtils.setScenario(initialScenario); + + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.quicksuggest.enabled"), + initialExpectedVisibility, + `quicksuggest.enabled is correct after setting initial scenario, initialExpectedVisibility=${initialExpectedVisibility}` + ); + + // Open prefs and check the initial visibility. + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CONTAINER_ID); + Assert.equal( + BrowserTestUtils.is_visible(container), + initialExpectedVisibility, + `The container has the expected initial visibility, initialExpectedVisibility=${initialExpectedVisibility}` + ); + + // Check the text elements' l10n IDs. + for (let [id, { enabled, disabled }] of Object.entries(EXPECTED_L10N_IDS)) { + Assert.equal( + doc.getElementById(id).dataset.l10nId, + initialExpectedVisibility ? enabled : disabled, + `Initial l10n ID for element with ID ${id}, initialExpectedVisibility=${initialExpectedVisibility}` + ); + } + + // Set the new scenario. + await QuickSuggestTestUtils.setScenario(newScenario); + + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.quicksuggest.enabled"), + newExpectedVisibility, + `quicksuggest.enabled is correct after setting new scenario, newExpectedVisibility=${newExpectedVisibility}` + ); + + // Check visibility again. + Assert.equal( + BrowserTestUtils.is_visible(container), + newExpectedVisibility, + `The container has the expected visibility after setting new scenario, newExpectedVisibility=${newExpectedVisibility}` + ); + + // Check the text elements' l10n IDs again. + for (let [id, { enabled, disabled }] of Object.entries(EXPECTED_L10N_IDS)) { + Assert.equal( + doc.getElementById(id).dataset.l10nId, + newExpectedVisibility ? enabled : disabled, + `New l10n ID for element with ID ${id}, newExpectedVisibility=${newExpectedVisibility}` + ); + } + + // Clean up. + gBrowser.removeCurrentTab(); + await QuickSuggestTestUtils.setScenario(null); +} + +// Verifies all 8 states of the 3 toggles and their related info box states. +add_task(async function togglesAndInfoBox() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-all"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored-sponsored"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored-data"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = true + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox("addressbar-firefox-suggest-info-nonsponsored"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-sponsored-data"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = true + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox("addressbar-firefox-suggest-info-sponsored"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = true + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-data"); + await SpecialPowers.popPrefEnv(); + + // suggest.quicksuggest.nonsponsored = false + // suggest.quicksuggest.sponsored = false + // quicksuggest.dataCollection.enabled = false + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", false], + ["browser.urlbar.suggest.quicksuggest.sponsored", false], + ["browser.urlbar.quicksuggest.dataCollection.enabled", false], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox(null); + await SpecialPowers.popPrefEnv(); + + gBrowser.removeCurrentTab(); +}); + +// Clicks each of the toggles and makes sure the prefs and info box are updated. +add_task(async function clickToggles() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let addressBarSection = doc.getElementById("locationBarGroup"); + addressBarSection.scrollIntoView(); + + async function clickToggle(id) { + let toggle = doc.getElementById(id); + let changed = BrowserTestUtils.waitForEvent(toggle, "toggle"); + let button = toggle.buttonEl; + await EventUtils.synthesizeMouseAtCenter( + button, + {}, + gBrowser.selectedBrowser.contentWindow + ); + await changed; + } + + // Set initial state. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: true, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-all"); + + // non-sponsored toggle + await clickToggle(NONSPONSORED_TOGGLE_ID); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ), + "suggest.quicksuggest.nonsponsored is false after clicking non-sponsored toggle" + ); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: true, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-sponsored-data"); + + // sponsored toggle + await clickToggle(SPONSORED_TOGGLE_ID); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ), + "suggest.quicksuggest.nonsponsored remains false after clicking sponsored toggle" + ); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.sponsored" + ), + "suggest.quicksuggest.sponsored is false after clicking sponsored toggle" + ); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: true, + }); + await assertInfoBox("addressbar-firefox-suggest-info-data"); + + // data collection toggle + await clickToggle(DATA_COLLECTION_TOGGLE_ID); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.nonsponsored" + ), + "suggest.quicksuggest.nonsponsored remains false after clicking sponsored toggle" + ); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.suggest.quicksuggest.sponsored" + ), + "suggest.quicksuggest.sponsored remains false after clicking data collection toggle" + ); + Assert.ok( + !Services.prefs.getBoolPref( + "browser.urlbar.quicksuggest.dataCollection.enabled" + ), + "quicksuggest.dataCollection.enabled is false after clicking data collection toggle" + ); + assertPrefUIState({ + [NONSPONSORED_TOGGLE_ID]: false, + [SPONSORED_TOGGLE_ID]: false, + [DATA_COLLECTION_TOGGLE_ID]: false, + }); + await assertInfoBox(null); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Clicks the learn-more links and checks the help page is opened in a new tab. +add_task(async function clickLearnMore() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let addressBarSection = doc.getElementById("locationBarGroup"); + addressBarSection.scrollIntoView(); + + // Set initial state so that the info box and learn more link are shown. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quicksuggest.nonsponsored", true], + ["browser.urlbar.suggest.quicksuggest.sponsored", true], + ["browser.urlbar.quicksuggest.dataCollection.enabled", true], + ], + }); + await assertInfoBox("addressbar-firefox-suggest-info-all"); + + let learnMoreLinks = doc.querySelectorAll("." + LEARN_MORE_CLASS); + Assert.equal( + learnMoreLinks.length, + 3, + "Expected number of learn-more links are present" + ); + for (let link of learnMoreLinks) { + Assert.ok( + BrowserTestUtils.is_visible(link), + "Learn-more link is visible: " + link.id + ); + } + + let prefsTab = gBrowser.selectedTab; + for (let link of learnMoreLinks) { + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ); + info("Clicking learn-more link: " + link.id); + Assert.ok(link.id, "Sanity check: Learn-more link has an ID"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + link.id, + {}, + gBrowser.selectedBrowser + ); + info("Waiting for help page to load in a new tab"); + await tabPromise; + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = prefsTab; + } + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Tests the visibility of the best match checkbox based on the values of +// `browser.urlbar.quicksuggest.enabled` and `browser.urlbar.bestMatch.enabled`. +add_task(async function bestMatchVisibility() { + for (let initialQuickSuggest of [false, true]) { + for (let initialBestMatch of [false, true]) { + for (let newQuickSuggest of [false, true]) { + for (let newBestMatch of [false, true]) { + await doBestMatchVisibilityTest({ + initialQuickSuggest, + initialBestMatch, + newQuickSuggest, + newBestMatch, + }); + } + } + } + } +}); + +// Tests the "Restore" button for dismissed suggestions. +add_task(async function restoreDismissedSuggestions() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let addressBarSection = doc.getElementById("locationBarGroup"); + addressBarSection.scrollIntoView(); + + let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID); + Assert.equal( + Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""), + "", + "Block list is empty initially" + ); + Assert.ok( + Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED), + "Weather suggestions are enabled initially" + ); + Assert.ok(button.disabled, "Restore button is disabled initially."); + + await QuickSuggest.blockedSuggestions.add("https://example.com/"); + Assert.notEqual( + Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""), + "", + "Block list is non-empty after adding URL" + ); + Assert.ok(!button.disabled, "Restore button is enabled after blocking URL."); + button.click(); + Assert.equal( + Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""), + "", + "Block list is empty clicking Restore button" + ); + Assert.ok(button.disabled, "Restore button is disabled after clicking it."); + + Services.prefs.setBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED, false); + Assert.ok( + !button.disabled, + "Restore button is enabled after disabling weather suggestions." + ); + button.click(); + Assert.ok( + Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED), + "Weather suggestions are enabled after clicking Restore button" + ); + Assert.ok( + button.disabled, + "Restore button is disabled after clicking it again." + ); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * Runs a test that checks the visibility of the Firefox Suggest best match + * checkbox. It does the following: + * + * 1. Sets the quick suggest and best match feature prefs + * 2. Opens about:preferences and checks the visibility of the checkbox + * 3. Sets the quick suggest and best match feature prefs again + * 4. Checks the visibility of the checkbox again + * + * @param {boolean} initialQuickSuggest + * The value to set for `browser.urlbar.quicksuggest.enabled` before + * about:preferences is opened. + * @param {boolean} initialBestMatch + * The value to set for `browser.urlbar.bestMatch.enabled` before + * about:preferences is opened. + * @param {boolean} newQuickSuggest + * The value to set for `browser.urlbar.quicksuggest.enabled` while + * about:preferences is open. + * @param {boolean} newBestMatch + * The value to set for `browser.urlbar.bestMatch.enabled` while + * about:preferences is open. + */ +async function doBestMatchVisibilityTest({ + initialQuickSuggest, + initialBestMatch, + newQuickSuggest, + newBestMatch, +}) { + info( + "Running best match visibility test: " + + JSON.stringify( + { + initialQuickSuggest, + initialBestMatch, + newQuickSuggest, + newBestMatch, + }, + null, + 2 + ) + ); + + // Set the initial pref values. + Services.prefs.setBoolPref( + "browser.urlbar.quicksuggest.enabled", + initialQuickSuggest + ); + Services.prefs.setBoolPref( + "browser.urlbar.bestMatch.enabled", + initialBestMatch + ); + + // Open prefs and check the initial visibility. + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(BEST_MATCH_CONTAINER_ID); + Assert.equal( + BrowserTestUtils.is_visible(container), + initialBestMatch, + "The checkbox container has the expected initial visibility" + ); + + // Set the new pref values. + Services.prefs.setBoolPref( + "browser.urlbar.quicksuggest.enabled", + newQuickSuggest + ); + Services.prefs.setBoolPref("browser.urlbar.bestMatch.enabled", newBestMatch); + + // Check visibility again. + Assert.equal( + BrowserTestUtils.is_visible(container), + newBestMatch, + "The checkbox container has the expected visibility after setting prefs" + ); + + // Clean up. + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("browser.urlbar.quicksuggest.enabled"); + Services.prefs.clearUserPref("browser.urlbar.bestMatch.enabled"); +} + +// Tests the visibility of the best match checkbox when the best match feature +// is enabled via a Nimbus experiment before about:preferences is opened. +add_task(async function bestMatchVisibility_experiment_beforeOpen() { + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + bestMatchEnabled: true, + }, + callback: async () => { + await openPreferencesViaOpenPreferencesAPI("privacy", { + leaveOpen: true, + }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(BEST_MATCH_CONTAINER_ID); + Assert.ok( + BrowserTestUtils.is_visible(container), + "The checkbox container is visible" + ); + gBrowser.removeCurrentTab(); + }, + }); +}); + +// Tests the visibility of the best match checkbox when the best match feature +// is enabled via a Nimbus experiment after about:preferences is opened. +add_task(async function bestMatchVisibility_experiment_afterOpen() { + // Open prefs and check the initial visibility. + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(BEST_MATCH_CONTAINER_ID); + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The checkbox container is hidden initially" + ); + + // Install an experiment with best match enabled. + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + bestMatchEnabled: true, + }, + callback: () => { + Assert.ok( + BrowserTestUtils.is_visible(container), + "The checkbox container is visible after installing the experiment" + ); + }, + }); + + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The checkbox container is hidden again after the experiment is uninstalled" + ); + + gBrowser.removeCurrentTab(); +}); + +// Check the pref and the checkbox for best match. +add_task(async function bestMatchToggle() { + // Enable the feature so that the toggle appears. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + const doc = gBrowser.selectedBrowser.contentDocument; + const checkbox = doc.getElementById(BEST_MATCH_CHECKBOX_ID); + checkbox.scrollIntoView(); + + info("Check if the checkbox stauts reflects the pref value"); + for (const isEnabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.bestmatch", isEnabled]], + }); + assertPrefUIState({ [BEST_MATCH_CHECKBOX_ID]: isEnabled }, "checked"); + await SpecialPowers.popPrefEnv(); + } + + info("Check if the pref value reflects the checkbox status"); + for (let i = 0; i < 2; i++) { + const initialValue = checkbox.checked; + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + BEST_MATCH_CHECKBOX_ID, + {}, + gBrowser.selectedBrowser + ); + Assert.ok(initialValue !== checkbox.checked); + Assert.equal( + Services.prefs.getBoolPref("browser.urlbar.suggest.bestmatch"), + checkbox.checked + ); + } + + // Clean up. + Services.prefs.clearUserPref("browser.urlbar.suggest.bestmatch"); + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +// Clicks the learn-more link for best match and checks the help page is opened +// in a new tab. +add_task(async function clickBestMatchLearnMore() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.bestMatch.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const link = doc.getElementById("firefoxSuggestBestMatchLearnMore"); + Assert.ok(BrowserTestUtils.is_visible(link), "Learn-more link is visible"); + + const tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + QuickSuggest.HELP_URL + ); + + info("Clicking learn-more link"); + link.scrollIntoView(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#firefoxSuggestBestMatchLearnMore", + {}, + gBrowser.selectedBrowser + ); + + info("Waiting for help page to load in a new tab"); + const tab = await tabPromise; + gBrowser.removeTab(tab); + + // Clean up. + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * Verifies the state of pref related toggles and checkboxes. + * + * @param {object} stateByElementID + * Maps toggle/checkbox element IDs to booleans. Each boolean + * is the expected state of the corresponding ID. + * @param {object} attr + * Attribute to check against the expected state. The "pressed" + * attribute is verified by default, since this is mostly used + * for toggle buttons. + */ +function assertPrefUIState(stateByElementID, attr = "pressed") { + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CONTAINER_ID); + Assert.ok(BrowserTestUtils.is_visible(container), "The container is visible"); + for (let [id, state] of Object.entries(stateByElementID)) { + let element = doc.getElementById(id); + Assert.equal(element[attr], state, "Expected state for ID: " + id); + } +} + +/** + * Verifies the state of the info box. + * + * @param {string} expectedL10nID + * The l10n ID of the string that should be visible in the info box, null if + * the info box should be hidden. + */ +async function assertInfoBox(expectedL10nID) { + info("Checking info box with expected l10n ID: " + expectedL10nID); + let doc = gBrowser.selectedBrowser.contentDocument; + let infoBox = doc.getElementById(INFO_BOX_ID); + await TestUtils.waitForCondition( + () => BrowserTestUtils.is_visible(infoBox) == !!expectedL10nID, + "Waiting for expected info box visibility: " + !!expectedL10nID + ); + + let infoIcon = infoBox.querySelector(".info-icon"); + Assert.equal( + BrowserTestUtils.is_visible(infoIcon), + !!expectedL10nID, + "The info icon is visible iff a description should be shown" + ); + + let learnMore = infoBox.querySelector("." + LEARN_MORE_CLASS); + Assert.ok(learnMore, "Found the info box learn more link"); + Assert.equal( + BrowserTestUtils.is_visible(learnMore), + !!expectedL10nID, + "The info box learn more link is visible iff a description should be shown" + ); + + if (expectedL10nID) { + let infoText = doc.getElementById(INFO_TEXT_ID); + Assert.equal( + infoText.dataset.l10nId, + expectedL10nID, + "Info text has expected l10n ID" + ); + } +} diff --git a/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js new file mode 100644 index 0000000000..6a6e419229 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_passwordGenerationAndAutofill.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function initialState() { + // check pref permutations to verify the UI opens in the correct state + const prefTests = [ + { + initialPrefs: [ + ["signon.rememberSignons", true], + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ["signon.autofillForms", true], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.rememberSignons", true], + ["signon.generation.available", true], + ["signon.generation.enabled", false], + ["signon.autofillForms", false], + ], + expected: "unchecked", + }, + { + initialPrefs: [ + ["signon.rememberSignons", true], + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ], + expected: "hidden", + }, + { + initialPrefs: [ + ["signon.rememberSignons", false], + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ["signon.autofillForms", true], + ], + expected: "disabled", + }, + ]; + for (let test of prefTests) { + // set initial pref values + info("initialState, testing with: " + JSON.stringify(test)); + await SpecialPowers.pushPrefEnv({ set: test.initialPrefs }); + + // open about:privacy in a tab + // verify expected conditions + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + let doc = browser.contentDocument; + let generatePasswordsCheckbox = doc.getElementById("generatePasswords"); + let autofillFormsCheckbox = doc.getElementById( + "passwordAutofillCheckbox" + ); + doc.getElementById("passwordSettings").scrollIntoView(); + + info("initialState, assert on expected state:" + test.expected); + switch (test.expected) { + case "hidden": + is_element_hidden( + generatePasswordsCheckbox, + "#generatePasswords checkbox is hidden" + ); + break; + case "checked": + is_element_visible( + generatePasswordsCheckbox, + "#generatePasswords checkbox is visible" + ); + ok( + generatePasswordsCheckbox.checked, + "#generatePasswords checkbox is checked" + ); + ok( + autofillFormsCheckbox.checked, + "#passwordAutofillCheckbox is checked" + ); + break; + case "unchecked": + ok( + !generatePasswordsCheckbox.checked, + "#generatePasswords checkbox is un-checked" + ); + ok( + !autofillFormsCheckbox.checked, + "#passwordAutofillCheckbox is un-checked" + ); + break; + case "disabled": + ok( + generatePasswordsCheckbox.disabled, + "#generatePasswords checkbox is disabled" + ); + ok( + autofillFormsCheckbox.disabled, + "#passwordAutofillCheckbox is disabled" + ); + break; + default: + ok(false, "Unknown expected state: " + test.expected); + } + } + ); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function toggleGenerationEnabled() { + // clicking the checkbox should toggle the pref + SpecialPowers.pushPrefEnv({ + set: [ + ["signon.generation.available", true], + ["signon.generation.enabled", false], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + let doc = browser.contentDocument; + let checkbox = doc.getElementById("generatePasswords"); + + info("waiting for the browser to have focus"); + await SimpleTest.promiseFocus(browser); + let prefChanged = TestUtils.waitForPrefChange( + "signon.generation.enabled" + ); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + checkbox.focus(); + is(doc.activeElement, checkbox, "checkbox is focused"); + EventUtils.synthesizeKey(" "); + + info("waiting for pref to change"); + await prefChanged; + ok(checkbox.checked, "#generatePasswords checkbox is checked"); + ok( + Services.prefs.getBoolPref("signon.generation.enabled"), + "enabled pref is now true" + ); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function toggleRememberSignon() { + // toggling rememberSignons checkbox should make generation checkbox disabled + SpecialPowers.pushPrefEnv({ + set: [ + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + let doc = browser.contentDocument; + let checkbox = doc.getElementById("savePasswords"); + let generationCheckbox = doc.getElementById("generatePasswords"); + + ok( + !generationCheckbox.disabled, + "generation checkbox is not initially disabled" + ); + + info("waiting for the browser to have focus"); + await SimpleTest.promiseFocus(browser); + let prefChanged = TestUtils.waitForPrefChange("signon.rememberSignons"); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + checkbox.focus(); + is(doc.activeElement, checkbox, "checkbox is focused"); + EventUtils.synthesizeKey(" "); + + info("waiting for pref to change"); + await prefChanged; + ok(!checkbox.checked, "#savePasswords checkbox is un-checked"); + ok(generationCheckbox.disabled, "generation checkbox becomes disabled"); + } + ); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_privacy_quickactions.js b/browser/components/preferences/tests/browser_privacy_quickactions.js new file mode 100644 index 0000000000..14f907cc5c --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_quickactions.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This tests the Privacy pane's Firefox QuickActions UI. + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderQuickActions: + "resource:///modules/UrlbarProviderQuickActions.sys.mjs", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.quickactions.enabled", true], + ], + }); + + UrlbarProviderQuickActions.addAction("testaction", { + commands: ["testaction"], + label: "quickactions-downloads2", + }); + + registerCleanupFunction(() => { + UrlbarProviderQuickActions.removeAction("testaction"); + }); +}); + +async function isGroupHidden(tab) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [], + async () => content.document.getElementById("quickActionsBox").hidden + ); +} + +add_task(async function test_show_prefs() { + Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", false); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#privacy" + ); + + Assert.ok( + await isGroupHidden(tab), + "The preferences are hidden when pref disabled" + ); + + Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", true); + + Assert.ok( + !(await isGroupHidden(tab)), + "The preferences are shown when pref enabled" + ); + + Services.prefs.clearUserPref("browser.urlbar.quickactions.showPrefs"); + await BrowserTestUtils.removeTab(tab); +}); + +async function testActionIsShown(window) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "testact", + waitForFocus: SimpleTest.waitForFocus, + }); + try { + let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + return result.providerName == "quickactions"; + } catch (e) { + return false; + } +} + +add_task(async function test_prefs() { + Services.prefs.setBoolPref("browser.urlbar.quickactions.showPrefs", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#privacy" + ); + + Assert.ok( + !(await testActionIsShown(window)), + "Actions are not shown while pref disabled" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + let checkbox = content.document.getElementById("enableQuickActions"); + is( + checkbox.checked, + false, + "Checkbox is not checked while feature is disabled" + ); + checkbox.click(); + }); + + Assert.ok( + await testActionIsShown(window), + "Actions are shown after user clicks checkbox" + ); + + Services.prefs.clearUserPref("browser.urlbar.quickactions.showPrefs"); + Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions"); + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_privacy_relayIntegration.js b/browser/components/preferences/tests/browser_privacy_relayIntegration.js new file mode 100644 index 0000000000..23d62f38bd --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_relayIntegration.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function initialState() { + // check pref permutations to verify the UI opens in the correct state + const prefTests = [ + { + initialPrefs: [ + ["signon.firefoxRelay.feature", undefined], + ["signon.rememberSignons", true], + ], + expected: "hidden", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "available"], + ["signon.rememberSignons", true], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "enabled"], + ["signon.rememberSignons", true], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "disabled"], + ["signon.rememberSignons", true], + ], + expected: "unchecked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", undefined], + ["signon.rememberSignons", false], + ], + expected: "hidden", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "available"], + ["signon.rememberSignons", false], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "enabled"], + ["signon.rememberSignons", false], + ], + expected: "checked", + }, + { + initialPrefs: [ + ["signon.firefoxRelay.feature", "disabled"], + ["signon.rememberSignons", false], + ], + expected: "unchecked", + }, + ]; + for (let test of prefTests) { + // set initial pref values + info("initialState, testing with: " + JSON.stringify(test)); + await SpecialPowers.pushPrefEnv({ set: test.initialPrefs }); + + // open about:privacy in a tab + // verify expected conditions + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + const doc = browser.contentDocument; + const relayGroup = doc.getElementById("relayIntegrationBox"); + const checkbox = doc.getElementById("relayIntegration"); + const savePasswords = doc.getElementById("savePasswords"); + doc.getElementById("passwordSettings").scrollIntoView(); + + Assert.equal( + checkbox.disabled, + !savePasswords.checked, + "#relayIntegration checkbox disabled when #passwordAutofillCheckbox is unchecked" + ); + + switch (test.expected) { + case "hidden": + is_element_hidden(relayGroup, "#relayIntegrationBox is hidden"); + break; + case "checked": + is_element_visible(relayGroup, "#relayIntegrationBox is visible"); + Assert.ok( + checkbox.checked, + "#relayIntegration checkbox is checked" + ); + break; + case "unchecked": + is_element_visible(relayGroup, "#relayIntegrationBox is visible"); + Assert.ok( + !checkbox.checked, + "#relayIntegration checkbox is un-checked" + ); + break; + default: + Assert.ok(false, "Unknown expected state: " + test.expected); + break; + } + } + ); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function toggleRelayIntegration() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.firefoxRelay.feature", "enabled"], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async browser => { + await SimpleTest.promiseFocus(browser); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + const doc = browser.contentDocument; + const relayCheckbox = doc.getElementById("relayIntegration"); + relayCheckbox.focus(); + Assert.equal(doc.activeElement, relayCheckbox, "checkbox is focused"); + Assert.equal( + relayCheckbox.checked, + true, + "#relayIntegration checkbox is not checked" + ); + + async function clickOnFeatureCheckbox( + expectedPrefValue, + expectedCheckValue, + message + ) { + const prefChanged = TestUtils.waitForPrefChange( + "signon.firefoxRelay.feature" + ); + EventUtils.synthesizeKey(" "); + await prefChanged; + Assert.equal( + Services.prefs.getStringPref("signon.firefoxRelay.feature"), + expectedPrefValue, + message + ); + Assert.equal( + relayCheckbox.checked, + expectedCheckValue, + `#relayIntegration checkbox is ${ + expectedCheckValue ? "checked" : "unchecked" + }` + ); + } + + await clickOnFeatureCheckbox( + "disabled", + false, + 'Turn integration off from "enabled" feature state' + ); + await clickOnFeatureCheckbox( + "available", + true, + 'Turn integration on from "enabled" feature state' + ); + await clickOnFeatureCheckbox( + "disabled", + false, + 'Turn integration off from "enabled" feature state' + ); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function toggleRememberSignon() { + // toggling rememberSignons checkbox should make generation checkbox disabled + SpecialPowers.pushPrefEnv({ + set: [ + ["signon.firefoxRelay.feature", "available"], + ["signon.rememberSignons", true], + ], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + const doc = browser.contentDocument; + const checkbox = doc.getElementById("savePasswords"); + const relayCheckbox = doc.getElementById("relayIntegration"); + + Assert.ok( + !relayCheckbox.disabled, + "generation checkbox is not initially disabled" + ); + + await SimpleTest.promiseFocus(browser); + const prefChanged = TestUtils.waitForPrefChange("signon.rememberSignons"); + + // the preferences "Search" bar obscures the checkbox if we scrollIntoView and try to click on it + // so use keyboard events instead + checkbox.focus(); + Assert.equal(doc.activeElement, checkbox, "checkbox is focused"); + EventUtils.synthesizeKey(" "); + + await prefChanged; + Assert.ok(!checkbox.checked, "#savePasswords checkbox is un-checked"); + Assert.ok( + relayCheckbox.disabled, + "Relay integration checkbox becomes disabled" + ); + } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function testLockedRelayPreference() { + // Locking relay preference should disable checkbox + Services.prefs.lockPref("signon.firefoxRelay.feature"); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:preferences#privacy", + }, + async function (browser) { + const doc = browser.contentDocument; + const relayCheckbox = doc.getElementById("relayIntegration"); + + Assert.ok(relayCheckbox.disabled, "Relay checkbox should be disabled"); + } + ); + + Services.prefs.unlockPref("signon.firefoxRelay.feature"); +}); diff --git a/browser/components/preferences/tests/browser_privacy_segmentation_pref.js b/browser/components/preferences/tests/browser_privacy_segmentation_pref.js new file mode 100644 index 0000000000..9b71d91e11 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_segmentation_pref.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the privacy segmentation pref and preferences UI. + +"use strict"; + +const PREF = "browser.dataFeatureRecommendations.enabled"; +const PREF_VISIBILITY = "browser.privacySegmentation.preferences.show"; + +add_task(async function test_preferences_section() { + if (!AppConstants.MOZ_DATA_REPORTING) { + ok(true, "Skipping test because data reporting is disabled"); + return; + } + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let section = doc.getElementById("privacySegmentationSection"); + let sectionHeader = section.querySelector("h2"); + let sectionDescription = section.querySelector("label"); + let radioGroup = section.querySelector( + "#privacyDataFeatureRecommendationRadioGroup" + ); + let radioEnabled = radioGroup.querySelector( + "#privacyDataFeatureRecommendationEnabled" + ); + let radioDisabled = radioGroup.querySelector( + "#privacyDataFeatureRecommendationDisabled" + ); + + for (let show of [false, true]) { + Services.prefs.setBoolPref(PREF_VISIBILITY, show); + let showStr = show ? "visible" : "hidden"; + + is( + BrowserTestUtils.is_visible(section), + show, + `Privacy Segmentation section should be ${showStr}.` + ); + is( + BrowserTestUtils.is_visible(sectionHeader), + show, + `Privacy Segmentation section header should be ${showStr}.` + ); + is( + BrowserTestUtils.is_visible(sectionDescription), + show, + `Privacy Segmentation section description should be ${showStr}.` + ); + is( + BrowserTestUtils.is_visible(radioGroup), + show, + `Privacy Segmentation radio group should be ${showStr}.` + ); + + // The section is visible, test radio buttons. + if (show) { + Services.prefs.setBoolPref(PREF, false); + + is( + radioGroup.value, + "false", + "Radio group should reflect initial pref state of false." + ); + + info("Selecting radio on."); + radioEnabled.click(); + is( + Services.prefs.getBoolPref(PREF), + true, + "Privacy Segmentation should be enabled." + ); + + info("Selecting radio off."); + radioDisabled.click(); + is( + Services.prefs.getBoolPref(PREF), + false, + "Privacy Segmentation should be disabled." + ); + + info("Updating pref externally"); + is( + radioGroup.value, + "false", + "Radio group should reflect initial pref state of false." + ); + Services.prefs.setBoolPref(PREF, true); + await BrowserTestUtils.waitForMutationCondition( + radioGroup, + { attributeFilter: ["value"] }, + () => radioGroup.value == "true" + ); + is( + radioGroup.value, + "true", + "Updating Privacy Segmentation pref also updates radio group." + ); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.prefs.clearUserPref(PREF_VISIBILITY); + Services.prefs.clearUserPref(PREF); +}); + +add_task(async function test_preferences_section_data_reporting_disabled() { + if (AppConstants.MOZ_DATA_REPORTING) { + ok(true, "Skipping test because data reporting is enabled"); + return; + } + + for (let show of [false, true]) { + Services.prefs.setBoolPref(PREF_VISIBILITY, show); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let section = doc.getElementById("privacySegmentationSection"); + is( + !!section, + show, + "Section should only exist when privacy segmentation section is enabled." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + + Services.prefs.clearUserPref(PREF_VISIBILITY); +}); diff --git a/browser/components/preferences/tests/browser_privacy_syncDataClearing.js b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js new file mode 100644 index 0000000000..d5b6b904ab --- /dev/null +++ b/browser/components/preferences/tests/browser_privacy_syncDataClearing.js @@ -0,0 +1,287 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * With no custom cleaning categories set and sanitizeOnShutdown disabled, + * the checkboxes "alwaysClear" and "deleteOnClose" should share the same state. + * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box. + */ +add_task(async function test_syncWithoutCustomPrefs() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected"); + + deleteOnCloseBox.click(); + + ok(deleteOnCloseBox.checked, "DeleteOnClose is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "DeleteOnClose sets alwaysClear in the same state, selected" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is set" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Downloads cleaning pref is not set" + ); + + deleteOnCloseBox.click(); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "DeleteOnclose sets alwaysClear in the same state, deselected" + ); + + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Downloads cleaning pref is not set" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Services.prefs.clearUserPref("privacy.clearOnShutdown.downloads"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.offlineApps"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cache"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.cookies"); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); +}); + +/* + * With custom cleaning category already set and SanitizeOnShutdown enabled, + * deselecting "deleteOnClose" should not change the state of "alwaysClear". + * The state of the cleaning categories cookies, cache and offlineApps should be in the state of the "deleteOnClose" box. + */ +add_task(async function test_syncWithCustomPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.history", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(alwaysClearBox.checked, "AlwaysClear initial state is selected"); + + deleteOnCloseBox.click(); + + ok(deleteOnCloseBox.checked, "DeleteOnClose is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, selected" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.history"), + "History cleaning pref is still set" + ); + + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is set" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is set" + ); + + deleteOnCloseBox.click(); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose is deselected"); + is( + !deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear is not synced with deleteOnClose, only deleteOnClose is deselected" + ); + + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Cookie cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Cache cleaning pref is reset" + ); + ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "OfflineApps cleaning pref is reset" + ); + ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.history"), + "History cleaning pref is still set" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +/* + * Setting/resetting cleaning prefs for cookies, cache, offline apps + * and selecting/deselecting the "alwaysClear" Box, also selects/deselects + * the "deleteOnClose" box. + */ + +add_task(async function test_syncWithCustomPrefs() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + let alwaysClearBox = document.getElementById("alwaysClear"); + + ok(!deleteOnCloseBox.checked, "DeleteOnClose initial state is deselected"); + ok(!alwaysClearBox.checked, "AlwaysClear initial state is deselected"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown.offlineApps", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + ok(alwaysClearBox.checked, "AlwaysClear is selected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, selected" + ); + + alwaysClearBox.click(); + + ok(!alwaysClearBox.checked, "AlwaysClear is deselected"); + is( + deleteOnCloseBox.checked, + alwaysClearBox.checked, + "AlwaysClear and deleteOnClose are in the same state, deselected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); + +/* + * On loading the page, the ClearOnClose box should be set according to the pref selection + */ +add_task(async function test_initialState() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", true], + ["privacy.clearOnShutdown.cache", true], + ["privacy.clearOnShutdown.offlineApps", true], + ["privacy.sanitize.sanitizeOnShutdown", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + let document = gBrowser.contentDocument; + let deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the prefs, selected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", false], + ["privacy.clearOnShutdown.cache", false], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.sanitize.sanitizeOnShutdown", true], + ["privacy.clearOnShutdown.history", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + document = gBrowser.contentDocument; + deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + !deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the prefs, deselected" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // When private browsing mode autostart is selected, the deleteOnClose Box is selected always + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.clearOnShutdown.cookies", false], + ["privacy.clearOnShutdown.cache", false], + ["privacy.clearOnShutdown.offlineApps", false], + ["privacy.sanitize.sanitizeOnShutdown", false], + ["browser.privatebrowsing.autostart", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + + document = gBrowser.contentDocument; + deleteOnCloseBox = document.getElementById("deleteOnClose"); + + ok( + deleteOnCloseBox.checked, + "DeleteOnClose is set accordingly to the private Browsing autostart pref, selected" + ); + + // Reset history mode + let historyMode = document.getElementById("historyMode"); + historyMode.value = "remember"; + historyMode.doCommand(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_privacypane_2.js b/browser/components/preferences/tests/browser_privacypane_2.js new file mode 100644 index 0000000000..46fb3347c8 --- /dev/null +++ b/browser/components/preferences/tests/browser_privacypane_2.js @@ -0,0 +1,19 @@ +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from privacypane_tests_perwindow.js */ +Services.scriptloader.loadSubScript( + rootDir + "privacypane_tests_perwindow.js", + this +); + +run_test_subset([ + test_pane_visibility, + test_dependent_elements, + test_dependent_cookie_elements, + test_dependent_clearonclose_elements, + test_dependent_prefs, +]); diff --git a/browser/components/preferences/tests/browser_privacypane_3.js b/browser/components/preferences/tests/browser_privacypane_3.js new file mode 100644 index 0000000000..e88a6059eb --- /dev/null +++ b/browser/components/preferences/tests/browser_privacypane_3.js @@ -0,0 +1,21 @@ +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from privacypane_tests_perwindow.js */ +Services.scriptloader.loadSubScript( + rootDir + "privacypane_tests_perwindow.js", + this +); + +run_test_subset([ + test_custom_retention("rememberHistory", "remember"), + test_custom_retention("rememberHistory", "custom"), + test_custom_retention("rememberForms", "custom"), + test_custom_retention("rememberForms", "custom"), + test_historymode_retention("remember", "custom"), + test_custom_retention("alwaysClear", "remember"), + test_custom_retention("alwaysClear", "custom"), +]); diff --git a/browser/components/preferences/tests/browser_proxy_backup.js b/browser/components/preferences/tests/browser_proxy_backup.js new file mode 100644 index 0000000000..fc05e19ada --- /dev/null +++ b/browser/components/preferences/tests/browser_proxy_backup.js @@ -0,0 +1,84 @@ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // network.proxy.type needs to be backed up and restored because mochitest + // changes this setting from the default + let oldNetworkProxyType = Services.prefs.getIntPref("network.proxy.type"); + registerCleanupFunction(function () { + Services.prefs.setIntPref("network.proxy.type", oldNetworkProxyType); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + for (let proxyType of ["http", "ssl", "socks"]) { + Services.prefs.clearUserPref("network.proxy." + proxyType); + Services.prefs.clearUserPref("network.proxy." + proxyType + "_port"); + if (proxyType == "http") { + continue; + } + Services.prefs.clearUserPref("network.proxy.backup." + proxyType); + Services.prefs.clearUserPref( + "network.proxy.backup." + proxyType + "_port" + ); + } + // On accepting the dialog, we also write TRR values, so we need to clear + // them. They are tested separately in browser_privacy_dnsoverhttps.js. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + }); + + let connectionURL = + "chrome://browser/content/preferences/dialogs/connection.xhtml"; + + // Set a shared proxy and an SSL backup + Services.prefs.setIntPref("network.proxy.type", 1); + Services.prefs.setBoolPref("network.proxy.share_proxy_settings", true); + Services.prefs.setCharPref("network.proxy.http", "example.com"); + Services.prefs.setIntPref("network.proxy.http_port", 1200); + Services.prefs.setCharPref("network.proxy.ssl", "example.com"); + Services.prefs.setIntPref("network.proxy.ssl_port", 1200); + Services.prefs.setCharPref("network.proxy.backup.ssl", "127.0.0.1"); + Services.prefs.setIntPref("network.proxy.backup.ssl_port", 9050); + + /* + The connection dialog alone won't save onaccept since it uses type="child", + so it has to be opened as a sub dialog of the main pref tab. + Open the main tab here. + */ + open_preferences(async function tabOpened(aContentWindow) { + is( + gBrowser.currentURI.spec, + "about:preferences", + "about:preferences loaded" + ); + let dialog = await openAndLoadSubDialog(connectionURL); + let dialogElement = dialog.document.getElementById("ConnectionsDialog"); + let dialogClosingPromise = BrowserTestUtils.waitForEvent( + dialogElement, + "dialogclosing" + ); + + ok(dialog, "connection window opened"); + dialogElement.acceptDialog(); + + let dialogClosingEvent = await dialogClosingPromise; + ok(dialogClosingEvent, "connection window closed"); + + // The SSL backup should not be replaced by the shared value + is( + Services.prefs.getCharPref("network.proxy.backup.ssl"), + "127.0.0.1", + "Shared proxy backup shouldn't be replaced" + ); + is( + Services.prefs.getIntPref("network.proxy.backup.ssl_port"), + 9050, + "Shared proxy port backup shouldn't be replaced" + ); + + gBrowser.removeCurrentTab(); + finish(); + }); +} diff --git a/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js b/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js new file mode 100644 index 0000000000..2a7cb2d10e --- /dev/null +++ b/browser/components/preferences/tests/browser_sanitizeOnShutdown_prefLocked.js @@ -0,0 +1,47 @@ +"use strict"; + +function switchToCustomHistoryMode(doc) { + // Select the last item in the menulist. + let menulist = doc.getElementById("historyMode"); + menulist.focus(); + EventUtils.sendKey("UP"); +} + +function testPrefStateMatchesLockedState() { + let win = gBrowser.contentWindow; + let doc = win.document; + switchToCustomHistoryMode(doc); + + let checkbox = doc.getElementById("alwaysClear"); + let preference = win.Preferences.get("privacy.sanitize.sanitizeOnShutdown"); + is( + checkbox.disabled, + preference.locked, + "Always Clear checkbox should be enabled when preference is not locked." + ); + + Services.prefs.clearUserPref("privacy.history.custom"); + gBrowser.removeCurrentTab(); +} + +add_setup(function () { + registerCleanupFunction(function resetPreferences() { + Services.prefs.unlockPref("privacy.sanitize.sanitizeOnShutdown"); + Services.prefs.clearUserPref("privacy.history.custom"); + }); +}); + +add_task(async function test_preference_enabled_when_unlocked() { + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + testPrefStateMatchesLockedState(); +}); + +add_task(async function test_preference_disabled_when_locked() { + Services.prefs.lockPref("privacy.sanitize.sanitizeOnShutdown"); + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + testPrefStateMatchesLockedState(); +}); diff --git a/browser/components/preferences/tests/browser_searchChangedEngine.js b/browser/components/preferences/tests/browser_searchChangedEngine.js new file mode 100644 index 0000000000..0882c9775e --- /dev/null +++ b/browser/components/preferences/tests/browser_searchChangedEngine.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +SearchTestUtils.init(this); + +function findRow(tree, expectedName) { + for (let i = 0; i < tree.view.rowCount; i++) { + let name = tree.view.getCellText( + i, + tree.columns.getNamedColumn("engineName") + ); + + if (name == expectedName) { + return i; + } + } + return -1; +} + +add_task(async function test_change_engine() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + + await SearchTestUtils.installSearchExtension({ + id: "example@tests.mozilla.org", + name: "Example", + version: "1.0", + keyword: "foo", + favicon_url: "img123.png", + }); + + let tree = doc.querySelector("#engineList"); + + let row = findRow(tree, "Example"); + + Assert.notEqual(row, -1, "Should have found the entry"); + Assert.ok( + tree.view + .getImageSrc(row, tree.columns.getNamedColumn("engineName")) + .includes("img123.png"), + "Should have the correct image URL" + ); + Assert.equal( + tree.view.getCellText(row, tree.columns.getNamedColumn("engineKeyword")), + "foo", + "Should show the correct keyword" + ); + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + await SearchTestUtils.installSearchExtension({ + id: "example@tests.mozilla.org", + name: "Example 2", + version: "2.0", + keyword: "bar", + favicon_url: "img456.png", + }); + await updatedPromise; + + row = findRow(tree, "Example 2"); + + Assert.notEqual(row, -1, "Should have found the updated entry"); + Assert.ok( + tree.view + .getImageSrc(row, tree.columns.getNamedColumn("engineName")) + .includes("img456.png"), + "Should have the correct image URL" + ); + Assert.equal( + tree.view.getCellText(row, tree.columns.getNamedColumn("engineKeyword")), + "bar", + "Should show the correct keyword" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchDefaultEngine.js b/browser/components/preferences/tests/browser_searchDefaultEngine.js new file mode 100644 index 0000000000..e64f88fab3 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchDefaultEngine.js @@ -0,0 +1,372 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); + +SearchTestUtils.init(this); + +add_setup(async function () { + await SearchTestUtils.installSearchExtension({ + name: "engine1", + search_url: "https://example.com/engine1", + search_url_get_params: "search={searchTerms}", + }); + await SearchTestUtils.installSearchExtension({ + name: "engine2", + search_url: "https://example.com/engine2", + search_url_get_params: "search={searchTerms}", + }); + + const defaultEngine = await Services.search.getDefault(); + const defaultPrivateEngine = await Services.search.getDefaultPrivate(); + + registerCleanupFunction(async () => { + await Services.search.setDefault( + defaultEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + await Services.search.setDefaultPrivate( + defaultPrivateEngine, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + }); +}); + +add_task(async function test_openWithPrivateDefaultNotEnabledFirst() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", false], + ["browser.search.separatePrivateDefault", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const separateEngineCheckbox = doc.getElementById( + "browserSeparateDefaultEngine" + ); + const privateDefaultVbox = doc.getElementById( + "browserPrivateEngineSelection" + ); + + Assert.ok( + separateEngineCheckbox.hidden, + "Should have hidden the separate search engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should have hidden the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", true]], + }); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should have displayed the separate search engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should not have displayed the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", true]], + }); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should still be displaying the separate search engine checkbox" + ); + Assert.ok( + !privateDefaultVbox.hidden, + "Should have displayed the private engine selection box" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_openWithPrivateDefaultEnabledFirst() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const separateEngineCheckbox = doc.getElementById( + "browserSeparateDefaultEngine" + ); + const privateDefaultVbox = doc.getElementById( + "browserPrivateEngineSelection" + ); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should not have hidden the separate search engine checkbox" + ); + Assert.ok( + !privateDefaultVbox.hidden, + "Should not have hidden the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", false]], + }); + + Assert.ok( + !separateEngineCheckbox.hidden, + "Should not have hidden the separate search engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should have hidden the private engine selection box" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", false]], + }); + + Assert.ok( + separateEngineCheckbox.hidden, + "Should have hidden the separate private engine checkbox" + ); + Assert.ok( + privateDefaultVbox.hidden, + "Should still be hiding the private engine selection box" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_separatePrivateDefault() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const separateEngineCheckbox = doc.getElementById( + "browserSeparateDefaultEngine" + ); + const privateDefaultVbox = doc.getElementById( + "browserPrivateEngineSelection" + ); + + Assert.ok( + privateDefaultVbox.hidden, + "Should not be displaying the private engine selection box" + ); + + separateEngineCheckbox.checked = false; + separateEngineCheckbox.doCommand(); + + Assert.ok( + Services.prefs.getBoolPref("browser.search.separatePrivateDefault"), + "Should have correctly set the pref" + ); + + Assert.ok( + !privateDefaultVbox.hidden, + "Should be displaying the private engine selection box" + ); + + separateEngineCheckbox.checked = true; + separateEngineCheckbox.doCommand(); + + Assert.ok( + !Services.prefs.getBoolPref("browser.search.separatePrivateDefault"), + "Should have correctly turned the pref off" + ); + + Assert.ok( + privateDefaultVbox.hidden, + "Should have hidden the private engine selection box" + ); + + gBrowser.removeCurrentTab(); +}); + +async function setDefaultEngine( + testPrivate, + currentEngineName, + expectedEngineName +) { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const defaultEngineSelector = doc.getElementById( + testPrivate ? "defaultPrivateEngine" : "defaultEngine" + ); + + Assert.equal( + defaultEngineSelector.selectedItem.engine.name, + currentEngineName, + "Should have the correct engine as default on first open" + ); + + const popup = defaultEngineSelector.menupopup; + const popupShown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + defaultEngineSelector, + {}, + defaultEngineSelector.ownerGlobal + ); + await popupShown; + + const items = Array.from(popup.children); + const engine2Item = items.find( + item => item.engine.name == expectedEngineName + ); + + const defaultChanged = SearchTestUtils.promiseSearchNotification( + testPrivate ? "engine-default-private" : "engine-default", + "browser-search-engine-modified" + ); + // Waiting for popupHiding here seemed to cause a race condition, however + // as we're really just interested in the notification, we'll just use + // that here. + EventUtils.synthesizeMouseAtCenter(engine2Item, {}, engine2Item.ownerGlobal); + await defaultChanged; + + const newDefault = testPrivate + ? await Services.search.getDefaultPrivate() + : await Services.search.getDefault(); + Assert.equal( + newDefault.name, + expectedEngineName, + "Should have changed the default engine to engine2" + ); +} + +add_task(async function test_setDefaultEngine() { + const engine1 = Services.search.getEngineByName("engine1"); + + // Set an initial default so we have a known engine. + await Services.search.setDefault( + engine1, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + + await setDefaultEngine(false, "engine1", "engine2"); + + TelemetryTestUtils.assertEvents( + [ + { + object: "change_default", + value: "user", + extra: { + prev_id: engine1.telemetryId, + new_id: "other-engine2", + new_name: "engine2", + new_load_path: "[addon]engine2@tests.mozilla.org", + new_sub_url: "", + }, + }, + ], + { category: "search", method: "engine" } + ); + + let snapshot = await Glean.searchEngineDefault.changed.testGetValue(); + delete snapshot[0].timestamp; + Assert.deepEqual( + snapshot[0], + { + category: "search.engine.default", + name: "changed", + extra: { + change_source: "user", + previous_engine_id: engine1.telemetryId, + new_engine_id: "other-engine2", + new_display_name: "engine2", + new_load_path: "[addon]engine2@tests.mozilla.org", + new_submission_url: "", + }, + }, + "Should have received the correct event details" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_setPrivateDefaultEngine() { + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ], + }); + + const engine2 = Services.search.getEngineByName("engine2"); + + // Set an initial default so we have a known engine. + await Services.search.setDefaultPrivate( + engine2, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); + + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + + await setDefaultEngine(true, "engine2", "engine1"); + + TelemetryTestUtils.assertEvents( + [ + { + object: "change_private", + value: "user", + extra: { + prev_id: engine2.telemetryId, + new_id: "other-engine1", + new_name: "engine1", + new_load_path: "[addon]engine1@tests.mozilla.org", + new_sub_url: "", + }, + }, + ], + { category: "search", method: "engine" } + ); + + let snapshot = await Glean.searchEnginePrivate.changed.testGetValue(); + delete snapshot[0].timestamp; + console.log(snapshot); + Assert.deepEqual( + snapshot[0], + { + category: "search.engine.private", + name: "changed", + extra: { + change_source: "user", + previous_engine_id: engine2.telemetryId, + new_engine_id: "other-engine1", + new_display_name: "engine1", + new_load_path: "[addon]engine1@tests.mozilla.org", + new_submission_url: "", + }, + }, + "Should have received the correct event details" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchFindMoreLink.js b/browser/components/preferences/tests/browser_searchFindMoreLink.js new file mode 100644 index 0000000000..92864c9f54 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchFindMoreLink.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = "https://example.org/"; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.searchEnginesURL", TEST_URL]], + }); +}); + +add_task(async function test_click_find_more_link() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser); + + gBrowser.selectedBrowser.contentDocument + .getElementById("addEngines") + .scrollIntoView(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#addEngines", + {}, + gBrowser.selectedBrowser.browsingContext + ); + + let tab = await promiseNewTab; + Assert.equal( + tab.linkedBrowser.documentURI.spec, + TEST_URL, + "Should have loaded the expected page" + ); + + // Close both tabs. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchRestoreDefaults.js b/browser/components/preferences/tests/browser_searchRestoreDefaults.js new file mode 100644 index 0000000000..80588d0cce --- /dev/null +++ b/browser/components/preferences/tests/browser_searchRestoreDefaults.js @@ -0,0 +1,259 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); +add_task(async function test_restore_functionality() { + // Ensure no engines are hidden to begin with. + for (let engine of await Services.search.getAppProvidedEngines()) { + if (engine.hidden) { + engine.hidden = false; + } + } + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + + Assert.ok( + restoreDefaultsButton.disabled, + "Should have disabled the restore default search engines button on open" + ); + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + + let tree = doc.querySelector("#engineList"); + // Check for default search engines to be displayed in the engineList + let defaultEngines = await Services.search.getAppProvidedEngines(); + for (let i = 0; i < defaultEngines.length; i++) { + let cellName = tree.view.getCellText( + i, + tree.columns.getNamedColumn("engineName") + ); + if (cellName == "DuckDuckGo") { + tree.view.selection.select(i); + break; + } + } + doc.getElementById("removeEngineButton").click(); + await updatedPromise; + + let engine = await Services.search.getEngineByName("DuckDuckGo"); + + Assert.ok(engine.hidden, "Should have hidden the engine"); + Assert.ok( + !restoreDefaultsButton.disabled, + "Should have enabled the restore default search engines button" + ); + + updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + // Let the stack unwind so that the restore defaults button can update its + // state. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + Assert.ok(!engine.hidden, "Should have re-enabled the disabled engine"); + Assert.ok( + restoreDefaultsButton.disabled, + "Should have disabled the restore default search engines button after use" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_restoreEnabledOnOpenWithEngineHidden() { + let engine = await Services.search.getEngineByName("DuckDuckGo"); + engine.hidden = true; + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + + Assert.ok( + !restoreDefaultsButton.disabled, + "Should have enabled the restore default search engines button on open" + ); + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + // Let the stack unwind so that the restore defaults button can update its + // state. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + Assert.ok(!engine.hidden, "Should have re-enabled the disabled engine"); + Assert.ok( + restoreDefaultsButton.disabled, + "Should have disabled the restore default search engines button after use" + ); + + gBrowser.removeCurrentTab(); +}); + +// This removes the last two engines and then the remaining engines from top to +// bottom, and then it restores the default engines. See bug 1681818. +add_task(async function test_removeOutOfOrder() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + Assert.ok( + restoreDefaultsButton.disabled, + "The restore-defaults button is disabled initially" + ); + + let tree = doc.querySelector("#engineList"); + let removeEngineButton = doc.getElementById("removeEngineButton"); + removeEngineButton.scrollIntoView(); + + let defaultEngines = await Services.search.getAppProvidedEngines(); + + // Remove the last two engines. After each removal, the selection should move + // to the first local shortcut. + for (let i = 0; i < 2; i++) { + tree.view.selection.select(defaultEngines.length - i - 1); + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + Assert.ok( + removeEngineButton.disabled, + "The remove-engine button is disabled because a local shortcut is selected" + ); + Assert.ok( + !restoreDefaultsButton.disabled, + "The restore-defaults button is enabled after removing an engine" + ); + } + + // Remove the remaining engines from top to bottom except for the default engine + // which can't be removed. + for (let i = 0; i < defaultEngines.length - 3; i++) { + tree.view.selection.select(0); + + if (defaultEngines[0].name == Services.search.defaultEngine.name) { + tree.view.selection.select(1); + } + + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + Assert.ok( + !restoreDefaultsButton.disabled, + "The restore-defaults button is enabled after removing an engine" + ); + } + Assert.ok( + removeEngineButton.disabled, + "The remove-engine button is disabled because only one engine remains" + ); + + // Click the restore-defaults button. + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + + // Wait for the restore-defaults button to update its state. + await TestUtils.waitForCondition( + () => restoreDefaultsButton.disabled, + "Waiting for the restore-defaults button to become disabled" + ); + + Assert.ok( + restoreDefaultsButton.disabled, + "The restore-defaults button is disabled after restoring defaults" + ); + Assert.equal( + tree.view.rowCount, + defaultEngines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "All engines are restored" + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_removeAndRestoreMultiple() { + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let restoreDefaultsButton = doc.getElementById("restoreDefaultSearchEngines"); + let tree = doc.querySelector("#engineList"); + let removeEngineButton = doc.getElementById("removeEngineButton"); + removeEngineButton.scrollIntoView(); + + let defaultEngines = await Services.search.getAppProvidedEngines(); + + // Remove the second and fourth engines. + for (let i = 0; i < 2; i++) { + tree.view.selection.select(i * 2 + 1); + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + } + + // Click the restore-defaults button. + let updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + + // Remove the third engine. + tree.view.selection.select(3); + updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + removeEngineButton.click(); + await updatedPromise; + + // Now restore again. + updatedPromise = SearchTestUtils.promiseSearchNotification( + SearchUtils.MODIFIED_TYPE.CHANGED, + SearchUtils.TOPIC_ENGINE_MODIFIED + ); + restoreDefaultsButton.click(); + await updatedPromise; + + // Wait for the restore-defaults button to update its state. + await TestUtils.waitForCondition( + () => restoreDefaultsButton.disabled, + "Waiting for the restore-defaults button to become disabled" + ); + + Assert.equal( + tree.view.rowCount, + defaultEngines.length + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "Should have the correct amount of engines" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchScroll.js b/browser/components/preferences/tests/browser_searchScroll.js new file mode 100644 index 0000000000..ef3af646e9 --- /dev/null +++ b/browser/components/preferences/tests/browser_searchScroll.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); +const { SearchUtils } = ChromeUtils.importESModule( + "resource://gre/modules/SearchUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); +SearchTestUtils.init(this); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.w3c_touch_events.enabled", 0]], + }); +}); + +add_task(async function test_scroll() { + info("Open preferences page for search"); + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + const doc = gBrowser.selectedBrowser.contentDocument; + const tree = doc.querySelector("#engineList"); + + info("Add engines to make the tree scrollable"); + for (let i = 0, n = parseInt(tree.getAttribute("rows")); i < n; i++) { + let extension = await SearchTestUtils.installSearchExtension({ + id: `${i}@tests.mozilla.org`, + name: `example${i}`, + version: "1.0", + keyword: `example${i}`, + }); + await AddonTestUtils.waitForSearchProviderStartup(extension); + } + + info("Make tree element move into viewport"); + const mainContent = doc.querySelector(".main-content"); + const readyForScrollIntoView = new Promise(r => { + mainContent.addEventListener("scroll", r, { once: true }); + }); + tree.scrollIntoView(); + await readyForScrollIntoView; + + const previousScroll = mainContent.scrollTop; + + await promiseMoveMouseAndScrollWheelOver(tree, 1, 1, false); + + Assert.equal( + previousScroll, + mainContent.scrollTop, + "Container element does not scroll" + ); + + info("Clean up"); + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js new file mode 100644 index 0000000000..33ab63e8bf --- /dev/null +++ b/browser/components/preferences/tests/browser_searchShowSuggestionsFirst.js @@ -0,0 +1,240 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const MAIN_PREF = "browser.search.suggest.enabled"; +const URLBAR_PREF = "browser.urlbar.suggest.searches"; +const FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst"; +const FIRST_CHECKBOX_ID = "showSearchSuggestionsFirstCheckbox"; + +add_setup(async function () { + // Make sure the main and urlbar suggestion prefs are enabled. + await SpecialPowers.pushPrefEnv({ + set: [ + [MAIN_PREF, true], + [URLBAR_PREF, true], + ], + }); +}); + +// Open preferences with search suggestions shown first (the default). +add_task(async function openWithSearchSuggestionsShownFirst() { + // Initially the pref should be true so search suggestions are shown first. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should be true initially" + ); + + // Open preferences. The checkbox should be checked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(FIRST_CHECKBOX_ID); + Assert.ok(checkbox.checked, "Checkbox should be checked"); + Assert.ok(!checkbox.disabled, "Checkbox should be enabled"); + + // Uncheck the checkbox. + checkbox.checked = false; + checkbox.doCommand(); + + // The pref should now be false so that history is shown first. + Assert.ok( + !Services.prefs.getBoolPref(FIRST_PREF), + "Pref should now be false to show history first" + ); + + // Make sure the checkbox state didn't change. + Assert.ok(!checkbox.checked, "Checkbox should remain unchecked"); + Assert.ok(!checkbox.disabled, "Checkbox should remain enabled"); + + // Clear the pref. + Services.prefs.clearUserPref(FIRST_PREF); + + // The checkbox should have become checked again. + Assert.ok( + checkbox.checked, + "Checkbox should become checked after clearing pref" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after clearing pref" + ); + + // Clean up. + gBrowser.removeCurrentTab(); +}); + +// Open preferences with history shown first. +add_task(async function openWithHistoryShownFirst() { + // Set the pref to show history first. + Services.prefs.setBoolPref(FIRST_PREF, false); + + // Open preferences. The checkbox should be unchecked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(FIRST_CHECKBOX_ID); + Assert.ok(!checkbox.checked, "Checkbox should be unchecked"); + Assert.ok(!checkbox.disabled, "Checkbox should be enabled"); + + // Check the checkbox. + checkbox.checked = true; + checkbox.doCommand(); + + // Make sure the checkbox state didn't change. + Assert.ok(checkbox.checked, "Checkbox should remain checked"); + Assert.ok(!checkbox.disabled, "Checkbox should remain enabled"); + + // The pref should now be true so that search suggestions are shown first. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should now be true to show search suggestions first" + ); + + // Set the pref to false again. + Services.prefs.setBoolPref(FIRST_PREF, false); + + // The checkbox should have become unchecked again. + Assert.ok( + !checkbox.checked, + "Checkbox should become unchecked after setting pref to false" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after setting pref to false" + ); + + // Clean up. + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref(FIRST_PREF); +}); + +// Checks how the show-suggestions-first pref and checkbox reacts to updates to +// URLBAR_PREF and MAIN_PREF. +add_task(async function superprefInteraction() { + // Initially the pref should be true so search suggestions are shown first. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should be true initially" + ); + + // Open preferences. The checkbox should be checked. + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById(FIRST_CHECKBOX_ID); + Assert.ok(checkbox.checked, "Checkbox should be checked"); + Assert.ok(!checkbox.disabled, "Checkbox should be enabled"); + + // Two superior prefs control the show-suggestion-first pref: URLBAR_PREF and + // MAIN_PREF. Toggle each and make sure the show-suggestion-first checkbox + // reacts appropriately. + for (let superiorPref of [URLBAR_PREF, MAIN_PREF]) { + info(`Testing superior pref ${superiorPref}`); + + // Set the superior pref to false. + Services.prefs.setBoolPref(superiorPref, false); + + // The pref should remain true. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain true" + ); + + // The checkbox should have become unchecked and disabled. + Assert.ok( + !checkbox.checked, + "Checkbox should become unchecked after disabling urlbar suggestions" + ); + Assert.ok( + checkbox.disabled, + "Checkbox should become disabled after disabling urlbar suggestions" + ); + + // Set the superior pref to true. + Services.prefs.setBoolPref(superiorPref, true); + + // The pref should remain true. + Assert.ok( + Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain true" + ); + + // The checkbox should have become checked and enabled again. + Assert.ok( + checkbox.checked, + "Checkbox should become checked after re-enabling urlbar suggestions" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should become enabled after re-enabling urlbar suggestions" + ); + + // Set the pref to false. + Services.prefs.setBoolPref(FIRST_PREF, false); + + // The checkbox should have become unchecked. + Assert.ok( + !checkbox.checked, + "Checkbox should become unchecked after setting pref to false" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after setting pref to false" + ); + + // Set the superior pref to false again. + Services.prefs.setBoolPref(superiorPref, false); + + // The pref should remain false. + Assert.ok( + !Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain false" + ); + + // The checkbox should remain unchecked and become disabled. + Assert.ok( + !checkbox.checked, + "Checkbox should remain unchecked after disabling urlbar suggestions" + ); + Assert.ok( + checkbox.disabled, + "Checkbox should become disabled after disabling urlbar suggestions" + ); + + // Set the superior pref to true. + Services.prefs.setBoolPref(superiorPref, true); + + // The pref should remain false. + Assert.ok( + !Services.prefs.getBoolPref(FIRST_PREF), + "Pref should remain false" + ); + + // The checkbox should remain unchecked and become enabled. + Assert.ok( + !checkbox.checked, + "Checkbox should remain unchecked after re-enabling urlbar suggestions" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should become enabled after re-enabling urlbar suggestions" + ); + + // Finally, set the pref back to true. + Services.prefs.setBoolPref(FIRST_PREF, true); + + // The checkbox should have become checked. + Assert.ok( + checkbox.checked, + "Checkbox should become checked after setting pref back to true" + ); + Assert.ok( + !checkbox.disabled, + "Checkbox should remain enabled after setting pref back to true" + ); + } + + // Clean up. + gBrowser.removeCurrentTab(); + + Services.prefs.clearUserPref(FIRST_PREF); + Services.prefs.clearUserPref(URLBAR_PREF); + Services.prefs.clearUserPref(MAIN_PREF); +}); diff --git a/browser/components/preferences/tests/browser_search_no_results_change_category.js b/browser/components/preferences/tests/browser_search_no_results_change_category.js new file mode 100644 index 0000000000..131492632e --- /dev/null +++ b/browser/components/preferences/tests/browser_search_no_results_change_category.js @@ -0,0 +1,44 @@ +"use strict"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + let query = "ffff____noresults____ffff"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let noResultsEl = gBrowser.contentDocument.querySelector( + "#no-results-message" + ); + is_element_visible( + noResultsEl, + "Should be reporting no results for this query" + ); + + await gBrowser.contentWindow.gotoPref("panePrivacy"); + is_element_hidden( + noResultsEl, + "Should not be showing the 'no results' message after selecting a category" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_searchTerms.js b/browser/components/preferences/tests/browser_search_searchTerms.js new file mode 100644 index 0000000000..0af0591355 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_searchTerms.js @@ -0,0 +1,201 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + Tests the showSearchTerms option on the about:preferences#search page. +*/ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { + const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/QuickSuggestTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +const GROUP_ID = "searchbarGroup"; +const CHECKBOX_ID = "searchShowSearchTermCheckbox"; +const PREF_SEARCHTERMS = "browser.urlbar.showSearchTerms.enabled"; +const PREF_FEATUREGATE = "browser.urlbar.showSearchTerms.featureGate"; + +/* + If Nimbus experiment is enabled, check option visibility. +*/ +add_task(async function showSearchTermsVisibility_experiment_beforeOpen() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, false]], + }); + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + showSearchTermsFeatureGate: true, + }, + callback: async () => { + await openPreferencesViaOpenPreferencesAPI("search", { + leaveOpen: true, + }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CHECKBOX_ID); + Assert.ok( + BrowserTestUtils.is_visible(container), + "The option box is visible" + ); + gBrowser.removeCurrentTab(); + }, + }); + await SpecialPowers.popPrefEnv(); +}); + +/* + If Nimbus experiment is not enabled initially but eventually enabled, + check option visibility on Preferences page. +*/ +add_task(async function showSearchTermsVisibility_experiment_afterOpen() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, false]], + }); + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let container = doc.getElementById(CHECKBOX_ID); + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The option box is initially hidden." + ); + + // Install experiment. + await QuickSuggestTestUtils.withExperiment({ + valueOverrides: { + showSearchTermsFeatureGate: true, + }, + callback: async () => { + Assert.ok( + BrowserTestUtils.is_visible(container), + "The option box is visible" + ); + }, + }); + + Assert.ok( + BrowserTestUtils.is_hidden(container), + "The option box is hidden again after the experiment is uninstalled." + ); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/* + Check using the checkbox modifies the preference. +*/ +add_task(async function showSearchTerms_checkbox() { + // Enable the feature. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, true]], + }); + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + doc.getElementById(GROUP_ID).scrollIntoView(); + + let option = doc.getElementById(CHECKBOX_ID); + + // Evaluate checkbox pref is true. + Assert.ok(option.checked, "Option box should be checked."); + + // Evaluate checkbox when pref is false. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_SEARCHTERMS, false]], + }); + Assert.ok(!option.checked, "Option box should not be checked."); + await SpecialPowers.popPrefEnv(); + + // Evaluate pref when checkbox is un-checked. + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + CHECKBOX_ID, + {}, + gBrowser.selectedBrowser + ); + Assert.equal( + Services.prefs.getBoolPref(PREF_SEARCHTERMS), + false, + "Preference should be false if un-checked." + ); + + // Evaluate pref when checkbox is checked. + await BrowserTestUtils.synthesizeMouseAtCenter( + "#" + CHECKBOX_ID, + {}, + gBrowser.selectedBrowser + ); + Assert.equal( + Services.prefs.getBoolPref(PREF_SEARCHTERMS), + true, + "Preference should be true if checked." + ); + + // Clean-up. + Services.prefs.clearUserPref(PREF_SEARCHTERMS); + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/* + When loading the search preferences panel, the + showSearchTerms checkbox should be disabled if + the search bar is enabled. +*/ +add_task(async function showSearchTerms_and_searchBar_preference_load() { + // Enable the feature. + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_FEATUREGATE, true], + ["browser.search.widget.inNavBar", true], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let checkbox = doc.getElementById(CHECKBOX_ID); + Assert.ok( + checkbox.disabled, + "showSearchTerms checkbox should be disabled when search bar is enabled." + ); + + // Clean-up. + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); + +/* + If the search bar is enabled while the search + preferences panel is open, the showSearchTerms + checkbox should not be clickable. +*/ +add_task(async function showSearchTerms_and_searchBar_preference_change() { + // Enable the feature. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_FEATUREGATE, true]], + }); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + + let checkbox = doc.getElementById(CHECKBOX_ID); + Assert.ok(!checkbox.disabled, "showSearchTerms checkbox should be enabled."); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.widget.inNavBar", true]], + }); + Assert.ok( + checkbox.disabled, + "showSearchTerms checkbox should be disabled when search bar is enabled." + ); + + // Clean-up. + await SpecialPowers.popPrefEnv(); + Assert.ok(!checkbox.disabled, "showSearchTerms checkbox should be enabled."); + + gBrowser.removeCurrentTab(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js b/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js new file mode 100644 index 0000000000..8235f8dd19 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialog_tooltip_saved_addresses.js @@ -0,0 +1,39 @@ +"use strict"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +add_task(async function test_show_search_term_tooltip_in_subdialog() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let keyword = "organization"; + await runSearchInput(keyword); + + let formAutofillGroupBox = gBrowser.contentDocument.getElementById( + "formAutofillGroupBox" + ); + let savedAddressesButton = + formAutofillGroupBox.querySelector(".accessory-button"); + + info("Clicking saved addresses button to open subdialog"); + savedAddressesButton.click(); + info("Waiting for addresses subdialog to appear"); + await BrowserTestUtils.waitForCondition(() => { + let dialogBox = gBrowser.contentDocument.querySelector(".dialogBox"); + return !!dialogBox; + }); + let tooltip = gBrowser.contentDocument.querySelector(".search-tooltip"); + + is_element_visible( + tooltip, + "Tooltip with search term should be visible in subdialog" + ); + is(tooltip.textContent, keyword, "Tooltip should have correct search term"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js new file mode 100644 index 0000000000..e676de0a6c --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_1.js @@ -0,0 +1,48 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Set Home Page" subdialog. + */ +add_task(async function () { + // Set custom URL so bookmark button will be shown on the page (otherwise it is hidden) + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.startup.homepage", "about:robots"], + ["browser.startup.page", 1], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("paneHome", { leaveOpen: true }); + + // Wait for Activity Stream to add its panels + await BrowserTestUtils.waitForCondition(() => + SpecialPowers.spawn( + gBrowser.selectedTab.linkedBrowser, + [], + () => !!content.document.getElementById("homeContentsGroup") + ) + ); + + await evaluateSearchResults("Set Home Page", "homepageGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Languages" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Choose languages", "languagesGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js new file mode 100644 index 0000000000..6ff05dee29 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_2.js @@ -0,0 +1,36 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Saved Logins" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("sites are stored", "passwordsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Exceptions - Enhanced Tracking Protection" subdialog: + * "You can specify which websites have Enhanced Tracking Protection turned off." #permissions-exceptions-manage-etp-desc + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults( + "Enhanced Tracking Protection turned off", + "trackingGroup" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js new file mode 100644 index 0000000000..45f8774d73 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_3.js @@ -0,0 +1,35 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Allowed Sites - Add-ons Installation" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("allowed to install add-ons", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Certificate Manager" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults( + "identify these certificate authorities", + "certSelection" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js new file mode 100644 index 0000000000..9cfcb51f1b --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_4.js @@ -0,0 +1,39 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Update History" subdialog. + */ +add_task(async function () { + // The updates panel is disabled in MSIX builds. + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + return; + } + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("updates have been installed", "updateApp"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Location Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("location permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js new file mode 100644 index 0000000000..9a4ae09696 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_5.js @@ -0,0 +1,46 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(2); + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Fonts" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + // Oh, Canada: + await evaluateSearchResults("Unified Canadian Syllabary", "fontsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Colors" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Link Colors", "colorsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Exceptions - Saved Logins" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("sites will not be saved", "passwordsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js new file mode 100644 index 0000000000..1091dd2dd4 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_6.js @@ -0,0 +1,35 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Block Lists" subdialog. + */ +add_task(async function () { + async function doTest() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("block online trackers", "trackingGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + await doTest(); +}); + +/** + * Test for searching for the "Allowed Sites - Pop-ups" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("open pop-up windows", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js new file mode 100644 index 0000000000..1c4a923f06 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_7.js @@ -0,0 +1,34 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(2); + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Device Manager" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Security Modules and Devices", "certSelection"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Connection Settings" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("Use system proxy settings", "connectionGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js new file mode 100644 index 0000000000..9827e89239 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_8.js @@ -0,0 +1,45 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(2); + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Camera Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("camera permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Microphone Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("microphone permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for searching for the "Notification Permissions" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("notification permissions", "permissionsGroup"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js new file mode 100644 index 0000000000..b7ca239358 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_subdialogs_within_preferences_site_data.js @@ -0,0 +1,45 @@ +/* + * This file contains tests for the Preferences search bar. + */ + +// Enabling Searching functionatily. Will display search bar form this testcase forward. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test for searching for the "Settings - Site Data" subdialog. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("cookies", ["siteDataGroup", "trackingGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("site data", ["siteDataGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("cache", ["siteDataGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + await evaluateSearchResults("cross-site", ["trackingGroup"]); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_within_preferences_1.js b/browser/components/preferences/tests/browser_search_within_preferences_1.js new file mode 100644 index 0000000000..504191ecb5 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_1.js @@ -0,0 +1,344 @@ +"use strict"; +/** + * This file contains tests for the Preferences search bar. + */ + +requestLongerTimeout(6); + +/** + * Tests to see if search bar is being shown when pref is turned on + */ +add_task(async function show_search_bar_when_pref_is_enabled() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + is_element_visible(searchInput, "Search box should be shown"); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for "Search Result" panel. + * After it runs a search, it tests if the "Search Results" panel is the only selected category. + * The search is then cleared, it then tests if the "General" panel is the only selected category. + */ +add_task(async function show_search_results_pane_only_then_revert_to_general() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "password"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let categoriesList = gBrowser.contentDocument.getElementById("categories"); + + for (let i = 0; i < categoriesList.childElementCount; i++) { + let child = categoriesList.itemChildren[i]; + is(child.selected, false, "No other panel should be selected"); + } + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + // Checks if back to generalPane + for (let i = 0; i < categoriesList.childElementCount; i++) { + let child = categoriesList.itemChildren[i]; + if (child.id == "category-general") { + is(child.selected, true, "General panel should be selected"); + } else if (child.id) { + is(child.selected, false, "No other panel should be selected"); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for "password" case. When we search "password", it should show the "passwordGroup" + */ +add_task(async function search_for_password_show_passwordGroup() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "password"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); + + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if ( + child.id == "passwordsGroup" || + child.id == "weavePrefsDeck" || + child.id == "header-searchResults" || + child.id == "certSelection" || + child.id == "connectionGroup" || + child.id == "dataMigrationGroup" + ) { + is_element_visible(child, "Should be in search results"); + } else if (child.id) { + is_element_hidden(child, "Should not be in search results"); + } + } + + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + let expectedChildren = [ + "paneGeneral", + "startupGroup", + "languagesGroup", + "webAppearanceGroup", + "colorsGroup", + "fontsGroup", + "zoomGroup", + "downloadsGroup", + "applicationsGroup", + "drmGroup", + "browsingGroup", + "performanceGroup", + "connectionGroup", + "generalCategory", + "languageAndAppearanceCategory", + "filesAndApplicationsCategory", + "performanceCategory", + "browsingCategory", + "networkProxyCategory", + "dataMigrationGroup", + "translationsGroup", + ]; + // Only visible for non-MSIX builds + if ( + AppConstants.platform !== "win" || + !Services.sysinfo.getProperty("hasWinPackageId", false) + ) { + expectedChildren.push("updatesCategory"); + expectedChildren.push("updateApp"); + } + // Checks if back to generalPane + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if (expectedChildren.includes(child.id)) { + is_element_visible(child, `Should be in general tab: ${child.id}`); + } else if (child.id) { + is_element_hidden(child, `Should not be in general tab: ${child.id}`); + } + } + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for if nothing is found + */ +add_task(async function search_with_nothing_found() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + let noResultsEl = gBrowser.contentDocument.querySelector( + "#no-results-message" + ); + let sorryMsgQueryEl = gBrowser.contentDocument.getElementById( + "sorry-message-query" + ); + + is_element_hidden(noResultsEl, "Should not be in search results yet"); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "coach"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + is_element_visible(noResultsEl, "Should be in search results"); + is( + sorryMsgQueryEl.textContent, + query, + "sorry-message-query should contain the query" + ); + + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + is_element_hidden(noResultsEl, "Should not be in search results"); + is( + sorryMsgQueryEl.textContent.length, + 0, + "sorry-message-query should be empty" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for if we go back to general tab after search case + */ +add_task(async function exiting_search_reverts_to_general_pane() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let generalPane = gBrowser.contentDocument.getElementById("generalCategory"); + + is_element_hidden(generalPane, "Should not be in general"); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "password"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + // Takes search off + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + let count = query.length; + while (count--) { + EventUtils.sendKey("BACK_SPACE"); + } + await searchCompletedPromise; + + // Checks if back to normal + is_element_visible(generalPane, "Should be in generalPane"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test for if we go to another tab after searching + */ +add_task(async function changing_tabs_after_searching() { + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "permission"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + // Search header should be shown for the permissions group + let permissionsSearchHeader = gBrowser.contentDocument.querySelector( + "#permissionsGroup .search-header" + ); + is( + permissionsSearchHeader.hidden, + false, + "Permissions search-header should be visible" + ); + + let privacyCategory = + gBrowser.contentDocument.getElementById("category-privacy"); + privacyCategory.click(); + is(searchInput.value, "", "search input should be empty"); + let categoriesList = gBrowser.contentDocument.getElementById("categories"); + for (let i = 0; i < categoriesList.childElementCount; i++) { + let child = categoriesList.itemChildren[i]; + if (child.id == "category-privacy") { + is(child.selected, true, "Privacy panel should be selected"); + } else if (child.id) { + is(child.selected, false, "No other panel should be selected"); + } + } + + // Search header should now be hidden when viewing the permissions group not through a search + is( + permissionsSearchHeader.hidden, + true, + "Permissions search-header should be hidden" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_within_preferences_2.js b/browser/components/preferences/tests/browser_search_within_preferences_2.js new file mode 100644 index 0000000000..6de068fbe4 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_2.js @@ -0,0 +1,180 @@ +"use strict"; +/** + * This file contains tests for the Preferences search bar. + */ + +/** + * Enabling searching functionality. Will display search bar from this testcase forward. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.preferences.search", true]], + }); +}); + +/** + * Test that we only search the selected child of a XUL deck. + * When we search "Remove Account", + * it should not show the "Remove Account" button if the Firefox account is not logged in yet. + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("paneSync", { leaveOpen: true }); + + let weavePrefsDeck = + gBrowser.contentDocument.getElementById("weavePrefsDeck"); + is( + weavePrefsDeck.selectedIndex, + 0, + "Should select the #noFxaAccount child node" + ); + + // Performs search. + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "Sync"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if (child.id == "header-searchResults" || child.id == "weavePrefsDeck") { + is_element_visible(child, "Should be in search results"); + } else if (child.id) { + is_element_hidden(child, "Should not be in search results"); + } + } + + // Ensure the "Remove Account" button exists in the hidden child of the <xul:deck>. + let unlinkFxaAccount = weavePrefsDeck.children[1].querySelector( + "#unverifiedUnlinkFxaAccount" + ); + is( + unlinkFxaAccount.label, + "Remove Account", + "The Remove Account button should exist" + ); + + // Performs search. + searchInput.focus(); + query = "Remove Account"; + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + let noResultsEl = gBrowser.contentDocument.querySelector( + "#no-results-message" + ); + is_element_visible(noResultsEl, "Should be reporting no results"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +/** + * Test that we search using `search-l10n-ids`. + * + * The test uses element `showUpdateHistory` and + * l10n id `language-and-appearance-header` and expects the element + * to be matched on the first word from the l10n id value ("Language" in en-US). + */ +add_task(async function () { + let l10nId = "language-and-appearance-header"; + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // First, lets make sure that the element is not matched without + // `search-l10n-ids`. + { + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + let suhElem = gBrowser.contentDocument.getElementById("showUpdateHistory"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + ok( + !suhElem.getAttribute("search-l10n-ids").includes(l10nId), + "showUpdateHistory element should not contain the l10n id here." + ); + + let query = "Language"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + is_element_hidden( + suhElem, + "showUpdateHistory should not be in search results" + ); + } + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Now, let's add the l10n id to the element and perform the same search again. + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + { + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let suhElem = gBrowser.contentDocument.getElementById("showUpdateHistory"); + suhElem.setAttribute("search-l10n-ids", l10nId); + + let query = "Language"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + is_element_hidden( + suhElem, + "showUpdateHistory should not be in search results" + ); + } else { + is_element_visible( + suhElem, + "showUpdateHistory should be in search results" + ); + } + } + + await BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_search_within_preferences_command.js b/browser/components/preferences/tests/browser_search_within_preferences_command.js new file mode 100644 index 0000000000..736493b418 --- /dev/null +++ b/browser/components/preferences/tests/browser_search_within_preferences_command.js @@ -0,0 +1,45 @@ +"use strict"; + +/** + * Test for "command" event on search input (when user clicks the x button) + */ +add_task(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let generalPane = gBrowser.contentDocument.getElementById("generalCategory"); + + is_element_hidden(generalPane, "Should not be in general"); + + // Performs search + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + is( + searchInput, + gBrowser.contentDocument.activeElement.closest("#searchInput"), + "Search input should be focused when visiting preferences" + ); + + let query = "x"; + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == query + ); + EventUtils.sendString(query); + await searchCompletedPromise; + + is_element_hidden(generalPane, "Should not be in generalPane"); + + // Takes search off with "command" + searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == "" + ); + searchInput.value = ""; + searchInput.doCommand(); + await searchCompletedPromise; + + // Checks if back to normal + is_element_visible(generalPane, "Should be in generalPane"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_searchsuggestions.js b/browser/components/preferences/tests/browser_searchsuggestions.js new file mode 100644 index 0000000000..f3ce1615ea --- /dev/null +++ b/browser/components/preferences/tests/browser_searchsuggestions.js @@ -0,0 +1,128 @@ +const SUGGEST_PREF_NAME = "browser.search.suggest.enabled"; +const URLBAR_SUGGEST_PREF_NAME = "browser.urlbar.suggest.searches"; +const PRIVATE_PREF_NAME = "browser.search.suggest.enabled.private"; + +let initialUrlbarSuggestValue; +let initialSuggestionsInPrivateValue; + +add_setup(async function () { + const originalSuggest = Services.prefs.getBoolPref(SUGGEST_PREF_NAME); + initialUrlbarSuggestValue = Services.prefs.getBoolPref( + URLBAR_SUGGEST_PREF_NAME + ); + initialSuggestionsInPrivateValue = + Services.prefs.getBoolPref(PRIVATE_PREF_NAME); + + registerCleanupFunction(() => { + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, originalSuggest); + Services.prefs.setBoolPref( + PRIVATE_PREF_NAME, + initialSuggestionsInPrivateValue + ); + }); +}); + +// Open with suggestions enabled +add_task(async function test_suggestions_start_enabled() { + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let urlbarBox = doc.getElementById("urlBarSuggestion"); + let privateBox = doc.getElementById("showSearchSuggestionsPrivateWindows"); + ok(!urlbarBox.disabled, "Should have enabled the urlbar checkbox"); + ok( + !privateBox.disabled, + "Should have enabled the private mode suggestions checkbox" + ); + is( + urlbarBox.checked, + initialUrlbarSuggestValue, + "Should have the correct value for the urlbar checkbox" + ); + is( + privateBox.checked, + initialSuggestionsInPrivateValue, + "Should have the correct value for the private mode suggestions checkbox" + ); + + async function toggleElement(id, prefName, element, initialValue, desc) { + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${id}`, + {}, + gBrowser.selectedBrowser + ); + is( + element.checked, + !initialValue, + `Should have flipped the ${desc} checkbox` + ); + let prefValue = Services.prefs.getBoolPref(prefName); + is( + prefValue, + !initialValue, + `Should have updated the ${desc} preference value` + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + `#${id}`, + {}, + gBrowser.selectedBrowser + ); + is( + element.checked, + initialValue, + `Should have flipped the ${desc} checkbox back to the original value` + ); + prefValue = Services.prefs.getBoolPref(prefName); + is( + prefValue, + initialValue, + `Should have updated the ${desc} preference back to the original value` + ); + } + + await toggleElement( + "urlBarSuggestion", + URLBAR_SUGGEST_PREF_NAME, + urlbarBox, + initialUrlbarSuggestValue, + "urlbar" + ); + await toggleElement( + "showSearchSuggestionsPrivateWindows", + PRIVATE_PREF_NAME, + privateBox, + initialSuggestionsInPrivateValue, + "private suggestion" + ); + + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false); + ok(!urlbarBox.checked, "Should have unchecked the urlbar box"); + ok(urlbarBox.disabled, "Should have disabled the urlbar box"); + ok(!privateBox.checked, "Should have unchecked the private suggestions box"); + ok(privateBox.disabled, "Should have disabled the private suggestions box"); + + gBrowser.removeCurrentTab(); +}); + +// Open with suggestions disabled +add_task(async function test_suggestions_start_disabled() { + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, false); + + await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let urlbarBox = doc.getElementById("urlBarSuggestion"); + ok(urlbarBox.disabled, "Should have the urlbar box disabled"); + let privateBox = doc.getElementById("showSearchSuggestionsPrivateWindows"); + ok(privateBox.disabled, "Should have the private suggestions box disabled"); + + Services.prefs.setBoolPref(SUGGEST_PREF_NAME, true); + + ok(!urlbarBox.disabled, "Should have enabled the urlbar box"); + ok(!privateBox.disabled, "Should have enabled the private suggestions box"); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/preferences/tests/browser_security-1.js b/browser/components/preferences/tests/browser_security-1.js new file mode 100644 index 0000000000..80f3e6902b --- /dev/null +++ b/browser/components/preferences/tests/browser_security-1.js @@ -0,0 +1,106 @@ +const PREFS = [ + "browser.safebrowsing.phishing.enabled", + "browser.safebrowsing.malware.enabled", + + "browser.safebrowsing.downloads.enabled", + + "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + "browser.safebrowsing.downloads.remote.block_uncommon", +]; + +let originals = PREFS.map(pref => [pref, Services.prefs.getBoolPref(pref)]); +let originalMalwareTable = Services.prefs.getCharPref( + "urlclassifier.malwareTable" +); +registerCleanupFunction(function () { + originals.forEach(([pref, val]) => Services.prefs.setBoolPref(pref, val)); + Services.prefs.setCharPref( + "urlclassifier.malwareTable", + originalMalwareTable + ); +}); + +// This test only opens the Preferences once, and then reloads the page +// each time that it wants to test various preference combinations. We +// only use one tab (instead of opening/closing for each test) for all +// to help improve test times on debug builds. +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +// test the safebrowsing preference +add_task(async function () { + async function checkPrefSwitch(val1, val2) { + Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled", val1); + Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", val2); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("enableSafeBrowsing"); + let blockDownloads = doc.getElementById("blockDownloads"); + let blockUncommon = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is( + blockDownloads.hasAttribute("disabled"), + !checked, + "block downloads checkbox is set correctly" + ); + + is( + checked, + val1 && val2, + "safebrowsing preference is initialized correctly" + ); + // should be disabled when checked is false (= pref is turned off) + is( + blockUncommon.hasAttribute("disabled"), + !checked, + "block uncommon checkbox is set correctly" + ); + + // scroll the checkbox into the viewport and click checkbox + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + // check that both settings are now turned on or off + is( + Services.prefs.getBoolPref("browser.safebrowsing.phishing.enabled"), + !checked, + "safebrowsing.enabled is set correctly" + ); + is( + Services.prefs.getBoolPref("browser.safebrowsing.malware.enabled"), + !checked, + "safebrowsing.malware.enabled is set correctly" + ); + + // check if the other checkboxes have updated + checked = checkbox.checked; + if (blockDownloads) { + is( + blockDownloads.hasAttribute("disabled"), + !checked, + "block downloads checkbox is set correctly" + ); + is( + blockUncommon.hasAttribute("disabled"), + !checked || !blockDownloads.checked, + "block uncommon checkbox is set correctly" + ); + } + } + + await checkPrefSwitch(true, true); + await checkPrefSwitch(false, true); + await checkPrefSwitch(true, false); + await checkPrefSwitch(false, false); +}); diff --git a/browser/components/preferences/tests/browser_security-2.js b/browser/components/preferences/tests/browser_security-2.js new file mode 100644 index 0000000000..88be366810 --- /dev/null +++ b/browser/components/preferences/tests/browser_security-2.js @@ -0,0 +1,177 @@ +const PREFS = [ + "browser.safebrowsing.phishing.enabled", + "browser.safebrowsing.malware.enabled", + + "browser.safebrowsing.downloads.enabled", + + "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + "browser.safebrowsing.downloads.remote.block_uncommon", +]; + +let originals = PREFS.map(pref => [pref, Services.prefs.getBoolPref(pref)]); +let originalMalwareTable = Services.prefs.getCharPref( + "urlclassifier.malwareTable" +); +registerCleanupFunction(function () { + originals.forEach(([pref, val]) => Services.prefs.setBoolPref(pref, val)); + Services.prefs.setCharPref( + "urlclassifier.malwareTable", + originalMalwareTable + ); +}); + +// This test only opens the Preferences once, and then reloads the page +// each time that it wants to test various preference combinations. We +// only use one tab (instead of opening/closing for each test) for all +// to help improve test times on debug builds. +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +// test the download protection preference +add_task(async function () { + async function checkPrefSwitch(val) { + Services.prefs.setBoolPref("browser.safebrowsing.downloads.enabled", val); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("blockDownloads"); + + let blockUncommon = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is(checked, val, "downloads preference is initialized correctly"); + // should be disabled when val is false (= pref is turned off) + is( + blockUncommon.hasAttribute("disabled"), + !val, + "block uncommon checkbox is set correctly" + ); + + // scroll the checkbox into view, otherwise the synthesizeMouseAtCenter will be ignored, and click it + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + // check that setting is now turned on or off + is( + Services.prefs.getBoolPref("browser.safebrowsing.downloads.enabled"), + !checked, + "safebrowsing.downloads preference is set correctly" + ); + + // check if the uncommon warning checkbox has updated + is( + blockUncommon.hasAttribute("disabled"), + val, + "block uncommon checkbox is set correctly" + ); + } + + await checkPrefSwitch(true); + await checkPrefSwitch(false); +}); + +requestLongerTimeout(2); +// test the unwanted/uncommon software warning preference +add_task(async function () { + async function checkPrefSwitch(val1, val2, isV2) { + Services.prefs.setBoolPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted", + val1 + ); + Services.prefs.setBoolPref( + "browser.safebrowsing.downloads.remote.block_uncommon", + val2 + ); + let testMalwareTable = "goog-malware-" + (isV2 ? "shavar" : "proto"); + testMalwareTable += ",test-malware-simple"; + if (val1 && val2) { + testMalwareTable += ",goog-unwanted-" + (isV2 ? "shavar" : "proto"); + testMalwareTable += ",moztest-unwanted-simple"; + } + Services.prefs.setCharPref("urlclassifier.malwareTable", testMalwareTable); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + let checkbox = doc.getElementById("blockUncommonUnwanted"); + let checked = checkbox.checked; + is( + checked, + val1 && val2, + "unwanted/uncommon preference is initialized correctly" + ); + + // scroll the checkbox into view, otherwise the synthesizeMouseAtCenter will be ignored, and click it + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + // check that both settings are now turned on or off + is( + Services.prefs.getBoolPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ), + !checked, + "block_potentially_unwanted is set correctly" + ); + is( + Services.prefs.getBoolPref( + "browser.safebrowsing.downloads.remote.block_uncommon" + ), + !checked, + "block_uncommon is set correctly" + ); + + // when the preference is on, the malware table should include these ids + let malwareTable = Services.prefs + .getCharPref("urlclassifier.malwareTable") + .split(","); + if (isV2) { + is( + malwareTable.includes("goog-unwanted-shavar"), + !checked, + "malware table doesn't include goog-unwanted-shavar" + ); + } else { + is( + malwareTable.includes("goog-unwanted-proto"), + !checked, + "malware table doesn't include goog-unwanted-proto" + ); + } + is( + malwareTable.includes("moztest-unwanted-simple"), + !checked, + "malware table doesn't include moztest-unwanted-simple" + ); + let sortedMalware = malwareTable.slice(0); + sortedMalware.sort(); + Assert.deepEqual( + malwareTable, + sortedMalware, + "malware table has been sorted" + ); + } + + await checkPrefSwitch(true, true, false); + await checkPrefSwitch(false, true, false); + await checkPrefSwitch(true, false, false); + await checkPrefSwitch(false, false, false); + await checkPrefSwitch(true, true, true); + await checkPrefSwitch(false, true, true); + await checkPrefSwitch(true, false, true); + await checkPrefSwitch(false, false, true); +}); diff --git a/browser/components/preferences/tests/browser_security-3.js b/browser/components/preferences/tests/browser_security-3.js new file mode 100644 index 0000000000..75872d6629 --- /dev/null +++ b/browser/components/preferences/tests/browser_security-3.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + registerCleanupFunction(async function () { + Services.prefs.unlockPref("browser.safebrowsing.phishing.enabled"); + Services.prefs.unlockPref("browser.safebrowsing.malware.enabled"); + Services.prefs.unlockPref("browser.safebrowsing.downloads.enabled"); + Services.prefs.unlockPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ); + Services.prefs.unlockPref( + "browser.safebrowsing.downloads.remote.block_uncommon" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }); +}); + +// This test just reloads the preferences page for the various tests. +add_task(async function () { + Services.prefs.lockPref("browser.safebrowsing.phishing.enabled"); + Services.prefs.lockPref("browser.safebrowsing.malware.enabled"); + Services.prefs.lockPref("browser.safebrowsing.downloads.enabled"); + Services.prefs.lockPref( + "browser.safebrowsing.downloads.remote.block_potentially_unwanted" + ); + Services.prefs.lockPref( + "browser.safebrowsing.downloads.remote.block_uncommon" + ); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let doc = gBrowser.selectedBrowser.contentDocument; + is( + doc.getElementById("enableSafeBrowsing").disabled, + true, + "Safe browsing should be disabled" + ); + is( + doc.getElementById("blockDownloads").disabled, + true, + "Block downloads should be disabled" + ); + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + Services.prefs.unlockPref("browser.safebrowsing.phishing.enabled"); + Services.prefs.unlockPref("browser.safebrowsing.malware.enabled"); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + doc = gBrowser.selectedBrowser.contentDocument; + + let checkbox = doc.getElementById("enableSafeBrowsing"); + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockDownloads").disabled, + true, + "Block downloads should be disabled" + ); + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockDownloads").disabled, + true, + "Block downloads should be disabled" + ); + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + Services.prefs.unlockPref("browser.safebrowsing.downloads.enabled"); + + gBrowser.reload(); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + doc = gBrowser.selectedBrowser.contentDocument; + + checkbox = doc.getElementById("blockDownloads"); + checkbox.scrollIntoView(); + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); + + EventUtils.synthesizeMouseAtCenter( + checkbox, + {}, + gBrowser.selectedBrowser.contentWindow + ); + + is( + doc.getElementById("blockUncommonUnwanted").disabled, + true, + "Block common unwanted should be disabled" + ); +}); diff --git a/browser/components/preferences/tests/browser_site_login_exceptions.js b/browser/components/preferences/tests/browser_site_login_exceptions.js new file mode 100644 index 0000000000..508232b234 --- /dev/null +++ b/browser/components/preferences/tests/browser_site_login_exceptions.js @@ -0,0 +1,101 @@ +"use strict"; +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +var exceptionsDialog; + +add_task(async function openLoginExceptionsSubDialog() { + // ensure rememberSignons is off for this test; + ok( + !Services.prefs.getBoolPref("signon.rememberSignons"), + "Check initial value of signon.rememberSignons pref" + ); + + // Undo the save password change. + registerCleanupFunction(async function () { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + if (savePasswordCheckBox.checked) { + savePasswordCheckBox.click(); + } + }); + + gBrowser.removeCurrentTab(); + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + Assert.ok( + !savePasswordCheckBox.checked, + "Save Password CheckBox should be unchecked by default" + ); + savePasswordCheckBox.click(); + + let loginExceptionsButton = doc.getElementById("passwordExceptions"); + loginExceptionsButton.click(); + }); + + exceptionsDialog = await dialogOpened; +}); + +add_task(async function addALoginException() { + let doc = exceptionsDialog.document; + + let richlistbox = doc.getElementById("permissionsBox"); + Assert.equal(richlistbox.itemCount, 0, "Row count should initially be 0"); + + let inputBox = doc.getElementById("url"); + inputBox.focus(); + + EventUtils.sendString("www.example.com", exceptionsDialog); + + let btnBlock = doc.getElementById("btnBlock"); + btnBlock.click(); + + await TestUtils.waitForCondition(() => richlistbox.itemCount == 2); + + let expectedResult = ["http://www.example.com", "https://www.example.com"]; + for (let website of expectedResult) { + let elements = richlistbox.getElementsByAttribute("origin", website); + is(elements.length, 1, "It should find only one coincidence"); + } +}); + +add_task(async function deleteALoginException() { + let doc = exceptionsDialog.document; + + let richlistbox = doc.getElementById("permissionsBox"); + let currentItems = 2; + Assert.equal( + richlistbox.itemCount, + currentItems, + `Row count should initially be ${currentItems}` + ); + richlistbox.focus(); + + while (richlistbox.itemCount) { + richlistbox.selectedIndex = 0; + + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_Backspace"); + } else { + EventUtils.synthesizeKey("KEY_Delete"); + } + + currentItems -= 1; + + await TestUtils.waitForCondition( + () => richlistbox.itemCount == currentItems + ); + is_element_visible( + content.gSubDialog._dialogs[0]._box, + "Subdialog is visible after deleting an element" + ); + } +}); diff --git a/browser/components/preferences/tests/browser_site_login_exceptions_policy.js b/browser/components/preferences/tests/browser_site_login_exceptions_policy.js new file mode 100644 index 0000000000..10f5039c6f --- /dev/null +++ b/browser/components/preferences/tests/browser_site_login_exceptions_policy.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const PERMISSIONS_URL = + "chrome://browser/content/preferences/dialogs/permissions.xhtml"; + +var exceptionsDialog; + +add_task(async function openLoginExceptionsSubDialog() { + // ensure rememberSignons is off for this test; + ok( + !Services.prefs.getBoolPref("signon.rememberSignons"), + "Check initial value of signon.rememberSignons pref" + ); + + // Undo the save password change. + registerCleanupFunction(async function () { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + if (savePasswordCheckBox.checked) { + savePasswordCheckBox.click(); + } + }); + + gBrowser.removeCurrentTab(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); + }); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + PasswordManagerExceptions: ["https://pwexception.example.com"], + }, + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let dialogOpened = promiseLoadSubDialog(PERMISSIONS_URL); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let doc = content.document; + let savePasswordCheckBox = doc.getElementById("savePasswords"); + savePasswordCheckBox.click(); + + let loginExceptionsButton = doc.getElementById("passwordExceptions"); + loginExceptionsButton.click(); + }); + + exceptionsDialog = await dialogOpened; + + let doc = exceptionsDialog.document; + + let richlistbox = doc.getElementById("permissionsBox"); + Assert.equal(richlistbox.itemCount, 1, `Row count should initially be 1`); + + richlistbox.focus(); + richlistbox.selectedIndex = 0; + Assert.ok(doc.getElementById("removePermission").disabled); +}); diff --git a/browser/components/preferences/tests/browser_spotlight.js b/browser/components/preferences/tests/browser_spotlight.js new file mode 100644 index 0000000000..4a1aae7ec1 --- /dev/null +++ b/browser/components/preferences/tests/browser_spotlight.js @@ -0,0 +1,72 @@ +add_task(async function test_openPreferences_spotlight() { + for (let [arg, expectedPane, expectedHash, expectedSubcategory] of [ + ["privacy-reports", "panePrivacy", "#privacy", "reports"], + ["privacy-address-autofill", "panePrivacy", "#privacy", "address-autofill"], + [ + "privacy-credit-card-autofill", + "panePrivacy", + "#privacy", + "credit-card-autofill", + ], + ["privacy-form-autofill", "panePrivacy", "#privacy", "form-autofill"], + ["privacy-logins", "panePrivacy", "#privacy", "logins"], + [ + "privacy-trackingprotection", + "panePrivacy", + "#privacy", + "trackingprotection", + ], + [ + "privacy-permissions-block-popups", + "panePrivacy", + "#privacy", + "permissions-block-popups", + ], + ]) { + if ( + arg == "privacy-credit-card-autofill" && + Services.prefs.getCharPref( + "extensions.formautofill.creditCards.supported" + ) == "off" + ) { + continue; + } + if ( + arg == "privacy-address-autofill" && + Services.prefs.getCharPref( + "extensions.formautofill.addresses.supported" + ) == "off" + ) { + continue; + } + + let prefs = await openPreferencesViaOpenPreferencesAPI(arg, { + leaveOpen: true, + }); + is(prefs.selectedPane, expectedPane, "The right pane is selected"); + let doc = gBrowser.contentDocument; + is( + doc.location.hash, + expectedHash, + "The subcategory should be removed from the URI" + ); + await TestUtils.waitForCondition( + () => doc.querySelector(".spotlight"), + "Wait for the spotlight" + ); + is( + doc.querySelector(".spotlight").getAttribute("data-subcategory"), + expectedSubcategory, + "The right subcategory is spotlighted" + ); + + doc.defaultView.spotlight(null); + is( + doc.querySelector(".spotlight"), + null, + "The spotlighted section is cleared" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +}); diff --git a/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js b/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js new file mode 100644 index 0000000000..73d12a1bf9 --- /dev/null +++ b/browser/components/preferences/tests/browser_statePartitioning_PBM_strings.js @@ -0,0 +1,124 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIOR_PBM_PREF = "network.cookie.cookieBehavior.pbmode"; + +const CB_STRICT_FEATURES_PREF = "browser.contentblocking.features.strict"; +const FPI_PREF = "privacy.firstparty.isolate"; + +async function testCookieBlockingInfoStandard( + cookieBehavior, + cookieBehaviorPBM, + isShown +) { + let defaults = Services.prefs.getDefaultBranch(""); + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, cookieBehavior); + defaults.setIntPref(COOKIE_BEHAVIOR_PBM_PREF, cookieBehaviorPBM); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + + // Select standard mode. + let standardRadioOption = doc.getElementById("standardRadio"); + standardRadioOption.click(); + + // Check the cookie blocking info for private windows for standard mode. + let elts = doc.querySelectorAll( + "#contentBlockingOptionStandard .extra-information-label.all-third-party-cookies-private-windows-option" + ); + for (let elt of elts) { + is( + elt.hidden, + !isShown, + `The visibility of cookie blocking info for standard mode is correct` + ); + } + + gBrowser.removeCurrentTab(); +} + +async function testCookieBlockingInfoStrict( + contentBlockingStrictFeatures, + isShown +) { + await SpecialPowers.pushPrefEnv({ + set: [ + [CB_STRICT_FEATURES_PREF, contentBlockingStrictFeatures], + [FPI_PREF, false], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + + // Select strict mode. + let strictRadioOption = doc.getElementById("strictRadio"); + strictRadioOption.click(); + + // Check the cookie blocking info for private windows for strict mode. + let elts = doc.querySelectorAll( + "#contentBlockingOptionStrict .extra-information-label.all-third-party-cookies-private-windows-option" + ); + for (let elt of elts) { + is( + elt.hidden, + !isShown, + `The cookie blocking info is hidden for strict mode` + ); + } + + gBrowser.removeCurrentTab(); +} + +add_task(async function runTests() { + await SpecialPowers.pushPrefEnv({ + set: [[FPI_PREF, false]], + }); + + let defaults = Services.prefs.getDefaultBranch(""); + let originalCookieBehavior = defaults.getIntPref(COOKIE_BEHAVIOR_PREF); + let originalCookieBehaviorPBM = defaults.getIntPref(COOKIE_BEHAVIOR_PBM_PREF); + + // Test if the cookie blocking info for state partitioning in PBM is + // shown in standard mode if the regular cookieBehavior is + // BEHAVIOR_REJECT_TRACKER and the private cookieBehavior is + // BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + await testCookieBlockingInfoStandard( + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + true + ); + + // Test if the cookie blocking info is hidden in standard mode if both + // cookieBehaviors are the same. + await testCookieBlockingInfoStandard( + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + false + ); + + // Test if the cookie blocking info is hidden for strict mode if + // cookieBehaviors both are BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN in + // the strict feature value. + await testCookieBlockingInfoStrict( + "tp,tpPrivate,cookieBehavior5,cookieBehaviorPBM5,cm,fp,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp", + false + ); + + // Test if the cookie blocking info is shown for strict mode if the regular + // cookieBehavior is BEHAVIOR_REJECT_TRACKER and the private cookieBehavior is + // BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + await testCookieBlockingInfoStrict( + "tp,tpPrivate,cookieBehavior4,cookieBehaviorPBM5,cm,fp,stp,emailTP,emailTPPrivate,lvl2,rp,rpTop,ocsp", + true + ); + + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, originalCookieBehavior); + defaults.setIntPref(COOKIE_BEHAVIOR_PBM_PREF, originalCookieBehaviorPBM); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/preferences/tests/browser_statePartitioning_strings.js b/browser/components/preferences/tests/browser_statePartitioning_strings.js new file mode 100644 index 0000000000..aed7e26977 --- /dev/null +++ b/browser/components/preferences/tests/browser_statePartitioning_strings.js @@ -0,0 +1,79 @@ +"use strict"; + +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const CB_STRICT_FEATURES_PREF = "browser.contentblocking.features.strict"; +const CB_STRICT_FEATURES_VALUE = "tp,tpPrivate,cookieBehavior5,cm,fp,stp,lvl2"; +const FPI_PREF = "privacy.firstparty.isolate"; +const COOKIE_BEHAVIOR_PREF = "network.cookie.cookieBehavior"; +const COOKIE_BEHAVIOR_VALUE = 5; + +async function testStrings() { + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.contentDocument; + + // Check the cookie blocking info strings + let elts = doc.querySelectorAll( + ".extra-information-label.cross-site-cookies-option" + ); + for (let elt of elts) { + ok(!elt.hidden, "The new cross-site cookies info label is visible"); + } + + // Check the learn more strings + elts = doc.querySelectorAll( + ".tail-with-learn-more.content-blocking-warning-description" + ); + for (let elt of elts) { + let id = doc.l10n.getAttributes(elt).id; + is( + id, + "content-blocking-and-isolating-etp-warning-description-2", + "The correct warning description string is in use" + ); + } + + // Check the cookie blocking mode menu option string + let elt = doc.querySelector("#isolateCookiesSocialMedia"); + let id = doc.l10n.getAttributes(elt).id; + is( + id, + "sitedata-option-block-cross-site-cookies", + "The correct string is in use for the cookie blocking option" + ); + + // Check the FPI warning is hidden with FPI off + let warningElt = doc.getElementById("fpiIncompatibilityWarning"); + is(warningElt.hidden, true, "The FPI warning is hidden"); + + gBrowser.removeCurrentTab(); + + // Check the FPI warning is shown only if MVP UI is enabled when FPI is on + await SpecialPowers.pushPrefEnv({ set: [[FPI_PREF, true]] }); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + doc = gBrowser.contentDocument; + warningElt = doc.getElementById("fpiIncompatibilityWarning"); + ok(!warningElt.hidden, `The FPI warning is visible`); + await SpecialPowers.popPrefEnv(); + + gBrowser.removeCurrentTab(); +} + +add_task(async function runTests() { + await SpecialPowers.pushPrefEnv({ + set: [ + [CB_STRICT_FEATURES_PREF, CB_STRICT_FEATURES_VALUE], + [FPI_PREF, false], + ], + }); + let defaults = Services.prefs.getDefaultBranch(""); + let originalCookieBehavior = defaults.getIntPref(COOKIE_BEHAVIOR_PREF); + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, COOKIE_BEHAVIOR_VALUE); + registerCleanupFunction(() => { + defaults.setIntPref(COOKIE_BEHAVIOR_PREF, originalCookieBehavior); + }); + + await testStrings(); +}); diff --git a/browser/components/preferences/tests/browser_subdialogs.js b/browser/components/preferences/tests/browser_subdialogs.js new file mode 100644 index 0000000000..9342575886 --- /dev/null +++ b/browser/components/preferences/tests/browser_subdialogs.js @@ -0,0 +1,639 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the sub-dialog infrastructure, not for actual sub-dialog functionality. + */ + +const gDialogURL = getRootDirectory(gTestPath) + "subdialog.xhtml"; +const gDialogURL2 = getRootDirectory(gTestPath) + "subdialog2.xhtml"; + +function open_subdialog_and_test_generic_start_state( + browser, + domcontentloadedFn, + url = gDialogURL +) { + let domcontentloadedFnStr = domcontentloadedFn + ? "(" + domcontentloadedFn.toString() + ")()" + : ""; + return SpecialPowers.spawn( + browser, + [{ url, domcontentloadedFnStr }], + async function (args) { + let rv = { acceptCount: 0 }; + let win = content.window; + content.gSubDialog.open(args.url, undefined, rv); + let subdialog = content.gSubDialog._topDialog; + + info("waiting for subdialog DOMFrameContentLoaded"); + let dialogOpenPromise; + await new Promise(resolve => { + win.addEventListener( + "DOMFrameContentLoaded", + function frameContentLoaded(ev) { + // We can get events for loads in other frames, so we have to filter + // those out. + if (ev.target != subdialog._frame) { + return; + } + win.removeEventListener( + "DOMFrameContentLoaded", + frameContentLoaded + ); + dialogOpenPromise = ContentTaskUtils.waitForEvent( + subdialog._overlay, + "dialogopen" + ); + resolve(); + }, + { capture: true } + ); + }); + let result; + if (args.domcontentloadedFnStr) { + // eslint-disable-next-line no-eval + result = eval(args.domcontentloadedFnStr); + } + + info("waiting for subdialog load"); + await dialogOpenPromise; + info("subdialog window is loaded"); + + let expectedStyleSheetURLs = subdialog._injectedStyleSheets.slice(0); + for (let styleSheet of subdialog._frame.contentDocument.styleSheets) { + let index = expectedStyleSheetURLs.indexOf(styleSheet.href); + if (index >= 0) { + expectedStyleSheetURLs.splice(index, 1); + } + } + + Assert.ok( + !!subdialog._frame.contentWindow, + "The dialog should be non-null" + ); + Assert.notEqual( + subdialog._frame.contentWindow.location.toString(), + "about:blank", + "Subdialog URL should not be about:blank" + ); + Assert.equal( + win.getComputedStyle(subdialog._overlay).visibility, + "visible", + "Overlay should be visible" + ); + Assert.equal( + expectedStyleSheetURLs.length, + 0, + "No stylesheets that were expected are missing" + ); + return result; + } + ); +} + +async function close_subdialog_and_test_generic_end_state( + browser, + closingFn, + closingButton, + acceptCount, + options +) { + let getDialogsCount = () => { + return SpecialPowers.spawn( + browser, + [], + () => content.window.gSubDialog._dialogs.length + ); + }; + let getStackChildrenCount = () => { + return SpecialPowers.spawn( + browser, + [], + () => content.window.gSubDialog._dialogStack.children.length + ); + }; + let dialogclosingPromise = SpecialPowers.spawn( + browser, + [{ closingButton, acceptCount }], + async function (expectations) { + let win = content.window; + let subdialog = win.gSubDialog._topDialog; + let frame = subdialog._frame; + + let frameWinUnload = ContentTaskUtils.waitForEvent( + frame.contentWindow, + "unload", + true + ); + + let actualAcceptCount; + info("waiting for dialogclosing"); + info("URI " + frame.currentURI?.spec); + let closingEvent = await ContentTaskUtils.waitForEvent( + frame.contentWindow, + "dialogclosing", + true, + () => { + actualAcceptCount = frame.contentWindow?.arguments[0].acceptCount; + return true; + } + ); + + info("Waiting for subdialog unload"); + await frameWinUnload; + + let contentClosingButton = closingEvent.detail.button; + + Assert.notEqual( + win.getComputedStyle(subdialog._overlay).visibility, + "visible", + "overlay is not visible" + ); + Assert.equal( + frame.getAttribute("style"), + "", + "inline styles should be cleared" + ); + Assert.equal( + contentClosingButton, + expectations.closingButton, + "closing event should indicate button was '" + + expectations.closingButton + + "'" + ); + Assert.equal( + actualAcceptCount, + expectations.acceptCount, + "should be 1 if accepted, 0 if canceled, undefined if closed w/out button" + ); + } + ); + let initialDialogsCount = await getDialogsCount(); + let initialStackChildrenCount = await getStackChildrenCount(); + if (options && options.runClosingFnOutsideOfContentTask) { + await closingFn(); + } else { + SpecialPowers.spawn(browser, [], closingFn); + } + + await dialogclosingPromise; + let endDialogsCount = await getDialogsCount(); + let endStackChildrenCount = await getStackChildrenCount(); + Assert.equal( + initialDialogsCount - 1, + endDialogsCount, + "dialog count should decrease by 1" + ); + Assert.equal( + initialStackChildrenCount - 1, + endStackChildrenCount, + "stack children count should decrease by 1" + ); +} + +let tab; + +add_task(async function test_initialize() { + tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences" + ); +}); + +add_task( + async function check_titlebar_focus_returnval_titlechanges_accepting() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + let domtitlechangedPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "DOMTitleChanged" + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let dialog = content.window.gSubDialog._topDialog; + let dialogWin = dialog._frame.contentWindow; + let dialogTitleElement = dialog._titleElement; + Assert.equal( + dialogTitleElement.textContent, + "Sample sub-dialog", + "Title should be correct initially" + ); + Assert.equal( + dialogWin.document.activeElement.value, + "Default text", + "Textbox with correct text is focused" + ); + dialogWin.document.title = "Updated title"; + }); + + info("waiting for DOMTitleChanged event"); + await domtitlechangedPromise; + + SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let dialogTitleElement = + content.window.gSubDialog._topDialog._titleElement; + Assert.equal( + dialogTitleElement.textContent, + "Updated title", + "subdialog should have updated title" + ); + }); + + // Accept the dialog + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); + } +); + +add_task(async function check_canceling_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .cancelDialog(); + }, + "cancel", + 0 + ); +}); + +add_task(async function check_reopening_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + info("opening another dialog which will close the first"); + await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + "", + gDialogURL2 + ); + + SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let win = content.window; + let dialogs = win.gSubDialog._dialogs; + let lowerDialog = dialogs[0]; + let topDialog = dialogs[1]; + Assert.equal(dialogs.length, 2, "There should be two visible dialogs"); + Assert.equal( + win.getComputedStyle(topDialog._overlay).visibility, + "visible", + "The top dialog should be visible" + ); + Assert.equal( + win.getComputedStyle(lowerDialog._overlay).visibility, + "visible", + "The lower dialog should be visible" + ); + Assert.equal( + win.getComputedStyle(topDialog._overlay).backgroundColor, + "rgba(0, 0, 0, 0.5)", + "The top dialog should have a semi-transparent overlay" + ); + Assert.equal( + win.getComputedStyle(lowerDialog._overlay).backgroundColor, + "rgba(0, 0, 0, 0)", + "The lower dialog should not have an overlay" + ); + }); + + info("closing two dialogs"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); +}); + +add_task(async function check_opening_while_closing() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + info("closing"); + content.window.gSubDialog._topDialog.close(); + info("reopening immediately after calling .close()"); + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentDocument + .getElementById("subDialog") + .acceptDialog(); + }, + "accept", + 1 + ); +}); + +add_task(async function window_close_on_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.close(); + }, + null, + 0 + ); +}); + +add_task(async function click_close_button_on_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + return BrowserTestUtils.synthesizeMouseAtCenter( + ".dialogClose", + {}, + tab.linkedBrowser + ); + }, + null, + 0, + { runClosingFnOutsideOfContentTask: true } + ); +}); + +add_task(async function background_click_should_close_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + // Clicking on an inactive part of dialog itself should not close the dialog. + // Click the dialog title bar here to make sure nothing happens. + info("clicking the dialog title bar"); + BrowserTestUtils.synthesizeMouseAtCenter( + ".dialogTitle", + {}, + tab.linkedBrowser + ); + + // Close the dialog by clicking on the overlay background. Simulate a click + // at point (2,2) instead of (0,0) so we are sure we're clicking on the + // overlay background instead of some boundary condition that a real user + // would never click. + info("clicking the overlay background"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + return BrowserTestUtils.synthesizeMouseAtPoint( + 2, + 2, + {}, + tab.linkedBrowser + ); + }, + null, + 0, + { runClosingFnOutsideOfContentTask: true } + ); +}); + +add_task(async function escape_should_close_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + info("canceling the dialog"); + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + return BrowserTestUtils.synthesizeKey("VK_ESCAPE", {}, tab.linkedBrowser); + }, + "cancel", + 0, + { runClosingFnOutsideOfContentTask: true } + ); +}); + +add_task(async function correct_width_and_height_should_be_used_for_dialog() { + await open_subdialog_and_test_generic_start_state(tab.linkedBrowser); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + function fuzzyEqual(value, expectedValue, fuzz, msg) { + Assert.greaterOrEqual(expectedValue + fuzz, value, msg); + Assert.lessOrEqual(expectedValue - fuzz, value, msg); + } + let topDialog = content.gSubDialog._topDialog; + let frameStyle = content.getComputedStyle(topDialog._frame); + let dialogStyle = topDialog.frameContentWindow.getComputedStyle( + topDialog.frameContentWindow.document.documentElement + ); + let fontSize = parseFloat(dialogStyle.fontSize); + let height = parseFloat(frameStyle.height); + let width = parseFloat(frameStyle.width); + + fuzzyEqual( + width, + fontSize * 32, + 2, + "Width should be set on the frame from the dialog" + ); + fuzzyEqual( + height, + fontSize * 5, + 2, + "Height should be set on the frame from the dialog" + ); + }); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.close(); + }, + null, + 0 + ); +}); + +add_task( + async function wrapped_text_in_dialog_should_have_expected_scrollHeight() { + let oldHeight = await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + function domcontentloadedFn() { + let frame = content.window.gSubDialog._topDialog._frame; + let doc = frame.contentDocument; + let scrollHeight = doc.documentElement.scrollHeight; + doc.documentElement.style.removeProperty("height"); + doc.getElementById("desc").textContent = ` + Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque + laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas + sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas + sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas + sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + voluptatem sequi nesciunt.`; + return scrollHeight; + } + ); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [oldHeight], + async function (contentOldHeight) { + function fuzzyEqual(value, expectedValue, fuzz, msg) { + Assert.greaterOrEqual(expectedValue + fuzz, value, msg); + Assert.lessOrEqual(expectedValue - fuzz, value, msg); + } + let topDialog = content.gSubDialog._topDialog; + let frame = topDialog._frame; + let frameStyle = content.getComputedStyle(frame); + let docEl = frame.contentDocument.documentElement; + let dialogStyle = topDialog.frameContentWindow.getComputedStyle(docEl); + let fontSize = parseFloat(dialogStyle.fontSize); + let height = parseFloat(frameStyle.height); + let width = parseFloat(frameStyle.width); + + fuzzyEqual( + width, + 32 * fontSize, + 2, + "Width should be set on the frame from the dialog" + ); + Assert.ok( + docEl.scrollHeight > contentOldHeight, + "Content height increased (from " + + contentOldHeight + + " to " + + docEl.scrollHeight + + ")." + ); + fuzzyEqual( + height, + docEl.scrollHeight, + 2, + "Height on the frame should be higher now. " + + "This test may fail on certain screen resoluition. " + + "See bug 1420576 and bug 1205717." + ); + } + ); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); + }, + null, + 0 + ); + } +); + +add_task(async function dialog_too_tall_should_get_reduced_in_height() { + await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + function domcontentloadedFn() { + let frame = content.window.gSubDialog._topDialog._frame; + frame.contentDocument.documentElement.style.height = "100000px"; + } + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + function fuzzyEqual(value, expectedValue, fuzz, msg) { + Assert.greaterOrEqual(expectedValue + fuzz, value, msg); + Assert.lessOrEqual(expectedValue - fuzz, value, msg); + } + let topDialog = content.gSubDialog._topDialog; + let frame = topDialog._frame; + let frameStyle = content.getComputedStyle(frame); + let dialogStyle = topDialog.frameContentWindow.getComputedStyle( + frame.contentDocument.documentElement + ); + let fontSize = parseFloat(dialogStyle.fontSize); + let height = parseFloat(frameStyle.height); + let width = parseFloat(frameStyle.width); + fuzzyEqual( + width, + 32 * fontSize, + 2, + "Width should be set on the frame from the dialog" + ); + Assert.less( + height, + content.window.innerHeight, + "Height on the frame should be smaller than window's innerHeight" + ); + }); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); + }, + null, + 0 + ); +}); + +add_task( + async function scrollWidth_and_scrollHeight_from_subdialog_should_size_the_browser() { + await open_subdialog_and_test_generic_start_state( + tab.linkedBrowser, + function domcontentloadedFn() { + let frame = content.window.gSubDialog._topDialog._frame; + frame.contentDocument.documentElement.style.removeProperty("height"); + frame.contentDocument.documentElement.style.removeProperty("width"); + } + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let frame = content.window.gSubDialog._topDialog._frame; + Assert.ok( + frame.style.width.endsWith("px"), + "Width (" + + frame.style.width + + ") should be set to a px value of the scrollWidth from the dialog" + ); + let cs = content.getComputedStyle(frame); + Assert.stringMatches( + cs.getPropertyValue("--subdialog-inner-height"), + /px$/, + "Height (" + + cs.getPropertyValue("--subdialog-inner-height") + + ") should be set to a px value of the scrollHeight from the dialog" + ); + }); + + await close_subdialog_and_test_generic_end_state( + tab.linkedBrowser, + function () { + content.window.gSubDialog._topDialog._frame.contentWindow.window.close(); + }, + null, + 0 + ); + } +); + +add_task(async function test_shutdown() { + gBrowser.removeTab(tab); +}); diff --git a/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js b/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js new file mode 100644 index 0000000000..b36d9ecea3 --- /dev/null +++ b/browser/components/preferences/tests/browser_sync_chooseWhatToSync.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +// This obj will be used in both tests +// First test makes sure accepting the preferences matches these values +// Second test makes sure the cancel dialog STILL matches these values +const syncPrefs = { + "services.sync.engine.addons": false, + "services.sync.engine.bookmarks": true, + "services.sync.engine.history": true, + "services.sync.engine.tabs": false, + "services.sync.engine.prefs": false, + "services.sync.engine.passwords": false, + "services.sync.engine.addresses": false, + "services.sync.engine.creditcards": false, +}; + +add_setup(async () => { + UIState._internal.notifyStateUpdated = () => {}; + const origNotifyStateUpdated = UIState._internal.notifyStateUpdated; + const origGet = UIState.get; + UIState.get = () => { + return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" }; + }; + + registerCleanupFunction(() => { + UIState._internal.notifyStateUpdated = origNotifyStateUpdated; + UIState.get = origGet; + }); +}); + +/** + * We don't actually enable sync here, but we just check that the preferences are correct + * when the callback gets hit (accepting/cancelling the dialog) + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1584132. + */ + +add_task(async function testDialogAccept() { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // This will check if the callback was actually called during the test + let callbackCalled = false; + + // Enabling all the sync UI is painful in tests, so we just open the dialog manually + let syncWindow = await openAndLoadSubDialog( + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml", + null, + {}, + () => { + for (const [prefKey, prefValue] of Object.entries(syncPrefs)) { + Assert.equal( + Services.prefs.getBoolPref(prefKey), + prefValue, + `${prefValue} is expected value` + ); + } + callbackCalled = true; + } + ); + + Assert.ok(syncWindow, "Choose what to sync window opened"); + let syncChooseDialog = + syncWindow.document.getElementById("syncChooseOptions"); + let syncCheckboxes = syncChooseDialog.querySelectorAll( + "checkbox[preference]" + ); + + // Adjust the checkbox values to the expectedValues in the list + [...syncCheckboxes].forEach(checkbox => { + if (syncPrefs[checkbox.getAttribute("preference")] !== checkbox.checked) { + checkbox.click(); + } + }); + + syncChooseDialog.acceptDialog(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.ok(callbackCalled, "Accept callback was called"); +}); + +add_task(async function testDialogCancel() { + const cancelSyncPrefs = { + "services.sync.engine.addons": true, + "services.sync.engine.bookmarks": false, + "services.sync.engine.history": true, + "services.sync.engine.tabs": true, + "services.sync.engine.prefs": false, + "services.sync.engine.passwords": true, + "services.sync.engine.addresses": true, + "services.sync.engine.creditcards": false, + }; + + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", true]], + }); + + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + + // This will check if the callback was actually called during the test + let callbackCalled = false; + + // Enabling all the sync UI is painful in tests, so we just open the dialog manually + let syncWindow = await openAndLoadSubDialog( + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml", + null, + {}, + () => { + // We want to test against our previously accepted values in the last test + for (const [prefKey, prefValue] of Object.entries(syncPrefs)) { + Assert.equal( + Services.prefs.getBoolPref(prefKey), + prefValue, + `${prefValue} is expected value` + ); + } + callbackCalled = true; + } + ); + + ok(syncWindow, "Choose what to sync window opened"); + let syncChooseDialog = + syncWindow.document.getElementById("syncChooseOptions"); + let syncCheckboxes = syncChooseDialog.querySelectorAll( + "checkbox[preference]" + ); + + // This time we're adjusting to the cancel list + [...syncCheckboxes].forEach(checkbox => { + if ( + cancelSyncPrefs[checkbox.getAttribute("preference")] !== checkbox.checked + ) { + checkbox.click(); + } + }); + + syncChooseDialog.cancelDialog(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.ok(callbackCalled, "Cancel callback was called"); +}); + +/** + * Tests that this subdialog can be opened via + * about:preferences?action=choose-what-to-sync#sync + */ +add_task(async function testDialogLaunchFromURI() { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", true]], + }); + + let dialogEventPromise = BrowserTestUtils.waitForEvent( + window, + "dialogopen", + true + ); + await BrowserTestUtils.withNewTab( + "about:preferences?action=choose-what-to-sync#sync", + async browser => { + let dialogEvent = await dialogEventPromise; + Assert.equal( + dialogEvent.detail.dialog._frame.contentWindow.location, + "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml" + ); + } + ); +}); diff --git a/browser/components/preferences/tests/browser_sync_disabled.js b/browser/components/preferences/tests/browser_sync_disabled.js new file mode 100644 index 0000000000..1f8518a1e4 --- /dev/null +++ b/browser/components/preferences/tests/browser_sync_disabled.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't show sync pane when it's disabled. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1536752. + */ +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", false]], + }); + await openPreferencesViaOpenPreferencesAPI("paneGeneral", { + leaveOpen: true, + }); + ok( + gBrowser.contentDocument.getElementById("category-sync").hidden, + "sync category hidden" + ); + + // Check that we don't get any results in sync when searching: + await evaluateSearchResults("sync", "no-results-message"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/browser_sync_pairing.js b/browser/components/preferences/tests/browser_sync_pairing.js new file mode 100644 index 0000000000..6491007a38 --- /dev/null +++ b/browser/components/preferences/tests/browser_sync_pairing.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); +const { FxAccountsPairingFlow } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsPairing.sys.mjs" +); + +// Use sinon for mocking. +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +let flowCounter = 0; + +add_setup(async function () { + Services.prefs.setBoolPref("identity.fxaccounts.pairing.enabled", true); + // Sync start-up might interfere with our tests, don't let UIState send UI updates. + const origNotifyStateUpdated = UIState._internal.notifyStateUpdated; + UIState._internal.notifyStateUpdated = () => {}; + + const origGet = UIState.get; + UIState.get = () => { + return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" }; + }; + + const origStart = FxAccountsPairingFlow.start; + FxAccountsPairingFlow.start = ({ emitter: e }) => { + return `https://foo.bar/${flowCounter++}`; + }; + + registerCleanupFunction(() => { + UIState._internal.notifyStateUpdated = origNotifyStateUpdated; + UIState.get = origGet; + FxAccountsPairingFlow.start = origStart; + }); +}); + +add_task(async function testShowsQRCode() { + await runWithPairingDialog(async win => { + let doc = win.document; + let qrContainer = doc.getElementById("qrContainer"); + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "ready" + ); + + // Verify that a QRcode is being shown. + Assert.ok( + qrContainer.style.backgroundImage.startsWith( + `url("data:image/gif;base64,R0lGODdhOgA6AIAAAAAAAP///ywAAAAAOgA6AAAC/4yPqcvtD6OctNqLs968+w+G4gKU5nkiJYO2JuW6KsDGKEw3a7AbPZ+r4Ry7nzFIQkKKN6Avlzowo78` + ) + ); + + // Close the dialog. + let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.contentDocument.querySelector(".dialogClose").click(); + + info("waiting for dialog to unload"); + await promiseUnloaded; + }); +}); + +add_task(async function testCantShowQrCode() { + const origStart = FxAccountsPairingFlow.start; + FxAccountsPairingFlow.start = async () => { + throw new Error("boom"); + }; + await runWithPairingDialog(async win => { + let doc = win.document; + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "error" + ); + + // Close the dialog. + let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.contentDocument.querySelector(".dialogClose").click(); + + info("waiting for dialog to unload"); + await promiseUnloaded; + }); + FxAccountsPairingFlow.start = origStart; +}); + +add_task(async function testSwitchToWebContent() { + await runWithPairingDialog(async win => { + let doc = win.document; + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "ready" + ); + + const spySwitchURL = sinon.spy(win.gFxaPairDeviceDialog, "_switchToUrl"); + const emitter = win.gFxaPairDeviceDialog._emitter; + emitter.emit("view:SwitchToWebContent", "about:robots"); + + Assert.equal(spySwitchURL.callCount, 1); + }); +}); + +add_task(async function testError() { + await runWithPairingDialog(async win => { + let doc = win.document; + let qrWrapper = doc.getElementById("qrWrapper"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "ready" + ); + + const emitter = win.gFxaPairDeviceDialog._emitter; + emitter.emit("view:Error"); + + await TestUtils.waitForCondition( + () => qrWrapper.getAttribute("pairing-status") == "error" + ); + + // Close the dialog. + let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload"); + gBrowser.contentDocument.querySelector(".dialogClose").click(); + + info("waiting for dialog to unload"); + await promiseUnloaded; + }); +}); + +async function runWithPairingDialog(test) { + await openPreferencesViaOpenPreferencesAPI("paneSync", { leaveOpen: true }); + + let promiseSubDialogLoaded = promiseLoadSubDialog( + "chrome://browser/content/preferences/fxaPairDevice.xhtml" + ); + gBrowser.contentWindow.gSyncPane.pairAnotherDevice(); + + let win = await promiseSubDialogLoaded; + + await test(win); + + sinon.restore(); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +} diff --git a/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js b/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js new file mode 100644 index 0000000000..8d1fa3c80b --- /dev/null +++ b/browser/components/preferences/tests/browser_warning_permanent_private_browsing.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function checkForPrompt(prefVal) { + return async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.history.custom", true], + ["browser.privatebrowsing.autostart", !prefVal], + ], + }); + + await openPreferencesViaOpenPreferencesAPI("panePrivacy", { + leaveOpen: true, + }); + let doc = gBrowser.contentDocument; + is( + doc.getElementById("historyMode").value, + "custom", + "Expect custom history mode" + ); + + // Stub out the prompt method as an easy way to check it was shown. We throw away + // the tab straight after so don't need to bother restoring it. + let promptFired = false; + doc.defaultView.confirmRestartPrompt = () => { + promptFired = true; + return doc.defaultView.CONFIRM_RESTART_PROMPT_RESTART_NOW; + }; + // Tick the checkbox and pretend the user did it: + let checkbox = doc.getElementById("privateBrowsingAutoStart"); + checkbox.checked = prefVal; + checkbox.doCommand(); + + // Now the prompt should have shown. + ok( + promptFired, + `Expect a prompt when turning permanent private browsing ${ + prefVal ? "on" : "off" + }!` + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + }; +} + +/** + * Check we show the prompt if the permanent private browsing pref is false + * and we flip the checkbox to true. + */ +add_task(checkForPrompt(true)); + +/** + * Check it works in the other direction: + */ +add_task(checkForPrompt(false)); diff --git a/browser/components/preferences/tests/empty_pdf_file.pdf b/browser/components/preferences/tests/empty_pdf_file.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/browser/components/preferences/tests/empty_pdf_file.pdf diff --git a/browser/components/preferences/tests/engine1/manifest.json b/browser/components/preferences/tests/engine1/manifest.json new file mode 100644 index 0000000000..5fa44ea692 --- /dev/null +++ b/browser/components/preferences/tests/engine1/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine1", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine1@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine1", + "search_url": "https://1.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/browser/components/preferences/tests/engine2/manifest.json b/browser/components/preferences/tests/engine2/manifest.json new file mode 100644 index 0000000000..7ab094198b --- /dev/null +++ b/browser/components/preferences/tests/engine2/manifest.json @@ -0,0 +1,27 @@ +{ + "name": "engine2", + "manifest_version": 2, + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "engine2@search.mozilla.org" + } + }, + "hidden": true, + "description": "A small test engine", + "icons": { + "16": "favicon.ico" + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "engine2", + "search_url": "https://2.example.com/search", + "params": [ + { + "name": "q", + "value": "{searchTerms}" + } + ] + } + } +} diff --git a/browser/components/preferences/tests/head.js b/browser/components/preferences/tests/head.js new file mode 100644 index 0000000000..3eb126e1ae --- /dev/null +++ b/browser/components/preferences/tests/head.js @@ -0,0 +1,334 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const kDefaultWait = 2000; + +function is_element_visible(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(!BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function is_element_hidden(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function open_preferences(aCallback) { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences"); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener( + "Initialized", + function () { + aCallback(gBrowser.contentWindow); + }, + { capture: true, once: true } + ); +} + +function openAndLoadSubDialog( + aURL, + aFeatures = null, + aParams = null, + aClosingCallback = null +) { + let promise = promiseLoadSubDialog(aURL); + content.gSubDialog.open( + aURL, + { features: aFeatures, closingCallback: aClosingCallback }, + aParams + ); + return promise; +} + +function promiseLoadSubDialog(aURL) { + return new Promise((resolve, reject) => { + content.gSubDialog._dialogStack.addEventListener( + "dialogopen", + function dialogopen(aEvent) { + if ( + aEvent.detail.dialog._frame.contentWindow.location == "about:blank" + ) { + return; + } + content.gSubDialog._dialogStack.removeEventListener( + "dialogopen", + dialogopen + ); + + is( + aEvent.detail.dialog._frame.contentWindow.location.toString(), + aURL, + "Check the proper URL is loaded" + ); + + // Check visibility + is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible"); + + // Check that stylesheets were injected + let expectedStyleSheetURLs = + aEvent.detail.dialog._injectedStyleSheets.slice(0); + for (let styleSheet of aEvent.detail.dialog._frame.contentDocument + .styleSheets) { + let i = expectedStyleSheetURLs.indexOf(styleSheet.href); + if (i >= 0) { + info("found " + styleSheet.href); + expectedStyleSheetURLs.splice(i, 1); + } + } + is( + expectedStyleSheetURLs.length, + 0, + "All expectedStyleSheetURLs should have been found" + ); + + // Wait for the next event tick to make sure the remaining part of the + // testcase runs after the dialog gets ready for input. + executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow)); + } + ); + }); +} + +async function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) { + let finalPaneEvent = Services.prefs.getBoolPref("identity.fxaccounts.enabled") + ? "sync-pane-loaded" + : "privacy-pane-loaded"; + let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + openPreferences(aPane, aOptions); + let newTabBrowser = gBrowser.selectedBrowser; + + if (!newTabBrowser.contentWindow) { + await BrowserTestUtils.waitForEvent(newTabBrowser, "Initialized", true); + await BrowserTestUtils.waitForEvent(newTabBrowser.contentWindow, "load"); + await finalPrefPaneLoaded; + } + + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + if (!aOptions || !aOptions.leaveOpen) { + gBrowser.removeCurrentTab(); + } + return { selectedPane }; +} + +async function runSearchInput(input) { + let searchInput = gBrowser.contentDocument.getElementById("searchInput"); + searchInput.focus(); + let searchCompletedPromise = BrowserTestUtils.waitForEvent( + gBrowser.contentWindow, + "PreferencesSearchCompleted", + evt => evt.detail == input + ); + EventUtils.sendString(input); + await searchCompletedPromise; +} + +async function evaluateSearchResults( + keyword, + searchResults, + includeExperiments = false +) { + searchResults = Array.isArray(searchResults) + ? searchResults + : [searchResults]; + searchResults.push("header-searchResults"); + + await runSearchInput(keyword); + + let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); + for (let i = 0; i < mainPrefTag.childElementCount; i++) { + let child = mainPrefTag.children[i]; + if (!includeExperiments && child.id?.startsWith("pane-experimental")) { + continue; + } + if (searchResults.includes(child.id)) { + is_element_visible(child, `${child.id} should be in search results`); + } else if (child.id) { + is_element_hidden(child, `${child.id} should not be in search results`); + } + } +} + +function waitForMutation(target, opts, cb) { + return new Promise(resolve => { + let observer = new MutationObserver(() => { + if (!cb || cb(target)) { + observer.disconnect(); + resolve(); + } + }); + observer.observe(target, opts); + }); +} + +// Used to add sample experimental features for testing. To use, create +// a DefinitionServer, then call addDefinition as needed. +class DefinitionServer { + constructor(definitionOverrides = []) { + let { HttpServer } = ChromeUtils.import( + "resource://testing-common/httpd.js" + ); + + this.server = new HttpServer(); + this.server.registerPathHandler("/definitions.json", this); + this.definitions = {}; + + for (const override of definitionOverrides) { + this.addDefinition(override); + } + + this.server.start(); + registerCleanupFunction( + () => new Promise(resolve => this.server.stop(resolve)) + ); + } + + // for nsIHttpRequestHandler + handle(request, response) { + response.write(JSON.stringify(this.definitions)); + } + + get definitionsUrl() { + const { primaryScheme, primaryHost, primaryPort } = this.server.identity; + return `${primaryScheme}://${primaryHost}:${primaryPort}/definitions.json`; + } + + addDefinition(overrides = {}) { + const definition = { + id: "test-feature", + // These l10n IDs are just random so we have some text to display + title: "experimental-features-media-jxl", + description: "pane-experimental-description2", + restartRequired: false, + type: "boolean", + preference: "test.feature", + defaultValue: false, + isPublic: false, + ...overrides, + }; + // convert targeted values, used by fromId + definition.isPublic = { default: definition.isPublic }; + definition.defaultValue = { default: definition.defaultValue }; + this.definitions[definition.id] = definition; + return definition; + } +} + +/** + * Creates observer that waits for and then compares all perm-changes with the observances in order. + * @param {Array} observances permission changes to observe (order is important) + * @returns {Promise} Promise object that resolves once all permission changes have been observed + */ +function createObserveAllPromise(observances) { + // Create new promise that resolves once all items + // in observances array have been observed. + return new Promise(resolve => { + let permObserver = { + observe(aSubject, aTopic, aData) { + if (aTopic != "perm-changed") { + return; + } + + if (!observances.length) { + // See bug 1063410 + return; + } + + let permission = aSubject.QueryInterface(Ci.nsIPermission); + let expected = observances.shift(); + + info( + `observed perm-changed for ${permission.principal.origin} (remaining ${observances.length})` + ); + + is(aData, expected.data, "type of message should be the same"); + for (let prop of ["type", "capability", "expireType"]) { + if (expected[prop]) { + is( + permission[prop], + expected[prop], + `property: "${prop}" should be equal (${permission.principal.origin})` + ); + } + } + + if (expected.origin) { + is( + permission.principal.origin, + expected.origin, + `property: "origin" should be equal (${permission.principal.origin})` + ); + } + + if (!observances.length) { + Services.obs.removeObserver(permObserver, "perm-changed"); + executeSoon(resolve); + } + }, + }; + Services.obs.addObserver(permObserver, "perm-changed"); + }); +} + +/** + * Waits for preference to be set and asserts the value. + * @param {string} pref - Preference key. + * @param {*} expectedValue - Expected value of the preference. + * @param {string} message - Assertion message. + */ +async function waitForAndAssertPrefState(pref, expectedValue, message) { + await TestUtils.waitForPrefChange(pref, value => { + if (value != expectedValue) { + return false; + } + is(value, expectedValue, message); + return true; + }); +} + +/** + * The Relay promo is not shown for distributions with a custom FxA instance, + * since Relay requires an account on our own server. These prefs are set to a + * dummy address by the test harness, filling the prefs with a "user value." + * This temporarily sets the default value equal to the dummy value, so that + * Firefox thinks we've configured the correct FxA server. + * @returns {Promise<MockFxAUtilityFunctions>} { mock, unmock } + */ +async function mockDefaultFxAInstance() { + /** + * @typedef {Object} MockFxAUtilityFunctions + * @property {function():void} mock - Makes the dummy values default, creating + * the illusion of a production FxA instance. + * @property {function():void} unmock - Restores the true defaults, creating + * the illusion of a custom FxA instance. + */ + + const defaultPrefs = Services.prefs.getDefaultBranch(""); + const userPrefs = Services.prefs.getBranch(""); + const realAuth = defaultPrefs.getCharPref("identity.fxaccounts.auth.uri"); + const realRoot = defaultPrefs.getCharPref("identity.fxaccounts.remote.root"); + const mockAuth = userPrefs.getCharPref("identity.fxaccounts.auth.uri"); + const mockRoot = userPrefs.getCharPref("identity.fxaccounts.remote.root"); + const mock = () => { + defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth); + defaultPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot); + userPrefs.clearUserPref("identity.fxaccounts.auth.uri"); + userPrefs.clearUserPref("identity.fxaccounts.remote.root"); + }; + const unmock = () => { + defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", realAuth); + defaultPrefs.setCharPref("identity.fxaccounts.remote.root", realRoot); + userPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth); + userPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot); + }; + + mock(); + registerCleanupFunction(unmock); + + return { mock, unmock }; +} diff --git a/browser/components/preferences/tests/privacypane_tests_perwindow.js b/browser/components/preferences/tests/privacypane_tests_perwindow.js new file mode 100644 index 0000000000..8c376d1705 --- /dev/null +++ b/browser/components/preferences/tests/privacypane_tests_perwindow.js @@ -0,0 +1,388 @@ +// This file gets imported into the same scope as head.js. +/* import-globals-from head.js */ + +async function runTestOnPrivacyPrefPane(testFunc) { + info("runTestOnPrivacyPrefPane entered"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences", + true, + true + ); + let browser = tab.linkedBrowser; + info("loaded about:preferences"); + await browser.contentWindow.gotoPref("panePrivacy"); + info("viewing privacy pane, executing testFunc"); + await testFunc(browser.contentWindow); + BrowserTestUtils.removeTab(tab); +} + +function controlChanged(element) { + element.doCommand(); +} + +// We can only test the panes that don't trigger a preference update +function test_pane_visibility(win) { + let modes = { + remember: "historyRememberPane", + custom: "historyCustomPane", + }; + + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let historypane = win.document.getElementById("historyPane"); + ok(historypane, "history mode pane should exist"); + + for (let mode in modes) { + historymode.value = mode; + controlChanged(historymode); + is( + historypane.selectedPanel, + win.document.getElementById(modes[mode]), + "The correct pane should be selected for the " + mode + " mode" + ); + is_element_visible( + historypane.selectedPanel, + "Correct pane should be visible for the " + mode + " mode" + ); + } +} + +function test_dependent_elements(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let pbautostart = win.document.getElementById("privateBrowsingAutoStart"); + ok(pbautostart, "the private browsing auto-start checkbox should exist"); + let controls = [ + win.document.getElementById("rememberHistory"), + win.document.getElementById("rememberForms"), + win.document.getElementById("deleteOnClose"), + win.document.getElementById("alwaysClear"), + ]; + controls.forEach(function (control) { + ok(control, "the dependent controls should exist"); + }); + let independents = [ + win.document.getElementById("contentBlockingBlockCookiesCheckbox"), + ]; + independents.forEach(function (control) { + ok(control, "the independent controls should exist"); + }); + let cookieexceptions = win.document.getElementById("cookieExceptions"); + ok(cookieexceptions, "the cookie exceptions button should exist"); + let deleteOnCloseCheckbox = win.document.getElementById("deleteOnClose"); + ok(deleteOnCloseCheckbox, "the delete on close checkbox should exist"); + let alwaysclear = win.document.getElementById("alwaysClear"); + ok(alwaysclear, "the clear data on close checkbox should exist"); + let rememberhistory = win.document.getElementById("rememberHistory"); + ok(rememberhistory, "the remember history checkbox should exist"); + let rememberforms = win.document.getElementById("rememberForms"); + ok(rememberforms, "the remember forms checkbox should exist"); + let alwaysclearsettings = win.document.getElementById("clearDataSettings"); + ok(alwaysclearsettings, "the clear data settings button should exist"); + + function expect_disabled(disabled) { + controls.forEach(function (control) { + is( + control.disabled, + disabled, + control.getAttribute("id") + + " should " + + (disabled ? "" : "not ") + + "be disabled" + ); + }); + if (disabled) { + ok( + !alwaysclear.checked, + "the clear data on close checkbox value should be as expected" + ); + ok( + !rememberhistory.checked, + "the remember history checkbox value should be as expected" + ); + ok( + !rememberforms.checked, + "the remember forms checkbox value should be as expected" + ); + } + } + function check_independents(expected) { + independents.forEach(function (control) { + is( + control.disabled, + expected, + control.getAttribute("id") + + " should " + + (expected ? "" : "not ") + + "be disabled" + ); + }); + + ok( + !cookieexceptions.disabled, + "the cookie exceptions button should never be disabled" + ); + ok( + alwaysclearsettings.disabled, + "the clear data settings button should always be disabled" + ); + } + + // controls should only change in custom mode + historymode.value = "remember"; + controlChanged(historymode); + expect_disabled(false); + check_independents(false); + + // setting the mode to custom shouldn't change anything + historymode.value = "custom"; + controlChanged(historymode); + expect_disabled(false); + check_independents(false); +} + +function test_dependent_cookie_elements(win) { + let deleteOnCloseCheckbox = win.document.getElementById("deleteOnClose"); + let deleteOnCloseNote = win.document.getElementById("deleteOnCloseNote"); + let blockCookiesMenu = win.document.getElementById("blockCookiesMenu"); + + let controls = [blockCookiesMenu, deleteOnCloseCheckbox]; + controls.forEach(function (control) { + ok(control, "the dependent cookie controls should exist"); + }); + let blockCookiesCheckbox = win.document.getElementById( + "contentBlockingBlockCookiesCheckbox" + ); + ok(blockCookiesCheckbox, "the block cookies checkbox should exist"); + + function expect_disabled(disabled, c = controls) { + c.forEach(function (control) { + is( + control.disabled, + disabled, + control.getAttribute("id") + + " should " + + (disabled ? "" : "not ") + + "be disabled" + ); + }); + } + + blockCookiesCheckbox.checked = true; + controlChanged(blockCookiesCheckbox); + expect_disabled(false); + + blockCookiesCheckbox.checked = false; + controlChanged(blockCookiesCheckbox); + expect_disabled(true, [blockCookiesMenu]); + expect_disabled(false, [deleteOnCloseCheckbox]); + is_element_hidden( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be hidden." + ); + + blockCookiesMenu.value = "always"; + controlChanged(blockCookiesMenu); + expect_disabled(true, [deleteOnCloseCheckbox]); + expect_disabled(false, [blockCookiesMenu]); + is_element_hidden( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be hidden." + ); + + if (win.contentBlockingCookiesAndSiteDataRejectTrackersEnabled) { + blockCookiesMenu.value = "trackers"; + } else { + blockCookiesMenu.value = "unvisited"; + } + controlChanged(blockCookiesMenu); + expect_disabled(false); + + let historymode = win.document.getElementById("historyMode"); + + // The History mode setting for "never remember history" should still + // disable the "keep cookies until..." menu. + historymode.value = "dontremember"; + controlChanged(historymode); + expect_disabled(true, [deleteOnCloseCheckbox]); + is_element_visible( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be visible." + ); + expect_disabled(false, [blockCookiesMenu]); + + historymode.value = "remember"; + controlChanged(historymode); + expect_disabled(false); + is_element_hidden( + deleteOnCloseNote, + "The notice for delete on close in permanent private browsing mode should be hidden." + ); +} + +function test_dependent_clearonclose_elements(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let pbautostart = win.document.getElementById("privateBrowsingAutoStart"); + ok(pbautostart, "the private browsing auto-start checkbox should exist"); + let alwaysclear = win.document.getElementById("alwaysClear"); + ok(alwaysclear, "the clear data on close checkbox should exist"); + let alwaysclearsettings = win.document.getElementById("clearDataSettings"); + ok(alwaysclearsettings, "the clear data settings button should exist"); + + function expect_disabled(disabled) { + is( + alwaysclearsettings.disabled, + disabled, + "the clear data settings should " + + (disabled ? "" : "not ") + + "be disabled" + ); + } + + historymode.value = "custom"; + controlChanged(historymode); + pbautostart.checked = false; + controlChanged(pbautostart); + alwaysclear.checked = false; + controlChanged(alwaysclear); + expect_disabled(true); + + alwaysclear.checked = true; + controlChanged(alwaysclear); + expect_disabled(false); + + alwaysclear.checked = false; + controlChanged(alwaysclear); + expect_disabled(true); +} + +async function test_dependent_prefs(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + let controls = [ + win.document.getElementById("rememberHistory"), + win.document.getElementById("rememberForms"), + ]; + controls.forEach(function (control) { + ok(control, "the micro-management controls should exist"); + }); + + function expect_checked(checked) { + controls.forEach(function (control) { + is( + control.checked, + checked, + control.getAttribute("id") + + " should " + + (checked ? "" : "not ") + + "be checked" + ); + }); + } + + // controls should be checked in remember mode + historymode.value = "remember"; + controlChanged(historymode); + // Initial updates from prefs are not sync, so wait: + await TestUtils.waitForCondition( + () => controls[0].getAttribute("checked") == "true" + ); + expect_checked(true); + + // even if they're unchecked in custom mode + historymode.value = "custom"; + controlChanged(historymode); + controls.forEach(function (control) { + control.checked = false; + controlChanged(control); + }); + expect_checked(false); + historymode.value = "remember"; + controlChanged(historymode); + expect_checked(true); +} + +function test_historymode_retention(mode, expect) { + return function test_historymode_retention_fn(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + + if ( + (historymode.value == "remember" && mode == "dontremember") || + (historymode.value == "dontremember" && mode == "remember") || + (historymode.value == "custom" && mode == "dontremember") + ) { + return; + } + + if (expect !== undefined) { + is( + historymode.value, + expect, + "history mode is expected to remain " + expect + ); + } + + historymode.value = mode; + controlChanged(historymode); + }; +} + +function test_custom_retention(controlToChange, expect, valueIncrement) { + return function test_custom_retention_fn(win) { + let historymode = win.document.getElementById("historyMode"); + ok(historymode, "history mode menulist should exist"); + + if (expect !== undefined) { + is( + historymode.value, + expect, + "history mode is expected to remain " + expect + ); + } + + historymode.value = "custom"; + controlChanged(historymode); + + controlToChange = win.document.getElementById(controlToChange); + ok(controlToChange, "the control to change should exist"); + switch (controlToChange.localName) { + case "checkbox": + controlToChange.checked = !controlToChange.checked; + break; + case "menulist": + controlToChange.value = valueIncrement; + break; + } + controlChanged(controlToChange); + }; +} + +const gPrefCache = new Map(); + +function cache_preferences(win) { + let prefs = win.Preferences.getAll(); + for (let pref of prefs) { + gPrefCache.set(pref.id, pref.value); + } +} + +function reset_preferences(win) { + let prefs = win.Preferences.getAll(); + // Avoid assigning undefined, which means clearing a "user"/test pref value + for (let pref of prefs) { + if (gPrefCache.has(pref.id)) { + pref.value = gPrefCache.get(pref.id); + } + } +} + +function run_test_subset(subset) { + info("subset: " + Array.from(subset, x => x.name).join(",") + "\n"); + let tests = [cache_preferences, ...subset, reset_preferences]; + for (let test of tests) { + add_task(runTestOnPrivacyPrefPane.bind(undefined, test)); + } +} diff --git a/browser/components/preferences/tests/siteData/browser.ini b/browser/components/preferences/tests/siteData/browser.ini new file mode 100644 index 0000000000..deb8e82f7d --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +support-files = + head.js + site_data_test.html + service_worker_test.html + service_worker_test.js + offline/offline.html + offline/manifest.appcache + +[browser_clearSiteData.js] +[browser_siteData.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_siteData2.js] +skip-if = + win10_2004 && (!debug && !asan) # Bug 1669937 + win11_2009 && (!debug && !asan) # Bug 1797751 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + apple_catalina && debug # Bug 1775910 +[browser_siteData3.js] +[browser_siteData_multi_select.js] +skip-if = tsan # Bug 1683730 diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/tests/siteData/browser_clearSiteData.js new file mode 100644 index 0000000000..217656707e --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_clearSiteData.js @@ -0,0 +1,219 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +async function testClearData(clearSiteData, clearCache) { + PermissionTestUtils.add( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage", + Services.perms.ALLOW_ACTION + ); + + // Open a test site which saves into appcache. + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Fill indexedDB with test data. + // Don't wait for the page to load, to register the content event handler as quickly as possible. + // If this test goes intermittent, we might have to tell the page to wait longer before + // firing the event. + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Register some service workers. + await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL); + await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // Test the initial states. + let cacheUsage = await SiteDataManager.getCacheSize(); + let quotaUsage = await SiteDataTestUtils.getQuotaUsage( + TEST_QUOTA_USAGE_ORIGIN + ); + let totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(cacheUsage, 0, "The cache usage should not be 0"); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + + let initialSizeLabelValue = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let sizeLabel = content.document.getElementById("totalSiteDataSize"); + return sizeLabel.textContent; + } + ); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearSiteDataButton = doc.getElementById("clearSiteDataButton"); + + let dialogOpened = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml" + ); + clearSiteDataButton.doCommand(); + let dialogWin = await dialogOpened; + + // Convert the usage numbers in the same way the UI does it to assert + // that they're displayed in the dialog. + let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage); + // For cache we just assert that the right unit (KB, probably) is displayed, + // since we've had cache intermittently changing under our feet. + let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage); + + let clearSiteDataCheckbox = + dialogWin.document.getElementById("clearSiteData"); + let clearCacheCheckbox = dialogWin.document.getElementById("clearCache"); + // The usage details are filled asynchronously, so we assert that they're present by + // waiting for them to be filled in. + await Promise.all([ + TestUtils.waitForCondition( + () => + clearSiteDataCheckbox.label && + clearSiteDataCheckbox.label.includes(convertedTotalUsage), + "Should show the quota usage" + ), + TestUtils.waitForCondition( + () => + clearCacheCheckbox.label && + clearCacheCheckbox.label.includes(convertedCacheUnit), + "Should show the cache usage" + ), + ]); + + // Check the boxes according to our test input. + clearSiteDataCheckbox.checked = clearSiteData; + clearCacheCheckbox.checked = clearCache; + + // Some additional promises/assertions to wait for + // when deleting site data. + let acceptPromise; + let updatePromise; + let cookiesClearedPromise; + if (clearSiteData) { + acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + updatePromise = promiseSiteDataManagerSitesUpdated(); + cookiesClearedPromise = promiseCookiesCleared(); + } + + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + + let clearButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + if (!clearSiteData && !clearCache) { + // Simulate user input on one of the checkboxes to trigger the event listener for + // disabling the clearButton. + clearCacheCheckbox.doCommand(); + // Check that the clearButton gets disabled by unchecking both options. + await TestUtils.waitForCondition( + () => clearButton.disabled, + "Clear button should be disabled" + ); + let cancelButton = dialogWin.document + .querySelector("dialog") + .getButton("cancel"); + // Cancel, since we can't delete anything. + cancelButton.click(); + } else { + // Delete stuff! + clearButton.click(); + } + + // For site data we display an extra warning dialog, make sure + // to accept it. + if (clearSiteData) { + await acceptPromise; + } + + await dialogClosed; + + if (clearCache) { + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getCacheSize(); + return usage == 0; + }, "The cache usage should be removed"); + } else { + Assert.greater( + await SiteDataManager.getCacheSize(), + 0, + "The cache usage should not be 0" + ); + } + + if (clearSiteData) { + await updatePromise; + await cookiesClearedPromise; + await promiseServiceWorkersCleared(); + + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getTotalUsage(); + return usage == 0; + }, "The total usage should be removed"); + } else { + quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN); + totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + } + + if (clearCache || clearSiteData) { + // Check that the size label in about:preferences updates after we cleared data. + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ initialSizeLabelValue }], + async function (opts) { + let sizeLabel = content.document.getElementById("totalSiteDataSize"); + await ContentTaskUtils.waitForCondition( + () => sizeLabel.textContent != opts.initialSizeLabelValue, + "Site data size label should have updated." + ); + } + ); + } + + let permission = PermissionTestUtils.getPermissionObject( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage" + ); + is( + clearSiteData ? permission : permission.capability, + clearSiteData ? null : Services.perms.ALLOW_ACTION, + "Should have the correct permission state." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SiteDataManager.removeAll(); +} + +// Test opening the "Clear All Data" dialog and cancelling. +add_task(async function () { + await testClearData(false, false); +}); + +// Test opening the "Clear All Data" dialog and removing all site data. +add_task(async function () { + await testClearData(true, false); +}); + +// Test opening the "Clear All Data" dialog and removing all cache. +add_task(async function () { + await testClearData(false, true); +}); + +// Test opening the "Clear All Data" dialog and removing everything. +add_task(async function () { + await testClearData(true, true); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData.js b/browser/components/preferences/tests/siteData/browser_siteData.js new file mode 100644 index 0000000000..d3a73d6f4b --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData.js @@ -0,0 +1,400 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function getPersistentStoragePermStatus(origin) { + let uri = Services.io.newURI(origin); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + return Services.perms.testExactPermissionFromPrincipal( + principal, + "persistent-storage" + ); +} + +// Test listing site using quota usage or site using appcache +// This is currently disabled because of bug 1414751. +add_task(async function () { + // Open a test site which would save into appcache + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Open a test site which would save into quota manager + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + let updatedPromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatedPromise; + await openSiteDataSettingsDialog(); + let dialog = content.gSubDialog._topDialog; + let dialogFrame = dialog._frame; + let frameDoc = dialogFrame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 2, "Should list sites using quota usage or appcache"); + + let appcacheSite = frameDoc.querySelector( + `richlistitem[host="${TEST_OFFLINE_HOST}"]` + ); + ok(appcacheSite, "Should list site using appcache"); + + let qoutaUsageSite = frameDoc.querySelector( + `richlistitem[host="${TEST_QUOTA_USAGE_HOST}"]` + ); + ok(qoutaUsageSite, "Should list site using quota usage"); + + // Always remember to clean up + await new Promise(resolve => { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TEST_QUOTA_USAGE_ORIGIN + ); + let request = Services.qms.clearStoragesForPrincipal( + principal, + null, + null, + true + ); + request.callback = resolve; + }); + + await SiteDataManager.removeAll(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}).skip(); // Bug 1414751 + +// Test buttons are disabled and loading message shown while updating sites +add_task(async function () { + let updatedPromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatedPromise; + let cacheSize = await SiteDataManager.getCacheSize(); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearBtn = doc.getElementById("clearSiteDataButton"); + let settingsButton = doc.getElementById("siteDataSettings"); + let totalSiteDataSizeLabel = doc.getElementById("totalSiteDataSize"); + is( + clearBtn.disabled, + false, + "Should enable clear button after sites updated" + ); + is( + settingsButton.disabled, + false, + "Should enable settings button after sites updated" + ); + await SiteDataManager.getTotalUsage().then(usage => { + let [value, unit] = DownloadUtils.convertByteUnits(usage + cacheSize); + Assert.deepEqual( + doc.l10n.getAttributes(totalSiteDataSizeLabel), + { + id: "sitedata-total-size", + args: { value, unit }, + }, + "Should show the right total site data size" + ); + }); + + Services.obs.notifyObservers(null, "sitedatamanager:updating-sites"); + is( + clearBtn.disabled, + true, + "Should disable clear button while updating sites" + ); + is( + settingsButton.disabled, + true, + "Should disable settings button while updating sites" + ); + Assert.deepEqual( + doc.l10n.getAttributes(totalSiteDataSizeLabel), + { + id: "sitedata-total-size-calculating", + args: null, + }, + "Should show the loading message while updating" + ); + + Services.obs.notifyObservers(null, "sitedatamanager:sites-updated"); + is( + clearBtn.disabled, + false, + "Should enable clear button after sites updated" + ); + is( + settingsButton.disabled, + false, + "Should enable settings button after sites updated" + ); + cacheSize = await SiteDataManager.getCacheSize(); + await SiteDataManager.getTotalUsage().then(usage => { + let [value, unit] = DownloadUtils.convertByteUnits(usage + cacheSize); + Assert.deepEqual( + doc.l10n.getAttributes(totalSiteDataSizeLabel), + { + id: "sitedata-total-size", + args: { value, unit }, + }, + "Should show the right total site data size" + ); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test clearing service worker through the settings panel +add_task(async function () { + // Register a test service worker + await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + // Test the initial states + await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL); + // Open the Site Data Settings panel and remove the site + await openSiteDataSettingsDialog(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + let updatePromise = promiseSiteDataManagerSitesUpdated(); + SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ TEST_OFFLINE_HOST }], + args => { + let host = args.TEST_OFFLINE_HOST; + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + let sitesList = frameDoc.getElementById("sitesList"); + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + if (site) { + let removeBtn = frameDoc.getElementById("removeSelected"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + site.click(); + removeBtn.doCommand(); + saveBtn.doCommand(); + } else { + ok(false, `Should have one site of ${host}`); + } + } + ); + await acceptRemovePromise; + await updatePromise; + await promiseServiceWorkersCleared(); + await SiteDataManager.removeAll(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test showing and removing sites with cookies. +add_task(async function () { + // Add some test cookies. + let uri = Services.io.newURI("https://example.com"); + let uri2 = Services.io.newURI("https://example.org"); + Services.cookies.add( + uri.host, + uri.pathQueryRef, + "test1", + "1", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Services.cookies.add( + uri.host, + uri.pathQueryRef, + "test2", + "2", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Services.cookies.add( + uri2.host, + uri2.pathQueryRef, + "test1", + "1", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Ensure that private browsing cookies are ignored. + Services.cookies.add( + uri.host, + uri.pathQueryRef, + "test3", + "3", + false, + false, + false, + Date.now() + 1000 * 60 * 60, + { privateBrowsingId: 1 }, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Get the exact creation date from the cookies (to avoid intermittents + // from minimal time differences, since we round up to minutes). + let cookies1 = Services.cookies.getCookiesFromHost(uri.host, {}); + let cookies2 = Services.cookies.getCookiesFromHost(uri2.host, {}); + // We made two valid cookies for example.com. + let cookie1 = cookies1[1]; + let cookie2 = cookies2[0]; + + let fullFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "short", + }); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // Open the site data manager and remove one site. + await openSiteDataSettingsDialog(); + let creationDate1 = new Date(cookie1.lastAccessed / 1000); + let creationDate1Formatted = fullFormatter.format(creationDate1); + let creationDate2 = new Date(cookie2.lastAccessed / 1000); + let creationDate2Formatted = fullFormatter.format(creationDate2); + let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [ + { + creationDate1Formatted, + creationDate2Formatted, + }, + ], + function (args) { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 2, "Should list two sites with cookies"); + let sitesList = frameDoc.getElementById("sitesList"); + let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`); + let site2 = sitesList.querySelector(`richlistitem[host="example.org"]`); + + let columns = site1.querySelectorAll(".item-box > label"); + let boxes = site1.querySelectorAll(".item-box"); + is(columns[0].value, "example.com", "Should show the correct host."); + is(columns[1].value, "2", "Should show the correct number of cookies."); + is(columns[2].value, "", "Should show no site data."); + is( + /(now|second)/.test(columns[3].value), + true, + "Should show the relative date." + ); + is( + boxes[3].getAttribute("tooltiptext"), + args.creationDate1Formatted, + "Should show the correct date." + ); + + columns = site2.querySelectorAll(".item-box > label"); + boxes = site2.querySelectorAll(".item-box"); + is(columns[0].value, "example.org", "Should show the correct host."); + is(columns[1].value, "1", "Should show the correct number of cookies."); + is(columns[2].value, "", "Should show no site data."); + is( + /(now|second)/.test(columns[3].value), + true, + "Should show the relative date." + ); + is( + boxes[3].getAttribute("tooltiptext"), + args.creationDate2Formatted, + "Should show the correct date." + ); + + let removeBtn = frameDoc.getElementById("removeSelected"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + site2.click(); + removeBtn.doCommand(); + saveBtn.doCommand(); + } + ); + await removeDialogOpenPromise; + + await TestUtils.waitForCondition( + () => Services.cookies.countCookiesFromHost(uri2.host) == 0, + "Cookies from the first host should be cleared" + ); + is( + Services.cookies.countCookiesFromHost(uri.host), + 2, + "Cookies from the second host should not be cleared" + ); + + // Open the site data manager and remove another site. + await openSiteDataSettingsDialog(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ creationDate1Formatted }], + function (args) { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 1, "Should list one site with cookies"); + let sitesList = frameDoc.getElementById("sitesList"); + let site1 = sitesList.querySelector(`richlistitem[host="example.com"]`); + + let columns = site1.querySelectorAll(".item-box > label"); + let boxes = site1.querySelectorAll(".item-box"); + is(columns[0].value, "example.com", "Should show the correct host."); + is(columns[1].value, "2", "Should show the correct number of cookies."); + is(columns[2].value, "", "Should show no site data."); + is( + /(now|second)/.test(columns[3].value), + true, + "Should show the relative date." + ); + is( + boxes[3].getAttribute("tooltiptext"), + args.creationDate1Formatted, + "Should show the correct date." + ); + + let removeBtn = frameDoc.getElementById("removeSelected"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + site1.click(); + removeBtn.doCommand(); + saveBtn.doCommand(); + } + ); + await acceptRemovePromise; + + await TestUtils.waitForCondition( + () => Services.cookies.countCookiesFromHost(uri.host) == 0, + "Cookies from the second host should be cleared" + ); + + await openSiteDataSettingsDialog(); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is(siteItems.length, 0, "Should list no sites with cookies"); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData2.js b/browser/components/preferences/tests/siteData/browser_siteData2.js new file mode 100644 index 0000000000..863a8dcefe --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData2.js @@ -0,0 +1,475 @@ +"use strict"; + +function assertAllSitesNotListed(win) { + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let removeAllBtn = frameDoc.getElementById("removeAll"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + is(sites.length, 0, "Should not list all sites"); + is(removeBtn.disabled, true, "Should disable the removeSelected button"); + is(removeAllBtn.disabled, true, "Should disable the removeAllBtn button"); +} + +// Test selecting and removing all sites one by one +add_task(async function test_selectRemove() { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = null; + let saveBtn = null; + let cancelBtn = null; + let settingsDialogClosePromise = null; + + // Test the initial state + assertSitesListed(doc, hosts); + + // Test the "Cancel" button + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(win); + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button but cancelling save + let cancelPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel"); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(win); + saveBtn.doCommand(); + await cancelPromise; + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button and accepting save + let acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + settingsDialogClosePromise = promiseSettingsDialogClose(); + updatePromise = promiseSiteDataManagerSitesUpdated(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + removeAllSitesOneByOne(); + assertAllSitesNotListed(win); + saveBtn.doCommand(); + await acceptPromise; + await settingsDialogClosePromise; + await updatePromise; + await openSiteDataSettingsDialog(); + assertAllSitesNotListed(win); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + function removeAllSitesOneByOne() { + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + let sitesList = frameDoc.getElementById("sitesList"); + let sites = sitesList.getElementsByTagName("richlistitem"); + for (let i = sites.length - 1; i >= 0; --i) { + sites[i].click(); + removeBtn.doCommand(); + } + } +}); + +// Test selecting and removing partial sites +add_task(async function test_removePartialSites() { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + { + usage: 1024, + origin: "https://s3-us-west-2.amazonaws.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://127.0.0.1", + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = null; + let saveBtn = null; + let cancelBtn = null; + let removeDialogOpenPromise = null; + let settingsDialogClosePromise = null; + + // Test the initial state + assertSitesListed(doc, hosts); + + // Test the "Cancel" button + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + await removeSelectedSite(hosts.slice(0, 2)); + assertSitesListed(doc, hosts.slice(2)); + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button but canceling save + removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "cancel", + REMOVE_DIALOG_URL + ); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + cancelBtn = frameDoc.querySelector("dialog").getButton("cancel"); + await removeSelectedSite(hosts.slice(0, 2)); + assertSitesListed(doc, hosts.slice(2)); + saveBtn.doCommand(); + await removeDialogOpenPromise; + cancelBtn.doCommand(); + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts); + + // Test the "Save Changes" button and accepting save + removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + settingsDialogClosePromise = promiseSettingsDialogClose(); + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + await removeSelectedSite(hosts.slice(0, 2)); + assertSitesListed(doc, hosts.slice(2)); + saveBtn.doCommand(); + await removeDialogOpenPromise; + await settingsDialogClosePromise; + await openSiteDataSettingsDialog(); + assertSitesListed(doc, hosts.slice(2)); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + function removeSelectedSite(removeHosts) { + frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + is( + removeBtn.disabled, + true, + "Should start with disabled removeSelected button" + ); + let sitesList = frameDoc.getElementById("sitesList"); + removeHosts.forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + if (site) { + site.click(); + let currentSelectedIndex = sitesList.selectedIndex; + is( + removeBtn.disabled, + false, + "Should enable the removeSelected button" + ); + removeBtn.doCommand(); + let newSelectedIndex = sitesList.selectedIndex; + if (currentSelectedIndex >= sitesList.itemCount) { + is(newSelectedIndex, currentSelectedIndex - 1); + } else { + is(newSelectedIndex, currentSelectedIndex); + } + } else { + ok(false, `Should not select and remove inexistent site of ${host}`); + } + }); + } +}); + +// Test searching and then removing only visible sites +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + // Search "foo" to only list foo.com sites + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let searchBox = frameDoc.getElementById("searchBox"); + searchBox.value = "xyz"; + searchBox.doCommand(); + assertSitesListed( + doc, + hosts.filter(host => host.includes("xyz")) + ); + + // Test only removing all visible sites listed + updatePromise = promiseSiteDataManagerSitesUpdated(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + let settingsDialogClosePromise = promiseSettingsDialogClose(); + let removeAllBtn = frameDoc.getElementById("removeAll"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + removeAllBtn.doCommand(); + saveBtn.doCommand(); + await acceptRemovePromise; + await settingsDialogClosePromise; + await updatePromise; + await openSiteDataSettingsDialog(); + assertSitesListed( + doc, + hosts.filter(host => !host.includes("xyz")) + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test dynamically clearing all site data +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + // Test the initial state + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + let doc = gBrowser.selectedBrowser.contentDocument; + assertSitesListed(doc, hosts); + + await addTestData([ + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + // Test clearing all site data dynamically + let win = gBrowser.selectedBrowser.contentWindow; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + updatePromise = promiseSiteDataManagerSitesUpdated(); + let acceptRemovePromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + let settingsDialogClosePromise = promiseSettingsDialogClose(); + let removeAllBtn = frameDoc.getElementById("removeAll"); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + removeAllBtn.doCommand(); + saveBtn.doCommand(); + await acceptRemovePromise; + await settingsDialogClosePromise; + await updatePromise; + await openSiteDataSettingsDialog(); + assertAllSitesNotListed(win); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Tests clearing search box content via backspace does not delete site data +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let searchBox = frameDoc.getElementById("searchBox"); + searchBox.value = "xyz"; + searchBox.doCommand(); + assertSitesListed( + doc, + hosts.filter(host => host.includes("xyz")) + ); + + // Make sure the focus is on the search box + searchBox.focus(); + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + } else { + EventUtils.synthesizeKey("VK_DELETE", {}, win); + } + assertSitesListed( + doc, + hosts.filter(host => host.includes("xyz")) + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Tests remove site data via backspace +add_task(async function () { + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + ]); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await openSiteDataSettingsDialog(); + + let win = gBrowser.selectedBrowser.contentWindow; + let doc = gBrowser.selectedBrowser.contentDocument; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + // Test initial state + assertSitesListed(doc, hosts); + + let sitesList = frameDoc.getElementById("sitesList"); + let site = sitesList.querySelector(`richlistitem[host="xyz.com"]`); + if (site) { + // Move the focus from the search box to the list and select an item + sitesList.focus(); + site.click(); + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + } else { + EventUtils.synthesizeKey("VK_DELETE", {}, win); + } + } + + assertSitesListed( + doc, + hosts.filter(host => !host.includes("xyz")) + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData3.js b/browser/components/preferences/tests/siteData/browser_siteData3.js new file mode 100644 index 0000000000..58aa5bf1b9 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData3.js @@ -0,0 +1,327 @@ +"use strict"; + +// Test not displaying sites which store 0 byte and don't have persistent storage. +add_task(async function test_exclusions() { + let hosts = await addTestData([ + { + usage: 0, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 0, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024, + origin: "http://email.bar.com", + persisted: false, + }, + { + usage: 0, + origin: "http://cookies.bar.com", + cookies: 5, + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + let doc = gBrowser.selectedBrowser.contentDocument; + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + assertSitesListed( + doc, + hosts.filter(host => host != "shopping.xyz.com") + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test grouping and listing sites across scheme, port and origin attributes by base domain. +add_task(async function test_grouping() { + let quotaUsage = 7000000; + let testData = [ + { + usage: quotaUsage, + origin: "https://account.xyz.com^userContextId=1", + cookies: 2, + persisted: true, + }, + { + usage: quotaUsage, + origin: "https://account.xyz.com", + cookies: 1, + persisted: false, + }, + { + usage: quotaUsage, + origin: "https://account.xyz.com:123", + cookies: 1, + persisted: false, + }, + { + usage: quotaUsage, + origin: "http://account.xyz.com", + cookies: 1, + persisted: false, + }, + { + usage: quotaUsage, + origin: "http://search.xyz.com", + cookies: 3, + persisted: false, + }, + { + usage: quotaUsage, + origin: "http://advanced.search.xyz.com", + cookies: 3, + persisted: true, + }, + { + usage: quotaUsage, + origin: "http://xyz.com", + cookies: 1, + persisted: false, + }, + ]; + await addTestData(testData); + + let updatedPromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatedPromise; + await openSiteDataSettingsDialog(); + let win = gBrowser.selectedBrowser.contentWindow; + let dialogFrame = win.gSubDialog._topDialog._frame; + let frameDoc = dialogFrame.contentDocument; + + let siteItems = frameDoc.getElementsByTagName("richlistitem"); + is( + siteItems.length, + 1, + "Should group sites across scheme, port and origin attributes" + ); + + let columns = siteItems[0].querySelectorAll(".item-box > label"); + + let expected = "xyz.com"; + is(columns[0].value, expected, "Should group and list sites by host"); + + let cookieCount = testData.reduce((count, { cookies }) => count + cookies, 0); + is( + columns[1].value, + cookieCount.toString(), + "Should group cookies across scheme, port and origin attributes" + ); + + let [value, unit] = DownloadUtils.convertByteUnits(quotaUsage * 4); + let l10nAttributes = frameDoc.l10n.getAttributes(columns[2]); + is( + l10nAttributes.id, + "site-storage-persistent", + "Should show the site as persistent if one origin is persistent." + ); + // The shown quota can be slightly larger than the raw data we put in (though it should + // never be smaller), but that doesn't really matter to us since we only want to test that + // the site data dialog accumulates this into a single column. + ok( + parseFloat(l10nAttributes.args.value) >= parseFloat(value), + "Should show the correct accumulated quota size." + ); + is( + l10nAttributes.args.unit, + unit, + "Should show the correct quota size unit." + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test sorting +add_task(async function test_sorting() { + let testData = [ + { + usage: 1024, + origin: "https://account.xyz.com", + cookies: 6, + persisted: true, + }, + { + usage: 1024 * 2, + origin: "https://books.foo.com", + cookies: 0, + persisted: false, + }, + { + usage: 1024 * 3, + origin: "http://cinema.bar.com", + cookies: 3, + persisted: true, + }, + { + usage: 1024 * 3, + origin: "http://vod.bar.com", + cookies: 2, + persisted: false, + }, + ]; + + await addTestData(testData); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let dialog = content.gSubDialog._topDialog; + let dialogFrame = dialog._frame; + let frameDoc = dialogFrame.contentDocument; + let hostCol = frameDoc.getElementById("hostCol"); + let usageCol = frameDoc.getElementById("usageCol"); + let cookiesCol = frameDoc.getElementById("cookiesCol"); + let sitesList = frameDoc.getElementById("sitesList"); + + function getHostOrder() { + let siteItems = sitesList.getElementsByTagName("richlistitem"); + return Array.from(siteItems).map(item => item.getAttribute("host")); + } + + // Test default sorting by usage, descending. + Assert.deepEqual( + getHostOrder(), + ["bar.com", "foo.com", "xyz.com"], + "Has sorted descending by usage" + ); + + // Test sorting on the usage column + usageCol.click(); + Assert.deepEqual( + getHostOrder(), + ["xyz.com", "foo.com", "bar.com"], + "Has sorted ascending by usage" + ); + usageCol.click(); + Assert.deepEqual( + getHostOrder(), + ["bar.com", "foo.com", "xyz.com"], + "Has sorted descending by usage" + ); + + // Test sorting on the host column + hostCol.click(); + Assert.deepEqual( + getHostOrder(), + ["bar.com", "foo.com", "xyz.com"], + "Has sorted ascending by base domain" + ); + hostCol.click(); + Assert.deepEqual( + getHostOrder(), + ["xyz.com", "foo.com", "bar.com"], + "Has sorted descending by base domain" + ); + + // Test sorting on the cookies column + cookiesCol.click(); + Assert.deepEqual( + getHostOrder(), + ["foo.com", "bar.com", "xyz.com"], + "Has sorted ascending by cookies" + ); + cookiesCol.click(); + Assert.deepEqual( + getHostOrder(), + ["xyz.com", "bar.com", "foo.com"], + "Has sorted descending by cookies" + ); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +// Test single entry removal +add_task(async function test_single_entry_removal() { + let testData = await addTestData([ + { + usage: 1024, + origin: "https://xyz.com", + cookies: 6, + persisted: true, + }, + { + usage: 1024 * 3, + origin: "http://bar.com", + cookies: 2, + persisted: false, + }, + ]); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let dialog = content.gSubDialog._topDialog; + let dialogFrame = dialog._frame; + let frameDoc = dialogFrame.contentDocument; + + let sitesList = frameDoc.getElementById("sitesList"); + let host = testData[0]; + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + sitesList.addItemToSelection(site); + frameDoc.getElementById("removeSelected").doCommand(); + let saveChangesButton = frameDoc.querySelector("dialog").getButton("accept"); + let dialogOpened = BrowserTestUtils.promiseAlertDialogOpen( + null, + REMOVE_DIALOG_URL + ); + setTimeout(() => saveChangesButton.doCommand(), 0); + let dialogWin = await dialogOpened; + let rootElement = dialogWin.document.getElementById( + "SiteDataRemoveSelectedDialog" + ); + is(rootElement.classList.length, 1, "There should only be one class set"); + is( + rootElement.classList[0], + "single-entry", + "The only class set should be single-entry (to hide the list)" + ); + let description = dialogWin.document.getElementById("removing-description"); + is( + description.getAttribute("data-l10n-id"), + "site-data-removing-single-desc", + "The description for single site should be selected" + ); + + let removalList = dialogWin.document.getElementById("removalList"); + is( + BrowserTestUtils.is_visible(removalList), + false, + "The removal list should be invisible" + ); + let removeButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + updatePromise = promiseSiteDataManagerSitesUpdated(); + removeButton.doCommand(); + await dialogClosed; + await updatePromise; + await openSiteDataSettingsDialog(); + + dialog = content.gSubDialog._topDialog; + dialogFrame = dialog._frame; + frameDoc = dialogFrame.contentDocument; + assertSitesListed(frameDoc, testData.slice(1)); + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js new file mode 100644 index 0000000000..5ce9d7e1e1 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_siteData_multi_select.js @@ -0,0 +1,119 @@ +"use strict"; + +// Test selecting and removing partial sites +add_task(async function () { + await SiteDataTestUtils.clear(); + + let hosts = await addTestData([ + { + usage: 1024, + origin: "https://127.0.0.1", + persisted: false, + }, + { + usage: 1024 * 4, + origin: "http://cinema.bar.com", + persisted: true, + }, + { + usage: 1024 * 3, + origin: "http://email.bar.com", + persisted: false, + }, + { + usage: 1024 * 2, + origin: "https://s3-us-west-2.amazonaws.com", + persisted: true, + }, + { + usage: 1024 * 6, + origin: "https://account.xyz.com", + persisted: true, + }, + { + usage: 1024 * 5, + origin: "https://shopping.xyz.com", + persisted: false, + }, + { + usage: 1024 * 5, + origin: "https://example.com", + persisted: false, + }, + { + usage: 1024 * 5, + origin: "https://example.net", + persisted: false, + }, + ]); + + // Align the order of test hosts with the order of the site data table. + hosts.sort(); + + let updatePromise = promiseSiteDataManagerSitesUpdated(); + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + await updatePromise; + await openSiteDataSettingsDialog(); + + let doc = gBrowser.selectedBrowser.contentDocument; + + // Test the initial state + assertSitesListed(doc, hosts); + let win = gBrowser.selectedBrowser.contentWindow; + let frameDoc = win.gSubDialog._topDialog._frame.contentDocument; + let removeBtn = frameDoc.getElementById("removeSelected"); + is( + removeBtn.disabled, + true, + "Should start with disabled removeSelected button" + ); + + let hostCol = frameDoc.getElementById("hostCol"); + hostCol.click(); + + let removeDialogOpenPromise = BrowserTestUtils.promiseAlertDialogOpen( + "accept", + REMOVE_DIALOG_URL + ); + let settingsDialogClosePromise = promiseSettingsDialogClose(); + + // Select some sites to remove. + let sitesList = frameDoc.getElementById("sitesList"); + hosts.slice(0, 2).forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + sitesList.addItemToSelection(site); + }); + + is(removeBtn.disabled, false, "Should enable the removeSelected button"); + removeBtn.doCommand(); + is(sitesList.selectedIndex, 0, "Should select next item"); + assertSitesListed(doc, hosts.slice(2)); + + // Select some other sites to remove with Delete. + hosts.slice(2, 4).forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + sitesList.addItemToSelection(site); + }); + + is(removeBtn.disabled, false, "Should enable the removeSelected button"); + // Move the focus from the search box to the list + sitesList.focus(); + EventUtils.synthesizeKey("VK_DELETE"); + is(sitesList.selectedIndex, 0, "Should select next item"); + assertSitesListed(doc, hosts.slice(4)); + + updatePromise = promiseSiteDataManagerSitesUpdated(); + let saveBtn = frameDoc.querySelector("dialog").getButton("accept"); + saveBtn.doCommand(); + + await removeDialogOpenPromise; + await settingsDialogClosePromise; + + await updatePromise; + await openSiteDataSettingsDialog(); + + assertSitesListed(doc, hosts.slice(4)); + + await SiteDataTestUtils.clear(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/tests/siteData/head.js b/browser/components/preferences/tests/siteData/head.js new file mode 100644 index 0000000000..01f56d879d --- /dev/null +++ b/browser/components/preferences/tests/siteData/head.js @@ -0,0 +1,280 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_QUOTA_USAGE_HOST = "example.com"; +const TEST_QUOTA_USAGE_ORIGIN = "https://" + TEST_QUOTA_USAGE_HOST; +const TEST_QUOTA_USAGE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_QUOTA_USAGE_ORIGIN + ) + "/site_data_test.html"; +const TEST_OFFLINE_HOST = "example.org"; +const TEST_OFFLINE_ORIGIN = "https://" + TEST_OFFLINE_HOST; +const TEST_OFFLINE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_OFFLINE_ORIGIN + ) + "/offline/offline.html"; +const TEST_SERVICE_WORKER_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + TEST_OFFLINE_ORIGIN + ) + "/service_worker_test.html"; + +const REMOVE_DIALOG_URL = + "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml"; + +ChromeUtils.defineESModuleGetters(this, { + SiteDataTestUtils: "resource://testing-common/SiteDataTestUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +function promiseSiteDataManagerSitesUpdated() { + return TestUtils.topicObserved("sitedatamanager:sites-updated", () => true); +} + +function is_element_visible(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(!BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function is_element_hidden(aElement, aMsg) { + isnot(aElement, null, "Element should not be null, when checking visibility"); + ok(BrowserTestUtils.is_hidden(aElement), aMsg); +} + +function promiseLoadSubDialog(aURL) { + return new Promise((resolve, reject) => { + content.gSubDialog._dialogStack.addEventListener( + "dialogopen", + function dialogopen(aEvent) { + if ( + aEvent.detail.dialog._frame.contentWindow.location == "about:blank" + ) { + return; + } + content.gSubDialog._dialogStack.removeEventListener( + "dialogopen", + dialogopen + ); + + is( + aEvent.detail.dialog._frame.contentWindow.location.toString(), + aURL, + "Check the proper URL is loaded" + ); + + // Check visibility + is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible"); + + // Check that stylesheets were injected + let expectedStyleSheetURLs = + aEvent.detail.dialog._injectedStyleSheets.slice(0); + for (let styleSheet of aEvent.detail.dialog._frame.contentDocument + .styleSheets) { + let i = expectedStyleSheetURLs.indexOf(styleSheet.href); + if (i >= 0) { + info("found " + styleSheet.href); + expectedStyleSheetURLs.splice(i, 1); + } + } + is( + expectedStyleSheetURLs.length, + 0, + "All expectedStyleSheetURLs should have been found" + ); + + // Wait for the next event tick to make sure the remaining part of the + // testcase runs after the dialog gets ready for input. + executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow)); + } + ); + }); +} + +function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) { + return new Promise(resolve => { + let finalPrefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + openPreferences(aPane); + let newTabBrowser = gBrowser.selectedBrowser; + + newTabBrowser.addEventListener( + "Initialized", + function () { + newTabBrowser.contentWindow.addEventListener( + "load", + async function () { + let win = gBrowser.contentWindow; + let selectedPane = win.history.state; + await finalPrefPaneLoaded; + if (!aOptions || !aOptions.leaveOpen) { + gBrowser.removeCurrentTab(); + } + resolve({ selectedPane }); + }, + { once: true } + ); + }, + { capture: true, once: true } + ); + }); +} + +function openSiteDataSettingsDialog() { + let doc = gBrowser.selectedBrowser.contentDocument; + let settingsBtn = doc.getElementById("siteDataSettings"); + let dialogOverlay = content.gSubDialog._preloadDialog._overlay; + let dialogLoadPromise = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml" + ); + let dialogInitPromise = TestUtils.topicObserved( + "sitedata-settings-init", + () => true + ); + let fullyLoadPromise = Promise.all([ + dialogLoadPromise, + dialogInitPromise, + ]).then(() => { + is_element_visible(dialogOverlay, "The Settings dialog should be visible"); + }); + settingsBtn.doCommand(); + return fullyLoadPromise; +} + +function promiseSettingsDialogClose() { + return new Promise(resolve => { + let win = gBrowser.selectedBrowser.contentWindow; + let dialogOverlay = win.gSubDialog._topDialog._overlay; + let dialogWin = win.gSubDialog._topDialog._frame.contentWindow; + dialogWin.addEventListener( + "unload", + function unload() { + if ( + dialogWin.document.documentURI === + "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml" + ) { + is_element_hidden( + dialogOverlay, + "The Settings dialog should be hidden" + ); + resolve(); + } + }, + { once: true } + ); + }); +} + +function assertSitesListed(doc, hosts) { + let frameDoc = content.gSubDialog._topDialog._frame.contentDocument; + let removeAllBtn = frameDoc.getElementById("removeAll"); + let sitesList = frameDoc.getElementById("sitesList"); + let totalSitesNumber = sitesList.getElementsByTagName("richlistitem").length; + is(totalSitesNumber, hosts.length, "Should list the right sites number"); + hosts.forEach(host => { + let site = sitesList.querySelector(`richlistitem[host="${host}"]`); + ok(site, `Should list the site of ${host}`); + }); + is(removeAllBtn.disabled, false, "Should enable the removeAllBtn button"); +} + +// Counter used by addTestData to generate unique cookie names across function +// calls. +let cookieID = 0; + +async function addTestData(data) { + let hosts = new Set(); + + for (let site of data) { + is( + typeof site.origin, + "string", + "Passed an origin string into addTestData." + ); + if (site.persisted) { + await SiteDataTestUtils.persist(site.origin); + } + + if (site.usage) { + await SiteDataTestUtils.addToIndexedDB(site.origin, site.usage); + } + + for (let i = 0; i < (site.cookies || 0); i++) { + SiteDataTestUtils.addToCookies({ + origin: site.origin, + name: `cookie${cookieID++}`, + }); + } + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + site.origin + ); + + hosts.add(principal.baseDomain || principal.host); + } + + return Array.from(hosts); +} + +function promiseCookiesCleared() { + return TestUtils.topicObserved("cookie-changed", (subj, data) => { + return data === "cleared"; + }); +} + +async function loadServiceWorkerTestPage(url) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await TestUtils.waitForCondition(() => { + return SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => + content.document.body.getAttribute( + "data-test-service-worker-registered" + ) === "true" + ); + }, `Fail to load service worker test ${url}`); + BrowserTestUtils.removeTab(tab); +} + +function promiseServiceWorkersCleared() { + return TestUtils.waitForCondition(() => { + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + if (!serviceWorkers.length) { + ok(true, "Cleared all service workers"); + return true; + } + return false; + }, "Should clear all service workers"); +} + +function promiseServiceWorkerRegisteredFor(url) { + return TestUtils.waitForCondition(() => { + try { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(url); + let sw = serviceWorkerManager.getRegistrationByPrincipal( + principal, + principal.spec + ); + if (sw) { + ok(true, `Found the service worker registered for ${url}`); + return true; + } + } catch (e) {} + return false; + }, `Should register service worker for ${url}`); +} diff --git a/browser/components/preferences/tests/siteData/offline/manifest.appcache b/browser/components/preferences/tests/siteData/offline/manifest.appcache new file mode 100644 index 0000000000..a9287c64e6 --- /dev/null +++ b/browser/components/preferences/tests/siteData/offline/manifest.appcache @@ -0,0 +1,3 @@ +CACHE MANIFEST +# V1 +offline.html diff --git a/browser/components/preferences/tests/siteData/offline/offline.html b/browser/components/preferences/tests/siteData/offline/offline.html new file mode 100644 index 0000000000..f76b8a2bce --- /dev/null +++ b/browser/components/preferences/tests/siteData/offline/offline.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html manifest="manifest.appcache">> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="public" /> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> + + </head> + + <body> + <h1>Set up offline appcache Test</h1> + </body> +</html> diff --git a/browser/components/preferences/tests/siteData/service_worker_test.html b/browser/components/preferences/tests/siteData/service_worker_test.html new file mode 100644 index 0000000000..56f5173481 --- /dev/null +++ b/browser/components/preferences/tests/siteData/service_worker_test.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="public" /> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> + + <title>Service Worker Test</title> + + </head> + + <body> + <h1>Service Worker Test</h1> + <script type="text/javascript"> + navigator.serviceWorker.register("service_worker_test.js") + .then(regis => document.body.setAttribute("data-test-service-worker-registered", "true")); + </script> + </body> +</html> diff --git a/browser/components/preferences/tests/siteData/service_worker_test.js b/browser/components/preferences/tests/siteData/service_worker_test.js new file mode 100644 index 0000000000..2aba167d18 --- /dev/null +++ b/browser/components/preferences/tests/siteData/service_worker_test.js @@ -0,0 +1 @@ +// empty worker, always succeed! diff --git a/browser/components/preferences/tests/siteData/site_data_test.html b/browser/components/preferences/tests/siteData/site_data_test.html new file mode 100644 index 0000000000..758106b0a5 --- /dev/null +++ b/browser/components/preferences/tests/siteData/site_data_test.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="Cache-Control" content="public" /> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1"> + + <title>Site Data Test</title> + + </head> + + <body> + <h1>Site Data Test</h1> + <script type="text/javascript"> + let request = indexedDB.open("TestDatabase", 1); + request.onupgradeneeded = function(e) { + let db = e.target.result; + db.createObjectStore("TestStore", { keyPath: "id" }); + }; + request.onsuccess = function(e) { + let db = e.target.result; + let tx = db.transaction("TestStore", "readwrite"); + let store = tx.objectStore("TestStore"); + tx.oncomplete = () => document.dispatchEvent(new CustomEvent("test-indexedDB-done", {bubbles: true, cancelable: false})); + store.put({ id: "test_id", description: "Site Data Test"}); + }; + </script> + </body> +</html> diff --git a/browser/components/preferences/tests/subdialog.xhtml b/browser/components/preferences/tests/subdialog.xhtml new file mode 100644 index 0000000000..54fa88c25d --- /dev/null +++ b/browser/components/preferences/tests/subdialog.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> + +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Sample sub-dialog" style="width: 32em; height: 5em;" + onload="document.getElementById('textbox').focus();"> +<dialog id="subDialog"> + <script> + document.addEventListener("dialogaccept", acceptSubdialog); + function acceptSubdialog() { + window.arguments[0].acceptCount++; + } + </script> + + <description id="desc">A sample sub-dialog for testing</description> + + <html:input id="textbox" value="Default text" /> + + <separator class="thin"/> + + <button oncommand="window.close();" label="Close" /> + +</dialog> +</window> diff --git a/browser/components/preferences/tests/subdialog2.xhtml b/browser/components/preferences/tests/subdialog2.xhtml new file mode 100644 index 0000000000..9ae04d5675 --- /dev/null +++ b/browser/components/preferences/tests/subdialog2.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> + +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Sample sub-dialog #2" style="width: 32em; height: 5em;" + onload="document.getElementById('textbox').focus();"> +<dialog id="subDialog"> + <script> + document.addEventListener("dialogaccept", acceptSubdialog); + function acceptSubdialog() { + window.arguments[0].acceptCount++; + } + </script> + + <description id="desc">A sample sub-dialog for testing</description> + + <html:input id="textbox" value="Default text" /> + + <separator class="thin"/> + + <button oncommand="window.close();" label="Close" /> + +</dialog> +</window> |