tor-browser

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

head.js (11270B)


      1 ChromeUtils.defineESModuleGetters(this, {
      2  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
      3 });
      4 
      5 /**
      6 * Called after opening a new window or switching windows, this will wait until
      7 * we are sure that an attempt to display a notification will not fail.
      8 */
      9 async function waitForWindowReadyForPopupNotifications(win) {
     10  // These are the same checks that PopupNotifications.sys.mjs makes before it
     11  // allows a notification to open.
     12  await TestUtils.waitForCondition(
     13    () => win.gBrowser.selectedBrowser.docShellIsActive,
     14    "The browser should be active"
     15  );
     16  await TestUtils.waitForCondition(
     17    () => Services.focus.activeWindow == win,
     18    "The window should be active"
     19  );
     20 }
     21 
     22 // Tests that call setup() should have a `tests` array defined for the actual
     23 // tests to be run.
     24 /* global tests */
     25 function setup() {
     26  // eslint-disable-next-line @microsoft/sdl/no-insecure-url
     27  BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/").then(
     28    goNext
     29  );
     30  registerCleanupFunction(() => {
     31    gBrowser.removeTab(gBrowser.selectedTab);
     32  });
     33 }
     34 
     35 function goNext() {
     36  executeSoon(() => executeSoon(runNextTest));
     37 }
     38 
     39 async function runNextTest() {
     40  if (!tests.length) {
     41    executeSoon(finish);
     42    return;
     43  }
     44 
     45  let nextTest = tests.shift();
     46  if (nextTest.onShown) {
     47    let shownState = false;
     48    onPopupEvent("popupshowing", function () {
     49      info("[" + nextTest.id + "] popup showing");
     50    });
     51    onPopupEvent("popupshown", function () {
     52      shownState = true;
     53      info("[" + nextTest.id + "] popup shown");
     54      (nextTest.onShown(this) || Promise.resolve()).then(undefined, ex =>
     55        Assert.ok(false, "onShown failed: " + ex)
     56      );
     57    });
     58    onPopupEvent(
     59      "popuphidden",
     60      function () {
     61        info("[" + nextTest.id + "] popup hidden");
     62        (nextTest.onHidden(this) || Promise.resolve()).then(
     63          () => goNext(),
     64          ex => Assert.ok(false, "onHidden failed: " + ex)
     65        );
     66      },
     67      () => shownState
     68    );
     69    info(
     70      "[" +
     71        nextTest.id +
     72        "] added listeners; panel is open: " +
     73        PopupNotifications.isPanelOpen
     74    );
     75  }
     76 
     77  info("[" + nextTest.id + "] running test");
     78  await nextTest.run();
     79 }
     80 
     81 function showNotification(notifyObj) {
     82  info("Showing notification " + notifyObj.id);
     83  return PopupNotifications.show(
     84    notifyObj.browser,
     85    notifyObj.id,
     86    notifyObj.message,
     87    notifyObj.anchorID,
     88    notifyObj.mainAction,
     89    notifyObj.secondaryActions,
     90    notifyObj.options
     91  );
     92 }
     93 
     94 function dismissNotification(popup) {
     95  info("Dismissing notification " + popup.childNodes[0].id);
     96  executeSoon(() => EventUtils.synthesizeKey("KEY_Escape"));
     97 }
     98 
     99 function BasicNotification(testId) {
    100  this.browser = gBrowser.selectedBrowser;
    101  this.id = "test-notification-" + testId;
    102  this.message = testId + ": Will you allow <> to perform this action?";
    103  this.anchorID = null;
    104  this.mainAction = {
    105    label: "Main Action",
    106    accessKey: "M",
    107    callback: ({ source }) => {
    108      this.mainActionClicked = true;
    109      this.mainActionSource = source;
    110    },
    111  };
    112  this.secondaryActions = [
    113    {
    114      label: "Secondary Action",
    115      accessKey: "S",
    116      callback: ({ source }) => {
    117        this.secondaryActionClicked = true;
    118        this.secondaryActionSource = source;
    119      },
    120    },
    121  ];
    122  this.options = {
    123    // eslint-disable-next-line @microsoft/sdl/no-insecure-url
    124    name: "http://example.com",
    125    eventCallback: eventName => {
    126      switch (eventName) {
    127        case "dismissed":
    128          this.dismissalCallbackTriggered = true;
    129          break;
    130        case "showing":
    131          this.showingCallbackTriggered = true;
    132          break;
    133        case "shown":
    134          this.shownCallbackTriggered = true;
    135          break;
    136        case "removed":
    137          this.removedCallbackTriggered = true;
    138          break;
    139        case "swapping":
    140          this.swappingCallbackTriggered = true;
    141          break;
    142      }
    143    },
    144  };
    145 }
    146 
    147 BasicNotification.prototype.addOptions = function (options) {
    148  for (let [name, value] of Object.entries(options)) {
    149    this.options[name] = value;
    150  }
    151 };
    152 
    153 function ErrorNotification(testId) {
    154  BasicNotification.call(this, testId);
    155  this.mainAction.callback = () => {
    156    this.mainActionClicked = true;
    157    throw new Error("Oops!");
    158  };
    159  this.secondaryActions[0].callback = () => {
    160    this.secondaryActionClicked = true;
    161    throw new Error("Oops!");
    162  };
    163 }
    164 
    165 ErrorNotification.prototype = BasicNotification.prototype;
    166 
    167 function checkPopup(popup, notifyObj) {
    168  info("Checking notification " + notifyObj.id);
    169 
    170  ok(notifyObj.showingCallbackTriggered, "showing callback was triggered");
    171  ok(notifyObj.shownCallbackTriggered, "shown callback was triggered");
    172 
    173  let notifications = popup.childNodes;
    174  is(notifications.length, 1, "one notification displayed");
    175  let notification = notifications[0];
    176  if (!notification) {
    177    return;
    178  }
    179 
    180  // PopupNotifications are not expected to show icons
    181  // unless popupIconURL or popupIconClass is passed in the options object.
    182  if (notifyObj.options.popupIconURL || notifyObj.options.popupIconClass) {
    183    let icon = notification.querySelector(".popup-notification-icon");
    184    if (notifyObj.id == "geolocation") {
    185      isnot(icon.getBoundingClientRect().width, 0, "icon for geo displayed");
    186      ok(
    187        popup.anchorNode.classList.contains("notification-anchor-icon"),
    188        "notification anchored to icon"
    189      );
    190    }
    191  }
    192 
    193  let description = notifyObj.message.split("<>");
    194  let text = {};
    195  text.start = description[0];
    196  text.end = description[1];
    197  is(notification.getAttribute("label"), text.start, "message matches");
    198  is(
    199    notification.getAttribute("name"),
    200    notifyObj.options.name,
    201    "message matches"
    202  );
    203  is(notification.getAttribute("endlabel"), text.end, "message matches");
    204 
    205  is(notification.id, notifyObj.id + "-notification", "id matches");
    206  if (notifyObj.mainAction) {
    207    is(
    208      notification.getAttribute("buttonlabel"),
    209      notifyObj.mainAction.label,
    210      "main action label matches"
    211    );
    212    is(
    213      notification.getAttribute("buttonaccesskey"),
    214      notifyObj.mainAction.accessKey,
    215      "main action accesskey matches"
    216    );
    217  }
    218  if (notifyObj.secondaryActions && notifyObj.secondaryActions.length) {
    219    let secondaryAction = notifyObj.secondaryActions[0];
    220    is(
    221      notification.getAttribute("secondarybuttonlabel"),
    222      secondaryAction.label,
    223      "secondary action label matches"
    224    );
    225    is(
    226      notification.getAttribute("secondarybuttonaccesskey"),
    227      secondaryAction.accessKey,
    228      "secondary action accesskey matches"
    229    );
    230  }
    231  // Additional secondary actions appear as menu items.
    232  let actualExtraSecondaryActions = Array.prototype.filter.call(
    233    notification.menupopup.childNodes,
    234    child => child.nodeName == "menuitem"
    235  );
    236  let extraSecondaryActions = notifyObj.secondaryActions
    237    ? notifyObj.secondaryActions.slice(1)
    238    : [];
    239  is(
    240    actualExtraSecondaryActions.length,
    241    extraSecondaryActions.length,
    242    "number of extra secondary actions matches"
    243  );
    244  extraSecondaryActions.forEach(function (a, i) {
    245    is(
    246      actualExtraSecondaryActions[i].getAttribute("label"),
    247      a.label,
    248      "label for extra secondary action " + i + " matches"
    249    );
    250    is(
    251      actualExtraSecondaryActions[i].getAttribute("accesskey"),
    252      a.accessKey,
    253      "accessKey for extra secondary action " + i + " matches"
    254    );
    255  });
    256 }
    257 
    258 ChromeUtils.defineLazyGetter(this, "gActiveListeners", () => {
    259  let listeners = new Map();
    260  registerCleanupFunction(() => {
    261    for (let [listener, eventName] of listeners) {
    262      PopupNotifications.panel.removeEventListener(eventName, listener);
    263    }
    264  });
    265  return listeners;
    266 });
    267 
    268 function onPopupEvent(eventName, callback, condition) {
    269  let listener = event => {
    270    if (
    271      event.target != PopupNotifications.panel ||
    272      (condition && !condition())
    273    ) {
    274      return;
    275    }
    276    PopupNotifications.panel.removeEventListener(eventName, listener);
    277    gActiveListeners.delete(listener);
    278    executeSoon(() => callback.call(PopupNotifications.panel));
    279  };
    280  gActiveListeners.set(listener, eventName);
    281  PopupNotifications.panel.addEventListener(eventName, listener);
    282 }
    283 
    284 function waitForNotificationPanel() {
    285  return new Promise(resolve => {
    286    onPopupEvent("popupshown", function () {
    287      resolve(this);
    288    });
    289  });
    290 }
    291 
    292 function waitForNotificationPanelHidden() {
    293  return new Promise(resolve => {
    294    onPopupEvent("popuphidden", function () {
    295      resolve(this);
    296    });
    297  });
    298 }
    299 
    300 function triggerMainCommand(popup) {
    301  let notifications = popup.childNodes;
    302  ok(!!notifications.length, "at least one notification displayed");
    303  let notification = notifications[0];
    304  info("Triggering main command for notification " + notification.id);
    305  EventUtils.synthesizeMouseAtCenter(notification.button, {});
    306 }
    307 
    308 function triggerSecondaryCommand(popup, index) {
    309  let notifications = popup.childNodes;
    310  ok(!!notifications.length, "at least one notification displayed");
    311  let notification = notifications[0];
    312  info("Triggering secondary command for notification " + notification.id);
    313 
    314  if (index == 0) {
    315    EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
    316    return;
    317  }
    318 
    319  // Extra secondary actions appear in a menu.
    320  notification.secondaryButton.nextElementSibling.focus();
    321 
    322  popup.addEventListener(
    323    "popupshown",
    324    function () {
    325      info("Command popup open for notification " + notification.id);
    326      // Press down until the desired command is selected. Decrease index by one
    327      // since the secondary action was handled above.
    328      for (let i = 0; i <= index - 1; i++) {
    329        EventUtils.synthesizeKey("KEY_ArrowDown");
    330      }
    331      // Activate
    332      EventUtils.synthesizeKey("KEY_Enter");
    333    },
    334    { once: true }
    335  );
    336 
    337  // One down event to open the popup
    338  info(
    339    "Open the popup to trigger secondary command for notification " +
    340      notification.id
    341  );
    342  EventUtils.synthesizeKey("KEY_ArrowDown", {
    343    altKey: !navigator.platform.includes("Mac"),
    344  });
    345 }
    346 
    347 /**
    348 * The security delay calculation in PopupNotification.sys.mjs is dependent on
    349 * the monotonically increasing value of ChromeUtils.now. This timestamp is
    350 * not relative to a fixed date, but to runtime.
    351 * We need to wait for the value ChromeUtils.now() to be larger than the
    352 * security delay in order to observe the bug. Only then does the
    353 * timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
    354 * value that is unconditionally greater than lazy.buttonDelay for
    355 * notification.timeShown = null = 0.
    356 * See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
    357 *
    358 * When running in automation as part of a larger test suite ChromeUtils.now()
    359 * should usually be already sufficiently high in which case this check should
    360 * directly resolve.
    361 */
    362 async function ensureSecurityDelayReady(timeNewWindowOpened = 0) {
    363  let secDelay = Services.prefs.getIntPref(
    364    "security.notification_enable_delay"
    365  );
    366 
    367  await TestUtils.waitForCondition(
    368    () => ChromeUtils.now() - timeNewWindowOpened > secDelay,
    369    "Wait for performance.now() > SECURITY_DELAY",
    370    500,
    371    50
    372  );
    373 }