diff --git a/browser/base/content/aboutDialog.xhtml b/browser/base/content/aboutDialog.xhtml index 1e5c0f1c59175e..277ac7ba8cd3c6 100644 --- a/browser/base/content/aboutDialog.xhtml +++ b/browser/base/content/aboutDialog.xhtml @@ -33,6 +33,8 @@ rel="stylesheet" href="chrome://branding/content/aboutDialog.css" /> + + diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc index 3a58888b28b477..4135b2021e17a7 100644 --- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -290,6 +290,10 @@ hidden="true" /> + + diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index 08aa7ba046672f..37a3293a248751 100644 --- a/browser/base/content/browser.xhtml +++ b/browser/base/content/browser.xhtml @@ -32,6 +32,7 @@ scrolling="false" persist="screenX screenY width height sizemode" data-l10n-sync="true"> + firefoxviewhidden="true"> - - - - - - - - - - - - - - - - - -
-
-

-
- -
-

-
- -
- - -
- -
- -
-
- - - - diff --git a/toolkit/components/aboutconfig/content/aboutconfig.js b/toolkit/components/aboutconfig/content/aboutconfig.js deleted file mode 100644 index ef4238975be62c..00000000000000 --- a/toolkit/components/aboutconfig/content/aboutconfig.js +++ /dev/null @@ -1,697 +0,0 @@ -/* 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 { DeferredTask } = ChromeUtils.importESModule( - "resource://gre/modules/DeferredTask.sys.mjs" -); -const { Preferences } = ChromeUtils.importESModule( - "resource://gre/modules/Preferences.sys.mjs" -); - -const SEARCH_TIMEOUT_MS = 100; -const SEARCH_AUTO_MIN_CRARACTERS = 3; - -const GETTERS_BY_PREF_TYPE = { - [Ci.nsIPrefBranch.PREF_BOOL]: "getBoolPref", - [Ci.nsIPrefBranch.PREF_INT]: "getIntPref", - [Ci.nsIPrefBranch.PREF_STRING]: "getStringPref", -}; - -const STRINGS_ADD_BY_TYPE = { - Boolean: "about-config-pref-add-type-boolean", - Number: "about-config-pref-add-type-number", - String: "about-config-pref-add-type-string", -}; - -// Fluent limits the maximum length of placeables. -const MAX_PLACEABLE_LENGTH = 2500; - -let gDefaultBranch = Services.prefs.getDefaultBranch(""); -let gFilterPrefsTask = new DeferredTask( - () => filterPrefs(), - SEARCH_TIMEOUT_MS, - 0 -); - -/** - * Maps the name of each preference in the back-end to its PrefRow object, - * separating the preferences that actually exist. This is as an optimization to - * avoid querying the preferences service each time the list is filtered. - */ -let gExistingPrefs = new Map(); -let gDeletedPrefs = new Map(); - -/** - * Also cache several values to improve the performance of common use cases. - */ -let gSortedExistingPrefs = null; -let gSearchInput = null; -let gShowOnlyModifiedCheckbox = null; -let gPrefsTable = null; - -/** - * Reference to the PrefRow currently being edited, if any. - */ -let gPrefInEdit = null; - -/** - * Lowercase substring that should be contained in the preference name. - */ -let gFilterString = null; - -/** - * RegExp that should be matched to the preference name. - */ -let gFilterPattern = null; - -/** - * True if we were requested to show all preferences. - */ -let gFilterShowAll = false; - -class PrefRow { - constructor(name, opts) { - this.name = name; - this.value = true; - this.hidden = false; - this.odd = false; - this.editing = false; - this.isAddRow = opts && opts.isAddRow; - this.refreshValue(); - } - - refreshValue() { - let prefType = Services.prefs.getPrefType(this.name); - - // If this preference has been deleted, we keep its last known value. - if (prefType == Ci.nsIPrefBranch.PREF_INVALID) { - this.hasDefaultValue = false; - this.hasUserValue = false; - this.isLocked = false; - if (gExistingPrefs.has(this.name)) { - gExistingPrefs.delete(this.name); - gSortedExistingPrefs = null; - } - gDeletedPrefs.set(this.name, this); - return; - } - - if (!gExistingPrefs.has(this.name)) { - gExistingPrefs.set(this.name, this); - gSortedExistingPrefs = null; - } - gDeletedPrefs.delete(this.name); - - try { - this.value = gDefaultBranch[GETTERS_BY_PREF_TYPE[prefType]](this.name); - this.hasDefaultValue = true; - } catch (ex) { - this.hasDefaultValue = false; - } - this.hasUserValue = Services.prefs.prefHasUserValue(this.name); - this.isLocked = Services.prefs.prefIsLocked(this.name); - - try { - if (this.hasUserValue) { - // This can throw for locked preferences without a default value. - this.value = Services.prefs[GETTERS_BY_PREF_TYPE[prefType]](this.name); - } else if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(this.value)) { - // We don't know which preferences should be read using getComplexValue, - // so we use a heuristic to determine if this is a localized preference. - // This can throw if there is no value in the localized files. - this.value = Services.prefs.getComplexValue( - this.name, - Ci.nsIPrefLocalizedString - ).data; - } - } catch (ex) { - this.value = ""; - } - } - - get type() { - return this.value.constructor.name; - } - - get exists() { - return this.hasDefaultValue || this.hasUserValue; - } - - get matchesFilter() { - if (!this.matchesModifiedFilter) { - return false; - } - - return ( - gFilterShowAll || - (gFilterPattern && gFilterPattern.test(this.name)) || - (gFilterString && this.name.toLowerCase().includes(gFilterString)) - ); - } - - get matchesModifiedFilter() { - const onlyShowModified = gShowOnlyModifiedCheckbox.checked; - return !onlyShowModified || this.hasUserValue; - } - - /** - * Returns a reference to the table row element to be added to the document, - * constructing and initializing it the first time this method is called. - */ - getElement() { - if (this._element) { - return this._element; - } - - this._element = document.createElement("tr"); - this._element._pref = this; - - let nameCell = document.createElement("th"); - let nameCellSpan = document.createElement("span"); - nameCell.appendChild(nameCellSpan); - this._element.append( - nameCell, - (this.valueCell = document.createElement("td")), - (this.editCell = document.createElement("td")), - (this.resetCell = document.createElement("td")) - ); - this.editCell.appendChild( - (this.editButton = document.createElement("button")) - ); - delete this.resetButton; - - nameCell.setAttribute("scope", "row"); - this.valueCell.className = "cell-value"; - this.editCell.className = "cell-edit"; - this.resetCell.className = "cell-reset"; - - // Add behind dots to prevent line breaking in random mid-word places. - let parts = this.name.split("."); - for (let i = 0; i < parts.length - 1; i++) { - nameCellSpan.append(parts[i] + ".", document.createElement("wbr")); - } - nameCellSpan.append(parts[parts.length - 1]); - - this.refreshElement(); - - return this._element; - } - - refreshElement() { - if (!this._element) { - // No need to update if this preference was never added to the table. - return; - } - - if (this.exists && !this.editing) { - // We need to place the text inside a "span" element to ensure that the - // text copied to the clipboard includes all whitespace. - let span = document.createElement("span"); - span.textContent = this.value; - // We additionally need to wrap this with another "span" element to convey - // the state to screen readers without affecting the visual presentation. - span.setAttribute("aria-hidden", "true"); - let outerSpan = document.createElement("span"); - if (this.type == "String" && this.value.length > MAX_PLACEABLE_LENGTH) { - // If the value is too long for localization, don't include the state. - // Since the preferences system is designed to store short values, this - // case happens very rarely, thus we keep the same DOM structure for - // consistency even though we could avoid the extra "span" element. - outerSpan.setAttribute("aria-label", this.value); - } else { - let spanL10nId = this.hasUserValue - ? "about-config-pref-accessible-value-custom" - : "about-config-pref-accessible-value-default"; - document.l10n.setAttributes(outerSpan, spanL10nId, { - value: "" + this.value, - }); - } - outerSpan.appendChild(span); - this.valueCell.textContent = ""; - this.valueCell.append(outerSpan); - if (this.type == "Boolean") { - document.l10n.setAttributes( - this.editButton, - "about-config-pref-toggle-button" - ); - this.editButton.className = "button-toggle semi-transparent"; - } else { - document.l10n.setAttributes( - this.editButton, - "about-config-pref-edit-button" - ); - this.editButton.className = "button-edit semi-transparent"; - } - this.editButton.removeAttribute("form"); - delete this.inputField; - } else { - this.valueCell.textContent = ""; - // The form is needed for the validation report to appear, but we need to - // prevent the associated button from reloading the page. - let form = document.createElement("form"); - form.addEventListener("submit", event => event.preventDefault()); - form.id = "form-edit"; - if (this.editing) { - this.inputField = document.createElement("input"); - this.inputField.value = this.value; - this.inputField.ariaLabel = this.name; - if (this.type == "Number") { - this.inputField.type = "number"; - this.inputField.required = true; - this.inputField.min = -2147483648; - this.inputField.max = 2147483647; - } else { - this.inputField.type = "text"; - } - form.appendChild(this.inputField); - document.l10n.setAttributes( - this.editButton, - "about-config-pref-save-button" - ); - this.editButton.className = "primary button-save semi-transparent"; - } else { - delete this.inputField; - for (let type of ["Boolean", "Number", "String"]) { - let radio = document.createElement("input"); - radio.type = "radio"; - radio.name = "type"; - radio.value = type; - radio.checked = this.type == type; - let radioSpan = document.createElement("span"); - document.l10n.setAttributes(radioSpan, STRINGS_ADD_BY_TYPE[type]); - let radioLabel = document.createElement("label"); - radioLabel.append(radio, radioSpan); - form.appendChild(radioLabel); - } - form.addEventListener("click", event => { - if (event.target.name != "type") { - return; - } - let type = event.target.value; - if (this.type != type) { - if (type == "Boolean") { - this.value = true; - } else if (type == "Number") { - this.value = 0; - } else { - this.value = ""; - } - } - }); - document.l10n.setAttributes( - this.editButton, - "about-config-pref-add-button" - ); - this.editButton.className = "button-add semi-transparent"; - } - this.valueCell.appendChild(form); - this.editButton.setAttribute("form", "form-edit"); - } - this.editButton.disabled = this.isLocked; - if (!this.isLocked && this.hasUserValue) { - if (!this.resetButton) { - this.resetButton = document.createElement("button"); - this.resetCell.appendChild(this.resetButton); - } - if (!this.hasDefaultValue) { - document.l10n.setAttributes( - this.resetButton, - "about-config-pref-delete-button" - ); - this.resetButton.className = - "button-delete ghost-button semi-transparent"; - } else { - document.l10n.setAttributes( - this.resetButton, - "about-config-pref-reset-button" - ); - this.resetButton.className = - "button-reset ghost-button semi-transparent"; - } - } else if (this.resetButton) { - this.resetButton.remove(); - delete this.resetButton; - } - - this.refreshClass(); - } - - refreshClass() { - if (!this._element) { - // No need to update if this preference was never added to the table. - return; - } - - let className; - if (this.hidden) { - className = "hidden"; - } else { - className = - (this.hasUserValue ? "has-user-value " : "") + - (this.isLocked ? "locked " : "") + - (this.exists ? "" : "deleted ") + - (this.isAddRow ? "add " : "") + - (this.odd ? "odd " : ""); - } - - if (this._lastClassName !== className) { - this._element.className = this._lastClassName = className; - } - } - - edit() { - if (gPrefInEdit) { - gPrefInEdit.endEdit(); - } - gPrefInEdit = this; - this.editing = true; - this.refreshElement(); - // The type=number input isn't selected unless it's focused first. - this.inputField.focus(); - this.inputField.select(); - } - - toggle() { - Services.prefs.setBoolPref(this.name, !this.value); - } - - editOrToggle() { - if (this.type == "Boolean") { - this.toggle(); - } else { - this.edit(); - } - } - - save() { - if (this.type == "Number") { - if (!this.inputField.reportValidity()) { - return; - } - Services.prefs.setIntPref(this.name, parseInt(this.inputField.value)); - } else { - Services.prefs.setStringPref(this.name, this.inputField.value); - } - this.refreshValue(); - this.endEdit(); - this.editButton.focus(); - } - - endEdit() { - this.editing = false; - this.refreshElement(); - gPrefInEdit = null; - } -} - -let gPrefObserverRegistered = false; -let gPrefObserver = { - observe(subject, topic, data) { - let pref = gExistingPrefs.get(data) || gDeletedPrefs.get(data); - if (pref) { - pref.refreshValue(); - if (!pref.editing) { - pref.refreshElement(); - } - return; - } - - let newPref = new PrefRow(data); - if (newPref.matchesFilter) { - document.getElementById("prefs").appendChild(newPref.getElement()); - } - }, -}; - -if (!Preferences.get("browser.aboutConfig.showWarning")) { - // When showing the filtered preferences directly, remove the warning elements - // immediately to prevent flickering, but wait to filter the preferences until - // the value of the textbox has been restored from previous sessions. - document.addEventListener("DOMContentLoaded", loadPrefs, { once: true }); - window.addEventListener( - "load", - () => { - if (document.getElementById("about-config-search").value) { - filterPrefs(); - } - }, - { once: true } - ); -} else { - document.addEventListener("DOMContentLoaded", function () { - let warningButton = document.getElementById("warningButton"); - warningButton.addEventListener("click", onWarningButtonClick); - warningButton.focus({ focusVisible: false }); - }); -} - -function onWarningButtonClick() { - Services.prefs.setBoolPref( - "browser.aboutConfig.showWarning", - document.getElementById("showWarningNextTime").checked - ); - loadPrefs(); -} - -function loadPrefs() { - [...document.styleSheets].find(s => s.title == "infop").disabled = true; - - let { content } = document.getElementById("main"); - document.body.textContent = ""; - document.body.appendChild(content); - - let search = (gSearchInput = document.getElementById("about-config-search")); - let prefs = (gPrefsTable = document.getElementById("prefs")); - let showAll = document.getElementById("show-all"); - gShowOnlyModifiedCheckbox = document.getElementById( - "about-config-show-only-modified" - ); - search.focus(); - gShowOnlyModifiedCheckbox.checked = false; - - for (let name of Services.prefs.getChildList("")) { - new PrefRow(name); - } - - search.addEventListener("keypress", event => { - if (event.key == "Escape") { - // The ESC key returns immediately to the initial empty page. - search.value = ""; - gFilterPrefsTask.disarm(); - filterPrefs(); - } else if (event.key == "Enter") { - // The Enter key filters immediately even if the search string is short. - gFilterPrefsTask.disarm(); - filterPrefs({ shortString: true }); - } - }); - - search.addEventListener("input", () => { - // We call "disarm" to restart the timer at every input. - gFilterPrefsTask.disarm(); - if (search.value.trim().length < SEARCH_AUTO_MIN_CRARACTERS) { - // Return immediately to the empty page if the search string is short. - filterPrefs(); - } else { - gFilterPrefsTask.arm(); - } - }); - - gShowOnlyModifiedCheckbox.addEventListener("change", () => { - // This checkbox: - // - Filters results to only modified prefs when search query is entered - // - Shows all modified prefs, in show all mode, and after initial checkbox click - let tableHidden = !document.body.classList.contains("table-shown"); - filterPrefs({ - showAll: - gFilterShowAll || (gShowOnlyModifiedCheckbox.checked && tableHidden), - }); - }); - - showAll.addEventListener("click", () => { - search.focus(); - search.value = ""; - gFilterPrefsTask.disarm(); - filterPrefs({ showAll: true }); - }); - - function shouldBeginEdit(event) { - if ( - event.target.localName != "button" && - event.target.localName != "input" - ) { - let row = event.target.closest("tr"); - return row && row._pref.exists; - } - return false; - } - - // Disable double/triple-click text selection since that triggers edit/toggle. - prefs.addEventListener("mousedown", event => { - if (event.detail > 1 && shouldBeginEdit(event)) { - event.preventDefault(); - } - }); - - prefs.addEventListener("click", event => { - if (event.detail == 2 && shouldBeginEdit(event)) { - event.target.closest("tr")._pref.editOrToggle(); - return; - } - - if (event.target.localName != "button") { - return; - } - - let pref = event.target.closest("tr")._pref; - let button = event.target.closest("button"); - - if (button.classList.contains("button-add")) { - pref.isAddRow = false; - Preferences.set(pref.name, pref.value); - if (pref.type == "Boolean") { - pref.refreshClass(); - } else { - pref.edit(); - } - } else if ( - button.classList.contains("button-toggle") || - button.classList.contains("button-edit") - ) { - pref.editOrToggle(); - } else if (button.classList.contains("button-save")) { - pref.save(); - } else { - // This is "button-reset" or "button-delete". - pref.editing = false; - Services.prefs.clearUserPref(pref.name); - pref.editButton.focus(); - } - }); - - window.addEventListener("keypress", event => { - if (event.target != search && event.key == "Escape" && gPrefInEdit) { - gPrefInEdit.endEdit(); - } - }); -} - -function filterPrefs(options = {}) { - if (gPrefInEdit) { - gPrefInEdit.endEdit(); - } - gDeletedPrefs.clear(); - - let searchName = gSearchInput.value.trim(); - if (searchName.length < SEARCH_AUTO_MIN_CRARACTERS && !options.shortString) { - searchName = ""; - } - - gFilterString = searchName.toLowerCase(); - gFilterShowAll = !!options.showAll; - - gFilterPattern = null; - if (gFilterString.includes("*")) { - gFilterPattern = new RegExp(gFilterString.replace(/\*+/g, ".*"), "i"); - gFilterString = ""; - } - - let showResults = gFilterString || gFilterPattern || gFilterShowAll; - document.body.classList.toggle("table-shown", showResults); - - let prefArray = []; - if (showResults) { - if (!gSortedExistingPrefs) { - gSortedExistingPrefs = [...gExistingPrefs.values()]; - gSortedExistingPrefs.sort((a, b) => a.name > b.name); - } - prefArray = gSortedExistingPrefs; - } - - // The slowest operations tend to be the addition and removal of DOM nodes, so - // this algorithm tries to reduce removals by hiding nodes instead. This - // happens frequently when the set narrows while typing preference names. We - // iterate the nodes already in the table in parallel to those we want to - // show, because the two lists are sorted and they will often match already. - let fragment = null; - let indexInArray = 0; - let elementInTable = gPrefsTable.firstElementChild; - let odd = false; - let hasVisiblePrefs = false; - while (indexInArray < prefArray.length || elementInTable) { - // For efficiency, filter the array while we are iterating. - let prefInArray = prefArray[indexInArray]; - if (prefInArray) { - if (!prefInArray.matchesFilter) { - indexInArray++; - continue; - } - prefInArray.hidden = false; - prefInArray.odd = odd; - } - - let prefInTable = elementInTable && elementInTable._pref; - if (!prefInTable) { - // We're at the end of the table, we just have to insert all the matching - // elements that remain in the array. We can use a fragment to make the - // insertions faster, which is useful during the initial filtering. - if (!fragment) { - fragment = document.createDocumentFragment(); - } - fragment.appendChild(prefInArray.getElement()); - } else if (prefInTable == prefInArray) { - // We got two matching elements, we just need to update the visibility. - elementInTable = elementInTable.nextElementSibling; - } else if (prefInArray && prefInArray.name < prefInTable.name) { - // The iteration in the table is ahead of the iteration in the array. - // Insert or move the array element, and advance the array index. - gPrefsTable.insertBefore(prefInArray.getElement(), elementInTable); - } else { - // The iteration in the array is ahead of the iteration in the table. - // Hide the element in the table, and advance to the next element. - let nextElementInTable = elementInTable.nextElementSibling; - if (!prefInTable.exists) { - // Remove rows for deleted preferences, or temporary addition rows. - elementInTable.remove(); - } else { - // Keep the element for the next filtering if the preference exists. - prefInTable.hidden = true; - prefInTable.refreshClass(); - } - elementInTable = nextElementInTable; - continue; - } - - prefInArray.refreshClass(); - odd = !odd; - indexInArray++; - hasVisiblePrefs = true; - } - - if (fragment) { - gPrefsTable.appendChild(fragment); - } - - gPrefsTable.toggleAttribute("has-visible-prefs", hasVisiblePrefs); - - if (searchName && !gExistingPrefs.has(searchName)) { - let addPrefRow = new PrefRow(searchName, { isAddRow: true }); - addPrefRow.odd = odd; - gPrefsTable.appendChild(addPrefRow.getElement()); - } - - // We only start observing preference changes after the first search is done, - // so that newly added preferences won't appear while the page is still empty. - if (!gPrefObserverRegistered) { - gPrefObserverRegistered = true; - Services.prefs.addObserver("", gPrefObserver); - window.addEventListener( - "unload", - () => { - Services.prefs.removeObserver("", gPrefObserver); - }, - { once: true } - ); - } -} diff --git a/toolkit/components/aboutconfig/content/background.svg b/toolkit/components/aboutconfig/content/background.svg deleted file mode 100644 index d03d081ee50388..00000000000000 --- a/toolkit/components/aboutconfig/content/background.svg +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/toolkit/components/aboutconfig/content/toggle.svg b/toolkit/components/aboutconfig/content/toggle.svg deleted file mode 100644 index 73f45b079ef758..00000000000000 --- a/toolkit/components/aboutconfig/content/toggle.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/toolkit/components/aboutconfig/jar.mn b/toolkit/components/aboutconfig/jar.mn deleted file mode 100644 index 400db0e1dc4997..00000000000000 --- a/toolkit/components/aboutconfig/jar.mn +++ /dev/null @@ -1,11 +0,0 @@ -#filter substitution -# 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/. - -toolkit.jar: - content/global/aboutconfig/aboutconfig.css (content/aboutconfig.css) - content/global/aboutconfig/aboutconfig.html (content/aboutconfig.html) - content/global/aboutconfig/aboutconfig.js (content/aboutconfig.js) - content/global/aboutconfig/background.svg (content/background.svg) - content/global/aboutconfig/toggle.svg (content/toggle.svg) diff --git a/toolkit/components/aboutconfig/test/browser/browser.toml b/toolkit/components/aboutconfig/test/browser/browser.toml deleted file mode 100644 index 373d51cf064ed7..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser.toml +++ /dev/null @@ -1,25 +0,0 @@ -[DEFAULT] -skip-if = [ - "debug", # Bug 1507747 - "asan", # Bug 1520398 - "tsan" -] -support-files = ["head.js"] - -["browser_accessibility.js"] - -["browser_basic.js"] - -["browser_clipboard.js"] - -["browser_edit.js"] -skip-if = ["os == 'linux' && ccov"] # Bug 1613515, the test consistently times out on Linux coverage builds. - -["browser_locked.js"] - -["browser_observe.js"] -skip-if = ["os == 'linux' && ccov"] # Bug 1614978, the test consistently times out on Linux coverage builds. - -["browser_search.js"] - -["browser_warning.js"] diff --git a/toolkit/components/aboutconfig/test/browser/browser_accessibility.js b/toolkit/components/aboutconfig/test/browser/browser_accessibility.js deleted file mode 100644 index 9310e401861fc6..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_accessibility.js +++ /dev/null @@ -1,39 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -const MAX_PLACEABLE_LENGTH = 2500; - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.aboutconfig.added", "=".repeat(MAX_PLACEABLE_LENGTH)], - ["test.aboutconfig.long", "=".repeat(MAX_PLACEABLE_LENGTH + 1)], - ], - }); -}); - -add_task(async function test_accessible_value() { - await AboutConfigTest.withNewTab(async function () { - for (let [name, expectHasUserValue] of [ - [PREF_BOOLEAN_DEFAULT_TRUE, false], - [PREF_BOOLEAN_USERVALUE_TRUE, true], - ["test.aboutconfig.added", true], - ]) { - let span = this.getRow(name).valueCell.querySelector("span"); - let expectedL10nId = expectHasUserValue - ? "about-config-pref-accessible-value-custom" - : "about-config-pref-accessible-value-default"; - Assert.equal(span.getAttribute("data-l10n-id"), expectedL10nId); - } - - // If the value is too long for localization, the state is not included. - let span = this.getRow("test.aboutconfig.long").valueCell.querySelector( - "span" - ); - Assert.ok(!span.hasAttribute("data-l10n-id")); - Assert.equal( - span.getAttribute("aria-label"), - Preferences.get("test.aboutconfig.long") - ); - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_basic.js b/toolkit/components/aboutconfig/test/browser/browser_basic.js deleted file mode 100644 index 014a98df9739d8..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_basic.js +++ /dev/null @@ -1,52 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - [ - "test.aboutconfig.userValueLikeLocalized", - "chrome://test/locale/testing.properties", - ], - ], - }); -}); - -add_task(async function test_load_title() { - await AboutConfigTest.withNewTab(async function () { - Assert.equal(this.document.title, "Advanced Preferences"); - }); -}); - -add_task(async function test_load_settings() { - await AboutConfigTest.withNewTab(async function () { - // Test if page contains elements. - Assert.equal(this.getRow(PREF_NUMBER_DEFAULT_ZERO).value, 0); - Assert.equal(this.getRow(PREF_STRING_DEFAULT_EMPTY).value, ""); - - // Test if the modified state is displayed for the right prefs. - Assert.ok( - !this.getRow(PREF_BOOLEAN_DEFAULT_TRUE).hasClass("has-user-value") - ); - Assert.ok( - this.getRow(PREF_BOOLEAN_USERVALUE_TRUE).hasClass("has-user-value") - ); - - // Test to see if values are localized, sampling from different files. If - // any of these are removed or their value changes, just update the value - // here or point to a different preference in the same file. - Assert.equal(this.getRow("font.language.group").value, "x-western"); - Assert.equal(this.getRow("intl.ellipsis").value, "\u2026"); - - // Test to see if user created value is not empty string when it matches - // /^chrome:\/\/.+\/locale\/.+\.properties/. - Assert.equal( - this.getRow("test.aboutconfig.userValueLikeLocalized").value, - "chrome://test/locale/testing.properties" - ); - - // Test to see if empty string when value matches - // /^chrome:\/\/.+\/locale\/.+\.properties/ and an exception is thrown. - Assert.equal(this.getRow(PREF_STRING_LOCALIZED_MISSING).value, ""); - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_clipboard.js b/toolkit/components/aboutconfig/test/browser/browser_clipboard.js deleted file mode 100644 index a8fca568c7e9ca..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_clipboard.js +++ /dev/null @@ -1,158 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.aboutconfig.copy.false", false], - ["test.aboutconfig.copy.number", 10], - ["test.aboutconfig.copy.spaces.1", " "], - ["test.aboutconfig.copy.spaces.2", " "], - ["test.aboutconfig.copy.spaces.3", " "], - ["test.aboutconfig.copy.string", "010.5"], - ], - }); -}); - -add_task(async function test_copy() { - await AboutConfigTest.withNewTab(async function () { - for (let [name, expectedString] of [ - [PREF_BOOLEAN_DEFAULT_TRUE, "true"], - [PREF_BOOLEAN_USERVALUE_TRUE, "true"], - [PREF_STRING_DEFAULT_EMPTY, ""], - ["test.aboutconfig.copy.false", "false"], - ["test.aboutconfig.copy.number", "10"], - ["test.aboutconfig.copy.spaces.1", " "], - ["test.aboutconfig.copy.spaces.2", " "], - ["test.aboutconfig.copy.spaces.3", " "], - ["test.aboutconfig.copy.string", "010.5"], - ]) { - // Limit the number of preferences shown so all the rows are visible. - this.search(name); - let row = this.getRow(name); - - let selectText = async target => { - let { width, height } = target.getBoundingClientRect(); - - // We intentionally turn off this a11y check, because the following - // series of mouse events is purposefully targeting a non-interactive - // text content. This action does not require the element to have an - // interactive accessible to be done by assistive technology with caret - // browsing (when/if supported), this rule check shall be ignored by - // a11y_checks suite. - AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); - EventUtils.synthesizeMouse( - target, - 1, - 1, - { type: "mousedown" }, - this.browser.contentWindow - ); - EventUtils.synthesizeMouse( - target, - width - 1, - height - 1, - { type: "mousemove" }, - this.browser.contentWindow - ); - EventUtils.synthesizeMouse( - target, - width - 1, - height - 1, - { type: "mouseup" }, - this.browser.contentWindow - ); - AccessibilityUtils.resetEnv(); - }; - - // Drag across the name cell. - await selectText(row.nameCell); - Assert.ok(row.nameCell.contains(this.window.getSelection().anchorNode)); - await SimpleTest.promiseClipboardChange(name, async () => { - await BrowserTestUtils.synthesizeKey( - "c", - { accelKey: true }, - this.browser - ); - }); - - // Drag across the value cell. - await selectText(row.valueCell); - let selection = this.window.getSelection(); - Assert.ok(row.valueCell.contains(selection.anchorNode)); - - if (expectedString !== "") { - // Non-empty values should have a selection. - Assert.ok(!selection.isCollapsed); - await SimpleTest.promiseClipboardChange(expectedString, async () => { - await BrowserTestUtils.synthesizeKey( - "c", - { accelKey: true }, - this.browser - ); - }); - } else { - // Nothing is selected for an empty value. - Assert.equal(selection.toString(), ""); - } - } - }); -}); - -add_task(async function test_copy_multiple() { - await AboutConfigTest.withNewTab(async function () { - // Lines are separated by a single LF character on all platforms. - let expectedString = - "test.aboutconfig.copy.false\tfalse\t\n" + - "test.aboutconfig.copy.number\t10\t\n" + - "test.aboutconfig.copy.spaces.1\t \t\n" + - "test.aboutconfig.copy.spaces.2\t \t\n" + - "test.aboutconfig.copy.spaces.3\t \t\n" + - "test.aboutconfig.copy.string\t010.5"; - - this.search("test.aboutconfig.copy."); - let startRow = this.getRow("test.aboutconfig.copy.false"); - let endRow = this.getRow("test.aboutconfig.copy.string"); - let { width, height } = endRow.valueCell.getBoundingClientRect(); - - // Drag from the top left of the first row to the bottom right of the last. - // We intentionally turn off this a11y check, because the following - // series of mouse events is purposefully targeting a non-interactive - // text content. This action does not require the element to have an - // interactive accessible to be done by assistive technology with caret - // browsing (when/if supported), this rule check shall be ignored by - // a11y_checks suite. - AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); - EventUtils.synthesizeMouse( - startRow.nameCell, - 1, - 1, - { type: "mousedown" }, - this.browser.contentWindow - ); - - EventUtils.synthesizeMouse( - endRow.valueCell, - width - 1, - height - 1, - { type: "mousemove" }, - this.browser.contentWindow - ); - EventUtils.synthesizeMouse( - endRow.valueCell, - width - 1, - height - 1, - { type: "mouseup" }, - this.browser.contentWindow - ); - AccessibilityUtils.resetEnv(); - - await SimpleTest.promiseClipboardChange(expectedString, async () => { - await BrowserTestUtils.synthesizeKey( - "c", - { accelKey: true }, - this.browser - ); - }); - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_edit.js b/toolkit/components/aboutconfig/test/browser/browser_edit.js deleted file mode 100644 index 24fb168e7642a5..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_edit.js +++ /dev/null @@ -1,451 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -const PREF_MODIFY_PREFIX = "test.aboutconfig.modify"; -const PREF_MODIFY_BOOLEAN = "test.aboutconfig.modify.boolean"; -const PREF_MODIFY_NUMBER = "test.aboutconfig.modify.number"; -const PREF_MODIFY_STRING = "test.aboutconfig.modify.string"; - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - [PREF_MODIFY_BOOLEAN, true], - [PREF_MODIFY_NUMBER, 1337], - [ - PREF_MODIFY_STRING, - "the answer to the life the universe and everything", - ], - ], - }); - - registerCleanupFunction(() => { - Services.prefs.clearUserPref(PREF_BOOLEAN_DEFAULT_TRUE); - Services.prefs.clearUserPref(PREF_NUMBER_DEFAULT_ZERO); - Services.prefs.clearUserPref(PREF_STRING_DEFAULT_EMPTY); - }); -}); - -add_task(async function test_add_user_pref() { - Assert.equal( - Services.prefs.getPrefType(PREF_NEW), - Ci.nsIPrefBranch.PREF_INVALID - ); - - await AboutConfigTest.withNewTab(async function () { - // The row for a new preference appears when searching for its name. - Assert.ok(!this.getRow(PREF_NEW)); - - for (let [radioIndex, expectedValue, expectedEditingMode] of [ - [0, true, false], - [1, 0, true], - [2, "", true], - ]) { - this.search(PREF_NEW); - let row = this.getRow(PREF_NEW); - Assert.ok(row.hasClass("deleted")); - Assert.ok(row.hasClass("add")); - - // Adding the preference should set the default for the data type. - row.element.querySelectorAll("input")[radioIndex].click(); - row.editColumnButton.click(); - Assert.ok(!row.hasClass("deleted")); - Assert.ok(!row.hasClass("add")); - Assert.ok(Preferences.get(PREF_NEW) === expectedValue); - - // Number and String preferences should be in edit mode. - Assert.equal(!!row.valueInput, expectedEditingMode); - - // Repeat the search to verify that the preference remains. - this.search(PREF_NEW); - row = this.getRow(PREF_NEW); - Assert.ok(!row.hasClass("deleted")); - Assert.ok(!row.hasClass("add")); - Assert.ok(!row.valueInput); - - // Reset the preference, then continue by adding a different type. - row.resetColumnButton.click(); - Assert.equal( - Services.prefs.getPrefType(PREF_NEW), - Ci.nsIPrefBranch.PREF_INVALID - ); - } - }); -}); - -add_task(async function test_delete_user_pref() { - for (let [radioIndex, testValue] of [ - [0, false], - [1, -1], - [2, "value"], - ]) { - Preferences.set(PREF_NEW, testValue); - await AboutConfigTest.withNewTab(async function () { - // Deleting the preference should keep the row. - let row = this.getRow(PREF_NEW); - row.resetColumnButton.click(); - Assert.ok(row.hasClass("deleted")); - Assert.equal( - Services.prefs.getPrefType(PREF_NEW), - Ci.nsIPrefBranch.PREF_INVALID - ); - - // Re-adding the preference should keep the same value. - Assert.ok(row.element.querySelectorAll("input")[radioIndex].checked); - row.editColumnButton.click(); - Assert.ok(!row.hasClass("deleted")); - Assert.ok(Preferences.get(PREF_NEW) === testValue); - - // Filtering again after deleting should remove the row. - row.resetColumnButton.click(); - this.showAll(); - Assert.ok(!this.getRow(PREF_NEW)); - }); - } -}); - -add_task(async function test_click_type_label_multiple_forms() { - // This test displays the row to add a preference while other preferences are - // also displayed, and tries to select the type of the new preference by - // clicking the label next to the radio button. This should work even if the - // user has deleted a different preference, and multiple forms are displayed. - const PREF_TO_DELETE = "test.aboutconfig.modify.boolean"; - const PREF_NEW_WHILE_DELETED = "test.aboutconfig.modify."; - - await AboutConfigTest.withNewTab(async function () { - this.search(PREF_NEW_WHILE_DELETED); - - // This preference will remain deleted during the test. - let existingRow = this.getRow(PREF_TO_DELETE); - existingRow.resetColumnButton.click(); - - let newRow = this.getRow(PREF_NEW_WHILE_DELETED); - - for (let [radioIndex, expectedValue] of [ - [0, true], - [1, 0], - [2, ""], - ]) { - let radioLabels = newRow.element.querySelectorAll("label > span"); - await this.document.l10n.translateElements(radioLabels); - - // Even if this is the second form on the page, the click should select - // the radio button next to the label, not the one on the first form. - EventUtils.synthesizeMouseAtCenter( - radioLabels[radioIndex], - {}, - this.browser.contentWindow - ); - - // Adding the preference should set the default for the data type. - newRow.editColumnButton.click(); - Assert.ok(Preferences.get(PREF_NEW_WHILE_DELETED) === expectedValue); - - // Reset the preference, then continue by adding a different type. - newRow.resetColumnButton.click(); - } - - // Re-adding the deleted preference should restore the value. - existingRow.editColumnButton.click(); - Assert.ok(Preferences.get(PREF_TO_DELETE) === true); - }); -}); - -add_task(async function test_reset_user_pref() { - await SpecialPowers.pushPrefEnv({ - set: [ - [PREF_BOOLEAN_DEFAULT_TRUE, false], - [PREF_STRING_LOCALIZED_MISSING, "user-value"], - ], - }); - - await AboutConfigTest.withNewTab(async function () { - // Click reset. - let row = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE); - row.resetColumnButton.click(); - // Check new layout and reset. - Assert.ok(!row.hasClass("has-user-value")); - Assert.ok(!row.resetColumnButton); - Assert.ok(!Services.prefs.prefHasUserValue(PREF_BOOLEAN_DEFAULT_TRUE)); - Assert.equal(this.getRow(PREF_BOOLEAN_DEFAULT_TRUE).value, "true"); - - // Filter again to test the preference cache. - this.showAll(); - row = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE); - Assert.ok(!row.hasClass("has-user-value")); - Assert.ok(!row.resetColumnButton); - Assert.equal(this.getRow(PREF_BOOLEAN_DEFAULT_TRUE).value, "true"); - - // Clicking reset on a localized preference without a corresponding value. - row = this.getRow(PREF_STRING_LOCALIZED_MISSING); - Assert.equal(row.value, "user-value"); - row.resetColumnButton.click(); - // Check new layout and reset. - Assert.ok(!row.hasClass("has-user-value")); - Assert.ok(!row.resetColumnButton); - Assert.ok(!Services.prefs.prefHasUserValue(PREF_STRING_LOCALIZED_MISSING)); - Assert.equal(this.getRow(PREF_STRING_LOCALIZED_MISSING).value, ""); - }); -}); - -add_task(async function test_modify() { - await AboutConfigTest.withNewTab(async function () { - // Test toggle for boolean prefs. - for (let nameOfBoolPref of [ - PREF_MODIFY_BOOLEAN, - PREF_BOOLEAN_DEFAULT_TRUE, - ]) { - let row = this.getRow(nameOfBoolPref); - // Do this a two times to reset the pref. - for (let i = 0; i < 2; i++) { - row.editColumnButton.click(); - // Check new layout and saving in backend. - Assert.equal( - this.getRow(nameOfBoolPref).value, - "" + Preferences.get(nameOfBoolPref) - ); - let prefHasUserValue = Services.prefs.prefHasUserValue(nameOfBoolPref); - Assert.equal(row.hasClass("has-user-value"), prefHasUserValue); - Assert.equal(!!row.resetColumnButton, prefHasUserValue); - } - } - - // Test abort of edit by starting with string and continuing with editing Int pref. - let row = this.getRow(PREF_MODIFY_STRING); - row.editColumnButton.click(); - row.valueInput.value = "test"; - let intRow = this.getRow(PREF_MODIFY_NUMBER); - intRow.editColumnButton.click(); - Assert.equal(intRow.valueInput.value, Preferences.get(PREF_MODIFY_NUMBER)); - Assert.ok(!row.valueInput); - Assert.equal(row.value, Preferences.get(PREF_MODIFY_STRING)); - - // Test validation of integer values. - for (let invalidValue of [ - "", - " ", - "a", - "1.5", - "-2147483649", - "2147483648", - ]) { - intRow.valueInput.value = invalidValue; - intRow.editColumnButton.click(); - // We should still be in edit mode. - Assert.ok(intRow.valueInput); - } - - // Test correct saving and DOM-update. - for (let [prefName, willDelete] of [ - [PREF_MODIFY_STRING, true], - [PREF_MODIFY_NUMBER, true], - [PREF_NUMBER_DEFAULT_ZERO, false], - [PREF_STRING_DEFAULT_EMPTY, false], - ]) { - row = this.getRow(prefName); - // Activate edit and check displaying. - row.editColumnButton.click(); - Assert.equal(row.valueInput.value, Preferences.get(prefName)); - row.valueInput.value = "42"; - // Save and check saving. - row.editColumnButton.click(); - Assert.equal(Preferences.get(prefName), "42"); - Assert.equal(row.value, "42"); - Assert.ok(row.hasClass("has-user-value")); - // Reset or delete the preference while editing. - row.editColumnButton.click(); - Assert.equal(row.valueInput.value, Preferences.get(prefName)); - row.resetColumnButton.click(); - Assert.ok(!row.hasClass("has-user-value")); - Assert.equal(row.hasClass("deleted"), willDelete); - } - }); - - // This test would have opened the invalid form popup, so just close it so as not to - // affect later tests. - let invalidFormPopup = window.document.getElementById("invalid-form-popup"); - invalidFormPopup.hidePopup(); - await BrowserTestUtils.waitForCondition(() => { - return invalidFormPopup.state == "closed"; - }, "form validation popup closed"); -}); - -add_task(async function test_edit_field_selected() { - let prefsToCheck = [ - [PREF_MODIFY_STRING, "A string", "A new string"], - [PREF_MODIFY_NUMBER, "100", "500"], - ]; - await AboutConfigTest.withNewTab(async function () { - for (let [prefName, startValue, endValue] of prefsToCheck) { - Preferences.set(prefName, startValue); - let row = this.getRow(prefName); - - Assert.equal(row.value, startValue); - row.editColumnButton.click(); - Assert.equal(row.valueInput.value, startValue); - Assert.equal( - row.valueInput.getAttribute("aria-label"), - prefName, - "The input field is labeled from the pref name" - ); - - EventUtils.sendString(endValue, this.window); - - row.editColumnButton.click(); - Assert.equal(row.value, endValue); - Assert.equal(Preferences.get(prefName), endValue); - } - }); -}); - -add_task(async function test_escape_cancels_edit() { - await AboutConfigTest.withNewTab(async function () { - let row = this.getRow(PREF_MODIFY_STRING); - Preferences.set(PREF_MODIFY_STRING, "Edit me, maybe"); - - for (let blurInput of [false, true]) { - Assert.ok(!row.valueInput); - row.editColumnButton.click(); - - Assert.ok(row.valueInput); - - Assert.equal(row.valueInput.value, "Edit me, maybe"); - row.valueInput.value = "Edited"; - - // Test both cases of the input being focused and not being focused. - if (blurInput) { - row.valueInput.blur(); - Assert.notEqual(this.document.activeElement, row.valueInput); - } else { - Assert.equal(this.document.activeElement, row.valueInput); - } - - EventUtils.synthesizeKey("KEY_Escape", {}, this.window); - - Assert.ok(!row.valueInput); - Assert.equal(row.value, "Edit me, maybe"); - Assert.equal(row.value, Preferences.get(PREF_MODIFY_STRING)); - } - }); -}); - -add_task(async function test_double_click_modify() { - Preferences.set(PREF_MODIFY_BOOLEAN, true); - Preferences.set(PREF_MODIFY_NUMBER, 10); - Preferences.set(PREF_MODIFY_STRING, "Hello!"); - - await AboutConfigTest.withNewTab(async function () { - this.search(PREF_MODIFY_PREFIX); - - let click = (target, opts) => - EventUtils.synthesizeMouseAtCenter(target, opts, this.window); - let doubleClick = target => { - // We intentionally turn off this a11y check, because the following series - // of clicks (in these test cases) is either performing an activation of - // the edit mode for prefs or selecting a text in focused inputs. The - // edit mode can be activated with a separate "Edit" or "Toggle" button - // provided for each pref, and the text selection can be performed with - // caret browsing (when supported). Thus, this rule check can be ignored - // by a11y_checks suite. - AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); - // Trigger two mouse events to simulate the first then second click. - click(target, { clickCount: 1 }); - click(target, { clickCount: 2 }); - AccessibilityUtils.resetEnv(); - }; - let tripleClick = target => { - // We intentionally turn off this a11y check, because the following series - // of clicks is purposefully targeting a non - interactive text content. - // This action does not require the element to have an interactive - // accessible to be done by assistive technology with caret browsing - // (when supported), this rule check shall be ignored by a11y_checks suite. - AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); - // Trigger all 3 mouse events to simulate the three mouse events we'd see. - click(target, { clickCount: 1 }); - click(target, { clickCount: 2 }); - click(target, { clickCount: 3 }); - AccessibilityUtils.resetEnv(); - }; - - // Check double-click to edit a boolean. - let boolRow = this.getRow(PREF_MODIFY_BOOLEAN); - Assert.equal(boolRow.value, "true"); - doubleClick(boolRow.valueCell); - Assert.equal(boolRow.value, "false"); - doubleClick(boolRow.nameCell); - Assert.equal(boolRow.value, "true"); - - // Check double-click to edit a number. - let intRow = this.getRow(PREF_MODIFY_NUMBER); - Assert.equal(intRow.value, 10); - doubleClick(intRow.valueCell); - Assert.equal(this.document.activeElement, intRow.valueInput); - EventUtils.sendString("75"); - EventUtils.synthesizeKey("KEY_Enter"); - Assert.equal(intRow.value, 75); - - // Check double-click focuses input when already editing. - Assert.equal(intRow.value, 75); - doubleClick(intRow.nameCell); - Assert.equal(this.document.activeElement, intRow.valueInput); - intRow.valueInput.blur(); - Assert.notEqual(this.document.activeElement, intRow.valueInput); - doubleClick(intRow.nameCell); - Assert.equal(this.document.activeElement, intRow.valueInput); - EventUtils.sendString("20"); - EventUtils.synthesizeKey("KEY_Enter"); - Assert.equal(intRow.value, 20); - - // Check double-click to edit a string. - let stringRow = this.getRow(PREF_MODIFY_STRING); - Assert.equal(stringRow.value, "Hello!"); - doubleClick(stringRow.valueCell); - Assert.equal( - this.document.activeElement, - stringRow.valueInput, - "The input is focused" - ); - EventUtils.sendString("New String!"); - EventUtils.synthesizeKey("KEY_Enter"); - Assert.equal(stringRow.value, "New String!"); - - // Check triple-click also edits the pref and selects the text inside. - tripleClick(stringRow.nameCell); - Assert.equal( - this.document.activeElement, - stringRow.valueInput, - "The input is focused" - ); - - // Check double-click inside input selects a word. - let newString = "Another string..."; - EventUtils.sendString(newString); - Assert.equal(this.window.getSelection().toString(), ""); - let stringInput = stringRow.valueInput; - doubleClick(stringInput); - let selectionLength = stringInput.selectionEnd - stringInput.selectionStart; - Assert.greater(selectionLength, 0); - Assert.less(selectionLength, newString.length); - EventUtils.synthesizeKey("KEY_Enter"); - Assert.equal(stringRow.value, newString); - - // Check that double/triple-click on the add row selects text as usual. - let addRow = this.getRow(PREF_MODIFY_PREFIX); - Assert.ok(addRow.hasClass("deleted")); - doubleClick(addRow.nameCell); - Assert.ok(PREF_MODIFY_PREFIX.includes(this.window.getSelection())); - tripleClick(addRow.nameCell); - Assert.equal(this.window.getSelection().toString(), PREF_MODIFY_PREFIX); - // Make sure the localized text is set in the value cell. - let labels = Array.from(addRow.valueCell.querySelectorAll("label > span")); - await this.document.l10n.translateElements(labels); - Assert.ok(labels.every(label => !!label.textContent)); - // Double-click the first input label text. - doubleClick(labels[0]); - Assert.equal(this.window.getSelection().toString(), labels[0].textContent); - tripleClick(addRow.valueCell.querySelector("label > span")); - Assert.equal( - this.window.getSelection().toString(), - labels.map(l => l.textContent).join("") - ); - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_locked.js b/toolkit/components/aboutconfig/test/browser/browser_locked.js deleted file mode 100644 index 6b06f22218b9f1..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_locked.js +++ /dev/null @@ -1,54 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -const PREF_STRING_NO_DEFAULT = "test.aboutconfig.a"; - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [[PREF_STRING_NO_DEFAULT, "some value"]], - }); -}); - -add_task(async function test_locked() { - registerCleanupFunction(() => { - Services.prefs.unlockPref(PREF_STRING_DEFAULT_NOTEMPTY); - Services.prefs.unlockPref(PREF_BOOLEAN_DEFAULT_TRUE); - Services.prefs.unlockPref(PREF_STRING_NO_DEFAULT); - }); - - Services.prefs.lockPref(PREF_STRING_DEFAULT_NOTEMPTY); - Services.prefs.lockPref(PREF_BOOLEAN_DEFAULT_TRUE); - Services.prefs.lockPref(PREF_STRING_NO_DEFAULT); - - await AboutConfigTest.withNewTab(async function () { - // Test locked default string pref. - let lockedPref = this.getRow(PREF_STRING_DEFAULT_NOTEMPTY); - Assert.ok(lockedPref.hasClass("locked")); - Assert.equal(lockedPref.value, PREF_STRING_DEFAULT_NOTEMPTY_VALUE); - Assert.ok(lockedPref.editColumnButton.classList.contains("button-edit")); - Assert.ok(lockedPref.editColumnButton.disabled); - - // Test locked default boolean pref. - lockedPref = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE); - Assert.ok(lockedPref.hasClass("locked")); - Assert.equal(lockedPref.value, "true"); - Assert.ok(lockedPref.editColumnButton.classList.contains("button-toggle")); - Assert.ok(lockedPref.editColumnButton.disabled); - - // Test locked user added pref. - lockedPref = this.getRow(PREF_STRING_NO_DEFAULT); - Assert.ok(lockedPref.hasClass("locked")); - Assert.equal(lockedPref.value, ""); - Assert.ok(lockedPref.editColumnButton.classList.contains("button-edit")); - Assert.ok(lockedPref.editColumnButton.disabled); - - // Test pref not locked. - let unlockedPref = this.getRow(PREF_BOOLEAN_USERVALUE_TRUE); - Assert.ok(!unlockedPref.hasClass("locked")); - Assert.equal(unlockedPref.value, "true"); - Assert.ok( - unlockedPref.editColumnButton.classList.contains("button-toggle") - ); - Assert.ok(!unlockedPref.editColumnButton.disabled); - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_observe.js b/toolkit/components/aboutconfig/test/browser/browser_observe.js deleted file mode 100644 index 1f1ab5d21745ca..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_observe.js +++ /dev/null @@ -1,163 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.aboutconfig.modify.boolean", true], - ["test.aboutconfig.modify.number", 1337], - [ - "test.aboutconfig.modify.string", - "the answer to the life the universe and everything", - ], - ], - }); - - registerCleanupFunction(() => { - Services.prefs.clearUserPref(PREF_BOOLEAN_DEFAULT_TRUE); - Services.prefs.clearUserPref(PREF_NUMBER_DEFAULT_ZERO); - Services.prefs.clearUserPref(PREF_STRING_DEFAULT_EMPTY); - }); -}); - -add_task(async function test_observe_add_user_pref_before_search() { - Assert.equal( - Services.prefs.getPrefType(PREF_NEW), - Ci.nsIPrefBranch.PREF_INVALID - ); - - await AboutConfigTest.withNewTab( - async function () { - this.bypassWarningButton.click(); - - // No results are shown after the warning page is dismissed or bypassed, - // and newly added preferences should not be displayed. - Preferences.set(PREF_NEW, true); - Assert.ok(!this.prefsTable.firstElementChild); - Preferences.reset(PREF_NEW); - }, - { dontBypassWarning: true } - ); -}); - -add_task(async function test_observe_add_user_pref() { - Assert.equal( - Services.prefs.getPrefType(PREF_NEW), - Ci.nsIPrefBranch.PREF_INVALID - ); - - await AboutConfigTest.withNewTab(async function () { - for (let value of [false, true, "", "value", 0, -10]) { - // A row should be added when a new preference is added. - Assert.ok(!this.getRow(PREF_NEW)); - Preferences.set(PREF_NEW, value); - let row = this.getRow(PREF_NEW); - Assert.equal(row.value, "" + value); - - // The row should stay when the preference is removed. - Preferences.reset(PREF_NEW); - Assert.ok(row.hasClass("deleted")); - - // Re-adding the preference from the interface should restore its value. - row.editColumnButton.click(); - if (value.constructor.name != "Boolean") { - row.editColumnButton.click(); - } - Assert.equal(row.value, "" + value); - Assert.ok(Preferences.get(PREF_NEW) === value); - - // Filtering again after deleting should remove the row. - Preferences.reset(PREF_NEW); - this.showAll(); - Assert.ok(!this.getRow(PREF_NEW)); - - // Searching for the preference name should give the ability to add it. - Preferences.reset(PREF_NEW); - this.search(PREF_NEW); - row = this.getRow(PREF_NEW); - Assert.ok(row.hasClass("deleted")); - - // The row for adding should be reused if the new preference is added. - Preferences.set(PREF_NEW, value); - Assert.equal(row.value, "" + value); - - // If a new preference does not match the filter it is not displayed. - Preferences.reset(PREF_NEW); - this.search(PREF_NEW + ".extra"); - Assert.ok(!this.getRow(PREF_NEW)); - Preferences.set(PREF_NEW, value); - Assert.ok(!this.getRow(PREF_NEW)); - - // Resetting the filter should display the new preference. - this.showAll(); - Assert.equal(this.getRow(PREF_NEW).value, "" + value); - - // Reset the preference, then continue by adding a different value. - Preferences.reset(PREF_NEW); - this.showAll(); - } - }); -}); - -add_task(async function test_observe_delete_user_pref() { - for (let value of [true, "value", -10]) { - Preferences.set(PREF_NEW, value); - await AboutConfigTest.withNewTab(async function () { - // Deleting the preference should keep the row. - let row = this.getRow(PREF_NEW); - Preferences.reset(PREF_NEW); - Assert.ok(row.hasClass("deleted")); - - // Filtering again should remove the row. - this.showAll(); - Assert.ok(!this.getRow(PREF_NEW)); - }); - } -}); - -add_task(async function test_observe_reset_user_pref() { - await SpecialPowers.pushPrefEnv({ - set: [[PREF_BOOLEAN_DEFAULT_TRUE, false]], - }); - - await AboutConfigTest.withNewTab(async function () { - let row = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE); - Preferences.reset(PREF_BOOLEAN_DEFAULT_TRUE); - Assert.ok(!row.hasClass("has-user-value")); - Assert.equal(row.value, "true"); - }); -}); - -add_task(async function test_observe_modify() { - await AboutConfigTest.withNewTab(async function () { - for (let [name, value] of [ - ["test.aboutconfig.modify.boolean", false], - ["test.aboutconfig.modify.number", -10], - ["test.aboutconfig.modify.string", "value"], - [PREF_BOOLEAN_DEFAULT_TRUE, false], - [PREF_NUMBER_DEFAULT_ZERO, 1], - [PREF_STRING_DEFAULT_EMPTY, "string"], - ]) { - let row = this.getRow(name); - Assert.notEqual(row.value, "" + value); - Preferences.set(name, value); - Assert.equal(row.value, "" + value); - - if (value.constructor.name == "Boolean") { - continue; - } - - // Changing the value or removing while editing should not take effect. - row.editColumnButton.click(); - row.valueInput.value = "42"; - Preferences.reset(name); - Assert.equal(row.element, this.getRow(name).element); - Assert.equal(row.valueInput.value, "42"); - - // Saving should store the value even if the preference was modified. - row.editColumnButton.click(); - Assert.equal(row.value, "42"); - Assert.equal(Preferences.get(name), "42"); - } - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_search.js b/toolkit/components/aboutconfig/test/browser/browser_search.js deleted file mode 100644 index 89a0c0c8668f14..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_search.js +++ /dev/null @@ -1,177 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["test.aboutconfig.a", "test value 1"], - ["test.aboutconfig.ab", "test value 2"], - ["test.aboutconfig.bc", "test value 3"], - ], - }); -}); - -add_task(async function test_search() { - await AboutConfigTest.withNewTab(async function () { - await this.document.l10n.translateFragment(this.document.documentElement); - let prefArray = Services.prefs.getChildList(""); - - // The total number of preferences may change at any time because of - // operations running in the background, so we only test approximately. - // The change in count would be because of one or two added preferences, - // but we tolerate a difference of up to 50 preferences just to be safe. - // We want thousands of prefs instead of a few dozen that are filtered. - Assert.greater(this.rows.length, prefArray.length - 50); - - // Filter a subset of preferences. The "browser.download." branch is - // chosen because it is very unlikely that its preferences would be - // modified by other code during the execution of this test. - this.search("Wser.down "); - - let filteredPrefArray = prefArray.filter(pref => - pref.includes("wser.down") - ); - // Adding +1 to the list since button does not match an exact - // preference name then a row is added for the user to add a - // new button preference if desired - Assert.equal(this.rows.length, filteredPrefArray.length + 1); - - // Show all preferences again after filtering. - this.showAll(); - Assert.equal(this.searchInput.value, ""); - - // The total number of preferences may change at any time because of - // operations running in the background, so we only test approximately. - // The change in count would be because of one or two added preferences, - // but we tolerate a difference of up to 50 preferences just to be safe. - // We want thousands of prefs instead of a few dozen that are filtered. - Assert.greater(this.rows.length, prefArray.length - 50); - - // Check if "Only show modified" feature works. - EventUtils.sendMouseEvent({ type: "click" }, this.showOnlyModifiedCheckbox); - Assert.ok(this.rows.every(r => r.hasClass("has-user-value"))); - - // Uncheck checkbox - EventUtils.sendMouseEvent({ type: "click" }, this.showOnlyModifiedCheckbox); - Assert.ok(!this.rows.every(r => r.hasClass("has-user-value"))); - - // Pressing ESC while showing all preferences returns to the initial page. - EventUtils.sendKey("escape"); - Assert.equal(this.rows.length, 0); - - // Test invalid search returns no preferences. - // Expecting 1 row to be returned since it offers the ability to add. - this.search("aJunkValueasdf"); - Assert.equal(this.rows.length, 1); - // The has-visible-prefs attribute is used to style the border of the add row. - Assert.ok(!this.prefsTable.hasAttribute("has-visible-prefs")); - let addRow = this.getRow("aJunkValueasdf"); - Assert.equal(getComputedStyle(addRow.valueCell)["border-top-width"], "0px"); - - // Pressing ESC clears the field and returns to the initial page. - EventUtils.sendKey("escape"); - Assert.equal(this.searchInput.value, ""); - Assert.equal(this.rows.length, 0); - - // Two preferences match this filter, and one of those matches exactly. - this.search("test.aboutconfig.a"); - Assert.equal(this.rows.length, 2); - - // When searching case insensitively, there is an additional row to add a - // new preference with the same name but a different case. - this.search("TEST.aboutconfig.a"); - Assert.equal(this.rows.length, 3); - // The has-visible-prefs attribute is used to style the border of the add row. - Assert.ok(this.prefsTable.hasAttribute("has-visible-prefs")); - addRow = this.getRow("TEST.aboutconfig.a"); - Assert.equal(getComputedStyle(addRow.valueCell)["border-top-width"], "1px"); - - // Entering an empty string returns to the initial page. - this.search(""); - Assert.equal(this.rows.length, 0); - Assert.ok(!this.prefsTable.hasAttribute("has-visible-prefs")); - }); -}); - -add_task(async function test_search_wildcard() { - await AboutConfigTest.withNewTab(async function () { - const extra = 1; // "Add" row - - // A trailing wildcard - this.search("test.about*"); - Assert.equal(this.rows.length, 3 + extra); - - // A wildcard in middle - this.search("test.about*a"); - Assert.equal(this.rows.length, 2 + extra); - this.search("test.about*ab"); - Assert.equal(this.rows.length, 1 + extra); - this.search("test.aboutcon*fig"); - Assert.equal(this.rows.length, 3 + extra); - - // Multiple wildcards in middle - this.search("test.about*fig*ab"); - Assert.equal(this.rows.length, 1 + extra); - this.search("test.about*config*ab"); - Assert.equal(this.rows.length, 1 + extra); - }); -}); - -add_task(async function test_search_delayed() { - await AboutConfigTest.withNewTab(async function () { - // Start with the initial empty page. - this.search(""); - - // We need to wait more than the search typing timeout to make sure that - // nothing happens when entering a short string. - EventUtils.synthesizeKey("t"); - EventUtils.synthesizeKey("e"); - // eslint-disable-next-line mozilla/no-arbitrary-setTimeout - await new Promise(resolve => setTimeout(resolve, 500)); - Assert.equal(this.rows.length, 0); - - // Pressing Enter will force a search to occur anyways. - EventUtils.sendKey("return"); - Assert.greater(this.rows.length, 0); - - // Prepare the table and the search field for the next test. - this.search("test.aboutconfig.a"); - Assert.equal(this.rows.length, 2); - - // The table is updated in a single microtask, so we don't need to wait for - // specific mutations, we can just continue when any of the children or - // their "hidden" attributes are updated. - let prefsTableChanged = new Promise(resolve => { - let observer = new MutationObserver(() => { - observer.disconnect(); - resolve(); - }); - observer.observe(this.prefsTable, { childList: true }); - for (let element of this.prefsTable.children) { - observer.observe(element, { attributes: true }); - } - }); - - // Add a character and test that the table is not updated immediately. - EventUtils.synthesizeKey("b"); - Assert.equal(this.rows.length, 2); - - // The table will eventually be updated after a delay. - await prefsTableChanged; - Assert.equal(this.rows.length, 1); - }); -}); - -add_task(async function test_search_add_row_color() { - await AboutConfigTest.withNewTab(async function () { - // When the row is the only one displayed, it doesn't have the "odd" class. - this.search("test.aboutconfig.add"); - Assert.equal(this.rows.length, 1); - Assert.ok(!this.getRow("test.aboutconfig.add").hasClass("odd")); - - // When displayed with one other preference, the "odd" class is present. - this.search("test.aboutconfig.b"); - Assert.equal(this.rows.length, 2); - Assert.ok(this.getRow("test.aboutconfig.b").hasClass("odd")); - }); -}); diff --git a/toolkit/components/aboutconfig/test/browser/browser_warning.js b/toolkit/components/aboutconfig/test/browser/browser_warning.js deleted file mode 100644 index d95e8f49eaa6bc..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/browser_warning.js +++ /dev/null @@ -1,41 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [["browser.aboutConfig.showWarning", true]], - }); -}); - -add_task(async function test_showWarningNextTime() { - for (let test of [ - { expectWarningPage: true, disableShowWarningNextTime: false }, - { expectWarningPage: true, disableShowWarningNextTime: true }, - { expectWarningPage: false }, - ]) { - await AboutConfigTest.withNewTab( - async function () { - if (test.expectWarningPage) { - this.assertWarningPage(true); - Assert.ok( - this.document.getElementById("showWarningNextTime").checked - ); - if (test.disableShowWarningNextTime) { - this.document.getElementById("showWarningNextTime").click(); - } - this.bypassWarningButton.click(); - } - - // No results are shown after the warning page is dismissed or bypassed. - this.assertWarningPage(false); - Assert.ok(!this.prefsTable.firstElementChild); - Assert.equal(this.document.activeElement, this.searchInput); - - // The show all button should be present and show all results immediately. - this.showAll(); - Assert.ok(this.prefsTable.firstElementChild); - }, - { dontBypassWarning: true } - ); - } -}); diff --git a/toolkit/components/aboutconfig/test/browser/head.js b/toolkit/components/aboutconfig/test/browser/head.js deleted file mode 100644 index 511fc07a37bfb7..00000000000000 --- a/toolkit/components/aboutconfig/test/browser/head.js +++ /dev/null @@ -1,173 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -const { Preferences } = ChromeUtils.importESModule( - "resource://gre/modules/Preferences.sys.mjs" -); - -// List of default preferences that can be used for tests, chosen because they -// have little or no side-effects when they are modified for a brief time. If -// any of these preferences are removed or their default state changes, just -// update the constant to point to a different preference with the same default. -const PREF_BOOLEAN_DEFAULT_TRUE = "accessibility.typeaheadfind.manual"; -const PREF_BOOLEAN_USERVALUE_TRUE = "browser.dom.window.dump.enabled"; -const PREF_NUMBER_DEFAULT_ZERO = "accessibility.typeaheadfind.casesensitive"; -const PREF_STRING_DEFAULT_EMPTY = "browser.helperApps.neverAsk.openFile"; -const PREF_STRING_DEFAULT_NOTEMPTY = "accessibility.typeaheadfind.soundURL"; -const PREF_STRING_DEFAULT_NOTEMPTY_VALUE = "beep"; -const PREF_STRING_LOCALIZED_MISSING = "intl.menuitems.alwaysappendaccesskeys"; - -// Other preference names used in tests. -const PREF_NEW = "test.aboutconfig.new"; - -// These tests can be slow to execute because they show all the preferences -// several times, and each time can require a second on some virtual machines. -requestLongerTimeout(2); - -class AboutConfigRowTest { - constructor(element) { - this.element = element; - } - - querySelector(selector) { - return this.element.querySelector(selector); - } - - get nameCell() { - return this.querySelector("th"); - } - - get name() { - return this.nameCell.textContent; - } - - get valueCell() { - return this.querySelector("td.cell-value"); - } - - get value() { - return this.valueCell.textContent; - } - - /** - * Text input field when the row is in edit mode. - */ - get valueInput() { - return this.valueCell.querySelector("input"); - } - - /** - * This is normally "edit" or "toggle" based on the preference type, "save" - * when the row is in edit mode, or "add" when the preference does not exist. - */ - get editColumnButton() { - return this.querySelector("td.cell-edit > button"); - } - - /** - * This can be "reset" or "delete" based on whether a default exists. - */ - get resetColumnButton() { - return this.querySelector("td:last-child > button"); - } - - hasClass(className) { - return this.element.classList.contains(className); - } -} - -class AboutConfigTest { - static withNewTab(testFn, options = {}) { - return BrowserTestUtils.withNewTab( - { - gBrowser, - url: "chrome://global/content/aboutconfig/aboutconfig.html", - }, - async browser => { - let scope = new this(browser); - await scope.setupNewTab(options); - await testFn.call(scope); - } - ); - } - - constructor(browser) { - this.browser = browser; - this.document = browser.contentDocument; - this.window = browser.contentWindow; - } - - async setupNewTab(options) { - await this.document.l10n.ready; - if (!options.dontBypassWarning) { - this.bypassWarningButton.click(); - this.showAll(); - } - } - - get showWarningNextTimeInput() { - return this.document.getElementById("showWarningNextTime"); - } - - get bypassWarningButton() { - return this.document.getElementById("warningButton"); - } - - get searchInput() { - return this.document.getElementById("about-config-search"); - } - - get showOnlyModifiedCheckbox() { - return this.document.getElementById("about-config-show-only-modified"); - } - - get prefsTable() { - return this.document.getElementById("prefs"); - } - - /** - * Array of AboutConfigRowTest objects, one for each row in the main table. - */ - get rows() { - let elements = this.prefsTable.querySelectorAll("tr:not(.hidden)"); - return Array.from(elements, element => new AboutConfigRowTest(element)); - } - - /** - * Returns the AboutConfigRowTest object for the row in the main table which - * corresponds to the given preference name, or undefined if none is present. - */ - getRow(name) { - return this.rows.find(row => row.name == name); - } - - /** - * Shows all preferences using the dedicated button. - */ - showAll() { - this.search(""); - this.document.getElementById("show-all").click(); - } - - /** - * Performs a new search using the dedicated textbox. This also makes sure - * that the list of preferences displayed is up to date. - */ - search(value) { - this.searchInput.value = value; - this.searchInput.focus(); - EventUtils.sendKey("return"); - } - - /** - * Checks whether or not the initial warning page is displayed. - */ - assertWarningPage(expected) { - Assert.equal(!!this.showWarningNextTimeInput, expected); - Assert.equal(!!this.bypassWarningButton, expected); - Assert.equal(!this.searchInput, expected); - Assert.equal(!this.prefsTable, expected); - } -} diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index 532b0e96513835..b5a18ccd044a97 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -92,7 +92,7 @@ if CONFIG["NS_PRINTING"]: if CONFIG["MOZ_BUILD_APP"] != "mobile/android": DIRS += [ - "aboutconfig", + "viewconfig", "narrate", "pictureinpicture", "reader", diff --git a/toolkit/components/viewconfig/content/config.js b/toolkit/components/viewconfig/content/config.js new file mode 100644 index 00000000000000..741314ba95e740 --- /dev/null +++ b/toolkit/components/viewconfig/content/config.js @@ -0,0 +1,827 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* 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 nsIPrefLocalizedString = Ci.nsIPrefLocalizedString; +const nsISupportsString = Ci.nsISupportsString; +const nsIPrefBranch = Ci.nsIPrefBranch; +const nsIClipboardHelper = Ci.nsIClipboardHelper; + +const nsClipboardHelper_CONTRACTID = "@mozilla.org/widget/clipboardhelper;1"; + +const gPrefBranch = Services.prefs; +const gClipboardHelper = Cc[nsClipboardHelper_CONTRACTID].getService( + nsIClipboardHelper +); + +var gLockProps = ["default", "user", "locked"]; +// we get these from a string bundle +var gLockStrs = []; +var gTypeStrs = []; + +const PREF_IS_DEFAULT_VALUE = 0; +const PREF_IS_MODIFIED = 1; +const PREF_IS_LOCKED = 2; + +var gPrefHash = {}; +var gPrefArray = []; +var gPrefView = gPrefArray; // share the JS array +var gSortedColumn = "prefCol"; +var gSortFunction = null; +var gSortDirection = 1; // 1 is ascending; -1 is descending +var gFilter = null; + +let gCategoriesRecordedOnce = new Set(); + +var view = { + get rowCount() { + return gPrefView.length; + }, + getCellText(index, col) { + if (!(index in gPrefView)) { + return ""; + } + + var value = gPrefView[index][col.id]; + + switch (col.id) { + case "lockCol": + return gLockStrs[value]; + case "typeCol": + return gTypeStrs[value]; + default: + return value; + } + }, + getRowProperties(index) { + return ""; + }, + getCellProperties(index, col) { + if (index in gPrefView) { + return gLockProps[gPrefView[index].lockCol]; + } + + return ""; + }, + getColumnProperties(col) { + return ""; + }, + treebox: null, + selection: null, + isContainer(index) { + return false; + }, + isContainerOpen(index) { + return false; + }, + isContainerEmpty(index) { + return false; + }, + isSorted() { + return true; + }, + canDrop(index, orientation) { + return false; + }, + drop(row, orientation) {}, + setTree(out) { + this.treebox = out; + }, + getParentIndex(rowIndex) { + return -1; + }, + hasNextSibling(rowIndex, afterIndex) { + return false; + }, + getLevel(index) { + return 1; + }, + getImageSrc(row, col) { + return ""; + }, + toggleOpenState(index) {}, + cycleHeader(col) { + var index = this.selection.currentIndex; + recordTelemetryOnce(gCategoryLabelForSortColumn[col.id]); + if (col.id == gSortedColumn) { + gSortDirection = -gSortDirection; + gPrefArray.reverse(); + if (gPrefView != gPrefArray) { + gPrefView.reverse(); + } + if (index >= 0) { + index = gPrefView.length - index - 1; + } + } else { + var pref = null; + if (index >= 0) { + pref = gPrefView[index]; + } + + var old = document.getElementById(gSortedColumn); + old.removeAttribute("sortDirection"); + gPrefArray.sort((gSortFunction = gSortFunctions[col.id])); + if (gPrefView != gPrefArray) { + gPrefView.sort(gSortFunction); + } + gSortedColumn = col.id; + if (pref) { + index = getViewIndexOfPref(pref); + } + } + col.element.setAttribute( + "sortDirection", + gSortDirection > 0 ? "ascending" : "descending" + ); + this.treebox.invalidate(); + if (index >= 0) { + this.selection.select(index); + this.treebox.ensureRowIsVisible(index); + } + }, + selectionChanged() {}, + cycleCell(row, col) {}, + isEditable(row, col) { + return false; + }, + setCellValue(row, col, value) {}, + setCellText(row, col, value) {}, + isSeparator(index) { + return false; + }, +}; + +// find the index in gPrefView of a pref object +// or -1 if it does not exist in the filtered view +function getViewIndexOfPref(pref) { + var low = -1, + high = gPrefView.length; + var index = (low + high) >> 1; + while (index > low) { + var mid = gPrefView[index]; + if (mid == pref) { + return index; + } + if (gSortFunction(mid, pref) < 0) { + low = index; + } else { + high = index; + } + index = (low + high) >> 1; + } + return -1; +} + +// find the index in gPrefView where a pref object belongs +function getNearestViewIndexOfPref(pref) { + var low = -1, + high = gPrefView.length; + var index = (low + high) >> 1; + while (index > low) { + if (gSortFunction(gPrefView[index], pref) < 0) { + low = index; + } else { + high = index; + } + index = (low + high) >> 1; + } + return high; +} + +// find the index in gPrefArray of a pref object +function getIndexOfPref(pref) { + var low = -1, + high = gPrefArray.length; + var index = (low + high) >> 1; + while (index > low) { + var mid = gPrefArray[index]; + if (mid == pref) { + return index; + } + if (gSortFunction(mid, pref) < 0) { + low = index; + } else { + high = index; + } + index = (low + high) >> 1; + } + return index; +} + +function getNearestIndexOfPref(pref) { + var low = -1, + high = gPrefArray.length; + var index = (low + high) >> 1; + while (index > low) { + if (gSortFunction(gPrefArray[index], pref) < 0) { + low = index; + } else { + high = index; + } + index = (low + high) >> 1; + } + return high; +} + +var gPrefListener = { + observe(subject, topic, prefName) { + if (topic != "nsPref:changed") { + return; + } + + var arrayIndex = gPrefArray.length; + var viewIndex = arrayIndex; + var selectedIndex = view.selection.currentIndex; + var pref; + var updateView = false; + var updateArray = false; + var addedRow = false; + if (prefName in gPrefHash) { + pref = gPrefHash[prefName]; + viewIndex = getViewIndexOfPref(pref); + arrayIndex = getIndexOfPref(pref); + fetchPref(prefName, arrayIndex); + // fetchPref replaces the existing pref object + pref = gPrefHash[prefName]; + if (viewIndex >= 0) { + // Might need to update the filtered view + gPrefView[viewIndex] = gPrefHash[prefName]; + view.treebox.invalidateRow(viewIndex); + } + if (gSortedColumn == "lockCol" || gSortedColumn == "valueCol") { + updateArray = true; + gPrefArray.splice(arrayIndex, 1); + if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) { + updateView = true; + gPrefView.splice(viewIndex, 1); + } + } + } else { + fetchPref(prefName, arrayIndex); + pref = gPrefArray.pop(); + updateArray = true; + addedRow = true; + if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) { + updateView = true; + } + } + if (updateArray) { + // Reinsert in the data array + var newIndex = getNearestIndexOfPref(pref); + gPrefArray.splice(newIndex, 0, pref); + + if (updateView) { + // View is filtered, reinsert in the view separately + newIndex = getNearestViewIndexOfPref(pref); + gPrefView.splice(newIndex, 0, pref); + } else if (gFilter) { + // View is filtered, but nothing to update + return; + } + + if (addedRow) { + view.treebox.rowCountChanged(newIndex, 1); + } + + // Invalidate the changed range in the view + var low = Math.min(viewIndex, newIndex); + var high = Math.max(viewIndex, newIndex); + view.treebox.invalidateRange(low, high); + + if (selectedIndex == viewIndex) { + selectedIndex = newIndex; + } else if (selectedIndex >= low && selectedIndex <= high) { + selectedIndex += newIndex > viewIndex ? -1 : 1; + } + if (selectedIndex >= 0) { + view.selection.select(selectedIndex); + if (selectedIndex == newIndex) { + view.treebox.ensureRowIsVisible(selectedIndex); + } + } + } + }, +}; + +function prefObject(prefName, prefIndex) { + this.prefCol = prefName; +} + +prefObject.prototype = { + lockCol: PREF_IS_DEFAULT_VALUE, + typeCol: nsIPrefBranch.PREF_STRING, + valueCol: "", +}; + +function fetchPref(prefName, prefIndex) { + var pref = new prefObject(prefName); + + gPrefHash[prefName] = pref; + gPrefArray[prefIndex] = pref; + + if (gPrefBranch.prefIsLocked(prefName)) { + pref.lockCol = PREF_IS_LOCKED; + } else if (gPrefBranch.prefHasUserValue(prefName)) { + pref.lockCol = PREF_IS_MODIFIED; + } + + try { + switch (gPrefBranch.getPrefType(prefName)) { + case gPrefBranch.PREF_BOOL: + pref.typeCol = gPrefBranch.PREF_BOOL; + // convert to a string + pref.valueCol = gPrefBranch.getBoolPref(prefName).toString(); + break; + case gPrefBranch.PREF_INT: + pref.typeCol = gPrefBranch.PREF_INT; + // convert to a string + pref.valueCol = gPrefBranch.getIntPref(prefName).toString(); + break; + default: + case gPrefBranch.PREF_STRING: + pref.valueCol = gPrefBranch.getStringPref(prefName); + // Try in case it's a localized string (will throw an exception if not) + if ( + pref.lockCol == PREF_IS_DEFAULT_VALUE && + /^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.valueCol) + ) { + pref.valueCol = gPrefBranch.getComplexValue( + prefName, + nsIPrefLocalizedString + ).data; + } + break; + } + } catch (e) { + // Also catch obscure cases in which you can't tell in advance + // that the pref exists but has no user or default value... + } +} + +async function onConfigLoad() { + let configContext = document.getElementById("configContext"); + configContext.addEventListener("popupshowing", function(event) { + if (event.target == this) { + updateContextMenu(); + } + }); + + let commandListeners = { + toggleSelected: ModifySelected, + modifySelected: ModifySelected, + copyPref, + copyName, + copyValue, + resetSelected: ResetSelected, + }; + + configContext.addEventListener("command", e => { + if (e.target.id in commandListeners) { + commandListeners[e.target.id](); + } + }); + + let configString = document.getElementById("configString"); + configString.addEventListener("command", function() { + NewPref(nsIPrefBranch.PREF_STRING); + }); + + let configInt = document.getElementById("configInt"); + configInt.addEventListener("command", function() { + NewPref(nsIPrefBranch.PREF_INT); + }); + + let configBool = document.getElementById("configBool"); + configBool.addEventListener("command", function() { + NewPref(nsIPrefBranch.PREF_BOOL); + }); + + let keyVKReturn = document.getElementById("keyVKReturn"); + keyVKReturn.addEventListener("command", ModifySelected); + + let textBox = document.getElementById("textbox"); + textBox.addEventListener("command", FilterPrefs); + + let configFocuSearch = document.getElementById("configFocuSearch"); + configFocuSearch.addEventListener("command", function() { + textBox.focus(); + }); + + let configFocuSearch2 = document.getElementById("configFocuSearch2"); + configFocuSearch2.addEventListener("command", function() { + textBox.focus(); + }); + + let warningButton = document.getElementById("warningButton"); + warningButton.addEventListener("command", ShowPrefs); + + let configTree = document.getElementById("configTree"); + configTree.addEventListener("select", function() { + window.updateCommands("select"); + }); + + let configTreeBody = document.getElementById("configTreeBody"); + configTreeBody.addEventListener("dblclick", function(event) { + if (event.button == 0) { + ModifySelected(); + } + }); + + // Load strings + let [ + lockDefault, + lockModified, + lockLocked, + typeString, + typeInt, + typeBool, + ] = await document.l10n.formatValues([ + { id: "config-default" }, + { id: "config-modified" }, + { id: "config-locked" }, + { id: "config-property-string" }, + { id: "config-property-int" }, + { id: "config-property-bool" }, + ]); + + gLockStrs[PREF_IS_DEFAULT_VALUE] = lockDefault; + gLockStrs[PREF_IS_MODIFIED] = lockModified; + gLockStrs[PREF_IS_LOCKED] = lockLocked; + gTypeStrs[nsIPrefBranch.PREF_STRING] = typeString; + gTypeStrs[nsIPrefBranch.PREF_INT] = typeInt; + gTypeStrs[nsIPrefBranch.PREF_BOOL] = typeBool; + + var showWarning = gPrefBranch.getBoolPref("browser.aboutConfig.showWarning"); + + if (showWarning) { + document.getElementById("warningButton").focus(); + } else { + ShowPrefs(); + } +} + +// Unhide the warning message +function ShowPrefs() { + recordTelemetryOnce("Show"); + gPrefBranch.getChildList("").forEach(fetchPref); + + var descending = document.getElementsByAttribute( + "sortDirection", + "descending" + ); + if (descending.item(0)) { + gSortedColumn = descending[0].id; + gSortDirection = -1; + } else { + var ascending = document.getElementsByAttribute( + "sortDirection", + "ascending" + ); + if (ascending.item(0)) { + gSortedColumn = ascending[0].id; + } else { + document + .getElementById(gSortedColumn) + .setAttribute("sortDirection", "ascending"); + } + } + gSortFunction = gSortFunctions[gSortedColumn]; + gPrefArray.sort(gSortFunction); + + gPrefBranch.addObserver("", gPrefListener); + + var configTree = document.getElementById("configTree"); + configTree.view = view; + configTree.controllers.insertControllerAt(0, configController); + + document.getElementById("configDeck").selectedIndex = 1; + document.getElementById("configTreeKeyset").removeAttribute("disabled"); + if (!document.getElementById("showWarningNextTime").checked) { + gPrefBranch.setBoolPref("browser.aboutConfig.showWarning", false); + } + + // Process about:config?filter= + var textbox = document.getElementById("textbox"); + // About URIs don't support query params, so do this manually + var loc = document.location.href; + var matches = /[?&]filter\=([^&]+)/i.exec(loc); + if (matches) { + textbox.value = decodeURIComponent(matches[1]); + } + + // Even if we did not set the filter string via the URL query, + // textbox might have been set via some other mechanism + if (textbox.value) { + FilterPrefs(); + } + textbox.focus(); +} + +function onConfigUnload() { + if ( + document.getElementById("configDeck").getAttribute("selectedIndex") == 1 + ) { + gPrefBranch.removeObserver("", gPrefListener); + var configTree = document.getElementById("configTree"); + configTree.view = null; + configTree.controllers.removeController(configController); + } +} + +function FilterPrefs() { + if ( + document.getElementById("configDeck").getAttribute("selectedIndex") != 1 + ) { + return; + } + + var substring = document.getElementById("textbox").value; + // Check for "/regex/[i]" + if (substring.charAt(0) == "/") { + recordTelemetryOnce("RegexSearch"); + var r = substring.match(/^\/(.*)\/(i?)$/); + try { + gFilter = RegExp(r[1], r[2]); + } catch (e) { + return; // Do nothing on incomplete or bad RegExp + } + } else if (substring) { + recordTelemetryOnce("Search"); + gFilter = RegExp( + substring + .replace(/([^* \w])/g, "\\$1") + .replace(/^\*+/, "") + .replace(/\*+/g, ".*"), + "i" + ); + } else { + gFilter = null; + } + + var prefCol = + view.selection && view.selection.currentIndex < 0 + ? null + : gPrefView[view.selection.currentIndex].prefCol; + var oldlen = gPrefView.length; + gPrefView = gPrefArray; + if (gFilter) { + gPrefView = []; + for (var i = 0; i < gPrefArray.length; ++i) { + if (gFilter.test(gPrefArray[i].prefCol + ";" + gPrefArray[i].valueCol)) { + gPrefView.push(gPrefArray[i]); + } + } + } + view.treebox.invalidate(); + view.treebox.rowCountChanged(oldlen, gPrefView.length - oldlen); + gotoPref(prefCol); +} + +function prefColSortFunction(x, y) { + if (x.prefCol > y.prefCol) { + return gSortDirection; + } + if (x.prefCol < y.prefCol) { + return -gSortDirection; + } + return 0; +} + +function lockColSortFunction(x, y) { + if (x.lockCol != y.lockCol) { + return gSortDirection * (y.lockCol - x.lockCol); + } + return prefColSortFunction(x, y); +} + +function typeColSortFunction(x, y) { + if (x.typeCol != y.typeCol) { + return gSortDirection * (y.typeCol - x.typeCol); + } + return prefColSortFunction(x, y); +} + +function valueColSortFunction(x, y) { + if (x.valueCol > y.valueCol) { + return gSortDirection; + } + if (x.valueCol < y.valueCol) { + return -gSortDirection; + } + return prefColSortFunction(x, y); +} + +const gSortFunctions = { + prefCol: prefColSortFunction, + lockCol: lockColSortFunction, + typeCol: typeColSortFunction, + valueCol: valueColSortFunction, +}; + +const gCategoryLabelForSortColumn = { + prefCol: "SortByName", + lockCol: "SortByStatus", + typeCol: "SortByType", + valueCol: "SortByValue", +}; + +const configController = { + supportsCommand: function supportsCommand(command) { + return command == "cmd_copy"; + }, + isCommandEnabled: function isCommandEnabled(command) { + return view.selection && view.selection.currentIndex >= 0; + }, + doCommand: function doCommand(command) { + copyPref(); + }, + onEvent: function onEvent(event) {}, +}; + +function updateContextMenu() { + var lockCol = PREF_IS_LOCKED; + var typeCol = nsIPrefBranch.PREF_STRING; + var valueCol = ""; + var copyDisabled = true; + var prefSelected = view.selection.currentIndex >= 0; + + if (prefSelected) { + var prefRow = gPrefView[view.selection.currentIndex]; + lockCol = prefRow.lockCol; + typeCol = prefRow.typeCol; + valueCol = prefRow.valueCol; + copyDisabled = false; + } + + var copyPref = document.getElementById("copyPref"); + copyPref.setAttribute("disabled", copyDisabled); + + var copyName = document.getElementById("copyName"); + copyName.setAttribute("disabled", copyDisabled); + + var copyValue = document.getElementById("copyValue"); + copyValue.setAttribute("disabled", copyDisabled); + + var resetSelected = document.getElementById("resetSelected"); + resetSelected.setAttribute("disabled", lockCol != PREF_IS_MODIFIED); + + var canToggle = typeCol == nsIPrefBranch.PREF_BOOL && valueCol != ""; + // indicates that a pref is locked or no pref is selected at all + var isLocked = lockCol == PREF_IS_LOCKED; + + var modifySelected = document.getElementById("modifySelected"); + modifySelected.setAttribute("disabled", isLocked); + modifySelected.hidden = canToggle; + + var toggleSelected = document.getElementById("toggleSelected"); + toggleSelected.setAttribute("disabled", isLocked); + toggleSelected.hidden = !canToggle; +} + +function copyPref() { + recordTelemetryOnce("Copy"); + var pref = gPrefView[view.selection.currentIndex]; + gClipboardHelper.copyString(pref.prefCol + ";" + pref.valueCol); +} + +function copyName() { + recordTelemetryOnce("CopyName"); + gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].prefCol); +} + +function copyValue() { + recordTelemetryOnce("CopyValue"); + gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].valueCol); +} + +function ModifySelected() { + recordTelemetryOnce("ModifyValue"); + if (view.selection.currentIndex >= 0) { + ModifyPref(gPrefView[view.selection.currentIndex]); + } +} + +function ResetSelected() { + recordTelemetryOnce("Reset"); + var entry = gPrefView[view.selection.currentIndex]; + gPrefBranch.clearUserPref(entry.prefCol); +} + +async function NewPref(type) { + recordTelemetryOnce("CreateNew"); + var result = { value: "" }; + var dummy = { value: 0 }; + + let [newTitle, newPrompt] = await document.l10n.formatValues([ + { id: "config-new-title", args: { type: gTypeStrs[type] } }, + { id: "config-new-prompt" }, + ]); + + if ( + Services.prompt.prompt(window, newTitle, newPrompt, result, null, dummy) + ) { + result.value = result.value.trim(); + if (!result.value) { + return; + } + + var pref; + if (result.value in gPrefHash) { + pref = gPrefHash[result.value]; + } else { + pref = { + prefCol: result.value, + lockCol: PREF_IS_DEFAULT_VALUE, + typeCol: type, + valueCol: "", + }; + } + if (ModifyPref(pref)) { + setTimeout(gotoPref, 0, result.value); + } + } +} + +function gotoPref(pref) { + // make sure the pref exists and is displayed in the current view + var index = pref in gPrefHash ? getViewIndexOfPref(gPrefHash[pref]) : -1; + if (index >= 0) { + view.selection.select(index); + view.treebox.ensureRowIsVisible(index); + } else { + view.selection.clearSelection(); + view.selection.currentIndex = -1; + } +} + +async function ModifyPref(entry) { + if (entry.lockCol == PREF_IS_LOCKED) { + return false; + } + + let [title] = await document.l10n.formatValues([ + { id: "config-modify-title", args: { type: gTypeStrs[entry.typeCol] } }, + ]); + + if (entry.typeCol == nsIPrefBranch.PREF_BOOL) { + var check = { value: entry.valueCol == "false" }; + if ( + !entry.valueCol && + !Services.prompt.select( + window, + title, + entry.prefCol, + [false, true], + check + ) + ) { + return false; + } + gPrefBranch.setBoolPref(entry.prefCol, check.value); + } else { + var result = { value: entry.valueCol }; + var dummy = { value: 0 }; + if ( + !Services.prompt.prompt(window, title, entry.prefCol, result, null, dummy) + ) { + return false; + } + if (entry.typeCol == nsIPrefBranch.PREF_INT) { + // | 0 converts to integer or 0; - 0 to float or NaN. + // Thus, this check should catch all cases. + var val = result.value | 0; + if (val != result.value - 0) { + const [err_title, err_text] = await document.l10n.formatValues([ + { id: "config-nan-title" }, + { id: "config-nan-text" }, + ]); + + Services.prompt.alert(window, err_title, err_text); + return false; + } + gPrefBranch.setIntPref(entry.prefCol, val); + } else { + gPrefBranch.setStringPref(entry.prefCol, result.value); + } + } + + Services.prefs.savePrefFile(null); + return true; +} + +function recordTelemetryOnce(categoryLabel) { + if (!gCategoriesRecordedOnce.has(categoryLabel)) { + // Don't raise an exception if Telemetry is not available. + try { + Services.telemetry + .getHistogramById("ABOUT_CONFIG_FEATURES_USAGE") + .add(categoryLabel); + } catch (ex) {} + gCategoriesRecordedOnce.add(categoryLabel); + } +} + +window.onload = onConfigLoad; +window.onunload = onConfigUnload; \ No newline at end of file diff --git a/toolkit/components/viewconfig/content/config.xhtml b/toolkit/components/viewconfig/content/config.xhtml new file mode 100644 index 00000000000000..07a5ba308d9ceb --- /dev/null +++ b/toolkit/components/viewconfig/content/config.xhtml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + +