tor-browser

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

contextmenu_common.js (14517B)


      1 // This file expects contextMenu to be defined in the scope it is loaded into.
      2 /* global contextMenu:true */
      3 
      4 var lastElement;
      5 const FRAME_OS_PID = "context-frameOsPid";
      6 
      7 function openContextMenuFor(element, shiftkey, waitForSpellCheck) {
      8  // Context menu should be closed before we open it again.
      9  is(
     10    SpecialPowers.wrap(contextMenu).state,
     11    "closed",
     12    "checking if popup is closed"
     13  );
     14 
     15  if (lastElement) {
     16    lastElement.blur();
     17  }
     18  element.focus();
     19 
     20  // Some elements need time to focus and spellcheck before any tests are
     21  // run on them.
     22  function actuallyOpenContextMenuFor() {
     23    lastElement = element;
     24    var eventDetails = { type: "contextmenu", button: 2, shiftKey: shiftkey };
     25    synthesizeMouse(element, 2, 2, eventDetails, element.ownerGlobal);
     26  }
     27 
     28  if (waitForSpellCheck) {
     29    var { onSpellCheck } = SpecialPowers.ChromeUtils.importESModule(
     30      "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
     31    );
     32    onSpellCheck(element, actuallyOpenContextMenuFor);
     33  } else {
     34    actuallyOpenContextMenuFor();
     35  }
     36 }
     37 
     38 function closeContextMenu() {
     39  contextMenu.hidePopup();
     40 }
     41 
     42 function getVisibleMenuItems(aMenu) {
     43  var items = [];
     44  var accessKeys = {};
     45  for (var i = 0; i < aMenu.children.length; i++) {
     46    var item = aMenu.children[i];
     47    if (item.hidden) {
     48      continue;
     49    }
     50 
     51    var key = item.accessKey;
     52    if (key) {
     53      key = key.toLowerCase();
     54    }
     55 
     56    if (item.nodeName == "menuitem") {
     57      var isGenerated =
     58        item.classList.contains("spell-suggestion") ||
     59        item.classList.contains("sendtab-target");
     60      if (isGenerated) {
     61        is(item.id, "", "child menuitem #" + i + " is generated");
     62      } else {
     63        ok(item.id, "child menuitem #" + i + " has an ID");
     64      }
     65      var label = item.getAttribute("label");
     66      ok(label.length, "menuitem " + item.id + " has a label");
     67      if (isGenerated) {
     68        is(key, null, "Generated items shouldn't have an access key");
     69        items.push("*" + label);
     70      } else if (
     71        item.id.indexOf("spell-check-dictionary-") != 0 &&
     72        item.id != "spell-no-suggestions" &&
     73        item.id != "spell-add-dictionaries-main" &&
     74        item.id != "fill-login-no-logins" &&
     75        // Inspect accessibility properties does not have an access key. See
     76        // bug 1630717 for more details.
     77        item.id != "context-inspect-a11y" &&
     78        !item.id.includes("context-media-playbackrate") &&
     79        item.id != "context-copy-link-to-highlight" &&
     80        item.id != "context-copy-clean-link-to-highlight"
     81      ) {
     82        if (item.id != FRAME_OS_PID) {
     83          ok(key, "menuitem " + item.id + " has an access key");
     84        }
     85        if (accessKeys[key]) {
     86          ok(
     87            false,
     88            "menuitem " + item.id + " has same accesskey as " + accessKeys[key]
     89          );
     90        } else {
     91          accessKeys[key] = item.id;
     92        }
     93      }
     94      if (!isGenerated) {
     95        items.push(item.id);
     96      }
     97      items.push(!item.disabled);
     98    } else if (item.nodeName == "menuseparator") {
     99      ok(true, "--- seperator id is " + item.id);
    100      items.push("---");
    101      items.push(null);
    102    } else if (item.nodeName == "menu") {
    103      ok(item.id, "child menu #" + i + " has an ID");
    104      ok(key, "menu has an access key");
    105      if (accessKeys[key]) {
    106        ok(
    107          false,
    108          "menu " + item.id + " has same accesskey as " + accessKeys[key]
    109        );
    110      } else {
    111        accessKeys[key] = item.id;
    112      }
    113      items.push(item.id);
    114      items.push(!item.disabled);
    115      // Add a dummy item so that the indexes in checkMenu are the same
    116      // for expectedItems and actualItems.
    117      items.push([]);
    118      items.push(null);
    119    } else if (item.nodeName == "menugroup") {
    120      ok(item.id, "child menugroup #" + i + " has an ID");
    121      items.push(item.id);
    122      items.push(!item.disabled);
    123      var menugroupChildren = [];
    124      for (var child of item.children) {
    125        if (child.hidden) {
    126          continue;
    127        }
    128 
    129        menugroupChildren.push([child.id, !child.disabled]);
    130      }
    131      items.push(menugroupChildren);
    132      items.push(null);
    133    } else {
    134      ok(
    135        false,
    136        "child #" +
    137          i +
    138          " of menu ID " +
    139          aMenu.id +
    140          " has an unknown type (" +
    141          item.nodeName +
    142          ")"
    143      );
    144    }
    145  }
    146  return items;
    147 }
    148 
    149 function checkContextMenu(expectedItems) {
    150  is(contextMenu.state, "open", "checking if popup is open");
    151  var data = { generatedSubmenuId: 1 };
    152  checkMenu(contextMenu, expectedItems, data);
    153 }
    154 
    155 function checkMenuItem(
    156  actualItem,
    157  actualEnabled,
    158  expectedItem,
    159  expectedEnabled,
    160  index
    161 ) {
    162  is(
    163    `${actualItem}`,
    164    expectedItem,
    165    "checking item #" + index / 2 + " (" + expectedItem + ") name"
    166  );
    167 
    168  if (
    169    (typeof expectedEnabled == "object" && expectedEnabled != null) ||
    170    (typeof actualEnabled == "object" && actualEnabled != null)
    171  ) {
    172    ok(!(actualEnabled == null), "actualEnabled is not null");
    173    ok(!(expectedEnabled == null), "expectedEnabled is not null");
    174    is(typeof actualEnabled, typeof expectedEnabled, "checking types");
    175 
    176    if (
    177      typeof actualEnabled != typeof expectedEnabled ||
    178      actualEnabled == null ||
    179      expectedEnabled == null
    180    ) {
    181      return;
    182    }
    183 
    184    is(
    185      actualEnabled.type,
    186      expectedEnabled.type,
    187      "checking item #" + index / 2 + " (" + expectedItem + ") type attr value"
    188    );
    189    var icon = actualEnabled.icon;
    190    if (icon) {
    191      var tmp = "";
    192      var j = icon.length - 1;
    193      while (j && icon[j] != "/") {
    194        tmp = icon[j--] + tmp;
    195      }
    196      icon = tmp;
    197    }
    198    is(
    199      icon,
    200      expectedEnabled.icon,
    201      "checking item #" + index / 2 + " (" + expectedItem + ") icon attr value"
    202    );
    203    is(
    204      actualEnabled.checked,
    205      expectedEnabled.checked,
    206      "checking item #" + index / 2 + " (" + expectedItem + ") has checked attr"
    207    );
    208    is(
    209      actualEnabled.disabled,
    210      expectedEnabled.disabled,
    211      "checking item #" +
    212        index / 2 +
    213        " (" +
    214        expectedItem +
    215        ") has disabled attr"
    216    );
    217  } else if (expectedEnabled != null) {
    218    is(
    219      actualEnabled,
    220      expectedEnabled,
    221      "checking item #" + index / 2 + " (" + expectedItem + ") enabled state"
    222    );
    223  }
    224 }
    225 
    226 /*
    227 * checkMenu - checks to see if the specified <menupopup> contains the
    228 * expected items and state.
    229 * expectedItems is a array of (1) item IDs and (2) a boolean specifying if
    230 * the item is enabled or not (or null to ignore it). Submenus can be checked
    231 * by providing a nested array entry after the expected <menu> ID.
    232 * For example: ["blah", true,              // item enabled
    233 *               "submenu", null,           // submenu
    234 *                   ["sub1", true,         // submenu contents
    235 *                    "sub2", false], null, // submenu contents
    236 *               "lol", false]              // item disabled
    237 *
    238 */
    239 function checkMenu(menu, expectedItems, data) {
    240  var actualItems = getVisibleMenuItems(menu, data);
    241  // ok(false, "Items are: " + actualItems);
    242  for (var i = 0; i < expectedItems.length; i += 2) {
    243    var actualItem = actualItems[i];
    244    var actualEnabled = actualItems[i + 1];
    245    var expectedItem = expectedItems[i];
    246    var expectedEnabled = expectedItems[i + 1];
    247    if (expectedItem instanceof Array) {
    248      ok(true, "Checking submenu/menugroup...");
    249      var previousId = expectedItems[i - 2]; // The last item was the menu ID.
    250      var previousItem = menu.getElementsByAttribute("id", previousId)[0];
    251      ok(
    252        previousItem,
    253        (previousItem ? previousItem.nodeName : "item") +
    254          " with previous id (" +
    255          previousId +
    256          ") found"
    257      );
    258      if (previousItem && previousItem.nodeName == "menu") {
    259        ok(previousItem, "got a submenu element of id='" + previousId + "'");
    260        is(
    261          previousItem.nodeName,
    262          "menu",
    263          "submenu element of id='" + previousId + "' has expected nodeName"
    264        );
    265        checkMenu(previousItem.menupopup, expectedItem, data, i);
    266      } else if (previousItem && previousItem.nodeName == "menugroup") {
    267        ok(expectedItem.length, "menugroup must not be empty");
    268        for (var j = 0; j < expectedItem.length / 2; j++) {
    269          checkMenuItem(
    270            actualItems[i][j][0],
    271            actualItems[i][j][1],
    272            expectedItem[j * 2],
    273            expectedItem[j * 2 + 1],
    274            i + j * 2
    275          );
    276        }
    277        i += j;
    278      } else {
    279        ok(false, "previous item is not a menu or menugroup");
    280      }
    281    } else {
    282      checkMenuItem(
    283        actualItem,
    284        actualEnabled,
    285        expectedItem,
    286        expectedEnabled,
    287        i
    288      );
    289    }
    290  }
    291  // Could find unexpected extra items at the end...
    292  is(
    293    actualItems.length,
    294    expectedItems.length,
    295    "checking expected number of menu entries"
    296  );
    297 }
    298 
    299 let lastElementSelector = null;
    300 /**
    301 * Right-clicks on the element that matches `selector` and checks the
    302 * context menu that appears against the `menuItems` array.
    303 *
    304 * @param {string} selector
    305 *        A selector passed to querySelector to find
    306 *        the element that will be referenced.
    307 * @param {Array} menuItems
    308 *        An array of menuitem ids and their associated enabled state. A state
    309 *        of null means that it will be ignored. Ids of '---' are used for
    310 *        menuseparators.
    311 * @param {object} options, optional
    312 *        skipFocusChange: don't move focus to the element before test, useful
    313 *                         if you want to delay spell-check initialization
    314 *        offsetX: horizontal mouse offset from the top-left corner of
    315 *                 the element, optional
    316 *        offsetY: vertical mouse offset from the top-left corner of the
    317 *                 element, optional
    318 *        centered: if true, mouse position is centered in element, defaults
    319 *                  to true if offsetX and offsetY are not provided
    320 *        waitForSpellCheck: wait until spellcheck is initialized before
    321 *                           starting test
    322 *        preCheckContextMenuFn: callback to run before opening menu
    323 *        onContextMenuShown: callback to run when the context menu is shown
    324 *        postCheckContextMenuFn: callback to run after opening menu
    325 *        keepMenuOpen: if true, we do not call hidePopup, the consumer is
    326 *                      responsible for calling it.
    327 * @return {Promise} resolved after the test finishes
    328 */
    329 async function test_contextmenu(selector, menuItems, options = {}) {
    330  contextMenu = document.getElementById("contentAreaContextMenu");
    331  is(contextMenu.state, "closed", "checking if popup is closed");
    332 
    333  // Default to centered if no positioning is defined.
    334  if (!options.offsetX && !options.offsetY) {
    335    options.centered = true;
    336  }
    337 
    338  if (!options.skipFocusChange) {
    339    await SpecialPowers.spawn(
    340      gBrowser.selectedBrowser,
    341      [[lastElementSelector, selector]],
    342      async function ([contentLastElementSelector, contentSelector]) {
    343        if (contentLastElementSelector) {
    344          let contentLastElement = content.document.querySelector(
    345            contentLastElementSelector
    346          );
    347          contentLastElement.blur();
    348        }
    349        let element = content.document.querySelector(contentSelector);
    350        element.focus();
    351      }
    352    );
    353    lastElementSelector = selector;
    354    info(`Moved focus to ${selector}`);
    355  }
    356 
    357  if (options.preCheckContextMenuFn) {
    358    await options.preCheckContextMenuFn();
    359    info("Completed preCheckContextMenuFn");
    360  }
    361 
    362  if (options.waitForSpellCheck) {
    363    info("Waiting for spell check");
    364    await SpecialPowers.spawn(
    365      gBrowser.selectedBrowser,
    366      [selector],
    367      async function (contentSelector) {
    368        let { onSpellCheck } = ChromeUtils.importESModule(
    369          "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs"
    370        );
    371        let element = content.document.querySelector(contentSelector);
    372        await new Promise(resolve => onSpellCheck(element, resolve));
    373        info("Spell check running");
    374      }
    375    );
    376  }
    377 
    378  let awaitPopupShown = BrowserTestUtils.waitForEvent(
    379    contextMenu,
    380    "popupshown"
    381  );
    382  await BrowserTestUtils.synthesizeMouse(
    383    selector,
    384    options.offsetX || 0,
    385    options.offsetY || 0,
    386    {
    387      type: "contextmenu",
    388      button: 2,
    389      shiftkey: options.shiftkey,
    390      centered: options.centered,
    391    },
    392    gBrowser.selectedBrowser
    393  );
    394  await awaitPopupShown;
    395  info("Popup Shown");
    396 
    397  if (options.onContextMenuShown) {
    398    await options.onContextMenuShown();
    399    info("Completed onContextMenuShown");
    400  }
    401 
    402  if (
    403    typeof options.awaitOnMenuBuilt === "object" &&
    404    options.awaitOnMenuBuilt.id
    405  ) {
    406    const elementId = options.awaitOnMenuBuilt.id;
    407    const menu = document.getElementById(elementId);
    408    await TestUtils.waitForCondition(
    409      () => menu && !menu.hidden,
    410      `Menu ${elementId} did not appear in time`
    411    );
    412    info(`Menu "${elementId}" was built and is now visible`);
    413  }
    414 
    415  if (menuItems) {
    416    if (Services.prefs.getBoolPref("devtools.inspector.enabled", true)) {
    417      let inspectItems = [];
    418      let hasSeparatorAboveAskChat = false;
    419      const hasViewSource =
    420        menuItems.includes("context-viewsource") ||
    421        menuItems.includes("context-viewpartialsource-selection");
    422 
    423      const askChatIndex = menuItems.indexOf("context-ask-chat");
    424      const isAskChatLastItem = menuItems.at(-6) === "context-ask-chat";
    425      if (askChatIndex >= 2) {
    426        hasSeparatorAboveAskChat = menuItems[askChatIndex - 2] === "---";
    427      }
    428 
    429      if (!hasViewSource && !(isAskChatLastItem && hasSeparatorAboveAskChat)) {
    430        inspectItems.push("---", null);
    431      }
    432 
    433      if (
    434        Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
    435        (Services.prefs.getBoolPref("devtools.everOpened", false) ||
    436          Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0)
    437      ) {
    438        inspectItems.push("context-inspect-a11y", true);
    439      }
    440      inspectItems.push("context-inspect", true);
    441 
    442      menuItems = menuItems.concat(inspectItems);
    443    }
    444 
    445    checkContextMenu(menuItems);
    446  }
    447 
    448  let awaitPopupHidden = BrowserTestUtils.waitForEvent(
    449    contextMenu,
    450    "popuphidden"
    451  );
    452 
    453  if (options.postCheckContextMenuFn) {
    454    await options.postCheckContextMenuFn();
    455    info("Completed postCheckContextMenuFn");
    456  }
    457 
    458  if (!options.keepMenuOpen) {
    459    contextMenu.hidePopup();
    460    await awaitPopupHidden;
    461  }
    462 }