gecko-dev/toolkit/components/aboutconfig/test/browser/browser_edit.js
Anna Yeddi 55597523f4 Bug 1882378 - Add an accessible name for Edit pref value inputs on about:config page. r=settings-reviewers,Gijs
When a value is being edited on the about:config page, the second column cell gets a form included with an `<input>` to edit the value of the pref, but this form field is not labeled and users of assistive technology would not know what is the purpose of this form and this input field. This would especially affect screen reader users with cognitive difficulties, memory loss, and neurodivergent users.

Example of the input would include: [this edit input](https://searchfox.org/mozilla-central/rev/9cd4ea81e27db6b767f1d9bbbcf47da238dd64fa/toolkit/components/aboutconfig/content/aboutconfig.js#256-265) and an example of failed a11y_checks tests that would need to have `fail-if` notation to be edited or removed from their test manifests, when this bug is fixed, would be: `toolkit/components/aboutconfig/test/browser/browser_clipboard.js`.

Note that only text and number inputs are being added and are requiring the label, the boolean value is changed with `Toggle` button.

Differential Revision: https://phabricator.services.mozilla.com/D202895
2024-02-28 17:51:50 +00:00

451 lines
16 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const PREF_MODIFY_PREFIX = "test.aboutconfig.modify";
const PREF_MODIFY_BOOLEAN = "test.aboutconfig.modify.boolean";
const PREF_MODIFY_NUMBER = "test.aboutconfig.modify.number";
const PREF_MODIFY_STRING = "test.aboutconfig.modify.string";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
[PREF_MODIFY_BOOLEAN, true],
[PREF_MODIFY_NUMBER, 1337],
[
PREF_MODIFY_STRING,
"the answer to the life the universe and everything",
],
],
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref(PREF_BOOLEAN_DEFAULT_TRUE);
Services.prefs.clearUserPref(PREF_NUMBER_DEFAULT_ZERO);
Services.prefs.clearUserPref(PREF_STRING_DEFAULT_EMPTY);
});
});
add_task(async function test_add_user_pref() {
Assert.equal(
Services.prefs.getPrefType(PREF_NEW),
Ci.nsIPrefBranch.PREF_INVALID
);
await AboutConfigTest.withNewTab(async function () {
// The row for a new preference appears when searching for its name.
Assert.ok(!this.getRow(PREF_NEW));
for (let [radioIndex, expectedValue, expectedEditingMode] of [
[0, true, false],
[1, 0, true],
[2, "", true],
]) {
this.search(PREF_NEW);
let row = this.getRow(PREF_NEW);
Assert.ok(row.hasClass("deleted"));
Assert.ok(row.hasClass("add"));
// Adding the preference should set the default for the data type.
row.element.querySelectorAll("input")[radioIndex].click();
row.editColumnButton.click();
Assert.ok(!row.hasClass("deleted"));
Assert.ok(!row.hasClass("add"));
Assert.ok(Preferences.get(PREF_NEW) === expectedValue);
// Number and String preferences should be in edit mode.
Assert.equal(!!row.valueInput, expectedEditingMode);
// Repeat the search to verify that the preference remains.
this.search(PREF_NEW);
row = this.getRow(PREF_NEW);
Assert.ok(!row.hasClass("deleted"));
Assert.ok(!row.hasClass("add"));
Assert.ok(!row.valueInput);
// Reset the preference, then continue by adding a different type.
row.resetColumnButton.click();
Assert.equal(
Services.prefs.getPrefType(PREF_NEW),
Ci.nsIPrefBranch.PREF_INVALID
);
}
});
});
add_task(async function test_delete_user_pref() {
for (let [radioIndex, testValue] of [
[0, false],
[1, -1],
[2, "value"],
]) {
Preferences.set(PREF_NEW, testValue);
await AboutConfigTest.withNewTab(async function () {
// Deleting the preference should keep the row.
let row = this.getRow(PREF_NEW);
row.resetColumnButton.click();
Assert.ok(row.hasClass("deleted"));
Assert.equal(
Services.prefs.getPrefType(PREF_NEW),
Ci.nsIPrefBranch.PREF_INVALID
);
// Re-adding the preference should keep the same value.
Assert.ok(row.element.querySelectorAll("input")[radioIndex].checked);
row.editColumnButton.click();
Assert.ok(!row.hasClass("deleted"));
Assert.ok(Preferences.get(PREF_NEW) === testValue);
// Filtering again after deleting should remove the row.
row.resetColumnButton.click();
this.showAll();
Assert.ok(!this.getRow(PREF_NEW));
});
}
});
add_task(async function test_click_type_label_multiple_forms() {
// This test displays the row to add a preference while other preferences are
// also displayed, and tries to select the type of the new preference by
// clicking the label next to the radio button. This should work even if the
// user has deleted a different preference, and multiple forms are displayed.
const PREF_TO_DELETE = "test.aboutconfig.modify.boolean";
const PREF_NEW_WHILE_DELETED = "test.aboutconfig.modify.";
await AboutConfigTest.withNewTab(async function () {
this.search(PREF_NEW_WHILE_DELETED);
// This preference will remain deleted during the test.
let existingRow = this.getRow(PREF_TO_DELETE);
existingRow.resetColumnButton.click();
let newRow = this.getRow(PREF_NEW_WHILE_DELETED);
for (let [radioIndex, expectedValue] of [
[0, true],
[1, 0],
[2, ""],
]) {
let radioLabels = newRow.element.querySelectorAll("label > span");
await this.document.l10n.translateElements(radioLabels);
// Even if this is the second form on the page, the click should select
// the radio button next to the label, not the one on the first form.
EventUtils.synthesizeMouseAtCenter(
radioLabels[radioIndex],
{},
this.browser.contentWindow
);
// Adding the preference should set the default for the data type.
newRow.editColumnButton.click();
Assert.ok(Preferences.get(PREF_NEW_WHILE_DELETED) === expectedValue);
// Reset the preference, then continue by adding a different type.
newRow.resetColumnButton.click();
}
// Re-adding the deleted preference should restore the value.
existingRow.editColumnButton.click();
Assert.ok(Preferences.get(PREF_TO_DELETE) === true);
});
});
add_task(async function test_reset_user_pref() {
await SpecialPowers.pushPrefEnv({
set: [
[PREF_BOOLEAN_DEFAULT_TRUE, false],
[PREF_STRING_LOCALIZED_MISSING, "user-value"],
],
});
await AboutConfigTest.withNewTab(async function () {
// Click reset.
let row = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE);
row.resetColumnButton.click();
// Check new layout and reset.
Assert.ok(!row.hasClass("has-user-value"));
Assert.ok(!row.resetColumnButton);
Assert.ok(!Services.prefs.prefHasUserValue(PREF_BOOLEAN_DEFAULT_TRUE));
Assert.equal(this.getRow(PREF_BOOLEAN_DEFAULT_TRUE).value, "true");
// Filter again to test the preference cache.
this.showAll();
row = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE);
Assert.ok(!row.hasClass("has-user-value"));
Assert.ok(!row.resetColumnButton);
Assert.equal(this.getRow(PREF_BOOLEAN_DEFAULT_TRUE).value, "true");
// Clicking reset on a localized preference without a corresponding value.
row = this.getRow(PREF_STRING_LOCALIZED_MISSING);
Assert.equal(row.value, "user-value");
row.resetColumnButton.click();
// Check new layout and reset.
Assert.ok(!row.hasClass("has-user-value"));
Assert.ok(!row.resetColumnButton);
Assert.ok(!Services.prefs.prefHasUserValue(PREF_STRING_LOCALIZED_MISSING));
Assert.equal(this.getRow(PREF_STRING_LOCALIZED_MISSING).value, "");
});
});
add_task(async function test_modify() {
await AboutConfigTest.withNewTab(async function () {
// Test toggle for boolean prefs.
for (let nameOfBoolPref of [
PREF_MODIFY_BOOLEAN,
PREF_BOOLEAN_DEFAULT_TRUE,
]) {
let row = this.getRow(nameOfBoolPref);
// Do this a two times to reset the pref.
for (let i = 0; i < 2; i++) {
row.editColumnButton.click();
// Check new layout and saving in backend.
Assert.equal(
this.getRow(nameOfBoolPref).value,
"" + Preferences.get(nameOfBoolPref)
);
let prefHasUserValue = Services.prefs.prefHasUserValue(nameOfBoolPref);
Assert.equal(row.hasClass("has-user-value"), prefHasUserValue);
Assert.equal(!!row.resetColumnButton, prefHasUserValue);
}
}
// Test abort of edit by starting with string and continuing with editing Int pref.
let row = this.getRow(PREF_MODIFY_STRING);
row.editColumnButton.click();
row.valueInput.value = "test";
let intRow = this.getRow(PREF_MODIFY_NUMBER);
intRow.editColumnButton.click();
Assert.equal(intRow.valueInput.value, Preferences.get(PREF_MODIFY_NUMBER));
Assert.ok(!row.valueInput);
Assert.equal(row.value, Preferences.get(PREF_MODIFY_STRING));
// Test validation of integer values.
for (let invalidValue of [
"",
" ",
"a",
"1.5",
"-2147483649",
"2147483648",
]) {
intRow.valueInput.value = invalidValue;
intRow.editColumnButton.click();
// We should still be in edit mode.
Assert.ok(intRow.valueInput);
}
// Test correct saving and DOM-update.
for (let [prefName, willDelete] of [
[PREF_MODIFY_STRING, true],
[PREF_MODIFY_NUMBER, true],
[PREF_NUMBER_DEFAULT_ZERO, false],
[PREF_STRING_DEFAULT_EMPTY, false],
]) {
row = this.getRow(prefName);
// Activate edit and check displaying.
row.editColumnButton.click();
Assert.equal(row.valueInput.value, Preferences.get(prefName));
row.valueInput.value = "42";
// Save and check saving.
row.editColumnButton.click();
Assert.equal(Preferences.get(prefName), "42");
Assert.equal(row.value, "42");
Assert.ok(row.hasClass("has-user-value"));
// Reset or delete the preference while editing.
row.editColumnButton.click();
Assert.equal(row.valueInput.value, Preferences.get(prefName));
row.resetColumnButton.click();
Assert.ok(!row.hasClass("has-user-value"));
Assert.equal(row.hasClass("deleted"), willDelete);
}
});
// This test would have opened the invalid form popup, so just close it so as not to
// affect later tests.
let invalidFormPopup = window.document.getElementById("invalid-form-popup");
invalidFormPopup.hidePopup();
await BrowserTestUtils.waitForCondition(() => {
return invalidFormPopup.state == "closed";
}, "form validation popup closed");
});
add_task(async function test_edit_field_selected() {
let prefsToCheck = [
[PREF_MODIFY_STRING, "A string", "A new string"],
[PREF_MODIFY_NUMBER, "100", "500"],
];
await AboutConfigTest.withNewTab(async function () {
for (let [prefName, startValue, endValue] of prefsToCheck) {
Preferences.set(prefName, startValue);
let row = this.getRow(prefName);
Assert.equal(row.value, startValue);
row.editColumnButton.click();
Assert.equal(row.valueInput.value, startValue);
Assert.equal(
row.valueInput.getAttribute("aria-label"),
prefName,
"The input field is labeled from the pref name"
);
EventUtils.sendString(endValue, this.window);
row.editColumnButton.click();
Assert.equal(row.value, endValue);
Assert.equal(Preferences.get(prefName), endValue);
}
});
});
add_task(async function test_escape_cancels_edit() {
await AboutConfigTest.withNewTab(async function () {
let row = this.getRow(PREF_MODIFY_STRING);
Preferences.set(PREF_MODIFY_STRING, "Edit me, maybe");
for (let blurInput of [false, true]) {
Assert.ok(!row.valueInput);
row.editColumnButton.click();
Assert.ok(row.valueInput);
Assert.equal(row.valueInput.value, "Edit me, maybe");
row.valueInput.value = "Edited";
// Test both cases of the input being focused and not being focused.
if (blurInput) {
row.valueInput.blur();
Assert.notEqual(this.document.activeElement, row.valueInput);
} else {
Assert.equal(this.document.activeElement, row.valueInput);
}
EventUtils.synthesizeKey("KEY_Escape", {}, this.window);
Assert.ok(!row.valueInput);
Assert.equal(row.value, "Edit me, maybe");
Assert.equal(row.value, Preferences.get(PREF_MODIFY_STRING));
}
});
});
add_task(async function test_double_click_modify() {
Preferences.set(PREF_MODIFY_BOOLEAN, true);
Preferences.set(PREF_MODIFY_NUMBER, 10);
Preferences.set(PREF_MODIFY_STRING, "Hello!");
await AboutConfigTest.withNewTab(async function () {
this.search(PREF_MODIFY_PREFIX);
let click = (target, opts) =>
EventUtils.synthesizeMouseAtCenter(target, opts, this.window);
let doubleClick = target => {
// We intentionally turn off this a11y check, because the following series
// of clicks (in these test cases) is either performing an activation of
// the edit mode for prefs or selecting a text in focused inputs. The
// edit mode can be activated with a separate "Edit" or "Toggle" button
// provided for each pref, and the text selection can be performed with
// caret browsing (when supported). Thus, this rule check can be ignored
// by a11y_checks suite.
AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
// Trigger two mouse events to simulate the first then second click.
click(target, { clickCount: 1 });
click(target, { clickCount: 2 });
AccessibilityUtils.resetEnv();
};
let tripleClick = target => {
// We intentionally turn off this a11y check, because the following series
// of clicks is purposefully targeting a non - interactive text content.
// This action does not require the element to have an interactive
// accessible to be done by assistive technology with caret browsing
// (when supported), this rule check shall be ignored by a11y_checks suite.
AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
// Trigger all 3 mouse events to simulate the three mouse events we'd see.
click(target, { clickCount: 1 });
click(target, { clickCount: 2 });
click(target, { clickCount: 3 });
AccessibilityUtils.resetEnv();
};
// Check double-click to edit a boolean.
let boolRow = this.getRow(PREF_MODIFY_BOOLEAN);
Assert.equal(boolRow.value, "true");
doubleClick(boolRow.valueCell);
Assert.equal(boolRow.value, "false");
doubleClick(boolRow.nameCell);
Assert.equal(boolRow.value, "true");
// Check double-click to edit a number.
let intRow = this.getRow(PREF_MODIFY_NUMBER);
Assert.equal(intRow.value, 10);
doubleClick(intRow.valueCell);
Assert.equal(this.document.activeElement, intRow.valueInput);
EventUtils.sendString("75");
EventUtils.synthesizeKey("KEY_Enter");
Assert.equal(intRow.value, 75);
// Check double-click focuses input when already editing.
Assert.equal(intRow.value, 75);
doubleClick(intRow.nameCell);
Assert.equal(this.document.activeElement, intRow.valueInput);
intRow.valueInput.blur();
Assert.notEqual(this.document.activeElement, intRow.valueInput);
doubleClick(intRow.nameCell);
Assert.equal(this.document.activeElement, intRow.valueInput);
EventUtils.sendString("20");
EventUtils.synthesizeKey("KEY_Enter");
Assert.equal(intRow.value, 20);
// Check double-click to edit a string.
let stringRow = this.getRow(PREF_MODIFY_STRING);
Assert.equal(stringRow.value, "Hello!");
doubleClick(stringRow.valueCell);
Assert.equal(
this.document.activeElement,
stringRow.valueInput,
"The input is focused"
);
EventUtils.sendString("New String!");
EventUtils.synthesizeKey("KEY_Enter");
Assert.equal(stringRow.value, "New String!");
// Check triple-click also edits the pref and selects the text inside.
tripleClick(stringRow.nameCell);
Assert.equal(
this.document.activeElement,
stringRow.valueInput,
"The input is focused"
);
// Check double-click inside input selects a word.
let newString = "Another string...";
EventUtils.sendString(newString);
Assert.equal(this.window.getSelection().toString(), "");
let stringInput = stringRow.valueInput;
doubleClick(stringInput);
let selectionLength = stringInput.selectionEnd - stringInput.selectionStart;
Assert.greater(selectionLength, 0);
Assert.less(selectionLength, newString.length);
EventUtils.synthesizeKey("KEY_Enter");
Assert.equal(stringRow.value, newString);
// Check that double/triple-click on the add row selects text as usual.
let addRow = this.getRow(PREF_MODIFY_PREFIX);
Assert.ok(addRow.hasClass("deleted"));
doubleClick(addRow.nameCell);
Assert.ok(PREF_MODIFY_PREFIX.includes(this.window.getSelection()));
tripleClick(addRow.nameCell);
Assert.equal(this.window.getSelection().toString(), PREF_MODIFY_PREFIX);
// Make sure the localized text is set in the value cell.
let labels = Array.from(addRow.valueCell.querySelectorAll("label > span"));
await this.document.l10n.translateElements(labels);
Assert.ok(labels.every(label => !!label.textContent));
// Double-click the first input label text.
doubleClick(labels[0]);
Assert.equal(this.window.getSelection().toString(), labels[0].textContent);
tripleClick(addRow.valueCell.querySelector("label > span"));
Assert.equal(
this.window.getSelection().toString(),
labels.map(l => l.textContent).join("")
);
});
});