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 }