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 });