tor-browser

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

head.js (17573B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 ChromeUtils.defineESModuleGetters(this, {
      8  CustomizableUI:
      9    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     10  CustomizableUITestUtils:
     11    "resource://testing-common/CustomizableUITestUtils.sys.mjs",
     12 });
     13 
     14 /**
     15 * Instance of CustomizableUITestUtils for the current browser window.
     16 */
     17 var gCUITestUtils = new CustomizableUITestUtils(window);
     18 
     19 Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
     20 registerCleanupFunction(() =>
     21  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck")
     22 );
     23 
     24 var { synthesizeDrop, synthesizeMouseAtCenter } = EventUtils;
     25 
     26 // As of bug 1960002, this width no longer technically forces overflow.
     27 // Instead, use `ensureToolbarOverflow()` below.
     28 const kForceOverflowWidthPx = 500;
     29 
     30 function createDummyXULButton(id, label, win = window) {
     31  let btn = win.document.createXULElement("toolbarbutton");
     32  btn.id = id;
     33  btn.setAttribute("label", label || id);
     34  btn.className = "toolbarbutton-1 chromeclass-toolbar-additional";
     35  win.gNavToolbox.palette.appendChild(btn);
     36  return btn;
     37 }
     38 
     39 var gAddedToolbars = new Set();
     40 
     41 function createToolbarWithPlacements(id, placements = [], properties = {}) {
     42  gAddedToolbars.add(id);
     43  let tb = document.createXULElement("toolbar");
     44  tb.id = id;
     45  tb.setAttribute("customizable", "true");
     46 
     47  properties.type = CustomizableUI.TYPE_TOOLBAR;
     48  properties.defaultPlacements = placements;
     49  CustomizableUI.registerArea(id, properties);
     50  gNavToolbox.appendChild(tb);
     51  CustomizableUI.registerToolbarNode(tb);
     52  return tb;
     53 }
     54 
     55 function createOverflowableToolbarWithPlacements(id, placements) {
     56  gAddedToolbars.add(id);
     57 
     58  let tb = document.createXULElement("toolbar");
     59  tb.id = id;
     60  tb.setAttribute("customizationtarget", id + "-target");
     61 
     62  let customizationtarget = document.createXULElement("hbox");
     63  customizationtarget.id = id + "-target";
     64  customizationtarget.setAttribute("flex", "1");
     65  tb.appendChild(customizationtarget);
     66 
     67  let overflowPanel = document.createXULElement("panel");
     68  overflowPanel.id = id + "-overflow";
     69  document.getElementById("mainPopupSet").appendChild(overflowPanel);
     70 
     71  let overflowList = document.createXULElement("vbox");
     72  overflowList.id = id + "-overflow-list";
     73  overflowPanel.appendChild(overflowList);
     74 
     75  let chevron = document.createXULElement("toolbarbutton");
     76  chevron.id = id + "-chevron";
     77  tb.appendChild(chevron);
     78 
     79  CustomizableUI.registerArea(id, {
     80    type: CustomizableUI.TYPE_TOOLBAR,
     81    defaultPlacements: placements,
     82    overflowable: true,
     83  });
     84 
     85  tb.setAttribute("customizable", "true");
     86  tb.setAttribute("overflowable", "true");
     87  tb.setAttribute("default-overflowpanel", overflowPanel.id);
     88  tb.setAttribute("default-overflowtarget", overflowList.id);
     89  tb.setAttribute("default-overflowbutton", chevron.id);
     90  tb.setAttribute("addon-webext-overflowbutton", "unified-extensions-button");
     91  tb.setAttribute("addon-webext-overflowtarget", "overflowed-extensions-list");
     92 
     93  gNavToolbox.appendChild(tb);
     94  CustomizableUI.registerToolbarNode(tb);
     95  return tb;
     96 }
     97 
     98 function removeCustomToolbars() {
     99  CustomizableUI.reset();
    100  for (let toolbarId of gAddedToolbars) {
    101    CustomizableUI.unregisterArea(toolbarId, true);
    102    let tb = document.getElementById(toolbarId);
    103    if (tb.hasAttribute("overflowpanel")) {
    104      let panel = document.getElementById(tb.getAttribute("overflowpanel"));
    105      if (panel) {
    106        panel.remove();
    107      }
    108    }
    109    tb.remove();
    110  }
    111  gAddedToolbars.clear();
    112 }
    113 
    114 function resetCustomization() {
    115  return CustomizableUI.reset();
    116 }
    117 
    118 function isInDevEdition() {
    119  return AppConstants.MOZ_DEV_EDITION;
    120 }
    121 
    122 function removeNonReleaseButtons(areaPanelPlacements) {
    123  if (isInDevEdition() && areaPanelPlacements.includes("developer-button")) {
    124    areaPanelPlacements.splice(
    125      areaPanelPlacements.indexOf("developer-button"),
    126      1
    127    );
    128  }
    129 }
    130 
    131 function removeNonOriginalButtons() {
    132  CustomizableUI.removeWidgetFromArea("sync-button");
    133 }
    134 
    135 function assertAreaPlacements(areaId, expectedPlacements) {
    136  let actualPlacements = getAreaWidgetIds(areaId);
    137  placementArraysEqual(areaId, actualPlacements, expectedPlacements);
    138 }
    139 
    140 function placementArraysEqual(areaId, actualPlacements, expectedPlacements) {
    141  info("Actual placements: " + actualPlacements.join(", "));
    142  info("Expected placements: " + expectedPlacements.join(", "));
    143  is(
    144    actualPlacements.length,
    145    expectedPlacements.length,
    146    "Area " + areaId + " should have " + expectedPlacements.length + " items."
    147  );
    148  let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
    149  for (let i = 0; i < minItems; i++) {
    150    if (typeof expectedPlacements[i] == "string") {
    151      is(
    152        actualPlacements[i],
    153        expectedPlacements[i],
    154        "Item " + i + " in " + areaId + " should match expectations."
    155      );
    156    } else if (expectedPlacements[i] instanceof RegExp) {
    157      ok(
    158        expectedPlacements[i].test(actualPlacements[i]),
    159        "Item " +
    160          i +
    161          " (" +
    162          actualPlacements[i] +
    163          ") in " +
    164          areaId +
    165          " should match " +
    166          expectedPlacements[i]
    167      );
    168    } else {
    169      ok(
    170        false,
    171        "Unknown type of expected placement passed to " +
    172          " assertAreaPlacements. Is your test broken?"
    173      );
    174    }
    175  }
    176 }
    177 
    178 function todoAssertAreaPlacements(areaId, expectedPlacements) {
    179  let actualPlacements = getAreaWidgetIds(areaId);
    180  let isPassing = actualPlacements.length == expectedPlacements.length;
    181  let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
    182  for (let i = 0; i < minItems; i++) {
    183    if (typeof expectedPlacements[i] == "string") {
    184      isPassing = isPassing && actualPlacements[i] == expectedPlacements[i];
    185    } else if (expectedPlacements[i] instanceof RegExp) {
    186      isPassing = isPassing && expectedPlacements[i].test(actualPlacements[i]);
    187    } else {
    188      ok(
    189        false,
    190        "Unknown type of expected placement passed to " +
    191          " assertAreaPlacements. Is your test broken?"
    192      );
    193    }
    194  }
    195  todo(
    196    isPassing,
    197    "The area placements for " +
    198      areaId +
    199      " should equal the expected placements."
    200  );
    201 }
    202 
    203 function getAreaWidgetIds(areaId) {
    204  return CustomizableUI.getWidgetIdsInArea(areaId);
    205 }
    206 
    207 function simulateItemDrag(aToDrag, aTarget, aEvent = {}, aOffset = 2) {
    208  let ev = aEvent;
    209  if (ev == "end" || ev == "start") {
    210    let win = aTarget.ownerGlobal;
    211    const dwu = win.windowUtils;
    212    let bounds = dwu.getBoundsWithoutFlushing(aTarget);
    213    if (ev == "end") {
    214      ev = {
    215        clientX: bounds.right - aOffset,
    216        clientY: bounds.bottom - aOffset,
    217      };
    218    } else {
    219      ev = { clientX: bounds.left + aOffset, clientY: bounds.top + aOffset };
    220    }
    221  }
    222  ev._domDispatchOnly = true;
    223  synthesizeDrop(
    224    aToDrag.parentNode,
    225    aTarget,
    226    null,
    227    null,
    228    aToDrag.ownerGlobal,
    229    aTarget.ownerGlobal,
    230    ev
    231  );
    232  // Ensure dnd suppression is cleared.
    233  synthesizeMouseAtCenter(aTarget, { type: "mouseup" }, aTarget.ownerGlobal);
    234 }
    235 
    236 function endCustomizing(aWindow = window) {
    237  if (!aWindow.document.documentElement.hasAttribute("customizing")) {
    238    return true;
    239  }
    240  let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
    241    aWindow.gNavToolbox,
    242    "aftercustomization"
    243  );
    244  aWindow.gCustomizeMode.exit();
    245  return afterCustomizationPromise;
    246 }
    247 
    248 function startCustomizing(aWindow = window) {
    249  if (aWindow.document.documentElement.hasAttribute("customizing")) {
    250    return null;
    251  }
    252  let customizationReadyPromise = BrowserTestUtils.waitForEvent(
    253    aWindow.gNavToolbox,
    254    "customizationready"
    255  );
    256  aWindow.gCustomizeMode.enter();
    257  return customizationReadyPromise;
    258 }
    259 
    260 function promiseObserverNotified(aTopic) {
    261  return new Promise(resolve => {
    262    Services.obs.addObserver(function onNotification(subject, topic, data) {
    263      Services.obs.removeObserver(onNotification, topic);
    264      resolve({ subject, data });
    265    }, aTopic);
    266  });
    267 }
    268 
    269 function openAndLoadWindow(aOptions, aWaitForDelayedStartup = false) {
    270  return new Promise(resolve => {
    271    let win = OpenBrowserWindow(aOptions);
    272    if (aWaitForDelayedStartup) {
    273      Services.obs.addObserver(function onDS(aSubject) {
    274        if (aSubject != win) {
    275          return;
    276        }
    277        Services.obs.removeObserver(onDS, "browser-delayed-startup-finished");
    278        resolve(win);
    279      }, "browser-delayed-startup-finished");
    280    } else {
    281      win.addEventListener(
    282        "load",
    283        function () {
    284          resolve(win);
    285        },
    286        { once: true }
    287      );
    288    }
    289  });
    290 }
    291 
    292 function promiseWindowClosed(win) {
    293  return new Promise(resolve => {
    294    win.addEventListener(
    295      "unload",
    296      function () {
    297        resolve();
    298      },
    299      { once: true }
    300    );
    301    win.close();
    302  });
    303 }
    304 
    305 function promiseOverflowShown(win) {
    306  let panelEl = win.document.getElementById("widget-overflow");
    307  return promisePanelElementShown(win, panelEl);
    308 }
    309 
    310 function promisePanelElementShown(win, aPanel) {
    311  return new Promise((resolve, reject) => {
    312    let timeoutId = win.setTimeout(() => {
    313      reject("Panel did not show within 20 seconds.");
    314    }, 20000);
    315    function onPanelOpen() {
    316      aPanel.removeEventListener("popupshown", onPanelOpen);
    317      win.clearTimeout(timeoutId);
    318      resolve();
    319    }
    320    aPanel.addEventListener("popupshown", onPanelOpen);
    321  });
    322 }
    323 
    324 function promiseOverflowHidden(win) {
    325  let panelEl = win.PanelUI.overflowPanel;
    326  return promisePanelElementHidden(win, panelEl);
    327 }
    328 
    329 function promisePanelElementHidden(win, aPanel) {
    330  return new Promise((resolve, reject) => {
    331    let timeoutId = win.setTimeout(() => {
    332      reject("Panel did not hide within 20 seconds.");
    333    }, 20000);
    334    function onPanelClose() {
    335      aPanel.removeEventListener("popuphidden", onPanelClose);
    336      win.clearTimeout(timeoutId);
    337      executeSoon(resolve);
    338    }
    339    aPanel.addEventListener("popuphidden", onPanelClose);
    340  });
    341 }
    342 
    343 function isPanelUIOpen() {
    344  return PanelUI.panel.state == "open" || PanelUI.panel.state == "showing";
    345 }
    346 
    347 function isOverflowOpen() {
    348  let panel = document.getElementById("widget-overflow");
    349  return panel.state == "open" || panel.state == "showing";
    350 }
    351 
    352 function subviewShown(aSubview) {
    353  return new Promise((resolve, reject) => {
    354    let win = aSubview.ownerGlobal;
    355    let timeoutId = win.setTimeout(() => {
    356      reject("Subview (" + aSubview.id + ") did not show within 20 seconds.");
    357    }, 20000);
    358    function onViewShown() {
    359      aSubview.removeEventListener("ViewShown", onViewShown);
    360      win.clearTimeout(timeoutId);
    361      resolve();
    362    }
    363    aSubview.addEventListener("ViewShown", onViewShown);
    364  });
    365 }
    366 
    367 function subviewHidden(aSubview) {
    368  return new Promise((resolve, reject) => {
    369    let win = aSubview.ownerGlobal;
    370    let timeoutId = win.setTimeout(() => {
    371      reject("Subview (" + aSubview.id + ") did not hide within 20 seconds.");
    372    }, 20000);
    373    function onViewHiding() {
    374      aSubview.removeEventListener("ViewHiding", onViewHiding);
    375      win.clearTimeout(timeoutId);
    376      resolve();
    377    }
    378    aSubview.addEventListener("ViewHiding", onViewHiding);
    379  });
    380 }
    381 
    382 function waitFor(aTimeout = 100) {
    383  return new Promise(resolve => {
    384    setTimeout(() => resolve(), aTimeout);
    385  });
    386 }
    387 
    388 /**
    389 * Wait for an attribute on a node to change
    390 *
    391 * @param aNode      Node on which the mutation is expected
    392 * @param aAttribute The attribute we're interested in
    393 * @param aFilterFn  A function to check if the new value is what we want.
    394 * @return {Promise} resolved when the requisite mutation shows up.
    395 */
    396 function promiseAttributeMutation(aNode, aAttribute, aFilterFn) {
    397  return new Promise(resolve => {
    398    info("waiting for mutation of attribute '" + aAttribute + "'.");
    399    let obs = new MutationObserver(mutations => {
    400      for (let mut of mutations) {
    401        let attr = mut.attributeName;
    402        let newValue = mut.target.getAttribute(attr);
    403        if (aFilterFn(newValue)) {
    404          ok(
    405            true,
    406            "mutation occurred: attribute '" +
    407              attr +
    408              "' changed to '" +
    409              newValue +
    410              "' from '" +
    411              mut.oldValue +
    412              "'."
    413          );
    414          obs.disconnect();
    415          resolve();
    416        } else {
    417          info(
    418            "Ignoring mutation that produced value " +
    419              newValue +
    420              " because of filter."
    421          );
    422        }
    423      }
    424    });
    425    obs.observe(aNode, { attributeFilter: [aAttribute] });
    426  });
    427 }
    428 
    429 function popupShown(aPopup) {
    430  return BrowserTestUtils.waitForPopupEvent(aPopup, "shown");
    431 }
    432 
    433 function popupHidden(aPopup) {
    434  return BrowserTestUtils.waitForPopupEvent(aPopup, "hidden");
    435 }
    436 
    437 // This is a simpler version of the context menu check that
    438 // exists in contextmenu_common.js.
    439 function checkContextMenu(aContextMenu, aExpectedEntries, aWindow = window) {
    440  let children = [...aContextMenu.children];
    441  // Ignore hidden nodes:
    442  children = children.filter(n => !n.hidden);
    443  for (let i = 0; i < children.length; i++) {
    444    let menuitem = children[i];
    445    try {
    446      if (aExpectedEntries[i][0] == "---") {
    447        is(menuitem.localName, "menuseparator", "menuseparator expected");
    448        continue;
    449      }
    450 
    451      let selector = aExpectedEntries[i][0];
    452      ok(
    453        menuitem.matches(selector),
    454        "menuitem should match " + selector + " selector"
    455      );
    456      let commandValue = menuitem.getAttribute("command");
    457      let relatedCommand = commandValue
    458        ? aWindow.document.getElementById(commandValue)
    459        : null;
    460      let menuItemDisabled = relatedCommand
    461        ? relatedCommand.getAttribute("disabled") == "true"
    462        : menuitem.getAttribute("disabled") == "true";
    463      is(
    464        menuItemDisabled,
    465        !aExpectedEntries[i][1],
    466        "disabled state for " + selector
    467      );
    468    } catch (e) {
    469      ok(false, "Exception when checking context menu: " + e);
    470    }
    471  }
    472 }
    473 
    474 function waitForOverflowButtonShown(win = window) {
    475  info("Waiting for overflow button to show");
    476  let ov = win.document.getElementById("nav-bar-overflow-button");
    477  return waitForElementShown(ov.icon);
    478 }
    479 function waitForElementShown(element) {
    480  return BrowserTestUtils.waitForCondition(() => {
    481    info("Checking if element has non-0 size");
    482    // We intentionally flush layout to ensure the element is actually shown.
    483    let rect = element.getBoundingClientRect();
    484    return rect.width > 0 && rect.height > 0;
    485  });
    486 }
    487 
    488 /**
    489 * Opens the history panel through the history toolbarbutton in the
    490 * navbar and returns a promise that resolves as soon as the panel is open
    491 * is showing.
    492 */
    493 async function openHistoryPanel(doc = document) {
    494  await waitForOverflowButtonShown();
    495  await doc.getElementById("nav-bar").overflowable.show();
    496  info("Menu panel was opened");
    497 
    498  let historyButton = doc.getElementById("history-panelmenu");
    499  Assert.ok(historyButton, "History button appears in Panel Menu");
    500 
    501  historyButton.click();
    502 
    503  let historyPanel = doc.getElementById("PanelUI-history");
    504  return BrowserTestUtils.waitForEvent(historyPanel, "ViewShown");
    505 }
    506 
    507 /**
    508 * Closes the history panel and returns a promise that resolves as sooon
    509 * as the panel is closed.
    510 */
    511 async function hideHistoryPanel(doc = document) {
    512  let historyView = doc.getElementById("PanelUI-history");
    513  let historyPanel = historyView.closest("panel");
    514  let promise = BrowserTestUtils.waitForEvent(historyPanel, "popuphidden");
    515  historyPanel.hidePopup();
    516  return promise;
    517 }
    518 
    519 /**
    520 * After bug 1960002, setting the window to its min-width is no longer enough
    521 * to trigger navbar overflow. So, to keep the overflow tests working, we need
    522 * to both change the window width and add more buttons to the navbar.
    523 *
    524 * Note this helper registers a cleanup function that undoes changes to the
    525 * supplied window's width and resets its toolbar state. Be sure to call this
    526 * helper before other cleanup functions that might assert on the state of the
    527 * window after it is reset.
    528 *
    529 * Set the shouldCleanup param to false if you don't need to register another
    530 * end-of-test cleanup function, for instance, if your test calls
    531 * `CustomizableUI.reset()` in the middle of asserts that rely on overflow.
    532 *
    533 * Returns the original window width in case a test needs to resize the window
    534 * before the cleanup function runs.
    535 */
    536 function ensureToolbarOverflow(aWindow, shouldCleanup = true) {
    537  const originalWindowWidth = aWindow.outerWidth;
    538 
    539  aWindow.resizeTo(kForceOverflowWidthPx, aWindow.outerHeight);
    540  CustomizableUI.addWidgetToArea(
    541    "history-panelmenu",
    542    CustomizableUI.AREA_NAVBAR,
    543    0
    544  );
    545  CustomizableUI.addWidgetToArea(
    546    "email-link-button",
    547    CustomizableUI.AREA_NAVBAR,
    548    0
    549  );
    550  CustomizableUI.addWidgetToArea("panic-button", CustomizableUI.AREA_NAVBAR, 0);
    551 
    552  if (shouldCleanup) {
    553    registerCleanupFunction(() => {
    554      unensureToolbarOverflow(aWindow, originalWindowWidth);
    555    });
    556  }
    557 
    558  return originalWindowWidth;
    559 }
    560 
    561 /**
    562 * Helper function that undoes what `ensureToolbarOverflow` does.
    563 */
    564 function unensureToolbarOverflow(aWindow, originalWindowWidth) {
    565  if (originalWindowWidth) {
    566    aWindow.resizeTo(originalWindowWidth, aWindow.outerHeight);
    567  }
    568  CustomizableUI.removeWidgetFromArea("history-panelmenu");
    569  CustomizableUI.removeWidgetFromArea("email-link-button");
    570  CustomizableUI.removeWidgetFromArea("panic-button");
    571 }