/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * This file tests the behaviour of the overflowable nav-bar with Unified * Extensions enabled and disabled. */ "use strict"; loadTestSubscript("head_unified_extensions.js"); requestLongerTimeout(2); const NUM_EXTENSIONS = 5; const OVERFLOW_WINDOW_WIDTH_PX = 450; const DEFAULT_WIDGET_IDS = [ "home-button", "library-button", "zoom-controls", "search-container", "sidebar-button", ]; const OVERFLOWED_EXTENSIONS_LIST_ID = "overflowed-extensions-list"; add_setup(async function () { // To make it easier to control things that will overflow, we'll start by // removing that's removable out of the nav-bar and adding just a fixed // set of items (DEFAULT_WIDGET_IDS) at the end of the nav-bar. let existingWidgetIDs = CustomizableUI.getWidgetIdsInArea( CustomizableUI.AREA_NAVBAR ); for (let widgetID of existingWidgetIDs) { if (CustomizableUI.isWidgetRemovable(widgetID)) { CustomizableUI.removeWidgetFromArea(widgetID); } } for (const widgetID of DEFAULT_WIDGET_IDS) { CustomizableUI.addWidgetToArea(widgetID, CustomizableUI.AREA_NAVBAR); } registerCleanupFunction(async () => { await CustomizableUI.reset(); }); }); /** * Returns the IDs of the children of parent. * * @param {Element} parent * @returns {string[]} the IDs of the children */ function getChildrenIDs(parent) { return Array.from(parent.children).map(child => child.id); } /** * Returns a NodeList of all non-hidden menu, menuitem and menuseparators * that are direct descendants of popup. * * @param {Element} popup * @returns {NodeList} the visible items. */ function getVisibleMenuItems(popup) { return popup.querySelectorAll( ":scope > :is(menu, menuitem, menuseparator):not([hidden])" ); } /** * This helper function does most of the heavy lifting for these tests. * It does the following in order: * * 1. Registers and enables NUM_EXTENSIONS test WebExtensions that add * browser_action buttons to the nav-bar. * 2. Resizes the window to force things after the URL bar to overflow. * 3. Calls an async test function to analyze the overflow lists. * 4. Restores the window's original width, ensuring that the IDs of the * nav-bar match the original set. * 5. Unloads all of the test WebExtensions * * @param {DOMWindow} win The browser window to perform the test on. * @param {object} options Additional options when running this test. * @param {Function} options.beforeOverflowed This optional async function will * be run after the extensions are created and added to the toolbar, but * before the toolbar overflows. The function is called with the following * arguments: * * {string[]} extensionIDs: The IDs of the test WebExtensions. * * The return value of the function is ignored. * @param {Function} options.whenOverflowed This optional async function will * run once the window is in the overflow state. The function is called * with the following arguments: * * {Element} defaultList: The DOM element that holds overflowed default * items. * {Element} unifiedExtensionList: The DOM element that holds overflowed * WebExtension browser_actions when Unified Extensions is enabled. * {string[]} extensionIDs: The IDs of the test WebExtensions. * * The return value of the function is ignored. * @param {Function} options.afterUnderflowed This optional async function will * be run after the window is expanded and the toolbar has underflowed, but * before the extensions are removed. This function is not passed any * arguments. The return value of the function is ignored. * */ async function withWindowOverflowed( win, { beforeOverflowed = async () => {}, whenOverflowed = async () => {}, afterUnderflowed = async () => {}, } = {} ) { const doc = win.document; doc.documentElement.removeAttribute("persist"); const navbar = doc.getElementById(CustomizableUI.AREA_NAVBAR); await ensureMaximizedWindow(win); // The OverflowableToolbar operates asynchronously at times, so we will // poll a widget's overflowedItem attribute to detect whether or not the // widgets have finished being moved. We'll use the first widget that // we added to the nav-bar, as this should be the left-most item in the // set that we added. const signpostWidgetID = "home-button"; // We'll also force the signpost widget to be extra-wide to ensure that it // overflows after we shrink the window. CustomizableUI.getWidget(signpostWidgetID).forWindow(win).node.style = "width: 150px"; const extWithMenuBrowserAction = ExtensionTestUtils.loadExtension({ manifest: { name: "Extension #0", browser_specific_settings: { gecko: { id: "unified-extensions-overflowable-toolbar@ext-0" }, }, browser_action: { default_area: "navbar", }, // We pass `activeTab` to have a different permission message when // hovering the primary/action button. permissions: ["activeTab", "contextMenus"], }, background() { browser.contextMenus.create( { id: "some-menu-id", title: "Click me!", contexts: ["all"], }, () => browser.test.sendMessage("menu-created") ); }, useAddonManager: "temporary", }); const extWithSubMenuBrowserAction = ExtensionTestUtils.loadExtension({ manifest: { name: "Extension #1", browser_specific_settings: { gecko: { id: "unified-extensions-overflowable-toolbar@ext-1" }, }, browser_action: { default_area: "navbar", }, permissions: ["contextMenus"], }, background() { browser.contextMenus.create({ id: "some-menu-id", title: "Open sub-menu", contexts: ["all"], }); browser.contextMenus.create( { id: "some-sub-menu-id", parentId: "some-menu-id", title: "Click me!", contexts: ["all"], }, () => browser.test.sendMessage("menu-created") ); }, useAddonManager: "temporary", }); const manifests = []; for (let i = 2; i < NUM_EXTENSIONS; ++i) { manifests.push({ name: `Extension #${i}`, browser_action: { default_area: "navbar", }, browser_specific_settings: { gecko: { id: `unified-extensions-overflowable-toolbar@ext-${i}` }, }, }); } const extensions = [ extWithMenuBrowserAction, extWithSubMenuBrowserAction, ...createExtensions(manifests), ]; // Adding browser actions is asynchronous, so this CustomizableUI listener // is used to make sure that the browser action widgets have finished getting // added. let listener = { _remainingBrowserActions: NUM_EXTENSIONS, _deferred: PromiseUtils.defer(), get promise() { return this._deferred.promise; }, onWidgetAdded(widgetID, area) { if (widgetID.endsWith("-browser-action")) { this._remainingBrowserActions--; } if (!this._remainingBrowserActions) { this._deferred.resolve(); } }, }; CustomizableUI.addListener(listener); // Start all the extensions sequentially. for (const extension of extensions) { await extension.startup(); } await Promise.all([ extWithMenuBrowserAction.awaitMessage("menu-created"), extWithSubMenuBrowserAction.awaitMessage("menu-created"), ]); await listener.promise; CustomizableUI.removeListener(listener); const extensionIDs = extensions.map(extension => extension.id); try { info("Running beforeOverflowed task"); await beforeOverflowed(extensionIDs); } finally { // The beforeOverflowed task may have moved some items out from the navbar, // so only listen for overflows for items still in there. const browserActionIDs = extensionIDs.map(id => AppUiTestInternals.getBrowserActionWidgetId(id) ); const browserActionsInNavBar = browserActionIDs.filter(widgetID => { let placement = CustomizableUI.getPlacementOfWidget(widgetID); return placement.area == CustomizableUI.AREA_NAVBAR; }); let widgetOverflowListener = { _remainingOverflowables: browserActionsInNavBar.length + DEFAULT_WIDGET_IDS.length, _deferred: PromiseUtils.defer(), get promise() { return this._deferred.promise; }, onWidgetOverflow(widgetNode, areaNode) { this._remainingOverflowables--; if (!this._remainingOverflowables) { this._deferred.resolve(); } }, }; CustomizableUI.addListener(widgetOverflowListener); win.resizeTo(OVERFLOW_WINDOW_WIDTH_PX, win.outerHeight); await widgetOverflowListener.promise; CustomizableUI.removeListener(widgetOverflowListener); Assert.ok( navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar." ); const defaultList = doc.getElementById( navbar.getAttribute("default-overflowtarget") ); const unifiedExtensionList = doc.getElementById( navbar.getAttribute("addon-webext-overflowtarget") ); try { info("Running whenOverflowed task"); await whenOverflowed(defaultList, unifiedExtensionList, extensionIDs); } finally { await ensureMaximizedWindow(win); // Notably, we don't wait for the nav-bar to not have the "overflowing" // attribute. This is because we might be running in an environment // where the nav-bar was overflowing to begin with. Let's just hope that // our sign-post widget has stopped overflowing. await TestUtils.waitForCondition(() => { return !doc .getElementById(signpostWidgetID) .hasAttribute("overflowedItem"); }); try { info("Running afterUnderflowed task"); await afterUnderflowed(); } finally { await Promise.all(extensions.map(extension => extension.unload())); } } } } async function verifyExtensionWidget(widget, win = window) { Assert.ok(widget, "expected widget"); let actionButton = widget.querySelector( ".unified-extensions-item-action-button" ); Assert.ok( actionButton.classList.contains("unified-extensions-item-action-button"), "expected action class on the button" ); ok( actionButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button in the panel" ); let menuButton = widget.lastElementChild; Assert.ok( menuButton.classList.contains("unified-extensions-item-menu-button"), "expected class on the button" ); ok( menuButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button in the panel" ); let contents = actionButton.querySelector( ".unified-extensions-item-contents" ); Assert.ok(contents, "expected contents element"); // This is needed to correctly position the contents (vbox) element in the // toolbarbutton. Assert.equal( contents.getAttribute("move-after-stack"), "true", "expected move-after-stack attribute to be set" ); // Make sure the contents element is inserted after the stack one (which is // automagically created by the toolbarbutton element). Assert.deepEqual( Array.from(actionButton.childNodes.values()).map( child => child.classList[0] ), [ // The stack (which contains the extension icon) should be the first // child. "toolbarbutton-badge-stack", // This is the widget label, which is hidden with CSS. "toolbarbutton-text", // This is the contents element, which displays the extension name and // messages. "unified-extensions-item-contents", ], "expected the correct order for the children of the action button" ); let name = contents.querySelector(".unified-extensions-item-name"); Assert.ok(name, "expected name element"); Assert.ok( name.textContent.startsWith("Extension "), "expected name to not be empty" ); Assert.ok( contents.querySelector(".unified-extensions-item-message-default"), "expected message default element" ); Assert.ok( contents.querySelector(".unified-extensions-item-message-hover"), "expected message hover element" ); Assert.equal( win.document.l10n.getAttributes(menuButton).id, "unified-extensions-item-open-menu", "expected l10n id attribute for the extension" ); Assert.deepEqual( Object.keys(win.document.l10n.getAttributes(menuButton).args), ["extensionName"], "expected l10n args attribute for the extension" ); Assert.ok( win.document.l10n .getAttributes(menuButton) .args.extensionName.startsWith("Extension "), "expected l10n args attribute to start with the correct name" ); Assert.ok( menuButton.getAttribute("aria-label") !== "", "expected menu button to have non-empty localized content" ); } /** * Tests that overflowed browser actions go to the Unified Extensions * panel, and default toolbar items go into the default overflow * panel. */ add_task(async function test_overflowable_toolbar() { let win = await BrowserTestUtils.openNewBrowserWindow(); let movedNode; await withWindowOverflowed(win, { whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { // Ensure that there are 5 items in the Unified Extensions overflow // list, and the default widgets should all be in the default overflow // list (though there might be more items from the nav-bar in there that // already existed in the nav-bar before we put the default widgets in // there as well). let defaultListIDs = getChildrenIDs(defaultList); for (const widgetID of DEFAULT_WIDGET_IDS) { Assert.ok( defaultListIDs.includes(widgetID), `Default overflow list should have ${widgetID}` ); } Assert.ok( unifiedExtensionList.children.length, "Should have items in the Unified Extension list." ); for (const child of Array.from(unifiedExtensionList.children)) { Assert.ok( extensionIDs.includes(child.dataset.extensionid), `Unified Extensions overflow list should have ${child.dataset.extensionid}` ); await verifyExtensionWidget(child, win); } let extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId( extensionIDs.at(-1) ); movedNode = CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node; Assert.equal(movedNode.getAttribute("cui-areatype"), "toolbar"); CustomizableUI.addWidgetToArea( extensionWidgetID, CustomizableUI.AREA_ADDONS ); Assert.equal( movedNode.getAttribute("cui-areatype"), "panel", "The moved browser action button should have the right cui-areatype set." ); }, afterUnderflowed: async () => { // Ensure that the moved node's parent is still the add-ons panel. Assert.equal( movedNode.parentElement.id, CustomizableUI.AREA_ADDONS, "The browser action should still be in the addons panel" ); CustomizableUI.addWidgetToArea(movedNode.id, CustomizableUI.AREA_NAVBAR); }, }); await BrowserTestUtils.closeWindow(win); }); add_task(async function test_context_menu() { let win = await BrowserTestUtils.openNewBrowserWindow(); await withWindowOverflowed(win, { whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { Assert.ok( unifiedExtensionList.children.length, "Should have items in the Unified Extension list." ); // Open the extension panel. await openExtensionsPanel(win); // Let's verify the context menus for the following extensions: // // - the first one defines a menu in the background script // - the second one defines a menu with submenu // - the third extension has no menu info("extension with browser action and a menu"); const firstExtensionWidget = unifiedExtensionList.children[0]; Assert.ok(firstExtensionWidget, "expected extension widget"); let contextMenu = await openUnifiedExtensionsContextMenu( firstExtensionWidget.dataset.extensionid, win ); Assert.ok(contextMenu, "expected a context menu"); let visibleItems = getVisibleMenuItems(contextMenu); // The context menu for the extension that declares a browser action menu // should have the menu item created by the extension, a menu separator, the control // for pinning the browser action to the toolbar, a menu separator and the 3 default menu items. is( visibleItems.length, 7, "expected a custom context menu item, a menu separator, the pin to " + "toolbar menu item, a menu separator, and the 3 default menu items" ); const [item, separator] = visibleItems; is( item.getAttribute("label"), "Click me!", "expected menu item as first child" ); is( separator.tagName, "menuseparator", "expected separator after last menu item created by the extension" ); await closeChromeContextMenu(contextMenu.id, null, win); info("extension with browser action and a menu with submenu"); const secondExtensionWidget = unifiedExtensionList.children[1]; Assert.ok(secondExtensionWidget, "expected extension widget"); contextMenu = await openUnifiedExtensionsContextMenu( secondExtensionWidget.dataset.extensionid, win ); visibleItems = getVisibleMenuItems(contextMenu); is(visibleItems.length, 7, "expected 7 menu items"); const popup = await openSubmenu(visibleItems[0]); is(popup.children.length, 1, "expected 1 submenu item"); is( popup.children[0].getAttribute("label"), "Click me!", "expected menu item" ); // The number of items in the (main) context menu should remain the same. visibleItems = getVisibleMenuItems(contextMenu); is(visibleItems.length, 7, "expected 7 menu items"); await closeChromeContextMenu(contextMenu.id, null, win); info("extension with no browser action and no menu"); // There is no context menu created by this extension, so there should // only be 3 menu items corresponding to the default manage/remove/report // items. const thirdExtensionWidget = unifiedExtensionList.children[2]; Assert.ok(thirdExtensionWidget, "expected extension widget"); contextMenu = await openUnifiedExtensionsContextMenu( thirdExtensionWidget.dataset.extensionid, win ); Assert.ok(contextMenu, "expected a context menu"); visibleItems = getVisibleMenuItems(contextMenu); is(visibleItems.length, 5, "expected 5 menu items"); await closeChromeContextMenu(contextMenu.id, null, win); // We can close the unified extensions panel now. await closeExtensionsPanel(win); }, }); await BrowserTestUtils.closeWindow(win); }); add_task(async function test_message_deck() { let win = await BrowserTestUtils.openNewBrowserWindow(); await withWindowOverflowed(win, { whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { Assert.ok( unifiedExtensionList.children.length, "Should have items in the Unified Extension list." ); const firstExtensionWidget = unifiedExtensionList.children[0]; Assert.ok(firstExtensionWidget, "expected extension widget"); Assert.ok( firstExtensionWidget.dataset.extensionid, "expected data attribute for extension ID" ); // Navigate to a page where `activeTab` is useful. await BrowserTestUtils.withNewTab( { gBrowser: win.gBrowser, url: "https://example.com/" }, async () => { // Open the extension panel. await openExtensionsPanel(win); info("verify message when focusing the action button"); const item = getUnifiedExtensionsItem( firstExtensionWidget.dataset.extensionid, win ); Assert.ok(item, "expected an item for the extension"); const actionButton = item.querySelector( ".unified-extensions-item-action-button" ); Assert.ok(actionButton, "expected action button"); const menuButton = item.querySelector( ".unified-extensions-item-menu-button" ); Assert.ok(menuButton, "expected menu button"); const messageDeck = item.querySelector( ".unified-extensions-item-message-deck" ); Assert.ok(messageDeck, "expected message deck"); is( messageDeck.selectedIndex, win.gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT, "expected selected message in the deck to be the default message" ); const defaultMessage = item.querySelector( ".unified-extensions-item-message-default" ); Assert.deepEqual( win.document.l10n.getAttributes(defaultMessage), { id: "origin-controls-state-when-clicked", args: null }, "expected correct l10n attributes for the default message" ); Assert.ok( defaultMessage.textContent !== "", "expected default message to not be empty" ); const hoverMessage = item.querySelector( ".unified-extensions-item-message-hover" ); Assert.deepEqual( win.document.l10n.getAttributes(hoverMessage), { id: "origin-controls-state-hover-run-visit-only", args: null }, "expected correct l10n attributes for the hover message" ); Assert.ok( hoverMessage.textContent !== "", "expected hover message to not be empty" ); const hoverMenuButtonMessage = item.querySelector( ".unified-extensions-item-message-hover-menu-button" ); Assert.deepEqual( win.document.l10n.getAttributes(hoverMenuButtonMessage), { id: "unified-extensions-item-message-manage", args: null }, "expected correct l10n attributes for the message when hovering the menu button" ); Assert.ok( hoverMenuButtonMessage.textContent !== "", "expected message for when the menu button is hovered to not be empty" ); // 1. Focus the action button of the first extension in the panel. let focused = BrowserTestUtils.waitForEvent(actionButton, "focus"); EventUtils.synthesizeKey("VK_TAB", {}, win); await focused; is( actionButton, win.document.activeElement, "expected action button of the first extension item to be focused" ); is( messageDeck.selectedIndex, win.gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER, "expected selected message in the deck to be the hover message" ); // 2. Focus the menu button, causing the action button to lose focus. focused = BrowserTestUtils.waitForEvent(menuButton, "focus"); EventUtils.synthesizeKey("VK_TAB", {}, win); await focused; is( menuButton, win.document.activeElement, "expected menu button of the first extension item to be focused" ); is( messageDeck.selectedIndex, win.gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER, "expected selected message in the deck to be the message when focusing the menu button" ); await closeExtensionsPanel(win); info("verify message when hovering the action button"); await openExtensionsPanel(win); is( messageDeck.selectedIndex, win.gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT, "expected selected message in the deck to be the default message" ); // 1. Hover the action button of the first extension in the panel. let hovered = BrowserTestUtils.waitForEvent( actionButton, "mouseover" ); EventUtils.synthesizeMouseAtCenter( actionButton, { type: "mouseover" }, win ); await hovered; is( messageDeck.selectedIndex, win.gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER, "expected selected message in the deck to be the hover message" ); // 2. Hover the menu button, causing the action button to no longer // be hovered. hovered = BrowserTestUtils.waitForEvent(menuButton, "mouseover"); EventUtils.synthesizeMouseAtCenter( menuButton, { type: "mouseover" }, win ); await hovered; is( messageDeck.selectedIndex, win.gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER, "expected selected message in the deck to be the message when hovering the menu button" ); await closeExtensionsPanel(win); } ); }, }); await BrowserTestUtils.closeWindow(win); }); /** * Tests that if we pin a browser action button listed in the addons panel * to the toolbar when that button would immediately overflow, that the * button is put into the addons panel overflow list. */ add_task(async function test_pinning_to_toolbar_when_overflowed() { let win = await BrowserTestUtils.openNewBrowserWindow(); let movedNode; let extensionWidgetID; let actionButton; let menuButton; await withWindowOverflowed(win, { beforeOverflowed: async extensionIDs => { // Before we overflow the toolbar, let's move the last item to the addons // panel. extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId( extensionIDs.at(-1) ); movedNode = CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node; actionButton = movedNode.querySelector( ".unified-extensions-item-action-button" ); ok( actionButton.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the action button in the navbar" ); ok( !actionButton.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the action button in the navbar" ); menuButton = movedNode.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButton.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the menu button in the navbar" ); ok( !menuButton.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the menu button in the navbar" ); CustomizableUI.addWidgetToArea( extensionWidgetID, CustomizableUI.AREA_ADDONS ); ok( actionButton.classList.contains("subviewbutton"), "expected .subviewbutton CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button in the panel" ); ok( menuButton.classList.contains("subviewbutton"), "expected .subviewbutton CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button in the panel" ); }, whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { ok( actionButton.classList.contains("subviewbutton"), "expected .subviewbutton CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button in the panel" ); ok( menuButton.classList.contains("subviewbutton"), "expected .subviewbutton CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button in the panel" ); // Now that the window is overflowed, let's move the widget in the addons // panel back to the navbar. This should cause the widget to overflow back // into the addons panel. CustomizableUI.addWidgetToArea( extensionWidgetID, CustomizableUI.AREA_NAVBAR ); await TestUtils.waitForCondition(() => { return movedNode.hasAttribute("overflowedItem"); }); Assert.equal( movedNode.parentElement, unifiedExtensionList, "Should have overflowed the extension button to the right list." ); ok( actionButton.classList.contains("subviewbutton"), "expected .subviewbutton CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button in the panel" ); ok( menuButton.classList.contains("subviewbutton"), "expected .subviewbutton CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button in the panel" ); }, }); await BrowserTestUtils.closeWindow(win); }); /** * This test verifies that, when an extension placed in the toolbar is * overflowed into the addons panel and context-clicked, it shows the "Pin to * Toolbar" item as checked, and that unchecking this menu item inserts the * extension into the dedicated addons area of the panel, and that the item * then does not underflow. */ add_task(async function test_unpin_overflowed_widget() { let win = await BrowserTestUtils.openNewBrowserWindow(); let extensionID; await withWindowOverflowed(win, { whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { const firstExtensionWidget = unifiedExtensionList.children[0]; Assert.ok(firstExtensionWidget, "expected an extension widget"); extensionID = firstExtensionWidget.dataset.extensionid; let movedNode = CustomizableUI.getWidget( firstExtensionWidget.id ).forWindow(win).node; Assert.equal( movedNode.getAttribute("cui-areatype"), "toolbar", "expected extension widget to be in the toolbar" ); Assert.ok( movedNode.hasAttribute("overflowedItem"), "expected extension widget to be overflowed" ); let actionButton = movedNode.querySelector( ".unified-extensions-item-action-button" ); ok( actionButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button in the panel" ); let menuButton = movedNode.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button in the panel" ); // Open the panel, then the context menu of the extension widget, verify // the 'Pin to Toolbar' menu item, then click on this menu item to // uncheck it (i.e. unpin the extension). await openExtensionsPanel(win); const contextMenu = await openUnifiedExtensionsContextMenu( extensionID, win ); Assert.ok(contextMenu, "expected a context menu"); const pinToToolbar = contextMenu.querySelector( ".unified-extensions-context-menu-pin-to-toolbar" ); Assert.ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item"); Assert.ok( !pinToToolbar.hidden, "expected 'Pin to Toolbar' to be visible" ); Assert.equal( pinToToolbar.getAttribute("checked"), "true", "expected 'Pin to Toolbar' to be checked" ); // Uncheck "Pin to Toolbar" menu item. Clicking a menu item in the // context menu closes the unified extensions panel automatically. const hidden = BrowserTestUtils.waitForEvent( win.gUnifiedExtensions.panel, "popuphidden", true ); contextMenu.activateItem(pinToToolbar); await hidden; // We expect the widget to no longer be overflowed. await TestUtils.waitForCondition(() => { return !movedNode.hasAttribute("overflowedItem"); }); Assert.equal( movedNode.parentElement.id, CustomizableUI.AREA_ADDONS, "expected extension widget to have been unpinned and placed in the addons area" ); Assert.equal( movedNode.getAttribute("cui-areatype"), "panel", "expected extension widget to be in the unified extensions panel" ); }, afterUnderflowed: async () => { await openExtensionsPanel(win); const item = getUnifiedExtensionsItem(extensionID, win); Assert.ok( item, "expected extension widget to be listed in the unified extensions panel" ); let actionButton = item.querySelector( ".unified-extensions-item-action-button" ); ok( actionButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button in the panel" ); let menuButton = item.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button in the panel" ); await closeExtensionsPanel(win); }, }); await BrowserTestUtils.closeWindow(win); }); add_task(async function test_overflow_with_a_second_window() { let win = await BrowserTestUtils.openNewBrowserWindow(); // Open a second window that will stay maximized. We want to be sure that // overflowing a widget in one window isn't going to affect the other window // since we have an instance (of a CUI widget) per window. let secondWin = await BrowserTestUtils.openNewBrowserWindow(); await ensureMaximizedWindow(secondWin); await BrowserTestUtils.openNewForegroundTab( secondWin.gBrowser, "https://example.com/" ); // Make sure the first window is the active window. let windowActivePromise = new Promise(resolve => { if (Services.focus.activeWindow == win) { resolve(); } else { win.addEventListener( "activate", () => { resolve(); }, { once: true } ); } }); win.focus(); await windowActivePromise; let extensionWidgetID; let aNode; let aNodeInSecondWindow; await withWindowOverflowed(win, { beforeOverflowed: async extensionIDs => { extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId( extensionIDs.at(-1) ); // This is the DOM node for the current window that is overflowed. aNode = CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node; Assert.ok( !aNode.hasAttribute("overflowedItem"), "expected extension widget to NOT be overflowed" ); let actionButton = aNode.querySelector( ".unified-extensions-item-action-button" ); ok( actionButton.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the action button" ); ok( !actionButton.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the action button" ); let menuButton = aNode.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButton.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the menu button" ); ok( !menuButton.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the menu button" ); // This is the DOM node of the same CUI widget but in the maximized // window opened before. aNodeInSecondWindow = CustomizableUI.getWidget(extensionWidgetID).forWindow(secondWin).node; let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector( ".unified-extensions-item-action-button" ); ok( actionButtonInSecondWindow.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the action button in the second window" ); ok( !actionButtonInSecondWindow.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the action button in the second window" ); let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButtonInSecondWindow.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the menu button in the second window" ); ok( !menuButtonInSecondWindow.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the menu button in the second window" ); }, whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { // The DOM node should have been overflowed. Assert.ok( aNode.hasAttribute("overflowedItem"), "expected extension widget to be overflowed" ); Assert.equal( aNode.getAttribute("widget-id"), extensionWidgetID, "expected the CUI widget ID to be set on the DOM node" ); // When the node is overflowed, we swap the CSS class on the action // and menu buttons since the node is now placed in the extensions panel. let actionButton = aNode.querySelector( ".unified-extensions-item-action-button" ); ok( actionButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the action button" ); ok( !actionButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the action button" ); let menuButton = aNode.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButton.classList.contains("subviewbutton"), "expected the .subviewbutton CSS class on the menu button" ); ok( !menuButton.classList.contains("toolbarbutton-1"), "expected no .toolbarbutton-1 CSS class on the menu button" ); // The DOM node in the other window should not have been overflowed. Assert.ok( !aNodeInSecondWindow.hasAttribute("overflowedItem"), "expected extension widget to NOT be overflowed in the other window" ); Assert.equal( aNodeInSecondWindow.getAttribute("widget-id"), extensionWidgetID, "expected the CUI widget ID to be set on the DOM node" ); // We expect no CSS class changes for the node in the other window. let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector( ".unified-extensions-item-action-button" ); ok( actionButtonInSecondWindow.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the action button in the second window" ); ok( !actionButtonInSecondWindow.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the action button in the second window" ); let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButtonInSecondWindow.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the menu button in the second window" ); ok( !menuButtonInSecondWindow.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the menu button in the second window" ); }, afterUnderflowed: async () => { // After underflow, we expect the CSS class on the action and menu // buttons of the DOM node of the current window to be updated. let actionButton = aNode.querySelector( ".unified-extensions-item-action-button" ); ok( actionButton.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the action button in the panel" ); ok( !actionButton.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the action button in the panel" ); let menuButton = aNode.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButton.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the menu button in the panel" ); ok( !menuButton.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the menu button in the panel" ); // The DOM node of the other window should not be changed. let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector( ".unified-extensions-item-action-button" ); ok( actionButtonInSecondWindow.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the action button in the second window" ); ok( !actionButtonInSecondWindow.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the action button in the second window" ); let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector( ".unified-extensions-item-menu-button" ); ok( menuButtonInSecondWindow.classList.contains("toolbarbutton-1"), "expected .toolbarbutton-1 CSS class on the menu button in the second window" ); ok( !menuButtonInSecondWindow.classList.contains("subviewbutton"), "expected no .subviewbutton CSS class on the menu button in the second window" ); }, }); await BrowserTestUtils.closeWindow(win); await BrowserTestUtils.closeWindow(secondWin); }); add_task(async function test_overflow_with_extension_in_collapsed_area() { const win = await BrowserTestUtils.openNewBrowserWindow(); const bookmarksToolbar = win.document.getElementById( CustomizableUI.AREA_BOOKMARKS ); let movedNode; let extensionWidgetID; let extensionWidgetPosition; await withWindowOverflowed(win, { beforeOverflowed: async extensionIDs => { // Before we overflow the toolbar, let's move the last item to the // (visible) bookmarks toolbar. extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId( extensionIDs.at(-1) ); movedNode = CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node; // Ensure that the toolbar is currently visible. await promiseSetToolbarVisibility(bookmarksToolbar, true); // Move an extension to the bookmarks toolbar. CustomizableUI.addWidgetToArea( extensionWidgetID, CustomizableUI.AREA_BOOKMARKS ); Assert.equal( movedNode.parentElement.id, CustomizableUI.AREA_BOOKMARKS, "expected extension widget to be in the bookmarks toolbar" ); Assert.ok( !movedNode.hasAttribute("artificallyOverflowed"), "expected node to not have any artificallyOverflowed prop" ); extensionWidgetPosition = CustomizableUI.getPlacementOfWidget(extensionWidgetID).position; // At this point we have an extension in the bookmarks toolbar, and this // toolbar is visible. We are going to resize the window (width) AND // collapse the toolbar to verify that the extension placed in the // bookmarks toolbar is overflowed in the panel without any side effects. }, whenOverflowed: async () => { // Ensure that the toolbar is currently collapsed. await promiseSetToolbarVisibility(bookmarksToolbar, false); Assert.equal( movedNode.parentElement.id, OVERFLOWED_EXTENSIONS_LIST_ID, "expected extension widget to be in the extensions panel" ); Assert.ok( movedNode.getAttribute("artificallyOverflowed"), "expected node to be artifically overflowed" ); // At this point the extension is in the panel because it was overflowed // after the bookmarks toolbar has been collapsed. The window is also // narrow, but we are going to restore the initial window size. Since the // visibility of the bookmarks toolbar hasn't changed, the extension // should still be in the panel. }, afterUnderflowed: async () => { Assert.equal( movedNode.parentElement.id, OVERFLOWED_EXTENSIONS_LIST_ID, "expected extension widget to still be in the extensions panel" ); Assert.ok( movedNode.getAttribute("artificallyOverflowed"), "expected node to still be artifically overflowed" ); // Ensure that the toolbar is visible again, which should move the // extension back to where it was initially. await promiseSetToolbarVisibility(bookmarksToolbar, true); Assert.equal( movedNode.parentElement.id, CustomizableUI.AREA_BOOKMARKS, "expected extension widget to be in the bookmarks toolbar" ); Assert.ok( !movedNode.hasAttribute("artificallyOverflowed"), "expected node to not have any artificallyOverflowed prop" ); Assert.equal( CustomizableUI.getPlacementOfWidget(extensionWidgetID).position, extensionWidgetPosition, "expected the extension to be back at the same position in the bookmarks toolbar" ); }, }); await BrowserTestUtils.closeWindow(win); }); add_task(async function test_overflowed_extension_cannot_be_moved() { let win = await BrowserTestUtils.openNewBrowserWindow(); let extensionID; await withWindowOverflowed(win, { whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => { const secondExtensionWidget = unifiedExtensionList.children[1]; Assert.ok(secondExtensionWidget, "expected an extension widget"); extensionID = secondExtensionWidget.dataset.extensionid; await openExtensionsPanel(win); const contextMenu = await openUnifiedExtensionsContextMenu( extensionID, win ); Assert.ok(contextMenu, "expected a context menu"); const moveUp = contextMenu.querySelector( ".unified-extensions-context-menu-move-widget-up" ); Assert.ok(moveUp, "expected 'move up' item in the context menu"); Assert.ok(moveUp.hidden, "expected 'move up' item to be hidden"); const moveDown = contextMenu.querySelector( ".unified-extensions-context-menu-move-widget-down" ); Assert.ok(moveDown, "expected 'move down' item in the context menu"); Assert.ok(moveDown.hidden, "expected 'move down' item to be hidden"); await closeChromeContextMenu(contextMenu.id, null, win); await closeExtensionsPanel(win); }, }); await BrowserTestUtils.closeWindow(win); });