tor-browser

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

browser_popupNotification_security_delay.js (15916B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const TEST_SECURITY_DELAY = 5000;
      7 
      8 SimpleTest.requestCompleteLog();
      9 
     10 /**
     11 * Shows a test PopupNotification.
     12 */
     13 function showNotification() {
     14  PopupNotifications.show(
     15    gBrowser.selectedBrowser,
     16    "foo",
     17    "Hello, World!",
     18    "default-notification-icon",
     19    {
     20      label: "ok",
     21      accessKey: "o",
     22      callback: () => {},
     23    },
     24    [
     25      {
     26        label: "cancel",
     27        accessKey: "c",
     28        callback: () => {},
     29      },
     30    ],
     31    {
     32      // Make test notifications persistent to ensure they are only closed
     33      // explicitly by test actions and survive tab switches.
     34      persistent: true,
     35    }
     36  );
     37 }
     38 
     39 add_setup(async function () {
     40  // Set a longer security delay for PopupNotification actions so we can test
     41  // the delay even if the test runs slowly.
     42  await SpecialPowers.pushPrefEnv({
     43    set: [
     44      ["test.wait300msAfterTabSwitch", true],
     45      ["security.notification_enable_delay", TEST_SECURITY_DELAY],
     46    ],
     47  });
     48 });
     49 
     50 /**
     51 * Test helper for security delay tests which performs the following steps:
     52 * 1. Shows a test notification.
     53 * 2. Waits for the notification panel to be shown and checks that the initial
     54 *    security delay blocks clicks.
     55 * 3. Waits for the security delay to expire. Clicks should now be allowed.
     56 * 4. Executes the provided onSecurityDelayExpired function. This function
     57 *    should renew the security delay.
     58 * 5. Tests that the security delay was renewed.
     59 * 6. Ensures that after the security delay the notification can be closed.
     60 *
     61 * @param {*} options
     62 * @param {function} options.onSecurityDelayExpired - Function to run after the
     63 *  security delay has expired. This function should trigger a renew of the
     64 *  security delay.
     65 * @param {function} options.cleanupFn - Optional cleanup function to run after
     66 * the test has completed.
     67 * @returns {Promise<void>} - Resolves when the test has completed.
     68 */
     69 async function runPopupNotificationSecurityDelayTest({
     70  onSecurityDelayExpired,
     71  cleanupFn = () => {},
     72 }) {
     73  await ensureSecurityDelayReady();
     74 
     75  info("Open a notification.");
     76  let popupShownPromise = waitForNotificationPanel();
     77  showNotification();
     78  await popupShownPromise;
     79  ok(
     80    PopupNotifications.isPanelOpen,
     81    "PopupNotification should be open after show call."
     82  );
     83 
     84  // Test that the initial security delay works.
     85  info(
     86    "Trigger main action via button click during the initial security delay."
     87  );
     88  triggerMainCommand(PopupNotifications.panel);
     89 
     90  await new Promise(resolve => setTimeout(resolve, 0));
     91 
     92  ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
     93  let notification = PopupNotifications.getNotification(
     94    "foo",
     95    gBrowser.selectedBrowser
     96  );
     97  ok(
     98    notification,
     99    "Notification should still be open because we clicked during the security delay."
    100  );
    101  // If the notification is no longer shown (test failure) skip the remaining
    102  // checks.
    103  if (!notification) {
    104    await cleanupFn();
    105    return;
    106  }
    107 
    108  info("Wait for security delay to expire.");
    109  await new Promise(resolve =>
    110    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    111    setTimeout(resolve, TEST_SECURITY_DELAY + 500)
    112  );
    113 
    114  info("Run test specific actions which restarts the security delay.");
    115  await onSecurityDelayExpired();
    116 
    117  info("Trigger main action via button click during the new security delay.");
    118  triggerMainCommand(PopupNotifications.panel);
    119 
    120  await new Promise(resolve => setTimeout(resolve, 0));
    121 
    122  ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
    123  notification = PopupNotifications.getNotification(
    124    "foo",
    125    gBrowser.selectedBrowser
    126  );
    127  ok(
    128    notification,
    129    "Notification should still be open because we clicked during the security delay."
    130  );
    131  // If the notification is no longer shown (test failure) skip the remaining
    132  // checks.
    133  if (!notification) {
    134    await cleanupFn();
    135    return;
    136  }
    137 
    138  // Ensure that once the security delay has passed the notification can be
    139  // closed again.
    140  let fakeTimeShown = TEST_SECURITY_DELAY + 500;
    141  info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
    142  notification.timeShown = performance.now() - fakeTimeShown;
    143 
    144  info("Trigger main action via button click outside security delay");
    145  let notificationHiddenPromise = waitForNotificationPanelHidden();
    146  triggerMainCommand(PopupNotifications.panel);
    147 
    148  info("Wait for panel to be hidden.");
    149  await notificationHiddenPromise;
    150 
    151  ok(
    152    !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
    153    "Should no longer see the notification."
    154  );
    155 
    156  info("Cleanup.");
    157  await cleanupFn();
    158 }
    159 
    160 /**
    161 * Tests that when we show a second notification while the panel is open the
    162 * timeShown attribute is correctly set and the security delay is enforced
    163 * properly.
    164 */
    165 add_task(async function test_timeShownMultipleNotifications() {
    166  await ensureSecurityDelayReady();
    167 
    168  ok(
    169    !PopupNotifications.isPanelOpen,
    170    "PopupNotification panel should not be open initially."
    171  );
    172 
    173  info("Open the first notification.");
    174  let popupShownPromise = waitForNotificationPanel();
    175  showNotification();
    176  await popupShownPromise;
    177  ok(
    178    PopupNotifications.isPanelOpen,
    179    "PopupNotification should be open after first show call."
    180  );
    181 
    182  is(
    183    PopupNotifications._currentNotifications.length,
    184    1,
    185    "There should only be one notification"
    186  );
    187 
    188  let notification = PopupNotifications.getNotification(
    189    "foo",
    190    gBrowser.selectedBrowser
    191  );
    192  is(notification?.id, "foo", "There should be a notification with id foo");
    193  ok(notification.timeShown, "The notification should have timeShown set");
    194 
    195  info(
    196    "Call show again with the same notification id while the PopupNotification panel is still open."
    197  );
    198  showNotification();
    199  ok(
    200    PopupNotifications.isPanelOpen,
    201    "PopupNotification should still open after second show call."
    202  );
    203  notification = PopupNotifications.getNotification(
    204    "foo",
    205    gBrowser.selectedBrowser
    206  );
    207  is(
    208    PopupNotifications._currentNotifications.length,
    209    1,
    210    "There should still only be one notification"
    211  );
    212 
    213  is(
    214    notification?.id,
    215    "foo",
    216    "There should still be a notification with id foo"
    217  );
    218  ok(notification.timeShown, "The notification should have timeShown set");
    219 
    220  let notificationHiddenPromise = waitForNotificationPanelHidden();
    221 
    222  info("Trigger main action via button click during security delay");
    223 
    224  // Wait for a tick of the event loop to ensure the button we're clicking
    225  // has been slotted into moz-button-group
    226  await new Promise(resolve => setTimeout(resolve, 0));
    227 
    228  triggerMainCommand(PopupNotifications.panel);
    229 
    230  await new Promise(resolve => setTimeout(resolve, 0));
    231 
    232  ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
    233  notification = PopupNotifications.getNotification(
    234    "foo",
    235    gBrowser.selectedBrowser
    236  );
    237  ok(
    238    notification,
    239    "Notification should still be open because we clicked during the security delay."
    240  );
    241 
    242  // If the notification is no longer shown (test failure) skip the remaining
    243  // checks.
    244  if (!notification) {
    245    return;
    246  }
    247 
    248  // Ensure that once the security delay has passed the notification can be
    249  // closed again.
    250  let fakeTimeShown = TEST_SECURITY_DELAY + 500;
    251  info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
    252  notification.timeShown = performance.now() - fakeTimeShown;
    253 
    254  info("Trigger main action via button click outside security delay");
    255  triggerMainCommand(PopupNotifications.panel);
    256 
    257  info("Wait for panel to be hidden.");
    258  await notificationHiddenPromise;
    259 
    260  ok(
    261    !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
    262    "Should no longer see the notification."
    263  );
    264 });
    265 
    266 /**
    267 * Tests that when we reshow a notification after a tab switch the timeShown
    268 * attribute is correctly reset and the security delay is enforced.
    269 */
    270 add_task(async function test_notificationReshowTabSwitch() {
    271  await runPopupNotificationSecurityDelayTest({
    272    onSecurityDelayExpired: async () => {
    273      let panelHiddenPromise = waitForNotificationPanelHidden();
    274      let panelShownPromise;
    275 
    276      info("Open a new tab which hides the notification panel.");
    277      await BrowserTestUtils.withNewTab("https://example.com", async () => {
    278        info("Wait for panel to be hidden by tab switch.");
    279        await panelHiddenPromise;
    280        panelShownPromise = waitForNotificationPanel();
    281      });
    282      info(
    283        "Wait for the panel to show again after the tab close. We're showing the original tab again."
    284      );
    285      await panelShownPromise;
    286 
    287      ok(
    288        PopupNotifications.isPanelOpen,
    289        "PopupNotification should be shown after tab close."
    290      );
    291      let notification = PopupNotifications.getNotification(
    292        "foo",
    293        gBrowser.selectedBrowser
    294      );
    295      is(
    296        notification?.id,
    297        "foo",
    298        "There should still be a notification with id foo"
    299      );
    300 
    301      info(
    302        "Because we re-show the panel after tab close / switch the security delay should have reset."
    303      );
    304    },
    305  });
    306 });
    307 
    308 /**
    309 * Tests that the security delay gets reset when a window is repositioned and
    310 * the PopupNotifications panel position is updated.
    311 */
    312 add_task(async function test_notificationWindowMove() {
    313  let screenX, screenY;
    314 
    315  await runPopupNotificationSecurityDelayTest({
    316    onSecurityDelayExpired: async () => {
    317      info("Reposition the window");
    318      // Remember original window position.
    319      screenX = window.screenX;
    320      screenY = window.screenY;
    321 
    322      let promisePopupPositioned = BrowserTestUtils.waitForEvent(
    323        PopupNotifications.panel,
    324        "popuppositioned"
    325      );
    326 
    327      // Move the window.
    328      window.moveTo(200, 200);
    329 
    330      // Wait for the panel to reposition and the PopupNotifications listener to run.
    331      await promisePopupPositioned;
    332      await new Promise(resolve => setTimeout(resolve, 0));
    333    },
    334    cleanupFn: async () => {
    335      // Reset window position
    336      window.moveTo(screenX, screenY);
    337    },
    338  });
    339 });
    340 
    341 /**
    342 * Tests that the security delay gets extended if a notification is shown during
    343 * a full screen transition.
    344 */
    345 add_task(async function test_notificationDuringFullScreenTransition() {
    346  // Log full screen transition messages.
    347  let loggingObserver = {
    348    observe(subject, topic) {
    349      info("Observed topic: " + topic);
    350    },
    351  };
    352  Services.obs.addObserver(loggingObserver, "fullscreen-transition-start");
    353  Services.obs.addObserver(loggingObserver, "fullscreen-transition-end");
    354  // Unregister observers when the test ends:
    355  registerCleanupFunction(() => {
    356    Services.obs.removeObserver(loggingObserver, "fullscreen-transition-start");
    357    Services.obs.removeObserver(loggingObserver, "fullscreen-transition-end");
    358  });
    359 
    360  if (Services.appinfo.OS == "Linux") {
    361    ok(
    362      "Skipping test on Linux because of disabled full screen transition in CI."
    363    );
    364    return;
    365  }
    366  // Bug 1882527: Intermittent failures on macOS.
    367  if (Services.appinfo.OS == "Darwin") {
    368    ok("Skipping test on macOS because of intermittent failures.");
    369    return;
    370  }
    371 
    372  await BrowserTestUtils.withNewTab("https://example.com", async browser => {
    373    await SpecialPowers.pushPrefEnv({
    374      set: [
    375        // Set a short security delay so we can observe it being extended.
    376        ["security.notification_enable_delay", 1],
    377        // Set a longer full screen exit transition so the test works on slow builds.
    378        ["full-screen-api.transition-duration.leave", "1000 1000"],
    379        // Waive the user activation requirement for full screen requests.
    380        // The PoC this test is based on relies on spam clicking which grants
    381        // user activation in the popup that requests full screen.
    382        // This isn't reliable in automation.
    383        ["full-screen-api.allow-trusted-requests-only", false],
    384        // macOS native full screen is not affected by the full screen
    385        // transition overlap. Test with the old full screen implementation.
    386        ["full-screen-api.macos-native-full-screen", false],
    387      ],
    388    });
    389 
    390    await ensureSecurityDelayReady();
    391 
    392    ok(
    393      !PopupNotifications.isPanelOpen,
    394      "PopupNotification panel should not be open initially."
    395    );
    396 
    397    info("Open a notification.");
    398    let popupShownPromise = waitForNotificationPanel();
    399    showNotification();
    400    await popupShownPromise;
    401    ok(
    402      PopupNotifications.isPanelOpen,
    403      "PopupNotification should be open after show call."
    404    );
    405 
    406    let notification = PopupNotifications.getNotification("foo", browser);
    407    is(notification?.id, "foo", "There should be a notification with id foo");
    408 
    409    info(
    410      "Open a new tab via window.open, enter full screen and remove the tab."
    411    );
    412 
    413    // There are two transitions, one for full screen entry and one for full screen exit.
    414    let transitionStartCount = 0;
    415    let transitionEndCount = 0;
    416    let promiseFullScreenTransitionStart = TestUtils.topicObserved(
    417      "fullscreen-transition-start",
    418      () => {
    419        transitionStartCount++;
    420        return transitionStartCount == 2;
    421      }
    422    );
    423    let promiseFullScreenTransitionEnd = TestUtils.topicObserved(
    424      "fullscreen-transition-end",
    425      () => {
    426        transitionEndCount++;
    427        return transitionEndCount == 2;
    428      }
    429    );
    430    let notificationShownPromise = waitForNotificationPanel();
    431 
    432    await SpecialPowers.spawn(browser, [], () => {
    433      // Use eval to execute in the privilege context of the website.
    434      content.eval(`
    435           let button = document.createElement("button");
    436           button.id = "triggerBtn";
    437           button.innerText = "Open Popup";
    438           button.addEventListener("click", () => {
    439             let popup = window.open("about:blank");
    440             popup.document.write(
    441               "<script>setTimeout(() => document.documentElement.requestFullscreen(), 500)</script>"
    442             );
    443             popup.document.write(
    444               "<script>setTimeout(() => window.close(), 1500)</script>"
    445             );
    446           });
    447           // Insert button at the top so the synthesized click works. Otherwise
    448           // the button may be outside of the viewport.
    449           document.body.prepend(button);
    450         `);
    451    });
    452 
    453    let timeClick = performance.now();
    454    await BrowserTestUtils.synthesizeMouseAtCenter("#triggerBtn", {}, browser);
    455 
    456    info("Wait for the exit transition to start. It's the second transition.");
    457    await promiseFullScreenTransitionStart;
    458    info("Full screen transition start");
    459    ok(true, "Full screen transition started");
    460    ok(
    461      window.isInFullScreenTransition,
    462      "Full screen transition is still running."
    463    );
    464 
    465    info(
    466      "Wait for notification to re-show on tab switch, after the popup has been closed"
    467    );
    468    await notificationShownPromise;
    469    ok(
    470      window.isInFullScreenTransition,
    471      "Full screen transition is still running."
    472    );
    473    info(
    474      "about to trigger notification. time between btn click and notification show: " +
    475        (performance.now() - timeClick)
    476    );
    477 
    478    info(
    479      "Trigger main action via button click during the extended security delay."
    480    );
    481    triggerMainCommand(PopupNotifications.panel);
    482 
    483    await new Promise(resolve => setTimeout(resolve, 0));
    484 
    485    ok(
    486      PopupNotifications.isPanelOpen,
    487      "PopupNotification should still be open."
    488    );
    489    notification = PopupNotifications.getNotification(
    490      "foo",
    491      gBrowser.selectedBrowser
    492    );
    493    ok(
    494      notification,
    495      "Notification should still be open because we clicked during the security delay."
    496    );
    497 
    498    info("Wait for full screen transition end.");
    499    await promiseFullScreenTransitionEnd;
    500    info("Full screen transition end");
    501 
    502    await SpecialPowers.popPrefEnv();
    503  });
    504 });