browser_autocomplete_popup_input.js (8160B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 add_task(async function () { 7 // Prevent the URL Bar to steal the focus. 8 const preventUrlBarFocus = e => { 9 e.preventDefault(); 10 }; 11 window.gURLBar.addEventListener("beforefocus", preventUrlBarFocus); 12 registerCleanupFunction(() => { 13 window.gURLBar.removeEventListener("beforefocus", preventUrlBarFocus); 14 }); 15 16 const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js"); 17 18 info("Create an autocompletion popup and an input that will be bound to it"); 19 const { doc } = await createHost(); 20 21 const input = doc.createElement("input"); 22 const prevInput = doc.createElement("input"); 23 doc.body.append(prevInput, input, doc.createElement("input")); 24 25 const onSelectCalled = []; 26 const onClickCalled = []; 27 const popup = new AutocompletePopup(doc, { 28 input, 29 position: "top", 30 autoSelect: true, 31 onSelect: item => onSelectCalled.push(item), 32 onClick: (e, item) => onClickCalled.push(item), 33 }); 34 35 input.focus(); 36 ok(hasFocus(input), "input has focus"); 37 38 info( 39 "Check that Tab moves the focus out of the input when the popup isn't opened" 40 ); 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"); 45 input.focus(); 46 await new Promise(res => setTimeout(res, 0)); 47 ok(hasFocus(input), "input is focused"); 48 49 await populateAndOpenPopup(popup); 50 51 const checkSelectedItem = (expected, info) => 52 checkPopupSelectedItem(popup, input, expected, info); 53 54 checkSelectedItem(popupItems[0], "First item from top is selected"); 55 is( 56 onSelectCalled[0].label, 57 popupItems[0].label, 58 "onSelect was called with expected param" 59 ); 60 61 info("Check that arrow down/up navigates into the list"); 62 EventUtils.synthesizeKey("KEY_ArrowDown"); 63 checkSelectedItem(popupItems[1], "item-1 is selected"); 64 is( 65 onSelectCalled[1].label, 66 popupItems[1].label, 67 "onSelect was called with expected param" 68 ); 69 70 EventUtils.synthesizeKey("KEY_ArrowDown"); 71 checkSelectedItem(popupItems[2], "item-2 is selected"); 72 is( 73 onSelectCalled[2].label, 74 popupItems[2].label, 75 "onSelect was called with expected param" 76 ); 77 78 EventUtils.synthesizeKey("KEY_ArrowDown"); 79 checkSelectedItem(popupItems[0], "item-0 is selected"); 80 is( 81 onSelectCalled[3].label, 82 popupItems[0].label, 83 "onSelect was called with expected param" 84 ); 85 86 EventUtils.synthesizeKey("KEY_ArrowUp"); 87 checkSelectedItem(popupItems[2], "item-2 is selected"); 88 is( 89 onSelectCalled[4].label, 90 popupItems[2].label, 91 "onSelect was called with expected param" 92 ); 93 94 EventUtils.synthesizeKey("KEY_ArrowUp"); 95 checkSelectedItem(popupItems[1], "item-2 is selected"); 96 is( 97 onSelectCalled[5].label, 98 popupItems[1].label, 99 "onSelect was called with expected param" 100 ); 101 102 info("Check that Escape closes the popup"); 103 let onPopupClosed = popup.once("popup-closed"); 104 EventUtils.synthesizeKey("KEY_Escape"); 105 await onPopupClosed; 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"); 109 110 info("Fill the input"); 111 const value = "item"; 112 EventUtils.sendString(value); 113 is(input.value, value, "input has the expected value"); 114 is( 115 input.selectionStart, 116 value.length, 117 "input cursor is at expected position" 118 ); 119 info("Open the popup again"); 120 await populateAndOpenPopup(popup); 121 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"), 128 ]); 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"); 135 136 info("Check that Arrow Left closes the popup"); 137 onPopupClosed = popup.once("popup-closed"); 138 EventUtils.synthesizeKey("KEY_ArrowLeft"); 139 await onPopupClosed; 140 is( 141 input.selectionStart, 142 value.length - 1, 143 "input cursor was moved one char back" 144 ); 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"); 148 149 info("Open the popup again"); 150 await populateAndOpenPopup(popup); 151 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"); 157 158 info("Check that Arrow Right triggers onClick"); 159 EventUtils.synthesizeKey("KEY_ArrowRight"); 160 is(onClickCalled.length, 1, "onClick was called"); 161 is( 162 onClickCalled[0], 163 popupItems[0], 164 "onClick was called with the selected item" 165 ); 166 ok(hasFocus(input), "input still has the focus"); 167 168 info("Check that Enter triggers onClick"); 169 EventUtils.synthesizeKey("KEY_Enter"); 170 is(onClickCalled.length, 2, "onClick was called"); 171 is( 172 onClickCalled[1], 173 popupItems[0], 174 "onClick was called with the selected item" 175 ); 176 ok(hasFocus(input), "input still has the focus"); 177 178 info("Check that Tab triggers onClick"); 179 EventUtils.synthesizeKey("KEY_Tab"); 180 is(onClickCalled.length, 3, "onClick was called"); 181 is( 182 onClickCalled[2], 183 popupItems[0], 184 "onClick was called with the selected item" 185 ); 186 ok(hasFocus(input), "input still has the focus"); 187 188 info( 189 "Check that Shift+Tab does not trigger onClick and move the focus out of the input" 190 ); 191 EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); 192 is(onClickCalled.length, 3, "onClick wasn't called"); 193 194 is(hasFocus(input), false, "input does not have the focus anymore"); 195 is(hasFocus(prevInput), true, "Shift+Tab moves the focus to prevInput"); 196 197 const onPopupClose = popup.once("popup-closed"); 198 popup.hidePopup(); 199 await onPopupClose; 200 }); 201 202 const popupItems = [ 203 { label: "item-0", value: "value-0" }, 204 { label: "item-1", value: "value-1" }, 205 { label: "item-2", value: "value-2" }, 206 ]; 207 208 async function populateAndOpenPopup(popup) { 209 popup.setItems(popupItems); 210 await popup.openPopup(); 211 } 212 213 /** 214 * Returns true if the give node is currently focused. 215 */ 216 function hasFocus(node) { 217 return ( 218 node.ownerDocument.activeElement == node && node.ownerDocument.hasFocus() 219 ); 220 } 221 222 /** 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. 225 * 226 * @param {AutocompletePopup} popup 227 * @param {HTMLInput} input 228 * @param {object} expectedSelectedItem 229 * @param {string} info 230 */ 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"); 235 } 236 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}`); 242 243 ok(popupItem, "Active descendant is found in the popup list"); 244 ok(cloneItem, "Active descendant is found in the list clone"); 245 is( 246 stripNS(popupItem.outerHTML), 247 cloneItem.outerHTML, 248 "Cloned item has the same HTML as the original element" 249 ); 250 } 251 252 function stripNS(text) { 253 return text.replace(RegExp(' xmlns="http://www.w3.org/1999/xhtml"', "g"), ""); 254 }