tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }