/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const TEST_SECURITY_DELAY = 5000; /** * Shows a test PopupNotification. */ function showNotification() { PopupNotifications.show( gBrowser.selectedBrowser, "foo", "Hello, World!", "default-notification-icon", { label: "ok", accessKey: "o", callback: () => {}, }, [ { label: "cancel", accessKey: "c", callback: () => {}, }, ], { // Make test notifications persistent to ensure they are only closed // explicitly by test actions and survive tab switches. persistent: true, } ); } add_setup(async function () { // Set a longer security delay for PopupNotification actions so we can test // the delay even if the test runs slowly. await SpecialPowers.pushPrefEnv({ set: [["security.notification_enable_delay", TEST_SECURITY_DELAY]], }); }); async function ensureSecurityDelayReady() { /** * The security delay calculation in PopupNotification.sys.mjs is dependent on * the monotonically increasing value of performance.now. This timestamp is * not relative to a fixed date, but to runtime. * We need to wait for the value performance.now() to be larger than the * security delay in order to observe the bug. Only then does the * timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown * value that is unconditionally greater than lazy.buttonDelay for * notification.timeShown = null = 0. * See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872 * * When running in automation as part of a larger test suite performance.now() * should usually be already sufficiently high in which case this check should * directly resolve. */ await TestUtils.waitForCondition( () => performance.now() > TEST_SECURITY_DELAY, "Wait for performance.now() > SECURITY_DELAY", 500, 50 ); } /** * Tests that when we show a second notification while the panel is open the * timeShown attribute is correctly set and the security delay is enforced * properly. */ add_task(async function test_timeShownMultipleNotifications() { await ensureSecurityDelayReady(); ok( !PopupNotifications.isPanelOpen, "PopupNotification panel should not be open initially." ); info("Open the first notification."); let popupShownPromise = waitForNotificationPanel(); showNotification(); await popupShownPromise; ok( PopupNotifications.isPanelOpen, "PopupNotification should be open after first show call." ); is( PopupNotifications._currentNotifications.length, 1, "There should only be one notification" ); let notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); is(notification?.id, "foo", "There should be a notification with id foo"); ok(notification.timeShown, "The notification should have timeShown set"); info( "Call show again with the same notification id while the PopupNotification panel is still open." ); showNotification(); ok( PopupNotifications.isPanelOpen, "PopupNotification should still open after second show call." ); notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); is( PopupNotifications._currentNotifications.length, 1, "There should still only be one notification" ); is( notification?.id, "foo", "There should still be a notification with id foo" ); ok(notification.timeShown, "The notification should have timeShown set"); let notificationHiddenPromise = waitForNotificationPanelHidden(); info("Trigger main action via button click during security delay"); triggerMainCommand(PopupNotifications.panel); await new Promise(resolve => setTimeout(resolve, 0)); ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open."); notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); ok( notification, "Notification should still be open because we clicked during the security delay." ); // If the notification is no longer shown (test failure) skip the remaining // checks. if (!notification) { return; } // Ensure that once the security delay has passed the notification can be // closed again. let fakeTimeShown = TEST_SECURITY_DELAY + 500; info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`); notification.timeShown = performance.now() - fakeTimeShown; info("Trigger main action via button click outside security delay"); triggerMainCommand(PopupNotifications.panel); info("Wait for panel to be hidden."); await notificationHiddenPromise; ok( !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser), "Should not longer see the notification." ); }); /** * Tests that when we reshow a notification after a tab switch the timeShown * attribute is correctly reset and the security delay is enforced. */ add_task(async function test_notificationReshowTabSwitch() { await ensureSecurityDelayReady(); ok( !PopupNotifications.isPanelOpen, "PopupNotification panel should not be open initially." ); info("Open the first notification."); let popupShownPromise = waitForNotificationPanel(); showNotification(); await popupShownPromise; ok( PopupNotifications.isPanelOpen, "PopupNotification should be open after first show call." ); let notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); is(notification?.id, "foo", "There should be a notification with id foo"); ok(notification.timeShown, "The notification should have timeShown set"); info("Trigger main action via button click during security delay"); triggerMainCommand(PopupNotifications.panel); await new Promise(resolve => setTimeout(resolve, 0)); ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open."); notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); ok( notification, "Notification should still be open because we clicked during the security delay." ); // If the notification is no longer shown (test failure) skip the remaining // checks. if (!notification) { return; } let panelHiddenPromise = waitForNotificationPanelHidden(); let panelShownPromise; info("Open a new tab which hides the notification panel."); await BrowserTestUtils.withNewTab("https://example.com", async browser => { info("Wait for panel to be hidden by tab switch."); await panelHiddenPromise; info( "Keep the tab open until the security delay for the original notification show has expired." ); await new Promise(resolve => // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(resolve, TEST_SECURITY_DELAY + 500) ); panelShownPromise = waitForNotificationPanel(); }); info( "Wait for the panel to show again after the tab close. We're showing the original tab again." ); await panelShownPromise; ok( PopupNotifications.isPanelOpen, "PopupNotification should be shown after tab close." ); notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); is( notification?.id, "foo", "There should still be a notification with id foo" ); let notificationHiddenPromise = waitForNotificationPanelHidden(); info( "Because we re-show the panel after tab close / switch the security delay should have reset." ); info("Trigger main action via button click during the new security delay."); triggerMainCommand(PopupNotifications.panel); await new Promise(resolve => setTimeout(resolve, 0)); ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open."); notification = PopupNotifications.getNotification( "foo", gBrowser.selectedBrowser ); ok( notification, "Notification should still be open because we clicked during the security delay." ); // If the notification is no longer shown (test failure) skip the remaining // checks. if (!notification) { return; } // Ensure that once the security delay has passed the notification can be // closed again. let fakeTimeShown = TEST_SECURITY_DELAY + 500; info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`); notification.timeShown = performance.now() - fakeTimeShown; info("Trigger main action via button click outside security delay"); triggerMainCommand(PopupNotifications.panel); info("Wait for panel to be hidden."); await notificationHiddenPromise; ok( !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser), "Should not longer see the notification." ); });