1 /* Any copyright is dedicated to the Public Domain.
2 * http://creativecommons.org/publicdomain/zero/1.0/ */
6 add_task(async function () {
7 // Prevent the URL Bar to steal the focus.
8 const preventUrlBarFocus = e => {
11 window.gURLBar.addEventListener("beforefocus", preventUrlBarFocus);
12 registerCleanupFunction(() => {
13 window.gURLBar.removeEventListener("beforefocus", preventUrlBarFocus);
16 const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
18 info("Create an autocompletion popup and an input that will be bound to it");
19 const { doc } = await createHost();
21 const input = doc.createElement("input");
22 const prevInput = doc.createElement("input");
23 doc.body.append(prevInput, input, doc.createElement("input"));
25 const onSelectCalled = [];
26 const onClickCalled = [];
27 const popup = new AutocompletePopup(doc, {
31 onSelect: item => onSelectCalled.push(item),
32 onClick: (e, item) => onClickCalled.push(item),
36 ok(hasFocus(input), "input has focus");
39 "Check that Tab moves the focus out of the input when the popup isn't opened"
41 EventUtils.synthesizeKey("KEY_Tab");
42 is(onClickCalled.length, 0, "onClick wasn't called");
43 is(hasFocus(input), false, "input does not have the focus anymore");
44 info("Set the focus back to the input and open the popup");
46 await new Promise(res => setTimeout(res, 0));
47 ok(hasFocus(input), "input is focused");
49 await populateAndOpenPopup(popup);
51 const checkSelectedItem = (expected, info) =>
52 checkPopupSelectedItem(popup, input, expected, info);
54 checkSelectedItem(popupItems[0], "First item from top is selected");
56 onSelectCalled[0].label,
58 "onSelect was called with expected param"
61 info("Check that arrow down/up navigates into the list");
62 EventUtils.synthesizeKey("KEY_ArrowDown");
63 checkSelectedItem(popupItems[1], "item-1 is selected");
65 onSelectCalled[1].label,
67 "onSelect was called with expected param"
70 EventUtils.synthesizeKey("KEY_ArrowDown");
71 checkSelectedItem(popupItems[2], "item-2 is selected");
73 onSelectCalled[2].label,
75 "onSelect was called with expected param"
78 EventUtils.synthesizeKey("KEY_ArrowDown");
79 checkSelectedItem(popupItems[0], "item-0 is selected");
81 onSelectCalled[3].label,
83 "onSelect was called with expected param"
86 EventUtils.synthesizeKey("KEY_ArrowUp");
87 checkSelectedItem(popupItems[2], "item-2 is selected");
89 onSelectCalled[4].label,
91 "onSelect was called with expected param"
94 EventUtils.synthesizeKey("KEY_ArrowUp");
95 checkSelectedItem(popupItems[1], "item-2 is selected");
97 onSelectCalled[5].label,
99 "onSelect was called with expected param"
102 info("Check that Escape closes the popup");
103 let onPopupClosed = popup.once("popup-closed");
104 EventUtils.synthesizeKey("KEY_Escape");
106 ok(true, "popup was closed with Escape key");
107 ok(hasFocus(input), "input still has the focus");
108 is(onClickCalled.length, 0, "onClick wasn't called");
110 info("Fill the input");
111 const value = "item";
112 EventUtils.sendString(value);
113 is(input.value, value, "input has the expected value");
115 input.selectionStart,
117 "input cursor is at expected position"
119 info("Open the popup again");
120 await populateAndOpenPopup(popup);
122 info("Check that Arrow Left + Shift does not close the popup");
123 const timeoutRes = "TIMED_OUT";
124 const onRaceEnded = Promise.race([
125 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
126 await new Promise(res => setTimeout(() => res(timeoutRes), 500)),
127 popup.once("popup-closed"),
129 EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
130 const raceResult = await onRaceEnded;
131 is(raceResult, timeoutRes, "popup wasn't closed");
132 ok(popup.isOpen, "popup is still open");
133 is(input.selectionEnd - input.selectionStart, 1, "text was selected");
134 ok(hasFocus(input), "input still has the focus");
136 info("Check that Arrow Left closes the popup");
137 onPopupClosed = popup.once("popup-closed");
138 EventUtils.synthesizeKey("KEY_ArrowLeft");
141 input.selectionStart,
143 "input cursor was moved one char back"
145 is(input.selectionEnd, input.selectionStart, "selection was removed");
146 is(onClickCalled.length, 0, "onClick wasn't called");
147 ok(hasFocus(input), "input still has the focus");
149 info("Open the popup again");
150 await populateAndOpenPopup(popup);
152 info("Check that Arrow Right + Shift does not trigger onClick");
153 EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
154 is(onClickCalled.length, 0, "onClick wasn't called");
155 is(input.selectionEnd - input.selectionStart, 1, "input text was selected");
156 ok(hasFocus(input), "input still has the focus");
158 info("Check that Arrow Right triggers onClick");
159 EventUtils.synthesizeKey("KEY_ArrowRight");
160 is(onClickCalled.length, 1, "onClick was called");
164 "onClick was called with the selected item"
166 ok(hasFocus(input), "input still has the focus");
168 info("Check that Enter triggers onClick");
169 EventUtils.synthesizeKey("KEY_Enter");
170 is(onClickCalled.length, 2, "onClick was called");
174 "onClick was called with the selected item"
176 ok(hasFocus(input), "input still has the focus");
178 info("Check that Tab triggers onClick");
179 EventUtils.synthesizeKey("KEY_Tab");
180 is(onClickCalled.length, 3, "onClick was called");
184 "onClick was called with the selected item"
186 ok(hasFocus(input), "input still has the focus");
189 "Check that Shift+Tab does not trigger onClick and move the focus out of the input"
191 EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
192 is(onClickCalled.length, 3, "onClick wasn't called");
194 is(hasFocus(input), false, "input does not have the focus anymore");
195 is(hasFocus(prevInput), true, "Shift+Tab moves the focus to prevInput");
197 const onPopupClose = popup.once("popup-closed");
203 { label: "item-0", value: "value-0" },
204 { label: "item-1", value: "value-1" },
205 { label: "item-2", value: "value-2" },
208 async function populateAndOpenPopup(popup) {
209 popup.setItems(popupItems);
210 await popup.openPopup();
214 * Returns true if the give node is currently focused.
216 function hasFocus(node) {
218 node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus()
223 * Check that the selected item in the popup is the expected one. Also check that the
224 * active descendant is properly set and that the popup has the focus.
226 * @param {AutocompletePopup} popup
227 * @param {HTMLInput} input
228 * @param {Object} expectedSelectedItem
229 * @param {String} info
231 function checkPopupSelectedItem(popup, input, expectedSelectedItem, info) {
232 is(popup.selectedItem.label, expectedSelectedItem.label, info);
233 checkActiveDescendant(popup, input);
234 ok(hasFocus(input), "input still has the focus");
237 function checkActiveDescendant(popup, input) {
238 const activeElement = input.ownerDocument.activeElement;
239 const descendantId = activeElement.getAttribute("aria-activedescendant");
240 const popupItem = popup._tooltip.panel.querySelector(`#${descendantId}`);
241 const cloneItem = input.ownerDocument.querySelector(`#${descendantId}`);
243 ok(popupItem, "Active descendant is found in the popup list");
244 ok(cloneItem, "Active descendant is found in the list clone");
246 stripNS(popupItem.outerHTML),
248 "Cloned item has the same HTML as the original element"
252 function stripNS(text) {
253 return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), "");