head.js (17573B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 ChromeUtils.defineESModuleGetters(this, { 8 CustomizableUI: 9 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 10 CustomizableUITestUtils: 11 "resource://testing-common/CustomizableUITestUtils.sys.mjs", 12 }); 13 14 /** 15 * Instance of CustomizableUITestUtils for the current browser window. 16 */ 17 var gCUITestUtils = new CustomizableUITestUtils(window); 18 19 Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true); 20 registerCleanupFunction(() => 21 Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck") 22 ); 23 24 var { synthesizeDrop, synthesizeMouseAtCenter } = EventUtils; 25 26 // As of bug 1960002, this width no longer technically forces overflow. 27 // Instead, use `ensureToolbarOverflow()` below. 28 const kForceOverflowWidthPx = 500; 29 30 function createDummyXULButton(id, label, win = window) { 31 let btn = win.document.createXULElement("toolbarbutton"); 32 btn.id = id; 33 btn.setAttribute("label", label || id); 34 btn.className = "toolbarbutton-1 chromeclass-toolbar-additional"; 35 win.gNavToolbox.palette.appendChild(btn); 36 return btn; 37 } 38 39 var gAddedToolbars = new Set(); 40 41 function createToolbarWithPlacements(id, placements = [], properties = {}) { 42 gAddedToolbars.add(id); 43 let tb = document.createXULElement("toolbar"); 44 tb.id = id; 45 tb.setAttribute("customizable", "true"); 46 47 properties.type = CustomizableUI.TYPE_TOOLBAR; 48 properties.defaultPlacements = placements; 49 CustomizableUI.registerArea(id, properties); 50 gNavToolbox.appendChild(tb); 51 CustomizableUI.registerToolbarNode(tb); 52 return tb; 53 } 54 55 function createOverflowableToolbarWithPlacements(id, placements) { 56 gAddedToolbars.add(id); 57 58 let tb = document.createXULElement("toolbar"); 59 tb.id = id; 60 tb.setAttribute("customizationtarget", id + "-target"); 61 62 let customizationtarget = document.createXULElement("hbox"); 63 customizationtarget.id = id + "-target"; 64 customizationtarget.setAttribute("flex", "1"); 65 tb.appendChild(customizationtarget); 66 67 let overflowPanel = document.createXULElement("panel"); 68 overflowPanel.id = id + "-overflow"; 69 document.getElementById("mainPopupSet").appendChild(overflowPanel); 70 71 let overflowList = document.createXULElement("vbox"); 72 overflowList.id = id + "-overflow-list"; 73 overflowPanel.appendChild(overflowList); 74 75 let chevron = document.createXULElement("toolbarbutton"); 76 chevron.id = id + "-chevron"; 77 tb.appendChild(chevron); 78 79 CustomizableUI.registerArea(id, { 80 type: CustomizableUI.TYPE_TOOLBAR, 81 defaultPlacements: placements, 82 overflowable: true, 83 }); 84 85 tb.setAttribute("customizable", "true"); 86 tb.setAttribute("overflowable", "true"); 87 tb.setAttribute("default-overflowpanel", overflowPanel.id); 88 tb.setAttribute("default-overflowtarget", overflowList.id); 89 tb.setAttribute("default-overflowbutton", chevron.id); 90 tb.setAttribute("addon-webext-overflowbutton", "unified-extensions-button"); 91 tb.setAttribute("addon-webext-overflowtarget", "overflowed-extensions-list"); 92 93 gNavToolbox.appendChild(tb); 94 CustomizableUI.registerToolbarNode(tb); 95 return tb; 96 } 97 98 function removeCustomToolbars() { 99 CustomizableUI.reset(); 100 for (let toolbarId of gAddedToolbars) { 101 CustomizableUI.unregisterArea(toolbarId, true); 102 let tb = document.getElementById(toolbarId); 103 if (tb.hasAttribute("overflowpanel")) { 104 let panel = document.getElementById(tb.getAttribute("overflowpanel")); 105 if (panel) { 106 panel.remove(); 107 } 108 } 109 tb.remove(); 110 } 111 gAddedToolbars.clear(); 112 } 113 114 function resetCustomization() { 115 return CustomizableUI.reset(); 116 } 117 118 function isInDevEdition() { 119 return AppConstants.MOZ_DEV_EDITION; 120 } 121 122 function removeNonReleaseButtons(areaPanelPlacements) { 123 if (isInDevEdition() && areaPanelPlacements.includes("developer-button")) { 124 areaPanelPlacements.splice( 125 areaPanelPlacements.indexOf("developer-button"), 126 1 127 ); 128 } 129 } 130 131 function removeNonOriginalButtons() { 132 CustomizableUI.removeWidgetFromArea("sync-button"); 133 } 134 135 function assertAreaPlacements(areaId, expectedPlacements) { 136 let actualPlacements = getAreaWidgetIds(areaId); 137 placementArraysEqual(areaId, actualPlacements, expectedPlacements); 138 } 139 140 function placementArraysEqual(areaId, actualPlacements, expectedPlacements) { 141 info("Actual placements: " + actualPlacements.join(", ")); 142 info("Expected placements: " + expectedPlacements.join(", ")); 143 is( 144 actualPlacements.length, 145 expectedPlacements.length, 146 "Area " + areaId + " should have " + expectedPlacements.length + " items." 147 ); 148 let minItems = Math.min(expectedPlacements.length, actualPlacements.length); 149 for (let i = 0; i < minItems; i++) { 150 if (typeof expectedPlacements[i] == "string") { 151 is( 152 actualPlacements[i], 153 expectedPlacements[i], 154 "Item " + i + " in " + areaId + " should match expectations." 155 ); 156 } else if (expectedPlacements[i] instanceof RegExp) { 157 ok( 158 expectedPlacements[i].test(actualPlacements[i]), 159 "Item " + 160 i + 161 " (" + 162 actualPlacements[i] + 163 ") in " + 164 areaId + 165 " should match " + 166 expectedPlacements[i] 167 ); 168 } else { 169 ok( 170 false, 171 "Unknown type of expected placement passed to " + 172 " assertAreaPlacements. Is your test broken?" 173 ); 174 } 175 } 176 } 177 178 function todoAssertAreaPlacements(areaId, expectedPlacements) { 179 let actualPlacements = getAreaWidgetIds(areaId); 180 let isPassing = actualPlacements.length == expectedPlacements.length; 181 let minItems = Math.min(expectedPlacements.length, actualPlacements.length); 182 for (let i = 0; i < minItems; i++) { 183 if (typeof expectedPlacements[i] == "string") { 184 isPassing = isPassing && actualPlacements[i] == expectedPlacements[i]; 185 } else if (expectedPlacements[i] instanceof RegExp) { 186 isPassing = isPassing && expectedPlacements[i].test(actualPlacements[i]); 187 } else { 188 ok( 189 false, 190 "Unknown type of expected placement passed to " + 191 " assertAreaPlacements. Is your test broken?" 192 ); 193 } 194 } 195 todo( 196 isPassing, 197 "The area placements for " + 198 areaId + 199 " should equal the expected placements." 200 ); 201 } 202 203 function getAreaWidgetIds(areaId) { 204 return CustomizableUI.getWidgetIdsInArea(areaId); 205 } 206 207 function simulateItemDrag(aToDrag, aTarget, aEvent = {}, aOffset = 2) { 208 let ev = aEvent; 209 if (ev == "end" || ev == "start") { 210 let win = aTarget.ownerGlobal; 211 const dwu = win.windowUtils; 212 let bounds = dwu.getBoundsWithoutFlushing(aTarget); 213 if (ev == "end") { 214 ev = { 215 clientX: bounds.right - aOffset, 216 clientY: bounds.bottom - aOffset, 217 }; 218 } else { 219 ev = { clientX: bounds.left + aOffset, clientY: bounds.top + aOffset }; 220 } 221 } 222 ev._domDispatchOnly = true; 223 synthesizeDrop( 224 aToDrag.parentNode, 225 aTarget, 226 null, 227 null, 228 aToDrag.ownerGlobal, 229 aTarget.ownerGlobal, 230 ev 231 ); 232 // Ensure dnd suppression is cleared. 233 synthesizeMouseAtCenter(aTarget, { type: "mouseup" }, aTarget.ownerGlobal); 234 } 235 236 function endCustomizing(aWindow = window) { 237 if (!aWindow.document.documentElement.hasAttribute("customizing")) { 238 return true; 239 } 240 let afterCustomizationPromise = BrowserTestUtils.waitForEvent( 241 aWindow.gNavToolbox, 242 "aftercustomization" 243 ); 244 aWindow.gCustomizeMode.exit(); 245 return afterCustomizationPromise; 246 } 247 248 function startCustomizing(aWindow = window) { 249 if (aWindow.document.documentElement.hasAttribute("customizing")) { 250 return null; 251 } 252 let customizationReadyPromise = BrowserTestUtils.waitForEvent( 253 aWindow.gNavToolbox, 254 "customizationready" 255 ); 256 aWindow.gCustomizeMode.enter(); 257 return customizationReadyPromise; 258 } 259 260 function promiseObserverNotified(aTopic) { 261 return new Promise(resolve => { 262 Services.obs.addObserver(function onNotification(subject, topic, data) { 263 Services.obs.removeObserver(onNotification, topic); 264 resolve({ subject, data }); 265 }, aTopic); 266 }); 267 } 268 269 function openAndLoadWindow(aOptions, aWaitForDelayedStartup = false) { 270 return new Promise(resolve => { 271 let win = OpenBrowserWindow(aOptions); 272 if (aWaitForDelayedStartup) { 273 Services.obs.addObserver(function onDS(aSubject) { 274 if (aSubject != win) { 275 return; 276 } 277 Services.obs.removeObserver(onDS, "browser-delayed-startup-finished"); 278 resolve(win); 279 }, "browser-delayed-startup-finished"); 280 } else { 281 win.addEventListener( 282 "load", 283 function () { 284 resolve(win); 285 }, 286 { once: true } 287 ); 288 } 289 }); 290 } 291 292 function promiseWindowClosed(win) { 293 return new Promise(resolve => { 294 win.addEventListener( 295 "unload", 296 function () { 297 resolve(); 298 }, 299 { once: true } 300 ); 301 win.close(); 302 }); 303 } 304 305 function promiseOverflowShown(win) { 306 let panelEl = win.document.getElementById("widget-overflow"); 307 return promisePanelElementShown(win, panelEl); 308 } 309 310 function promisePanelElementShown(win, aPanel) { 311 return new Promise((resolve, reject) => { 312 let timeoutId = win.setTimeout(() => { 313 reject("Panel did not show within 20 seconds."); 314 }, 20000); 315 function onPanelOpen() { 316 aPanel.removeEventListener("popupshown", onPanelOpen); 317 win.clearTimeout(timeoutId); 318 resolve(); 319 } 320 aPanel.addEventListener("popupshown", onPanelOpen); 321 }); 322 } 323 324 function promiseOverflowHidden(win) { 325 let panelEl = win.PanelUI.overflowPanel; 326 return promisePanelElementHidden(win, panelEl); 327 } 328 329 function promisePanelElementHidden(win, aPanel) { 330 return new Promise((resolve, reject) => { 331 let timeoutId = win.setTimeout(() => { 332 reject("Panel did not hide within 20 seconds."); 333 }, 20000); 334 function onPanelClose() { 335 aPanel.removeEventListener("popuphidden", onPanelClose); 336 win.clearTimeout(timeoutId); 337 executeSoon(resolve); 338 } 339 aPanel.addEventListener("popuphidden", onPanelClose); 340 }); 341 } 342 343 function isPanelUIOpen() { 344 return PanelUI.panel.state == "open" || PanelUI.panel.state == "showing"; 345 } 346 347 function isOverflowOpen() { 348 let panel = document.getElementById("widget-overflow"); 349 return panel.state == "open" || panel.state == "showing"; 350 } 351 352 function subviewShown(aSubview) { 353 return new Promise((resolve, reject) => { 354 let win = aSubview.ownerGlobal; 355 let timeoutId = win.setTimeout(() => { 356 reject("Subview (" + aSubview.id + ") did not show within 20 seconds."); 357 }, 20000); 358 function onViewShown() { 359 aSubview.removeEventListener("ViewShown", onViewShown); 360 win.clearTimeout(timeoutId); 361 resolve(); 362 } 363 aSubview.addEventListener("ViewShown", onViewShown); 364 }); 365 } 366 367 function subviewHidden(aSubview) { 368 return new Promise((resolve, reject) => { 369 let win = aSubview.ownerGlobal; 370 let timeoutId = win.setTimeout(() => { 371 reject("Subview (" + aSubview.id + ") did not hide within 20 seconds."); 372 }, 20000); 373 function onViewHiding() { 374 aSubview.removeEventListener("ViewHiding", onViewHiding); 375 win.clearTimeout(timeoutId); 376 resolve(); 377 } 378 aSubview.addEventListener("ViewHiding", onViewHiding); 379 }); 380 } 381 382 function waitFor(aTimeout = 100) { 383 return new Promise(resolve => { 384 setTimeout(() => resolve(), aTimeout); 385 }); 386 } 387 388 /** 389 * Wait for an attribute on a node to change 390 * 391 * @param aNode Node on which the mutation is expected 392 * @param aAttribute The attribute we're interested in 393 * @param aFilterFn A function to check if the new value is what we want. 394 * @return {Promise} resolved when the requisite mutation shows up. 395 */ 396 function promiseAttributeMutation(aNode, aAttribute, aFilterFn) { 397 return new Promise(resolve => { 398 info("waiting for mutation of attribute '" + aAttribute + "'."); 399 let obs = new MutationObserver(mutations => { 400 for (let mut of mutations) { 401 let attr = mut.attributeName; 402 let newValue = mut.target.getAttribute(attr); 403 if (aFilterFn(newValue)) { 404 ok( 405 true, 406 "mutation occurred: attribute '" + 407 attr + 408 "' changed to '" + 409 newValue + 410 "' from '" + 411 mut.oldValue + 412 "'." 413 ); 414 obs.disconnect(); 415 resolve(); 416 } else { 417 info( 418 "Ignoring mutation that produced value " + 419 newValue + 420 " because of filter." 421 ); 422 } 423 } 424 }); 425 obs.observe(aNode, { attributeFilter: [aAttribute] }); 426 }); 427 } 428 429 function popupShown(aPopup) { 430 return BrowserTestUtils.waitForPopupEvent(aPopup, "shown"); 431 } 432 433 function popupHidden(aPopup) { 434 return BrowserTestUtils.waitForPopupEvent(aPopup, "hidden"); 435 } 436 437 // This is a simpler version of the context menu check that 438 // exists in contextmenu_common.js. 439 function checkContextMenu(aContextMenu, aExpectedEntries, aWindow = window) { 440 let children = [...aContextMenu.children]; 441 // Ignore hidden nodes: 442 children = children.filter(n => !n.hidden); 443 for (let i = 0; i < children.length; i++) { 444 let menuitem = children[i]; 445 try { 446 if (aExpectedEntries[i][0] == "---") { 447 is(menuitem.localName, "menuseparator", "menuseparator expected"); 448 continue; 449 } 450 451 let selector = aExpectedEntries[i][0]; 452 ok( 453 menuitem.matches(selector), 454 "menuitem should match " + selector + " selector" 455 ); 456 let commandValue = menuitem.getAttribute("command"); 457 let relatedCommand = commandValue 458 ? aWindow.document.getElementById(commandValue) 459 : null; 460 let menuItemDisabled = relatedCommand 461 ? relatedCommand.getAttribute("disabled") == "true" 462 : menuitem.getAttribute("disabled") == "true"; 463 is( 464 menuItemDisabled, 465 !aExpectedEntries[i][1], 466 "disabled state for " + selector 467 ); 468 } catch (e) { 469 ok(false, "Exception when checking context menu: " + e); 470 } 471 } 472 } 473 474 function waitForOverflowButtonShown(win = window) { 475 info("Waiting for overflow button to show"); 476 let ov = win.document.getElementById("nav-bar-overflow-button"); 477 return waitForElementShown(ov.icon); 478 } 479 function waitForElementShown(element) { 480 return BrowserTestUtils.waitForCondition(() => { 481 info("Checking if element has non-0 size"); 482 // We intentionally flush layout to ensure the element is actually shown. 483 let rect = element.getBoundingClientRect(); 484 return rect.width > 0 && rect.height > 0; 485 }); 486 } 487 488 /** 489 * Opens the history panel through the history toolbarbutton in the 490 * navbar and returns a promise that resolves as soon as the panel is open 491 * is showing. 492 */ 493 async function openHistoryPanel(doc = document) { 494 await waitForOverflowButtonShown(); 495 await doc.getElementById("nav-bar").overflowable.show(); 496 info("Menu panel was opened"); 497 498 let historyButton = doc.getElementById("history-panelmenu"); 499 Assert.ok(historyButton, "History button appears in Panel Menu"); 500 501 historyButton.click(); 502 503 let historyPanel = doc.getElementById("PanelUI-history"); 504 return BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); 505 } 506 507 /** 508 * Closes the history panel and returns a promise that resolves as sooon 509 * as the panel is closed. 510 */ 511 async function hideHistoryPanel(doc = document) { 512 let historyView = doc.getElementById("PanelUI-history"); 513 let historyPanel = historyView.closest("panel"); 514 let promise = BrowserTestUtils.waitForEvent(historyPanel, "popuphidden"); 515 historyPanel.hidePopup(); 516 return promise; 517 } 518 519 /** 520 * After bug 1960002, setting the window to its min-width is no longer enough 521 * to trigger navbar overflow. So, to keep the overflow tests working, we need 522 * to both change the window width and add more buttons to the navbar. 523 * 524 * Note this helper registers a cleanup function that undoes changes to the 525 * supplied window's width and resets its toolbar state. Be sure to call this 526 * helper before other cleanup functions that might assert on the state of the 527 * window after it is reset. 528 * 529 * Set the shouldCleanup param to false if you don't need to register another 530 * end-of-test cleanup function, for instance, if your test calls 531 * `CustomizableUI.reset()` in the middle of asserts that rely on overflow. 532 * 533 * Returns the original window width in case a test needs to resize the window 534 * before the cleanup function runs. 535 */ 536 function ensureToolbarOverflow(aWindow, shouldCleanup = true) { 537 const originalWindowWidth = aWindow.outerWidth; 538 539 aWindow.resizeTo(kForceOverflowWidthPx, aWindow.outerHeight); 540 CustomizableUI.addWidgetToArea( 541 "history-panelmenu", 542 CustomizableUI.AREA_NAVBAR, 543 0 544 ); 545 CustomizableUI.addWidgetToArea( 546 "email-link-button", 547 CustomizableUI.AREA_NAVBAR, 548 0 549 ); 550 CustomizableUI.addWidgetToArea("panic-button", CustomizableUI.AREA_NAVBAR, 0); 551 552 if (shouldCleanup) { 553 registerCleanupFunction(() => { 554 unensureToolbarOverflow(aWindow, originalWindowWidth); 555 }); 556 } 557 558 return originalWindowWidth; 559 } 560 561 /** 562 * Helper function that undoes what `ensureToolbarOverflow` does. 563 */ 564 function unensureToolbarOverflow(aWindow, originalWindowWidth) { 565 if (originalWindowWidth) { 566 aWindow.resizeTo(originalWindowWidth, aWindow.outerHeight); 567 } 568 CustomizableUI.removeWidgetFromArea("history-panelmenu"); 569 CustomizableUI.removeWidgetFromArea("email-link-button"); 570 CustomizableUI.removeWidgetFromArea("panic-button"); 571 }