summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/browser_autocomplete_popup_input.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/test/browser_autocomplete_popup_input.js')
-rw-r--r--devtools/client/shared/test/browser_autocomplete_popup_input.js251
1 files changed, 251 insertions, 0 deletions
diff --git a/devtools/client/shared/test/browser_autocomplete_popup_input.js b/devtools/client/shared/test/browser_autocomplete_popup_input.js
new file mode 100644
index 0000000000..a7d04f1c9c
--- /dev/null
+++ b/devtools/client/shared/test/browser_autocomplete_popup_input.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ // Prevent the URL Bar to steal the focus.
+ const preventUrlBarFocus = e => {
+ e.preventDefault();
+ };
+ window.gURLBar.addEventListener("beforefocus", preventUrlBarFocus);
+ registerCleanupFunction(() => {
+ window.gURLBar.removeEventListener("beforefocus", preventUrlBarFocus);
+ });
+
+ const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
+
+ info("Create an autocompletion popup and an input that will be bound to it");
+ const { doc } = await createHost();
+
+ const input = doc.createElement("input");
+ doc.body.append(input, doc.createElement("input"));
+
+ const onSelectCalled = [];
+ const onClickCalled = [];
+ const popup = new AutocompletePopup(doc, {
+ input,
+ position: "top",
+ autoSelect: true,
+ onSelect: item => onSelectCalled.push(item),
+ onClick: (e, item) => onClickCalled.push(item),
+ });
+
+ input.focus();
+ ok(hasFocus(input), "input has focus");
+
+ info(
+ "Check that Tab moves the focus out of the input when the popup isn't opened"
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(onClickCalled.length, 0, "onClick wasn't called");
+ is(hasFocus(input), false, "input does not have the focus anymore");
+ info("Set the focus back to the input and open the popup");
+ input.focus();
+ await new Promise(res => setTimeout(res, 0));
+ ok(hasFocus(input), "input is focused");
+
+ await populateAndOpenPopup(popup);
+
+ const checkSelectedItem = (expected, info) =>
+ checkPopupSelectedItem(popup, input, expected, info);
+
+ checkSelectedItem(popupItems[0], "First item from top is selected");
+ is(
+ onSelectCalled[0].label,
+ popupItems[0].label,
+ "onSelect was called with expected param"
+ );
+
+ info("Check that arrow down/up navigates into the list");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ checkSelectedItem(popupItems[1], "item-1 is selected");
+ is(
+ onSelectCalled[1].label,
+ popupItems[1].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ checkSelectedItem(popupItems[2], "item-2 is selected");
+ is(
+ onSelectCalled[2].label,
+ popupItems[2].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ checkSelectedItem(popupItems[0], "item-0 is selected");
+ is(
+ onSelectCalled[3].label,
+ popupItems[0].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ checkSelectedItem(popupItems[2], "item-2 is selected");
+ is(
+ onSelectCalled[4].label,
+ popupItems[2].label,
+ "onSelect was called with expected param"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ checkSelectedItem(popupItems[1], "item-2 is selected");
+ is(
+ onSelectCalled[5].label,
+ popupItems[1].label,
+ "onSelect was called with expected param"
+ );
+
+ info("Check that Escape closes the popup");
+ let onPopupClosed = popup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await onPopupClosed;
+ ok(true, "popup was closed with Escape key");
+ ok(hasFocus(input), "input still has the focus");
+ is(onClickCalled.length, 0, "onClick wasn't called");
+
+ info("Fill the input");
+ const value = "item";
+ EventUtils.sendString(value);
+ is(input.value, value, "input has the expected value");
+ is(
+ input.selectionStart,
+ value.length,
+ "input cursor is at expected position"
+ );
+ info("Open the popup again");
+ await populateAndOpenPopup(popup);
+
+ info("Check that Arrow Left + Shift does not close the popup");
+ const timeoutRes = "TIMED_OUT";
+ const onRaceEnded = Promise.race([
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(res => setTimeout(() => res(timeoutRes), 500)),
+ popup.once("popup-closed"),
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ const raceResult = await onRaceEnded;
+ is(raceResult, timeoutRes, "popup wasn't closed");
+ ok(popup.isOpen, "popup is still open");
+ is(input.selectionEnd - input.selectionStart, 1, "text was selected");
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Arrow Left closes the popup");
+ onPopupClosed = popup.once("popup-closed");
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ await onPopupClosed;
+ is(
+ input.selectionStart,
+ value.length - 1,
+ "input cursor was moved one char back"
+ );
+ is(input.selectionEnd, input.selectionStart, "selection was removed");
+ is(onClickCalled.length, 0, "onClick wasn't called");
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Open the popup again");
+ await populateAndOpenPopup(popup);
+
+ info("Check that Arrow Right + Shift does not trigger onClick");
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ is(onClickCalled.length, 0, "onClick wasn't called");
+ is(input.selectionEnd - input.selectionStart, 1, "input text was selected");
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Arrow Right triggers onClick");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(onClickCalled.length, 1, "onClick was called");
+ is(
+ onClickCalled[0],
+ popupItems[0],
+ "onClick was called with the selected item"
+ );
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Enter triggers onClick");
+ EventUtils.synthesizeKey("KEY_Enter");
+ is(onClickCalled.length, 2, "onClick was called");
+ is(
+ onClickCalled[1],
+ popupItems[0],
+ "onClick was called with the selected item"
+ );
+ ok(hasFocus(input), "input still has the focus");
+
+ info("Check that Tab triggers onClick");
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(onClickCalled.length, 3, "onClick was called");
+ is(
+ onClickCalled[2],
+ popupItems[0],
+ "onClick was called with the selected item"
+ );
+ ok(hasFocus(input), "input still has the focus");
+
+ info(
+ "Check that Shift+Tab does not trigger onClick and move the focus out of the input"
+ );
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ is(onClickCalled.length, 3, "onClick wasn't called");
+ is(hasFocus(input), false, "input does not have the focus anymore");
+
+ const onPopupClose = popup.once("popup-closed");
+ popup.hidePopup();
+ await onPopupClose;
+});
+
+const popupItems = [
+ { label: "item-0", value: "value-0" },
+ { label: "item-1", value: "value-1" },
+ { label: "item-2", value: "value-2" },
+];
+
+async function populateAndOpenPopup(popup) {
+ popup.setItems(popupItems);
+ await popup.openPopup();
+}
+
+/**
+ * Returns true if the give node is currently focused.
+ */
+function hasFocus(node) {
+ return (
+ node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
+ );
+}
+
+/**
+ * Check that the selected item in the popup is the expected one. Also check that the
+ * active descendant is properly set and that the popup has the focus.
+ *
+ * @param {AutocompletePopup} popup
+ * @param {HTMLInput} input
+ * @param {Object} expectedSelectedItem
+ * @param {String} info
+ */
+function checkPopupSelectedItem(popup, input, expectedSelectedItem, info) {
+ is(popup.selectedItem.label, expectedSelectedItem.label, info);
+ checkActiveDescendant(popup, input);
+ ok(hasFocus(input), "input still has the focus");
+}
+
+function checkActiveDescendant(popup, input) {
+ const activeElement = input.ownerDocument.activeElement;
+ const descendantId = activeElement.getAttribute("aria-activedescendant");
+ const popupItem = popup._tooltip.panel.querySelector(`#${descendantId}`);
+ const cloneItem = input.ownerDocument.querySelector(`#${descendantId}`);
+
+ ok(popupItem, "Active descendant is found in the popup list");
+ ok(cloneItem, "Active descendant is found in the list clone");
+ is(
+ stripNS(popupItem.outerHTML),
+ cloneItem.outerHTML,
+ "Cloned item has the same HTML as the original element"
+ );
+}
+
+function stripNS(text) {
+ return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");
+}