From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../aboutconfig/test/browser/browser.toml | 27 ++ .../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, 1297 insertions(+) create mode 100644 toolkit/components/aboutconfig/test/browser/browser.toml 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/browser') diff --git a/toolkit/components/aboutconfig/test/browser/browser.toml b/toolkit/components/aboutconfig/test/browser/browser.toml new file mode 100644 index 0000000000..acccf1f8f0 --- /dev/null +++ b/toolkit/components/aboutconfig/test/browser/browser.toml @@ -0,0 +1,27 @@ +[DEFAULT] +skip-if = [ + "debug", # Bug 1507747 + "asan", # Bug 1520398 + "tsan" +] +support-files = ["head.js"] + +["browser_accessibility.js"] + +["browser_basic.js"] + +["browser_clipboard.js"] +fail-if = ["a11y_checks"] # Bug 1854447 th, td.cell-value, #prefs may not be focusable; #show-all may be unlabeled + +["browser_edit.js"] +fail-if = ["a11y_checks"] # Bug 1854447 span, th, td.cell-value may not be focusable; #show-all, input, button-add/delete/reset ghost-buttons may not be labeled +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