summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutconfig
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/aboutconfig')
-rw-r--r--toolkit/components/aboutconfig/content/aboutconfig.css290
-rw-r--r--toolkit/components/aboutconfig/content/aboutconfig.html108
-rw-r--r--toolkit/components/aboutconfig/content/aboutconfig.js696
-rw-r--r--toolkit/components/aboutconfig/content/background.svg4
-rw-r--r--toolkit/components/aboutconfig/content/toggle.svg7
-rw-r--r--toolkit/components/aboutconfig/jar.mn11
-rw-r--r--toolkit/components/aboutconfig/moz.build9
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser.ini15
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_accessibility.js39
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_basic.js52
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_clipboard.js141
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_edit.js430
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_locked.js54
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_observe.js163
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_search.js177
-rw-r--r--toolkit/components/aboutconfig/test/browser/browser_warning.js41
-rw-r--r--toolkit/components/aboutconfig/test/browser/head.js173
17 files changed, 2410 insertions, 0 deletions
diff --git a/toolkit/components/aboutconfig/content/aboutconfig.css b/toolkit/components/aboutconfig/content/aboutconfig.css
new file mode 100644
index 0000000000..b1ffe12a86
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/aboutconfig.css
@@ -0,0 +1,290 @@
+/* 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/. */
+
+:root {
+ --prefs-table-border-width: 1px;
+ --prefs-table-border: var(--prefs-table-border-width) solid var(--in-content-box-border-color);
+}
+
+.hidden {
+ display: none;
+}
+
+.table-shown > #show-all,
+.table-shown > .config-background-wrapper {
+ display: none;
+}
+
+.config-background {
+ background: url("chrome://global/content/aboutconfig/background.svg") no-repeat;
+ height: 182px;
+ margin: 32px auto;
+ width: 235px;
+}
+
+.config-help-text {
+ text-align: center;
+}
+
+.title {
+ background-image: url("chrome://global/skin/icons/warning.svg");
+ fill: #fcd100;
+}
+
+#toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ box-sizing: border-box;
+ width: 100%;
+ background-color: var(--in-content-page-background);
+ padding: 10px;
+ padding-bottom: 0;
+ min-width: 644px;
+ display: flex;
+}
+
+.checkbox-container {
+ /* Center align and get rid of whitespace. */
+ display: inline-flex;
+ align-items: center;
+ margin-inline-start: 1ch;
+}
+
+#about-config-search {
+ -moz-context-properties: fill, fill-opacity;
+ fill: currentColor;
+ box-sizing: border-box;
+ flex-grow: 1;
+ background-image: url("chrome://global/skin/icons/search-glass.svg");
+ background-repeat: no-repeat;
+ background-position: 8px center;
+ background-size: 16px;
+ /* Set horizontal margin to 0 to ensure alignment with table. */
+ margin-inline: 0;
+ text-align: match-parent;
+ /* All prefs must be left-to-right. */
+ direction: ltr;
+}
+
+@media not (prefers-contrast) {
+ #about-config-search {
+ fill-opacity: 0.4;
+ }
+}
+
+#about-config-search:placeholder-shown {
+ /* Display the placeholder in its natural directionality,
+ * even if the user changes the text direction manually
+ * (e.g. via RightCtrl+Shift). */
+ direction: inherit;
+}
+
+:root:dir(ltr) #about-config-search {
+ /* Be explicit about padding direction since
+ * `about-config-search` is forced to be LTR. */
+ padding-left: 32px;
+}
+
+:root:dir(rtl) #about-config-search {
+ background-position-x: right 8px;
+ padding-right: 32px;
+}
+
+#show-all {
+ display: block;
+ margin: 10px auto;
+}
+
+#prefs {
+ background-color: var(--in-content-box-background);
+ color: var(--in-content-text-color);
+ margin: 10px;
+ table-layout: fixed;
+ width: calc(100% - 20px);
+ min-width: 644px;
+ /* To stay consistent with about:preferences (664px - 20px margin). */
+ border: var(--prefs-table-border);
+ border-radius: 4px;
+ border-spacing: 0;
+}
+
+#prefs > tr.odd {
+ background-color: var(--in-content-box-background-odd);
+}
+
+#prefs > tr:hover {
+ background-color: var(--in-content-item-hover);
+ color: var(--in-content-item-hover-text);
+}
+
+#prefs > tr.has-user-value {
+ font-weight: bold;
+}
+
+#prefs > tr.locked {
+ opacity: 0.4;
+ background-image: url("chrome://global/skin/icons/security.svg");
+ background-repeat: no-repeat;
+ background-position: 9px center;
+ background-size: 16px 16px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+#prefs > tr.locked:dir(rtl) {
+ background-position-x: right 9px;
+}
+
+#prefs > tr > td,
+#prefs > tr > th {
+ padding: 4px;
+ font-weight: inherit;
+}
+
+#prefs > tr > th {
+ direction: ltr;
+ text-align: match-parent;
+}
+
+#prefs > tr:dir(ltr) > th {
+ /* Be explicit about padding direction since `th` is forced to be LTR. */
+ padding-left: 30px;
+}
+
+#prefs > tr:dir(rtl) > th {
+ padding-right: 30px;
+}
+
+#prefs > tr.deleted > th > span {
+ font-weight: bold;
+ color: var(--text-color-deemphasized);
+}
+
+#prefs > tr > td.cell-edit,
+#prefs > tr > td.cell-reset {
+ width: 40px;
+ padding: 0;
+}
+
+.cell-value {
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+tr:not(.deleted) > .cell-value {
+ /* Always display the text in the value cell using left-to-right rules, but
+ align it according to the page direction. This doesn't apply to the radio
+ buttons shown for deleted preferences. */
+ direction: ltr;
+ text-align: match-parent;
+}
+
+:root:dir(ltr) tr:not(.deleted) > .cell-value > #form-edit {
+ /* Make the text in the form stay in the same place as before editing the pref. */
+ margin-left: -8px;
+}
+
+:root:dir(rtl) tr:not(.deleted) > .cell-value > #form-edit {
+ margin-right: -8px;
+}
+
+#form-edit > label {
+ /* Make the radiobutton's text wrap to a new line along with
+ the radiobutton itself, when space is constrained. */
+ display: inline-block;
+ margin-inline-end: 30px;
+}
+
+#form-edit > label:last-of-type {
+ margin-inline-end: 0;
+}
+
+#form-edit > label > :is(input[type="radio"], span) {
+ vertical-align: middle;
+}
+
+td.cell-value > form > input[type="text"],
+td.cell-value > form > input[type="number"] {
+ appearance: textfield;
+ margin: 0;
+ width: 100%;
+ box-sizing: border-box;
+ /* Align the text inside the input field in the same way as the table cell,
+ for both the left-to-right and right-to-left directions. */
+ text-align: match-parent;
+}
+
+.button-add,
+.button-save,
+.button-edit,
+.button-toggle,
+.button-delete,
+.button-reset {
+ -moz-context-properties: fill;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 16px;
+ fill: currentColor;
+ min-width: auto;
+ width: 32px;
+}
+
+.button-add {
+ background-image: url("chrome://global/skin/icons/plus.svg");
+}
+
+.button-save {
+ background-image: url("chrome://global/skin/icons/check.svg");
+}
+
+.button-edit {
+ background-image: url("chrome://global/skin/icons/edit.svg");
+}
+
+.button-toggle {
+ background-image: url("chrome://global/content/aboutconfig/toggle.svg");
+}
+
+.button-delete {
+ background-image: url("chrome://global/skin/icons/delete.svg");
+}
+
+.button-reset {
+ background-image: url("chrome://global/skin/icons/undo.svg");
+}
+
+.button-reset:dir(rtl) {
+ transform: scaleX(-1);
+}
+
+/* The ::before creates a blank space between the last visible pref and the add row. */
+#prefs[has-visible-prefs] > .add > th::before,
+#prefs[has-visible-prefs] > .add > td::before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ /* Make it wider by the border width so the border-inline is hidden. */
+ inset-inline: calc(var(--prefs-table-border-width) * -1);
+ height: 12px;
+ background-color: var(--in-content-page-background);
+ /* This is visually the top border on the add row. */
+ border-bottom: var(--prefs-table-border);
+}
+
+#prefs[has-visible-prefs] > .add > th,
+#prefs[has-visible-prefs] > .add > td {
+ /* This is the border underneath the last existing pref row. */
+ border-top: var(--prefs-table-border);
+ padding-top: 14px;
+ position: relative;
+}
+
+@media (prefers-contrast) {
+ #prefs > tr.deleted:hover > th > span {
+ color: inherit;
+ }
+}
diff --git a/toolkit/components/aboutconfig/content/aboutconfig.html b/toolkit/components/aboutconfig/content/aboutconfig.html
new file mode 100644
index 0000000000..8962cdd86d
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/aboutconfig.html
@@ -0,0 +1,108 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:; object-src 'none'"
+ />
+ <meta charset="utf-8" />
+ <meta name="color-scheme" content="light dark" />
+ <link
+ rel="stylesheet"
+ media="screen, projection"
+ type="text/css"
+ href="chrome://global/skin/in-content/common.css"
+ />
+ <link
+ rel="stylesheet"
+ media="screen, projection"
+ type="text/css"
+ href="chrome://global/skin/in-content/info-pages.css"
+ title="infop"
+ />
+ <link
+ rel="stylesheet"
+ type="text/css"
+ href="chrome://global/content/aboutconfig/aboutconfig.css"
+ />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/about/config.ftl" />
+ <link rel="icon" href="chrome://global/skin/icons/settings.svg" />
+ <script src="chrome://global/content/aboutconfig/aboutconfig.js"></script>
+ <title data-l10n-id="about-config-page-title"></title>
+ </head>
+ <body>
+ <div
+ class="container"
+ role="alertdialog"
+ aria-labelledby="warningTitle"
+ aria-describedby="warningDescription"
+ >
+ <div class="title">
+ <h1
+ id="warningTitle"
+ class="title-text"
+ data-l10n-id="about-config-intro-warning-title"
+ ></h1>
+ </div>
+
+ <div class="description">
+ <p
+ id="warningDescription"
+ data-l10n-id="about-config-intro-warning-text"
+ ></p>
+ </div>
+
+ <div class="toggle-container-with-text">
+ <input type="checkbox" id="showWarningNextTime" checked />
+ <label
+ for="showWarningNextTime"
+ data-l10n-id="about-config-intro-warning-checkbox"
+ ></label>
+ </div>
+
+ <div class="button-container">
+ <button
+ id="warningButton"
+ class="primary"
+ data-l10n-id="about-config-intro-warning-button"
+ ></button>
+ </div>
+ </div>
+
+ <template id="main">
+ <div id="toolbar">
+ <!-- Use a unique ID to prevent showing autocomplete results from other
+ browser pages with similarly named fields. -->
+ <input
+ type="text"
+ id="about-config-search"
+ data-l10n-id="about-config-search-input1"
+ />
+ <label class="checkbox-container">
+ <input type="checkbox" id="about-config-show-only-modified" />
+ <span data-l10n-id="about-config-show-only-modified"></span>
+ </label>
+ </div>
+
+ <table id="prefs"></table>
+
+ <div class="config-background-wrapper">
+ <button
+ id="show-all"
+ class="ghost-button"
+ data-l10n-id="about-config-show-all"
+ ></button>
+ <div class="config-background"></div>
+ <p
+ class="config-help-text"
+ data-l10n-id="about-config-caution-text"
+ ></p>
+ </div>
+ </template>
+ </body>
+</html>
diff --git a/toolkit/components/aboutconfig/content/aboutconfig.js b/toolkit/components/aboutconfig/content/aboutconfig.js
new file mode 100644
index 0000000000..e11021ed64
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/aboutconfig.js
@@ -0,0 +1,696 @@
+/* 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 <wbr> 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;
+ 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", event => {
+ 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
new file mode 100644
index 0000000000..d03d081ee5
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/background.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="a"><stop offset="0" stop-color="#ccfbff"/><stop offset="1" stop-color="#c9e4ff"/></linearGradient><linearGradient id="b" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="6.5576" x2="120.5576" href="#a" y1="318.3719" y2="152.8721"/><linearGradient id="c" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="37.205" x2="151.205" href="#a" y1="339.4831" y2="173.9827"/><linearGradient id="d"><stop offset="0" stop-color="#00c8d7"/><stop offset="1" stop-color="#0a84ff"/></linearGradient><linearGradient id="e" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="-51.3427" x2="242.3216" href="#d" y1="411.209" y2="56.9062"/><linearGradient id="f" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="98.7014" x2="116.0054" href="#d" y1="238.5525" y2="212.9205"/><linearGradient id="g" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="70.9844" x2="164.4848" href="#d" y1="291.4737" y2="152.9739"/><linearGradient id="h" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="53.6667" x2="192.6669" href="#a" y1="337.889" y2="159.389"/><linearGradient id="i" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="16.7153" x2="201.7151" href="#d" y1="374.8648" y2="156.3646"/><g fill="#eaeaee"><path d="m174.2 162.1c16.4-3.1 26.8-7.5 26.8-12.5 0-9.3-35.7-16.8-79.8-16.8s-79.8 7.5-79.8 16.8c0 7.8 25.2 14.3 59.3 16.2-1.5 1.2-2.3 2.5-2.3 3.8 0 6.5 19.1 11.7 42.8 11.7s42.8-5.3 42.8-11.7c-.1-2.8-3.7-5.4-9.8-7.5z"/><ellipse cx="203.2" cy="165.4" rx="12.3" ry="4"/><ellipse cx="78.9" cy="171.1" rx="9.5" ry="3.3"/><ellipse cx="207.9" cy="156.9" rx="4.8" ry="2"/><path d="m53.6 93.2h123.4c.6 0 1.1-.5 1.1-1.1s-.5-1.1-1.1-1.1h-123.4c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1zm16.7-5.6h30.9c.3 0 .6-.3.6-.6s-.3-.6-.6-.6h-30.9c-.3 0-.6.3-.6.6s.3.6.6.6zm-38.9 10.5h13.4c.3 0 .6-.3.6-.6s-.3-.6-.6-.6h-13.4c-.3 0-.6.3-.6.6s.3.6.6.6zm22.3-1.1c-.3 0-.6.3-.6.6s.3.6.6.6h3.3c.3 0 .6-.3.6-.6s-.3-.6-.6-.6zm123.1.5c0 .3.3.6.6.6h3.3c.3 0 .6-.3.6-.6s-.3-.5-.6-.5h-3.3c-.3 0-.6.2-.6.5zm8.4.6h1.1c.3 0 .6-.3.6-.6s-.3-.6-.6-.6h-1.1c-.3 0-.6.3-.6.6s.3.6.6.6zm-122.6-1.1h-1.1c-.3 0-.6.3-.6.6s.3.6.6.6h1.1c.3 0 .6-.3.6-.6 0-.4-.3-.6-.6-.6zm133.7 1.1h13.4c.3 0 .6-.3.6-.6s-.3-.5-.6-.5h-13.4c-.3 0-.6.3-.6.6s.3.5.6.5zm-27.3-.6c0-.3-.3-.6-.6-.6h-13.4c-.3 0-.6.3-.6.6s.3.6.6.6h13.4c.4 0 .6-.3.6-.6zm52.5 29.2h-56.7c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1h56.7c.6 0 1.1-.5 1.1-1.1s-.5-1.1-1.1-1.1zm-162 0h-50.6c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1h50.7c.6 0 1.1-.5 1.1-1.1s-.6-1.1-1.2-1.1z"/></g><path d="m163.4 155.3-12.3-103.2c-.3-2.1-2-3.7-4.2-3.7h-62c-2.4 0-4.1 1.8-4.6 4l-19.3 92.3c-.3 1.2.1 2.5.9 3.5s2 1.5 3.3 1.5h27.1l.8 6.5c.3 2.1 2 3.7 4.2 3.7h62c1.2 0 2.3-.5 3.1-1.4s1.1-2 1-3.2z" fill="#fff"/><path d="m94.7 144.1-10-83.1-17.3 83.1z" fill="url(#b)"/><path d="m86.9 54.2 12 100.2h59l-11.9-100.2z" fill="url(#c)"/><path d="m161.1 155.7-12.3-103.2c-.1-.8-.7-1.3-1.5-1.3h-62c-.9 0-1.6.5-1.8 1.3l-19.4 92.9c-.1.4 0 .9.3 1.3.3.3.7.6 1.2.6h29.4l1.1 8.9c.1.8.7 1.3 1.5 1.3h62c.4 0 .8-.2 1.1-.5.3-.5.5-.9.4-1.3zm-79.3-80.4 8 68.9h-22.4c0-.1 14.4-68.9 14.4-68.9zm17.1 79.1-12-100.2h59l12 100.2z" fill="url(#e)"/><path d="m91 57.7 11.3 92.5h51.9l-11.3-92.5z" fill="#f9f9fa"/><path d="m108.3 91.1c-2.2-.1-4 1.6-4.2 3.8-.1 2.3 1.5 4.2 3.6 4.4.8.1 1.5-.1 2.1-.5.4-1.5 1-3.6 1.6-5.6-.6-1.2-1.8-2-3.1-2.1z" fill="url(#f)"/><path d="m143 105.5-11.9-5.2c-.5-.2-1.2-.2-1.6.2l-5.8 4.8-.1-.1-6.8-6.2 10.4-10.3c.6-.6.7-1.7.1-2.4s-1.6-.7-2.3-.1l-10.1 10c1.2-4.1 3-10.4 4.3-14.8.3-.9-.2-1.8-1.1-2.1-.8-.3-1.7.2-2 1.1 0 0-.7 2.5-1.7 5.6-.4 1.2-.7 2.5-1.1 3.9-2.9 9.9-2.8 10.1-2.8 10.6 0 .3.2.6.3.8l.1.1c.2.3.4.6.6.8l8.2 7.5.2.2.1.1 5.2 4.8-5.5 9.9-1.4 2.6c-.4.8-.2 1.8.6 2.3.3.2.6.3.9.2.5 0 1-.3 1.3-.8l7.6-13.7c.4-.7.3-1.6-.3-2.1l-4.3-4.1.1-.1 1.7-1.4 4.7-3.9 7.8 3.4 3.2 1.4c.2.1.5.1.7.1.6 0 1.1-.4 1.4-1 .5-.8.2-1.8-.7-2.1z" fill="url(#g)"/><path d="m139.2 30h-27.2c-2.4-4.4-8.4-14-15.9-15.4-10-1.9-12 8-12 8s-6.6-17.2-23.4-15c-18.5 2.5-10.2 21.7-10 22.4h-28.3c-.7 0-1.3.6-1.3 1.3s.6 1.3 1.3 1.3h116.8c.7 0 1.3-.6 1.3-1.3s-.6-1.3-1.3-1.3z" fill="#fff"/><path d="m138.4 27.6h-7.3c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h7.3c.4 0 .7.3.7.7s-.4.7-.7.7zm-19.1 0h-1.3c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h1.3c.4 0 .7.3.7.7-.1.4-.4.7-.7.7zm-68.1-.8h-2.1c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h1.1c-.1-.2-.2-.5-.3-.8s.1-.7.4-.8.7.1.8.4c.3 1 .6 1.6.6 1.6.1.2.1.4 0 .6 0 .3-.2.4-.5.4zm-12.6 0h-15.7c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h15.7c.4 0 .7.3.7.7s-.3.7-.7.7zm74.6 0c-.2 0-.5-.1-.6-.3-.4-.8-1.1-1.9-2-3.3-.2-.3-.1-.7.2-.9s.7-.1.9.2c.9 1.4 1.7 2.6 2.1 3.4.2.3.1.7-.3.9-.1-.1-.2 0-.3 0zm-63.8-6.8c-.3 0-.6-.2-.6-.6-.1-.4-.1-.9-.1-1.3s.2-.7.6-.7.7.2.7.6.1.9.1 1.3c0 .3-.3.7-.7.7zm34.1-2.9c-.2 0-.5-.1-.6-.3-.2-.3-.4-.7-.6-1.1-.2-.3-.1-.7.2-.9s.7-.1.9.2c.2.4.5.8.7 1.2.2.3.1.7-.3.9-.1 0-.2 0-.3 0zm20.8-1.6c-.2 0-.3-.1-.4-.2-2.5-2.1-5-3.5-7.4-3.9-2.4-.5-4.5-.2-6.3.7-.3.2-.7 0-.9-.3s0-.7.3-.9c2-1 4.4-1.3 7.1-.8s5.4 1.9 8.1 4.2c.3.2.3.6.1.9-.2.2-.4.3-.6.3zm-24.5-3.7c-.2 0-.4-.1-.5-.2-.9-1-1.8-1.9-2.7-2.7-.3-.2-.3-.6-.1-.9s.6-.3.9-.1c1 .8 2 1.8 2.9 2.8.2.3.2.7-.1.9-.1.1-.3.2-.4.2zm-27-3.9c-.2 0-.4-.1-.5-.2-.3-.3-.3-.7 0-.9 2.1-1.9 5.1-3.1 8.8-3.6 2.3-.3 4.5-.3 6.6.1.4.1.6.4.5.8s-.4.6-.8.5c-2-.3-4.1-.4-6.2-.1-3.5.5-6.2 1.6-8.1 3.3 0 .1-.2.1-.3.1z" fill="#eaeaee"/><path d="m233.4 63.7h-15.4c-1.3-2.5-4.7-7.8-8.9-8.6-5.6-1.1-6.7 4.5-6.7 4.5s-3.7-9.6-13.1-8.4c-10.5 1.4-5.6 12.5-5.6 12.5h-15.8.5-.2c-.7 0-1.3.6-1.3 1.3s.6 1.3 1.3 1.3h65.2c.7 0 1.3-.6 1.3-1.3s-.6-1.3-1.3-1.3z" fill="#fff"/><path d="m184.2 61.9h-15.8c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h15.8c.4 0 .7.3.7.7s-.3.7-.7.7zm48.6-.2h-.6c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h.6c.4 0 .7.3.7.7s-.3.7-.7.7zm-5.8 0h-4c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h3.9c.4 0 .7.3.7.7s-.3.7-.6.7zm-24.2-3.9c-.3 0-.5-.2-.6-.4s-.1-.4.2-1.1c.6-1.8 2.3-4.5 5.8-4.5.5 0 1 0 1.5.1 2 .4 3.9 1.5 5.9 3.4.3.3.3.7 0 .9-.3.3-.7.3-.9 0-1.8-1.7-3.5-2.7-5.2-3-.4-.1-.8-.1-1.2-.1-3.8 0-4.6 3.9-4.7 4.1-.2.4-.5.6-.8.6zm-18.3-5.9c-.1 0-.3-.1-.4-.2-.3-.2-.3-.6-.1-.9.9-1 2.1-1.8 3.6-2.3.3-.1.7.1.8.4s-.1.7-.4.8c-1.3.4-2.3 1-3 1.9-.1.2-.3.3-.5.3zm9.8-2.2c-.1 0-.1 0-.2 0-.4-.1-.8-.2-1.2-.3s-.6-.4-.6-.7.4-.6.7-.6c.5.1.9.2 1.4.3.3.1.6.5.5.8 0 .3-.3.5-.6.5z" fill="#eaeaee"/><path d="m128.8 68.2h-21c-1.7 0-3-1.3-3-3 0-1.6 1.3-3 3-3h21c1.6 0 3 1.3 3 3 0 1.6-1.3 3-3 3z" fill="url(#h)"/><path d="m128.8 68.8h-21c-2 0-3.6-1.6-3.6-3.6s1.6-3.6 3.6-3.6h21c2 0 3.6 1.6 3.6 3.6 0 1.9-1.6 3.6-3.6 3.6zm-21-6c-1.3 0-2.4 1.1-2.4 2.4s1.1 2.4 2.4 2.4h21c1.3 0 2.4-1.1 2.4-2.4s-1.1-2.4-2.4-2.4z" fill="url(#i)"/></svg>
diff --git a/toolkit/components/aboutconfig/content/toggle.svg b/toolkit/components/aboutconfig/content/toggle.svg
new file mode 100644
index 0000000000..73f45b079e
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/toggle.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill">
+ <path d="M15 9H.5c-.39.39-.39 1.61 0 2l4.793 4.707a1 1 0 0 0 1.414-1.414L3.414 11H15a1 1 0 0 0 0-2zM1 6.988h14.5c.39-.39.39-1.61 0-2L10.707.28a1 1 0 0 0-1.414 1.414l3.293 3.293H1a1 1 0 0 0 0 2z"/>
+</svg>
+
diff --git a/toolkit/components/aboutconfig/jar.mn b/toolkit/components/aboutconfig/jar.mn
new file mode 100644
index 0000000000..400db0e1dc
--- /dev/null
+++ b/toolkit/components/aboutconfig/jar.mn
@@ -0,0 +1,11 @@
+#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/moz.build b/toolkit/components/aboutconfig/moz.build
new file mode 100644
index 0000000000..47182555bb
--- /dev/null
+++ b/toolkit/components/aboutconfig/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/toolkit/components/aboutconfig/test/browser/browser.ini b/toolkit/components/aboutconfig/test/browser/browser.ini
new file mode 100644
index 0000000000..a26dc43e58
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+skip-if = debug || asan || tsan # Bug 1507747 and bug 1520398
+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
new file mode 100644
index 0000000000..9310e40186
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_accessibility.js
@@ -0,0 +1,39 @@
+/* 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
new file mode 100644
index 0000000000..014a98df97
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_basic.js
@@ -0,0 +1,52 @@
+/* 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
new file mode 100644
index 0000000000..bcaa2c0328
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_clipboard.js
@@ -0,0 +1,141 @@
+/* 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();
+ 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
+ );
+ };
+
+ // 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.
+ 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
+ );
+
+ 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
new file mode 100644
index 0000000000..9d10fb1e75
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_edit.js
@@ -0,0 +1,430 @@
+/* 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);
+
+ 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 => {
+ // Trigger two mouse events to simulate the first then second click.
+ click(target, { clickCount: 1 });
+ click(target, { clickCount: 2 });
+ };
+ let tripleClick = target => {
+ // 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 });
+ };
+
+ // 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
new file mode 100644
index 0000000000..6b06f22218
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_locked.js
@@ -0,0 +1,54 @@
+/* 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
new file mode 100644
index 0000000000..1f1ab5d217
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_observe.js
@@ -0,0 +1,163 @@
+/* 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
new file mode 100644
index 0000000000..89a0c0c866
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_search.js
@@ -0,0 +1,177 @@
+/* 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
new file mode 100644
index 0000000000..d95e8f49ea
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/browser_warning.js
@@ -0,0 +1,41 @@
+/* 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
new file mode 100644
index 0000000000..511fc07a37
--- /dev/null
+++ b/toolkit/components/aboutconfig/test/browser/head.js
@@ -0,0 +1,173 @@
+/* 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);
+ }
+}