From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Sun, 7 Apr 2024 21:33:14 +0200
Subject: Adding upstream version 115.7.0esr.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 .../aboutconfig/test/browser/browser.ini           |  15 +
 .../test/browser/browser_accessibility.js          |  39 ++
 .../aboutconfig/test/browser/browser_basic.js      |  52 +++
 .../aboutconfig/test/browser/browser_clipboard.js  | 141 +++++++
 .../aboutconfig/test/browser/browser_edit.js       | 430 +++++++++++++++++++++
 .../aboutconfig/test/browser/browser_locked.js     |  54 +++
 .../aboutconfig/test/browser/browser_observe.js    | 163 ++++++++
 .../aboutconfig/test/browser/browser_search.js     | 177 +++++++++
 .../aboutconfig/test/browser/browser_warning.js    |  41 ++
 .../components/aboutconfig/test/browser/head.js    | 173 +++++++++
 10 files changed, 1285 insertions(+)
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser.ini
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_accessibility.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_basic.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_clipboard.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_edit.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_locked.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_observe.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_search.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/browser_warning.js
 create mode 100644 toolkit/components/aboutconfig/test/browser/head.js

(limited to 'toolkit/components/aboutconfig/test')

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);
+  }
+}
-- 
cgit v1.2.3