diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/customizableui/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
158 files changed, 17042 insertions, 0 deletions
diff --git a/browser/components/customizableui/test/CustomizableUITestUtils.sys.mjs b/browser/components/customizableui/test/CustomizableUITestUtils.sys.mjs new file mode 100644 index 0000000000..2cb4e13f99 --- /dev/null +++ b/browser/components/customizableui/test/CustomizableUITestUtils.sys.mjs @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Shared functions generally available for tests involving PanelMultiView and + * the CustomizableUI elements in the browser window. + */ + +import { Assert } from "resource://testing-common/Assert.sys.mjs"; + +import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; +import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", +}); + +export class CustomizableUITestUtils { + /** + * Constructs an instance that operates with the specified browser window. + */ + constructor(window) { + this.window = window; + this.document = window.document; + this.PanelUI = window.PanelUI; + } + + /** + * Opens a closed PanelMultiView via the specified function while waiting for + * the main view with the specified ID to become fully interactive. + */ + async openPanelMultiView(panel, mainView, openFn) { + if (panel.state == "open") { + // Some tests may intermittently leave the panel open. We report this, but + // don't fail so we don't introduce new intermittent test failures. + Assert.ok( + true, + "A previous test left the panel open. This should be" + + " fixed, but we can still do a best-effort recovery and" + + " assume that the requested view will be made visible." + ); + await openFn(); + return; + } + + if (panel.state == "hiding") { + // There may still be tests that don't wait after invoking a command that + // causes the main menu panel to close. Depending on timing, the panel may + // or may not be fully closed when the following test runs. We handle this + // case gracefully so we don't risk introducing new intermittent test + // failures that may show up at a later time. + Assert.ok( + true, + "A previous test requested the panel to close but" + + " didn't wait for the operation to complete. While" + + " the test should be fixed, we can still continue." + ); + } else { + Assert.equal(panel.state, "closed", "The panel is closed to begin with."); + } + + let promiseShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); + await openFn(); + await promiseShown; + } + + /** + * Closes an open PanelMultiView via the specified function while waiting for + * the operation to complete. + */ + async hidePanelMultiView(panel, closeFn) { + Assert.ok(panel.state == "open", "The panel is open to begin with."); + + let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + await closeFn(); + await promiseHidden; + } + + /** + * Opens the main menu and waits for it to become fully interactive. + */ + async openMainMenu() { + await this.openPanelMultiView( + this.PanelUI.panel, + this.PanelUI.mainView, + () => this.PanelUI.show() + ); + } + + /** + * Closes the main menu and waits for the operation to complete. + */ + async hideMainMenu() { + await this.hidePanelMultiView(this.PanelUI.panel, () => + this.PanelUI.hide() + ); + } + + /** + * Add the search bar into the nav bar and verify it does not overflow. + * + * @returns {Promise} + * @resolves The search bar element. + * @rejects If search bar is not found, or overflows. + */ + async addSearchBar() { + lazy.CustomizableUI.addWidgetToArea( + "search-container", + lazy.CustomizableUI.AREA_NAVBAR, + lazy.CustomizableUI.getPlacementOfWidget("urlbar-container").position + 1 + ); + + // addWidgetToArea adds the search bar into the nav bar first. If the + // search bar overflows, OverflowableToolbar for the nav bar moves the + // search bar into the overflow panel in its overflow event handler + // asynchronously. + // + // We should first wait for the layout flush to make sure either the search + // bar fits into the nav bar, or overflow event gets dispatched and the + // overflow event handler is called. + await this.window.promiseDocumentFlushed(() => {}); + + // Check if the OverflowableToolbar is handling the overflow event. + let navbar = this.window.document.getElementById( + lazy.CustomizableUI.AREA_NAVBAR + ); + await TestUtils.waitForCondition(() => { + return !navbar.overflowable.isHandlingOverflow(); + }); + + let searchbar = this.window.document.getElementById("searchbar"); + if (!searchbar) { + throw new Error("The search bar should exist."); + } + + // If the search bar overflows, it's placed inside the overflow panel. + // + // We cannot use navbar's property to check if overflow happens, since it + // can be different widget than the search bar that overflows. + if (searchbar.closest("#widget-overflow")) { + throw new Error( + "The search bar should not overflow from the nav bar. " + + "This test fails if the screen resolution is small and " + + "the search bar overflows from the nav bar." + ); + } + + return searchbar; + } + + removeSearchBar() { + lazy.CustomizableUI.removeWidgetFromArea("search-container"); + } +} diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini new file mode 100644 index 0000000000..180f8ebde2 --- /dev/null +++ b/browser/components/customizableui/test/browser.ini @@ -0,0 +1,210 @@ +[DEFAULT] +support-files = + head.js + support/test_967000_charEncoding_page.html + +[browser_1003588_no_specials_in_panel.js] +[browser_1008559_anchor_undo_restore.js] +[browser_1042100_default_placements_update.js] +[browser_1058573_showToolbarsDropdown.js] +[browser_1087303_button_fullscreen.js] +tags = fullscreen +skip-if = os == "mac" +[browser_1087303_button_preferences.js] +[browser_1089591_still_customizable_after_reset.js] +[browser_1096763_seen_widgets_post_reset.js] +[browser_1161838_inserted_new_default_buttons.js] +skip-if = verify +[browser_1484275_PanelMultiView_toggle_with_other_popup.js] +[browser_1701883_restore_defaults_pocket_pref.js] +[browser_1702200_PanelMultiView_header_separator.js] +[browser_1795260_searchbar_overflow_toolbar.js] +tags = overflowable-toolbar +[browser_694291_searchbar_preference.js] +[browser_873501_handle_specials.js] +[browser_876926_customize_mode_wrapping.js] +skip-if = + (os == "linux") && !debug # Bug 1682752 +[browser_876944_customize_mode_create_destroy.js] +[browser_877006_missing_view.js] +[browser_877178_unregisterArea.js] +[browser_877447_skip_missing_ids.js] +[browser_878452_drag_to_panel.js] +[browser_884402_customize_from_overflow.js] +tags = overflowable-toolbar +[browser_885052_customize_mode_observers_disabed.js] +tags = fullscreen + +[browser_885530_showInPrivateBrowsing.js] +[browser_886323_buildArea_removable_nodes.js] +[browser_890262_destroyWidget_after_add_to_panel.js] +[browser_892955_isWidgetRemovable_for_removed_widgets.js] +[browser_892956_destroyWidget_defaultPlacements.js] +[browser_901207_searchbar_in_panel.js] +[browser_909779_overflow_toolbars_new_window.js] +tags = overflowable-toolbar +skip-if = os == "linux" + +[browser_913972_currentset_overflow.js] +tags = overflowable-toolbar +[browser_914138_widget_API_overflowable_toolbar.js] +tags = overflowable-toolbar +skip-if = os == "linux" + +[browser_918049_skipintoolbarset_dnd.js] +[browser_923857_customize_mode_event_wrapping_during_reset.js] +[browser_927717_customize_drag_empty_toolbar.js] +[browser_934113_menubar_removable.js] +# Because this test is about the menubar, it can't be run on mac +skip-if = os == "mac" + +[browser_934951_zoom_in_toolbar.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_938980_navbar_collapsed.js] +[browser_938995_indefaultstate_nonremovable.js] +[browser_940013_registerToolbarNode_calls_registerArea.js] +[browser_940307_panel_click_closure_handling.js] +skip-if = (verify && debug && (os == 'linux')) +[browser_940946_removable_from_navbar_customizemode.js] +[browser_941083_invalidate_wrapper_cache_createWidget.js] +skip-if = verify +[browser_942581_unregisterArea_keeps_placements.js] +[browser_944887_destroyWidget_should_destroy_in_palette.js] +[browser_945739_showInPrivateBrowsing_customize_mode.js] +[browser_947914_button_copy.js] +[browser_947914_button_cut.js] +[browser_947914_button_find.js] +[browser_947914_button_history.js] +https_first_disabled = true +support-files = dummy_history_item.html +[browser_947914_button_newPrivateWindow.js] +[browser_947914_button_newWindow.js] +[browser_947914_button_paste.js] +[browser_947914_button_print.js] +[browser_947914_button_zoomIn.js] +[browser_947914_button_zoomOut.js] +[browser_947914_button_zoomReset.js] +skip-if = (os == "linux" && debug) # Intermittent failures +[browser_947987_removable_default.js] +[browser_948985_non_removable_defaultArea.js] +[browser_952963_areaType_getter_no_area.js] +skip-if = verify +[browser_956602_remove_special_widget.js] +[browser_962069_drag_to_overflow_chevron.js] +tags = overflowable-toolbar +[browser_963639_customizing_attribute_non_customizable_toolbar.js] +[browser_968565_insert_before_hidden_items.js] +[browser_969427_recreate_destroyed_widget_after_reset.js] +[browser_969661_character_encoding_navbar_disabled.js] +[browser_970511_undo_restore_default.js] +skip-if = verify +[browser_972267_customizationchange_events.js] +[browser_976792_insertNodeInWindow.js] +tags = overflowable-toolbar +skip-if = os == "linux" +[browser_978084_dragEnd_after_move.js] +skip-if = verify +[browser_980155_add_overflow_toolbar.js] +tags = overflowable-toolbar +skip-if = verify +[browser_981305_separator_insertion.js] +[browser_981418-widget-onbeforecreated-handler.js] +skip-if = verify +[browser_982656_restore_defaults_builtin_widgets.js] +[browser_984455_bookmarks_items_reparenting.js] +[browser_985815_propagate_setToolbarVisibility.js] +[browser_987177_destroyWidget_xul.js] +skip-if = verify +[browser_987177_xul_wrapper_updating.js] +[browser_987492_window_api.js] +[browser_987640_charEncoding.js] +[browser_989338_saved_placements_not_resaved.js] +[browser_989751_subviewbutton_class.js] +[browser_992747_toggle_noncustomizable_toolbar.js] +[browser_993322_widget_notoolbar.js] +skip-if = verify +[browser_995164_registerArea_during_customize_mode.js] +[browser_996364_registerArea_different_properties.js] +[browser_996635_remove_non_widgets.js] + +# Unit tests for the PanelMultiView module. These are independent from +# CustomizableUI, but are located here together with the module they're testing. +[browser_PanelMultiView.js] +[browser_PanelMultiView_focus.js] +[browser_PanelMultiView_keyboard.js] +[browser_addons_area.js] +[browser_allow_dragging_removable_false.js] +[browser_backfwd_enabled_post_customize.js] +[browser_bookmarks_empty_message.js] +[browser_bookmarks_toolbar_collapsed_restore_default.js] +[browser_bookmarks_toolbar_shown_newtab.js] +[browser_bootstrapped_custom_toolbar.js] +[browser_check_tooltips_in_navbar.js] +[browser_create_button_widget.js] +[browser_ctrl_click_panel_opening.js] +[browser_currentset_post_reset.js] +[browser_customization_context_menus.js] +[browser_customizemode_contextmenu_menubuttonstate.js] +[browser_customizemode_lwthemes.js] +[browser_customizemode_uidensity.js] +[browser_disable_commands_customize.js] +[browser_drag_outside_palette.js] +[browser_editcontrols_update.js] +[browser_exit_background_customize_mode.js] +https_first_disabled = true +[browser_flexible_space_area.js] +[browser_help_panel_cloning.js] +[browser_hidden_widget_overflow.js] +[browser_history_after_appMenu.js] +[browser_history_recently_closed.js] +[browser_history_recently_closed_middleclick.js] +https_first_disabled = true +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_history_restore_session.js] +[browser_insert_before_moved_node.js] +[browser_menubar_visibility.js] +skip-if = os == "mac" # no toggle-able menubar on macOS. +[browser_newtab_button_customizemode.js] +[browser_open_from_popup.js] +[browser_open_in_lazy_tab.js] +[browser_overflow_use_subviews.js] +tags = overflowable-toolbar +skip-if = verify +[browser_palette_labels.js] +[browser_panelUINotifications.js] +[browser_panelUINotifications_bannerVisibility.js] +[browser_panelUINotifications_fullscreen.js] +tags = fullscreen +skip-if = os == "mac" +[browser_panelUINotifications_fullscreen_noAutoHideToolbar.js] +skip-if = (verify && (os == 'linux' || os == 'mac')) +tags = fullscreen +[browser_panelUINotifications_modals.js] +[browser_panelUINotifications_multiWindow.js] +[browser_panel_keyboard_navigation.js] +[browser_panel_locationSpecific.js] +[browser_panel_toggle.js] +[browser_proton_moreTools_panel.js] +[browser_proton_toolbar_hide_toolbarbuttons.js] +[browser_registerArea.js] +[browser_reload_tab.js] +[browser_remote_attribute.js] +[browser_remote_tabs_button.js] +skip-if = (verify && debug && (os == 'linux' || os == 'mac')) +[browser_remove_customized_specials.js] +[browser_reset_builtin_widget_currentArea.js] +[browser_reset_dom_events.js] +[browser_screenshot_button_disabled.js] +[browser_sidebar_toggle.js] +skip-if = verify +[browser_switch_to_customize_mode.js] +[browser_synced_tabs_menu.js] +[browser_tabbar_big_widgets.js] +[browser_toolbar_collapsed_states.js] +[browser_touchbar_customization.js] +skip-if = (os == "linux" || os == "win") +[browser_unified_extensions_reset.js] +[browser_widget_animation.js] +[browser_widget_recreate_events.js] diff --git a/browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js b/browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js new file mode 100644 index 0000000000..5aa2860827 --- /dev/null +++ b/browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function simulateItemDragAndEnd(aToDrag, aTarget) { + var ds = Cc["@mozilla.org/widget/dragservice;1"].getService( + Ci.nsIDragService + ); + + ds.startDragSessionForTests( + Ci.nsIDragService.DRAGDROP_ACTION_MOVE | + Ci.nsIDragService.DRAGDROP_ACTION_COPY | + Ci.nsIDragService.DRAGDROP_ACTION_LINK + ); + try { + var [result, dataTransfer] = EventUtils.synthesizeDragOver( + aToDrag.parentNode, + aTarget + ); + EventUtils.synthesizeDropAfterDragOver(result, dataTransfer, aTarget); + // Send dragend to move dragging item back to initial place. + EventUtils.sendDragEvent( + { type: "dragend", dataTransfer }, + aToDrag.parentNode + ); + } finally { + ds.endDragSession(true); + } +} + +add_task(async function checkNoAddingToPanel() { + let area = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL; + let previousPlacements = getAreaWidgetIds(area); + CustomizableUI.addWidgetToArea("separator", area); + CustomizableUI.addWidgetToArea("spring", area); + CustomizableUI.addWidgetToArea("spacer", area); + assertAreaPlacements(area, previousPlacements); + + let oldNumberOfItems = previousPlacements.length; + if (getAreaWidgetIds(area).length != oldNumberOfItems) { + CustomizableUI.reset(); + } +}); + +add_task(async function checkAddingToToolbar() { + let area = CustomizableUI.AREA_NAVBAR; + let previousPlacements = getAreaWidgetIds(area); + CustomizableUI.addWidgetToArea("separator", area); + CustomizableUI.addWidgetToArea("spring", area); + CustomizableUI.addWidgetToArea("spacer", area); + let expectedPlacements = [...previousPlacements].concat([ + /separator/, + /spring/, + /spacer/, + ]); + assertAreaPlacements(area, expectedPlacements); + + let newlyAddedElements = getAreaWidgetIds(area).slice(-3); + while (newlyAddedElements.length) { + CustomizableUI.removeWidgetFromArea(newlyAddedElements.shift()); + } + + assertAreaPlacements(area, previousPlacements); + + let oldNumberOfItems = previousPlacements.length; + if (getAreaWidgetIds(area).length != oldNumberOfItems) { + CustomizableUI.reset(); + } +}); + +add_task(async function checkDragging() { + let startArea = CustomizableUI.AREA_TABSTRIP; + let targetArea = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL; + let startingToolbarPlacements = getAreaWidgetIds(startArea); + let startingTargetPlacements = getAreaWidgetIds(targetArea); + + CustomizableUI.addWidgetToArea("separator", startArea); + CustomizableUI.addWidgetToArea("spring", startArea); + CustomizableUI.addWidgetToArea("spacer", startArea); + + let placementsWithSpecials = getAreaWidgetIds(startArea); + let elementsToMove = []; + for (let id of placementsWithSpecials) { + if (CustomizableUI.isSpecialWidget(id)) { + elementsToMove.push(id); + } + } + is(elementsToMove.length, 3, "Should have 3 elements to try and drag."); + + await startCustomizing(); + let existingSpecial = null; + existingSpecial = + gCustomizeMode.visiblePalette.querySelector("toolbarspring"); + ok( + existingSpecial, + "Should have a flexible space in the palette by default in photon" + ); + for (let id of elementsToMove) { + simulateItemDragAndEnd( + document.getElementById(id), + document.getElementById(targetArea) + ); + } + + assertAreaPlacements(startArea, placementsWithSpecials); + assertAreaPlacements(targetArea, startingTargetPlacements); + + for (let id of elementsToMove) { + simulateItemDrag( + document.getElementById(id), + gCustomizeMode.visiblePalette + ); + } + + assertAreaPlacements(startArea, startingToolbarPlacements); + assertAreaPlacements(targetArea, startingTargetPlacements); + + let allSpecials = gCustomizeMode.visiblePalette.querySelectorAll( + "toolbarspring,toolbarseparator,toolbarspacer" + ); + allSpecials = [...allSpecials].filter(special => special != existingSpecial); + ok( + !allSpecials.length, + "No (new) specials should make it to the palette alive." + ); + await endCustomizing(); +}); + +add_task(async function asyncCleanup() { + await endCustomizing(); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js b/browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js new file mode 100644 index 0000000000..a7da97cc95 --- /dev/null +++ b/browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kAnchorAttribute = "cui-anchorid"; + +/** + * Check that anchor gets set correctly when moving an item from the panel to the toolbar + * and into the palette. + */ +add_task(async function () { + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await startCustomizing(); + let button = document.getElementById("history-panelmenu"); + is( + button.getAttribute(kAnchorAttribute), + "nav-bar-overflow-button", + "Button (" + button.id + ") starts out with correct anchor" + ); + + let navbar = CustomizableUI.getCustomizationTarget( + document.getElementById("nav-bar") + ); + let onMouseUp = BrowserTestUtils.waitForEvent(navbar, "mouseup"); + simulateItemDrag(button, navbar); + await onMouseUp; + is( + CustomizableUI.getPlacementOfWidget(button.id).area, + "nav-bar", + "Button (" + button.id + ") ends up in nav-bar" + ); + + ok( + !button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") has no anchor in toolbar" + ); + + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + is( + button.getAttribute(kAnchorAttribute), + "nav-bar-overflow-button", + "Button (" + button.id + ") has anchor again" + ); + + let resetButton = document.getElementById("customization-reset-button"); + ok(!resetButton.hasAttribute("disabled"), "Should be able to reset now."); + await gCustomizeMode.reset(); + + ok( + !button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") once again has no anchor in customize panel" + ); + + await endCustomizing(); +}); + +/** + * Check that anchor gets set correctly when moving an item from the panel to the toolbar + * using 'reset' + */ +add_task(async function () { + await startCustomizing(); + let button = document.getElementById("stop-reload-button"); + ok( + !button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") has no anchor in toolbar" + ); + + let panel = document.getElementById(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL); + let onMouseUp = BrowserTestUtils.waitForEvent(panel, "mouseup"); + simulateItemDrag(button, panel); + await onMouseUp; + is( + CustomizableUI.getPlacementOfWidget(button.id).area, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + "Button (" + button.id + ") ends up in panel" + ); + is( + button.getAttribute(kAnchorAttribute), + "nav-bar-overflow-button", + "Button (" + button.id + ") has correct anchor in the panel" + ); + + let resetButton = document.getElementById("customization-reset-button"); + ok(!resetButton.hasAttribute("disabled"), "Should be able to reset now."); + await gCustomizeMode.reset(); + + ok( + !button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") once again has no anchor in toolbar" + ); + + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_1042100_default_placements_update.js b/browser/components/customizableui/test/browser_1042100_default_placements_update.js new file mode 100644 index 0000000000..c20546c300 --- /dev/null +++ b/browser/components/customizableui/test/browser_1042100_default_placements_update.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function getSavedStatePlacements(area) { + return CustomizableUI.getTestOnlyInternalProp("gSavedState").placements[area]; +} + +// NB: This uses some ugly hacks to get into the CUI module from elsewhere... +// don't try this at home, kids. +function test() { + // Customize something to make sure stuff changed: + CustomizableUI.addWidgetToArea( + "save-page-button", + CustomizableUI.AREA_NAVBAR + ); + + let oldState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + registerCleanupFunction(() => + CustomizableUI.setTestOnlyInternalProp("gSavedState", oldState) + ); + + let gFuturePlacements = + CustomizableUI.getTestOnlyInternalProp("gFuturePlacements"); + is( + gFuturePlacements.size, + 0, + "All future placements should be dealt with by now." + ); + + let gPalette = CustomizableUI.getTestOnlyInternalProp("gPalette"); + let CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + ); + CustomizableUIInternal._updateForNewVersion(); + is(gFuturePlacements.size, 0, "No change to future placements initially."); + + let currentVersion = CustomizableUI.getTestOnlyInternalProp("kVersion"); + + // Add our widget to the defaults: + let testWidgetNew = { + id: "test-messing-with-default-placements-new", + label: "Test messing with default placements - should be inserted", + defaultArea: CustomizableUI.AREA_NAVBAR, + introducedInVersion: currentVersion + 1, + }; + + let normalizedWidget = CustomizableUIInternal.normalizeWidget( + testWidgetNew, + CustomizableUI.SOURCE_BUILTIN + ); + ok(normalizedWidget, "Widget should be normalizable"); + if (!normalizedWidget) { + return; + } + gPalette.set(testWidgetNew.id, normalizedWidget); + + let testWidgetOld = { + id: "test-messing-with-default-placements-old", + label: "Test messing with default placements - should NOT be inserted", + defaultArea: CustomizableUI.AREA_NAVBAR, + introducedInVersion: currentVersion, + }; + + normalizedWidget = CustomizableUIInternal.normalizeWidget( + testWidgetOld, + CustomizableUI.SOURCE_BUILTIN + ); + ok(normalizedWidget, "Widget should be normalizable"); + if (!normalizedWidget) { + return; + } + gPalette.set(testWidgetOld.id, normalizedWidget); + + // Now increase the version in the module: + CustomizableUI.setTestOnlyInternalProp( + "kVersion", + CustomizableUI.getTestOnlyInternalProp("kVersion") + 1 + ); + + let hadSavedState = !!CustomizableUI.getTestOnlyInternalProp("gSavedState"); + if (!hadSavedState) { + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + currentVersion: CustomizableUI.getTestOnlyInternalProp("kVersion") - 1, + }); + } + + // Then call the re-init routine so we re-add the builtin widgets + CustomizableUIInternal._updateForNewVersion(); + is(gFuturePlacements.size, 1, "Should have 1 more future placement"); + let haveNavbarPlacements = gFuturePlacements.has(CustomizableUI.AREA_NAVBAR); + ok(haveNavbarPlacements, "Should have placements for nav-bar"); + if (haveNavbarPlacements) { + let placements = [...gFuturePlacements.get(CustomizableUI.AREA_NAVBAR)]; + + // Ignore widgets that are placed using the pref facility and not the + // versioned facility. They're independent of kVersion and the saved + // state's current version, so they may be present in the placements. + for (let i = 0; i < placements.length; ) { + if (placements[i] == testWidgetNew.id) { + i++; + continue; + } + let pref = "browser.toolbarbuttons.introduced." + placements[i]; + let introduced = Services.prefs.getBoolPref(pref, false); + if (!introduced) { + i++; + continue; + } + placements.splice(i, 1); + } + + is(placements.length, 1, "Should have 1 newly placed widget in nav-bar"); + is( + placements[0], + testWidgetNew.id, + "Should have our test widget to be placed in nav-bar" + ); + } + + // Reset kVersion + CustomizableUI.setTestOnlyInternalProp( + "kVersion", + CustomizableUI.getTestOnlyInternalProp("kVersion") - 1 + ); + + // Now test that the builtin photon migrations work: + + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + currentVersion: 6, + placements: { + "nav-bar": ["urlbar-container", "bookmarks-menu-button"], + "PanelUI-contents": ["panic-button", "edit-controls"], + }, + }); + Services.prefs.setIntPref("browser.proton.toolbar.version", 0); + CustomizableUIInternal._updateForNewVersion(); + CustomizableUIInternal._updateForNewProtonVersion(); + { + let navbarPlacements = getSavedStatePlacements("nav-bar"); + let springs = navbarPlacements.filter(id => id.includes("spring")); + is(springs.length, 2, "Should have 2 toolbarsprings in placements now"); + navbarPlacements = navbarPlacements.filter(id => !id.includes("spring")); + Assert.deepEqual( + navbarPlacements, + [ + "back-button", + "forward-button", + "stop-reload-button", + "urlbar-container", + "downloads-button", + "fxa-toolbar-menu-button", + ], + "Nav-bar placements should be correct." + ); + + Assert.deepEqual(getSavedStatePlacements("widget-overflow-fixed-list"), [ + "panic-button", + ]); + } + + // Finally, test the downloads and fxa avatar button migrations work. + let oldNavbarPlacements = [ + "urlbar-container", + "customizableui-special-spring3", + "search-container", + ]; + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + currentVersion: 10, + placements: { + "nav-bar": Array.from(oldNavbarPlacements), + "widget-overflow-fixed-list": ["downloads-button"], + }, + }); + CustomizableUIInternal._updateForNewVersion(); + Assert.deepEqual( + getSavedStatePlacements("nav-bar"), + oldNavbarPlacements.concat(["downloads-button", "fxa-toolbar-menu-button"]), + "Downloads button inserted in navbar" + ); + Assert.deepEqual( + getSavedStatePlacements("widget-overflow-fixed-list"), + [], + "Overflow panel is empty" + ); + + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + currentVersion: 10, + placements: { + "nav-bar": ["downloads-button"].concat(oldNavbarPlacements), + }, + }); + CustomizableUIInternal._updateForNewVersion(); + Assert.deepEqual( + getSavedStatePlacements("nav-bar"), + oldNavbarPlacements.concat(["downloads-button", "fxa-toolbar-menu-button"]), + "Downloads button reinserted in navbar" + ); + + oldNavbarPlacements = [ + "urlbar-container", + "customizableui-special-spring3", + "search-container", + "other-widget", + ]; + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + currentVersion: 10, + placements: { + "nav-bar": Array.from(oldNavbarPlacements), + }, + }); + CustomizableUIInternal._updateForNewVersion(); + let expectedNavbarPlacements = [ + "urlbar-container", + "customizableui-special-spring3", + "search-container", + "downloads-button", + "other-widget", + "fxa-toolbar-menu-button", + ]; + Assert.deepEqual( + getSavedStatePlacements("nav-bar"), + expectedNavbarPlacements, + "Downloads button inserted in navbar before other widgets" + ); + + gFuturePlacements.delete(CustomizableUI.AREA_NAVBAR); + gPalette.delete(testWidgetNew.id); + gPalette.delete(testWidgetOld.id); +} diff --git a/browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js b/browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js new file mode 100644 index 0000000000..0e57ef8a28 --- /dev/null +++ b/browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + info("Check that toggleable toolbars dropdown in always shown"); + + info("Remove all possible custom toolbars"); + await removeCustomToolbars(); + + info("Enter customization mode"); + await startCustomizing(); + + let toolbarsToggle = document.getElementById( + "customization-toolbar-visibility-button" + ); + ok(toolbarsToggle, "The toolbars toggle dropdown exists"); + ok( + !toolbarsToggle.hasAttribute("hidden"), + "The toolbars toggle dropdown is displayed" + ); +}); + +add_task(async function asyncCleanup() { + info("Exit customization mode"); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_1087303_button_fullscreen.js b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js new file mode 100644 index 0000000000..f67e81b892 --- /dev/null +++ b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +add_task(async function () { + info("Check fullscreen button existence and functionality"); + + CustomizableUI.addWidgetToArea( + "fullscreen-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + + let fullscreenButton = document.getElementById("fullscreen-button"); + ok(fullscreenButton, "Fullscreen button appears in Panel Menu"); + + let fullscreenPromise = promiseFullscreenChange(); + fullscreenButton.click(); + await fullscreenPromise; + + ok(window.fullScreen, "Fullscreen mode was opened"); + + // exit full screen mode + fullscreenPromise = promiseFullscreenChange(); + window.fullScreen = !window.fullScreen; + await fullscreenPromise; + + ok(!window.fullScreen, "Successfully exited fullscreen"); +}); + +function promiseFullscreenChange() { + return new Promise((resolve, reject) => { + info("Wait for fullscreen change"); + + let timeoutId = setTimeout(() => { + window.removeEventListener("fullscreen", onFullscreenChange, true); + reject("Fullscreen change did not happen within " + 20000 + "ms"); + }, 20000); + + function onFullscreenChange(event) { + clearTimeout(timeoutId); + window.removeEventListener("fullscreen", onFullscreenChange, true); + info("Fullscreen event received"); + resolve(); + } + window.addEventListener("fullscreen", onFullscreenChange, true); + }); +} diff --git a/browser/components/customizableui/test/browser_1087303_button_preferences.js b/browser/components/customizableui/test/browser_1087303_button_preferences.js new file mode 100644 index 0000000000..7db48341cb --- /dev/null +++ b/browser/components/customizableui/test/browser_1087303_button_preferences.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +var newTab = null; + +add_task(async function () { + info("Check preferences button existence and functionality"); + CustomizableUI.addWidgetToArea( + "preferences-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let preferencesButton = document.getElementById("preferences-button"); + ok(preferencesButton, "Preferences button exists in Panel Menu"); + preferencesButton.click(); + + newTab = gBrowser.selectedTab; + await waitForPageLoad(newTab); + + let openedPage = gBrowser.currentURI.spec; + is(openedPage, "about:preferences", "Preferences page was opened"); +}); + +add_task(function asyncCleanup() { + if (gBrowser.tabs.length == 1) { + BrowserTestUtils.addTab(gBrowser, "about:blank"); + } + + gBrowser.removeTab(gBrowser.selectedTab); + info("Tabs were restored"); +}); + +function waitForPageLoad(aTab) { + return new Promise((resolve, reject) => { + let timeoutId = setTimeout(() => { + aTab.linkedBrowser.removeEventListener("load", onTabLoad, true); + reject("Page didn't load within " + 20000 + "ms"); + }, 20000); + + async function onTabLoad(event) { + clearTimeout(timeoutId); + aTab.linkedBrowser.removeEventListener("load", onTabLoad, true); + info("Tab event received: load"); + resolve(); + } + + aTab.linkedBrowser.addEventListener("load", onTabLoad, true, true); + }); +} diff --git a/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js b/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js new file mode 100644 index 0000000000..b0bbbd726c --- /dev/null +++ b/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js @@ -0,0 +1,24 @@ +"use strict"; + +// Dragging the elements again after a reset should work +add_task(async function () { + await startCustomizing(); + let historyButton = document.getElementById("wrapper-history-panelmenu"); + let devButton = document.getElementById("wrapper-developer-button"); + + ok(historyButton && devButton, "Draggable elements should exist"); + simulateItemDrag(historyButton, devButton); + await gCustomizeMode.reset(); + ok(CustomizableUI.inDefaultState, "Should be back in default state"); + + historyButton = document.getElementById("wrapper-history-panelmenu"); + devButton = document.getElementById("wrapper-developer-button"); + ok(historyButton && devButton, "Draggable elements should exist"); + simulateItemDrag(historyButton, devButton); + + await endCustomizing(); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js b/browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js new file mode 100644 index 0000000000..74854f499c --- /dev/null +++ b/browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js @@ -0,0 +1,41 @@ +"use strict"; + +const BUTTONID = "test-seenwidget-post-reset"; + +add_task(async function () { + CustomizableUI.createWidget({ + id: BUTTONID, + label: "Test widget seen post reset", + defaultArea: CustomizableUI.AREA_NAVBAR, + }); + + const kPrefCustomizationState = "browser.uiCustomization.state"; + ok( + CustomizableUI.getTestOnlyInternalProp("gSeenWidgets").has(BUTTONID), + "Widget should be seen after createWidget is called." + ); + CustomizableUI.reset(); + ok( + CustomizableUI.getTestOnlyInternalProp("gSeenWidgets").has(BUTTONID), + "Widget should still be seen after reset." + ); + CustomizableUI.addWidgetToArea(BUTTONID, CustomizableUI.AREA_NAVBAR); + gCustomizeMode.removeFromArea(document.getElementById(BUTTONID)); + let hasUserValue = Services.prefs.prefHasUserValue(kPrefCustomizationState); + ok(hasUserValue, "Pref should be set right now."); + if (hasUserValue) { + let seenArray = JSON.parse( + Services.prefs.getCharPref(kPrefCustomizationState) + ).seen; + isnot( + seenArray.indexOf(BUTTONID), + -1, + "Widget should be in saved 'seen' list." + ); + } +}); + +registerCleanupFunction(function () { + CustomizableUI.destroyWidget(BUTTONID); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js b/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js new file mode 100644 index 0000000000..b9501e94f8 --- /dev/null +++ b/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js @@ -0,0 +1,109 @@ +"use strict"; + +// NB: This uses some ugly hacks to get into the CUI module from elsewhere... +// don't try this at home, kids. +function test() { + // Customize something to make sure stuff changed: + CustomizableUI.addWidgetToArea( + "save-page-button", + CustomizableUI.AREA_NAVBAR + ); + + let gFuturePlacements = + CustomizableUI.getTestOnlyInternalProp("gFuturePlacements"); + is( + gFuturePlacements.size, + 0, + "All future placements should be dealt with by now." + ); + + let CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + ); + + // Force us to have a saved state: + CustomizableUIInternal.saveState(); + CustomizableUIInternal.loadSavedState(); + + CustomizableUIInternal._updateForNewVersion(); + is(gFuturePlacements.size, 0, "No change to future placements initially."); + + // Add our widget to the defaults: + let testWidgetNew = { + id: "test-messing-with-default-placements-new-pref", + label: "Test messing with default placements - pref-based", + defaultArea: CustomizableUI.AREA_NAVBAR, + introducedInVersion: "pref", + }; + + let normalizedWidget = CustomizableUIInternal.normalizeWidget( + testWidgetNew, + CustomizableUI.SOURCE_BUILTIN + ); + ok(normalizedWidget, "Widget should be normalizable"); + if (!normalizedWidget) { + return; + } + let gPalette = CustomizableUI.getTestOnlyInternalProp("gPalette"); + gPalette.set(testWidgetNew.id, normalizedWidget); + + // Now adjust default placements for area: + let navbarArea = CustomizableUI.getTestOnlyInternalProp("gAreas").get( + CustomizableUI.AREA_NAVBAR + ); + let navbarPlacements = navbarArea.get("defaultPlacements"); + navbarPlacements.splice( + navbarPlacements.indexOf("bookmarks-menu-button") + 1, + 0, + testWidgetNew.id + ); + + let savedPlacements = + CustomizableUI.getTestOnlyInternalProp("gSavedState").placements[ + CustomizableUI.AREA_NAVBAR + ]; + // Then call the re-init routine so we re-add the builtin widgets + CustomizableUIInternal._updateForNewVersion(); + is(gFuturePlacements.size, 1, "Should have 1 more future placement"); + let futureNavbarPlacements = gFuturePlacements.get( + CustomizableUI.AREA_NAVBAR + ); + ok(futureNavbarPlacements, "Should have placements for nav-bar"); + if (futureNavbarPlacements) { + ok( + futureNavbarPlacements.has(testWidgetNew.id), + "widget should be in future placements" + ); + } + CustomizableUIInternal._placeNewDefaultWidgetsInArea( + CustomizableUI.AREA_NAVBAR + ); + + let indexInSavedPlacements = savedPlacements.indexOf(testWidgetNew.id); + info("Saved placements: " + savedPlacements.join(", ")); + isnot(indexInSavedPlacements, -1, "Widget should have been inserted"); + is( + indexInSavedPlacements, + savedPlacements.indexOf("bookmarks-menu-button") + 1, + "Widget should be in the right place." + ); + + if (futureNavbarPlacements) { + ok( + !futureNavbarPlacements.has(testWidgetNew.id), + "widget should be out of future placements" + ); + } + + if (indexInSavedPlacements != -1) { + savedPlacements.splice(indexInSavedPlacements, 1); + } + + gFuturePlacements.delete(CustomizableUI.AREA_NAVBAR); + let indexInDefaultPlacements = navbarPlacements.indexOf(testWidgetNew.id); + if (indexInDefaultPlacements != -1) { + navbarPlacements.splice(indexInDefaultPlacements, 1); + } + gPalette.delete(testWidgetNew.id); + CustomizableUI.reset(); +} diff --git a/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js b/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js new file mode 100644 index 0000000000..89b86dba20 --- /dev/null +++ b/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = "data:text/html,<html><body></body></html>"; + +/** + * Test steps that may lead to the panel being stuck on Windows (bug 1484275). + */ +add_task(async function test_PanelMultiView_toggle_with_other_popup() { + // For proper cleanup, create a bookmark that we will remove later. + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: TEST_URL, + }); + registerCleanupFunction(() => PlacesUtils.bookmarks.remove(bookmark)); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL, + }, + async function (browser) { + // 1. Open the main menu. + await gCUITestUtils.openMainMenu(); + + // 2. Open another popup not managed by PanelMultiView. + StarUI._createPanelIfNeeded(); + let bookmarkPanel = document.getElementById("editBookmarkPanel"); + let shown = BrowserTestUtils.waitForEvent(bookmarkPanel, "popupshown"); + let hidden = BrowserTestUtils.waitForEvent(bookmarkPanel, "popuphidden"); + EventUtils.synthesizeKey("D", { accelKey: true }); + await shown; + + // 3. Click the button to which the main menu is anchored. We need a native + // mouse event to simulate the exact platform behavior with popups. + let clickFn = () => + EventUtils.promiseNativeMouseEventAndWaitForEvent({ + type: "click", + target: document.getElementById("PanelUI-button"), + atCenter: true, + eventTypeToWait: "mouseup", + }); + + // On Windows and macOS, the operation will close both popups. + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + await gCUITestUtils.hidePanelMultiView(PanelUI.panel, clickFn); + await new Promise(resolve => executeSoon(resolve)); + + // 4. Test that the popup can be opened again after it's been closed. + await gCUITestUtils.openMainMenu(); + Assert.equal(PanelUI.panel.state, "open"); + } else { + // On other platforms, the operation will close both popups and reopen the + // main menu immediately, so we wait for the reopen to occur. + shown = BrowserTestUtils.waitForEvent(PanelUI.mainView, "ViewShown"); + clickFn(); + await shown; + } + + await gCUITestUtils.hideMainMenu(); + + // Make sure the events for the bookmarks panel have also been processed + // before closing the tab and removing the bookmark. + await hidden; + } + ); +}); diff --git a/browser/components/customizableui/test/browser_1701883_restore_defaults_pocket_pref.js b/browser/components/customizableui/test/browser_1701883_restore_defaults_pocket_pref.js new file mode 100644 index 0000000000..a2085958fd --- /dev/null +++ b/browser/components/customizableui/test/browser_1701883_restore_defaults_pocket_pref.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Turning off Pocket pref should still be considered default state. +add_task(async function () { + ok(CustomizableUI.inDefaultState, "Default state to begin"); + + Assert.ok( + Services.prefs.getBoolPref("extensions.pocket.enabled"), + "Pocket feature is enabled by default" + ); + + Services.prefs.setBoolPref("extensions.pocket.enabled", false); + + ok(CustomizableUI.inDefaultState, "Should still be default state"); + await resetCustomization(); + + Assert.ok( + !Services.prefs.getBoolPref("extensions.pocket.enabled"), + "Pocket feature is still off" + ); + ok(CustomizableUI.inDefaultState, "Should still be default state"); + + Services.prefs.setBoolPref("extensions.pocket.enabled", true); +}); diff --git a/browser/components/customizableui/test/browser_1702200_PanelMultiView_header_separator.js b/browser/components/customizableui/test/browser_1702200_PanelMultiView_header_separator.js new file mode 100644 index 0000000000..471d33c37a --- /dev/null +++ b/browser/components/customizableui/test/browser_1702200_PanelMultiView_header_separator.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests whether the separator insertion works correctly for the + * special case where we remove the content except the header itself + * before showing the panel view. + */ + +const TEST_SITE = "http://127.0.0.1"; +const RECENTLY_CLOSED_TABS_PANEL_ID = "appMenu-library-recentlyClosedTabs"; +const RECENTLY_CLOSED_TABS_ITEM_ID = "appMenuRecentlyClosedTabs"; + +function assertHeaderSeparator() { + let header = document.querySelector( + `#${RECENTLY_CLOSED_TABS_PANEL_ID} .panel-header` + ); + Assert.equal( + header.nextSibling.tagName, + "toolbarseparator", + "toolbarseparator should be shown below header" + ); +} + +/** + * Open and close a tab so we can access the "Recently + * closed tabs" panel + */ +add_task(async function test_setup() { + let tab = BrowserTestUtils.addTab(gBrowser, TEST_SITE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser, false, null, true); + await BrowserTestUtils.removeTab(tab); +}); + +/** + * Tests whether the toolbarseparator is shown correctly + * after re-entering same sub view, see bug 1702200 + * + * - App Menu + * - History + * - Recently closed tabs + */ +add_task(async function test_header_toolbarseparator() { + await gCUITestUtils.openMainMenu(); + + let historyView = PanelMultiView.getViewNode(document, "PanelUI-history"); + document.getElementById("appMenu-history-button").click(); + await BrowserTestUtils.waitForEvent(historyView, "ViewShown"); + + // Open Recently Closed Tabs and make sure there is a header separator + let closedTabsView = PanelMultiView.getViewNode( + document, + RECENTLY_CLOSED_TABS_PANEL_ID + ); + Assert.ok(!document.getElementById(RECENTLY_CLOSED_TABS_ITEM_ID).disabled); + document.getElementById(RECENTLY_CLOSED_TABS_ITEM_ID).click(); + await BrowserTestUtils.waitForEvent(closedTabsView, "ViewShown"); + assertHeaderSeparator(); + + // Go back and re-open the same view, header separator should be + // re-added as well + document + .querySelector(`#${RECENTLY_CLOSED_TABS_PANEL_ID} .subviewbutton-back`) + .click(); + await BrowserTestUtils.waitForEvent(historyView, "ViewShown"); + document.getElementById(RECENTLY_CLOSED_TABS_ITEM_ID).click(); + await BrowserTestUtils.waitForEvent(closedTabsView, "ViewShown"); + assertHeaderSeparator(); + + await gCUITestUtils.hideMainMenu(); +}); diff --git a/browser/components/customizableui/test/browser_1795260_searchbar_overflow_toolbar.js b/browser/components/customizableui/test/browser_1795260_searchbar_overflow_toolbar.js new file mode 100644 index 0000000000..2191b153cb --- /dev/null +++ b/browser/components/customizableui/test/browser_1795260_searchbar_overflow_toolbar.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const WIDGET_ID = "search-container"; + +registerCleanupFunction(() => { + CustomizableUI.reset(); + Services.prefs.clearUserPref("browser.search.widget.inNavBar"); +}); + +add_task(async function test_syncPreferenceWithWidget() { + // Move the searchbar to the nav bar. + CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR); + + let container = document.getElementById(WIDGET_ID); + // Set a disproportionately large width, which could be from a saved bigger + // window, or what not. + let width = window.innerWidth * 2; + container.setAttribute("width", width); + container.style.width = `${width}px`; + + // Stuff shouldn't overflow. + ok( + container.getBoundingClientRect().width < window.innerWidth, + "Searchbar shouldn't overflow" + ); +}); diff --git a/browser/components/customizableui/test/browser_694291_searchbar_preference.js b/browser/components/customizableui/test/browser_694291_searchbar_preference.js new file mode 100644 index 0000000000..f65d8f0adc --- /dev/null +++ b/browser/components/customizableui/test/browser_694291_searchbar_preference.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const WIDGET_ID = "search-container"; +const PREF_NAME = "browser.search.widget.inNavBar"; + +function checkDefaults() { + ok(!Services.prefs.getBoolPref(PREF_NAME)); + is(CustomizableUI.getPlacementOfWidget(WIDGET_ID), null); +} + +add_task(async function test_defaults() { + // Verify the default state before the first test. + checkDefaults(); +}); + +add_task(async function test_syncPreferenceWithWidget() { + // Moving the widget to any position in the navigation toolbar should turn the + // preference to true. + CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR); + ok(Services.prefs.getBoolPref(PREF_NAME)); + + // Moving the widget to any position outside of the navigation toolbar should + // turn the preference back to false. + CustomizableUI.addWidgetToArea( + WIDGET_ID, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + ok(!Services.prefs.getBoolPref(PREF_NAME)); +}); + +add_task(async function test_syncWidgetWithPreference() { + // setting the preference should move the widget to the navigation toolbar and + // place it right after the location bar. + Services.prefs.setBoolPref(PREF_NAME, true); + let placement = CustomizableUI.getPlacementOfWidget(WIDGET_ID); + is(placement.area, CustomizableUI.AREA_NAVBAR); + is( + placement.position, + CustomizableUI.getPlacementOfWidget("urlbar-container").position + 1 + ); + + // This should move the widget back to the customization palette. + Services.prefs.setBoolPref(PREF_NAME, false); + checkDefaults(); +}); diff --git a/browser/components/customizableui/test/browser_873501_handle_specials.js b/browser/components/customizableui/test/browser_873501_handle_specials.js new file mode 100644 index 0000000000..1711aee392 --- /dev/null +++ b/browser/components/customizableui/test/browser_873501_handle_specials.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbarName = "test-specials-toolbar"; + +registerCleanupFunction(removeCustomToolbars); + +// Add a toolbar with two springs and the downloads button. +add_task(async function addToolbarWith2SpringsAndDownloadsButton() { + // Create the toolbar with a single spring: + createToolbarWithPlacements(kToolbarName, ["spring"]); + ok(document.getElementById(kToolbarName), "Toolbar should be created."); + + // Check it's there with a generated ID: + assertAreaPlacements(kToolbarName, [/customizableui-special-spring\d+/]); + let [springId] = getAreaWidgetIds(kToolbarName); + + // Add a second spring, check if that's there and doesn't share IDs + CustomizableUI.addWidgetToArea("spring", kToolbarName); + assertAreaPlacements(kToolbarName, [ + springId, + /customizableui-special-spring\d+/, + ]); + let [, spring2Id] = getAreaWidgetIds(kToolbarName); + + isnot(springId, spring2Id, "Springs shouldn't have identical IDs."); + + // Try moving the downloads button to this new toolbar, between the two springs: + CustomizableUI.addWidgetToArea("downloads-button", kToolbarName, 1); + assertAreaPlacements(kToolbarName, [springId, "downloads-button", spring2Id]); + await removeCustomToolbars(); +}); + +// Add separators around the downloads button. +add_task(async function addSeparatorsAroundDownloadsButton() { + createToolbarWithPlacements(kToolbarName, ["separator"]); + ok(document.getElementById(kToolbarName), "Toolbar should be created."); + + // Check it's there with a generated ID: + assertAreaPlacements(kToolbarName, [/customizableui-special-separator\d+/]); + let [separatorId] = getAreaWidgetIds(kToolbarName); + + CustomizableUI.addWidgetToArea("separator", kToolbarName); + assertAreaPlacements(kToolbarName, [ + separatorId, + /customizableui-special-separator\d+/, + ]); + let [, separator2Id] = getAreaWidgetIds(kToolbarName); + + isnot(separatorId, separator2Id, "Separator ids shouldn't be equal."); + + CustomizableUI.addWidgetToArea("downloads-button", kToolbarName, 1); + assertAreaPlacements(kToolbarName, [ + separatorId, + "downloads-button", + separator2Id, + ]); + await removeCustomToolbars(); +}); + +// Add spacers around the downloads button. +add_task(async function addSpacersAroundDownloadsButton() { + createToolbarWithPlacements(kToolbarName, ["spacer"]); + ok(document.getElementById(kToolbarName), "Toolbar should be created."); + + // Check it's there with a generated ID: + assertAreaPlacements(kToolbarName, [/customizableui-special-spacer\d+/]); + let [spacerId] = getAreaWidgetIds(kToolbarName); + + CustomizableUI.addWidgetToArea("spacer", kToolbarName); + assertAreaPlacements(kToolbarName, [ + spacerId, + /customizableui-special-spacer\d+/, + ]); + let [, spacer2Id] = getAreaWidgetIds(kToolbarName); + + isnot(spacerId, spacer2Id, "Spacer ids shouldn't be equal."); + + CustomizableUI.addWidgetToArea("downloads-button", kToolbarName, 1); + assertAreaPlacements(kToolbarName, [spacerId, "downloads-button", spacer2Id]); + await removeCustomToolbars(); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js b/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js new file mode 100644 index 0000000000..cf73326e53 --- /dev/null +++ b/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kXULWidgetId = "a-test-button"; // we'll create a button with this ID. +const kAPIWidgetId = "save-page-button"; +const kPanel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL; +const kToolbar = CustomizableUI.AREA_NAVBAR; +const kVisiblePalette = "customization-palette"; + +function checkWrapper(id) { + is( + document.querySelectorAll("#wrapper-" + id).length, + 1, + "There should be exactly 1 wrapper for " + + id + + " in the customizing window." + ); +} + +async function ensureVisible(node) { + let isInPalette = node.parentNode.parentNode == gNavToolbox.palette; + if (isInPalette) { + node.scrollIntoView(); + } + let dwu = window.windowUtils; + await BrowserTestUtils.waitForCondition(() => { + let nodeBounds = dwu.getBoundsWithoutFlushing(node); + if (isInPalette) { + let paletteBounds = dwu.getBoundsWithoutFlushing(gNavToolbox.palette); + if ( + !( + nodeBounds.top >= paletteBounds.top && + nodeBounds.bottom <= paletteBounds.bottom + ) + ) { + return false; + } + } + return nodeBounds.height && nodeBounds.width; + }); +} + +var move = { + async drag(id, target) { + let targetNode = document.getElementById(target); + if (CustomizableUI.getCustomizationTarget(targetNode)) { + targetNode = CustomizableUI.getCustomizationTarget(targetNode); + } + let nodeToMove = document.getElementById(id); + await ensureVisible(nodeToMove); + + simulateItemDrag(nodeToMove, targetNode, "end"); + }, + async dragToItem(id, target) { + let targetNode = document.getElementById(target); + if (CustomizableUI.getCustomizationTarget(targetNode)) { + targetNode = CustomizableUI.getCustomizationTarget(targetNode); + } + let items = targetNode.querySelectorAll("toolbarpaletteitem"); + if (target == kPanel) { + targetNode = items[items.length - 1]; + } else { + targetNode = items[0]; + } + let nodeToMove = document.getElementById(id); + await ensureVisible(nodeToMove); + simulateItemDrag(nodeToMove, targetNode, "start"); + }, + API(id, target) { + if (target == kVisiblePalette) { + return CustomizableUI.removeWidgetFromArea(id); + } + return CustomizableUI.addWidgetToArea(id, target, null); + }, +}; + +function isLast(containerId, defaultPlacements, id) { + assertAreaPlacements(containerId, defaultPlacements.concat([id])); + let thisTarget = CustomizableUI.getCustomizationTarget( + document.getElementById(containerId) + ); + is( + thisTarget.lastElementChild.firstElementChild.id, + id, + "Widget " + id + " should be in " + containerId + " in customizing window." + ); + let otherTarget = CustomizableUI.getCustomizationTarget( + otherWin.document.getElementById(containerId) + ); + is( + otherTarget.lastElementChild.id, + id, + "Widget " + id + " should be in " + containerId + " in other window." + ); +} + +function getLastVisibleNodeInToolbar(containerId, win = window) { + let container = CustomizableUI.getCustomizationTarget( + win.document.getElementById(containerId) + ); + let rv = container.lastElementChild; + while (rv?.hidden || rv?.firstElementChild?.hidden) { + rv = rv.previousElementSibling; + } + return rv; +} + +function isLastVisibleInToolbar(containerId, defaultPlacements, id) { + let newPlacements; + for (let i = defaultPlacements.length - 1; i >= 0; i--) { + let el = document.getElementById(defaultPlacements[i]); + if (el && !el.hidden) { + newPlacements = [...defaultPlacements]; + newPlacements.splice(i + 1, 0, id); + break; + } + } + if (!newPlacements) { + assertAreaPlacements(containerId, defaultPlacements.concat([id])); + } else { + assertAreaPlacements(containerId, newPlacements); + } + is( + getLastVisibleNodeInToolbar(containerId).firstElementChild.id, + id, + "Widget " + id + " should be in " + containerId + " in customizing window." + ); + is( + getLastVisibleNodeInToolbar(containerId, otherWin).id, + id, + "Widget " + id + " should be in " + containerId + " in other window." + ); +} + +function isFirst(containerId, defaultPlacements, id) { + assertAreaPlacements(containerId, [id].concat(defaultPlacements)); + let thisTarget = CustomizableUI.getCustomizationTarget( + document.getElementById(containerId) + ); + is( + thisTarget.firstElementChild.firstElementChild.id, + id, + "Widget " + id + " should be in " + containerId + " in customizing window." + ); + let otherTarget = CustomizableUI.getCustomizationTarget( + otherWin.document.getElementById(containerId) + ); + is( + otherTarget.firstElementChild.id, + id, + "Widget " + id + " should be in " + containerId + " in other window." + ); +} + +async function checkToolbar(id, method) { + // Place at start of the toolbar: + let toolbarPlacements = getAreaWidgetIds(kToolbar); + await move[method](id, kToolbar); + if (method == "dragToItem") { + isFirst(kToolbar, toolbarPlacements, id); + } else if (method == "drag") { + isLastVisibleInToolbar(kToolbar, toolbarPlacements, id); + } else { + isLast(kToolbar, toolbarPlacements, id); + } + checkWrapper(id); +} + +async function checkPanel(id, method) { + let panelPlacements = getAreaWidgetIds(kPanel); + await move[method](id, kPanel); + let children = document + .getElementById(kPanel) + .querySelectorAll("toolbarpaletteitem"); + let otherChildren = otherWin.document.getElementById(kPanel).children; + let newPlacements = panelPlacements.concat([id]); + // Relative position of the new item from the end: + let position = -1; + // For the drag to item case, we drag to the last item, making the dragged item the + // penultimate item. We can't well use the first item because the panel has complicated + // rules about rearranging wide items (which, by default, the first two items are). + if (method == "dragToItem") { + newPlacements.pop(); + newPlacements.splice(panelPlacements.length - 1, 0, id); + position = -2; + } + assertAreaPlacements(kPanel, newPlacements); + is( + children[children.length + position].firstElementChild.id, + id, + "Widget " + id + " should be in " + kPanel + " in customizing window." + ); + is( + otherChildren[otherChildren.length + position].id, + id, + "Widget " + id + " should be in " + kPanel + " in other window." + ); + checkWrapper(id); +} + +async function checkPalette(id, method) { + // Move back to palette: + await move[method](id, kVisiblePalette); + ok(CustomizableUI.inDefaultState, "Should end in default state"); + let visibleChildren = gCustomizeMode.visiblePalette.children; + let expectedChild = + method == "dragToItem" + ? visibleChildren[0] + : visibleChildren[visibleChildren.length - 1]; + // Items dragged to the end of the palette should be the final item. That they're the penultimate + // item when dragged is tracked in bug 1395950. Once that's fixed, this hack can be removed. + if (method == "drag") { + expectedChild = expectedChild.previousElementSibling; + } + is( + expectedChild.firstElementChild.id, + id, + "Widget " + + id + + " was moved using " + + method + + " and should now be wrapped in palette in customizing window." + ); + if (id == kXULWidgetId) { + ok( + otherWin.gNavToolbox.palette.querySelector("#" + id), + "Widget " + id + " should be in invisible palette in other window." + ); + } + checkWrapper(id); +} + +// This test needs a XUL button that's in the palette by default. No such +// button currently exists, so we create a simple one. +function createXULButtonForWindow(win) { + createDummyXULButton(kXULWidgetId, "test-button", win); +} + +function removeXULButtonForWindow(win) { + win.gNavToolbox.palette.querySelector(`#${kXULWidgetId}`).remove(); +} + +var otherWin; + +// Moving widgets in two windows, one with customize mode and one without, should work. +add_task(async function MoveWidgetsInTwoWindows() { + CustomizableUI.createWidget({ + id: "cui-mode-wrapping-some-panel-item", + label: "Test panel wrapping", + }); + await startCustomizing(); + otherWin = await openAndLoadWindow(null, true); + await otherWin.PanelUI.ensureReady(); + // Create the XUL button to use in the test in both windows. + createXULButtonForWindow(window); + createXULButtonForWindow(otherWin); + ok(CustomizableUI.inDefaultState, "Should start in default state"); + + for (let widgetId of [kXULWidgetId, kAPIWidgetId]) { + for (let method of ["API", "drag", "dragToItem"]) { + info("Moving widget " + widgetId + " using " + method); + await checkToolbar(widgetId, method); + // We add an item to the panel because otherwise we can't test dragging + // to items that are already there. We remove it because + // 'checkPalette' checks that we leave the browser in the default state. + CustomizableUI.addWidgetToArea( + "cui-mode-wrapping-some-panel-item", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await checkPanel(widgetId, method); + CustomizableUI.removeWidgetFromArea("cui-mode-wrapping-some-panel-item"); + await checkPalette(widgetId, method); + CustomizableUI.addWidgetToArea( + "cui-mode-wrapping-some-panel-item", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await checkPanel(widgetId, method); + await checkToolbar(widgetId, method); + CustomizableUI.removeWidgetFromArea("cui-mode-wrapping-some-panel-item"); + await checkPalette(widgetId, method); + } + } + await promiseWindowClosed(otherWin); + otherWin = null; + await endCustomizing(); + removeXULButtonForWindow(window); +}); + +add_task(async function asyncCleanup() { + CustomizableUI.destroyWidget("cui-mode-wrapping-some-panel-item"); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js new file mode 100644 index 0000000000..33eccccbbf --- /dev/null +++ b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kTestWidget1 = "test-customize-mode-create-destroy1"; + +// Creating and destroying a widget should correctly wrap/unwrap stuff +add_task(async function testWrapUnwrap() { + await startCustomizing(); + CustomizableUI.createWidget({ + id: kTestWidget1, + label: "Pretty label", + tooltiptext: "Pretty tooltip", + }); + let elem = document.getElementById(kTestWidget1); + let wrapper = document.getElementById("wrapper-" + kTestWidget1); + ok(elem, "There should be an item"); + ok(wrapper, "There should be a wrapper"); + is( + wrapper.firstElementChild.id, + kTestWidget1, + "Wrapper should have test widget" + ); + is( + wrapper.parentNode.id, + "customization-palette", + "Wrapper should be in palette" + ); + CustomizableUI.destroyWidget(kTestWidget1); + wrapper = document.getElementById("wrapper-" + kTestWidget1); + ok(!wrapper, "There should be a wrapper"); + let item = document.getElementById(kTestWidget1); + ok(!item, "There should no longer be an item"); +}); + +add_task(async function asyncCleanup() { + await endCustomizing(); + try { + CustomizableUI.destroyWidget(kTestWidget1); + } catch (ex) {} + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_877006_missing_view.js b/browser/components/customizableui/test/browser_877006_missing_view.js new file mode 100644 index 0000000000..c01d2f7b35 --- /dev/null +++ b/browser/components/customizableui/test/browser_877006_missing_view.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Should be able to add broken view widget +add_task(function testAddbrokenViewWidget() { + const kWidgetId = "test-877006-broken-widget"; + let widgetSpec = { + id: kWidgetId, + type: "view", + viewId: "idontexist", + /* Empty handler so we try to attach it maybe? */ + onViewShowing() {}, + }; + + let noError = true; + try { + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + } catch (ex) { + console.error(ex); + noError = false; + } + ok( + noError, + "Should not throw an exception trying to add a broken view widget." + ); + + noError = true; + try { + CustomizableUI.destroyWidget(kWidgetId); + } catch (ex) { + console.error(ex); + noError = false; + } + ok( + noError, + "Should not throw an exception trying to remove the broken view widget." + ); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_877178_unregisterArea.js b/browser/components/customizableui/test/browser_877178_unregisterArea.js new file mode 100644 index 0000000000..7b171462ff --- /dev/null +++ b/browser/components/customizableui/test/browser_877178_unregisterArea.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +registerCleanupFunction(removeCustomToolbars); + +// Sanity checks +add_task(function sanityChecks() { + SimpleTest.doesThrow( + () => CustomizableUI.registerArea("@foo"), + "Registering areas with an invalid ID should throw." + ); + + SimpleTest.doesThrow( + () => CustomizableUI.registerArea([]), + "Registering areas with an invalid ID should throw." + ); + + SimpleTest.doesThrow( + () => CustomizableUI.unregisterArea("@foo"), + "Unregistering areas with an invalid ID should throw." + ); + + SimpleTest.doesThrow( + () => CustomizableUI.unregisterArea([]), + "Unregistering areas with an invalid ID should throw." + ); + + SimpleTest.doesThrow( + () => CustomizableUI.unregisterArea("unknown"), + "Unregistering an area that's not registered should throw." + ); +}); + +// Check areas are loaded with their default placements. +add_task(function checkLoadedAres() { + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state." + ); +}); + +// Check registering and unregistering a new area. +add_task(function checkRegisteringAndUnregistering() { + const kToolbarId = "test-registration-toolbar"; + const kButtonId = "test-registration-button"; + createDummyXULButton(kButtonId); + createToolbarWithPlacements(kToolbarId, ["spring", kButtonId, "spring"]); + assertAreaPlacements(kToolbarId, [ + /customizableui-special-spring\d+/, + kButtonId, + /customizableui-special-spring\d+/, + ]); + ok( + !CustomizableUI.inDefaultState, + "With a new toolbar it is no longer in a default state." + ); + removeCustomToolbars(); // Will call unregisterArea for us + ok( + CustomizableUI.inDefaultState, + "When the toolbar is unregistered, " + + "everything will return to the default state." + ); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_877447_skip_missing_ids.js b/browser/components/customizableui/test/browser_877447_skip_missing_ids.js new file mode 100644 index 0000000000..83e7edbba3 --- /dev/null +++ b/browser/components/customizableui/test/browser_877447_skip_missing_ids.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +registerCleanupFunction(removeCustomToolbars); + +add_task(function skipMissingIDS() { + const kButtonId = "look-at-me-disappear-button"; + CustomizableUI.reset(); + ok(CustomizableUI.inDefaultState, "Should be in the default state."); + let btn = createDummyXULButton(kButtonId, "Gone!"); + CustomizableUI.addWidgetToArea(kButtonId, CustomizableUI.AREA_NAVBAR); + ok( + !CustomizableUI.inDefaultState, + "Should no longer be in the default state." + ); + is( + btn.parentNode.parentNode.id, + CustomizableUI.AREA_NAVBAR, + "Button should be in navbar" + ); + btn.remove(); + is(btn.parentNode, null, "Button is no longer in the navbar"); + ok( + CustomizableUI.inDefaultState, + "Should be back in the default state, " + + "despite unknown button ID in placements." + ); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_878452_drag_to_panel.js b/browser/components/customizableui/test/browser_878452_drag_to_panel.js new file mode 100644 index 0000000000..284583c853 --- /dev/null +++ b/browser/components/customizableui/test/browser_878452_drag_to_panel.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +CustomizableUI.createWidget({ + id: "cui-panel-item-to-drag-to", + defaultArea: CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + label: "Item in panel to drag to", +}); + +// Dragging an item from the palette to another button in the panel should work. +add_task(async function () { + await startCustomizing(); + let btn = document.getElementById("new-window-button"); + let placements = getAreaWidgetIds(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL); + + let lastButtonIndex = placements.length - 1; + let lastButton = placements[lastButtonIndex]; + let placementsAfterInsert = placements + .slice(0, lastButtonIndex) + .concat(["new-window-button", lastButton]); + let lastButtonNode = document.getElementById(lastButton); + simulateItemDrag(btn, lastButtonNode, "start"); + assertAreaPlacements( + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + placementsAfterInsert + ); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + CustomizableUI.removeWidgetFromArea("cui-panel-item-to-drag-to"); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); + await endCustomizing(); +}); + +// Dragging an item from the palette to the panel itself should also work. +add_task(async function () { + CustomizableUI.addWidgetToArea( + "cui-panel-item-to-drag-to", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await startCustomizing(); + let btn = document.getElementById("new-window-button"); + let panel = document.getElementById(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL); + let placements = getAreaWidgetIds(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL); + + let placementsAfterAppend = placements.concat(["new-window-button"]); + simulateItemDrag(btn, panel); + assertAreaPlacements( + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + placementsAfterAppend + ); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + CustomizableUI.removeWidgetFromArea("cui-panel-item-to-drag-to"); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); + await endCustomizing(); +}); + +// Dragging an item from the palette to an empty panel should also work. +add_task(async function () { + let widgetIds = getAreaWidgetIds(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL); + while (widgetIds.length) { + CustomizableUI.removeWidgetFromArea(widgetIds.shift()); + } + await startCustomizing(); + let btn = document.getElementById("new-window-button"); + let panel = document.getElementById(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL); + + assertAreaPlacements(panel.id, []); + + let placementsAfterAppend = ["new-window-button"]; + simulateItemDrag(btn, panel); + assertAreaPlacements( + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + placementsAfterAppend + ); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + assertAreaPlacements(panel.id, []); + await endCustomizing(); +}); + +registerCleanupFunction(async function asyncCleanup() { + CustomizableUI.destroyWidget("cui-panel-item-to-drag-to"); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_884402_customize_from_overflow.js b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js new file mode 100644 index 0000000000..583a03db2f --- /dev/null +++ b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js @@ -0,0 +1,117 @@ +"use strict"; + +var overflowPanel = document.getElementById("widget-overflow"); + +var originalWindowWidth; +registerCleanupFunction(function () { + overflowPanel.removeAttribute("animate"); + window.resizeTo(originalWindowWidth, window.outerHeight); + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + return TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +// Right-click on an item within the overflow panel should +// show a context menu with options to move it. +add_task(async function () { + overflowPanel.setAttribute("animate", "false"); + let fxaButton = document.getElementById("fxa-toolbar-menu-button"); + if (BrowserTestUtils.is_hidden(fxaButton)) { + // FxA button is likely hidden since the user is logged out. + let initialFxaStatus = document.documentElement.getAttribute("fxastatus"); + document.documentElement.setAttribute("fxastatus", "signed_in"); + registerCleanupFunction(() => + document.documentElement.setAttribute("fxastatus", initialFxaStatus) + ); + ok(BrowserTestUtils.is_visible(fxaButton), "FxA button is now visible"); + } + + originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let chevron = document.getElementById("nav-bar-overflow-button"); + let shownPanelPromise = promisePanelElementShown(window, overflowPanel); + chevron.click(); + await shownPanelPromise; + + let contextMenu = document.getElementById( + "customizationPanelItemContextMenu" + ); + let shownContextPromise = popupShown(contextMenu); + ok(fxaButton, "fxa-toolbar-menu-button was found"); + is( + fxaButton.getAttribute("overflowedItem"), + "true", + "FxA button is overflowing" + ); + EventUtils.synthesizeMouse(fxaButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownContextPromise; + + is( + overflowPanel.state, + "open", + "The widget overflow panel should still be open." + ); + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", true], + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + let hiddenPromise = promisePanelElementHidden(window, overflowPanel); + let moveToPanel = contextMenu.querySelector(".customize-context-moveToPanel"); + if (moveToPanel) { + contextMenu.activateItem(moveToPanel); + } else { + contextMenu.hidePopup(); + } + await hiddenContextPromise; + await hiddenPromise; + + let fxaButtonPlacement = CustomizableUI.getPlacementOfWidget( + "fxa-toolbar-menu-button" + ); + ok(fxaButtonPlacement, "FxA button should still have a placement"); + is( + fxaButtonPlacement && fxaButtonPlacement.area, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + "FxA button should be pinned now" + ); + CustomizableUI.reset(); + + // In some cases, it can take a tick for the navbar to overflow again. Wait for it: + await TestUtils.waitForCondition(() => + fxaButton.hasAttribute("overflowedItem") + ); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + fxaButtonPlacement = CustomizableUI.getPlacementOfWidget( + "fxa-toolbar-menu-button" + ); + ok(fxaButtonPlacement, "FxA button should still have a placement"); + is( + fxaButtonPlacement && fxaButtonPlacement.area, + "nav-bar", + "FxA button should be back in the navbar now" + ); + + is( + fxaButton.getAttribute("overflowedItem"), + "true", + "FxA button should still be overflowed" + ); +}); diff --git a/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js new file mode 100644 index 0000000000..346608dc99 --- /dev/null +++ b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function isFullscreenSizeMode() { + let sizemode = document.documentElement.getAttribute("sizemode"); + return sizemode == "fullscreen"; +} + +// Observers should be disabled when in customization mode. +add_task(async function () { + CustomizableUI.addWidgetToArea( + "fullscreen-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + // Show the panel so it isn't hidden and has bindings applied etc.: + await document.getElementById("nav-bar").overflowable.show(); + + // Hide it again. + document.getElementById("widget-overflow").hidePopup(); + + let fullscreenButton = document.getElementById("fullscreen-button"); + ok( + !fullscreenButton.checked, + "Fullscreen button should not be checked when not in fullscreen." + ); + ok( + !isFullscreenSizeMode(), + "Should not be in fullscreen sizemode before we enter fullscreen." + ); + + BrowserFullScreen(); + await TestUtils.waitForCondition(() => isFullscreenSizeMode()); + ok( + fullscreenButton.checked, + "Fullscreen button should be checked when in fullscreen." + ); + + await startCustomizing(); + + let fullscreenButtonWrapper = document.getElementById( + "wrapper-fullscreen-button" + ); + ok( + fullscreenButtonWrapper.hasAttribute("itemobserves"), + "Observer should be moved to wrapper" + ); + fullscreenButton = document.getElementById("fullscreen-button"); + ok( + !fullscreenButton.hasAttribute("observes"), + "Observer should be removed from button" + ); + ok( + !fullscreenButton.checked, + "Fullscreen button should no longer be checked during customization mode" + ); + + await endCustomizing(); + + BrowserFullScreen(); + fullscreenButton = document.getElementById("fullscreen-button"); + await TestUtils.waitForCondition(() => !isFullscreenSizeMode()); + ok( + !fullscreenButton.checked, + "Fullscreen button should not be checked when not in fullscreen." + ); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js b/browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js new file mode 100644 index 0000000000..e1f763e2eb --- /dev/null +++ b/browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kWidgetId = "some-widget"; + +function assertWidgetExists(aWindow, aExists) { + if (aExists) { + ok( + aWindow.document.getElementById(kWidgetId), + "Should have found test widget in the window" + ); + } else { + is( + aWindow.document.getElementById(kWidgetId), + null, + "Should not have found test widget in the window" + ); + } +} + +// A widget that is created with showInPrivateBrowsing undefined should +// have that value default to true. +add_task(function () { + let wrapper = CustomizableUI.createWidget({ + id: kWidgetId, + }); + ok( + wrapper.showInPrivateBrowsing, + "showInPrivateBrowsing should have defaulted to true." + ); + CustomizableUI.destroyWidget(kWidgetId); +}); + +// Add a widget via the API with showInPrivateBrowsing set to false +// and ensure it does not appear in pre-existing or newly created +// private windows. +add_task(async function () { + let plain1 = await openAndLoadWindow(); + let private1 = await openAndLoadWindow({ private: true }); + CustomizableUI.createWidget({ + id: kWidgetId, + removable: true, + showInPrivateBrowsing: false, + }); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + assertWidgetExists(plain1, true); + assertWidgetExists(private1, false); + + // Now open up some new windows. The widget should exist in the new + // plain window, but not the new private window. + let plain2 = await openAndLoadWindow(); + let private2 = await openAndLoadWindow({ private: true }); + assertWidgetExists(plain2, true); + assertWidgetExists(private2, false); + + // Try moving the widget around and make sure it doesn't get added + // to the private windows. We'll start by appending it to the tabstrip. + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_TABSTRIP); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + // And then move it to the beginning of the tabstrip. + CustomizableUI.moveWidgetWithinArea(kWidgetId, 0); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + CustomizableUI.removeWidgetFromArea("some-widget"); + assertWidgetExists(plain1, false); + assertWidgetExists(plain2, false); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + await Promise.all( + [plain1, plain2, private1, private2].map(promiseWindowClosed) + ); + + CustomizableUI.destroyWidget("some-widget"); +}); + +// Add a widget via the API with showInPrivateBrowsing set to true, +// and ensure that it appears in pre-existing or newly created +// private browsing windows. +add_task(async function () { + let plain1 = await openAndLoadWindow(); + let private1 = await openAndLoadWindow({ private: true }); + + CustomizableUI.createWidget({ + id: kWidgetId, + removable: true, + showInPrivateBrowsing: true, + }); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + assertWidgetExists(plain1, true); + assertWidgetExists(private1, true); + + // Now open up some new windows. The widget should exist in the new + // plain window, but not the new private window. + let plain2 = await openAndLoadWindow(); + let private2 = await openAndLoadWindow({ private: true }); + + assertWidgetExists(plain2, true); + assertWidgetExists(private2, true); + + // Try moving the widget around and make sure it doesn't get added + // to the private windows. We'll start by appending it to the tabstrip. + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_TABSTRIP); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, true); + assertWidgetExists(private2, true); + + // And then move it to the beginning of the tabstrip. + CustomizableUI.moveWidgetWithinArea(kWidgetId, 0); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, true); + assertWidgetExists(private2, true); + + CustomizableUI.removeWidgetFromArea("some-widget"); + assertWidgetExists(plain1, false); + assertWidgetExists(plain2, false); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + await Promise.all( + [plain1, plain2, private1, private2].map(promiseWindowClosed) + ); + + CustomizableUI.destroyWidget("some-widget"); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js b/browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js new file mode 100644 index 0000000000..99968a8266 --- /dev/null +++ b/browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kButtonId = "test-886323-removable-moved-node"; +const kLazyAreaId = "test-886323-lazy-area-for-removability-testing"; + +var gNavBar = document.getElementById(CustomizableUI.AREA_NAVBAR); +var gLazyArea; + +// Removable nodes shouldn't be moved by buildArea +add_task(async function () { + let dummyBtn = createDummyXULButton(kButtonId, "Dummy"); + dummyBtn.setAttribute("removable", "true"); + CustomizableUI.getCustomizationTarget(gNavBar).appendChild(dummyBtn); + let popupSet = document.getElementById("mainPopupSet"); + gLazyArea = document.createXULElement("panel"); + gLazyArea.id = kLazyAreaId; + gLazyArea.hidden = true; + popupSet.appendChild(gLazyArea); + CustomizableUI.registerArea(kLazyAreaId, { + type: CustomizableUI.TYPE_PANEL, + defaultPlacements: [], + }); + CustomizableUI.addWidgetToArea(kButtonId, kLazyAreaId); + assertAreaPlacements( + kLazyAreaId, + [kButtonId], + "Placements should have changed because widget is removable." + ); + let btn = document.getElementById(kButtonId); + btn.setAttribute("removable", "false"); + gLazyArea._customizationTarget = gLazyArea; + CustomizableUI.registerToolbarNode(gLazyArea, []); + assertAreaPlacements( + kLazyAreaId, + [], + "Placements should no longer include widget." + ); + is( + btn.parentNode.id, + CustomizableUI.getCustomizationTarget(gNavBar).id, + "Button shouldn't actually have moved as it's not removable" + ); + btn = document.getElementById(kButtonId); + if (btn) { + btn.remove(); + } + CustomizableUI.removeWidgetFromArea(kButtonId); + CustomizableUI.unregisterArea(kLazyAreaId); + gLazyArea.remove(); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js b/browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js new file mode 100644 index 0000000000..61e354dd61 --- /dev/null +++ b/browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kLazyAreaId = "test-890262-lazy-area"; +const kWidget1Id = "test-890262-widget1"; +const kWidget2Id = "test-890262-widget2"; + +setupArea(); + +// Destroying a widget after defaulting it to a lazy area should work. +add_task(function () { + CustomizableUI.createWidget({ + id: kWidget1Id, + removable: true, + defaultArea: kLazyAreaId, + }); + let noError = true; + try { + CustomizableUI.destroyWidget(kWidget1Id); + } catch (ex) { + console.error(ex); + noError = false; + } + ok( + noError, + "Shouldn't throw an exception for a widget that was created in a not-yet-constructed area" + ); +}); + +// Destroying a widget after moving it to a lazy area should work. +add_task(function () { + CustomizableUI.createWidget({ + id: kWidget2Id, + removable: true, + defaultArea: CustomizableUI.AREA_NAVBAR, + }); + + CustomizableUI.addWidgetToArea(kWidget2Id, kLazyAreaId); + let noError = true; + try { + CustomizableUI.destroyWidget(kWidget2Id); + } catch (ex) { + console.error(ex); + noError = false; + } + ok( + noError, + "Shouldn't throw an exception for a widget that was added to a not-yet-constructed area" + ); +}); + +add_task(async function asyncCleanup() { + let lazyArea = document.getElementById(kLazyAreaId); + if (lazyArea) { + lazyArea.remove(); + } + try { + CustomizableUI.unregisterArea(kLazyAreaId); + } catch (ex) {} // If we didn't register successfully for some reason + await resetCustomization(); +}); + +function setupArea() { + let lazyArea = document.createXULElement("hbox"); + lazyArea.id = kLazyAreaId; + document.getElementById("nav-bar").appendChild(lazyArea); + CustomizableUI.registerArea(kLazyAreaId, { + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: [], + }); +} diff --git a/browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js b/browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js new file mode 100644 index 0000000000..7584c52bb6 --- /dev/null +++ b/browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kWidgetId = "test-892955-remove-widget"; + +// Removing a destroyed widget should work. +add_task(async function () { + let widgetSpec = { + id: kWidgetId, + defaultArea: CustomizableUI.AREA_NAVBAR, + }; + + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.destroyWidget(kWidgetId); + let noError = true; + try { + CustomizableUI.removeWidgetFromArea(kWidgetId); + } catch (ex) { + noError = false; + console.error(ex); + } + ok(noError, "Shouldn't throw an error removing a destroyed widget."); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js b/browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js new file mode 100644 index 0000000000..7ad68e16d0 --- /dev/null +++ b/browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kWidgetId = "test-892956-destroyWidget-defaultPlacement"; + +// destroyWidget should clean up defaultPlacements if the widget had a defaultArea +add_task(async function () { + ok( + CustomizableUI.inDefaultState, + "Should be in the default state when we start" + ); + + let widgetSpec = { + id: kWidgetId, + defaultArea: CustomizableUI.AREA_NAVBAR, + }; + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.destroyWidget(kWidgetId); + ok( + CustomizableUI.inDefaultState, + "Should be in the default state when we finish" + ); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js new file mode 100644 index 0000000000..1576e10cec --- /dev/null +++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +logActiveElement(); + +async function waitForSearchBarFocus() { + let searchbar = document.getElementById("searchbar"); + await TestUtils.waitForCondition(function () { + logActiveElement(); + return document.activeElement === searchbar.textbox; + }); +} + +// Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel. +add_task(async function check_shortcut_when_in_closed_overflow_panel_closed() { + CustomizableUI.addWidgetToArea( + "search-container", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + let shownPanelPromise = promiseOverflowShown(window); + sendWebSearchKeyCommand(); + await shownPanelPromise; + + await waitForSearchBarFocus(); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("KEY_Escape"); + await hiddenPanelPromise; + CustomizableUI.reset(); +}); + +// Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened. +add_task(async function check_shortcut_when_in_opened_overflow_panel() { + CustomizableUI.addWidgetToArea( + "search-container", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await document.getElementById("nav-bar").overflowable.show(); + + sendWebSearchKeyCommand(); + + await waitForSearchBarFocus(); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("KEY_Escape"); + await hiddenPanelPromise; + CustomizableUI.reset(); +}); + +// Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed. +add_task(async function check_shortcut_when_in_overflow() { + this.originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + Services.prefs.setBoolPref("browser.search.widget.inNavBar", true); + + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => { + return ( + navbar.getAttribute("overflowing") == "true" && + !navbar.querySelector("#search-container") + ); + }); + ok( + !navbar.querySelector("#search-container"), + "Search container should be overflowing" + ); + + let shownPanelPromise = promiseOverflowShown(window); + sendWebSearchKeyCommand(); + await shownPanelPromise; + + let chevron = document.getElementById("nav-bar-overflow-button"); + await TestUtils.waitForCondition(() => chevron.open); + + await waitForSearchBarFocus(); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("KEY_Escape"); + await hiddenPanelPromise; + + Services.prefs.setBoolPref("browser.search.widget.inNavBar", false); + + navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + window.resizeTo(this.originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok( + !navbar.hasAttribute("overflowing"), + "Should not have an overflowing toolbar." + ); +}); + +// Ctrl+K should focus the search bar if it is in the navbar and not overflowing. +add_task(async function check_shortcut_when_not_in_overflow() { + Services.prefs.setBoolPref("browser.search.widget.inNavBar", true); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_NAVBAR, "Should be in nav-bar"); + + sendWebSearchKeyCommand(); + + // This fails if the screen resolution is small and the search bar overflows + // from the nav bar even with the original window width. + await waitForSearchBarFocus(); + + Services.prefs.setBoolPref("browser.search.widget.inNavBar", false); +}); + +function sendWebSearchKeyCommand() { + document.documentElement.focus(); + EventUtils.synthesizeKey("k", { accelKey: true }); +} + +function logActiveElement() { + let element = document.activeElement; + let str = ""; + while (element && element.parentNode) { + str = + " (" + + element.localName + + "#" + + element.id + + "." + + [...element.classList].join(".") + + ") >" + + str; + element = element.parentNode; + } + info("Active element: " + element ? str : "null"); +} diff --git a/browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js b/browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js new file mode 100644 index 0000000000..bca9a05b94 --- /dev/null +++ b/browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Resize to a small window, open a new window, check that new window handles overflow properly +add_task(async function () { + let originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + let oldChildCount = + CustomizableUI.getCustomizationTarget(navbar).childElementCount; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + ok( + CustomizableUI.getCustomizationTarget(navbar).childElementCount < + oldChildCount, + "Should have fewer children." + ); + let newWindow = await openAndLoadWindow(); + let otherNavBar = newWindow.document.getElementById( + CustomizableUI.AREA_NAVBAR + ); + await TestUtils.waitForCondition(() => + otherNavBar.hasAttribute("overflowing") + ); + ok( + otherNavBar.hasAttribute("overflowing"), + "Other window should have an overflowing toolbar." + ); + await promiseWindowClosed(newWindow); + + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok( + !navbar.hasAttribute("overflowing"), + "Should no longer have an overflowing toolbar." + ); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_913972_currentset_overflow.js b/browser/components/customizableui/test/browser_913972_currentset_overflow.js new file mode 100644 index 0000000000..fde37e4f3c --- /dev/null +++ b/browser/components/customizableui/test/browser_913972_currentset_overflow.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + +registerCleanupFunction(async function asyncCleanup() { + await resetCustomization(); +}); + +// Resize to a small window, resize back, shouldn't affect default state. +add_task(async function () { + let originalWindowWidth = window.outerWidth; + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + let navbarTarget = CustomizableUI.getCustomizationTarget(navbar); + let oldChildCount = navbarTarget.childElementCount; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition( + () => navbar.hasAttribute("overflowing"), + "Navbar has a overflowing attribute" + ); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok( + CustomizableUI.inDefaultState, + "Should still be in default state when overflowing." + ); + ok( + navbarTarget.childElementCount < oldChildCount, + "Should have fewer children." + ); + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition( + () => !navbar.hasAttribute("overflowing"), + "Navbar does not have an overflowing attribute" + ); + ok( + !navbar.hasAttribute("overflowing"), + "Should no longer have an overflowing toolbar." + ); + ok( + CustomizableUI.inDefaultState, + "Should still be in default state now we're no longer overflowing." + ); + + // Verify actual physical placements match those of the placement array: + let placementCounter = 0; + let placements = CustomizableUI.getWidgetIdsInArea( + CustomizableUI.AREA_NAVBAR + ); + for (let node of navbarTarget.children) { + if (node.getAttribute("skipintoolbarset") == "true") { + continue; + } + is( + placements[placementCounter++], + node.id, + "Nodes should match after overflow" + ); + } + is( + placements.length, + placementCounter, + "Should have as many nodes as expected" + ); + is( + navbarTarget.childElementCount, + oldChildCount, + "Number of nodes should match" + ); +}); + +// Enter and exit customization mode, check that default state is correct. +add_task(async function () { + ok(CustomizableUI.inDefaultState, "Should start in default state."); + await startCustomizing(); + ok( + CustomizableUI.inDefaultState, + "Should be in default state in customization mode." + ); + await endCustomizing(); + ok( + CustomizableUI.inDefaultState, + "Should be in default state after customization mode." + ); +}); diff --git a/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js b/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js new file mode 100644 index 0000000000..1d57e3d1fb --- /dev/null +++ b/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js @@ -0,0 +1,347 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); +var overflowList = document.getElementById( + navbar.getAttribute("default-overflowtarget") +); + +const kTestBtn1 = "test-addWidgetToArea-overflow"; +const kTestBtn2 = "test-removeWidgetFromArea-overflow"; +const kTestBtn3 = "test-createWidget-overflow"; +const kTestBtn4 = "test-createWidget-overflow-first-item"; +const kTestBtn5 = "test-addWidgetToArea-overflow-first-item"; +const kSidebarBtn = "sidebar-button"; +const kLibraryButton = "library-button"; +const kDownloadsBtn = "downloads-button"; +const kSearchBox = "search-container"; + +var originalWindowWidth; + +// Adding a widget should add it next to the widget it's being inserted next to. +add_task(async function subsequent_widget() { + originalWindowWidth = window.outerWidth; + createDummyXULButton(kTestBtn1, "Test"); + ok( + !navbar.hasAttribute("overflowing"), + "Should start subsequent_widget with a non-overflowing toolbar." + ); + ok( + CustomizableUI.inDefaultState, + "Should start subsequent_widget in default state." + ); + CustomizableUI.addWidgetToArea(kSidebarBtn, "nav-bar"); + await waitForElementShown(document.getElementById(kSidebarBtn)); + + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => { + return ( + navbar.hasAttribute("overflowing") && + document.getElementById(kSidebarBtn).getAttribute("overflowedItem") == + "true" + ); + }); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok( + !navbar.querySelector("#" + kSidebarBtn), + "Sidebar button should no longer be in the navbar" + ); + let sidebarBtnNode = overflowList.querySelector("#" + kSidebarBtn); + ok(sidebarBtnNode, "Sidebar button should be overflowing"); + ok( + sidebarBtnNode && sidebarBtnNode.getAttribute("overflowedItem") == "true", + "Sidebar button should have overflowedItem attribute" + ); + + let placementOfSidebarButton = CustomizableUI.getWidgetIdsInArea( + navbar.id + ).indexOf(kSidebarBtn); + CustomizableUI.addWidgetToArea( + kTestBtn1, + navbar.id, + placementOfSidebarButton + ); + ok( + !navbar.querySelector("#" + kTestBtn1), + "New button should not be in the navbar" + ); + let newButtonNode = overflowList.querySelector("#" + kTestBtn1); + ok(newButtonNode, "New button should be overflowing"); + ok( + newButtonNode && newButtonNode.getAttribute("overflowedItem") == "true", + "New button should have overflowedItem attribute" + ); + let nextEl = newButtonNode && newButtonNode.nextElementSibling; + is( + nextEl && nextEl.id, + kSidebarBtn, + "Test button should be next to sidebar button." + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok( + !navbar.hasAttribute("overflowing"), + "Should not have an overflowing toolbar." + ); + ok( + navbar.querySelector("#" + kSidebarBtn), + "Sidebar button should be in the navbar" + ); + ok( + sidebarBtnNode && sidebarBtnNode.getAttribute("overflowedItem") != "true", + "Sidebar button should no longer have overflowedItem attribute" + ); + ok( + !overflowList.querySelector("#" + kSidebarBtn), + "Sidebar button should no longer be overflowing" + ); + ok( + navbar.querySelector("#" + kTestBtn1), + "Test button should be in the navbar" + ); + ok( + !overflowList.querySelector("#" + kTestBtn1), + "Test button should no longer be overflowing" + ); + ok( + newButtonNode && newButtonNode.getAttribute("overflowedItem") != "true", + "New button should no longer have overflowedItem attribute" + ); + let el = document.getElementById(kTestBtn1); + if (el) { + CustomizableUI.removeWidgetFromArea(kTestBtn1); + el.remove(); + } + CustomizableUI.removeWidgetFromArea(kSidebarBtn); + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +// Removing a widget should remove it from the overflow list if that is where it is, and update it accordingly. +add_task(async function remove_widget() { + createDummyXULButton(kTestBtn2, "Test"); + ok( + !navbar.hasAttribute("overflowing"), + "Should start remove_widget with a non-overflowing toolbar." + ); + ok( + CustomizableUI.inDefaultState, + "Should start remove_widget in default state." + ); + CustomizableUI.addWidgetToArea(kTestBtn2, navbar.id); + ok( + !navbar.hasAttribute("overflowing"), + "Should still have a non-overflowing toolbar." + ); + + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok( + !navbar.querySelector("#" + kTestBtn2), + "Test button should not be in the navbar" + ); + ok( + overflowList.querySelector("#" + kTestBtn2), + "Test button should be overflowing" + ); + + CustomizableUI.removeWidgetFromArea(kTestBtn2); + + ok( + !overflowList.querySelector("#" + kTestBtn2), + "Test button should not be overflowing." + ); + ok( + !navbar.querySelector("#" + kTestBtn2), + "Test button should not be in the navbar" + ); + ok( + gNavToolbox.palette.querySelector("#" + kTestBtn2), + "Test button should be in the palette" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok( + !navbar.hasAttribute("overflowing"), + "Should not have an overflowing toolbar." + ); + let el = document.getElementById(kTestBtn2); + if (el) { + CustomizableUI.removeWidgetFromArea(kTestBtn2); + el.remove(); + } + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +// Constructing a widget while overflown should set the right class on it. +add_task(async function construct_widget() { + originalWindowWidth = window.outerWidth; + ok( + !navbar.hasAttribute("overflowing"), + "Should start construct_widget with a non-overflowing toolbar." + ); + ok( + CustomizableUI.inDefaultState, + "Should start construct_widget in default state." + ); + + CustomizableUI.addWidgetToArea(kSidebarBtn, "nav-bar"); + await waitForElementShown(document.getElementById(kSidebarBtn)); + + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => { + return ( + navbar.hasAttribute("overflowing") && + document.getElementById(kSidebarBtn).getAttribute("overflowedItem") == + "true" + ); + }); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok( + !navbar.querySelector("#" + kSidebarBtn), + "Sidebar button should no longer be in the navbar" + ); + let sidebarBtnNode = overflowList.querySelector("#" + kSidebarBtn); + ok(sidebarBtnNode, "Sidebar button should be overflowing"); + ok( + sidebarBtnNode && sidebarBtnNode.getAttribute("overflowedItem") == "true", + "Sidebar button should have overflowedItem class" + ); + + let testBtnSpec = { + id: kTestBtn3, + label: "Overflowable widget test", + defaultArea: "nav-bar", + }; + CustomizableUI.createWidget(testBtnSpec); + let testNode = overflowList.querySelector("#" + kTestBtn3); + ok(testNode, "Test button should be overflowing"); + ok( + testNode && testNode.getAttribute("overflowedItem") == "true", + "Test button should have overflowedItem class" + ); + + CustomizableUI.destroyWidget(kTestBtn3); + testNode = document.getElementById(kTestBtn3); + ok(!testNode, "Test button should be gone"); + + CustomizableUI.createWidget(testBtnSpec); + testNode = overflowList.querySelector("#" + kTestBtn3); + ok(testNode, "Test button should be overflowing"); + ok( + testNode && testNode.getAttribute("overflowedItem") == "true", + "Test button should have overflowedItem class" + ); + + CustomizableUI.removeWidgetFromArea(kTestBtn3); + testNode = document.getElementById(kTestBtn3); + ok(!testNode, "Test button should be gone"); + CustomizableUI.destroyWidget(kTestBtn3); + CustomizableUI.removeWidgetFromArea(kSidebarBtn); + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +add_task(async function insertBeforeFirstItemInOverflow() { + originalWindowWidth = window.outerWidth; + + ok( + !navbar.hasAttribute("overflowing"), + "Should start insertBeforeFirstItemInOverflow with a non-overflowing toolbar." + ); + ok( + CustomizableUI.inDefaultState, + "Should start insertBeforeFirstItemInOverflow in default state." + ); + + CustomizableUI.addWidgetToArea( + kLibraryButton, + "nav-bar", + CustomizableUI.getWidgetIdsInArea("nav-bar").indexOf( + "save-to-pocket-button" + ) + ); + let libraryButton = document.getElementById(kLibraryButton); + await waitForElementShown(libraryButton); + // Ensure nothing flexes to make the resize predictable: + navbar + .querySelectorAll("toolbarspring") + .forEach(s => CustomizableUI.removeWidgetFromArea(s.id)); + let urlbar = document.getElementById("urlbar-container"); + urlbar.style.minWidth = urlbar.getBoundingClientRect().width + "px"; + // Negative number to make the window smaller by the difference between the left side of + // the item next to the library button and left side of the hamburger one. + // The width of the overflow button that needs to appear will then be enough to + // also hide the library button. + let resizeWidthToMakeLibraryLast = + libraryButton.nextElementSibling.getBoundingClientRect().left - + PanelUI.menuButton.parentNode.getBoundingClientRect().left + + 10; // Leave some margin for the margins between buttons etc.; + info( + "Resizing to " + + resizeWidthToMakeLibraryLast + + " , waiting for library to overflow." + ); + window.resizeBy(resizeWidthToMakeLibraryLast, 0); + await TestUtils.waitForCondition(() => { + return ( + libraryButton.getAttribute("overflowedItem") == "true" && + !libraryButton.previousElementSibling + ); + }); + + let testBtnSpec = { id: kTestBtn4, label: "Overflowable widget test" }; + let placementOfLibraryButton = CustomizableUI.getWidgetIdsInArea( + navbar.id + ).indexOf(kLibraryButton); + CustomizableUI.createWidget(testBtnSpec); + CustomizableUI.addWidgetToArea( + kTestBtn4, + "nav-bar", + placementOfLibraryButton + ); + let testNode = overflowList.querySelector("#" + kTestBtn4); + ok(testNode, "Test button should be overflowing"); + ok( + testNode && testNode.getAttribute("overflowedItem") == "true", + "Test button should have overflowedItem class" + ); + CustomizableUI.destroyWidget(kTestBtn4); + testNode = document.getElementById(kTestBtn4); + ok(!testNode, "Test button should be gone"); + + createDummyXULButton(kTestBtn5, "Test"); + CustomizableUI.addWidgetToArea( + kTestBtn5, + "nav-bar", + placementOfLibraryButton + ); + testNode = overflowList.querySelector("#" + kTestBtn5); + ok(testNode, "Test button should be overflowing"); + ok( + testNode && testNode.getAttribute("overflowedItem") == "true", + "Test button should have overflowedItem class" + ); + CustomizableUI.removeWidgetFromArea(kTestBtn5); + testNode && testNode.remove(); + + urlbar.style.removeProperty("min-width"); + CustomizableUI.removeWidgetFromArea(kLibraryButton); + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + await resetCustomization(); +}); + +registerCleanupFunction(async function asyncCleanup() { + document.getElementById("urlbar-container").style.removeProperty("min-width"); + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js b/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js new file mode 100644 index 0000000000..bd0a7d5795 --- /dev/null +++ b/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var navbar; +var skippedItem; + +// Attempting to drag a skipintoolbarset item should work. +add_task(async function () { + navbar = document.getElementById("nav-bar"); + skippedItem = document.createXULElement("toolbarbutton"); + skippedItem.id = "test-skipintoolbarset-item"; + skippedItem.setAttribute("label", "Test"); + skippedItem.setAttribute("skipintoolbarset", "true"); + skippedItem.setAttribute("removable", "true"); + CustomizableUI.getCustomizationTarget(navbar).appendChild(skippedItem); + let stopReloadButton = document.getElementById("stop-reload-button"); + await startCustomizing(); + await waitForElementShown(skippedItem); + ok(CustomizableUI.inDefaultState, "Should still be in default state"); + simulateItemDrag(skippedItem, stopReloadButton, "start", 0); + ok(CustomizableUI.inDefaultState, "Should still be in default state"); + let skippedItemWrapper = skippedItem.parentNode; + is( + skippedItemWrapper.nextElementSibling && + skippedItemWrapper.nextElementSibling.id, + stopReloadButton.parentNode.id, + "Should be next to stop/reload button" + ); + simulateItemDrag(stopReloadButton, skippedItem, "start", 0); + let wrapper = stopReloadButton.parentNode; + is( + wrapper.nextElementSibling && wrapper.nextElementSibling.id, + skippedItem.parentNode.id, + "Should be next to skipintoolbarset item" + ); + ok(CustomizableUI.inDefaultState, "Should still be in default state"); +}); + +add_task(async function asyncCleanup() { + await endCustomizing(); + skippedItem.remove(); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js b/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js new file mode 100644 index 0000000000..d416f34144 --- /dev/null +++ b/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Customize mode reset button should revert correctly +add_task(async function () { + await startCustomizing(); + let devButton = document.getElementById("developer-button"); + let fxaButton = document.getElementById("fxa-toolbar-menu-button"); + let stopReloadButton = document.getElementById("stop-reload-button"); + let palette = document.getElementById("customization-palette"); + ok( + devButton && fxaButton && stopReloadButton && palette, + "Stuff should exist" + ); + simulateItemDrag(devButton, fxaButton); + simulateItemDrag(stopReloadButton, palette); + await gCustomizeMode.reset(); + ok(CustomizableUI.inDefaultState, "Should be back in default state"); + await endCustomizing(); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js b/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js new file mode 100644 index 0000000000..340e840d83 --- /dev/null +++ b/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kTestToolbarId = "test-empty-drag"; + +// Attempting to drag an item to an empty container should work. +add_task(async function () { + await createToolbarWithPlacements(kTestToolbarId, []); + await startCustomizing(); + let libraryButton = document.getElementById("library-button"); + let customToolbar = document.getElementById(kTestToolbarId); + simulateItemDrag(libraryButton, customToolbar); + assertAreaPlacements(kTestToolbarId, ["library-button"]); + ok( + libraryButton.parentNode && + libraryButton.parentNode.parentNode == customToolbar, + "Button should really be in toolbar" + ); + await endCustomizing(); + removeCustomToolbars(); +}); + +add_task(async function asyncCleanup() { + await endCustomizing(); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_934113_menubar_removable.js b/browser/components/customizableui/test/browser_934113_menubar_removable.js new file mode 100644 index 0000000000..8f41baba7a --- /dev/null +++ b/browser/components/customizableui/test/browser_934113_menubar_removable.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Attempting to drag the menubar to the navbar shouldn't work. +add_task(async function () { + await startCustomizing(); + let menuItems = document.getElementById("menubar-items"); + let navbar = document.getElementById("nav-bar"); + let menubar = document.getElementById("toolbar-menubar"); + // Force the menu to be shown. + const kAutohide = menubar.getAttribute("autohide"); + menubar.setAttribute("autohide", "false"); + simulateItemDrag(menuItems, CustomizableUI.getCustomizationTarget(navbar)); + + is( + getAreaWidgetIds("nav-bar").indexOf("menubar-items"), + -1, + "Menu bar shouldn't be in the navbar." + ); + ok( + !navbar.querySelector("#menubar-items"), + "Shouldn't find menubar items in the navbar." + ); + ok( + menubar.querySelector("#menubar-items"), + "Should find menubar items in the menubar." + ); + isnot( + getAreaWidgetIds("toolbar-menubar").indexOf("menubar-items"), + -1, + "Menubar items shouldn't be missing from the navbar." + ); + menubar.setAttribute("autohide", kAutohide); + await endCustomizing(); +}); + +add_task(async function asyncCleanup() { + await endCustomizing(); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js b/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js new file mode 100644 index 0000000000..db1d6175ab --- /dev/null +++ b/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +let gZoomResetButton; + +async function waitForZoom(zoom) { + if (parseInt(gZoomResetButton.label) == zoom) { + return; + } + await promiseAttributeMutation(gZoomResetButton, "label", v => { + return parseInt(v) == zoom; + }); +} + +// Bug 934951 - Zoom controls percentage label doesn't update when it's in the toolbar and you navigate. +add_task(async function () { + CustomizableUI.addWidgetToArea("zoom-controls", CustomizableUI.AREA_NAVBAR); + gZoomResetButton = document.getElementById("zoom-reset-button"); + let tab1 = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + let tab2 = BrowserTestUtils.addTab(gBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + gBrowser.selectedTab = tab1; + + registerCleanupFunction(() => { + info("Cleaning up."); + CustomizableUI.reset(); + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab1); + }); + + is( + parseInt(gZoomResetButton.label, 10), + 100, + "Default zoom is 100% for about:mozilla" + ); + FullZoom.enlarge(); + await waitForZoom(110); + is( + parseInt(gZoomResetButton.label, 10), + 110, + "Zoom is changed to 110% for about:mozilla" + ); + + let tabSelectPromise = TestUtils.topicObserved( + "browser-fullZoom:location-change" + ); + gBrowser.selectedTab = tab2; + await tabSelectPromise; + await waitForZoom(100); + is( + parseInt(gZoomResetButton.label, 10), + 100, + "Default zoom is 100% for about:robots" + ); + + gBrowser.selectedTab = tab1; + await waitForZoom(110); + FullZoom.reset(); + await waitForZoom(100); + is( + parseInt(gZoomResetButton.label, 10), + 100, + "Default zoom is 100% for about:mozilla" + ); + + // Test zoom label updates while navigating pages in the same tab. + FullZoom.enlarge(); + await waitForZoom(110); + is( + parseInt(gZoomResetButton.label, 10), + 110, + "Zoom is changed to 110% for about:mozilla" + ); + await promiseTabLoadEvent(tab1, "about:home"); + await waitForZoom(100); + is( + parseInt(gZoomResetButton.label, 10), + 100, + "Default zoom is 100% for about:home" + ); + gBrowser.selectedBrowser.goBack(); + await waitForZoom(110); + is( + parseInt(gZoomResetButton.label, 10), + 110, + "Zoom is still 110% for about:mozilla" + ); + FullZoom.reset(); +}); diff --git a/browser/components/customizableui/test/browser_938980_navbar_collapsed.js b/browser/components/customizableui/test/browser_938980_navbar_collapsed.js new file mode 100644 index 0000000000..28b75c0a37 --- /dev/null +++ b/browser/components/customizableui/test/browser_938980_navbar_collapsed.js @@ -0,0 +1,214 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(2); + +var bookmarksToolbar = document.getElementById("PersonalToolbar"); +var navbar = document.getElementById("nav-bar"); +var tabsToolbar = document.getElementById("TabsToolbar"); + +// Customization reset should restore visibility to default-visible toolbars. +add_task(async function () { + is(navbar.collapsed, false, "Test should start with navbar visible"); + setToolbarVisibility(navbar, false); + is(navbar.collapsed, true, "navbar should be hidden now"); + + await resetCustomization(); + + is( + navbar.collapsed, + false, + "Customization reset should restore visibility to the navbar" + ); +}); + +// Customization reset should restore collapsed-state to default-collapsed toolbars. +add_task(async function () { + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state" + ); + + is( + bookmarksToolbar.collapsed, + true, + "Test should start with bookmarks toolbar collapsed" + ); + ok(bookmarksToolbar.collapsed, "bookmarksToolbar should be collapsed"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + is(navbar.collapsed, false, "The nav-bar should be shown by default"); + + setToolbarVisibility(bookmarksToolbar, true); + setToolbarVisibility(navbar, false); + ok(!bookmarksToolbar.collapsed, "bookmarksToolbar should be visible now"); + ok(navbar.collapsed, "navbar should be collapsed"); + is( + CustomizableUI.inDefaultState, + false, + "Should no longer be in default state" + ); + + await startCustomizing(); + await gCustomizeMode.reset(); + await endCustomizing(); + + is( + bookmarksToolbar.collapsed, + true, + "Customization reset should restore collapsed-state to the bookmarks toolbar" + ); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + ok( + bookmarksToolbar.collapsed, + "The bookmarksToolbar should be collapsed after reset" + ); + ok( + CustomizableUI.inDefaultState, + "Everything should be back to default state" + ); +}); + +// Check that the menubar will be collapsed by resetting, if the platform supports it. +add_task(async function () { + let menubar = document.getElementById("toolbar-menubar"); + const canMenubarCollapse = CustomizableUI.isToolbarDefaultCollapsed( + menubar.id + ); + if (!canMenubarCollapse) { + return; + } + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state" + ); + + is( + menubar.getBoundingClientRect().height, + 0, + "menubar should be hidden by default" + ); + setToolbarVisibility(menubar, true); + isnot( + menubar.getBoundingClientRect().height, + 0, + "menubar should be visible now" + ); + + await startCustomizing(); + await gCustomizeMode.reset(); + + is( + menubar.getAttribute("autohide"), + "true", + "The menubar should have autohide=true after reset in customization mode" + ); + is( + menubar.getBoundingClientRect().height, + 0, + "The menubar should have height=0 after reset in customization mode" + ); + + await endCustomizing(); + + is( + menubar.getAttribute("autohide"), + "true", + "The menubar should have autohide=true after reset" + ); + is( + menubar.getBoundingClientRect().height, + 0, + "The menubar should have height=0 after reset" + ); +}); + +// Customization reset should restore collapsed-state to default-collapsed toolbars. +add_task(async function () { + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state" + ); + ok(bookmarksToolbar.collapsed, "bookmarksToolbar should be collapsed"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + + setToolbarVisibility(bookmarksToolbar, true); + ok(!bookmarksToolbar.collapsed, "bookmarksToolbar should be visible now"); + is( + CustomizableUI.inDefaultState, + false, + "Should no longer be in default state" + ); + + await startCustomizing(); + + ok( + !bookmarksToolbar.collapsed, + "The bookmarksToolbar should be visible before reset" + ); + ok(!navbar.collapsed, "The navbar should be visible before reset"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + + await gCustomizeMode.reset(); + + ok( + bookmarksToolbar.collapsed, + "The bookmarksToolbar should be collapsed after reset" + ); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + ok(!navbar.collapsed, "The navbar should still be visible after reset"); + ok( + CustomizableUI.inDefaultState, + "Everything should be back to default state" + ); + await endCustomizing(); +}); + +// Check that the menubar will be collapsed by resetting, if the platform supports it. +add_task(async function () { + let menubar = document.getElementById("toolbar-menubar"); + const canMenubarCollapse = CustomizableUI.isToolbarDefaultCollapsed( + menubar.id + ); + if (!canMenubarCollapse) { + return; + } + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state" + ); + await startCustomizing(); + let resetButton = document.getElementById("customization-reset-button"); + is( + resetButton.disabled, + true, + "The reset button should be disabled when in default state" + ); + + setToolbarVisibility(menubar, true); + is( + resetButton.disabled, + false, + "The reset button should be enabled when not in default state" + ); + ok( + !CustomizableUI.inDefaultState, + "No longer in default state when the menubar is shown" + ); + + await gCustomizeMode.reset(); + + is( + resetButton.disabled, + true, + "The reset button should be disabled when in default state" + ); + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state" + ); + + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js b/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js new file mode 100644 index 0000000000..2d926e1725 --- /dev/null +++ b/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kWidgetId = "test-non-removable-widget"; + +// Adding non-removable items to a toolbar or the panel shouldn't change inDefaultState +add_task(async function () { + ok(CustomizableUI.inDefaultState, "Should start in default state"); + + let button = createDummyXULButton( + kWidgetId, + "Test non-removable inDefaultState handling" + ); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + button.setAttribute("removable", "false"); + ok( + CustomizableUI.inDefaultState, + "Should still be in default state after navbar addition" + ); + button.remove(); + + button = createDummyXULButton( + kWidgetId, + "Test non-removable inDefaultState handling" + ); + CustomizableUI.addWidgetToArea( + kWidgetId, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + button.setAttribute("removable", "false"); + ok( + CustomizableUI.inDefaultState, + "Should still be in default state after panel addition" + ); + button.remove(); + ok( + CustomizableUI.inDefaultState, + "Should be in default state after destroying both widgets" + ); + // reset now that button is gone. + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js b/browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js new file mode 100644 index 0000000000..c4fa54f782 --- /dev/null +++ b/browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbarId = "test-registerToolbarNode-toolbar"; +const kButtonId = "test-registerToolbarNode-button"; +registerCleanupFunction(cleanup); + +// Registering a toolbar without a defaultset attribute should +// wait for the registerArea call +add_task(async function () { + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state." + ); + let btn = createDummyXULButton(kButtonId); + let toolbar = document.createXULElement("toolbar"); + toolbar.id = kToolbarId; + toolbar.setAttribute("customizable", true); + gNavToolbox.appendChild(toolbar); + CustomizableUI.registerToolbarNode(toolbar); + ok( + !CustomizableUI.areas.includes(kToolbarId), + "Toolbar should not yet have been registered automatically." + ); + CustomizableUI.registerArea(kToolbarId, { defaultPlacements: [kButtonId] }); + ok( + CustomizableUI.areas.includes(kToolbarId), + "Toolbar should have been registered now." + ); + is( + CustomizableUI.getAreaType(kToolbarId), + CustomizableUI.TYPE_TOOLBAR, + "Area should be registered as toolbar" + ); + assertAreaPlacements(kToolbarId, [kButtonId]); + ok( + !CustomizableUI.inDefaultState, + "No longer in default state after toolbar is registered and visible." + ); + CustomizableUI.unregisterArea(kToolbarId, true); + toolbar.remove(); + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state." + ); + btn.remove(); +}); + +async function cleanup() { + let toolbar = document.getElementById(kToolbarId); + if (toolbar) { + toolbar.remove(); + } + let btn = + document.getElementById(kButtonId) || + gNavToolbox.querySelector("#" + kButtonId); + if (btn) { + btn.remove(); + } + await resetCustomization(); +} diff --git a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js new file mode 100644 index 0000000000..38a50d6c24 --- /dev/null +++ b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var button, menuButton; +/* Clicking a button should close the panel */ +add_task(async function plain_button() { + button = document.createXULElement("toolbarbutton"); + button.id = "browser_940307_button"; + button.setAttribute("label", "Button"); + gNavToolbox.palette.appendChild(button); + CustomizableUI.addWidgetToArea( + button.id, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + let hiddenAgain = promiseOverflowHidden(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + await hiddenAgain; + CustomizableUI.removeWidgetFromArea(button.id); + button.remove(); +}); + +add_task(async function searchbar_in_panel() { + CustomizableUI.addWidgetToArea( + "search-container", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + + let searchbar = document.getElementById("searchbar"); + await TestUtils.waitForCondition( + () => "value" in searchbar && searchbar.value === "" + ); + + // Focusing a non-empty searchbox will cause us to open the + // autocomplete panel and search for suggestions, which would + // trigger network requests. Temporarily disable suggestions. + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", false]], + }); + let dontShowPopup = e => e.preventDefault(); + let searchbarPopup = searchbar.textbox.popup; + searchbarPopup.addEventListener("popupshowing", dontShowPopup); + + searchbar.value = "foo"; + searchbar.focus(); + + // Can't use promisePanelElementShown() here since the search bar + // creates its context menu lazily the first time it is opened. + let contextMenuShown = new Promise(resolve => { + let listener = event => { + if (searchbar._menupopup && event.target == searchbar._menupopup) { + window.removeEventListener("popupshown", listener); + resolve(searchbar._menupopup); + } + }; + window.addEventListener("popupshown", listener); + }); + EventUtils.synthesizeMouseAtCenter(searchbar, { + type: "contextmenu", + button: 2, + }); + let contextmenu = await contextMenuShown; + + ok(isOverflowOpen(), "Panel should still be open"); + + let selectAll = contextmenu.querySelector("[cmd='cmd_selectAll']"); + let contextMenuHidden = promisePanelElementHidden(window, contextmenu); + contextmenu.activateItem(selectAll); + await contextMenuHidden; + + ok(isOverflowOpen(), "Panel should still be open"); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("KEY_Escape"); + await hiddenPanelPromise; + ok(!isOverflowOpen(), "Panel should no longer be open"); + + // Allow search bar popup to show again. + searchbarPopup.removeEventListener("popupshowing", dontShowPopup); + + // We focused the search bar earlier - ensure we don't keep doing that. + gURLBar.select(); + + CustomizableUI.reset(); +}); + +add_task(async function disabled_button_in_panel() { + button = document.createXULElement("toolbarbutton"); + button.id = "browser_946166_button_disabled"; + button.setAttribute("disabled", "true"); + button.setAttribute("label", "Button"); + gNavToolbox.palette.appendChild(button); + CustomizableUI.addWidgetToArea( + button.id, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + EventUtils.synthesizeMouseAtCenter(button, {}); + is(PanelUI.overflowPanel.state, "open", "Popup stays open"); + button.removeAttribute("disabled"); + let hiddenAgain = promiseOverflowHidden(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + await hiddenAgain; + button.remove(); +}); + +registerCleanupFunction(function () { + if (button && button.parentNode) { + button.remove(); + } + if (menuButton && menuButton.parentNode) { + menuButton.remove(); + } + // Sadly this isn't task.jsm-enabled, so we can't wait for this to happen. But we should + // definitely close it here and hope it won't interfere with other tests. + // Of course, all the tests are meant to do this themselves, but if they fail... + if (isOverflowOpen()) { + PanelUI.overflowPanel.hidePopup(); + } + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js b/browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js new file mode 100644 index 0000000000..2144dd2483 --- /dev/null +++ b/browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kTestBtnId = "test-removable-navbar-customize-mode"; + +// Items without the removable attribute in the navbar should be considered non-removable +add_task(async function () { + let btn = createDummyXULButton( + kTestBtnId, + "Test removable in navbar in customize mode" + ); + CustomizableUI.getCustomizationTarget( + document.getElementById("nav-bar") + ).appendChild(btn); + await startCustomizing(); + ok( + !CustomizableUI.isWidgetRemovable(kTestBtnId), + "Widget should not be considered removable" + ); + await endCustomizing(); + document.getElementById(kTestBtnId).remove(); +}); + +add_task(async function asyncCleanup() { + await endCustomizing(); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js b/browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js new file mode 100644 index 0000000000..6df5084849 --- /dev/null +++ b/browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// See https://bugzilla.mozilla.org/show_bug.cgi?id=941083 + +const kWidgetId = "test-invalidate-wrapper-cache"; + +// Check createWidget invalidates the widget cache +add_task(function () { + let groupWrapper = CustomizableUI.getWidget(kWidgetId); + ok(groupWrapper, "Should get group wrapper."); + let singleWrapper = groupWrapper.forWindow(window); + ok(singleWrapper, "Should get single wrapper."); + + CustomizableUI.createWidget({ + id: kWidgetId, + label: "Test invalidating widgets caching", + }); + + let newGroupWrapper = CustomizableUI.getWidget(kWidgetId); + ok(newGroupWrapper, "Should get a group wrapper again."); + isnot(newGroupWrapper, groupWrapper, "Wrappers shouldn't be the same."); + isnot( + newGroupWrapper.provider, + groupWrapper.provider, + "Wrapper providers shouldn't be the same." + ); + + let newSingleWrapper = newGroupWrapper.forWindow(window); + isnot( + newSingleWrapper, + singleWrapper, + "Single wrappers shouldn't be the same." + ); + isnot( + newSingleWrapper.provider, + singleWrapper.provider, + "Single wrapper providers shouldn't be the same." + ); + + CustomizableUI.destroyWidget(kWidgetId); + ok( + !CustomizableUI.getWidget(kWidgetId), + "Shouldn't get a wrapper after destroying the widget." + ); +}); diff --git a/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js new file mode 100644 index 0000000000..6f18d590b7 --- /dev/null +++ b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbarName = "test-unregisterArea-placements-toolbar"; +const kTestWidgetPfx = "test-widget-for-unregisterArea-placements-"; +const kTestWidgetCount = 3; +registerCleanupFunction(removeCustomToolbars); + +// unregisterArea should keep placements by default and restore them when re-adding the area +add_task(async function () { + let widgetIds = []; + for (let i = 0; i < kTestWidgetCount; i++) { + let id = kTestWidgetPfx + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "unregisterArea test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + } + for (let i = kTestWidgetCount; i < kTestWidgetCount * 2; i++) { + let id = kTestWidgetPfx + i; + widgetIds.push(id); + createDummyXULButton(id, "unregisterArea XUL test " + i); + } + let toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + checkAbstractAndRealPlacements(toolbarNode, widgetIds); + + // Now move one of them: + CustomizableUI.moveWidgetWithinArea(kTestWidgetPfx + kTestWidgetCount, 0); + // Clone the array so we know this is the modified one: + let modifiedWidgetIds = [...widgetIds]; + let movedWidget = modifiedWidgetIds.splice(kTestWidgetCount, 1)[0]; + modifiedWidgetIds.unshift(movedWidget); + + // Check it: + checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds); + + // Then unregister + CustomizableUI.unregisterArea(kToolbarName); + + // Check we tell the outside world no dangerous things: + checkWidgetFates(widgetIds); + // Only then remove the real node + toolbarNode.remove(); + + // Now move one of the items to the palette, and another to the navbar: + let lastWidget = modifiedWidgetIds.pop(); + CustomizableUI.removeWidgetFromArea(lastWidget); + lastWidget = modifiedWidgetIds.pop(); + CustomizableUI.addWidgetToArea(lastWidget, CustomizableUI.AREA_NAVBAR); + + // Recreate ourselves with the default placements being the same: + toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + // Then check that after doing this, our actual placements match + // the modified list, not the default one. + checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds); + + // Now remove completely: + CustomizableUI.unregisterArea(kToolbarName, true); + checkWidgetFates(modifiedWidgetIds); + toolbarNode.remove(); + + // One more time: + // Recreate ourselves with the default placements being the same: + toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + // Should now be back to default: + checkAbstractAndRealPlacements(toolbarNode, widgetIds); + CustomizableUI.unregisterArea(kToolbarName, true); + checkWidgetFates(widgetIds); + toolbarNode.remove(); + + // XXXgijs: ensure cleanup function doesn't barf: + gAddedToolbars.delete(kToolbarName); + + // Remove all the XUL widgets, destroy the others: + for (let widget of widgetIds) { + let widgetWrapper = CustomizableUI.getWidget(widget); + if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL) { + gNavToolbox.palette.querySelector("#" + widget).remove(); + } else { + CustomizableUI.destroyWidget(widget); + } + } +}); + +function checkAbstractAndRealPlacements(aNode, aExpectedPlacements) { + assertAreaPlacements(kToolbarName, aExpectedPlacements); + let physicalWidgetIds = Array.from(aNode.children, node => node.id); + placementArraysEqual(aNode.id, physicalWidgetIds, aExpectedPlacements); +} + +function checkWidgetFates(aWidgetIds) { + for (let widget of aWidgetIds) { + ok( + !CustomizableUI.getPlacementOfWidget(widget), + "Widget should be in palette" + ); + ok(!document.getElementById(widget), "Widget should not be in the DOM"); + let widgetInPalette = !!gNavToolbox.palette.querySelector("#" + widget); + let widgetProvider = CustomizableUI.getWidget(widget).provider; + let widgetIsXULWidget = widgetProvider == CustomizableUI.PROVIDER_XUL; + is( + widgetInPalette, + widgetIsXULWidget, + "Just XUL Widgets should be in the palette" + ); + } +} + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js b/browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js new file mode 100644 index 0000000000..3da14ab217 --- /dev/null +++ b/browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kWidgetId = "test-destroy-in-palette"; + +// Check destroyWidget destroys the node if it's in the palette +add_task(async function () { + CustomizableUI.createWidget({ + id: kWidgetId, + label: "Test destroying widgets in palette.", + }); + await startCustomizing(); + await endCustomizing(); + ok( + gNavToolbox.palette.querySelector("#" + kWidgetId), + "Widget still exists in palette." + ); + CustomizableUI.destroyWidget(kWidgetId); + ok( + !gNavToolbox.palette.querySelector("#" + kWidgetId), + "Widget no longer exists in palette." + ); +}); diff --git a/browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js b/browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js new file mode 100644 index 0000000000..bac97aed3b --- /dev/null +++ b/browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kWidgetId = "test-private-browsing-customize-mode-widget"; + +// Add a widget via the API with showInPrivateBrowsing set to false +// and ensure it does not appear in the list of unused widgets in private +// windows. +add_task(async function testPrivateBrowsingCustomizeModeWidget() { + CustomizableUI.createWidget({ + id: kWidgetId, + showInPrivateBrowsing: false, + }); + + let normalWidgetArray = CustomizableUI.getUnusedWidgets(gNavToolbox.palette); + normalWidgetArray = normalWidgetArray.map(w => w.id); + ok( + normalWidgetArray.indexOf(kWidgetId) > -1, + "Widget should appear as unused in non-private window" + ); + + let privateWindow = await openAndLoadWindow({ private: true }); + let privateWidgetArray = CustomizableUI.getUnusedWidgets( + privateWindow.gNavToolbox.palette + ); + privateWidgetArray = privateWidgetArray.map(w => w.id); + is( + privateWidgetArray.indexOf(kWidgetId), + -1, + "Widget should not appear as unused in private window" + ); + await promiseWindowClosed(privateWindow); + + CustomizableUI.destroyWidget(kWidgetId); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_copy.js b/browser/components/customizableui/test/browser_947914_button_copy.js new file mode 100644 index 0000000000..e6e0e287c4 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_copy.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var initialLocation = gBrowser.currentURI.spec; +var globalClipboard; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function () { + info("Check copy button existence and functionality"); + CustomizableUI.addWidgetToArea( + "edit-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + let testText = "copy text test"; + + gURLBar.focus(); + info("The URL bar was focused"); + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let copyButton = document.getElementById("copy-button"); + ok(copyButton, "Copy button exists in Panel Menu"); + ok( + copyButton.getAttribute("disabled"), + "Copy button is initially disabled" + ); + + // copy text from URL bar + gURLBar.value = testText; + gURLBar.valueIsTyped = true; + gURLBar.focus(); + gURLBar.select(); + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + ok( + !copyButton.hasAttribute("disabled"), + "Copy button is enabled when selecting" + ); + + await SimpleTest.promiseClipboardChange(testText, () => { + copyButton.click(); + }); + + is( + gURLBar.value, + testText, + "Selected text is unaltered when clicking copy" + ); + } + ); +}); + +registerCleanupFunction(function cleanup() { + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_cut.js b/browser/components/customizableui/test/browser_947914_button_cut.js new file mode 100644 index 0000000000..3ea5622b51 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_cut.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var initialLocation = gBrowser.currentURI.spec; +var globalClipboard; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function () { + info("Check cut button existence and functionality"); + CustomizableUI.addWidgetToArea( + "edit-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + let testText = "cut text test"; + + gURLBar.focus(); + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let cutButton = document.getElementById("cut-button"); + ok(cutButton, "Cut button exists in Panel Menu"); + ok(cutButton.hasAttribute("disabled"), "Cut button is disabled"); + + // cut text from URL bar + gURLBar.value = testText; + gURLBar.valueIsTyped = true; + gURLBar.focus(); + gURLBar.select(); + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + ok( + !cutButton.hasAttribute("disabled"), + "Cut button is enabled when selecting" + ); + await SimpleTest.promiseClipboardChange(testText, () => { + cutButton.click(); + }); + is( + gURLBar.value, + "", + "Selected text is removed from source when clicking on cut" + ); + } + ); +}); + +registerCleanupFunction(function cleanup() { + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_find.js b/browser/components/customizableui/test/browser_947914_button_find.js new file mode 100644 index 0000000000..c767239d9d --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_find.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + info("Check find button existence and functionality"); + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + CustomizableUI.addWidgetToArea( + "find-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let findButton = document.getElementById("find-button"); + ok(findButton, "Find button exists in Panel Menu"); + + let findBarPromise = gBrowser.isFindBarInitialized() + ? null + : BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabFindInitialized"); + + findButton.click(); + await findBarPromise; + ok(!gFindBar.hasAttribute("hidden"), "Findbar opened successfully"); + + // close find bar + gFindBar.close(); + info("Findbar was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_history.js b/browser/components/customizableui/test/browser_947914_button_history.js new file mode 100644 index 0000000000..d4ad28c04f --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_history.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +add_task(async function () { + info("Check history button existence and functionality"); + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "dummy_history_item.html" + ); + BrowserTestUtils.removeTab(tab); + + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH); // will 404, but we don't care. + + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let historyButton = document.getElementById("history-panelmenu"); + ok(historyButton, "History button appears in Panel Menu"); + + historyButton.click(); + + let historyPanel = document.getElementById("PanelUI-history"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); + await promise; + ok(historyPanel.getAttribute("visible"), "History Panel is in view"); + + let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let panelHiddenPromise = promiseOverflowHidden(window); + + let historyItems = document.getElementById("appMenu_historyMenu"); + let historyItemForURL = historyItems.querySelector( + "toolbarbutton.bookmark-item[label='Happy History Hero']" + ); + ok( + historyItemForURL, + "Should have a history item for the history we just made." + ); + EventUtils.synthesizeMouseAtCenter(historyItemForURL, {}); + await browserLoaded; + is( + gBrowser.currentURI.spec, + TEST_PATH + "dummy_history_item.html", + "Should have expected page load" + ); + + await panelHiddenPromise; + BrowserTestUtils.removeTab(tab); + info("Menu panel was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js new file mode 100644 index 0000000000..cc8842a3e8 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + info("Check private browsing button existence and functionality"); + CustomizableUI.addWidgetToArea( + "privatebrowsing-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let windowWasHandled = false; + let privateWindow = null; + + let observerWindowOpened = { + observe(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + privateWindow = aSubject; + privateWindow.addEventListener( + "load", + function () { + is( + privateWindow.location.href, + AppConstants.BROWSER_CHROME_URL, + "A new browser window was opened" + ); + ok( + PrivateBrowsingUtils.isWindowPrivate(privateWindow), + "Window is private" + ); + windowWasHandled = true; + }, + { once: true } + ); + } + }, + }; + + Services.ww.registerNotification(observerWindowOpened); + + let privateBrowsingButton = document.getElementById("privatebrowsing-button"); + ok(privateBrowsingButton, "Private browsing button exists in Panel Menu"); + privateBrowsingButton.click(); + + try { + await TestUtils.waitForCondition(() => windowWasHandled); + await promiseWindowClosed(privateWindow); + info("The new private window was closed"); + } catch (e) { + ok(false, "The new private browser window was not properly handled"); + } finally { + Services.ww.unregisterNotification(observerWindowOpened); + } +}); diff --git a/browser/components/customizableui/test/browser_947914_button_newWindow.js b/browser/components/customizableui/test/browser_947914_button_newWindow.js new file mode 100644 index 0000000000..591d13191e --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_newWindow.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + info("Check new window button existence and functionality"); + CustomizableUI.addWidgetToArea( + "new-window-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let windowWasHandled = false; + let newWindow = null; + + let observerWindowOpened = { + observe(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + newWindow = aSubject; + newWindow.addEventListener( + "load", + function () { + is( + newWindow.location.href, + AppConstants.BROWSER_CHROME_URL, + "A new browser window was opened" + ); + ok( + !PrivateBrowsingUtils.isWindowPrivate(newWindow), + "Window is not private" + ); + windowWasHandled = true; + }, + { once: true } + ); + } + }, + }; + + Services.ww.registerNotification(observerWindowOpened); + + let newWindowButton = document.getElementById("new-window-button"); + ok(newWindowButton, "New Window button exists in Panel Menu"); + newWindowButton.click(); + + try { + await TestUtils.waitForCondition(() => windowWasHandled); + await promiseWindowClosed(newWindow); + info("The new window was closed"); + } catch (e) { + ok(false, "The new browser window was not properly handled"); + } finally { + Services.ww.unregisterNotification(observerWindowOpened); + } +}); diff --git a/browser/components/customizableui/test/browser_947914_button_paste.js b/browser/components/customizableui/test/browser_947914_button_paste.js new file mode 100644 index 0000000000..a5d107faa6 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_paste.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var initialLocation = gBrowser.currentURI.spec; +var globalClipboard; + +add_task(async function () { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function () { + CustomizableUI.addWidgetToArea( + "edit-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + info("Check paste button existence and functionality"); + + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + globalClipboard = Services.clipboard.kGlobalClipboard; + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let pasteButton = document.getElementById("paste-button"); + ok(pasteButton, "Paste button exists in Panel Menu"); + + // add text to clipboard + let text = "Sample text for testing"; + clipboard.copyString(text); + + // test paste button by pasting text to URL bar + gURLBar.focus(); + await gCUITestUtils.openMainMenu(); + info("Menu panel was opened"); + + ok(!pasteButton.hasAttribute("disabled"), "Paste button is enabled"); + pasteButton.click(); + + is(gURLBar.value, text, "Text pasted successfully"); + + await gCUITestUtils.hideMainMenu(); + } + ); +}); + +registerCleanupFunction(function cleanup() { + CustomizableUI.reset(); + Services.clipboard.emptyClipboard(globalClipboard); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_print.js b/browser/components/customizableui/test/browser_947914_button_print.js new file mode 100644 index 0000000000..96d2328b2d --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_print.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const isOSX = Services.appinfo.OS === "Darwin"; + +add_task(async function () { + CustomizableUI.addWidgetToArea( + "print-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "http://example.com/", + }, + async function () { + info("Check print button existence and functionality"); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + await TestUtils.waitForCondition( + () => document.getElementById("print-button") != null + ); + + let printButton = document.getElementById("print-button"); + ok(printButton, "Print button exists in Panel Menu"); + + printButton.click(); + + // Ensure we're showing the preview... + await BrowserTestUtils.waitForCondition(() => { + let preview = document.querySelector(".printPreviewBrowser"); + return preview && BrowserTestUtils.is_visible(preview); + }); + + ok(true, "Entered print preview mode"); + + gBrowser.getTabDialogBox(gBrowser.selectedBrowser).abortAllDialogs(); + // Wait for the preview to go away + await BrowserTestUtils.waitForCondition( + () => !document.querySelector(".printPreviewBrowser") + ); + + info("Exited print preview"); + } + ); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_zoomIn.js b/browser/components/customizableui/test/browser_947914_button_zoomIn.js new file mode 100644 index 0000000000..361a1f53fb --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_zoomIn.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + info("Check zoom in button existence and functionality"); + + is(ZoomManager.zoom, 1, "Initial zoom factor should be 1"); + + CustomizableUI.addWidgetToArea( + "zoom-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + registerCleanupFunction(async () => { + CustomizableUI.reset(); + let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + let gLoadContext = Cu.createLoadContext(); + await new Promise(resolve => { + gContentPrefs.removeByName(window.FullZoom.name, gLoadContext, { + handleResult() {}, + handleCompletion() { + resolve(); + }, + }); + }); + }); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let zoomInButton = document.getElementById("zoom-in-button"); + ok(zoomInButton, "Zoom in button exists in Panel Menu"); + + zoomInButton.click(); + let pageZoomLevel = parseInt(ZoomManager.zoom * 100); + info("Page zoom level is: " + pageZoomLevel); + + let zoomResetButton = document.getElementById("zoom-reset-button"); + await TestUtils.waitForCondition(() => { + info( + "Current zoom is " + parseInt(zoomResetButton.getAttribute("label"), 10) + ); + return parseInt(zoomResetButton.getAttribute("label"), 10) == pageZoomLevel; + }); + + ok(pageZoomLevel > 100, "Page zoomed in correctly"); + + // close the Panel + let panelHiddenPromise = promiseOverflowHidden(window); + document.getElementById("widget-overflow").hidePopup(); + await panelHiddenPromise; + info("Menu panel was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_zoomOut.js b/browser/components/customizableui/test/browser_947914_button_zoomOut.js new file mode 100644 index 0000000000..b6175bccf5 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_zoomOut.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + info("Check zoom out button existence and functionality"); + + is(ZoomManager.zoom, 1, "Initial zoom factor should be 1"); + + CustomizableUI.addWidgetToArea( + "zoom-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + registerCleanupFunction(async () => { + CustomizableUI.reset(); + let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + let gLoadContext = Cu.createLoadContext(); + await new Promise(resolve => { + gContentPrefs.removeByName(window.FullZoom.name, gLoadContext, { + handleResult() {}, + handleCompletion() { + resolve(); + }, + }); + }); + }); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let zoomOutButton = document.getElementById("zoom-out-button"); + ok(zoomOutButton, "Zoom out button exists in Panel Menu"); + + zoomOutButton.click(); + let pageZoomLevel = Math.round(ZoomManager.zoom * 100); + console.log("Page zoom level is: ", pageZoomLevel); + + let zoomResetButton = document.getElementById("zoom-reset-button"); + await TestUtils.waitForCondition(() => { + console.log( + "Current zoom is ", + parseInt(zoomResetButton.getAttribute("label"), 10) + ); + return parseInt(zoomResetButton.getAttribute("label"), 10) == pageZoomLevel; + }); + + ok(pageZoomLevel < 100, "Page zoomed out correctly"); + + // close the panel + let panelHiddenPromise = promiseOverflowHidden(window); + document.getElementById("widget-overflow").hidePopup(); + await panelHiddenPromise; + info("Menu panel was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_zoomReset.js b/browser/components/customizableui/test/browser_947914_button_zoomReset.js new file mode 100644 index 0000000000..7dc8299b28 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_zoomReset.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var initialPageZoom = ZoomManager.zoom; + +add_task(async function () { + info("Check zoom reset button existence and functionality"); + is(initialPageZoom, 1, "Page zoom reset correctly"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http://example.com", waitForLoad: true }, + async function (browser) { + CustomizableUI.addWidgetToArea( + "zoom-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + registerCleanupFunction(() => CustomizableUI.reset()); + + CustomizableUI.addWidgetToArea( + "zoom-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + { + let zoomChange = BrowserTestUtils.waitForEvent( + gBrowser, + "FullZoomChange" + ); + ZoomManager.zoom = 0.5; + await zoomChange; + } + + await document.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let zoomResetButton = document.getElementById("zoom-reset-button"); + ok(zoomResetButton, "Zoom reset button exists in Panel Menu"); + + let zoomChange = BrowserTestUtils.waitForEvent( + gBrowser, + "FullZoomChange" + ); + zoomResetButton.click(); + await zoomChange; + + let pageZoomLevel = Math.floor(ZoomManager.zoom * 100); + let expectedZoomLevel = 100; + let buttonZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10); + is(pageZoomLevel, expectedZoomLevel, "Page zoom reset correctly"); + is( + pageZoomLevel, + buttonZoomLevel, + "Button displays the correct zoom level" + ); + + // close the panel + let panelHiddenPromise = promiseOverflowHidden(window); + document.getElementById("widget-overflow").hidePopup(); + await panelHiddenPromise; + info("Menu panel was closed"); + } + ); +}); + +add_task(async function asyncCleanup() { + // reset zoom level + ZoomManager.zoom = initialPageZoom; + info("Zoom level was restored"); +}); diff --git a/browser/components/customizableui/test/browser_947987_removable_default.js b/browser/components/customizableui/test/browser_947987_removable_default.js new file mode 100644 index 0000000000..84bbd3ed59 --- /dev/null +++ b/browser/components/customizableui/test/browser_947987_removable_default.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var kWidgetId = "test-removable-widget-default"; +const kNavBar = CustomizableUI.AREA_NAVBAR; +var widgetCounter = 0; + +registerCleanupFunction(removeCustomToolbars); + +// Sanity checks +add_task(function () { + let brokenSpec = { id: kWidgetId + widgetCounter++, removable: false }; + SimpleTest.doesThrow( + () => CustomizableUI.createWidget(brokenSpec), + "Creating non-removable widget without defaultArea should throw." + ); + + // Widget without removable set should be removable: + let wrapper = CustomizableUI.createWidget({ + id: kWidgetId + widgetCounter++, + }); + ok( + CustomizableUI.isWidgetRemovable(wrapper.id), + "Should be removable by default." + ); + CustomizableUI.destroyWidget(wrapper.id); +}); + +// Test non-removable widget with defaultArea +add_task(async function () { + // Non-removable widget with defaultArea should work: + let spec = { + id: kWidgetId + widgetCounter++, + removable: false, + defaultArea: kNavBar, + }; + let widgetWrapper; + try { + widgetWrapper = CustomizableUI.createWidget(spec); + } catch (ex) { + ok( + false, + "Creating a non-removable widget with a default area should not throw." + ); + return; + } + + let placement = CustomizableUI.getPlacementOfWidget(spec.id); + ok(placement, "Widget should be placed."); + is(placement.area, kNavBar, "Widget should be in navbar"); + let singleWrapper = widgetWrapper.forWindow(window); + ok(singleWrapper, "Widget should exist in window."); + ok(singleWrapper.node, "Widget node should exist in window."); + let expectedParent = CustomizableUI.getCustomizeTargetForArea( + kNavBar, + window + ); + is( + singleWrapper.node.parentNode, + expectedParent, + "Widget should be in navbar." + ); + + let otherWin = await openAndLoadWindow(true); + placement = CustomizableUI.getPlacementOfWidget(spec.id); + ok(placement, "Widget should be placed."); + is(placement && placement.area, kNavBar, "Widget should be in navbar"); + + singleWrapper = widgetWrapper.forWindow(otherWin); + ok(singleWrapper, "Widget should exist in other window."); + if (singleWrapper) { + ok(singleWrapper.node, "Widget node should exist in other window."); + if (singleWrapper.node) { + let expectedParentInOtherWin = CustomizableUI.getCustomizeTargetForArea( + kNavBar, + otherWin + ); + is( + singleWrapper.node.parentNode, + expectedParentInOtherWin, + "Widget should be in navbar in other window." + ); + } + } + CustomizableUI.destroyWidget(spec.id); + await promiseWindowClosed(otherWin); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js b/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js new file mode 100644 index 0000000000..ce8d3c2d3a --- /dev/null +++ b/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const kWidgetId = "test-destroy-non-removable-defaultArea"; + +add_task(function () { + let spec = { + id: kWidgetId, + label: "Test non-removable defaultArea re-adding.", + removable: false, + defaultArea: CustomizableUI.AREA_NAVBAR, + }; + CustomizableUI.createWidget(spec); + let placement = CustomizableUI.getPlacementOfWidget(kWidgetId); + ok(placement, "Should have placed the widget."); + is( + placement && placement.area, + CustomizableUI.AREA_NAVBAR, + "Widget should be in navbar" + ); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.removeWidgetFromArea(kWidgetId); + + CustomizableUI.createWidget(spec); + ok(placement, "Should have placed the widget."); + is( + placement && placement.area, + CustomizableUI.AREA_NAVBAR, + "Widget should be in navbar" + ); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.removeWidgetFromArea(kWidgetId); + + const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; + Services.prefs.setBoolPref(kPrefCustomizationAutoAdd, false); + CustomizableUI.createWidget(spec); + ok(placement, "Should have placed the widget."); + is( + placement && placement.area, + CustomizableUI.AREA_NAVBAR, + "Widget should be in navbar" + ); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.removeWidgetFromArea(kWidgetId); + Services.prefs.clearUserPref(kPrefCustomizationAutoAdd); +}); diff --git a/browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js b/browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js new file mode 100644 index 0000000000..1dab702fc8 --- /dev/null +++ b/browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbarName = "test-unregisterArea-areaType"; +const kUnregisterAreaTestWidget = "test-widget-for-unregisterArea-areaType"; +const kTestWidget = "test-widget-no-area-areaType"; +registerCleanupFunction(removeCustomToolbars); + +registerCleanupFunction(() => { + try { + CustomizableUI.destroyWidget(kTestWidget); + CustomizableUI.destroyWidget(kUnregisterAreaTestWidget); + } catch (ex) { + console.error(ex); + } +}); + +function checkAreaType(widget) { + try { + // widget.areaType returns either null or undefined + ok(!widget.areaType, "areaType should be null"); + } catch (ex) { + info("Fetching areaType threw: " + ex); + ok(false, "areaType getter shouldn't throw."); + } +} + +// widget wrappers in unregisterArea'd areas and nowhere shouldn't throw when checking areaTypes. +add_task(async function () { + // Using the ID before it's been created will imply a XUL wrapper; we'll test + // an API-based wrapper below + let toolbarNode = createToolbarWithPlacements(kToolbarName, [ + kUnregisterAreaTestWidget, + ]); + CustomizableUI.unregisterArea(kToolbarName); + toolbarNode.remove(); + + let w = CustomizableUI.getWidget(kUnregisterAreaTestWidget); + checkAreaType(w); + + w = CustomizableUI.getWidget(kTestWidget); + checkAreaType(w); + + let spec = { + id: kUnregisterAreaTestWidget, + type: "button", + removable: true, + label: "areaType test", + tooltiptext: "areaType test", + }; + CustomizableUI.createWidget(spec); + toolbarNode = createToolbarWithPlacements(kToolbarName, [ + kUnregisterAreaTestWidget, + ]); + CustomizableUI.unregisterArea(kToolbarName); + toolbarNode.remove(); + w = CustomizableUI.getWidget(spec.id); + checkAreaType(w); + CustomizableUI.removeWidgetFromArea(kUnregisterAreaTestWidget); + checkAreaType(w); + // XXXgijs: ensure cleanup function doesn't barf: + gAddedToolbars.delete(kToolbarName); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_956602_remove_special_widget.js b/browser/components/customizableui/test/browser_956602_remove_special_widget.js new file mode 100644 index 0000000000..237103b79e --- /dev/null +++ b/browser/components/customizableui/test/browser_956602_remove_special_widget.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Adding a separator and then dragging it out of the navbar shouldn't throw +add_task(async function () { + try { + let navbar = document.getElementById("nav-bar"); + let separatorSelector = + "toolbarseparator[id^=customizableui-special-separator]"; + ok( + !navbar.querySelector(separatorSelector), + "Shouldn't be a separator in the navbar" + ); + CustomizableUI.addWidgetToArea("separator", "nav-bar"); + await startCustomizing(); + let separator = navbar.querySelector(separatorSelector); + ok(separator, "There should be a separator in the navbar now."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(separator, palette); + ok( + !palette.querySelector(separatorSelector), + "No separator in the palette." + ); + } catch (ex) { + console.error(ex); + ok(false, "Shouldn't throw an exception moving an item to the navbar."); + } finally { + await endCustomizing(); + } +}); + +add_task(async function asyncCleanup() { + resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js b/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js new file mode 100644 index 0000000000..edfc8ff06d --- /dev/null +++ b/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var originalWindowWidth; + +// Drag to overflow chevron should open the overflow panel. +add_task(async function () { + // Load a page so the identity box can be dragged. + BrowserTestUtils.loadURIString(gBrowser, "http://mochi.test:8888/"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let widgetOverflowPanel = document.getElementById("widget-overflow"); + let panelShownPromise = promisePanelElementShown(window, widgetOverflowPanel); + let identityBox = document.getElementById("identity-icon-box"); + let overflowChevron = document.getElementById("nav-bar-overflow-button"); + + // Listen for hiding immediately so we don't miss the event because of the + // async-ness of the 'shown' yield... + let panelHiddenPromise = promisePanelElementHidden( + window, + widgetOverflowPanel + ); + + var ds = Cc["@mozilla.org/widget/dragservice;1"].getService( + Ci.nsIDragService + ); + + ds.startDragSessionForTests( + Ci.nsIDragService.DRAGDROP_ACTION_MOVE | + Ci.nsIDragService.DRAGDROP_ACTION_COPY | + Ci.nsIDragService.DRAGDROP_ACTION_LINK + ); + try { + var [result, dataTransfer] = EventUtils.synthesizeDragOver( + identityBox, + overflowChevron + ); + + // Wait for showing panel before ending drag session. + await panelShownPromise; + + EventUtils.synthesizeDropAfterDragOver( + result, + dataTransfer, + overflowChevron + ); + } finally { + ds.endDragSession(true); + } + + info("Overflow panel is shown."); + + widgetOverflowPanel.hidePopup(); + await panelHiddenPromise; +}); + +add_task(async function () { + window.resizeTo(originalWindowWidth, window.outerHeight); + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok( + !navbar.hasAttribute("overflowing"), + "Should not have an overflowing toolbar." + ); +}); diff --git a/browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js b/browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js new file mode 100644 index 0000000000..db829ab411 --- /dev/null +++ b/browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbar = "test-toolbar-963639-non-customizable-customizing-attribute"; + +add_task(async function () { + info( + "Test for Bug 963639 - CustomizeMode _onToolbarVisibilityChange sets @customizing on non-customizable toolbars" + ); + + let toolbar = document.createXULElement("toolbar"); + toolbar.id = kToolbar; + gNavToolbox.appendChild(toolbar); + + let testToolbar = document.getElementById(kToolbar); + ok(testToolbar, "Toolbar was created."); + is( + gNavToolbox.getElementsByAttribute("id", kToolbar).length, + 1, + "Toolbar was added to the navigator toolbox" + ); + + toolbar.setAttribute( + "toolbarname", + "NonCustomizableToolbarCustomizingAttribute" + ); + toolbar.setAttribute("collapsed", "true"); + + await startCustomizing(); + window.setToolbarVisibility(toolbar, "true"); + isnot( + toolbar.getAttribute("customizing"), + "true", + "Toolbar doesn't have the customizing attribute" + ); + + await endCustomizing(); + gNavToolbox.removeChild(toolbar); + + is( + gNavToolbox.getElementsByAttribute("id", kToolbar).length, + 0, + "Toolbar was removed from the navigator toolbox" + ); +}); diff --git a/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js b/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js new file mode 100644 index 0000000000..dbc45880d2 --- /dev/null +++ b/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kHidden1Id = "test-hidden-button-1"; +const kHidden2Id = "test-hidden-button-2"; + +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + +// When we drag an item onto a customizable area, and not over a specific target, we +// should assume that we're appending them to the area. If doing so, we should scan +// backwards over any hidden items and insert the item before those hidden items. +add_task(async function () { + ok(CustomizableUI.inDefaultState, "Should be in the default state"); + + // Iterate backwards over the items in the nav-bar until we find the first + // one that is not hidden. + let placements = CustomizableUI.getWidgetsInArea(CustomizableUI.AREA_NAVBAR); + let lastVisible = null; + for (let widgetGroup of placements.reverse()) { + let widget = widgetGroup.forWindow(window); + if (widget && widget.node && !widget.node.hidden) { + lastVisible = widget.node; + break; + } + } + + if (!lastVisible) { + ok(false, "Apparently, there are no visible items in the nav-bar."); + } + + info("The last visible item in the nav-bar has ID: " + lastVisible.id); + + let hidden1 = createDummyXULButton(kHidden1Id, "You can't see me"); + let hidden2 = createDummyXULButton(kHidden2Id, "You can't see me either."); + hidden1.hidden = hidden2.hidden = true; + + // Make sure we have some hidden items at the end of the nav-bar. + CustomizableUI.addWidgetToArea(kHidden1Id, "nav-bar"); + CustomizableUI.addWidgetToArea(kHidden2Id, "nav-bar"); + + // Drag an item and drop it onto the nav-bar customization target, but + // not over a particular item. + await startCustomizing(); + let homeButton = document.getElementById("home-button"); + let navbarTarget = CustomizableUI.getCustomizationTarget(navbar); + simulateItemDrag(homeButton, navbarTarget, "end"); + + await endCustomizing(); + + is( + homeButton.previousElementSibling.id, + lastVisible.id, + "The downloads button should be placed after the last visible item." + ); + + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js b/browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js new file mode 100644 index 0000000000..8207cd5737 --- /dev/null +++ b/browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function getPlacementArea(id) { + let placement = CustomizableUI.getPlacementOfWidget(id); + return placement && placement.area; +} + +// Check that a destroyed widget recreated after a reset call goes to +// the navigation bar. +add_task(function () { + const kWidgetId = "test-recreate-after-reset"; + let spec = { + id: kWidgetId, + label: "Test re-create after reset.", + removable: true, + defaultArea: CustomizableUI.AREA_NAVBAR, + }; + + CustomizableUI.createWidget(spec); + is( + getPlacementArea(kWidgetId), + CustomizableUI.AREA_NAVBAR, + "widget is in the navigation bar" + ); + + CustomizableUI.destroyWidget(kWidgetId); + isnot( + getPlacementArea(kWidgetId), + CustomizableUI.AREA_NAVBAR, + "widget removed from the navigation bar" + ); + + CustomizableUI.reset(); + + CustomizableUI.createWidget(spec); + is( + getPlacementArea(kWidgetId), + CustomizableUI.AREA_NAVBAR, + "widget recreated and added back to the nav bar" + ); + + CustomizableUI.destroyWidget(kWidgetId); +}); diff --git a/browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js b/browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js new file mode 100644 index 0000000000..71e83274c2 --- /dev/null +++ b/browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Adding the character encoding menu to the panel, exiting customize mode, +// and moving it to the nav-bar should have it disabled if the page in the +// content area isn't eligible to have its encoding overridden. +add_task(async function () { + await startCustomizing(); + CustomizableUI.addWidgetToArea( + "characterencoding-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await endCustomizing(); + await document.getElementById("nav-bar").overflowable.show(); + let panelHiddenPromise = promiseOverflowHidden(window); + PanelUI.overflowPanel.hidePopup(); + await panelHiddenPromise; + CustomizableUI.addWidgetToArea("characterencoding-button", "nav-bar"); + let button = document.getElementById("characterencoding-button"); + ok(button.hasAttribute("disabled"), "Button should be disabled"); +}); + +add_task(function asyncCleanup() { + resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_970511_undo_restore_default.js b/browser/components/customizableui/test/browser_970511_undo_restore_default.js new file mode 100644 index 0000000000..5477b41b80 --- /dev/null +++ b/browser/components/customizableui/test/browser_970511_undo_restore_default.js @@ -0,0 +1,274 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(2); + +// Restoring default should reset density and show an "undo" option which undoes +// the restoring operation. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.compactmode.show", true]], + }); + let stopReloadButtonId = "stop-reload-button"; + CustomizableUI.removeWidgetFromArea(stopReloadButtonId); + await startCustomizing(); + ok(!CustomizableUI.inDefaultState, "Not in default state to begin with"); + is( + CustomizableUI.getPlacementOfWidget(stopReloadButtonId), + null, + "Stop/reload button is in palette" + ); + let undoResetButton = document.getElementById( + "customization-undo-reset-button" + ); + is(undoResetButton.hidden, true, "The undo button is hidden before reset"); + + let densityButton = document.getElementById("customization-uidensity-button"); + let popup = document.getElementById("customization-uidensity-menu"); + let popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(densityButton, {}); + info("Clicked on density button"); + await popupShownPromise; + + let compactModeItem = document.getElementById( + "customization-uidensity-menuitem-compact" + ); + let win = document.getElementById("main-window"); + let densityChangedPromise = new Promise(resolve => { + let observer = new MutationObserver(() => { + if (win.getAttribute("uidensity") == "compact") { + resolve(); + observer.disconnect(); + } + }); + observer.observe(win, { + attributes: true, + attributeFilter: ["uidensity"], + }); + }); + + compactModeItem.doCommand(); + info("Clicked on compact density"); + await densityChangedPromise; + + await gCustomizeMode.reset(); + + ok(CustomizableUI.inDefaultState, "In default state after reset"); + is(undoResetButton.hidden, false, "The undo button is visible after reset"); + is( + win.hasAttribute("uidensity"), + false, + "The window has been restored to normal density." + ); + + await gCustomizeMode.undoReset(); + + is( + win.getAttribute("uidensity"), + "compact", + "Density has been reset to compact." + ); + ok(!CustomizableUI.inDefaultState, "Not in default state after undo-reset"); + is( + undoResetButton.hidden, + true, + "The undo button is hidden after clicking on the undo button" + ); + is( + CustomizableUI.getPlacementOfWidget(stopReloadButtonId), + null, + "Stop/reload button is in palette" + ); + + await gCustomizeMode.reset(); + await SpecialPowers.popPrefEnv(); +}); + +// Performing an action after a reset will hide the undo button. +add_task(async function action_after_reset_hides_undo() { + let stopReloadButtonId = "stop-reload-button"; + CustomizableUI.removeWidgetFromArea(stopReloadButtonId); + ok(!CustomizableUI.inDefaultState, "Not in default state to begin with"); + is( + CustomizableUI.getPlacementOfWidget(stopReloadButtonId), + null, + "Stop/reload button is in palette" + ); + let undoResetButton = document.getElementById( + "customization-undo-reset-button" + ); + is(undoResetButton.hidden, true, "The undo button is hidden before reset"); + + await gCustomizeMode.reset(); + + ok(CustomizableUI.inDefaultState, "In default state after reset"); + is(undoResetButton.hidden, false, "The undo button is visible after reset"); + + CustomizableUI.addWidgetToArea( + stopReloadButtonId, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + is( + undoResetButton.hidden, + true, + "The undo button is hidden after another change" + ); +}); + +// "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button +add_task(async function () { + let undoResetButton = document.getElementById( + "customization-undo-reset-button" + ); + is(undoResetButton.hidden, true, "The undo button is hidden before a reset"); + ok( + !CustomizableUI.inDefaultState, + "The browser should not be in default state" + ); + await gCustomizeMode.reset(); + + is(undoResetButton.hidden, false, "The undo button is visible after a reset"); + await endCustomizing(); + await startCustomizing(); + is( + undoResetButton.hidden, + true, + "The undo reset button should be hidden after entering customization mode" + ); +}); + +// Bug 971626 - Restore Defaults should collapse the Title Bar +add_task(async function () { + { + const supported = TabsInTitlebar.systemSupported; + is(typeof supported, "boolean"); + info("TabsInTitlebar support: " + supported); + if (!supported) { + return; + } + } + + const kDefaultValue = Services.appinfo.drawInTitlebar; + let restoreDefaultsButton = document.getElementById( + "customization-reset-button" + ); + let titlebarCheckbox = document.getElementById( + "customization-titlebar-visibility-checkbox" + ); + let undoResetButton = document.getElementById( + "customization-undo-reset-button" + ); + ok( + CustomizableUI.inDefaultState, + "Should be in default state at start of test" + ); + ok( + restoreDefaultsButton.disabled, + "Restore defaults button should be disabled when in default state" + ); + is( + titlebarCheckbox.hasAttribute("checked"), + !kDefaultValue, + "Title bar checkbox should reflect pref value" + ); + is( + undoResetButton.hidden, + true, + "Undo reset button should be hidden at start of test" + ); + + let prefName = "browser.tabs.inTitlebar"; + Services.prefs.setIntPref(prefName, !kDefaultValue); + ok( + !restoreDefaultsButton.disabled, + "Restore defaults button should be enabled when pref changed" + ); + is( + Services.appinfo.drawInTitlebar, + !kDefaultValue, + "Title bar checkbox should reflect changed pref value" + ); + is( + titlebarCheckbox.hasAttribute("checked"), + kDefaultValue, + "Title bar checkbox should reflect changed pref value" + ); + ok( + !CustomizableUI.inDefaultState, + "With titlebar flipped, no longer default" + ); + is( + undoResetButton.hidden, + true, + "Undo reset button should be hidden after pref change" + ); + + await gCustomizeMode.reset(); + ok( + restoreDefaultsButton.disabled, + "Restore defaults button should be disabled after reset" + ); + is( + titlebarCheckbox.hasAttribute("checked"), + !kDefaultValue, + "Title bar checkbox should reflect default value after reset" + ); + is( + Services.prefs.getIntPref(prefName), + 2, + "Reset should reset drawInTitlebar" + ); + is( + Services.appinfo.drawInTitlebar, + kDefaultValue, + "Default state should be restored" + ); + ok(CustomizableUI.inDefaultState, "In default state after titlebar reset"); + is( + undoResetButton.hidden, + false, + "Undo reset button should be visible after reset" + ); + ok( + !undoResetButton.disabled, + "Undo reset button should be enabled after reset" + ); + + await gCustomizeMode.undoReset(); + ok( + !restoreDefaultsButton.disabled, + "Restore defaults button should be enabled after undo-reset" + ); + is( + titlebarCheckbox.hasAttribute("checked"), + kDefaultValue, + "Title bar checkbox should reflect undo-reset value" + ); + ok(!CustomizableUI.inDefaultState, "No longer in default state after undo"); + is( + Services.prefs.getIntPref(prefName), + kDefaultValue ? 0 : 1, + "Undo-reset goes back to previous pref value" + ); + is( + undoResetButton.hidden, + true, + "Undo reset button should be hidden after undo-reset clicked" + ); + + Services.prefs.clearUserPref(prefName); + ok(CustomizableUI.inDefaultState, "In default state after pref cleared"); + is( + undoResetButton.hidden, + true, + "Undo reset button should be hidden at end of test" + ); +}); + +add_task(async function asyncCleanup() { + await gCustomizeMode.reset(); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_972267_customizationchange_events.js b/browser/components/customizableui/test/browser_972267_customizationchange_events.js new file mode 100644 index 0000000000..7d27b94136 --- /dev/null +++ b/browser/components/customizableui/test/browser_972267_customizationchange_events.js @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Create a new window, then move the stop/reload button to the menu and check both windows have +// customizationchange events fire on the toolbox: +add_task(async function () { + let newWindow = await openAndLoadWindow(); + let otherToolbox = newWindow.gNavToolbox; + + let handlerCalledCount = 0; + let handler = ev => { + handlerCalledCount++; + }; + + let stopReloadButton = document.getElementById("stop-reload-button"); + + gNavToolbox.addEventListener("customizationchange", handler); + otherToolbox.addEventListener("customizationchange", handler); + + await gCustomizeMode.addToPanel(stopReloadButton); + + is(handlerCalledCount, 2, "Should be called for both windows."); + + handlerCalledCount = 0; + gCustomizeMode.addToToolbar(stopReloadButton); + is(handlerCalledCount, 2, "Should be called for both windows."); + + gNavToolbox.removeEventListener("customizationchange", handler); + otherToolbox.removeEventListener("customizationchange", handler); + + await promiseWindowClosed(newWindow); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js b/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js new file mode 100644 index 0000000000..6a8cb26958 --- /dev/null +++ b/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js @@ -0,0 +1,597 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbarName = "test-insertNodeInWindow-placements-toolbar"; +const kTestWidgetPrefix = "test-widget-for-insertNodeInWindow-placements-"; + +/* +Tries to replicate the situation of having a placement list like this: + +exists-1,trying-to-insert-this,doesn't-exist,exists-2 +*/ +add_task(async function () { + let testWidgetExists = [true, false, false, true]; + let widgetIds = []; + for (let i = 0; i < testWidgetExists.length; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (testWidgetExists[i]) { + let spec = { + id, + type: "button", + removable: true, + label: "test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + assertAreaPlacements(kToolbarName, widgetIds); + + let btnId = kTestWidgetPrefix + 1; + let btn = createDummyXULButton(btnId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is( + btn.parentNode.id, + kToolbarName, + "New XUL widget should be placed inside new toolbar" + ); + + is( + btn.previousElementSibling.id, + toolbarNode.firstElementChild.id, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + btn.remove(); + removeCustomToolbars(); + await resetCustomization(); +}); + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +situation similar to: + +exists-1,exists-2,overflow-1,trying-to-insert-this,overflow-2 +*/ +add_task(async function () { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "insertNodeInWindow test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition( + () => + navbar.hasAttribute("overflowing") && + !navbar.querySelector("#" + widgetIds[0]) + ); + + let testWidgetId = kTestWidgetPrefix + 3; + + CustomizableUI.destroyWidget(testWidgetId); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + ok( + navbar.overflowable.isInOverflowList(btn), + "New XUL widget should be placed inside overflow of toolbar" + ); + is( + btn.previousElementSibling.id, + kTestWidgetPrefix + 2, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + is( + btn.nextElementSibling.id, + kTestWidgetPrefix + 4, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + await resetCustomization(); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,doesn't-exist,trying-to-insert-this,overflow-2 +*/ +add_task(async function () { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "insertNodeInWindow test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition( + () => + navbar.hasAttribute("overflowing") && + !navbar.querySelector("#" + widgetIds[0]) + ); + + let testWidgetId = kTestWidgetPrefix + 3; + + CustomizableUI.destroyWidget(kTestWidgetPrefix + 2); + CustomizableUI.destroyWidget(testWidgetId); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + ok( + navbar.overflowable.isInOverflowList(btn), + "New XUL widget should be placed inside overflow of toolbar" + ); + is( + btn.previousElementSibling.id, + kTestWidgetPrefix + 1, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + is( + btn.nextElementSibling.id, + kTestWidgetPrefix + 4, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + await resetCustomization(); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,doesn't-exist,trying-to-insert-this,doesn't-exist +*/ +add_task(async function () { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "insertNodeInWindow test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition( + () => + navbar.hasAttribute("overflowing") && + !navbar.querySelector("#" + widgetIds[0]) + ); + + let testWidgetId = kTestWidgetPrefix + 3; + + CustomizableUI.destroyWidget(kTestWidgetPrefix + 2); + CustomizableUI.destroyWidget(testWidgetId); + CustomizableUI.destroyWidget(kTestWidgetPrefix + 4); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + ok( + navbar.overflowable.isInOverflowList(btn), + "New XUL widget should be placed inside overflow of toolbar" + ); + is( + btn.previousElementSibling.id, + kTestWidgetPrefix + 1, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + is( + btn.nextElementSibling, + null, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + await resetCustomization(); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,can't-overflow,trying-to-insert-this,overflow-2 +*/ +add_task(async function () { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 5; i >= 0; i--) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "insertNodeInWindow test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar", 0); + } + + for (let i = 10; i < 15; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "insertNodeInWindow test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + // Wait for all the widgets to overflow. We can't just wait for the + // `overflowing` attribute because we leave time for layout flushes + // inbetween, so it's possible for the timeout to run before the + // navbar has "settled" + await TestUtils.waitForCondition(() => { + return ( + navbar.hasAttribute("overflowing") && + CustomizableUI.getCustomizationTarget( + navbar + ).lastElementChild.getAttribute("overflows") == "false" + ); + }); + + // Find last widget that doesn't allow overflowing + let nonOverflowing = + CustomizableUI.getCustomizationTarget(navbar).lastElementChild; + is( + nonOverflowing.getAttribute("overflows"), + "false", + "Last child is expected to not allow overflowing" + ); + isnot( + nonOverflowing.getAttribute("skipintoolbarset"), + "true", + "Last child is expected to not be skipintoolbarset" + ); + + let testWidgetId = kTestWidgetPrefix + 10; + CustomizableUI.destroyWidget(testWidgetId); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + ok( + navbar.overflowable.isInOverflowList(btn), + "New XUL widget should be placed inside overflow of toolbar" + ); + is( + btn.nextElementSibling.id, + kTestWidgetPrefix + 11, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + await resetCustomization(); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,trying-to-insert-this,can't-overflow,overflow-2 +*/ +add_task(async function () { + let widgetIds = []; + let missingId = 2; + let nonOverflowableId = 3; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (i != missingId) { + // Setting min-width to make the overflow state not depend on styling of the button and/or + // screen width + let spec = { + id, + type: "button", + removable: true, + label: "test", + tooltiptext: "" + i, + onCreated(node) { + node.style.minWidth = "200px"; + if (id == kTestWidgetPrefix + nonOverflowableId) { + node.setAttribute("overflows", false); + } + }, + }; + info("Creating: " + id); + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createOverflowableToolbarWithPlacements( + kToolbarName, + widgetIds + ); + assertAreaPlacements(kToolbarName, widgetIds); + ok( + !toolbarNode.hasAttribute("overflowing"), + "Toolbar shouldn't overflow to start with." + ); + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition( + () => + toolbarNode.hasAttribute("overflowing") && + !toolbarNode.querySelector("#" + widgetIds[1]) + ); + ok( + toolbarNode.hasAttribute("overflowing"), + "Should have an overflowing toolbar." + ); + + let btnId = kTestWidgetPrefix + missingId; + let btn = createDummyXULButton(btnId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is( + btn.parentNode.id, + kToolbarName + "-overflow-list", + "New XUL widget should be placed inside new toolbar's overflow" + ); + is( + btn.previousElementSibling.id, + kTestWidgetPrefix + 1, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + is( + btn.nextElementSibling.id, + kTestWidgetPrefix + 4, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition( + () => !toolbarNode.hasAttribute("overflowing") + ); + + btn.remove(); + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + removeCustomToolbars(); + await resetCustomization(); +}); + +/* +Tests nodes do *not* get placed in the toolbar's overflow. Replicates a +plcements situation similar to: + +exists-1,trying-to-insert-this,exists-2,overflowed-1 +*/ +add_task(async function () { + let widgetIds = []; + let missingId = 1; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (i != missingId) { + // Setting min-width to make the overflow state not depend on styling of the button and/or + // screen width + let spec = { + id, + type: "button", + removable: true, + label: "test", + tooltiptext: "" + i, + onCreated(node) { + node.style.minWidth = "200px"; + }, + }; + info("Creating: " + id); + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createOverflowableToolbarWithPlacements( + kToolbarName, + widgetIds + ); + assertAreaPlacements(kToolbarName, widgetIds); + ok( + !toolbarNode.hasAttribute("overflowing"), + "Toolbar shouldn't overflow to start with." + ); + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => + toolbarNode.hasAttribute("overflowing") + ); + ok( + toolbarNode.hasAttribute("overflowing"), + "Should have an overflowing toolbar." + ); + + let btnId = kTestWidgetPrefix + missingId; + let btn = createDummyXULButton(btnId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is( + btn.parentNode.id, + kToolbarName + "-target", + "New XUL widget should be placed inside new toolbar" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition( + () => !toolbarNode.hasAttribute("overflowing") + ); + + btn.remove(); + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + removeCustomToolbars(); + await resetCustomization(); +}); + +/* +Tests inserting a node onto the end of an overflowing toolbar *doesn't* put it in +the overflow list when the widget disallows overflowing. ie: + +exists-1,exists-2,overflows-1,trying-to-insert-this + +Where trying-to-insert-this has overflows=false +*/ +add_task(async function () { + let widgetIds = []; + let missingId = 3; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (i != missingId) { + // Setting min-width to make the overflow state not depend on styling of the button and/or + // screen width + let spec = { + id, + type: "button", + removable: true, + label: "test", + tooltiptext: "" + i, + onCreated(node) { + node.style.minWidth = "200px"; + }, + }; + info("Creating: " + id); + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createOverflowableToolbarWithPlacements( + kToolbarName, + widgetIds + ); + assertAreaPlacements(kToolbarName, widgetIds); + ok( + !toolbarNode.hasAttribute("overflowing"), + "Toolbar shouldn't overflow to start with." + ); + + let originalWindowWidth = window.outerWidth; + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => + toolbarNode.hasAttribute("overflowing") + ); + ok( + toolbarNode.hasAttribute("overflowing"), + "Should have an overflowing toolbar." + ); + + let btnId = kTestWidgetPrefix + missingId; + let btn = createDummyXULButton(btnId, "test"); + btn.setAttribute("overflows", false); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is( + btn.parentNode.id, + kToolbarName + "-target", + "New XUL widget should be placed inside new toolbar" + ); + is( + btn.nextElementSibling, + null, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements" + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); + await TestUtils.waitForCondition( + () => !toolbarNode.hasAttribute("overflowing") + ); + + btn.remove(); + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + removeCustomToolbars(); + await resetCustomization(); +}); + +add_task(async function asyncCleanUp() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_978084_dragEnd_after_move.js b/browser/components/customizableui/test/browser_978084_dragEnd_after_move.js new file mode 100644 index 0000000000..c818a1b468 --- /dev/null +++ b/browser/components/customizableui/test/browser_978084_dragEnd_after_move.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var draggedItem; + +/** + * Check that customizing-movingItem gets removed on a drop when the item is moved. + */ + +// Drop on the palette +add_task(async function () { + draggedItem = document.createXULElement("toolbarbutton"); + draggedItem.id = "test-dragEnd-after-move1"; + draggedItem.setAttribute("label", "Test"); + draggedItem.setAttribute("removable", "true"); + let navbar = document.getElementById("nav-bar"); + CustomizableUI.getCustomizationTarget(navbar).appendChild(draggedItem); + await startCustomizing(); + simulateItemDrag(draggedItem, gCustomizeMode.visiblePalette); + is( + document.documentElement.hasAttribute("customizing-movingItem"), + false, + "Make sure customizing-movingItem is removed after dragging to the palette" + ); + await endCustomizing(); +}); + +// Drop on a customization target itself +add_task(async function () { + draggedItem = document.createXULElement("toolbarbutton"); + draggedItem.id = "test-dragEnd-after-move2"; + draggedItem.setAttribute("label", "Test"); + draggedItem.setAttribute("removable", "true"); + let dest = createToolbarWithPlacements("test-dragEnd"); + let navbar = document.getElementById("nav-bar"); + CustomizableUI.getCustomizationTarget(navbar).appendChild(draggedItem); + await startCustomizing(); + simulateItemDrag(draggedItem, CustomizableUI.getCustomizationTarget(dest)); + is( + document.documentElement.hasAttribute("customizing-movingItem"), + false, + "Make sure customizing-movingItem is removed" + ); + await endCustomizing(); +}); + +registerCleanupFunction(async function asyncCleanup() { + await endCustomizing(); + removeCustomToolbars(); +}); diff --git a/browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js b/browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js new file mode 100644 index 0000000000..7cb78a2ea4 --- /dev/null +++ b/browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kToolbarName = "test-new-overflowable-toolbar"; +const kTestWidgetPrefix = "test-widget-for-overflowable-toolbar-"; + +add_task(async function addOverflowingToolbar() { + let originalWindowWidth = window.outerWidth; + + let widgetIds = []; + registerCleanupFunction(() => { + try { + for (let id of widgetIds) { + CustomizableUI.destroyWidget(id); + } + } catch (ex) { + console.error(ex); + } + }); + + for (let i = 0; i < 10; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = { + id, + type: "button", + removable: true, + label: "test", + tooltiptext: "" + i, + }; + CustomizableUI.createWidget(spec); + } + + let toolbarNode = createOverflowableToolbarWithPlacements( + kToolbarName, + widgetIds + ); + assertAreaPlacements(kToolbarName, widgetIds); + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + isnot( + toolbarNode.overflowable, + null, + "Toolbar should have overflowable controller" + ); + isnot( + CustomizableUI.getCustomizationTarget(toolbarNode), + null, + "Toolbar should have customization target" + ); + isnot( + CustomizableUI.getCustomizationTarget(toolbarNode), + toolbarNode, + "Customization target should not be toolbar node" + ); + + let oldChildCount = + CustomizableUI.getCustomizationTarget(toolbarNode).childElementCount; + let overflowableList = document.getElementById( + kToolbarName + "-overflow-list" + ); + let oldOverflowCount = overflowableList.childElementCount; + + isnot(oldChildCount, 0, "Toolbar should have non-overflowing widgets"); + + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition(() => + toolbarNode.hasAttribute("overflowing") + ); + ok( + toolbarNode.hasAttribute("overflowing"), + "Should have an overflowing toolbar." + ); + ok( + CustomizableUI.getCustomizationTarget(toolbarNode).childElementCount < + oldChildCount, + "Should have fewer children." + ); + ok( + overflowableList.childElementCount > oldOverflowCount, + "Should have more overflowed widgets." + ); + + window.resizeTo(originalWindowWidth, window.outerHeight); +}); + +add_task(async function asyncCleanup() { + removeCustomToolbars(); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_981305_separator_insertion.js b/browser/components/customizableui/test/browser_981305_separator_insertion.js new file mode 100644 index 0000000000..cce18f33a2 --- /dev/null +++ b/browser/components/customizableui/test/browser_981305_separator_insertion.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var tempElements = []; + +function insertTempItemsIntoMenu(parentMenu) { + // Last element is null to insert at the end: + let beforeEls = [ + parentMenu.firstElementChild, + parentMenu.lastElementChild, + null, + ]; + for (let i = 0; i < beforeEls.length; i++) { + let sep = document.createXULElement("menuseparator"); + tempElements.push(sep); + parentMenu.insertBefore(sep, beforeEls[i]); + let menu = document.createXULElement("menu"); + tempElements.push(menu); + parentMenu.insertBefore(menu, beforeEls[i]); + // And another separator for good measure: + sep = document.createXULElement("menuseparator"); + tempElements.push(sep); + parentMenu.insertBefore(sep, beforeEls[i]); + } +} + +async function checkSeparatorInsertion(menuId, buttonId, subviewId) { + info("Checking for duplicate separators in " + buttonId + " widget"); + let menu = document.getElementById(menuId); + insertTempItemsIntoMenu(menu); + + CustomizableUI.addWidgetToArea( + buttonId, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + + let button = document.getElementById(buttonId); + button.click(); + let subview = document.getElementById(subviewId); + await BrowserTestUtils.waitForEvent(subview, "ViewShown"); + + let subviewBody = subview.firstElementChild; + ok(subviewBody.firstElementChild, "Subview should have a kid"); + is( + subviewBody.firstElementChild.localName, + "toolbarbutton", + "There should be no separators to start with" + ); + + for (let kid of subviewBody.children) { + if (kid.localName == "menuseparator") { + ok( + kid.previousElementSibling && + kid.previousElementSibling.localName != "menuseparator", + "Separators should never have another separator next to them, and should never be the first node." + ); + } + } + + let panelHiddenPromise = promiseOverflowHidden(window); + PanelUI.overflowPanel.hidePopup(); + await panelHiddenPromise; + + CustomizableUI.reset(); +} + +add_task(async function check_devtools_separator() { + const panelviewId = "PanelUI-developer-tools"; + + await checkSeparatorInsertion( + "menuWebDeveloperPopup", + "developer-button", + panelviewId + ); +}); + +registerCleanupFunction(function () { + for (let el of tempElements) { + el.remove(); + } + tempElements = null; +}); diff --git a/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js b/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js new file mode 100644 index 0000000000..c3aee43404 --- /dev/null +++ b/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; +const kWidgetId = "test-981418-widget-onbeforecreated"; + +// Should be able to add broken view widget +add_task(async function testAddOnBeforeCreatedWidget() { + let onBeforeCreatedCalled = false; + let widgetSpec = { + id: kWidgetId, + type: "view", + viewId: kWidgetId + "idontexistyet", + onBeforeCreated(doc) { + let view = doc.createXULElement("panelview"); + view.id = kWidgetId + "idontexistyet"; + document.getElementById("appMenu-viewCache").appendChild(view); + onBeforeCreatedCalled = true; + }, + }; + + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + + ok(onBeforeCreatedCalled, "onBeforeCreated should have been called"); + + let widgetNode = document.getElementById(kWidgetId); + let viewNode = document.getElementById(kWidgetId + "idontexistyet"); + ok(widgetNode, "Widget should exist"); + ok(viewNode, "Panelview should exist"); + + let viewShownPromise = BrowserTestUtils.waitForEvent(viewNode, "ViewShown"); + widgetNode.click(); + await viewShownPromise; + + let widgetPanel = document.getElementById("customizationui-widget-panel"); + ok(widgetPanel, "Widget panel should exist"); + + let panelHiddenPromise = promisePanelElementHidden(window, widgetPanel); + widgetPanel.hidePopup(); + await panelHiddenPromise; + + CustomizableUI.addWidgetToArea( + kWidgetId, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await waitForOverflowButtonShown(); + await document.getElementById("nav-bar").overflowable.show(); + + viewShownPromise = BrowserTestUtils.waitForEvent(viewNode, "ViewShown"); + widgetNode.click(); + await viewShownPromise; + + let panelHidden = promiseOverflowHidden(window); + PanelUI.overflowPanel.hidePopup(); + await panelHidden; + + CustomizableUI.destroyWidget(kWidgetId); +}); + +add_task(async function asyncCleanup() { + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js b/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js new file mode 100644 index 0000000000..20314d6790 --- /dev/null +++ b/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Restoring default should not place addon widgets back in the toolbar +add_task(async function () { + ok(CustomizableUI.inDefaultState, "Default state to begin"); + + const kWidgetId = + "bug982656-add-on-widget-should-not-restore-to-default-area"; + let widgetSpec = { + id: kWidgetId, + defaultArea: CustomizableUI.AREA_NAVBAR, + }; + CustomizableUI.createWidget(widgetSpec); + + ok(!CustomizableUI.inDefaultState, "Not in default state after widget added"); + is( + CustomizableUI.getPlacementOfWidget(kWidgetId).area, + CustomizableUI.AREA_NAVBAR, + "Widget should be in navbar" + ); + + await resetCustomization(); + + ok(CustomizableUI.inDefaultState, "Back in default state after reset"); + is( + CustomizableUI.getPlacementOfWidget(kWidgetId), + null, + "Widget now in palette" + ); + CustomizableUI.destroyWidget(kWidgetId); +}); + +// resetCustomization shouldn't move 3rd party widgets out of custom toolbars +add_task(async function () { + const kToolbarId = "bug982656-toolbar-with-defaultset"; + const kWidgetId = + "bug982656-add-on-widget-should-restore-to-default-area-when-area-is-not-builtin"; + ok( + CustomizableUI.inDefaultState, + "Everything should be in its default state." + ); + let toolbar = createToolbarWithPlacements(kToolbarId); + ok(CustomizableUI.areas.includes(kToolbarId), "Toolbar has been registered."); + is( + CustomizableUI.getAreaType(kToolbarId), + CustomizableUI.TYPE_TOOLBAR, + "Area should be registered as toolbar" + ); + + let widgetSpec = { + id: kWidgetId, + defaultArea: kToolbarId, + }; + CustomizableUI.createWidget(widgetSpec); + + ok( + !CustomizableUI.inDefaultState, + "No longer in default state after toolbar is registered and visible." + ); + is( + CustomizableUI.getPlacementOfWidget(kWidgetId).area, + kToolbarId, + "Widget should be in custom toolbar" + ); + + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "Back in default state after reset"); + is( + CustomizableUI.getPlacementOfWidget(kWidgetId).area, + kToolbarId, + "Widget still in custom toolbar" + ); + ok(toolbar.collapsed, "Custom toolbar should be collapsed after reset"); + + toolbar.remove(); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.unregisterArea(kToolbarId); +}); diff --git a/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js new file mode 100644 index 0000000000..6d2295cc16 --- /dev/null +++ b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js @@ -0,0 +1,302 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var gNavBar = document.getElementById(CustomizableUI.AREA_NAVBAR); +var gOverflowList = document.getElementById( + gNavBar.getAttribute("default-overflowtarget") +); + +const kBookmarksButton = "bookmarks-menu-button"; +const kBookmarksItems = "personal-bookmarks"; +const kOriginalWindowWidth = window.outerWidth; + +/** + * Helper function that opens the bookmarks menu, and returns a Promise that + * resolves as soon as the menu is ready for interaction. + */ +function bookmarksMenuPanelShown() { + return new Promise(resolve => { + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + let onPopupShown = e => { + if (e.target == bookmarksMenuPopup) { + bookmarksMenuPopup.removeEventListener("popupshown", onPopupShown); + resolve(); + } + }; + bookmarksMenuPopup.addEventListener("popupshown", onPopupShown); + }); +} + +/** + * Checks that the placesContext menu is correctly attached to the + * controller of some view. Returns a Promise that resolves as soon + * as the context menu is closed. + * + * @param aItemWithContextMenu the item that we need to synthesize the + * right click on in order to open the context menu. + */ +function checkPlacesContextMenu(aItemWithContextMenu) { + return (async function () { + let contextMenu = document.getElementById("placesContext"); + let newBookmarkItem = document.getElementById("placesContext_new:bookmark"); + info("Waiting for context menu on " + aItemWithContextMenu.id); + let shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouseAtCenter(aItemWithContextMenu, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + ok( + !newBookmarkItem.hasAttribute("disabled"), + "New bookmark item shouldn't be disabled" + ); + + info("Closing context menu"); + let hiddenPromise = popupHidden(contextMenu); + // Use hidePopup instead of the closePopup helper because macOS native + // context menus can't be closed by synthesized ESC in automation. + contextMenu.hidePopup(); + await hiddenPromise; + })(); +} + +/** + * Opens the bookmarks menu panel, and then opens each of the "special" + * submenus in that list. Then it checks that those submenu's context menus + * are properly hooked up to a controller. + */ +function checkSpecialContextMenus() { + return (async function () { + let bookmarksMenuButton = document.getElementById(kBookmarksButton); + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + + const kSpecialItemIDs = { + BMB_bookmarksToolbar: "BMB_bookmarksToolbarPopup", + BMB_unsortedBookmarks: "BMB_unsortedBookmarksPopup", + }; + + // Open the bookmarks menu button context menus and ensure that + // they have the proper views attached. + let shownPromise = bookmarksMenuPanelShown(); + + EventUtils.synthesizeMouseAtCenter(bookmarksMenuButton, {}); + info("Waiting for bookmarks menu popup to show after clicking dropmarker."); + await shownPromise; + + for (let menuID in kSpecialItemIDs) { + let menuItem = document.getElementById(menuID); + let menuPopup = document.getElementById(kSpecialItemIDs[menuID]); + info("Waiting to open menu for " + menuID); + shownPromise = popupShown(menuPopup); + menuPopup.openPopup(menuItem, null, 0, 0, false, false, null); + await shownPromise; + + await checkPlacesContextMenu(menuPopup); + info("Closing menu for " + menuID); + await closePopup(menuPopup); + } + + info("Closing bookmarks menu"); + await closePopup(bookmarksMenuPopup); + })(); +} + +/** + * Closes a focused popup by simulating pressing the Escape key, + * and returns a Promise that resolves as soon as the popup is closed. + * + * @param aPopup the popup node to close. + */ +function closePopup(aPopup) { + let hiddenPromise = popupHidden(aPopup); + EventUtils.synthesizeKey("KEY_Escape"); + return hiddenPromise; +} + +/** + * Helper function that checks that the context menu of the + * bookmark toolbar items chevron popup is correctly hooked up + * to the controller of a view. + */ +function checkBookmarksItemsChevronContextMenu() { + return (async function () { + let chevronPopup = document.getElementById("PlacesChevronPopup"); + let shownPromise = popupShown(chevronPopup); + let chevron = document.getElementById("PlacesChevron"); + EventUtils.synthesizeMouseAtCenter(chevron, {}); + info("Waiting for bookmark toolbar item chevron popup to show"); + await shownPromise; + await TestUtils.waitForCondition(() => { + for (let child of chevronPopup.children) { + if (child.style.visibility != "hidden") { + return true; + } + } + return false; + }); + await checkPlacesContextMenu(chevronPopup); + info("Waiting for bookmark toolbar item chevron popup to close"); + await closePopup(chevronPopup); + })(); +} + +/** + * Forces the window to a width that causes the nav-bar to overflow + * its contents. Returns a Promise that resolves as soon as the + * overflowable nav-bar is showing its chevron. + */ +function overflowEverything() { + info("Waiting for overflow"); + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + return TestUtils.waitForCondition(() => gNavBar.hasAttribute("overflowing")); +} + +/** + * Returns the window to its original size from the start of the test, + * and returns a Promise that resolves when the nav-bar is no longer + * overflowing. + */ +function stopOverflowing() { + info("Waiting until we stop overflowing"); + window.resizeTo(kOriginalWindowWidth, window.outerHeight); + return TestUtils.waitForCondition(() => !gNavBar.hasAttribute("overflowing")); +} + +/** + * Checks that an item with ID aID is overflowing in the nav-bar. + * + * @param aID the ID of the node to check for overflowingness. + */ +function checkOverflowing(aID) { + ok( + !gNavBar.querySelector("#" + aID), + "Item with ID " + aID + " should no longer be in the gNavBar" + ); + let item = gOverflowList.querySelector("#" + aID); + ok(item, "Item with ID " + aID + " should be overflowing"); + is( + item.getAttribute("overflowedItem"), + "true", + "Item with ID " + aID + " should have overflowedItem attribute" + ); +} + +/** + * Checks that an item with ID aID is not overflowing in the nav-bar. + * + * @param aID the ID of hte node to check for non-overflowingness. + */ +function checkNotOverflowing(aID) { + ok( + !gOverflowList.querySelector("#" + aID), + "Item with ID " + aID + " should no longer be overflowing" + ); + let item = gNavBar.querySelector("#" + aID); + ok(item, "Item with ID " + aID + " should be in the nav bar"); + ok( + !item.hasAttribute("overflowedItem"), + "Item with ID " + aID + " should not have overflowedItem attribute" + ); +} + +/** + * Test that overflowing the bookmarks menu button doesn't break the + * context menus for the Unsorted and Bookmarks Toolbar menu items. + */ +add_task(async function testOverflowingBookmarksButtonContextMenu() { + ok(CustomizableUI.inDefaultState, "Should start in default state."); + // The DevEdition has the DevTools button in the toolbar by default. Remove it + // to prevent branch-specific available toolbar space. + CustomizableUI.removeWidgetFromArea("developer-button"); + CustomizableUI.removeWidgetFromArea( + "library-button", + CustomizableUI.AREA_NAVBAR + ); + CustomizableUI.addWidgetToArea(kBookmarksButton, CustomizableUI.AREA_NAVBAR); + ok( + !gNavBar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + + // Open the Unsorted and Bookmarks Toolbar context menus and ensure + // that they have views attached. + await checkSpecialContextMenus(); + + await overflowEverything(); + checkOverflowing(kBookmarksButton); + + await stopOverflowing(); + checkNotOverflowing(kBookmarksButton); + + await checkSpecialContextMenus(); +}); + +/** + * Test that the bookmarks toolbar items context menu still works if moved + * to the menu from the overflow panel, and then back to the toolbar. + */ +add_task(async function testOverflowingBookmarksItemsContextMenu() { + info("Ensuring panel is ready."); + await PanelUI.ensureReady(); + + let bookmarksToolbarItems = document.getElementById(kBookmarksItems); + await gCustomizeMode.addToToolbar(bookmarksToolbarItems); + await TestUtils.waitForCondition( + () => document.getElementById("PlacesToolbar")._placesView + ); + await checkPlacesContextMenu(bookmarksToolbarItems); + + await overflowEverything(); + checkOverflowing(kBookmarksItems); + + await gCustomizeMode.addToPanel(bookmarksToolbarItems); + + await stopOverflowing(); + + await gCustomizeMode.addToToolbar(bookmarksToolbarItems); + await TestUtils.waitForCondition( + () => document.getElementById("PlacesToolbar")._placesView + ); + await checkPlacesContextMenu(bookmarksToolbarItems); +}); + +/** + * Test that overflowing the bookmarks toolbar items doesn't cause the + * context menu in the bookmarks toolbar items chevron to stop working. + */ +add_task(async function testOverflowingBookmarksItemsChevronContextMenu() { + // If it's not already there, let's move the bookmarks toolbar items to + // the nav-bar. + let bookmarksToolbarItems = document.getElementById(kBookmarksItems); + await gCustomizeMode.addToToolbar(bookmarksToolbarItems); + + // We make the PlacesToolbarItems element be super tiny in order to force + // the bookmarks toolbar items into overflowing and making the chevron + // show itself. + let placesToolbarItems = document.getElementById("PlacesToolbarItems"); + let placesChevron = document.getElementById("PlacesChevron"); + placesToolbarItems.style.maxWidth = "10px"; + info("Waiting for chevron to no longer be collapsed"); + await TestUtils.waitForCondition(() => !placesChevron.collapsed); + + await checkBookmarksItemsChevronContextMenu(); + + await overflowEverything(); + checkOverflowing(kBookmarksItems); + + await stopOverflowing(); + checkNotOverflowing(kBookmarksItems); + + await checkBookmarksItemsChevronContextMenu(); + + placesToolbarItems.style.removeProperty("max-width"); +}); + +add_task(async function asyncCleanup() { + window.resizeTo(kOriginalWindowWidth, window.outerHeight); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js b/browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js new file mode 100644 index 0000000000..3b2bd13731 --- /dev/null +++ b/browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + ok(CustomizableUI.inDefaultState, "Should start in default state."); + this.otherWin = await openAndLoadWindow({ private: true }, true); + await startCustomizing(this.otherWin); + let resetButton = this.otherWin.document.getElementById( + "customization-reset-button" + ); + ok(resetButton.disabled, "Reset button should be disabled"); + + if (typeof CustomizableUI.setToolbarVisibility == "function") { + CustomizableUI.setToolbarVisibility("PersonalToolbar", true); + } else { + setToolbarVisibility(document.getElementById("PersonalToolbar"), true); + } + + let otherPersonalToolbar = + this.otherWin.document.getElementById("PersonalToolbar"); + let personalToolbar = document.getElementById("PersonalToolbar"); + ok( + !otherPersonalToolbar.collapsed, + "Toolbar should be uncollapsed in private window" + ); + ok( + !personalToolbar.collapsed, + "Toolbar should be uncollapsed in normal window" + ); + ok(!resetButton.disabled, "Reset button should be enabled"); + + await this.otherWin.gCustomizeMode.reset(); + + ok( + otherPersonalToolbar.collapsed, + "Toolbar should be collapsed in private window" + ); + ok(personalToolbar.collapsed, "Toolbar should be collapsed in normal window"); + ok(resetButton.disabled, "Reset button should be disabled"); + + await endCustomizing(this.otherWin); + + await promiseWindowClosed(this.otherWin); +}); + +add_task(async function asyncCleanup() { + if (this.otherWin && !this.otherWin.closed) { + await promiseWindowClosed(this.otherWin); + } + if (!CustomizableUI.inDefaultState) { + CustomizableUI.reset(); + } +}); diff --git a/browser/components/customizableui/test/browser_987177_destroyWidget_xul.js b/browser/components/customizableui/test/browser_987177_destroyWidget_xul.js new file mode 100644 index 0000000000..5881011b85 --- /dev/null +++ b/browser/components/customizableui/test/browser_987177_destroyWidget_xul.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const BUTTONID = "test-XUL-wrapper-destroyWidget"; + +add_task(function () { + let btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + let firstWrapper = CustomizableUI.getWidget(BUTTONID).forWindow(window); + ok(firstWrapper, "Should get a wrapper"); + ok(firstWrapper.node, "Node should be there on first wrapper."); + + btn.remove(); + CustomizableUI.destroyWidget(BUTTONID); + let secondWrapper = CustomizableUI.getWidget(BUTTONID).forWindow(window); + isnot( + firstWrapper, + secondWrapper, + "Wrappers should be different after destroyWidget call." + ); + ok(!firstWrapper.node, "No node should be there on old wrapper."); + ok(!secondWrapper.node, "No node should be there on new wrapper."); + + btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + let thirdWrapper = CustomizableUI.getWidget(BUTTONID).forWindow(window); + ok(thirdWrapper, "Should get a wrapper"); + is(secondWrapper, thirdWrapper, "Should get the second wrapper again."); + ok(firstWrapper.node, "Node should be there on old wrapper."); + ok(secondWrapper.node, "Node should be there on second wrapper."); + ok(thirdWrapper.node, "Node should be there on third wrapper."); +}); diff --git a/browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js b/browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js new file mode 100644 index 0000000000..9ef22c4e1b --- /dev/null +++ b/browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const BUTTONID = "test-XUL-wrapper-widget"; +add_task(function () { + let btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + let groupWrapper = CustomizableUI.getWidget(BUTTONID); + ok(groupWrapper, "Should get a group wrapper"); + let singleWrapper = groupWrapper.forWindow(window); + ok(singleWrapper, "Should get a single wrapper"); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.addWidgetToArea(BUTTONID, CustomizableUI.AREA_NAVBAR); + + let otherSingleWrapper = groupWrapper.forWindow(window); + is( + singleWrapper, + otherSingleWrapper, + "Should get the same wrapper after adding the node to the navbar." + ); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.removeWidgetFromArea(BUTTONID); + + otherSingleWrapper = groupWrapper.forWindow(window); + isnot( + singleWrapper, + otherSingleWrapper, + "Shouldn't get the same wrapper after removing it from the navbar." + ); + singleWrapper = otherSingleWrapper; + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + btn.remove(); + otherSingleWrapper = groupWrapper.forWindow(window); + is( + singleWrapper, + otherSingleWrapper, + "Should get the same wrapper after physically removing the node." + ); + is( + singleWrapper.node, + null, + "Wrapper's node should be null now that it's left the DOM." + ); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, null, "That instance should be null."); + + btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + otherSingleWrapper = groupWrapper.forWindow(window); + is( + singleWrapper, + otherSingleWrapper, + "Should get the same wrapper after readding the node." + ); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.addWidgetToArea(BUTTONID, CustomizableUI.AREA_NAVBAR); + + otherSingleWrapper = groupWrapper.forWindow(window); + is( + singleWrapper, + otherSingleWrapper, + "Should get the same wrapper after adding the node to the navbar." + ); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.removeWidgetFromArea(BUTTONID); + + otherSingleWrapper = groupWrapper.forWindow(window); + isnot( + singleWrapper, + otherSingleWrapper, + "Shouldn't get the same wrapper after removing it from the navbar." + ); + singleWrapper = otherSingleWrapper; + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + btn.remove(); + otherSingleWrapper = groupWrapper.forWindow(window); + is( + singleWrapper, + otherSingleWrapper, + "Should get the same wrapper after physically removing the node." + ); + is( + singleWrapper.node, + null, + "Wrapper's node should be null now that it's left the DOM." + ); + is( + groupWrapper.instances.length, + 1, + "There should be 1 instance on the group wrapper" + ); + is(groupWrapper.instances[0].node, null, "That instance should be null."); +}); diff --git a/browser/components/customizableui/test/browser_987492_window_api.js b/browser/components/customizableui/test/browser_987492_window_api.js new file mode 100644 index 0000000000..5e69573d60 --- /dev/null +++ b/browser/components/customizableui/test/browser_987492_window_api.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function testOneWindow() { + let windows = []; + for (let win of CustomizableUI.windows) { + windows.push(win); + } + is(windows.length, 1, "Should have one customizable window"); +}); + +add_task(async function testOpenCloseWindow() { + let newWindow = null; + let openListener = { + onWindowOpened(window) { + newWindow = window; + }, + }; + CustomizableUI.addListener(openListener); + + { + let win = await openAndLoadWindow(null, true); + is( + newWindow, + win, + "onWindowOpen event should have received expected window" + ); + isnot(newWindow, null, "Should have gotten onWindowOpen event"); + } + + CustomizableUI.removeListener(openListener); + + let windows = []; + for (let win of CustomizableUI.windows) { + windows.push(win); + } + is(windows.length, 2, "Should have two customizable windows"); + isnot( + windows.indexOf(window), + -1, + "Current window should be in window collection." + ); + isnot( + windows.indexOf(newWindow), + -1, + "New window should be in window collection." + ); + + let closedWindow = null; + let closeListener = { + onWindowClosed(window) { + closedWindow = window; + }, + }; + CustomizableUI.addListener(closeListener); + await promiseWindowClosed(newWindow); + isnot(closedWindow, null, "Should have gotten onWindowClosed event"); + is( + newWindow, + closedWindow, + "Closed window should match previously opened window" + ); + CustomizableUI.removeListener(closeListener); + + windows = []; + for (let win of CustomizableUI.windows) { + windows.push(win); + } + is(windows.length, 1, "Should have one customizable window"); + isnot( + windows.indexOf(window), + -1, + "Current window should be in window collection." + ); + is( + windows.indexOf(closedWindow), + -1, + "Closed window should not be in window collection." + ); +}); diff --git a/browser/components/customizableui/test/browser_987640_charEncoding.js b/browser/components/customizableui/test/browser_987640_charEncoding.js new file mode 100644 index 0000000000..65e38a0b85 --- /dev/null +++ b/browser/components/customizableui/test/browser_987640_charEncoding.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_PAGE = + "http://mochi.test:8888/browser/browser/components/customizableui/test/support/test_967000_charEncoding_page.html"; + +add_task(async function () { + info("Check Character Encoding panel functionality"); + + // add the Character Encoding button to the panel + CustomizableUI.addWidgetToArea( + "characterencoding-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE, + true, + true + ); + + await document.getElementById("nav-bar").overflowable.show(); + let charEncodingButton = document.getElementById("characterencoding-button"); + + ok( + !charEncodingButton.hasAttribute("disabled"), + "The encoding button should be enabled" + ); + + let browserStopPromise = BrowserTestUtils.browserStopped(gBrowser, TEST_PAGE); + charEncodingButton.click(); + await browserStopPromise; + is( + gBrowser.selectedBrowser.characterSet, + "UTF-8", + "The encoding should be changed to UTF-8" + ); + ok( + !gBrowser.selectedBrowser.mayEnableCharacterEncodingMenu, + "The encoding menu should be disabled" + ); + + is( + charEncodingButton.getAttribute("disabled"), + "true", + "We should disable the encoding button in toolbar" + ); + + CustomizableUI.removeWidgetFromArea("characterencoding-button"); + CustomizableUI.addWidgetToArea( + "characterencoding-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await waitForOverflowButtonShown(); + await document.getElementById("nav-bar").overflowable.show(); + charEncodingButton = document.getElementById("characterencoding-button"); + + // check the encoding menu again + is( + charEncodingButton.getAttribute("disabled"), + "true", + "We should disable the encoding button in overflow menu" + ); + + BrowserTestUtils.removeTab(newTab); +}); + +add_task(async function asyncCleanup() { + // reset the panel to the default state + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "The UI is in default state again."); +}); diff --git a/browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js b/browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js new file mode 100644 index 0000000000..d2b87a7a31 --- /dev/null +++ b/browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const BUTTONID = "test-widget-saved-earlier"; +const AREAID = "test-area-saved-earlier"; + +var hadSavedState; +function test() { + let gSavedState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + hadSavedState = gSavedState != null; + if (!hadSavedState) { + gSavedState = { placements: {} }; + CustomizableUI.setTestOnlyInternalProp("gSavedState", gSavedState); + } + gSavedState.placements[AREAID] = [BUTTONID]; + // Put bogus stuff in the saved state for the nav-bar, so as to check the current placements + // override this one... + gSavedState.placements[CustomizableUI.AREA_NAVBAR] = ["bogus-navbar-item"]; + + CustomizableUI.setTestOnlyInternalProp("gDirty", true); + CustomizableUI.getTestOnlyInternalProp("CustomizableUIInternal").saveState(); + + let newSavedState = JSON.parse( + Services.prefs.getCharPref("browser.uiCustomization.state") + ); + let savedArea = Array.isArray(newSavedState.placements[AREAID]); + ok( + savedArea, + "Should have re-saved the state, even though the area isn't registered" + ); + + if (savedArea) { + placementArraysEqual(AREAID, newSavedState.placements[AREAID], [BUTTONID]); + } + ok( + !CustomizableUI.getTestOnlyInternalProp("gPlacements").has(AREAID), + "Placements map shouldn't have been affected" + ); + + let savedNavbar = Array.isArray( + newSavedState.placements[CustomizableUI.AREA_NAVBAR] + ); + ok(savedNavbar, "Should have saved nav-bar contents"); + if (savedNavbar) { + placementArraysEqual( + CustomizableUI.AREA_NAVBAR, + newSavedState.placements[CustomizableUI.AREA_NAVBAR], + CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR) + ); + } +} + +registerCleanupFunction(function () { + if (!hadSavedState) { + CustomizableUI.setTestOnlyInternalProp("gSavedState", null); + } else { + let gSavedState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + let savedPlacements = gSavedState.placements; + delete savedPlacements[AREAID]; + let realNavBarPlacements = CustomizableUI.getWidgetIdsInArea( + CustomizableUI.AREA_NAVBAR + ); + savedPlacements[CustomizableUI.AREA_NAVBAR] = realNavBarPlacements; + } + CustomizableUI.setTestOnlyInternalProp("gDirty", true); + CustomizableUI.getTestOnlyInternalProp("CustomizableUIInternal").saveState(); +}); diff --git a/browser/components/customizableui/test/browser_989751_subviewbutton_class.js b/browser/components/customizableui/test/browser_989751_subviewbutton_class.js new file mode 100644 index 0000000000..d97017cfcd --- /dev/null +++ b/browser/components/customizableui/test/browser_989751_subviewbutton_class.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kCustomClass = "acustomclassnoonewilluse"; +const kDevPanelId = "PanelUI-developer-tools"; +var tempElement = null; + +function insertClassNameToMenuChildren(parentMenu) { + // Skip hidden menuitem elements, not copied via fillSubviewFromMenuItems. + let el = parentMenu.querySelector("menuitem:not([hidden])"); + el.classList.add(kCustomClass); + tempElement = el; +} + +function checkSubviewButtonClass(menuId, buttonId, subviewId) { + return async function () { + // Initialize DevTools before starting the test in order to create menuitems in + // menuWebDeveloperPopup. + ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ).require("devtools/client/framework/devtools-browser"); + + info( + "Checking for items without the subviewbutton class in " + + buttonId + + " widget" + ); + let menu = document.getElementById(menuId); + insertClassNameToMenuChildren(menu); + + CustomizableUI.addWidgetToArea( + buttonId, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + await document.getElementById("nav-bar").overflowable.show(); + + let button = document.getElementById(buttonId); + button.click(); + + await BrowserTestUtils.waitForEvent(PanelUI.overflowPanel, "ViewShown"); + let subview = document.getElementById(subviewId); + ok(subview.firstElementChild, "Subview should have a kid"); + + // The Developer Panel contains the Customize Toolbar item, + // as well as the Developer Tools items (bug 1703150). We only want to query for + // the Developer Tools items in this case. + let query = "#appmenu-developer-tools-view toolbarbutton"; + let subviewchildren = subview.querySelectorAll(query); + + for (let i = 0; i < subviewchildren.length; i++) { + let item = subviewchildren[i]; + let itemReadable = + "Item '" + item.label + "' (classes: " + item.className + ")"; + ok( + item.classList.contains("subviewbutton"), + itemReadable + " should have the subviewbutton class." + ); + if (i == 0) { + ok( + item.classList.contains(kCustomClass), + itemReadable + " should still have its own class, too." + ); + } + } + + let panelHiddenPromise = promiseOverflowHidden(window); + PanelUI.overflowPanel.hidePopup(); + await panelHiddenPromise; + + CustomizableUI.reset(); + }; +} + +add_task( + checkSubviewButtonClass( + "menuWebDeveloperPopup", + "developer-button", + kDevPanelId + ) +); + +registerCleanupFunction(function () { + tempElement.classList.remove(kCustomClass); + tempElement = null; +}); diff --git a/browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js b/browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js new file mode 100644 index 0000000000..d89845f03b --- /dev/null +++ b/browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TOOLBARID = "test-noncustomizable-toolbar-for-toggling"; +function test() { + let tb = document.createXULElement("toolbar"); + tb.id = TOOLBARID; + gNavToolbox.appendChild(tb); + try { + CustomizableUI.setToolbarVisibility(TOOLBARID, false); + } catch (ex) { + ok(false, "Should not throw exceptions trying to set toolbar visibility."); + } + is(tb.getAttribute("collapsed"), "true", "Toolbar should be collapsed"); + try { + CustomizableUI.setToolbarVisibility(TOOLBARID, true); + } catch (ex) { + ok(false, "Should not throw exceptions trying to set toolbar visibility."); + } + is(tb.getAttribute("collapsed"), "false", "Toolbar should be uncollapsed"); + tb.remove(); +} diff --git a/browser/components/customizableui/test/browser_993322_widget_notoolbar.js b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js new file mode 100644 index 0000000000..5e6cc65585 --- /dev/null +++ b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const BUTTONID = "test-API-created-widget-toolbar-gone"; +const TOOLBARID = "test-API-created-extra-toolbar"; + +add_task(async function () { + let toolbar = createToolbarWithPlacements(TOOLBARID, []); + CustomizableUI.addWidgetToArea(BUTTONID, TOOLBARID); + is( + CustomizableUI.getPlacementOfWidget(BUTTONID).area, + TOOLBARID, + "Should be on toolbar" + ); + is(toolbar.children.length, 0, "Toolbar has no kid"); + + CustomizableUI.unregisterArea(TOOLBARID); + CustomizableUI.createWidget({ + id: BUTTONID, + label: "Test widget toolbar gone", + }); + + let currentWidget = CustomizableUI.getWidget(BUTTONID); + + await startCustomizing(); + let buttonNode = document.getElementById(BUTTONID); + ok(buttonNode, "Should find button in window"); + if (buttonNode) { + is( + buttonNode.parentNode.localName, + "toolbarpaletteitem", + "Node should be wrapped" + ); + is( + buttonNode.parentNode.getAttribute("place"), + "palette", + "Node should be in palette" + ); + is( + buttonNode, + gNavToolbox.palette.querySelector("#" + BUTTONID), + "Node should really be in palette." + ); + } + is( + currentWidget.forWindow(window).node, + buttonNode, + "Should have the same node for customize mode" + ); + await endCustomizing(); + + CustomizableUI.destroyWidget(BUTTONID); + CustomizableUI.unregisterArea(TOOLBARID, true); + toolbar.remove(); + gAddedToolbars.clear(); +}); diff --git a/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js new file mode 100644 index 0000000000..8829611083 --- /dev/null +++ b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js @@ -0,0 +1,278 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TOOLBARID = "test-toolbar-added-during-customize-mode"; + +// The ID of a button that is not placed (ie, is in the palette) by default +const kNonPlacedWidgetId = "open-file-button"; + +add_task(async function () { + await startCustomizing(); + let toolbar = createToolbarWithPlacements(TOOLBARID, []); + CustomizableUI.addWidgetToArea(kNonPlacedWidgetId, TOOLBARID); + let button = document.getElementById(kNonPlacedWidgetId); + ok(button, "Button should exist."); + is( + button.parentNode.localName, + "toolbarpaletteitem", + "Button's parent node should be a wrapper." + ); + + simulateItemDrag(button, gNavToolbox.palette); + ok( + !CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), + "Button moved to the palette" + ); + ok( + gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), + "Button really is in palette." + ); + + button.scrollIntoView(); + simulateItemDrag(button, toolbar); + ok( + CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), + "Button moved out of palette" + ); + is( + CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, + TOOLBARID, + "Button's back on toolbar" + ); + ok( + toolbar.querySelector(`#${kNonPlacedWidgetId}`), + "Button really is on toolbar." + ); + + await endCustomizing(); + isnot( + button.parentNode.localName, + "toolbarpaletteitem", + "Button's parent node should not be a wrapper outside customize mode." + ); + await startCustomizing(); + + is( + button.parentNode.localName, + "toolbarpaletteitem", + "Button's parent node should be a wrapper back in customize mode." + ); + + simulateItemDrag(button, gNavToolbox.palette); + ok( + !CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), + "Button moved to the palette" + ); + ok( + gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), + "Button really is in palette." + ); + + ok( + !CustomizableUI.inDefaultState, + "Not in default state while toolbar is not collapsed yet." + ); + setToolbarVisibility(toolbar, false); + ok( + CustomizableUI.inDefaultState, + "In default state while toolbar is collapsed." + ); + + setToolbarVisibility(toolbar, true); + + info( + "Check that removing the area registration from within customize mode works" + ); + CustomizableUI.unregisterArea(TOOLBARID); + ok( + CustomizableUI.inDefaultState, + "Now that the toolbar is no longer registered, should be in default state." + ); + ok( + !gCustomizeMode.areas.has(toolbar), + "Toolbar shouldn't be known to customize mode." + ); + + CustomizableUI.registerArea(TOOLBARID, { defaultPlacements: [] }); + CustomizableUI.registerToolbarNode(toolbar, []); + ok( + !CustomizableUI.inDefaultState, + "Now that the toolbar is registered again, should no longer be in default state." + ); + ok( + gCustomizeMode.areas.has(toolbar), + "Toolbar should be known to customize mode again." + ); + + button.scrollIntoView(); + simulateItemDrag(button, toolbar); + ok( + CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), + "Button moved out of palette" + ); + is( + CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, + TOOLBARID, + "Button's back on toolbar" + ); + ok( + toolbar.querySelector(`#${kNonPlacedWidgetId}`), + "Button really is on toolbar." + ); + + let otherWin = await openAndLoadWindow({}, true); + let otherTB = otherWin.document.createXULElement("toolbar"); + otherTB.id = TOOLBARID; + otherTB.setAttribute("customizable", "true"); + let wasInformedCorrectlyOfAreaAppearing = false; + let listener = { + onAreaNodeRegistered(aArea, aNode) { + if (aNode == otherTB) { + wasInformedCorrectlyOfAreaAppearing = true; + } + }, + }; + CustomizableUI.addListener(listener); + otherWin.gNavToolbox.appendChild(otherTB); + CustomizableUI.registerToolbarNode(otherTB); + ok( + wasInformedCorrectlyOfAreaAppearing, + "Should have been told area was registered." + ); + CustomizableUI.removeListener(listener); + + ok( + otherTB.querySelector(`#${kNonPlacedWidgetId}`), + "Button is on other toolbar, too." + ); + + simulateItemDrag(button, gNavToolbox.palette); + ok( + !CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), + "Button moved to the palette" + ); + ok( + gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), + "Button really is in palette." + ); + ok( + !otherTB.querySelector(`#${kNonPlacedWidgetId}`), + "Button is in palette in other window, too." + ); + + button.scrollIntoView(); + simulateItemDrag(button, toolbar); + ok( + CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), + "Button moved out of palette" + ); + is( + CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, + TOOLBARID, + "Button's back on toolbar" + ); + ok( + toolbar.querySelector(`#${kNonPlacedWidgetId}`), + "Button really is on toolbar." + ); + ok( + otherTB.querySelector(`#${kNonPlacedWidgetId}`), + "Button is on other toolbar, too." + ); + + let wasInformedCorrectlyOfAreaDisappearing = false; + // XXXgijs So we could be using promiseWindowClosed here. However, after + // repeated random oranges, I'm instead relying on onWindowClosed below to + // fire appropriately - it is linked to an unload event as well, and so + // reusing it prevents a potential race between unload handlers where the + // one from promiseWindowClosed could fire before the onWindowClosed + // (and therefore onAreaNodeRegistered) one, causing the test to fail. + let windowClosed = await new Promise(resolve => { + listener = { + onAreaNodeUnregistered(aArea, aNode, aReason) { + if (aArea == TOOLBARID) { + is(aNode, otherTB, "Should be informed about other toolbar"); + is( + aReason, + CustomizableUI.REASON_WINDOW_CLOSED, + "Reason should be correct." + ); + wasInformedCorrectlyOfAreaDisappearing = + aReason === CustomizableUI.REASON_WINDOW_CLOSED; + } + }, + onWindowClosed(aWindow) { + if (aWindow == otherWin) { + resolve(aWindow); + } else { + info("Other window was closed!"); + info( + "Other window title: " + + (aWindow.document && aWindow.document.title) + ); + info( + "Our window title: " + + (otherWin.document && otherWin.document.title) + ); + } + }, + }; + CustomizableUI.addListener(listener); + otherWin.close(); + }); + + is( + windowClosed, + otherWin, + "Window should have sent onWindowClosed notification." + ); + ok( + wasInformedCorrectlyOfAreaDisappearing, + "Should be told about window closing." + ); + // Closing the other window should not be counted against this window's customize mode: + is( + button.parentNode.localName, + "toolbarpaletteitem", + "Button's parent node should still be a wrapper." + ); + ok( + gCustomizeMode.areas.has(toolbar), + "Toolbar should still be a customizable area for this customize mode instance." + ); + + await gCustomizeMode.reset(); + + await endCustomizing(); + + CustomizableUI.removeListener(listener); + wasInformedCorrectlyOfAreaDisappearing = false; + listener = { + onAreaNodeUnregistered(aArea, aNode, aReason) { + if (aArea == TOOLBARID) { + is(aNode, toolbar, "Should be informed about this window's toolbar"); + is( + aReason, + CustomizableUI.REASON_AREA_UNREGISTERED, + "Reason for final removal should be correct." + ); + wasInformedCorrectlyOfAreaDisappearing = + aReason === CustomizableUI.REASON_AREA_UNREGISTERED; + } + }, + }; + CustomizableUI.addListener(listener); + removeCustomToolbars(); + ok( + wasInformedCorrectlyOfAreaDisappearing, + "Should be told about area being unregistered." + ); + CustomizableUI.removeListener(listener); + ok( + CustomizableUI.inDefaultState, + "Should be fine after exiting customize mode." + ); +}); diff --git a/browser/components/customizableui/test/browser_996364_registerArea_different_properties.js b/browser/components/customizableui/test/browser_996364_registerArea_different_properties.js new file mode 100644 index 0000000000..7d8ea59150 --- /dev/null +++ b/browser/components/customizableui/test/browser_996364_registerArea_different_properties.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Calling CustomizableUI.registerArea twice with no +// properties should not throw an exception. +add_task(function () { + try { + CustomizableUI.registerArea("area-996364", {}); + CustomizableUI.registerArea("area-996364", {}); + } catch (ex) { + ok(false, ex.message); + } + + CustomizableUI.unregisterArea("area-996364", true); +}); + +add_task(function () { + let exceptionThrown = false; + try { + CustomizableUI.registerArea("area-996364-2", { + type: CustomizableUI.TYPE_TOOLBAR, + defaultCollapsed: "false", + }); + } catch (ex) { + exceptionThrown = true; + } + ok( + exceptionThrown, + "defaultCollapsed is not allowed as an external property" + ); + + // No need to unregister the area because registration fails. +}); + +add_task(function () { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996364-3", { + type: CustomizableUI.TYPE_TOOLBAR, + }); + CustomizableUI.registerArea("area-996364-3", { + type: CustomizableUI.TYPE_PANEL, + }); + } catch (ex) { + exceptionThrown = ex; + } + ok( + exceptionThrown, + "Exception expected, an area cannot change types: " + + (exceptionThrown ? exceptionThrown : "[no exception]") + ); + + CustomizableUI.unregisterArea("area-996364-3", true); +}); + +add_task(function () { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996364-4", { + type: CustomizableUI.TYPE_PANEL, + }); + CustomizableUI.registerArea("area-996364-4", { + type: CustomizableUI.TYPE_TOOLBAR, + }); + } catch (ex) { + exceptionThrown = ex; + } + ok( + exceptionThrown, + "Exception expected, an area cannot change types: " + + (exceptionThrown ? exceptionThrown : "[no exception]") + ); + + CustomizableUI.unregisterArea("area-996364-4", true); +}); + +add_task(function () { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-1", { + anchor: "PanelUI-menu-button", + type: CustomizableUI.TYPE_PANEL, + defaultPlacements: [], + }); + CustomizableUI.registerArea("area-996899-1", { + anchor: "home-button", + type: CustomizableUI.TYPE_PANEL, + defaultPlacements: [], + }); + } catch (ex) { + exceptionThrown = ex; + } + ok( + !exceptionThrown, + "Changing anchors shouldn't throw an exception: " + + (exceptionThrown ? exceptionThrown : "[no exception]") + ); + CustomizableUI.unregisterArea("area-996899-1", true); +}); + +add_task(function () { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-2", { + anchor: "PanelUI-menu-button", + type: CustomizableUI.TYPE_PANEL, + defaultPlacements: [], + }); + CustomizableUI.registerArea("area-996899-2", { + anchor: "PanelUI-menu-button", + type: CustomizableUI.TYPE_PANEL, + defaultPlacements: ["new-window-button"], + }); + } catch (ex) { + exceptionThrown = ex; + } + ok( + !exceptionThrown, + "Changing defaultPlacements shouldn't throw an exception: " + + (exceptionThrown ? exceptionThrown : "[no exception]") + ); + CustomizableUI.unregisterArea("area-996899-2", true); +}); + +add_task(function () { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-4", { overflowable: true }); + CustomizableUI.registerArea("area-996899-4", { overflowable: false }); + } catch (ex) { + exceptionThrown = ex; + } + ok( + exceptionThrown, + "Changing 'overflowable' should throw an exception: " + + (exceptionThrown ? exceptionThrown : "[no exception]") + ); + CustomizableUI.unregisterArea("area-996899-4", true); +}); diff --git a/browser/components/customizableui/test/browser_996635_remove_non_widgets.js b/browser/components/customizableui/test/browser_996635_remove_non_widgets.js new file mode 100644 index 0000000000..80f68433e8 --- /dev/null +++ b/browser/components/customizableui/test/browser_996635_remove_non_widgets.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// NB: This is testing what happens if something that /isn't/ a customizable +// widget gets used in CustomizableUI APIs. Don't use this as an example of +// what should happen in a "normal" case or how you should use the API. +function test() { + // First create a button that isn't customizable, and add it in the nav-bar, + // but not in the customizable part of it (the customization target) but + // next to the main (hamburger) menu button. + const buttonID = "Test-non-widget-non-removable-button"; + let btn = document.createXULElement("toolbarbutton"); + btn.id = buttonID; + btn.label = "Hi"; + btn.setAttribute("style", "width: 20px; height: 20px; background-color: red"); + document.getElementById("nav-bar").appendChild(btn); + registerCleanupFunction(function () { + btn.remove(); + }); + + // Now try to add this non-customizable button to the tabstrip. This will + // update the internal bookkeeping (ie placements) information, but shouldn't + // move the node. + CustomizableUI.addWidgetToArea(buttonID, CustomizableUI.AREA_TABSTRIP); + let placement = CustomizableUI.getPlacementOfWidget(buttonID); + // Check our bookkeeping + ok(placement, "Button should be placed"); + is( + placement && placement.area, + CustomizableUI.AREA_TABSTRIP, + "Should be placed on tabstrip." + ); + // Check we didn't move the node. + is( + btn.parentNode && btn.parentNode.id, + "nav-bar", + "Actual button should still be on navbar." + ); + + // Now remove the node again. This should remove the bookkeeping, but again + // not affect the actual node. + CustomizableUI.removeWidgetFromArea(buttonID); + placement = CustomizableUI.getPlacementOfWidget(buttonID); + // Check our bookkeeping: + ok(!placement, "Button should no longer have a placement."); + // Check our node. + is( + btn.parentNode && btn.parentNode.id, + "nav-bar", + "Actual button should still be on navbar." + ); +} diff --git a/browser/components/customizableui/test/browser_PanelMultiView.js b/browser/components/customizableui/test/browser_PanelMultiView.js new file mode 100644 index 0000000000..80778b94b0 --- /dev/null +++ b/browser/components/customizableui/test/browser_PanelMultiView.js @@ -0,0 +1,566 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Unit tests for the PanelMultiView module. + */ + +const PANELS_COUNT = 2; +let gPanelAnchors = []; +let gPanels = []; +let gPanelMultiViews = []; + +const PANELVIEWS_COUNT = 4; +let gPanelViews = []; +let gPanelViewLabels = []; + +const EVENT_TYPES = [ + "popupshown", + "popuphidden", + "PanelMultiViewHidden", + "ViewShowing", + "ViewShown", + "ViewHiding", +]; + +/** + * Checks that the element is displayed, including the state of the popup where + * the element is located. This can trigger a synchronous reflow if necessary, + * because even though the code under test is designed to avoid synchronous + * reflows, it can raise completion events while a layout flush is still needed. + * + * In production code, event handlers for ViewShown have to wait for a flush if + * they need to read style or layout information, like other code normally does. + */ +function is_visible(element) { + let win = element.ownerGlobal; + let style = win.getComputedStyle(element); + if (style.display == "none") { + return false; + } + if (style.visibility != "visible") { + return false; + } + if (win.XULPopupElement.isInstance(element) && element.state != "open") { + return false; + } + + // Hiding a parent element will hide all its children + if (element.parentNode != element.ownerDocument) { + return is_visible(element.parentNode); + } + + return true; +} + +/** + * Checks whether the label in the specified view is visible. + */ +function assertLabelVisible(viewIndex, expectedVisible) { + Assert.equal( + is_visible(gPanelViewLabels[viewIndex]), + expectedVisible, + `Visibility of label in view ${viewIndex}` + ); +} + +/** + * Opens the specified view as the main view in the specified panel. + */ +async function openPopup(panelIndex, viewIndex) { + gPanelMultiViews[panelIndex].setAttribute( + "mainViewId", + gPanelViews[viewIndex].id + ); + + let promiseShown = BrowserTestUtils.waitForEvent( + gPanelViews[viewIndex], + "ViewShown" + ); + PanelMultiView.openPopup( + gPanels[panelIndex], + gPanelAnchors[panelIndex], + "bottomright topright" + ); + await promiseShown; + + Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active); + assertLabelVisible(viewIndex, true); +} + +/** + * Closes the specified panel. + */ +async function hidePopup(panelIndex) { + gPanelMultiViews[panelIndex].setAttribute( + "mainViewId", + gPanelViews[panelIndex].id + ); + + let promiseHidden = BrowserTestUtils.waitForEvent( + gPanels[panelIndex], + "popuphidden" + ); + PanelMultiView.hidePopup(gPanels[panelIndex]); + await promiseHidden; +} + +/** + * Opens the specified subview in the specified panel. + */ +async function showSubView(panelIndex, viewIndex) { + let promiseShown = BrowserTestUtils.waitForEvent( + gPanelViews[viewIndex], + "ViewShown" + ); + gPanelMultiViews[panelIndex].showSubView(gPanelViews[viewIndex]); + await promiseShown; + + Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active); + assertLabelVisible(viewIndex, true); +} + +/** + * Navigates backwards to the specified view, which is displayed as a result. + */ +async function goBack(panelIndex, viewIndex) { + let promiseShown = BrowserTestUtils.waitForEvent( + gPanelViews[viewIndex], + "ViewShown" + ); + gPanelMultiViews[panelIndex].goBack(); + await promiseShown; + + Assert.ok(PanelView.forNode(gPanelViews[viewIndex]).active); + assertLabelVisible(viewIndex, true); +} + +/** + * Records the specified events on an element into the specified array. An + * optional callback can be used to respond to events and trigger nested events. + */ +function recordEvents( + element, + eventTypes, + recordArray, + eventCallback = () => {} +) { + let nestedEvents = []; + element.recorders = eventTypes.map(eventType => { + let recorder = { + eventType, + listener(event) { + let eventString = + nestedEvents.join("") + `${event.originalTarget.id}: ${event.type}`; + info(`Event on ${eventString}`); + recordArray.push(eventString); + // Any synchronous event triggered from within the given callback will + // include information about the current event. + nestedEvents.unshift(`${eventString} > `); + eventCallback(event); + nestedEvents.shift(); + }, + }; + element.addEventListener(recorder.eventType, recorder.listener); + return recorder; + }); +} + +/** + * Stops recording events on an element. + */ +function stopRecordingEvents(element) { + for (let recorder of element.recorders) { + element.removeEventListener(recorder.eventType, recorder.listener); + } + delete element.recorders; +} + +/** + * Sets up the elements in the browser window that will be used by all the other + * regression tests. Since the panel and view elements can live anywhere in the + * document, they are simply added to the same toolbar as the panel anchors. + * + * <toolbar id="nav-bar"> + * <toolbarbutton/> -> gPanelAnchors[panelIndex] + * <panel> -> gPanels[panelIndex] + * <panelmultiview/> -> gPanelMultiViews[panelIndex] + * </panel> + * <panelview> -> gPanelViews[viewIndex] + * <label/> -> gPanelViewLabels[viewIndex] + * </panelview> + * </toolbar> + */ +add_task(async function test_setup() { + let navBar = document.getElementById("nav-bar"); + + for (let i = 0; i < PANELS_COUNT; i++) { + gPanelAnchors[i] = document.createXULElement("toolbarbutton"); + gPanelAnchors[i].classList.add( + "toolbarbutton-1", + "chromeclass-toolbar-additional" + ); + navBar.appendChild(gPanelAnchors[i]); + + gPanels[i] = document.createXULElement("panel"); + gPanels[i].id = "panel-" + i; + gPanels[i].setAttribute("type", "arrow"); + gPanels[i].setAttribute("photon", true); + navBar.appendChild(gPanels[i]); + + gPanelMultiViews[i] = document.createXULElement("panelmultiview"); + gPanelMultiViews[i].id = "panelmultiview-" + i; + gPanels[i].appendChild(gPanelMultiViews[i]); + } + + for (let i = 0; i < PANELVIEWS_COUNT; i++) { + gPanelViews[i] = document.createXULElement("panelview"); + gPanelViews[i].id = "panelview-" + i; + navBar.appendChild(gPanelViews[i]); + + gPanelViewLabels[i] = document.createXULElement("label"); + gPanelViewLabels[i].setAttribute("value", "PanelView " + i); + gPanelViews[i].appendChild(gPanelViewLabels[i]); + } + + registerCleanupFunction(() => { + [...gPanelAnchors, ...gPanels, ...gPanelViews].forEach(e => e.remove()); + }); +}); + +/** + * Shows and hides all views in a panel with this static structure: + * + * - Panel 0 + * - View 0 + * - View 1 + * - View 3 + * - View 2 + */ +add_task(async function test_simple() { + // Show main view 0. + await openPopup(0, 0); + + // Show and hide subview 1. + await showSubView(0, 1); + assertLabelVisible(0, false); + await goBack(0, 0); + assertLabelVisible(1, false); + + // Show subview 3. + await showSubView(0, 3); + assertLabelVisible(0, false); + + // Show and hide subview 2. + await showSubView(0, 2); + assertLabelVisible(3, false); + await goBack(0, 3); + assertLabelVisible(2, false); + + // Hide subview 3. + await goBack(0, 0); + assertLabelVisible(3, false); + + // Hide main view 0. + await hidePopup(0); + assertLabelVisible(0, false); +}); + +/** + * Tests the event sequence in a panel with this static structure: + * + * - Panel 0 + * - View 0 + * - View 1 + * - View 3 + * - View 2 + */ +add_task(async function test_simple_event_sequence() { + let recordArray = []; + recordEvents(gPanels[0], EVENT_TYPES, recordArray); + + await openPopup(0, 0); + await showSubView(0, 1); + await goBack(0, 0); + await showSubView(0, 3); + await showSubView(0, 2); + await goBack(0, 3); + await goBack(0, 0); + await hidePopup(0); + + stopRecordingEvents(gPanels[0]); + + Assert.deepEqual(recordArray, [ + "panelview-0: ViewShowing", + "panelview-0: ViewShown", + "panel-0: popupshown", + "panelview-1: ViewShowing", + "panelview-1: ViewShown", + "panelview-1: ViewHiding", + "panelview-0: ViewShown", + "panelview-3: ViewShowing", + "panelview-3: ViewShown", + "panelview-2: ViewShowing", + "panelview-2: ViewShown", + "panelview-2: ViewHiding", + "panelview-3: ViewShown", + "panelview-3: ViewHiding", + "panelview-0: ViewShown", + "panelview-0: ViewHiding", + "panelmultiview-0: PanelMultiViewHidden", + "panel-0: popuphidden", + ]); +}); + +/** + * Tests that further navigation is suppressed until the new view is shown. + */ +add_task(async function test_navigation_suppression() { + await openPopup(0, 0); + + // Test re-entering the "showSubView" method. + let promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[1], "ViewShown"); + gPanelMultiViews[0].showSubView(gPanelViews[1]); + Assert.ok( + !PanelView.forNode(gPanelViews[0]).active, + "The previous view should become inactive synchronously." + ); + + // The following call will have no effect. + gPanelMultiViews[0].showSubView(gPanelViews[2]); + await promiseShown; + + // Test re-entering the "goBack" method. + promiseShown = BrowserTestUtils.waitForEvent(gPanelViews[0], "ViewShown"); + gPanelMultiViews[0].goBack(); + Assert.ok( + !PanelView.forNode(gPanelViews[1]).active, + "The previous view should become inactive synchronously." + ); + + // The following call will have no effect. + gPanelMultiViews[0].goBack(); + await promiseShown; + + // Main view 0 should be displayed. + assertLabelVisible(0, true); + + await hidePopup(0); +}); + +/** + * Tests reusing views that are already open in another panel. In this test, the + * structure of the first panel will change dynamically: + * + * - Panel 0 + * - View 0 + * - View 1 + * - Panel 1 + * - View 1 + * - View 2 + * - Panel 0 + * - View 1 + * - View 0 + */ +add_task(async function test_switch_event_sequence() { + let recordArray = []; + recordEvents(gPanels[0], EVENT_TYPES, recordArray); + recordEvents(gPanels[1], EVENT_TYPES, recordArray); + + // Show panel 0. + await openPopup(0, 0); + await showSubView(0, 1); + + // Show panel 1 with the view that is already open and visible in panel 0. + // This will close panel 0 automatically. + await openPopup(1, 1); + await showSubView(1, 2); + + // Show panel 0 with a view that is already open but invisible in panel 1. + // This will close panel 1 automatically. + await openPopup(0, 1); + await showSubView(0, 0); + + // Hide panel 0. + await hidePopup(0); + + stopRecordingEvents(gPanels[0]); + stopRecordingEvents(gPanels[1]); + + Assert.deepEqual(recordArray, [ + "panelview-0: ViewShowing", + "panelview-0: ViewShown", + "panel-0: popupshown", + "panelview-1: ViewShowing", + "panelview-1: ViewShown", + "panelview-1: ViewHiding", + "panelview-0: ViewHiding", + "panelmultiview-0: PanelMultiViewHidden", + "panel-0: popuphidden", + "panelview-1: ViewShowing", + "panel-1: popupshown", + "panelview-1: ViewShown", + "panelview-2: ViewShowing", + "panelview-2: ViewShown", + "panel-1: popuphidden", + "panelview-2: ViewHiding", + "panelview-1: ViewHiding", + "panelmultiview-1: PanelMultiViewHidden", + "panelview-1: ViewShowing", + "panelview-1: ViewShown", + "panel-0: popupshown", + "panelview-0: ViewShowing", + "panelview-0: ViewShown", + "panelview-0: ViewHiding", + "panelview-1: ViewHiding", + "panelmultiview-0: PanelMultiViewHidden", + "panel-0: popuphidden", + ]); +}); + +/** + * Tests the event sequence when opening the main view is canceled. + */ +add_task(async function test_cancel_mainview_event_sequence() { + let recordArray = []; + recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => { + if (event.type == "ViewShowing") { + event.preventDefault(); + } + }); + + gPanelMultiViews[0].setAttribute("mainViewId", gPanelViews[0].id); + + let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[0], "popuphidden"); + PanelMultiView.openPopup( + gPanels[0], + gPanelAnchors[0], + "bottomright topright" + ); + await promiseHidden; + + stopRecordingEvents(gPanels[0]); + + Assert.deepEqual(recordArray, [ + "panelview-0: ViewShowing", + "panelview-0: ViewHiding", + "panelmultiview-0: PanelMultiViewHidden", + "panelmultiview-0: popuphidden", + ]); +}); + +/** + * Tests the event sequence when opening a subview is canceled. + */ +add_task(async function test_cancel_subview_event_sequence() { + let recordArray = []; + recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => { + if ( + event.type == "ViewShowing" && + event.originalTarget.id == gPanelViews[1].id + ) { + event.preventDefault(); + } + }); + + await openPopup(0, 0); + + let promiseHiding = BrowserTestUtils.waitForEvent( + gPanelViews[1], + "ViewHiding" + ); + gPanelMultiViews[0].showSubView(gPanelViews[1]); + await promiseHiding; + + // Only the subview should have received the hidden event at this point. + Assert.deepEqual(recordArray, [ + "panelview-0: ViewShowing", + "panelview-0: ViewShown", + "panel-0: popupshown", + "panelview-1: ViewShowing", + "panelview-1: ViewHiding", + ]); + recordArray.length = 0; + + await hidePopup(0); + + stopRecordingEvents(gPanels[0]); + + Assert.deepEqual(recordArray, [ + "panelview-0: ViewHiding", + "panelmultiview-0: PanelMultiViewHidden", + "panel-0: popuphidden", + ]); +}); + +/** + * Tests the event sequence when closing the panel while opening the main view. + */ +add_task(async function test_close_while_showing_mainview_event_sequence() { + let recordArray = []; + recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => { + if (event.type == "ViewShowing") { + PanelMultiView.hidePopup(gPanels[0]); + } + }); + + gPanelMultiViews[0].setAttribute("mainViewId", gPanelViews[0].id); + + let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[0], "popuphidden"); + let promiseHiding = BrowserTestUtils.waitForEvent( + gPanelViews[0], + "ViewHiding" + ); + PanelMultiView.openPopup( + gPanels[0], + gPanelAnchors[0], + "bottomright topright" + ); + await promiseHiding; + await promiseHidden; + + stopRecordingEvents(gPanels[0]); + + Assert.deepEqual(recordArray, [ + "panelview-0: ViewShowing", + "panelview-0: ViewShowing > panelview-0: ViewHiding", + "panelview-0: ViewShowing > panelmultiview-0: PanelMultiViewHidden", + "panelview-0: ViewShowing > panelmultiview-0: popuphidden", + ]); +}); + +/** + * Tests the event sequence when closing the panel while opening a subview. + */ +add_task(async function test_close_while_showing_subview_event_sequence() { + let recordArray = []; + recordEvents(gPanels[0], EVENT_TYPES, recordArray, event => { + if ( + event.type == "ViewShowing" && + event.originalTarget.id == gPanelViews[1].id + ) { + PanelMultiView.hidePopup(gPanels[0]); + } + }); + + await openPopup(0, 0); + + let promiseHidden = BrowserTestUtils.waitForEvent(gPanels[0], "popuphidden"); + gPanelMultiViews[0].showSubView(gPanelViews[1]); + await promiseHidden; + + stopRecordingEvents(gPanels[0]); + + Assert.deepEqual(recordArray, [ + "panelview-0: ViewShowing", + "panelview-0: ViewShown", + "panel-0: popupshown", + "panelview-1: ViewShowing", + "panelview-1: ViewShowing > panelview-1: ViewHiding", + "panelview-1: ViewShowing > panelview-0: ViewHiding", + "panelview-1: ViewShowing > panelmultiview-0: PanelMultiViewHidden", + "panelview-1: ViewShowing > panel-0: popuphidden", + ]); +}); diff --git a/browser/components/customizableui/test/browser_PanelMultiView_focus.js b/browser/components/customizableui/test/browser_PanelMultiView_focus.js new file mode 100644 index 0000000000..869b5cc808 --- /dev/null +++ b/browser/components/customizableui/test/browser_PanelMultiView_focus.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the focus behavior when opening PanelViews. + */ + +let gAnchor; +let gPanel; +let gPanelMultiView; +let gMainView; +let gMainButton; +let gMainSubButton; +let gSubView; +let gSubButton; + +function createWith(doc, tag, props) { + let el = doc.createXULElement(tag); + for (let prop in props) { + el.setAttribute(prop, props[prop]); + } + return el; +} + +add_setup(async function () { + let navBar = document.getElementById("nav-bar"); + gAnchor = document.createXULElement("toolbarbutton"); + // Must be focusable in order for key presses to work. + gAnchor.style["-moz-user-focus"] = "normal"; + navBar.appendChild(gAnchor); + let onPress = event => + PanelMultiView.openPopup(gPanel, gAnchor, { + triggerEvent: event, + }); + gAnchor.addEventListener("keypress", onPress); + gAnchor.addEventListener("click", onPress); + gPanel = document.createXULElement("panel"); + navBar.appendChild(gPanel); + gPanelMultiView = document.createXULElement("panelmultiview"); + gPanelMultiView.setAttribute("mainViewId", "testMainView"); + gPanel.appendChild(gPanelMultiView); + + gMainView = document.createXULElement("panelview"); + gMainView.id = "testMainView"; + gPanelMultiView.appendChild(gMainView); + gMainButton = createWith(document, "button", { label: "gMainButton" }); + gMainView.appendChild(gMainButton); + gMainSubButton = createWith(document, "button", { label: "gMainSubButton" }); + gMainView.appendChild(gMainSubButton); + gMainSubButton.addEventListener("command", () => + gPanelMultiView.showSubView("testSubView", gMainSubButton) + ); + + gSubView = document.createXULElement("panelview"); + gSubView.id = "testSubView"; + gPanelMultiView.appendChild(gSubView); + gSubButton = createWith(document, "button", { label: "gSubButton" }); + gSubView.appendChild(gSubButton); + + registerCleanupFunction(() => { + gAnchor.remove(); + gPanel.remove(); + }); +}); + +// Activate the main view by pressing a key. Focus should be moved inside. +add_task(async function testMainViewByKeypress() { + gAnchor.focus(); + await gCUITestUtils.openPanelMultiView(gPanel, gMainView, () => + EventUtils.synthesizeKey(" ") + ); + Assert.equal( + document.activeElement, + gMainButton, + "Focus on button in main view" + ); + await gCUITestUtils.hidePanelMultiView(gPanel, () => + PanelMultiView.hidePopup(gPanel) + ); +}); + +// Activate the main view by clicking the mouse. Focus should not be moved +// inside. +add_task(async function testMainViewByClick() { + await gCUITestUtils.openPanelMultiView(gPanel, gMainView, () => + gAnchor.click() + ); + Assert.notEqual( + document.activeElement, + gMainButton, + "Focus not on button in main view" + ); + await gCUITestUtils.hidePanelMultiView(gPanel, () => + PanelMultiView.hidePopup(gPanel) + ); +}); + +// Activate the subview by pressing a key. Focus should be moved to the first +// button after the Back button. +add_task(async function testSubViewByKeypress() { + await gCUITestUtils.openPanelMultiView(gPanel, gMainView, () => + gAnchor.click() + ); + while (document.activeElement != gMainSubButton) { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + } + let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown"); + EventUtils.synthesizeKey(" "); + await shown; + Assert.equal( + document.activeElement, + gSubButton, + "Focus on first button after Back button in subview" + ); + await gCUITestUtils.hidePanelMultiView(gPanel, () => + PanelMultiView.hidePopup(gPanel) + ); +}); + +// Activate the subview by clicking the mouse. Focus should not be moved +// inside. +add_task(async function testSubViewByClick() { + await gCUITestUtils.openPanelMultiView(gPanel, gMainView, () => + gAnchor.click() + ); + let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown"); + gMainSubButton.click(); + await shown; + let backButton = gSubView.querySelector(".subviewbutton-back"); + Assert.notEqual( + document.activeElement, + backButton, + "Focus not on Back button in subview" + ); + Assert.notEqual( + document.activeElement, + gSubButton, + "Focus not on button after Back button in subview" + ); + await gCUITestUtils.hidePanelMultiView(gPanel, () => + PanelMultiView.hidePopup(gPanel) + ); +}); + +// Test that focus is restored when going back to a previous view. +add_task(async function testBackRestoresFocus() { + await gCUITestUtils.openPanelMultiView(gPanel, gMainView, () => + gAnchor.click() + ); + while (document.activeElement != gMainSubButton) { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + } + let shown = BrowserTestUtils.waitForEvent(gSubView, "ViewShown"); + EventUtils.synthesizeKey(" "); + await shown; + shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await shown; + Assert.equal( + document.activeElement, + gMainSubButton, + "Focus on sub button in main view" + ); + await gCUITestUtils.hidePanelMultiView(gPanel, () => + PanelMultiView.hidePopup(gPanel) + ); +}); diff --git a/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js new file mode 100644 index 0000000000..baaa38c224 --- /dev/null +++ b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js @@ -0,0 +1,582 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the keyboard behavior of PanelViews. + */ + +const kEmbeddedDocUrl = + 'data:text/html,<textarea id="docTextarea">value</textarea><button id="docButton"></button>'; + +let gAnchor; +let gPanel; +let gPanelMultiView; +let gMainView; +let gMainContext; +let gMainButton1; +let gMainMenulist; +let gMainRadiogroup; +let gMainTextbox; +let gMainButton2; +let gMainButton3; +let gCheckbox; +let gNamespacedLink; +let gLink; +let gMainTabOrder; +let gMainArrowOrder; +let gSubView; +let gSubButton; +let gSubTextarea; +let gBrowserView; +let gBrowserBrowser; +let gIframeView; +let gIframeIframe; +let gToggle; + +async function openPopup() { + let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown"); + PanelMultiView.openPopup(gPanel, gAnchor, "bottomright topright"); + await shown; +} + +async function hidePopup() { + let hidden = BrowserTestUtils.waitForEvent(gPanel, "popuphidden"); + PanelMultiView.hidePopup(gPanel); + await hidden; +} + +async function showSubView(view = gSubView) { + let shown = BrowserTestUtils.waitForEvent(view, "ViewShown"); + // We must show with an anchor so the Back button is generated. + gPanelMultiView.showSubView(view, gMainButton1); + await shown; +} + +async function expectFocusAfterKey(aKey, aFocus) { + let res = aKey.match(/^(Shift\+)?(.+)$/); + let shift = Boolean(res[1]); + let key; + if (res[2].length == 1) { + key = res[2]; // Character. + } else { + key = "KEY_" + res[2]; // Tab, ArrowRight, etc. + } + info("Waiting for focus on " + aFocus.id); + let focused = BrowserTestUtils.waitForEvent(aFocus, "focus"); + EventUtils.synthesizeKey(key, { shiftKey: shift }); + await focused; + ok(true, aFocus.id + " focused after " + aKey + " pressed"); +} + +add_setup(async function () { + // This shouldn't be necessary - but it is, because we use same-process frames. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1565276 covers improving this. + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], + }); + let navBar = document.getElementById("nav-bar"); + gAnchor = document.createXULElement("toolbarbutton"); + navBar.appendChild(gAnchor); + gPanel = document.createXULElement("panel"); + navBar.appendChild(gPanel); + gPanelMultiView = document.createXULElement("panelmultiview"); + gPanelMultiView.setAttribute("mainViewId", "testMainView"); + gPanel.appendChild(gPanelMultiView); + + gMainView = document.createXULElement("panelview"); + gMainView.id = "testMainView"; + gPanelMultiView.appendChild(gMainView); + gMainContext = document.createXULElement("menupopup"); + gMainContext.id = "gMainContext"; + gMainView.appendChild(gMainContext); + gMainContext.appendChild(document.createXULElement("menuitem")); + gMainButton1 = document.createXULElement("button"); + gMainButton1.id = "gMainButton1"; + gMainView.appendChild(gMainButton1); + // We use this for anchoring subviews, so it must have a label. + gMainButton1.setAttribute("label", "gMainButton1"); + gMainButton1.setAttribute("context", "gMainContext"); + gMainMenulist = document.createXULElement("menulist"); + gMainMenulist.id = "gMainMenulist"; + gMainView.appendChild(gMainMenulist); + let menuPopup = document.createXULElement("menupopup"); + gMainMenulist.appendChild(menuPopup); + let item = document.createXULElement("menuitem"); + item.setAttribute("value", "1"); + item.setAttribute("selected", "true"); + menuPopup.appendChild(item); + item = document.createXULElement("menuitem"); + item.setAttribute("value", "2"); + menuPopup.appendChild(item); + gMainRadiogroup = document.createXULElement("radiogroup"); + gMainRadiogroup.id = "gMainRadiogroup"; + gMainView.appendChild(gMainRadiogroup); + let radio = document.createXULElement("radio"); + radio.setAttribute("value", "1"); + radio.setAttribute("selected", "true"); + gMainRadiogroup.appendChild(radio); + radio = document.createXULElement("radio"); + radio.setAttribute("value", "2"); + gMainRadiogroup.appendChild(radio); + gMainTextbox = document.createElement("input"); + gMainTextbox.id = "gMainTextbox"; + gMainView.appendChild(gMainTextbox); + gMainTextbox.setAttribute("value", "value"); + gMainButton2 = document.createXULElement("button"); + gMainButton2.id = "gMainButton2"; + gMainView.appendChild(gMainButton2); + gMainButton3 = document.createXULElement("button"); + gMainButton3.id = "gMainButton3"; + gMainView.appendChild(gMainButton3); + gCheckbox = document.createXULElement("checkbox"); + gCheckbox.id = "gCheckbox"; + gMainView.appendChild(gCheckbox); + + // moz-support-links in XUL documents are created with the + // <html:a> tag and so we need to test this separately from + // <a> tags. + gNamespacedLink = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "html:a" + ); + gNamespacedLink.href = "www.mozilla.org"; + gNamespacedLink.innerText = "gNamespacedLink"; + gNamespacedLink.id = "gNamespacedLink"; + gMainView.appendChild(gNamespacedLink); + gLink = document.createElement("a"); + gLink.href = "www.mozilla.org"; + gLink.innerText = "gLink"; + gLink.id = "gLink"; + gMainView.appendChild(gLink); + await window.ensureCustomElements("moz-toggle"); + gToggle = document.createElement("moz-toggle"); + gMainView.appendChild(gToggle); + + gMainTabOrder = [ + gMainButton1, + gMainMenulist, + gMainRadiogroup, + gMainTextbox, + gMainButton2, + gMainButton3, + gCheckbox, + gNamespacedLink, + gLink, + gToggle, + ]; + gMainArrowOrder = [ + gMainButton1, + gMainButton2, + gMainButton3, + gCheckbox, + gNamespacedLink, + gLink, + gToggle, + ]; + + gSubView = document.createXULElement("panelview"); + gSubView.id = "testSubView"; + gPanelMultiView.appendChild(gSubView); + gSubButton = document.createXULElement("button"); + gSubView.appendChild(gSubButton); + gSubTextarea = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "textarea" + ); + gSubTextarea.id = "gSubTextarea"; + gSubView.appendChild(gSubTextarea); + gSubTextarea.value = "value"; + + gBrowserView = document.createXULElement("panelview"); + gBrowserView.id = "testBrowserView"; + gPanelMultiView.appendChild(gBrowserView); + gBrowserBrowser = document.createXULElement("browser"); + gBrowserBrowser.id = "GBrowserBrowser"; + gBrowserBrowser.setAttribute("type", "content"); + gBrowserBrowser.setAttribute("src", kEmbeddedDocUrl); + gBrowserBrowser.style.minWidth = gBrowserBrowser.style.minHeight = "100px"; + gBrowserView.appendChild(gBrowserBrowser); + + gIframeView = document.createXULElement("panelview"); + gIframeView.id = "testIframeView"; + gPanelMultiView.appendChild(gIframeView); + gIframeIframe = document.createXULElement("iframe"); + gIframeIframe.id = "gIframeIframe"; + gIframeIframe.setAttribute("src", kEmbeddedDocUrl); + gIframeView.appendChild(gIframeIframe); + + registerCleanupFunction(() => { + gAnchor.remove(); + gPanel.remove(); + }); +}); + +// Test that the tab key focuses all expected controls. +add_task(async function testTab() { + await openPopup(); + for (let elem of gMainTabOrder) { + await expectFocusAfterKey("Tab", elem); + } + // Wrap around. + await expectFocusAfterKey("Tab", gMainTabOrder[0]); + await hidePopup(); +}); + +// Test that the shift+tab key focuses all expected controls. +add_task(async function testShiftTab() { + await openPopup(); + for (let i = gMainTabOrder.length - 1; i >= 0; --i) { + await expectFocusAfterKey("Shift+Tab", gMainTabOrder[i]); + } + // Wrap around. + await expectFocusAfterKey( + "Shift+Tab", + gMainTabOrder[gMainTabOrder.length - 1] + ); + await hidePopup(); +}); + +// Test that the down arrow key skips menulists and textboxes. +add_task(async function testDownArrow() { + await openPopup(); + for (let elem of gMainArrowOrder) { + await expectFocusAfterKey("ArrowDown", elem); + } + // Wrap around. + await expectFocusAfterKey("ArrowDown", gMainArrowOrder[0]); + await hidePopup(); +}); + +// Test that the up arrow key skips menulists and textboxes. +add_task(async function testUpArrow() { + await openPopup(); + for (let i = gMainArrowOrder.length - 1; i >= 0; --i) { + await expectFocusAfterKey("ArrowUp", gMainArrowOrder[i]); + } + // Wrap around. + await expectFocusAfterKey( + "ArrowUp", + gMainArrowOrder[gMainArrowOrder.length - 1] + ); + await hidePopup(); +}); + +// Test that the home/end keys move to the first/last controls. +add_task(async function testHomeEnd() { + await openPopup(); + await expectFocusAfterKey("Home", gMainArrowOrder[0]); + await expectFocusAfterKey("End", gMainArrowOrder[gMainArrowOrder.length - 1]); + await hidePopup(); +}); + +// Test that the up/down arrow keys work as expected in menulists. +add_task(async function testArrowsMenulist() { + await openPopup(); + gMainMenulist.focus(); + is(document.activeElement, gMainMenulist, "menulist focused"); + is(gMainMenulist.value, "1", "menulist initial value 1"); + if (AppConstants.platform == "macosx") { + // On Mac, down/up arrows just open the menulist. + let popup = gMainMenulist.menupopup; + for (let key of ["ArrowDown", "ArrowUp"]) { + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeKey("KEY_" + key); + await shown; + ok(gMainMenulist.open, "menulist open after " + key); + let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await hidden; + ok(!gMainMenulist.open, "menulist closed after Escape"); + } + } else { + // On other platforms, down/up arrows change the value without opening the + // menulist. + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement, + gMainMenulist, + "menulist still focused after ArrowDown" + ); + is(gMainMenulist.value, "2", "menulist value 2 after ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement, + gMainMenulist, + "menulist still focused after ArrowUp" + ); + is(gMainMenulist.value, "1", "menulist value 1 after ArrowUp"); + } + await hidePopup(); +}); + +// Test that the tab key closes an open menu list. +add_task(async function testTabOpenMenulist() { + await openPopup(); + gMainMenulist.focus(); + is(document.activeElement, gMainMenulist, "menulist focused"); + let popup = gMainMenulist.menupopup; + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + gMainMenulist.open = true; + await shown; + ok(gMainMenulist.open, "menulist open"); + let menuHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + EventUtils.synthesizeKey("KEY_Tab"); + await menuHidden; + ok(!gMainMenulist.open, "menulist closed after Tab"); + is(gPanel.state, "open", "Panel should be open"); + await hidePopup(); +}); + +if (AppConstants.platform == "macosx") { + // Test that using the mouse to open a menulist still allows keyboard navigation + // inside it. + add_task(async function testNavigateMouseOpenedMenulist() { + await openPopup(); + let popup = gMainMenulist.menupopup; + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + gMainMenulist.open = true; + await shown; + ok(gMainMenulist.open, "menulist open"); + let oldFocus = document.activeElement; + let oldSelectedItem = gMainMenulist.selectedItem; + ok( + oldSelectedItem.hasAttribute("_moz-menuactive"), + "Selected item should show up as active" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await TestUtils.waitForCondition( + () => !oldSelectedItem.hasAttribute("_moz-menuactive") + ); + is(oldFocus, document.activeElement, "Focus should not move on mac"); + ok( + !oldSelectedItem.hasAttribute("_moz-menuactive"), + "Selected item should change" + ); + + let menuHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + EventUtils.synthesizeKey("KEY_Tab"); + await menuHidden; + ok(!gMainMenulist.open, "menulist closed after Tab"); + is(gPanel.state, "open", "Panel should be open"); + await hidePopup(); + }); +} + +// Test that the up/down arrow keys work as expected in radiogroups. +add_task(async function testArrowsRadiogroup() { + await openPopup(); + gMainRadiogroup.focus(); + is(document.activeElement, gMainRadiogroup, "radiogroup focused"); + is(gMainRadiogroup.value, "1", "radiogroup initial value 1"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement, + gMainRadiogroup, + "radiogroup still focused after ArrowDown" + ); + is(gMainRadiogroup.value, "2", "radiogroup value 2 after ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement, + gMainRadiogroup, + "radiogroup still focused after ArrowUp" + ); + is(gMainRadiogroup.value, "1", "radiogroup value 1 after ArrowUp"); + await hidePopup(); +}); + +// Test that pressing space in a textbox inserts a space (instead of trying to +// activate the control). +add_task(async function testSpaceTextbox() { + await openPopup(); + gMainTextbox.focus(); + gMainTextbox.selectionStart = gMainTextbox.selectionEnd = 0; + EventUtils.synthesizeKey(" "); + is(gMainTextbox.value, " value", "Space typed into textbox"); + gMainTextbox.value = "value"; + await hidePopup(); +}); + +// Tests that the left arrow key normally moves back to the previous view. +add_task(async function testLeftArrow() { + await openPopup(); + await showSubView(); + let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await shown; + ok("Moved to previous view after ArrowLeft"); + await hidePopup(); +}); + +// Tests that the left arrow key moves the caret in a textarea in a subview +// (instead of going back to the previous view). +add_task(async function testLeftArrowTextarea() { + await openPopup(); + await showSubView(); + gSubTextarea.focus(); + is(document.activeElement, gSubTextarea, "textarea focused"); + EventUtils.synthesizeKey("KEY_End"); + is(gSubTextarea.selectionStart, 5, "selectionStart 5 after End"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + is(gSubTextarea.selectionStart, 4, "selectionStart 4 after ArrowLeft"); + is(document.activeElement, gSubTextarea, "textarea still focused"); + await hidePopup(); +}); + +// Test navigation to a button which is initially disabled and later enabled. +add_task(async function testDynamicButton() { + gMainButton2.disabled = true; + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + await expectFocusAfterKey("ArrowDown", gMainButton3); + gMainButton2.disabled = false; + await expectFocusAfterKey("ArrowUp", gMainButton2); + await hidePopup(); +}); + +add_task(async function testActivation() { + function checkActivated(elem, activationFn, reason) { + let activated = false; + elem.onclick = function () { + activated = true; + }; + activationFn(); + ok(activated, "Should have activated button after " + reason); + elem.onclick = null; + } + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + checkActivated( + gMainButton1, + () => EventUtils.synthesizeKey("KEY_Enter"), + "pressing enter" + ); + checkActivated( + gMainButton1, + () => EventUtils.synthesizeKey(" "), + "pressing space" + ); + checkActivated( + gMainButton1, + () => EventUtils.synthesizeKey("KEY_Enter", { code: "NumpadEnter" }), + "pressing numpad enter" + ); + await hidePopup(); +}); + +// Test that keyboard activation works for buttons responding to mousedown +// events (instead of command or click). The Library button does this, for +// example. +add_task(async function testActivationMousedown() { + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + let activated = false; + gMainButton1.onmousedown = function () { + activated = true; + }; + EventUtils.synthesizeKey(" "); + ok(activated, "mousedown activated after space"); + gMainButton1.onmousedown = null; + await hidePopup(); +}); + +// Test that tab and the arrow keys aren't overridden in embedded documents. +async function testTabArrowsEmbeddedDoc(aView, aEmbedder) { + await openPopup(); + await showSubView(aView); + let doc = aEmbedder.contentDocument; + if (doc.readyState != "complete" || doc.location.href != kEmbeddedDocUrl) { + info(`Embedded doc readyState ${doc.readyState}, location ${doc.location}`); + info("Waiting for load on embedder"); + // Browsers don't fire load events, and iframes don't fire load events in + // typeChrome windows. We can handle both by using a capturing event + // listener to capture the load event from the child document. + await BrowserTestUtils.waitForEvent(aEmbedder, "load", true); + // The original doc might have been a temporary about:blank, so fetch it + // again. + doc = aEmbedder.contentDocument; + } + is(doc.location.href, kEmbeddedDocUrl, "Embedded doc has correct URl"); + let backButton = aView.querySelector(".subviewbutton-back"); + backButton.id = "docBack"; + await expectFocusAfterKey("Tab", backButton); + // Documents don't have an id property, but expectFocusAfterKey wants one. + doc.id = "doc"; + await expectFocusAfterKey("Tab", doc); + // Make sure tab/arrows aren't overridden within the embedded document. + let textarea = doc.getElementById("docTextarea"); + // Tab should really focus the textarea, but default tab handling seems to + // skip everything inside the embedder element when run in this test. This + // behaves as expected in real panels, though. Force focus to the textarea + // and then test from there. + textarea.focus(); + is(doc.activeElement, textarea, "textarea focused"); + is(textarea.selectionStart, 0, "selectionStart initially 0"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + is(textarea.selectionStart, 1, "selectionStart 1 after ArrowRight"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + is(textarea.selectionStart, 0, "selectionStart 0 after ArrowLeft"); + is(doc.activeElement, textarea, "textarea still focused"); + let docButton = doc.getElementById("docButton"); + await expectFocusAfterKey("Tab", docButton); + await hidePopup(); +} + +// Test that tab and the arrow keys aren't overridden in embedded browsers. +add_task(async function testTabArrowsBrowser() { + await testTabArrowsEmbeddedDoc(gBrowserView, gBrowserBrowser); +}); + +// Test that tab and the arrow keys aren't overridden in embedded iframes. +add_task(async function testTabArrowsIframe() { + await testTabArrowsEmbeddedDoc(gIframeView, gIframeIframe); +}); + +// Test that the arrow keys aren't overridden in context menus. +add_task(async function testArowsContext() { + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + let shown = BrowserTestUtils.waitForEvent(gMainContext, "popupshown"); + // There's no cross-platform way to open a context menu from the keyboard. + gMainContext.openPopup(gMainButton1); + await shown; + let item = gMainContext.children[0]; + ok( + !item.getAttribute("_moz-menuactive"), + "First context menu item initially inactive" + ); + let active = BrowserTestUtils.waitForEvent(item, "DOMMenuItemActive"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await active; + ok( + item.getAttribute("_moz-menuactive"), + "First context menu item active after ArrowDown" + ); + is( + document.activeElement, + gMainButton1, + "gMainButton1 still focused after ArrowDown" + ); + let hidden = BrowserTestUtils.waitForEvent(gMainContext, "popuphidden"); + gMainContext.hidePopup(); + await hidden; + await hidePopup(); +}); + +add_task(async function testMozToggle() { + await openPopup(); + is(gToggle.pressed, false, "The toggle is not pressed initially."); + // Focus the toggle via keyboard navigation. + while (document.activeElement !== gToggle) { + EventUtils.synthesizeKey("KEY_Tab"); + } + EventUtils.synthesizeKey(" "); + await gToggle.updateComplete; + is(gToggle.pressed, true, "Toggle pressed state changes via spacebar."); + EventUtils.synthesizeKey("KEY_Enter"); + await gToggle.updateComplete; + is(gToggle.pressed, false, "Toggle pressed state changes via enter."); + await hidePopup(); +}); diff --git a/browser/components/customizableui/test/browser_addons_area.js b/browser/components/customizableui/test/browser_addons_area.js new file mode 100644 index 0000000000..533d48b238 --- /dev/null +++ b/browser/components/customizableui/test/browser_addons_area.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that widgets provided by extensions can be added to the + * ADDONS area, but all other widgets cannot. + */ +add_task(async function test_only_extension_widgets_in_addons_area() { + registerCleanupFunction(async () => { + await CustomizableUI.reset(); + }); + + Assert.ok( + !CustomizableUI.canWidgetMoveToArea( + "home-button", + CustomizableUI.AREA_ADDONS + ), + "Cannot move a built-in button to the ADDONS area." + ); + + // Now double-check that we cannot accidentally default a non-extension + // widget into the ADDONS area. + const kTestDynamicWidget = "a-test-widget"; + CustomizableUI.createWidget({ + id: kTestDynamicWidget, + label: "Test widget", + defaultArea: CustomizableUI.AREA_ADDONS, + }); + Assert.equal( + CustomizableUI.getPlacementOfWidget(kTestDynamicWidget), + null, + "An attempt to put a non-extension widget into the ADDONS area by default should fail." + ); + CustomizableUI.destroyWidget(kTestDynamicWidget); + + const kWebExtensionButtonID1 = "a-test-extension-button"; + + CustomizableUI.createWidget({ + id: kWebExtensionButtonID1, + label: "Test extension widget", + defaultArea: CustomizableUI.AREA_NAVBAR, + webExtension: true, + }); + + Assert.ok( + CustomizableUI.canWidgetMoveToArea( + kWebExtensionButtonID1, + CustomizableUI.AREA_ADDONS + ), + "Can move extension button to the addons area." + ); + + CustomizableUI.destroyWidget(kWebExtensionButtonID1); + + // Now check that extension buttons can default to the ADDONS area, if need + // be. + + const kWebExtensionButtonID2 = "a-test-extension-button-2"; + + CustomizableUI.createWidget({ + id: kWebExtensionButtonID2, + label: "Test extension widget 2", + defaultArea: CustomizableUI.AREA_ADDONS, + webExtension: true, + }); + + Assert.equal( + CustomizableUI.getPlacementOfWidget(kWebExtensionButtonID2)?.area, + CustomizableUI.AREA_ADDONS, + "An attempt to put an extension widget into the ADDONS area by default should work." + ); + + CustomizableUI.destroyWidget(kWebExtensionButtonID2); +}); diff --git a/browser/components/customizableui/test/browser_allow_dragging_removable_false.js b/browser/components/customizableui/test/browser_allow_dragging_removable_false.js new file mode 100644 index 0000000000..76269f44ae --- /dev/null +++ b/browser/components/customizableui/test/browser_allow_dragging_removable_false.js @@ -0,0 +1,42 @@ +"use strict"; + +/** + * Test dragging a removable=false widget within its own area as well as to the palette. + */ +add_task(async function () { + await startCustomizing(); + let forwardButton = document.getElementById("forward-button"); + is( + forwardButton.getAttribute("removable"), + "false", + "forward-button should not be removable" + ); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + let urlbarContainer = document.getElementById("urlbar-container"); + let placementsAfterDrag = getAreaWidgetIds(CustomizableUI.AREA_NAVBAR); + placementsAfterDrag.splice(placementsAfterDrag.indexOf("forward-button"), 1); + placementsAfterDrag.splice( + placementsAfterDrag.indexOf("urlbar-container"), + 0, + "forward-button" + ); + + // Force layout flush to ensure the drag completes as expected + urlbarContainer.clientWidth; + + simulateItemDrag(forwardButton, urlbarContainer, "start"); + assertAreaPlacements(CustomizableUI.AREA_NAVBAR, placementsAfterDrag); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(forwardButton, palette); + is( + CustomizableUI.getPlacementOfWidget("forward-button").area, + CustomizableUI.AREA_NAVBAR, + "forward-button was not able to move to palette" + ); + + await endCustomizing(); + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); diff --git a/browser/components/customizableui/test/browser_backfwd_enabled_post_customize.js b/browser/components/customizableui/test/browser_backfwd_enabled_post_customize.js new file mode 100644 index 0000000000..e92a363dee --- /dev/null +++ b/browser/components/customizableui/test/browser_backfwd_enabled_post_customize.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +/** + * Back/fwd buttons should be re-enabled after customizing. + */ +add_task(async function test_back_forward_buttons() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH); + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.loadURIString( + tab.linkedBrowser, + "data:text/html,A separate page" + ); + await loaded; + loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.loadURIString( + tab.linkedBrowser, + "data:text/html,Another separate page" + ); + await loaded; + gBrowser.goBack(); + await BrowserTestUtils.waitForCondition(() => gBrowser.canGoForward); + + let backButton = document.getElementById("back-button"); + let forwardButton = document.getElementById("forward-button"); + + await BrowserTestUtils.waitForCondition( + () => + !backButton.hasAttribute("disabled") && + !forwardButton.hasAttribute("disabled") + ); + + ok(!backButton.hasAttribute("disabled"), "Back button shouldn't be disabled"); + ok( + !forwardButton.hasAttribute("disabled"), + "Forward button shouldn't be disabled" + ); + await startCustomizing(); + + is( + backButton.getAttribute("disabled"), + "true", + "Back button should be disabled in customize mode" + ); + is( + forwardButton.getAttribute("disabled"), + "true", + "Forward button should be disabled in customize mode" + ); + + await endCustomizing(); + + await BrowserTestUtils.waitForCondition( + () => + !backButton.hasAttribute("disabled") && + !forwardButton.hasAttribute("disabled") + ); + + ok( + !backButton.hasAttribute("disabled"), + "Back button shouldn't be disabled after customize mode" + ); + ok( + !forwardButton.hasAttribute("disabled"), + "Forward button shouldn't be disabled after customize mode" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/customizableui/test/browser_bookmarks_empty_message.js b/browser/components/customizableui/test/browser_bookmarks_empty_message.js new file mode 100644 index 0000000000..d4bcba1b27 --- /dev/null +++ b/browser/components/customizableui/test/browser_bookmarks_empty_message.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function empty_message_on_non_empty_bookmarks_toolbar() { + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "Default state to begin"); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.toolbars.bookmarks.visibility", "always"]], + }); + + CustomizableUI.removeWidgetFromArea("import-button"); + CustomizableUI.removeWidgetFromArea("personal-bookmarks"); + CustomizableUI.addWidgetToArea( + "bookmarks-menu-button", + CustomizableUI.AREA_BOOKMARKS, + 0 + ); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + let doc = newWin.document; + ok( + BrowserTestUtils.is_visible(doc.getElementById("PersonalToolbar")), + "Personal toolbar should be visible" + ); + ok( + doc.getElementById("personal-toolbar-empty").hidden, + "Empty message should be hidden" + ); + + await BrowserTestUtils.closeWindow(newWin); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_bookmarks_toolbar_collapsed_restore_default.js b/browser/components/customizableui/test/browser_bookmarks_toolbar_collapsed_restore_default.js new file mode 100644 index 0000000000..84ddc37d29 --- /dev/null +++ b/browser/components/customizableui/test/browser_bookmarks_toolbar_collapsed_restore_default.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +// Restoring default should set Bookmarks Toolbar back to "newtab" +add_task(async function () { + let prefName = "browser.toolbars.bookmarks.visibility"; + let toolbar = document.querySelector("#PersonalToolbar"); + for (let state of ["always", "never"]) { + info(`Testing setting toolbar state to '${state}'`); + + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "Default state to begin"); + + setToolbarVisibility(toolbar, state, true, false); + + is( + Services.prefs.getCharPref(prefName), + state, + "Pref updated to: " + state + ); + ok(!CustomizableUI.inDefaultState, "Not in default state"); + + await resetCustomization(); + + ok(CustomizableUI.inDefaultState, "Back in default state after reset"); + is( + Services.prefs.getCharPref(prefName), + "newtab", + "Pref should get reset to 'newtab'" + ); + } +}); diff --git a/browser/components/customizableui/test/browser_bookmarks_toolbar_shown_newtab.js b/browser/components/customizableui/test/browser_bookmarks_toolbar_shown_newtab.js new file mode 100644 index 0000000000..38f385e38c --- /dev/null +++ b/browser/components/customizableui/test/browser_bookmarks_toolbar_shown_newtab.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +// Entering customize mode should show the toolbar as long as it's not set to "never" +add_task(async function () { + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "Default state to begin"); + + let toolbar = document.querySelector("#PersonalToolbar"); + for (let state of ["always", "never", "newtab"]) { + info(`Testing setting toolbar state to '${state}'`); + + setToolbarVisibility(toolbar, state, true, false); + + await startCustomizing(); + + let expected = state != "never"; + await TestUtils.waitForCondition( + () => !toolbar.collapsed == expected, + `Waiting for toolbar visibility, state=${state}, visible=${!toolbar.collapsed}, expected=${expected}` + ); + is( + !toolbar.collapsed, + expected, + "The toolbar should be visible when state isn't 'never'" + ); + + await endCustomizing(); + } + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js b/browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js new file mode 100644 index 0000000000..67325b7b36 --- /dev/null +++ b/browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(2); + +const kTestBarID = "testBar"; +const kWidgetID = "characterencoding-button"; + +function createTestBar() { + let testBar = document.createXULElement("toolbar"); + testBar.id = kTestBarID; + testBar.setAttribute("customizable", "true"); + CustomizableUI.registerArea(kTestBarID, { + type: CustomizableUI.TYPE_TOOLBAR, + }); + gNavToolbox.appendChild(testBar); + CustomizableUI.registerToolbarNode(testBar); + return testBar; +} + +/** + * Helper function that does the following: + * + * 1) Creates a custom toolbar and registers it + * with CustomizableUI. + * 2) Adds the widget with ID aWidgetID to that new + * toolbar. + * 3) Enters customize mode and makes sure that the + * widget is still in the right toolbar. + * 4) Exits customize mode, then removes and deregisters + * the custom toolbar. + * 5) Checks that the widget has no placement. + * 6) Re-adds and re-registers a custom toolbar with the same + * ID and options as the first one. + * 7) Enters customize mode and checks that the widget is + * properly back in the toolbar. + * 8) Exits customize mode, removes and de-registers the + * toolbar, and resets the toolbars to default. + */ +function checkRestoredPresence(aWidgetID) { + return (async function () { + let testBar = createTestBar(); + CustomizableUI.addWidgetToArea(aWidgetID, kTestBarID); + let placement = CustomizableUI.getPlacementOfWidget(aWidgetID); + is( + placement.area, + kTestBarID, + "Expected " + aWidgetID + " to be in the test toolbar" + ); + + CustomizableUI.unregisterArea(testBar.id); + testBar.remove(); + + placement = CustomizableUI.getPlacementOfWidget(aWidgetID); + is(placement, null, "Expected " + aWidgetID + " to be in the palette"); + + testBar = createTestBar(); + + await startCustomizing(); + placement = CustomizableUI.getPlacementOfWidget(aWidgetID); + is( + placement.area, + kTestBarID, + "Expected " + aWidgetID + " to be in the test toolbar" + ); + await endCustomizing(); + + CustomizableUI.unregisterArea(testBar.id); + testBar.remove(); + + await resetCustomization(); + })(); +} + +add_task(async function () { + await checkRestoredPresence("downloads-button"); + await checkRestoredPresence("characterencoding-button"); +}); diff --git a/browser/components/customizableui/test/browser_check_tooltips_in_navbar.js b/browser/components/customizableui/test/browser_check_tooltips_in_navbar.js new file mode 100644 index 0000000000..f36b55032d --- /dev/null +++ b/browser/components/customizableui/test/browser_check_tooltips_in_navbar.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function check_tooltips_in_navbar() { + await startCustomizing(); + let homeButtonWrapper = document.getElementById("wrapper-home-button"); + let homeButton = document.getElementById("home-button"); + is( + homeButtonWrapper.getAttribute("tooltiptext"), + homeButton.getAttribute("label"), + "the wrapper's tooltip should match the button's label" + ); + ok( + homeButtonWrapper.getAttribute("tooltiptext"), + "the button should have tooltip text" + ); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_create_button_widget.js b/browser/components/customizableui/test/browser_create_button_widget.js new file mode 100644 index 0000000000..ac234167cf --- /dev/null +++ b/browser/components/customizableui/test/browser_create_button_widget.js @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kButton = "test_dynamically_created_button"; +var initialLocation = gBrowser.currentURI.spec; + +add_task(async function () { + info("Check dynamically created button functionality"); + + // Let's create a simple button that will open about:addons. + let widgetSpec = { + id: kButton, + type: "button", + onClick() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:addons"); + }, + }; + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.addWidgetToArea(kButton, CustomizableUI.AREA_NAVBAR); + ok( + !CustomizableUI.isWebExtensionWidget(kButton), + "This button should not be considered an extension widget." + ); + + // check the button's functionality in navigation bar + let button = document.getElementById(kButton); + let navBar = document.getElementById("nav-bar"); + ok(button, "Dynamically created button exists"); + ok(navBar.contains(button), "Dynamically created button is in the navbar"); + await checkButtonFunctionality(button); + + resetTabs(); + + // move the add-on button in the Panel Menu + CustomizableUI.addWidgetToArea( + kButton, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + ok( + !navBar.contains(button), + "Dynamically created button was removed from the browser bar" + ); + + await waitForOverflowButtonShown(); + + // check the button's functionality in the Overflow Panel. + await document.getElementById("nav-bar").overflowable.show(); + var panelMenu = document.getElementById("widget-overflow-mainView"); + let buttonInPanel = panelMenu.getElementsByAttribute("id", kButton); + ok( + panelMenu.contains(button), + "Dynamically created button was added to the Panel Menu" + ); + await checkButtonFunctionality(buttonInPanel[0]); +}); + +add_task(async function asyncCleanup() { + resetTabs(); + + // reset the UI to the default state + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "The UI is in default state again."); + + // destroy the widget + CustomizableUI.destroyWidget(kButton); +}); + +function resetTabs() { + // close all opened tabs + while (gBrowser.tabs.length > 1) { + gBrowser.removeTab(gBrowser.selectedTab); + } + + // restore the initial tab + BrowserTestUtils.addTab(gBrowser, initialLocation); + gBrowser.removeTab(gBrowser.selectedTab); +} + +async function checkButtonFunctionality(aButton) { + aButton.click(); + await TestUtils.waitForCondition( + () => gBrowser.currentURI && gBrowser.currentURI.spec == "about:addons" + ); +} diff --git a/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js b/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js new file mode 100644 index 0000000000..9377c28950 --- /dev/null +++ b/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_appMenu_mainView() { + // On macOS, ctrl-click shouldn't open the panel because this normally opens + // the context menu. This happens via the `contextmenu` event which is created + // by widget code, so our simulated clicks do not do so, so we can't test + // anything on macOS: + if (AppConstants.platform == "macosx") { + ok(true, "The test is ignored on Mac"); + return; + } + + let mainViewID = "appMenu-protonMainView"; + const mainView = document.getElementById(mainViewID); + + let shownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); + // Should still open the panel when Ctrl key is pressed. + EventUtils.synthesizeMouseAtCenter(PanelUI.menuButton, { ctrlKey: true }); + await shownPromise; + ok(true, "Main menu shown after button pressed"); + + // Close the main panel. + let hiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden"); + mainView.closest("panel").hidePopup(); + await hiddenPromise; +}); + +add_task(async function test_appMenu_libraryView() { + // On macOS, ctrl-click shouldn't open the panel because this normally opens + // the context menu. This happens via the `contextmenu` event which is created + // by widget code, so our simulated clicks do not do so, so we can't test + // anything on macOS: + if (AppConstants.platform == "macosx") { + ok(true, "The test is ignored on Mac"); + return; + } + + CustomizableUI.addWidgetToArea("library-button", "nav-bar"); + const button = document.getElementById("library-button"); + await waitForElementShown(button); + + // Should still open the panel when Ctrl key is pressed. + EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true }); + const libraryView = document.getElementById("appMenu-libraryView"); + let shownPromise = BrowserTestUtils.waitForEvent(libraryView, "ViewShown"); + await shownPromise; + ok(true, "Library menu shown after button pressed"); + + // Close the Library panel. + let hiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden"); + libraryView.closest("panel").hidePopup(); + await hiddenPromise; +}); diff --git a/browser/components/customizableui/test/browser_currentset_post_reset.js b/browser/components/customizableui/test/browser_currentset_post_reset.js new file mode 100644 index 0000000000..31bcd150b1 --- /dev/null +++ b/browser/components/customizableui/test/browser_currentset_post_reset.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function checkSpacers() { + let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar"); + let currentSetWidgets = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + )._getCurrentWidgetsInContainer(document.getElementById("nav-bar")); + navbarWidgets = navbarWidgets.filter(w => CustomizableUI.isSpecialWidget(w)); + currentSetWidgets = currentSetWidgets.filter(w => + CustomizableUI.isSpecialWidget(w) + ); + Assert.deepEqual( + navbarWidgets, + currentSetWidgets, + "Should have the same 'special' widgets in currentset and placements" + ); +} + +/** + * Check that after a reset, CUI's internal bookkeeping correctly deals with flexible spacers. + */ +add_task(async function () { + await startCustomizing(); + checkSpacers(); + + CustomizableUI.addWidgetToArea( + "spring", + "nav-bar", + 4 /* Insert before the last extant spacer */ + ); + await gCustomizeMode.reset(); + checkSpacers(); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_customization_context_menus.js b/browser/components/customizableui/test/browser_customization_context_menus.js new file mode 100644 index 0000000000..0cdfb74839 --- /dev/null +++ b/browser/components/customizableui/test/browser_customization_context_menus.js @@ -0,0 +1,633 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(2); + +const isOSX = Services.appinfo.OS === "Darwin"; + +const overflowButton = document.getElementById("nav-bar-overflow-button"); +const overflowPanel = document.getElementById("widget-overflow"); + +// Right-click on the stop/reload button should +// show a context menu with options to move it. +add_task(async function home_button_context() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let stopReloadButton = document.getElementById("stop-reload-button"); + EventUtils.synthesizeMouse(stopReloadButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; +}); + +// Right-click on an empty bit of tabstrip should +// show a context menu without options to move it, +// but with tab-specific options instead. +add_task(async function tabstrip_context() { + // ensure there are tabs to reload/bookmark: + let extraTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let tabstrip = document.getElementById("tabbrowser-tabs"); + let rect = tabstrip.getBoundingClientRect(); + EventUtils.synthesizeMouse(tabstrip, rect.width - 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let closedTabsAvailable = + SessionStore.getClosedTabCountForWindow(window) == 0; + info("Closed tabs: " + closedTabsAvailable); + let expectedEntries = [ + ["#toolbar-context-openANewTab", true], + ["---"], + ["#toolbar-context-reloadSelectedTab", true], + ["#toolbar-context-bookmarkSelectedTab", true], + ["#toolbar-context-selectAllTabs", true], + ["#toolbar-context-undoCloseTab", !closedTabsAvailable], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; + BrowserTestUtils.removeTab(extraTab); +}); + +// Right-click on the title bar spacer before the tabstrip should show a +// context menu without options to move it and no tab-specific options. +add_task(async function titlebar_spacer_context() { + if (!TabsInTitlebar.enabled) { + info("Skipping test that requires tabs in the title bar."); + return; + } + + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let spacer = document.querySelector( + "#TabsToolbar .titlebar-spacer[type='pre-tabs']" + ); + EventUtils.synthesizeMouseAtCenter(spacer, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", false], + [".customize-context-removeFromToolbar", false], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; +}); + +// Right-click on an empty bit of extra toolbar should +// show a context menu with moving options disabled, +// and a toggle option for the extra toolbar +add_task(async function empty_toolbar_context() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let toolbar = createToolbarWithPlacements("880164_empty_toolbar", []); + toolbar.setAttribute("context", "toolbar-context-menu"); + toolbar.setAttribute("toolbarname", "Fancy Toolbar for Context Menu"); + EventUtils.synthesizeMouseAtCenter(toolbar, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", false], + [".customize-context-removeFromToolbar", false], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["#toggle_880164_empty_toolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; + removeCustomToolbars(); +}); + +// Right-click on the urlbar-container should +// show a context menu with disabled options to move it. +add_task(async function urlbar_context() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let urlBarContainer = document.getElementById("urlbar-container"); + // Need to make sure not to click within an edit field. + EventUtils.synthesizeMouse(urlBarContainer, 100, 1, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", false], + [".customize-context-removeFromToolbar", false], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; +}); + +// Right-click on the searchbar and moving it to the menu +// and back should move the search-container instead. +add_task(async function searchbar_context_move_to_panel_and_back() { + // This is specifically testing the addToPanel function for the search bar, so + // we have to move it to its correct position in the navigation toolbar first. + // The preference will be restored when the customizations are reset later. + Services.prefs.setBoolPref("browser.search.widget.inNavBar", true); + + let searchbar = document.getElementById("searchbar"); + // This fails if the screen resolution is small and the search bar overflows + // from the nav bar. + await gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is( + placement.area, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, + "Should be in panel" + ); + + await waitForOverflowButtonShown(); + + let shownPanelPromise = popupShown(overflowPanel); + overflowButton.click(); + await shownPanelPromise; + let hiddenPanelPromise = popupHidden(overflowPanel); + overflowPanel.hidePopup(); + await hiddenPanelPromise; + + gCustomizeMode.addToToolbar(searchbar); + placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_NAVBAR, "Should be in navbar"); + await gCustomizeMode.removeFromArea(searchbar); + placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement, null, "Should be in palette"); + CustomizableUI.reset(); + placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement, null, "Should be in palette"); +}); + +// Right-click on an item within the panel should +// show a context menu with options to move it. +add_task(async function context_within_panel() { + CustomizableUI.addWidgetToArea( + "new-window-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + let shownPanelPromise = popupShown(overflowPanel); + overflowButton.click(); + await shownPanelPromise; + + let contextMenu = document.getElementById( + "customizationPanelItemContextMenu" + ); + let shownContextPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("new-window-button"); + ok(newWindowButton, "new-window-button was found"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownContextPromise; + + is(overflowPanel.state, "open", "The overflow panel should still be open."); + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", true], + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; + + let hiddenPromise = popupHidden(overflowPanel); + overflowPanel.hidePopup(); + await hiddenPromise; + + CustomizableUI.removeWidgetFromArea("new-window-button"); +}); + +// Right-click on the stop/reload button while in customization mode +// should show a context menu with options to move it. +add_task(async function context_home_button_in_customize_mode() { + await startCustomizing(); + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let stopReloadButton = document.getElementById("wrapper-stop-reload-button"); + EventUtils.synthesizeMouse(stopReloadButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", false] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; +}); + +// Right-click on an item in the palette should +// show a context menu with options to move it. +add_task(async function context_click_in_palette() { + let contextMenu = document.getElementById( + "customizationPaletteItemContextMenu" + ); + let shownPromise = popupShown(contextMenu); + let openFileButton = document.getElementById("wrapper-open-file-button"); + EventUtils.synthesizeMouse(openFileButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-addToToolbar", true], + [".customize-context-addToPanel", true], + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; +}); + +// Right-click on an item in the panel while in customization mode +// should show a context menu with options to move it. +add_task(async function context_click_in_customize_mode() { + CustomizableUI.addWidgetToArea( + "new-window-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + let contextMenu = document.getElementById( + "customizationPanelItemContextMenu" + ); + let shownPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("wrapper-new-window-button"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", false], + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; + CustomizableUI.removeWidgetFromArea("new-window-button"); + await endCustomizing(); +}); + +// Test the toolbarbutton panel context menu in customization mode +// without opening the panel before customization mode +add_task(async function context_click_customize_mode_panel_not_opened() { + CustomizableUI.addWidgetToArea( + "new-window-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + this.otherWin = await openAndLoadWindow(null, true); + + await new Promise(resolve => waitForFocus(resolve, this.otherWin)); + + await startCustomizing(this.otherWin); + + let contextMenu = this.otherWin.document.getElementById( + "customizationPanelItemContextMenu" + ); + let shownPromise = popupShown(contextMenu); + let newWindowButton = this.otherWin.document.getElementById( + "wrapper-new-window-button" + ); + EventUtils.synthesizeMouse( + newWindowButton, + 2, + 2, + { type: "contextmenu", button: 2 }, + this.otherWin + ); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", false], + ]; + checkContextMenu(contextMenu, expectedEntries, this.otherWin); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; + await endCustomizing(this.otherWin); + CustomizableUI.removeWidgetFromArea("new-window-button"); + await promiseWindowClosed(this.otherWin); + this.otherWin = null; + + await new Promise(resolve => waitForFocus(resolve, window)); +}); + +// Bug 945191 - Combined buttons show wrong context menu options +// when they are in the toolbar. +add_task(async function context_combined_buttons_toolbar() { + CustomizableUI.addWidgetToArea( + "zoom-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + await startCustomizing(); + let contextMenu = document.getElementById( + "customizationPanelItemContextMenu" + ); + let shownPromise = popupShown(contextMenu); + let zoomControls = document.getElementById("wrapper-zoom-controls"); + EventUtils.synthesizeMouse(zoomControls, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + // Execute the command to move the item from the panel to the toolbar. + let moveToToolbar = contextMenu.querySelector( + ".customize-context-moveToToolbar" + ); + moveToToolbar.doCommand(); + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; + await endCustomizing(); + + zoomControls = document.getElementById("zoom-controls"); + is( + zoomControls.parentNode.id, + "nav-bar-customization-target", + "Zoom-controls should be on the nav-bar" + ); + + contextMenu = document.getElementById("toolbar-context-menu"); + shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouse(zoomControls, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"], + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenPromise; + await resetCustomization(); +}); + +// Bug 947586 - After customization, panel items show wrong context menu options +add_task(async function context_after_customization_panel() { + info("Check panel context menu is correct after customization"); + await startCustomizing(); + await endCustomizing(); + + CustomizableUI.addWidgetToArea( + "new-window-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + let shownPanelPromise = popupShown(overflowPanel); + overflowButton.click(); + await shownPanelPromise; + + let contextMenu = document.getElementById( + "customizationPanelItemContextMenu" + ); + let shownContextPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("new-window-button"); + ok(newWindowButton, "new-window-button was found"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownContextPromise; + + is(overflowPanel.state, "open", "The panel should still be open."); + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", true], + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; + + let hiddenPromise = popupHidden(overflowPanel); + overflowPanel.hidePopup(); + await hiddenPromise; + CustomizableUI.removeWidgetFromArea("new-window-button"); +}); + +// Bug 982027 - moving icon around removes custom context menu. +add_task(async function custom_context_menus() { + let widgetId = "custom-context-menu-toolbarbutton"; + let expectedContext = "myfancycontext"; + let widget = createDummyXULButton(widgetId, "Test ctxt menu"); + widget.setAttribute("context", expectedContext); + CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR); + is( + widget.getAttribute("context"), + expectedContext, + "Should have context menu when added to the toolbar." + ); + + await startCustomizing(); + is( + widget.getAttribute("context"), + "", + "Should not have own context menu in the toolbar now that we're customizing." + ); + is( + widget.getAttribute("wrapped-context"), + expectedContext, + "Should keep own context menu wrapped when in toolbar." + ); + + let panel = document.getElementById("widget-overflow-fixed-list"); + simulateItemDrag(widget, panel); + is( + widget.getAttribute("context"), + "", + "Should not have own context menu when in the panel." + ); + is( + widget.getAttribute("wrapped-context"), + expectedContext, + "Should keep own context menu wrapped now that we're in the panel." + ); + + simulateItemDrag( + widget, + CustomizableUI.getCustomizationTarget(document.getElementById("nav-bar")) + ); + is( + widget.getAttribute("context"), + "", + "Should not have own context menu when back in toolbar because we're still customizing." + ); + is( + widget.getAttribute("wrapped-context"), + expectedContext, + "Should keep own context menu wrapped now that we're back in the toolbar." + ); + + await endCustomizing(); + is( + widget.getAttribute("context"), + expectedContext, + "Should have context menu again now that we're out of customize mode." + ); + CustomizableUI.removeWidgetFromArea(widgetId); + widget.remove(); + ok( + CustomizableUI.inDefaultState, + "Should be in default state after removing button." + ); +}); + +// Bug 1690575 - 'pin to overflow menu' and 'remove from toolbar' should be hidden +// for flexible spaces +add_task(async function flexible_space_context_menu() { + CustomizableUI.addWidgetToArea("spring", "nav-bar"); + let springs = document.querySelectorAll("#nav-bar toolbarspring"); + let lastSpring = springs[springs.length - 1]; + ok(lastSpring, "we added a spring"); + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouse(lastSpring, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + let expectedEntries = [ + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true], + ]; + + if (!isOSX) { + expectedEntries.unshift(["#toggle_toolbar-menubar", true]); + } + + checkContextMenu(contextMenu, expectedEntries); + contextMenu.hidePopup(); + gCustomizeMode.removeFromArea(lastSpring); + ok(!lastSpring.parentNode, "Spring should have been removed successfully."); +}); diff --git a/browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js b/browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js new file mode 100644 index 0000000000..78b621054c --- /dev/null +++ b/browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js @@ -0,0 +1,71 @@ +"use strict"; + +add_task(async function () { + ok( + !PanelUI.menuButton.hasAttribute("open"), + "Menu button should not be 'pressed' outside customize mode" + ); + ok( + !PanelUI.menuButton.hasAttribute("disabled"), + "Menu button should not be disabled outside of customize mode" + ); + await startCustomizing(); + + ok( + !PanelUI.menuButton.hasAttribute("open"), + "Menu button should still not be 'pressed' when in customize mode" + ); + is( + PanelUI.menuButton.getAttribute("disabled"), + "true", + "Menu button should be disabled in customize mode" + ); + + let contextMenu = document.getElementById( + "customizationPaletteItemContextMenu" + ); + let shownPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("wrapper-new-window-button"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + ok( + !PanelUI.menuButton.hasAttribute("open"), + "Menu button should still not be 'pressed' when in customize mode after opening a context menu" + ); + is( + PanelUI.menuButton.getAttribute("disabled"), + "true", + "Menu button should still be disabled in customize mode" + ); + is( + PanelUI.menuButton.getAttribute("disabled"), + "true", + "Menu button should still be disabled in customize mode after opening context menu" + ); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + await hiddenContextPromise; + ok( + !PanelUI.menuButton.hasAttribute("open"), + "Menu button should still not be 'pressed' when in customize mode after hiding a context menu" + ); + is( + PanelUI.menuButton.getAttribute("disabled"), + "true", + "Menu button should still be disabled in customize mode after hiding context menu" + ); + await endCustomizing(); + + ok( + !PanelUI.menuButton.hasAttribute("open"), + "Menu button should not be 'pressed' after ending customize mode" + ); + ok( + !PanelUI.menuButton.hasAttribute("disabled"), + "Menu button should not be disabled after ending customize mode" + ); +}); diff --git a/browser/components/customizableui/test/browser_customizemode_lwthemes.js b/browser/components/customizableui/test/browser_customizemode_lwthemes.js new file mode 100644 index 0000000000..3b19566ac0 --- /dev/null +++ b/browser/components/customizableui/test/browser_customizemode_lwthemes.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function () { + await startCustomizing(); + // Find the footer buttons to test. + let manageLink = document.querySelector("#customization-lwtheme-link"); + + let waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons"); + manageLink.click(); + let addonsTab = await waitForNewTab; + + is(gBrowser.currentURI.spec, "about:addons", "Manage opened about:addons"); + BrowserTestUtils.removeTab(addonsTab); + + // Wait for customize mode to be re-entered now that the customize tab is + // active. This is needed for endCustomizing() to work properly. + await TestUtils.waitForCondition( + () => document.documentElement.getAttribute("customizing") == "true" + ); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_customizemode_uidensity.js b/browser/components/customizableui/test/browser_customizemode_uidensity.js new file mode 100644 index 0000000000..12280fc49e --- /dev/null +++ b/browser/components/customizableui/test/browser_customizemode_uidensity.js @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PREF_UI_DENSITY = "browser.uidensity"; +const PREF_AUTO_TOUCH_MODE = "browser.touchmode.auto"; + +async function testModeMenuitem(mode, modePref) { + await startCustomizing(); + + let win = document.getElementById("main-window"); + let popupButton = document.getElementById("customization-uidensity-button"); + let popup = document.getElementById("customization-uidensity-menu"); + + // Show the popup. + let popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(popupButton, {}); + await popupShownPromise; + + let item = document.getElementById( + "customization-uidensity-menuitem-" + mode + ); + let normalItem = document.getElementById( + "customization-uidensity-menuitem-normal" + ); + + is( + normalItem.getAttribute("active"), + "true", + "Normal mode menuitem should be active by default" + ); + + // Hover over the mode menuitem and wait for the event that updates the UI + // density. + let mouseoverPromise = BrowserTestUtils.waitForEvent(item, "mouseover"); + EventUtils.synthesizeMouseAtCenter(item, { type: "mouseover" }); + await mouseoverPromise; + + is( + win.getAttribute("uidensity"), + mode, + `UI Density should be set to ${mode} on ${mode} menuitem hover` + ); + + is( + Services.prefs.getIntPref(PREF_UI_DENSITY), + window.gUIDensity.MODE_NORMAL, + `UI Density pref should still be set to normal on ${mode} menuitem hover` + ); + + // Hover the normal menuitem again and check that the UI density reset to normal. + EventUtils.synthesizeMouseAtCenter(normalItem, { type: "mouseover" }); + await BrowserTestUtils.waitForCondition(() => !win.hasAttribute("uidensity")); + + ok( + !win.hasAttribute("uidensity"), + `UI Density should be reset when no longer hovering the ${mode} menuitem` + ); + + // Select the custom UI density and wait for the popup to be hidden. + let popupHiddenPromise = popupHidden(popup); + EventUtils.synthesizeMouseAtCenter(item, {}); + await popupHiddenPromise; + + // Check that the click permanently changed the UI density. + is( + win.getAttribute("uidensity"), + mode, + `UI Density should be set to ${mode} on ${mode} menuitem click` + ); + is( + Services.prefs.getIntPref(PREF_UI_DENSITY), + modePref, + `UI Density pref should be set to ${mode} when clicking the ${mode} menuitem` + ); + + // Open the popup again. + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(popupButton, {}); + await popupShownPromise; + + // Check that the menuitem is still active after opening and closing the popup. + is( + item.getAttribute("active"), + "true", + `${mode} mode menuitem should be active` + ); + + // Hide the popup again. + popupHiddenPromise = popupHidden(popup); + EventUtils.synthesizeMouseAtCenter(popupButton, {}); + await popupHiddenPromise; + + // Check that the menuitem is still active after re-opening customize mode. + await endCustomizing(); + await startCustomizing(); + + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(popupButton, {}); + await popupShownPromise; + + is( + item.getAttribute("active"), + "true", + `${mode} mode menuitem should be active after entering and exiting customize mode` + ); + + // Click the normal menuitem and check that the density is reset. + popupHiddenPromise = popupHidden(popup); + EventUtils.synthesizeMouseAtCenter(normalItem, {}); + await popupHiddenPromise; + + ok( + !win.hasAttribute("uidensity"), + "UI Density should be reset when clicking the normal menuitem" + ); + + is( + Services.prefs.getIntPref(PREF_UI_DENSITY), + window.gUIDensity.MODE_NORMAL, + "UI Density pref should be set to normal." + ); + + // Show the popup and click on the mode menuitem again to test the + // reset default feature. + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(popupButton, {}); + await popupShownPromise; + + popupHiddenPromise = popupHidden(popup); + EventUtils.synthesizeMouseAtCenter(item, {}); + await popupHiddenPromise; + + is( + win.getAttribute("uidensity"), + mode, + `UI Density should be set to ${mode} on ${mode} menuitem click` + ); + + is( + Services.prefs.getIntPref(PREF_UI_DENSITY), + modePref, + `UI Density pref should be set to ${mode} when clicking the ${mode} menuitem` + ); + + await gCustomizeMode.reset(); + + ok( + !win.hasAttribute("uidensity"), + "UI Density should be reset when clicking the normal menuitem" + ); + + is( + Services.prefs.getIntPref(PREF_UI_DENSITY), + window.gUIDensity.MODE_NORMAL, + "UI Density pref should be set to normal." + ); + + await endCustomizing(); +} + +add_task(async function test_touch_mode_menuitem() { + // OSX doesn't get touch mode for now. + if (AppConstants.platform == "macosx") { + is( + document.getElementById("customization-uidensity-menuitem-touch"), + null, + "There's no touch option on Mac OSX" + ); + return; + } + + await testModeMenuitem("touch", window.gUIDensity.MODE_TOUCH); + + // Test the checkbox for automatic Touch Mode transition + // in Windows 10 Tablet Mode. + if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) { + await startCustomizing(); + + let popupButton = document.getElementById("customization-uidensity-button"); + let popup = document.getElementById("customization-uidensity-menu"); + let popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(popupButton, {}); + await popupShownPromise; + + let checkbox = document.getElementById( + "customization-uidensity-autotouchmode-checkbox" + ); + ok(checkbox.checked, "Checkbox should be checked by default"); + + // Test toggling the checkbox. + EventUtils.synthesizeMouseAtCenter(checkbox, {}); + is( + Services.prefs.getBoolPref(PREF_AUTO_TOUCH_MODE), + false, + "Automatic Touch Mode is off when the checkbox is unchecked." + ); + + EventUtils.synthesizeMouseAtCenter(checkbox, {}); + is( + Services.prefs.getBoolPref(PREF_AUTO_TOUCH_MODE), + true, + "Automatic Touch Mode is on when the checkbox is checked." + ); + + // Test reset to defaults. + EventUtils.synthesizeMouseAtCenter(checkbox, {}); + is( + Services.prefs.getBoolPref(PREF_AUTO_TOUCH_MODE), + false, + "Automatic Touch Mode is off when the checkbox is unchecked." + ); + + await gCustomizeMode.reset(); + is( + Services.prefs.getBoolPref(PREF_AUTO_TOUCH_MODE), + true, + "Automatic Touch Mode is on when the checkbox is checked." + ); + } +}); + +add_task(async function cleanup() { + await endCustomizing(); + + Services.prefs.clearUserPref(PREF_UI_DENSITY); + Services.prefs.clearUserPref(PREF_AUTO_TOUCH_MODE); +}); diff --git a/browser/components/customizableui/test/browser_disable_commands_customize.js b/browser/components/customizableui/test/browser_disable_commands_customize.js new file mode 100644 index 0000000000..f3eb06efbe --- /dev/null +++ b/browser/components/customizableui/test/browser_disable_commands_customize.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Most commands don't make sense in customize mode. Check that they're + * disabled, so shortcuts can't activate them either. Also check that + * some basic commands (close tab/window, quit, new tab, new window) + * remain functional. + */ +add_task(async function test_disable_commands() { + let disabledCommands = ["cmd_print", "Browser:SavePage", "Browser:SendLink"]; + let enabledCommands = [ + "cmd_newNavigatorTab", + "cmd_newNavigator", + "cmd_quitApplication", + "cmd_close", + "cmd_closeWindow", + ]; + + function checkDisabled() { + for (let cmd of disabledCommands) { + is( + document.getElementById(cmd).getAttribute("disabled"), + "true", + `Command ${cmd} should be disabled` + ); + } + for (let cmd of enabledCommands) { + ok( + !document.getElementById(cmd).hasAttribute("disabled"), + `Command ${cmd} should NOT be disabled` + ); + } + } + await startCustomizing(); + + checkDisabled(); + + // Do a reset just for fun, making sure we don't accidentally + // break things: + await gCustomizeMode.reset(); + + checkDisabled(); + + await endCustomizing(); + for (let cmd of disabledCommands.concat(enabledCommands)) { + ok( + !document.getElementById(cmd).hasAttribute("disabled"), + `Command ${cmd} should NOT be disabled after customize mode` + ); + } +}); + +/** + * When buttons are connected to a command, they should not get + * disabled just because we move them. + */ +add_task(async function test_dont_disable_when_moving() { + let button = gNavToolbox.palette.querySelector("#print-button"); + ok(button.hasAttribute("command"), "Button should have a command attribute."); + await startCustomizing(); + CustomizableUI.addWidgetToArea("print-button", "nav-bar"); + await endCustomizing(); + ok( + !button.hasAttribute("disabled"), + "Should not have disabled attribute after adding the button." + ); + ok( + button.hasAttribute("command"), + "Button should still have a command attribute." + ); + + await startCustomizing(); + await gCustomizeMode.reset(); + await endCustomizing(); + ok( + !button.hasAttribute("disabled"), + "Should not have disabled attribute when resetting in customize mode" + ); + ok( + button.hasAttribute("command"), + "Button should still have a command attribute." + ); +}); diff --git a/browser/components/customizableui/test/browser_drag_outside_palette.js b/browser/components/customizableui/test/browser_drag_outside_palette.js new file mode 100644 index 0000000000..2785a08896 --- /dev/null +++ b/browser/components/customizableui/test/browser_drag_outside_palette.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that moving items from the toolbar or panel to the palette by + * dropping on the panel container (not inside the visible panel) works. + */ +add_task(async function () { + await startCustomizing(); + let panelContainer = document.getElementById("customization-panel-container"); + // Try dragging an item from the navbar: + let stopReloadButton = document.getElementById("stop-reload-button"); + let oldNavbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar"); + simulateItemDrag(stopReloadButton, panelContainer); + assertAreaPlacements( + CustomizableUI.AREA_NAVBAR, + oldNavbarPlacements.filter(w => w != "stop-reload-button") + ); + ok( + stopReloadButton.closest("#customization-palette"), + "Button should be in the palette" + ); + + // Put it in the panel and try again from there: + let panelHolder = document.getElementById("customization-panelHolder"); + simulateItemDrag(stopReloadButton, panelHolder); + assertAreaPlacements(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, [ + "stop-reload-button", + ]); + + simulateItemDrag(stopReloadButton, panelContainer); + assertAreaPlacements(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, []); + + ok( + stopReloadButton.closest("#customization-palette"), + "Button should be in the palette" + ); + + // Check we can't move non-removable items like this: + let urlbar = document.getElementById("urlbar-container"); + simulateItemDrag(urlbar, panelContainer); + assertAreaPlacements( + CustomizableUI.AREA_NAVBAR, + oldNavbarPlacements.filter(w => w != "stop-reload-button") + ); +}); + +registerCleanupFunction(async function () { + await gCustomizeMode.reset(); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_editcontrols_update.js b/browser/components/customizableui/test/browser_editcontrols_update.js new file mode 100644 index 0000000000..9f064e521a --- /dev/null +++ b/browser/components/customizableui/test/browser_editcontrols_update.js @@ -0,0 +1,307 @@ +// This test checks that the edit command enabled state (cut/paste) is updated +// properly when the edit controls are on the toolbar, popup and not present. +// It also verifies that the performance optimiation implemented by +// updateEditUIVisibility in browser.js is applied. + +let isMac = navigator.platform.indexOf("Mac") == 0; + +function checkState(allowCut, desc, testWindow = window) { + is( + testWindow.document.getElementById("cmd_cut").getAttribute("disabled") == + "true", + !allowCut, + desc + " - cut" + ); + is( + testWindow.document.getElementById("cmd_paste").getAttribute("disabled") == + "true", + false, + desc + " - paste" + ); +} + +// Add a special controller to the urlbar and browser to listen in on when +// commands are being updated. Return a promise that resolves when 'count' +// updates have occurred. +function expectCommandUpdate(count, testWindow = window) { + return new Promise((resolve, reject) => { + let overrideController = { + supportsCommand(cmd) { + return cmd == "cmd_delete"; + }, + isCommandEnabled(cmd) { + if (!count) { + ok(false, "unexpected update"); + reject(); + } + + if (!--count) { + testWindow.gURLBar.inputField.controllers.removeControllerAt( + 0, + overrideController + ); + testWindow.gBrowser.selectedBrowser.controllers.removeControllerAt( + 0, + overrideController + ); + resolve(true); + } + }, + }; + + if (!count) { + SimpleTest.executeSoon(() => { + testWindow.gURLBar.inputField.controllers.removeControllerAt( + 0, + overrideController + ); + testWindow.gBrowser.selectedBrowser.controllers.removeControllerAt( + 0, + overrideController + ); + resolve(false); + }); + } + + testWindow.gURLBar.inputField.controllers.insertControllerAt( + 0, + overrideController + ); + testWindow.gBrowser.selectedBrowser.controllers.insertControllerAt( + 0, + overrideController + ); + }); +} + +// Call this between `.select()` to make sure the selection actually changes +// and thus TextInputListener::UpdateTextInputCommands() is called. +function deselectURLBarAndSpin() { + gURLBar.inputField.setSelectionRange(0, 0); + return new Promise(setTimeout); +} + +add_task(async function test_init() { + // Put something on the clipboard to verify that the paste button is properly enabled during the test. + let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + await new Promise(resolve => { + SimpleTest.waitForClipboard( + "Sample", + function () { + clipboardHelper.copyString("Sample"); + }, + resolve + ); + }); + + // Open and close the panel first so that it is fully initialized. + await gCUITestUtils.openMainMenu(); + await gCUITestUtils.hideMainMenu(); +}); + +// Test updating when the panel is open with the edit-controls on the panel. +// Updates should occur. +add_task(async function test_panelui_opened() { + document.commandDispatcher.unlock(); + gURLBar.focus(); + gURLBar.value = "test"; + + await gCUITestUtils.openMainMenu(); + + checkState(false, "Update when edit-controls is on panel and visible"); + + let overridePromise = expectCommandUpdate(1); + gURLBar.select(); + await overridePromise; + + checkState( + true, + "Update when edit-controls is on panel and selection changed" + ); + + overridePromise = expectCommandUpdate(0); + await gCUITestUtils.hideMainMenu(); + await overridePromise; + + // Check that updates do not occur after the panel has been closed. + checkState(true, "Update when edit-controls is on panel and hidden"); + + // Mac will update the enabled state even when the panel is closed so that + // main menubar shortcuts will work properly. + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(isMac ? 1 : 0); + gURLBar.select(); + await overridePromise; + checkState( + true, + "Update when edit-controls is on panel, hidden and selection changed" + ); +}); + +// Test updating when the edit-controls are moved to the toolbar. +add_task(async function test_panelui_customize_to_toolbar() { + await startCustomizing(); + let navbar = document.getElementById("nav-bar"); + simulateItemDrag( + document.getElementById("edit-controls"), + CustomizableUI.getCustomizationTarget(navbar), + "end" + ); + await endCustomizing(); + + // updateEditUIVisibility should be called when customization ends but isn't. See bug 1359790. + updateEditUIVisibility(); + + // The URL bar may have been focused to begin with, which means + // that subsequent calls to focus it won't result in command + // updates, so we'll make sure to blur it. + gURLBar.blur(); + + let overridePromise = expectCommandUpdate(1); + gURLBar.select(); + gURLBar.focus(); + gURLBar.value = "other"; + await overridePromise; + checkState(false, "Update when edit-controls on toolbar and focused"); + + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(1); + gURLBar.select(); + await overridePromise; + checkState( + true, + "Update when edit-controls on toolbar and selection changed" + ); + + const kOverflowPanel = document.getElementById("widget-overflow"); + + let originalWidth = window.outerWidth; + registerCleanupFunction(async function () { + kOverflowPanel.removeAttribute("animate"); + window.resizeTo(originalWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + CustomizableUI.reset(); + }); + + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + await TestUtils.waitForCondition( + () => + navbar.hasAttribute("overflowing") && + !navbar.querySelector("edit-controls") + ); + + // Mac will update the enabled state even when the buttons are overflowing, + // so main menubar shortcuts will work properly. + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(isMac ? 1 : 0); + gURLBar.select(); + await overridePromise; + checkState( + true, + "Update when edit-controls is on overflow panel, hidden and selection changed" + ); + + // Check that we get an update if we select content while the panel is open. + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(1); + await navbar.overflowable.show(); + gURLBar.select(); + await overridePromise; + + // And that we don't (except on mac) when the panel is hidden. + kOverflowPanel.hidePopup(); + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(isMac ? 1 : 0); + gURLBar.select(); + await overridePromise; + + window.resizeTo(originalWidth, window.outerHeight); + await TestUtils.waitForCondition(() => !navbar.hasAttribute("overflowing")); + + CustomizableUI.addWidgetToArea( + "edit-controls", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + // updateEditUIVisibility should be called when customization happens but isn't. See bug 1359790. + updateEditUIVisibility(); + + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(isMac ? 1 : 0); + gURLBar.select(); + await overridePromise; + + // Check that we get an update if we select content while the panel is open. + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(1); + await navbar.overflowable.show(); + gURLBar.select(); + await overridePromise; + + // And that we don't (except on mac) when the panel is hidden. + kOverflowPanel.hidePopup(); + await deselectURLBarAndSpin(); + overridePromise = expectCommandUpdate(isMac ? 1 : 0); + gURLBar.select(); + await overridePromise; +}); + +// Test updating when the edit-controls are moved to the palette. +add_task(async function test_panelui_customize_to_palette() { + await startCustomizing(); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(document.getElementById("edit-controls"), palette); + await endCustomizing(); + + // updateEditUIVisibility should be called when customization ends but isn't. See bug 1359790. + updateEditUIVisibility(); + + let overridePromise = expectCommandUpdate(isMac ? 1 : 0); + gURLBar.focus(); + gURLBar.value = "other"; + gURLBar.select(); + await overridePromise; + + // If the UI isn't found, the command is set to be enabled. + checkState( + true, + "Update when edit-controls is on palette, hidden and selection changed" + ); +}); + +add_task(async function finish() { + await resetCustomization(); +}); + +// Test updating in the initial state when the edit-controls are on the panel but +// have not yet been created. This needs to be done in a new window to ensure that +// other tests haven't opened the panel. +add_task(async function test_initial_state() { + let testWindow = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(testWindow); + + // For focusing the URL bar to have an effect, we need to ensure the URL bar isn't + // initially focused: + testWindow.gBrowser.selectedTab.focus(); + await TestUtils.waitForCondition(() => !testWindow.gURLBar.focused); + + let overridePromise = expectCommandUpdate(isMac, testWindow); + + testWindow.gURLBar.focus(); + testWindow.gURLBar.value = "test"; + + await overridePromise; + + // Commands won't update when no edit UI is present. They default to being + // enabled so that keyboard shortcuts will work. The real enabled state will + // be checked when shortcut is pressed. + checkState( + !isMac, + "No update when edit-controls is on panel and not visible", + testWindow + ); + + await BrowserTestUtils.closeWindow(testWindow); + await SimpleTest.promiseFocus(window); +}); diff --git a/browser/components/customizableui/test/browser_exit_background_customize_mode.js b/browser/components/customizableui/test/browser_exit_background_customize_mode.js new file mode 100644 index 0000000000..b89efef582 --- /dev/null +++ b/browser/components/customizableui/test/browser_exit_background_customize_mode.js @@ -0,0 +1,44 @@ +"use strict"; + +/** + * Tests that if customize mode is currently attached to a background + * tab, and that tab browses to a new location, that customize mode + * is detached from that tab. + */ +add_task(async function test_exit_background_customize_mode() { + let nonCustomizingTab = gBrowser.selectedTab; + + Assert.equal( + gBrowser.tabContainer.querySelector("tab[customizemode=true]"), + null, + "Should not have a tab marked as being the customize tab now." + ); + + await startCustomizing(); + is(gBrowser.tabs.length, 2, "Should have 2 tabs"); + + let custTab = gBrowser.selectedTab; + + let finishedCustomizing = BrowserTestUtils.waitForEvent( + gNavToolbox, + "aftercustomization" + ); + await BrowserTestUtils.switchTab(gBrowser, nonCustomizingTab); + await finishedCustomizing; + + let newURL = "http://example.com/"; + BrowserTestUtils.loadURIString(custTab.linkedBrowser, newURL); + await BrowserTestUtils.browserLoaded(custTab.linkedBrowser, false, newURL); + + Assert.equal( + gBrowser.tabContainer.querySelector("tab[customizemode=true]"), + null, + "Should not have a tab marked as being the customize tab now." + ); + + await startCustomizing(); + is(gBrowser.tabs.length, 3, "Should have 3 tabs now"); + + await endCustomizing(); + BrowserTestUtils.removeTab(custTab); +}); diff --git a/browser/components/customizableui/test/browser_flexible_space_area.js b/browser/components/customizableui/test/browser_flexible_space_area.js new file mode 100644 index 0000000000..f3189096de --- /dev/null +++ b/browser/components/customizableui/test/browser_flexible_space_area.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function getSpringCount(area) { + return CustomizableUI.getWidgetIdsInArea(area).filter(id => + id.includes("spring") + ).length; +} + +/** + * Check that no matter where we add a flexible space, we + * never end up without a flexible space in the palette. + */ +add_task(async function test_flexible_space_addition() { + await startCustomizing(); + let palette = document.getElementById("customization-palette"); + // Make the bookmarks toolbar visible: + CustomizableUI.setToolbarVisibility(CustomizableUI.AREA_BOOKMARKS, true); + let areas = [CustomizableUI.AREA_NAVBAR, CustomizableUI.AREA_BOOKMARKS]; + if (AppConstants.platform != "macosx") { + areas.push(CustomizableUI.AREA_MENUBAR); + } + + for (let area of areas) { + let spacer = palette.querySelector("toolbarspring"); + let toolbar = document.getElementById(area); + toolbar = CustomizableUI.getCustomizationTarget(toolbar); + + let springCount = getSpringCount(area); + simulateItemDrag(spacer, toolbar); + // Check we added the spring: + is( + springCount + 1, + getSpringCount(area), + "Should now have an extra spring" + ); + + // Check there's still one in the palette: + let newSpacer = palette.querySelector("toolbarspring"); + ok(newSpacer, "Should have created a new spring"); + } +}); +registerCleanupFunction(async function asyncCleanup() { + await endCustomizing(); + await resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_help_panel_cloning.js b/browser/components/customizableui/test/browser_help_panel_cloning.js new file mode 100644 index 0000000000..4234a52cd8 --- /dev/null +++ b/browser/components/customizableui/test/browser_help_panel_cloning.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global PanelUI */ + +let gAppMenuStrings = new Localization( + ["branding/brand.ftl", "browser/appmenu.ftl"], + true +); + +const CLONED_ATTRS = ["command", "oncommand", "onclick", "key", "disabled"]; + +/** + * Tests that the Help panel inside of the AppMenu properly clones + * the items from the Help menupopup. Also ensures that the AppMenu + * string variants for those menuitems exist inside of appmenu.ftl. + */ +add_task(async function test_help_panel_cloning() { + await gCUITestUtils.openMainMenu(); + registerCleanupFunction(async () => { + await gCUITestUtils.hideMainMenu(); + }); + + // Showing the Help panel should be enough to get the menupopup to + // populate itself. + let anchor = document.getElementById("PanelUI-menu-button"); + PanelUI.showHelpView(anchor); + + let appMenuHelpSubview = document.getElementById("PanelUI-helpView"); + await BrowserTestUtils.waitForEvent(appMenuHelpSubview, "ViewShowing"); + + let helpMenuPopup = document.getElementById("menu_HelpPopup"); + let helpMenuPopupItems = helpMenuPopup.querySelectorAll("menuitem"); + + for (let helpMenuPopupItem of helpMenuPopupItems) { + if (helpMenuPopupItem.hidden) { + continue; + } + + let appMenuHelpId = "appMenu_" + helpMenuPopupItem.id; + info(`Checking ${appMenuHelpId}`); + + let appMenuHelpItem = appMenuHelpSubview.querySelector(`#${appMenuHelpId}`); + Assert.ok(appMenuHelpItem, "Should have found a cloned AppMenu help item"); + + let appMenuHelpItemL10nId = appMenuHelpItem.dataset.l10nId; + // There is a convention that the Help menu item should have an + // appmenu-data-l10n-id attribute set as the AppMenu-specific localization + // id. + Assert.equal( + helpMenuPopupItem.getAttribute("appmenu-data-l10n-id"), + appMenuHelpItemL10nId, + "Help menuitem supplied a data-l10n-id for the AppMenu Help item" + ); + + let [strings] = gAppMenuStrings.formatMessagesSync([ + { id: appMenuHelpItemL10nId }, + ]); + Assert.ok(strings, "Should have found strings for the AppMenu help item"); + + // Make sure the CLONED_ATTRs are actually cloned. + for (let attr of CLONED_ATTRS) { + if (attr == "oncommand" && helpMenuPopupItem.hasAttribute("command")) { + // If the original element had a "command" attribute set, then the + // cloned element will have its "oncommand" attribute set to equal + // the "oncommand" attribute of the <command> pointed to via the + // original's "command" attribute once it is inserted into the DOM. + // + // This is by virtue of the broadcasting ability of XUL <command> + // elements. + let commandNode = document.getElementById( + helpMenuPopupItem.getAttribute("command") + ); + Assert.equal( + commandNode.getAttribute("oncommand"), + appMenuHelpItem.getAttribute("oncommand"), + "oncommand was properly cloned." + ); + } else { + Assert.equal( + helpMenuPopupItem.getAttribute(attr), + appMenuHelpItem.getAttribute(attr), + `${attr} attribute was cloned.` + ); + } + } + } +}); diff --git a/browser/components/customizableui/test/browser_hidden_widget_overflow.js b/browser/components/customizableui/test/browser_hidden_widget_overflow.js new file mode 100644 index 0000000000..0221bfc336 --- /dev/null +++ b/browser/components/customizableui/test/browser_hidden_widget_overflow.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if only hidden widgets are overflowed that the + * OverflowableToolbar won't show the overflow panel anchor. + */ + +const kHiddenButtonID = "fake-hidden-button"; +const kDisplayNoneButtonID = "display-none-button"; +const kWebExtensionButtonID1 = "fake-webextension-button-1"; +const kWebExtensionButtonID2 = "fake-webextension-button-2"; +let gWin = null; + +add_setup(async function () { + gWin = await BrowserTestUtils.openNewBrowserWindow(); + + // To make it easier to write a test where we can control overflowing + // for a test that can run in a bunch of environments with slightly + // different rules on when things will overflow, we'll go ahead and + // just remove everything removable from the nav-bar by default. Then + // we'll add our hidden item, and a single WebExtension item, and + // force toolbar overflow. + let widgetIDs = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR); + for (let widgetID of widgetIDs) { + if (CustomizableUI.isWidgetRemovable(widgetID)) { + CustomizableUI.removeWidgetFromArea(widgetID); + } + } + + CustomizableUI.createWidget({ + id: kWebExtensionButtonID1, + label: "Test WebExtension widget 1", + defaultArea: CustomizableUI.AREA_NAVBAR, + webExtension: true, + }); + + CustomizableUI.createWidget({ + id: kWebExtensionButtonID2, + label: "Test WebExtension widget 2", + defaultArea: CustomizableUI.AREA_NAVBAR, + webExtension: true, + }); + + // Let's force the WebExtension widgets to be significantly wider. This + // just makes it easier to ensure that both of these (which are to the left + // of the hidden widget) get overflowed. + for (let webExtID of [kWebExtensionButtonID1, kWebExtensionButtonID2]) { + let webExtNode = CustomizableUI.getWidget(webExtID).forWindow(gWin).node; + webExtNode.style.minWidth = "100px"; + } + + CustomizableUI.createWidget({ + id: kHiddenButtonID, + label: "Test hidden=true widget", + defaultArea: CustomizableUI.AREA_NAVBAR, + }); + + // Now hide the button with hidden=true so that it has no dimensions. + let hiddenButtonNode = + CustomizableUI.getWidget(kHiddenButtonID).forWindow(gWin).node; + hiddenButtonNode.hidden = true; + + CustomizableUI.createWidget({ + id: kDisplayNoneButtonID, + label: "Test display:none widget", + defaultArea: CustomizableUI.AREA_NAVBAR, + }); + + // Now hide the button with display: none so that it has no dimensions. + let displayNoneButtonNode = + CustomizableUI.getWidget(kDisplayNoneButtonID).forWindow(gWin).node; + displayNoneButtonNode.style.display = "none"; + + registerCleanupFunction(async () => { + CustomizableUI.destroyWidget(kWebExtensionButtonID1); + CustomizableUI.destroyWidget(kWebExtensionButtonID2); + CustomizableUI.destroyWidget(kHiddenButtonID); + CustomizableUI.destroyWidget(kDisplayNoneButtonID); + await BrowserTestUtils.closeWindow(gWin); + await CustomizableUI.reset(); + }); +}); + +add_task(async function test_hidden_widget_overflow() { + gWin.resizeTo(kForceOverflowWidthPx, window.outerHeight); + + // Wait until the left-most fake WebExtension button is overflowing. + let webExtNode = CustomizableUI.getWidget(kWebExtensionButtonID1).forWindow( + gWin + ).node; + await BrowserTestUtils.waitForMutationCondition( + webExtNode, + { attributes: true }, + () => { + return webExtNode.hasAttribute("overflowedItem"); + } + ); + + let hiddenButtonNode = + CustomizableUI.getWidget(kHiddenButtonID).forWindow(gWin).node; + Assert.ok( + hiddenButtonNode.hasAttribute("overflowedItem"), + "Hidden button should be overflowed." + ); + + let overflowButton = gWin.document.getElementById("nav-bar-overflow-button"); + + Assert.ok( + !BrowserTestUtils.is_visible(overflowButton), + "Overflow panel button should be hidden." + ); +}); diff --git a/browser/components/customizableui/test/browser_history_after_appMenu.js b/browser/components/customizableui/test/browser_history_after_appMenu.js new file mode 100644 index 0000000000..89c4b467a2 --- /dev/null +++ b/browser/components/customizableui/test/browser_history_after_appMenu.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Checks that opening the History view using the default toolbar button works + * also while the view is displayed in the main menu. + */ +add_task(async function test_history_after_appMenu() { + // First add the button to the toolbar and wait for it to show up: + CustomizableUI.addWidgetToArea("history-panelmenu", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("history-panelmenu") + ); + await waitForElementShown(document.getElementById("history-panelmenu")); + + let historyView = PanelMultiView.getViewNode(document, "PanelUI-history"); + // Open the main menu. + await gCUITestUtils.openMainMenu(); + + // Show the History view as a subview of the main menu. + document.getElementById("appMenu-history-button").click(); + await BrowserTestUtils.waitForEvent(historyView, "ViewShown"); + + // Show the History view as the main view of the History panel. + document.getElementById("history-panelmenu").click(); + await BrowserTestUtils.waitForEvent(historyView, "ViewShown"); + + // Close the history panel. + let historyPanel = historyView.closest("panel"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "popuphidden"); + historyPanel.hidePopup(); + await promise; +}); diff --git a/browser/components/customizableui/test/browser_history_recently_closed.js b/browser/components/customizableui/test/browser_history_recently_closed.js new file mode 100644 index 0000000000..01039ed960 --- /dev/null +++ b/browser/components/customizableui/test/browser_history_recently_closed.js @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +add_task(async function testRecentlyClosedDisabled() { + info("Check history recently closed tabs/windows section"); + + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + // We need to make sure the history is cleared before starting the test + await Sanitizer.sanitize(["history"]); + + await openHistoryPanel(); + + let recentlyClosedTabs = document.getElementById("appMenuRecentlyClosedTabs"); + let recentlyClosedWindows = document.getElementById( + "appMenuRecentlyClosedWindows" + ); + + // Wait for the disabled attribute to change, as we receive + // the "viewshown" event before this changes + await BrowserTestUtils.waitForCondition( + () => recentlyClosedTabs.getAttribute("disabled"), + "Waiting for button to become disabled" + ); + Assert.ok( + recentlyClosedTabs.getAttribute("disabled"), + "Recently closed tabs button disabled" + ); + Assert.ok( + recentlyClosedWindows.getAttribute("disabled"), + "Recently closed windows button disabled" + ); + + await hideHistoryPanel(); + + gBrowser.selectedTab.focus(); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "dummy_history_item.html" + ); + BrowserTestUtils.removeTab(tab); + + await openHistoryPanel(); + + await BrowserTestUtils.waitForCondition( + () => !recentlyClosedTabs.getAttribute("disabled"), + "Waiting for button to be enabled" + ); + Assert.ok( + !recentlyClosedTabs.getAttribute("disabled"), + "Recently closed tabs is available" + ); + Assert.ok( + recentlyClosedWindows.getAttribute("disabled"), + "Recently closed windows button disabled" + ); + + await hideHistoryPanel(); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + let loadedPromise = BrowserTestUtils.browserLoaded( + newWin.gBrowser.selectedBrowser + ); + BrowserTestUtils.loadURIString( + newWin.gBrowser.selectedBrowser, + "about:mozilla" + ); + await loadedPromise; + await BrowserTestUtils.closeWindow(newWin); + + await openHistoryPanel(); + + await BrowserTestUtils.waitForCondition( + () => !recentlyClosedWindows.getAttribute("disabled"), + "Waiting for button to be enabled" + ); + Assert.ok( + !recentlyClosedTabs.getAttribute("disabled"), + "Recently closed tabs is available" + ); + Assert.ok( + !recentlyClosedWindows.getAttribute("disabled"), + "Recently closed windows is available" + ); + + await hideHistoryPanel(); +}); + +add_task(async function testRecentlyClosedTabsDisabledPersists() { + info("Check history recently closed tabs/windows section"); + + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + // We need to make sure the history is cleared before starting the test + await Sanitizer.sanitize(["history"]); + + await openHistoryPanel(); + + let recentlyClosedTabs = document.getElementById("appMenuRecentlyClosedTabs"); + Assert.ok( + recentlyClosedTabs.getAttribute("disabled"), + "Recently closed tabs button disabled" + ); + + await hideHistoryPanel(); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + await openHistoryPanel(newWin.document); + recentlyClosedTabs = newWin.document.getElementById( + "appMenuRecentlyClosedTabs" + ); + Assert.ok( + recentlyClosedTabs.getAttribute("disabled"), + "Recently closed tabs is disabled" + ); + + // We close the window without hiding the panel first, which used to interfere + // with populating the view subsequently. + await BrowserTestUtils.closeWindow(newWin); + + newWin = await BrowserTestUtils.openNewBrowserWindow(); + await openHistoryPanel(newWin.document); + recentlyClosedTabs = newWin.document.getElementById( + "appMenuRecentlyClosedTabs" + ); + Assert.ok( + recentlyClosedTabs.getAttribute("disabled"), + "Recently closed tabs is disabled" + ); + await hideHistoryPanel(newWin.document); + await BrowserTestUtils.closeWindow(newWin); +}); + +add_task(async function testRecentlyClosedWindows() { + // We need to make sure the history is cleared before starting the test + await Sanitizer.sanitize(["history"]); + + // Open and close a new window. + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + let loadedPromise = BrowserTestUtils.browserLoaded( + newWin.gBrowser.selectedBrowser + ); + BrowserTestUtils.loadURIString( + newWin.gBrowser.selectedBrowser, + "https://example.com" + ); + await loadedPromise; + await BrowserTestUtils.closeWindow(newWin); + + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + await openHistoryPanel(); + + // Open the "Recently closed windows" panel. + document.getElementById("appMenuRecentlyClosedWindows").click(); + + let winPanel = document.getElementById( + "appMenu-library-recentlyClosedWindows" + ); + await BrowserTestUtils.waitForEvent(winPanel, "ViewShown"); + ok(true, "Opened 'Recently closed windows' panel"); + + // Click the first toolbar button in the panel. + let panelBody = winPanel.querySelector(".panel-subview-body"); + let toolbarButton = panelBody.querySelector("toolbarbutton"); + let newWindowPromise = BrowserTestUtils.waitForNewWindow(); + EventUtils.sendMouseEvent({ type: "click" }, toolbarButton, window); + + newWin = await newWindowPromise; + is( + newWin.gBrowser.currentURI.spec, + "https://example.com/", + "Opened correct URL" + ); + is(gBrowser.tabs.length, 1, "Did not open new tabs"); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/components/customizableui/test/browser_history_recently_closed_middleclick.js b/browser/components/customizableui/test/browser_history_recently_closed_middleclick.js new file mode 100644 index 0000000000..309554ce23 --- /dev/null +++ b/browser/components/customizableui/test/browser_history_recently_closed_middleclick.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Verifies that middle-clicking "Recently Closed Tabs" in both history +// menus works as expected. + +const URLS = [ + "http://example.com/", + "http://example.org/", + "http://example.net/", +]; + +async function setupTest() { + // Navigate the initial tab to ensure that it won't be reused for the tab + // that will be reopened. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURIString( + gBrowser.selectedBrowser, + "https://example.com" + ); + await loadPromise; + + // Populate the recently closed tabs list. + for (let url of URLS) { + await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + } + for (let i = 0; i < URLS.length; i++) { + gBrowser.removeTab(gBrowser.selectedTab); + } + + return gBrowser.tabs.length; +} + +add_task(async function testMenubar() { + if (AppConstants.platform === "macosx") { + ok(true, "Can't open menu items on macOS"); + return; + } + + let nOpenTabs = await setupTest(); + + // Open the "History" menu. + let menu = document.getElementById("history-menu"); + let popupPromise = BrowserTestUtils.waitForEvent(menu, "popupshown"); + menu.open = true; + await popupPromise; + ok(true, "Opened 'History' menu"); + + // Open the "Recently Closed Tabs" submenu. + let undoMenu = document.getElementById("historyUndoMenu"); + popupPromise = BrowserTestUtils.waitForEvent(undoMenu, "popupshown"); + undoMenu.open = true; + let popupEvent = await popupPromise; + ok(true, "Opened 'Recently Closed Tabs' menu"); + + // And now middle-click the first item in that menu, and ensure that we're + // only opening a single new tab. + let menuitems = popupEvent.target.querySelectorAll("menuitem"); + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + popupEvent.target.activateItem(menuitems[0], { button: 1 }); + + let newTab = await newTabPromise; + is(newTab.linkedBrowser.currentURI.spec, URLS[0], "Opened correct URL"); + is(gBrowser.tabs.length, nOpenTabs + 1, "Only opened 1 new tab"); + + gBrowser.removeTab(newTab); +}); + +add_task(async function testHistoryPanel() { + let nOpenTabs = await setupTest(); + + // Setup history panel. + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + await openHistoryPanel(); + + // Open the "Recently closed tabs" panel. + let recentlyClosedTabs = document.getElementById("appMenuRecentlyClosedTabs"); + recentlyClosedTabs.click(); + + let recentlyClosedTabsPanel = document.getElementById( + "appMenu-library-recentlyClosedTabs" + ); + await BrowserTestUtils.waitForEvent(recentlyClosedTabsPanel, "ViewShown"); + ok(true, "Opened 'Recently closed tabs' panel"); + + let panelBody = recentlyClosedTabsPanel.querySelector(".panel-subview-body"); + let toolbarButtons = panelBody.querySelectorAll("toolbarbutton"); + + // Middle-click the first toolbar button in the panel. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + EventUtils.sendMouseEvent( + { type: "click", button: 1 }, + toolbarButtons[0], + window + ); + + let newTab = await newTabPromise; + is(newTab.linkedBrowser.currentURI.spec, URLS[0], "Opened correct URL"); + is(gBrowser.tabs.length, nOpenTabs + 1, "Only opened 1 new tab"); + + gBrowser.removeTab(newTab); +}); diff --git a/browser/components/customizableui/test/browser_history_restore_session.js b/browser/components/customizableui/test/browser_history_restore_session.js new file mode 100644 index 0000000000..a8b9529209 --- /dev/null +++ b/browser/components/customizableui/test/browser_history_restore_session.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function testRestoreSession() { + info("Check history panel's restore previous session button"); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + CustomizableUI.addWidgetToArea( + "history-panelmenu", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + registerCleanupFunction(() => CustomizableUI.reset()); + + // We need to make sure the history is cleared before starting the test + await Sanitizer.sanitize(["history"]); + + await openHistoryPanel(win.document); + + let restorePrevSessionBtn = win.document.getElementById( + "appMenu-restoreSession" + ); + + Assert.ok( + restorePrevSessionBtn.hidden, + "Restore previous session button is not visible" + ); + await hideHistoryPanel(win.document); + + BrowserTestUtils.addTab(win.gBrowser, "about:mozilla"); + await BrowserTestUtils.closeWindow(win); + + win = await BrowserTestUtils.openNewBrowserWindow(); + + let lastSession = ChromeUtils.importESModule( + "resource:///modules/sessionstore/SessionStore.sys.mjs" + )._LastSession; + lastSession.setState(true); + + await openHistoryPanel(win.document); + + restorePrevSessionBtn = win.document.getElementById("appMenu-restoreSession"); + Assert.ok( + !restorePrevSessionBtn.hidden, + "Restore previous session button is visible" + ); + + await hideHistoryPanel(win.document); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/customizableui/test/browser_insert_before_moved_node.js b/browser/components/customizableui/test/browser_insert_before_moved_node.js new file mode 100644 index 0000000000..611f4e3ce0 --- /dev/null +++ b/browser/components/customizableui/test/browser_insert_before_moved_node.js @@ -0,0 +1,51 @@ +"use strict"; + +/** + * Check inserting before a node that has moved from the toolbar into a + * non-customizable bit of the browser works. + */ +add_task(async function () { + for (let toolbar of ["nav-bar", "TabsToolbar"]) { + CustomizableUI.createWidget({ + id: "real-button", + label: "test real button", + }); + CustomizableUI.addWidgetToArea("real-button", toolbar); + CustomizableUI.addWidgetToArea("moved-button-not-here", toolbar); + let placements = CustomizableUI.getWidgetIdsInArea(toolbar); + Assert.deepEqual( + placements.slice(-2), + ["real-button", "moved-button-not-here"], + "Should have correct placements" + ); + let otherButton = document.createXULElement("toolbarbutton"); + otherButton.id = "moved-button-not-here"; + if (toolbar == "nav-bar") { + gURLBar.textbox.parentNode.appendChild(otherButton); + } else { + gBrowser.tabContainer.appendChild(otherButton); + } + CustomizableUI.destroyWidget("real-button"); + CustomizableUI.createWidget({ + id: "real-button", + label: "test real button", + }); + + let button = document.getElementById("real-button"); + ok(button, "Button should exist"); + if (button) { + let expectedContainer = CustomizableUI.getCustomizationTarget( + document.getElementById(toolbar) + ); + is( + button.parentNode, + expectedContainer, + "Button should be in the toolbar" + ); + } + + CustomizableUI.destroyWidget("real-button"); + otherButton.remove(); + CustomizableUI.reset(); + } +}); diff --git a/browser/components/customizableui/test/browser_menubar_visibility.js b/browser/components/customizableui/test/browser_menubar_visibility.js new file mode 100644 index 0000000000..82f2959905 --- /dev/null +++ b/browser/components/customizableui/test/browser_menubar_visibility.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that menubar visibility is propagated correctly to new windows. + */ +add_task(async function test_menubar_visbility() { + let menubar = document.getElementById("toolbar-menubar"); + is(menubar.getAttribute("autohide"), "true", "Menubar should be autohiding"); + registerCleanupFunction(() => { + Services.xulStore.removeValue( + AppConstants.BROWSER_CHROME_URL, + menubar.id, + "autohide" + ); + menubar.setAttribute("autohide", "true"); + }); + + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouse( + document.getElementById("stop-reload-button"), + 2, + 2, + { + type: "contextmenu", + button: 2, + } + ); + await shownPromise; + let attrChanged = BrowserTestUtils.waitForAttribute( + "autohide", + menubar, + "false" + ); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("toggle_toolbar-menubar"), + {} + ); + await attrChanged; + contextMenu.hidePopup(); // to be safe. + + is( + menubar.getAttribute("autohide"), + "false", + "Menubar should now be permanently visible." + ); + let persistedValue = Services.xulStore.getValue( + AppConstants.BROWSER_CHROME_URL, + menubar.id, + "autohide" + ); + is(persistedValue, "false", "New value should be persisted"); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + + is( + win.document.getElementById("toolbar-menubar").getAttribute("autohide"), + "false", + "Menubar should also be permanently visible in the new window." + ); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/customizableui/test/browser_newtab_button_customizemode.js b/browser/components/customizableui/test/browser_newtab_button_customizemode.js new file mode 100644 index 0000000000..c30616f3a3 --- /dev/null +++ b/browser/components/customizableui/test/browser_newtab_button_customizemode.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests in this file check that user customizations to the tabstrip show + * the correct type of new tab button while the tabstrip isn't overflowing. + */ + +const kGlobalNewTabButton = document.getElementById("new-tab-button"); +const kInnerNewTabButton = gBrowser.tabContainer.newTabButton; + +function assertNewTabButton(which) { + if (which == "global") { + isnot( + kGlobalNewTabButton.getBoundingClientRect().width, + 0, + "main new tab button should be visible" + ); + is( + kInnerNewTabButton.getBoundingClientRect().width, + 0, + "inner new tab button should be hidden" + ); + } else if (which == "inner") { + is( + kGlobalNewTabButton.getBoundingClientRect().width, + 0, + "main new tab button should be hidden" + ); + isnot( + kInnerNewTabButton.getBoundingClientRect().width, + 0, + "inner new tab button should be visible" + ); + } else { + ok(false, "Unexpected button: " + which); + } +} + +/** + * Add and remove items *after* the new tab button in customize mode. + */ +add_task(async function addremove_after_newtab_customizemode() { + await startCustomizing(); + await waitForElementShown(kGlobalNewTabButton); + simulateItemDrag( + document.getElementById("stop-reload-button"), + kGlobalNewTabButton, + "end" + ); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should have the adjacent newtab attribute" + ); + await endCustomizing(); + assertNewTabButton("inner"); + + await startCustomizing(); + let dropTarget = document.getElementById("forward-button"); + await waitForElementShown(dropTarget); + simulateItemDrag( + document.getElementById("stop-reload-button"), + dropTarget, + "end" + ); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should still have the adjacent newtab attribute" + ); + await endCustomizing(); + assertNewTabButton("inner"); + ok(CustomizableUI.inDefaultState, "Should be in default state"); +}); + +/** + * Add and remove items *before* the new tab button in customize mode. + */ +add_task(async function addremove_before_newtab_customizemode() { + await startCustomizing(); + await waitForElementShown(kGlobalNewTabButton); + simulateItemDrag( + document.getElementById("stop-reload-button"), + kGlobalNewTabButton, + "start" + ); + ok( + !gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should no longer have the adjacent newtab attribute" + ); + await endCustomizing(); + assertNewTabButton("global"); + await startCustomizing(); + let dropTarget = document.getElementById("forward-button"); + await waitForElementShown(dropTarget); + simulateItemDrag( + document.getElementById("stop-reload-button"), + dropTarget, + "end" + ); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should have the adjacent newtab attribute again" + ); + await endCustomizing(); + assertNewTabButton("inner"); + ok(CustomizableUI.inDefaultState, "Should be in default state"); +}); + +/** + * Add and remove items *after* the new tab button outside of customize mode. + */ +add_task(async function addremove_after_newtab_api() { + CustomizableUI.addWidgetToArea("stop-reload-button", "TabsToolbar"); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should have the adjacent newtab attribute" + ); + assertNewTabButton("inner"); + + CustomizableUI.reset(); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should still have the adjacent newtab attribute" + ); + assertNewTabButton("inner"); + ok(CustomizableUI.inDefaultState, "Should be in default state"); +}); + +/** + * Add and remove items *before* the new tab button outside of customize mode. + */ +add_task(async function addremove_before_newtab_api() { + let index = + CustomizableUI.getWidgetIdsInArea("TabsToolbar").indexOf("new-tab-button"); + CustomizableUI.addWidgetToArea("stop-reload-button", "TabsToolbar", index); + ok( + !gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should no longer have the adjacent newtab attribute" + ); + assertNewTabButton("global"); + + CustomizableUI.removeWidgetFromArea("stop-reload-button"); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should have the adjacent newtab attribute again" + ); + assertNewTabButton("inner"); + + CustomizableUI.reset(); + ok(CustomizableUI.inDefaultState, "Should be in default state"); +}); + +/** + * Reset to defaults in customize mode to see if that doesn't break things. + */ +add_task(async function reset_before_newtab_customizemode() { + await startCustomizing(); + await waitForElementShown(kGlobalNewTabButton); + simulateItemDrag( + document.getElementById("stop-reload-button"), + kGlobalNewTabButton, + "start" + ); + ok( + !gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should no longer have the adjacent newtab attribute" + ); + await endCustomizing(); + assertNewTabButton("global"); + await startCustomizing(); + await gCustomizeMode.reset(); + ok( + gBrowser.tabContainer.hasAttribute("hasadjacentnewtabbutton"), + "tabs should have the adjacent newtab attribute again" + ); + await endCustomizing(); + assertNewTabButton("inner"); + ok(CustomizableUI.inDefaultState, "Should be in default state"); +}); diff --git a/browser/components/customizableui/test/browser_open_from_popup.js b/browser/components/customizableui/test/browser_open_from_popup.js new file mode 100644 index 0000000000..bf140fde79 --- /dev/null +++ b/browser/components/customizableui/test/browser_open_from_popup.js @@ -0,0 +1,24 @@ +"use strict"; + +/** + * Check that opening customize mode in a popup opens it in the main window. + */ +add_task(async function open_customize_mode_from_popup() { + let promiseWindow = BrowserTestUtils.waitForNewWindow(); + SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + content.window.open("about:blank", "_blank", "height=300,toolbar=no"); + }); + let win = await promiseWindow; + let customizePromise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + win.gCustomizeMode.enter(); + await customizePromise; + ok( + document.documentElement.hasAttribute("customizing"), + "Should have opened customize mode in the parent window" + ); + await endCustomizing(); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/customizableui/test/browser_open_in_lazy_tab.js b/browser/components/customizableui/test/browser_open_in_lazy_tab.js new file mode 100644 index 0000000000..c18de67698 --- /dev/null +++ b/browser/components/customizableui/test/browser_open_in_lazy_tab.js @@ -0,0 +1,42 @@ +"use strict"; + +/** + * Check that customize mode can be loaded in a lazy tab. + */ +add_task(async function open_customize_mode_in_lazy_tab() { + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + createLazyBrowser: true, + }); + gCustomizeMode.setTab(tab); + + is(tab.linkedPanel, "", "Tab should be lazy"); + + let title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle", [ + document.getElementById("bundle_brand").getString("brandShortName"), + ]); + is(tab.label, title, "Tab should have correct title"); + + let customizePromise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + gCustomizeMode.enter(); + await customizePromise; + + is( + tab.getAttribute("customizemode"), + "true", + "Tab should be in customize mode" + ); + + let customizationContainer = document.getElementById( + "customization-container" + ); + is( + customizationContainer.hidden, + false, + "Customization container should be visible" + ); + + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_overflow_use_subviews.js b/browser/components/customizableui/test/browser_overflow_use_subviews.js new file mode 100644 index 0000000000..1e6e227364 --- /dev/null +++ b/browser/components/customizableui/test/browser_overflow_use_subviews.js @@ -0,0 +1,88 @@ +"use strict"; + +const kOverflowPanel = document.getElementById("widget-overflow"); + +var gOriginalWidth; +async function stopOverflowing() { + kOverflowPanel.removeAttribute("animate"); + window.resizeTo(gOriginalWidth, window.outerHeight); + await TestUtils.waitForCondition( + () => !document.getElementById("nav-bar").hasAttribute("overflowing") + ); + CustomizableUI.reset(); +} + +registerCleanupFunction(stopOverflowing); + +/** + * This checks that subview-compatible items show up as subviews rather than + * re-anchored panels. If we ever remove the library widget, please + * replace this test with another subview - don't remove it. + */ +add_task(async function check_library_subview_in_overflow() { + kOverflowPanel.setAttribute("animate", "false"); + gOriginalWidth = window.outerWidth; + + CustomizableUI.addWidgetToArea("library-button", CustomizableUI.AREA_NAVBAR); + + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok( + !navbar.hasAttribute("overflowing"), + "Should start with a non-overflowing toolbar." + ); + window.resizeTo(kForceOverflowWidthPx, window.outerHeight); + + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + + let chevron = document.getElementById("nav-bar-overflow-button"); + let shownPanelPromise = BrowserTestUtils.waitForEvent( + kOverflowPanel, + "ViewShown" + ); + chevron.click(); + await shownPanelPromise; + + let button = document.getElementById("library-button"); + button.click(); + + let libraryView = document.getElementById("appMenu-libraryView"); + await BrowserTestUtils.waitForEvent(libraryView, "ViewShown"); + let hasSubviews = !!kOverflowPanel.querySelector("panelmultiview"); + let expectedPanel = hasSubviews + ? kOverflowPanel + : document.getElementById("customizationui-widget-panel"); + is(libraryView.closest("panel"), expectedPanel, "Should be inside the panel"); + expectedPanel.hidePopup(); + await Promise.resolve(); // wait for popup to hide fully. + await stopOverflowing(); +}); + +/** + * This checks that non-subview-compatible items still work correctly. + * Ideally we should make the downloads panel and bookmarks/library item + * proper subview items, then this test can go away, and potentially we can + * simplify some of the subview anchoring code. + */ +add_task(async function check_downloads_panel_in_overflow() { + let button = document.getElementById("downloads-button"); + await gCustomizeMode.addToPanel(button); + await waitForOverflowButtonShown(); + + let chevron = document.getElementById("nav-bar-overflow-button"); + let shownPanelPromise = promisePanelElementShown(window, kOverflowPanel); + chevron.click(); + await shownPanelPromise; + + button.click(); + await TestUtils.waitForCondition(() => { + let panel = document.getElementById("downloadsPanel"); + return panel && panel.state != "closed"; + }); + let downloadsPanel = document.getElementById("downloadsPanel"); + isnot( + downloadsPanel.state, + "closed", + "Should be attempting to show the downloads panel." + ); + downloadsPanel.hidePopup(); +}); diff --git a/browser/components/customizableui/test/browser_palette_labels.js b/browser/components/customizableui/test/browser_palette_labels.js new file mode 100644 index 0000000000..42767a8ee2 --- /dev/null +++ b/browser/components/customizableui/test/browser_palette_labels.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that all customizable buttons have labels and icons. + * + * This is primarily designed to ensure we don't end up with items without + * labels in customize mode. In the past, this has happened due to race + * conditions, where labels would be correct if and only if the item had + * already been moved into a toolbar or panel in the main UI before + * (forcing it to be constructed and any fluent identifiers to be localized + * and applied). + * We use a new window to ensure that earlier tests using some of the widgets + * in the palette do not influence our checks to see that such items get + * labels, "even" if the first time they're rendered is in customize mode's + * palette. + */ +add_task(async function test_all_buttons_have_labels() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + registerCleanupFunction(async () => { + await endCustomizing(win); + return BrowserTestUtils.closeWindow(win); + }); + await startCustomizing(win); + let { palette } = win.gNavToolbox; + // Wait for things to paint. + await TestUtils.waitForCondition(() => { + return !!Array.from(palette.querySelectorAll(".toolbarbutton-icon")).filter( + n => { + let rect = n.getBoundingClientRect(); + return rect.height > 0 && rect.width > 0; + } + ).length; + }, "Must start rendering icons."); + + for (let wrapper of palette.children) { + if (wrapper.hasAttribute("title")) { + ok(true, wrapper.firstElementChild.id + " has a label."); + } else { + info( + `${wrapper.firstElementChild.id} doesn't seem to have a label, waiting.` + ); + await BrowserTestUtils.waitForAttribute("title", wrapper); + ok( + wrapper.hasAttribute("title"), + wrapper.firstElementChild.id + " has a label." + ); + } + let icons = Array.from(wrapper.querySelectorAll(".toolbarbutton-icon")); + // If there are icons, at least one must be visible + // (not everything necessarily has one, e.g. the search bar has no icon) + if (icons.length) { + let visibleIcons = icons.filter(n => { + let rect = n.getBoundingClientRect(); + return rect.height > 0 && rect.width > 0; + }); + Assert.greater( + visibleIcons.length, + 0, + `${wrapper.firstElementChild.id} should have at least one visible icon.` + ); + } + } +}); diff --git a/browser/components/customizableui/test/browser_panelUINotifications.js b/browser/components/customizableui/test/browser_panelUINotifications.js new file mode 100644 index 0000000000..818fcbad39 --- /dev/null +++ b/browser/components/customizableui/test/browser_panelUINotifications.js @@ -0,0 +1,597 @@ +"use strict"; + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +/** + * Tests that when we click on the main call-to-action of the doorhanger, the provided + * action is called, and the doorhanger removed. + */ +add_task(async function testMainActionCalled() { + let options = { + gBrowser: window.gBrowser, + url: "about:blank", + }; + + await BrowserTestUtils.withNewTab(options, function (browser) { + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let button = doorhanger.button; + button.click(); + + ok(mainActionCalled, "Main action callback was called"); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + }); +}); + +/** + * This tests that when we click the secondary action for a notification, + * it will display the badge for that notification on the PanelUI menu button. + * Once we click on this button, we should see an item in the menu which will + * call our main action. + */ +add_task(async function testSecondaryActionWorkflow() { + let options = { + gBrowser: window.gBrowser, + url: "about:blank", + }; + + await BrowserTestUtils.withNewTab(options, async function (browser) { + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let secondaryActionButton = doorhanger.secondaryButton; + secondaryActionButton.click(); + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is displaying on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is displaying on PanelUI button." + ); + let menuItem = PanelUI.mainView.querySelector(".panel-banner-item"); + is( + menuItem.getAttribute("data-l10n-id"), + "appmenuitem-banner-update-manual", + "Showing correct label" + ); + is(menuItem.hidden, false, "update-manual menu item is showing."); + + await gCUITestUtils.hideMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is shown on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + menuItem.click(); + ok(mainActionCalled, "Main action callback was called"); + + AppMenuNotifications.removeNotification(/.*/); + }); +}); + +/** + * This tests that the PanelUI update downloading badge and banner + * notification are correctly displayed and that clicking the banner + * item calls the main action. + */ +add_task(async function testDownloadingBadge() { + let options = { + gBrowser: window.gBrowser, + url: "about:blank", + }; + + await BrowserTestUtils.withNewTab(options, async function (browser) { + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + // The downloading notification is always displayed in a dismissed state. + AppMenuNotifications.showNotification( + "update-downloading", + mainAction, + undefined, + { dismissed: true } + ); + is(PanelUI.notificationPanel.state, "closed", "doorhanger is closed."); + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-downloading", + "Downloading badge is displaying on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-downloading", + "Downloading badge is displaying on PanelUI button." + ); + let menuItem = PanelUI.mainView.querySelector(".panel-banner-item"); + is( + menuItem.getAttribute("data-l10n-id"), + "appmenuitem-banner-update-downloading", + "Showing correct label (downloading)" + ); + is(menuItem.hidden, false, "update-downloading menu item is showing."); + + await gCUITestUtils.hideMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-downloading", + "Downloading badge is shown on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + menuItem.click(); + ok(mainActionCalled, "Main action callback was called"); + + AppMenuNotifications.removeNotification(/.*/); + }); +}); + +/** + * We want to ensure a few things with this: + * - Adding a doorhanger will make a badge disappear + * - once the notification for the doorhanger is resolved (removed, not just dismissed), + * then we display any other badges that are remaining. + */ +add_task(async function testInteractionWithBadges() { + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + // Remove the fxa toolbar button from the navbar to ensure the notification + // is displayed on the app menu button. + let { CustomizableUI } = ChromeUtils.importESModule( + "resource:///modules/CustomizableUI.sys.mjs" + ); + CustomizableUI.removeWidgetFromArea("fxa-toolbar-menu-button"); + + AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication"); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Fxa badge is shown on PanelUI button." + ); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + + isnot( + PanelUI.menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Fxa badge is hidden on PanelUI button." + ); + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let secondaryActionButton = doorhanger.secondaryButton; + secondaryActionButton.click(); + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is displaying on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is displaying on PanelUI button." + ); + let menuItem = PanelUI.mainView.querySelector(".panel-banner-item"); + is( + menuItem.getAttribute("data-l10n-id"), + "appmenuitem-banner-update-manual", + "Showing correct label" + ); + is(menuItem.hidden, false, "update-manual menu item is showing."); + + menuItem.click(); + ok(mainActionCalled, "Main action callback was called"); + + is( + PanelUI.menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Fxa badge is shown on PanelUI button." + ); + AppMenuNotifications.removeNotification(/.*/); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + }); +}); + +/** + * This tests that adding a badge will not dismiss any existing doorhangers. + */ +add_task(async function testAddingBadgeWhileDoorhangerIsShowing() { + await BrowserTestUtils.withNewTab("about:blank", function (browser) { + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication"); + + isnot( + PanelUI.menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Fxa badge is hidden on PanelUI button." + ); + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let mainActionButton = doorhanger.button; + mainActionButton.click(); + + ok(mainActionCalled, "Main action callback was called"); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Fxa badge is shown on PanelUI button." + ); + AppMenuNotifications.removeNotification(/.*/); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + }); +}); + +/** + * Tests that badges operate like a stack. + */ +add_task(async function testMultipleBadges() { + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + let doc = browser.ownerDocument; + let menuButton = doc.getElementById("PanelUI-menu-button"); + + is( + menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + is( + menuButton.hasAttribute("badge"), + false, + "Should not have the badge attribute set" + ); + + AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication"); + is( + menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Should have fxa-needs-authentication badge status" + ); + + AppMenuNotifications.showBadgeOnlyNotification("update-succeeded"); + is( + menuButton.getAttribute("badge-status"), + "update-succeeded", + "Should have update-succeeded badge status (update > fxa)" + ); + + AppMenuNotifications.showBadgeOnlyNotification("update-failed"); + is( + menuButton.getAttribute("badge-status"), + "update-failed", + "Should have update-failed badge status" + ); + + AppMenuNotifications.removeNotification(/^update-/); + is( + menuButton.getAttribute("badge-status"), + "fxa-needs-authentication", + "Should have fxa-needs-authentication badge status" + ); + + AppMenuNotifications.removeNotification(/^fxa-/); + is( + menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + + await gCUITestUtils.openMainMenu(); + is( + menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status (Hamburger menu opened)" + ); + await gCUITestUtils.hideMainMenu(); + + AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication"); + AppMenuNotifications.showBadgeOnlyNotification("update-succeeded"); + AppMenuNotifications.removeNotification(/.*/); + is( + menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + }); +}); + +/** + * Tests that non-badges also operate like a stack. + */ +add_task(async function testMultipleNonBadges() { + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + let updateManualAction = { + called: false, + callback: () => { + updateManualAction.called = true; + }, + }; + let updateRestartAction = { + called: false, + callback: () => { + updateRestartAction.called = true; + }, + }; + + AppMenuNotifications.showNotification("update-manual", updateManualAction); + + let notifications; + let doorhanger; + + isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing."); + notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + AppMenuNotifications.showNotification( + "update-restart", + updateRestartAction + ); + + isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing."); + notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-restart-notification", + "PanelUI is displaying the update-restart notification." + ); + + let secondaryActionButton = doorhanger.secondaryButton; + secondaryActionButton.click(); + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-restart", + "update-restart badge is displaying on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-restart", + "update-restart badge is displaying on PanelUI button." + ); + let menuItem = PanelUI.mainView.querySelector(".panel-banner-item"); + is( + menuItem.getAttribute("data-l10n-id"), + "appmenuitem-banner-update-restart", + "Showing correct label" + ); + is(menuItem.hidden, false, "update-restart menu item is showing."); + + menuItem.click(); + ok( + updateRestartAction.called, + "update-restart main action callback was called" + ); + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "update-manual badge is displaying on PanelUI button." + ); + + await gCUITestUtils.openMainMenu(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "update-manual badge is displaying on PanelUI button." + ); + is( + menuItem.getAttribute("data-l10n-id"), + "appmenuitem-banner-update-manual", + "Showing correct label" + ); + is(menuItem.hidden, false, "update-manual menu item is showing."); + + menuItem.click(); + ok( + updateManualAction.called, + "update-manual main action callback was called" + ); + }); +}); diff --git a/browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js b/browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js new file mode 100644 index 0000000000..ff4c10ee11 --- /dev/null +++ b/browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +/** + * The update banner should become visible when the badge-only notification is + * shown before opening the menu. + */ +add_task(async function testBannerVisibilityBeforeOpen() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + AppMenuNotifications.showBadgeOnlyNotification("update-restart"); + + let menuButton = newWin.document.getElementById("PanelUI-menu-button"); + let shown = BrowserTestUtils.waitForEvent( + newWin.PanelUI.mainView, + "ViewShown" + ); + menuButton.click(); + await shown; + + let banner = newWin.document.getElementById("appMenu-proton-update-banner"); + + let labelPromise = BrowserTestUtils.waitForMutationCondition( + banner, + { attributes: true, attributeFilter: ["label"] }, + () => banner.hasAttribute("label") + ); + + ok(!banner.hidden, "Update banner should be shown"); + + await labelPromise; + + ok(banner.getAttribute("label") != "", "Update banner should contain text"); + + AppMenuNotifications.removeNotification(/.*/); + + await BrowserTestUtils.closeWindow(newWin); +}); + +/** + * The update banner should become visible when the badge-only notification is + * shown during the menu is opened. + */ +add_task(async function testBannerVisibilityDuringOpen() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + let menuButton = newWin.document.getElementById("PanelUI-menu-button"); + let shown = BrowserTestUtils.waitForEvent( + newWin.PanelUI.mainView, + "ViewShown" + ); + menuButton.click(); + await shown; + + let banner = newWin.document.getElementById("appMenu-proton-update-banner"); + ok( + !banner.hasAttribute("label"), + "Update banner shouldn't contain text before notification" + ); + + let labelPromise = BrowserTestUtils.waitForMutationCondition( + banner, + { attributes: true, attributeFilter: ["label"] }, + () => banner.hasAttribute("label") + ); + + AppMenuNotifications.showNotification("update-restart"); + + ok(!banner.hidden, "Update banner should be shown"); + + await labelPromise; + + ok(banner.getAttribute("label") != "", "Update banner should contain text"); + + AppMenuNotifications.removeNotification(/.*/); + + await BrowserTestUtils.closeWindow(newWin); +}); + +/** + * The update banner should become visible when the badge-only notification is + * shown after opening/closing the menu, so that the DOM tree is there but + * the menu is closed. + */ +add_task(async function testBannerVisibilityAfterClose() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + let menuButton = newWin.document.getElementById("PanelUI-menu-button"); + let shown = BrowserTestUtils.waitForEvent( + newWin.PanelUI.mainView, + "ViewShown" + ); + menuButton.click(); + await shown; + + ok(newWin.PanelUI.mainView.hasAttribute("visible")); + + let banner = newWin.document.getElementById("appMenu-proton-update-banner"); + + ok(banner.hidden, "Update banner should be hidden before notification"); + ok( + !banner.hasAttribute("label"), + "Update banner shouldn't contain text before notification" + ); + + let labelPromise = BrowserTestUtils.waitForMutationCondition( + banner, + { attributes: true, attributeFilter: ["label"] }, + () => banner.hasAttribute("label") + ); + + let hidden = BrowserTestUtils.waitForCondition(() => { + return !newWin.PanelUI.mainView.hasAttribute("visible"); + }); + menuButton.click(); + await hidden; + + AppMenuNotifications.showBadgeOnlyNotification("update-restart"); + + shown = BrowserTestUtils.waitForEvent(newWin.PanelUI.mainView, "ViewShown"); + menuButton.click(); + await shown; + + ok(!banner.hidden, "Update banner should be shown"); + + await labelPromise; + + ok(banner.getAttribute("label") != "", "Update banner should contain text"); + + AppMenuNotifications.removeNotification(/.*/); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js new file mode 100644 index 0000000000..4b3340696b --- /dev/null +++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js @@ -0,0 +1,92 @@ +"use strict"; + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +add_task(async function testFullscreen() { + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let popuphiddenPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popuphidden" + ); + document.documentElement.focus(); + EventUtils.synthesizeKey("KEY_F11"); + await popuphiddenPromise; + await new Promise(executeSoon); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + FullScreen.showNavToolbox(); + is( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is displaying on PanelUI button." + ); + + let popupshownPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popupshown" + ); + EventUtils.synthesizeKey("KEY_F11"); + await popupshownPromise; + await new Promise(executeSoon); + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + isnot( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is not displaying on PanelUI button." + ); + + doorhanger.button.click(); + ok(mainActionCalled, "Main action callback was called"); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); +}); diff --git a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js new file mode 100644 index 0000000000..853c39e89f --- /dev/null +++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js @@ -0,0 +1,145 @@ +"use strict"; + +// This test tends to trigger a race in the fullscreen time telemetry, +// where the fullscreen enter and fullscreen exit events (which use the +// same histogram ID) overlap. That causes TelemetryStopwatch to log an +// error. +SimpleTest.ignoreAllUncaughtExceptions(true); + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +function waitForDocshellActivated() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + // Setting docshell activated/deactivated will trigger visibility state + // changes to relevant state ("visible" or "hidden"). AFAIK, there is no + // such event notifying docshell is being activated, so I use + // "visibilitychange" event rather than polling the isActive flag. + await ContentTaskUtils.waitForEvent( + content.document, + "visibilitychange", + true /* capture */, + aEvent => { + return content.browsingContext.isActive; + } + ); + }); +} + +function waitForFullscreen() { + return Promise.all([ + BrowserTestUtils.waitForEvent(window, "fullscreen"), + // In the platforms that support reporting occlusion state (e.g. Mac), + // enter/exit fullscreen mode will trigger docshell being set to non-activate + // and then set to activate back again. For those platforms, we should wait + // until the docshell has been activated again before starting next test, + // otherwise, the fullscreen request might be denied. + Services.appinfo.OS === "Darwin" + ? waitForDocshellActivated() + : Promise.resolve(), + ]); +} + +add_task(async function testFullscreen() { + if (Services.appinfo.OS !== "Darwin") { + await SpecialPowers.pushPrefEnv({ + set: [["browser.fullscreen.autohide", false]], + }); + } + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + await BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown"); + + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let fullscreenPromise = waitForFullscreen(); + EventUtils.synthesizeKey("KEY_F11"); + await fullscreenPromise; + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is still showing after entering fullscreen." + ); + + let popuphiddenPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popuphidden" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + content.document.documentElement.requestFullscreen(); + }); + await popuphiddenPromise; + await new Promise(executeSoon); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is hidden after entering DOM fullscreen." + ); + + let popupshownPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popupshown" + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + content.document.exitFullscreen(); + }); + await popupshownPromise; + await new Promise(executeSoon); + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is shown after exiting DOM fullscreen." + ); + isnot( + PanelUI.menuButton.getAttribute("badge-status"), + "update-manual", + "Badge is not displaying on PanelUI button." + ); + + doorhanger.button.click(); + ok(mainActionCalled, "Main action callback was called"); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + + fullscreenPromise = BrowserTestUtils.waitForEvent(window, "fullscreen"); + EventUtils.synthesizeKey("KEY_F11"); + await fullscreenPromise; +}); diff --git a/browser/components/customizableui/test/browser_panelUINotifications_modals.js b/browser/components/customizableui/test/browser_panelUINotifications_modals.js new file mode 100644 index 0000000000..87be14fcee --- /dev/null +++ b/browser/components/customizableui/test/browser_panelUINotifications_modals.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +add_task(async function testModals() { + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let popuphiddenPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popuphidden" + ); + + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); + Services.prompt.asyncAlert( + window.browsingContext, + Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, + "Test alert", + "Test alert description" + ); + await popuphiddenPromise; + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + + let popupshownPromise = BrowserTestUtils.waitForEvent( + PanelUI.notificationPanel, + "popupshown" + ); + + await dialogPromise; + await popupshownPromise; + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + + doorhanger.button.click(); + ok(mainActionCalled, "Main action callback was called"); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); +}); diff --git a/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js new file mode 100644 index 0000000000..29470a4cbb --- /dev/null +++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js @@ -0,0 +1,206 @@ +"use strict"; + +const { AppMenuNotifications } = ChromeUtils.importESModule( + "resource://gre/modules/AppMenuNotifications.sys.mjs" +); + +/** + * Tests that when we try to show a notification in a background window, it + * does not display until the window comes back into the foreground. However, + * it should display a badge. + */ +add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() { + let options = { + gBrowser: window.gBrowser, + url: "about:blank", + }; + + await BrowserTestUtils.withNewTab(options, async function (browser) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + let mainActionCalled = false; + let mainAction = { + callback: () => { + mainActionCalled = true; + }, + }; + AppMenuNotifications.showNotification("update-manual", mainAction); + is( + PanelUI.notificationPanel.state, + "closed", + "The background window's doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + true, + "The background window has a badge." + ); + + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); + isnot( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is showing." + ); + let notifications = [...PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + is( + doorhanger.id, + "appMenu-update-manual-notification", + "PanelUI is displaying the update-manual notification." + ); + + let button = doorhanger.button; + button.click(); + + ok(mainActionCalled, "Main action callback was called"); + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + }); +}); + +/** + * Tests that when we try to show a notification in a background window and in + * a foreground window, if the foreground window's main action is called, the + * background window's doorhanger will be removed. + */ +add_task( + async function testBackgroundWindowNotificationsAreRemovedByForeground() { + let options = { + gBrowser: window.gBrowser, + url: "about:blank", + }; + + await BrowserTestUtils.withNewTab(options, async function (browser) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + AppMenuNotifications.showNotification("update-manual", { callback() {} }); + let notifications = [...win.PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + doorhanger.button.click(); + + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); + + is( + PanelUI.notificationPanel.state, + "closed", + "update-manual doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + false, + "Should not have a badge status" + ); + }); + } +); + +/** + * Tests that when we try to show a notification in a background window and in + * a foreground window, if the foreground window's doorhanger is dismissed, + * the background window's doorhanger will also be dismissed once the window + * regains focus. + */ +add_task( + async function testBackgroundWindowNotificationsAreDismissedByForeground() { + let options = { + gBrowser: window.gBrowser, + url: "about:blank", + }; + + await BrowserTestUtils.withNewTab(options, async function (browser) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + AppMenuNotifications.showNotification("update-manual", { callback() {} }); + let notifications = [...win.PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + let button = doorhanger.secondaryButton; + button.click(); + + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); + + is( + PanelUI.notificationPanel.state, + "closed", + "The background window's doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + true, + "The dismissed notification should still have a badge status" + ); + + AppMenuNotifications.removeNotification(/.*/); + }); + } +); + +/** + * Tests that when we open a new window while a notification is showing, the + * notification also shows on the new window. + */ +add_task(async function testOpenWindowAfterShowingNotification() { + AppMenuNotifications.showNotification("update-manual", { callback() {} }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(win); + let notifications = [...win.PanelUI.notificationPanel.children].filter( + n => !n.hidden + ); + is( + notifications.length, + 1, + "PanelUI doorhanger is only displaying one notification." + ); + let doorhanger = notifications[0]; + let button = doorhanger.secondaryButton; + button.click(); + + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); + + is( + PanelUI.notificationPanel.state, + "closed", + "The background window's doorhanger is closed." + ); + is( + PanelUI.menuButton.hasAttribute("badge-status"), + true, + "The dismissed notification should still have a badge status" + ); + + AppMenuNotifications.removeNotification(/.*/); +}); diff --git a/browser/components/customizableui/test/browser_panel_keyboard_navigation.js b/browser/components/customizableui/test/browser_panel_keyboard_navigation.js new file mode 100644 index 0000000000..9b5ad48cce --- /dev/null +++ b/browser/components/customizableui/test/browser_panel_keyboard_navigation.js @@ -0,0 +1,326 @@ +"use strict"; + +/** + * Test keyboard navigation in the app menu panel. + */ + +const kHelpButtonId = "appMenu-help-button2"; + +function getEnabledNavigableElementsForView(panelView) { + return Array.from( + panelView.querySelectorAll("button,toolbarbutton,menulist,.text-link") + ).filter(element => { + let bounds = element.getBoundingClientRect(); + return !element.disabled && bounds.width > 0 && bounds.height > 0; + }); +} + +add_task(async function testUpDownKeys() { + await gCUITestUtils.openMainMenu(); + + let buttons = getEnabledNavigableElementsForView(PanelUI.mainView); + + for (let button of buttons) { + if (button.disabled) { + continue; + } + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + document.commandDispatcher.focusedElement, + button, + "The correct button should be focused after navigating downward" + ); + } + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + document.commandDispatcher.focusedElement, + buttons[0], + "Pressing upwards should cycle around and select the first button again" + ); + + for (let i = buttons.length - 1; i >= 0; --i) { + let button = buttons[i]; + if (button.disabled) { + continue; + } + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + document.commandDispatcher.focusedElement, + button, + "The first button should be focused after navigating upward" + ); + } + + await gCUITestUtils.hideMainMenu(); +}); + +add_task(async function testHomeEndKeys() { + await gCUITestUtils.openMainMenu(); + + let buttons = getEnabledNavigableElementsForView(PanelUI.mainView); + let enabledButtons = buttons.filter(btn => !btn.disabled); + let firstButton = enabledButtons[0]; + let lastButton = enabledButtons.pop(); + + Assert.ok(firstButton != lastButton, "There is more than one button"); + + EventUtils.synthesizeKey("KEY_End"); + Assert.equal( + document.commandDispatcher.focusedElement, + lastButton, + "The last button should be focused after pressing End" + ); + + EventUtils.synthesizeKey("KEY_Home"); + Assert.equal( + document.commandDispatcher.focusedElement, + firstButton, + "The first button should be focused after pressing Home" + ); + + await gCUITestUtils.hideMainMenu(); +}); + +add_task(async function testEnterKeyBehaviors() { + await gCUITestUtils.openMainMenu(); + + let buttons = getEnabledNavigableElementsForView(PanelUI.mainView); + + // Navigate to the 'Help' button, which points to a subview. + EventUtils.synthesizeKey("KEY_ArrowUp"); + let focusedElement = document.commandDispatcher.focusedElement; + Assert.equal( + focusedElement, + buttons[buttons.length - 1], + "The last button should be focused after navigating upward" + ); + + // Make sure the Help button is in focus. + while ( + !focusedElement || + !focusedElement.id || + focusedElement.id != kHelpButtonId + ) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + focusedElement = document.commandDispatcher.focusedElement; + } + EventUtils.synthesizeKey("KEY_Enter"); + + let helpView = document.getElementById("PanelUI-helpView"); + await BrowserTestUtils.waitForEvent(helpView, "ViewShown"); + + let helpButtons = getEnabledNavigableElementsForView(helpView); + Assert.ok( + helpButtons[0].classList.contains("subviewbutton-back"), + "First button in help view should be a back button" + ); + + // For posterity, check navigating the subview using up/ down arrow keys as well. + // When opening a subview, the first control *after* the Back button gets + // focus. + EventUtils.synthesizeKey("KEY_ArrowUp"); + focusedElement = document.commandDispatcher.focusedElement; + Assert.equal( + focusedElement, + helpButtons[0], + "The Back button should be focused after navigating upward" + ); + for (let i = helpButtons.length - 1; i >= 0; --i) { + let button = helpButtons[i]; + if (button.disabled) { + continue; + } + EventUtils.synthesizeKey("KEY_ArrowUp"); + focusedElement = document.commandDispatcher.focusedElement; + Assert.equal( + focusedElement, + button, + "The previous button should be focused after navigating upward" + ); + } + + // Make sure the back button is in focus again. + while (focusedElement != helpButtons[0]) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + focusedElement = document.commandDispatcher.focusedElement; + } + + // The first button is the back button. Hittin Enter should navigate us back. + let promise = BrowserTestUtils.waitForEvent(PanelUI.mainView, "ViewShown"); + EventUtils.synthesizeKey("KEY_Enter"); + await promise; + + // Let's test a 'normal' command button. + focusedElement = document.commandDispatcher.focusedElement; + const kFindButtonId = "appMenu-find-button2"; + while ( + !focusedElement || + !focusedElement.id || + focusedElement.id != kFindButtonId + ) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + focusedElement = document.commandDispatcher.focusedElement; + } + let findBarPromise = gBrowser.isFindBarInitialized() + ? null + : BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "TabFindInitialized"); + Assert.equal( + focusedElement.id, + kFindButtonId, + "Find button should be selected" + ); + + await gCUITestUtils.hidePanelMultiView(PanelUI.panel, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + + await findBarPromise; + Assert.ok(!gFindBar.hidden, "Findbar should have opened"); + gFindBar.close(); +}); + +add_task(async function testLeftRightKeys() { + await gCUITestUtils.openMainMenu(); + + // Navigate to the 'Help' button, which points to a subview. + let focusedElement = document.commandDispatcher.focusedElement; + while ( + !focusedElement || + !focusedElement.id || + focusedElement.id != kHelpButtonId + ) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + focusedElement = document.commandDispatcher.focusedElement; + } + Assert.equal( + focusedElement.id, + kHelpButtonId, + "The last button should be focused after navigating upward" + ); + + // Hitting ArrowRight on a button that points to a subview should navigate us + // there. + EventUtils.synthesizeKey("KEY_ArrowRight"); + let helpView = document.getElementById("PanelUI-helpView"); + await BrowserTestUtils.waitForEvent(helpView, "ViewShown"); + + // Hitting ArrowLeft should navigate us back. + let promise = BrowserTestUtils.waitForEvent(PanelUI.mainView, "ViewShown"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await promise; + + focusedElement = document.commandDispatcher.focusedElement; + Assert.equal( + focusedElement.id, + kHelpButtonId, + "Help button should be focused again now that we're back in the main view" + ); + + await gCUITestUtils.hideMainMenu(); +}); + +add_task(async function testTabKey() { + await gCUITestUtils.openMainMenu(); + + let buttons = getEnabledNavigableElementsForView(PanelUI.mainView); + + for (let button of buttons) { + if (button.disabled) { + continue; + } + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + document.commandDispatcher.focusedElement, + button, + "The correct button should be focused after tabbing" + ); + } + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + document.commandDispatcher.focusedElement, + buttons[0], + "Pressing tab should cycle around and select the first button again" + ); + + for (let i = buttons.length - 1; i >= 0; --i) { + let button = buttons[i]; + if (button.disabled) { + continue; + } + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + Assert.equal( + document.commandDispatcher.focusedElement, + button, + "The correct button should be focused after shift + tabbing" + ); + } + + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + Assert.equal( + document.commandDispatcher.focusedElement, + buttons[buttons.length - 1], + "Pressing shift + tab should cycle around and select the last button again" + ); + + await gCUITestUtils.hideMainMenu(); +}); + +add_task(async function testInterleavedTabAndArrowKeys() { + await gCUITestUtils.openMainMenu(); + + let buttons = getEnabledNavigableElementsForView(PanelUI.mainView); + let tab = false; + + for (let button of buttons) { + if (button.disabled) { + continue; + } + if (tab) { + EventUtils.synthesizeKey("KEY_Tab"); + } else { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + tab = !tab; + } + + Assert.equal( + document.commandDispatcher.focusedElement, + buttons[buttons.length - 1], + "The last button should be focused after a mix of Tab and ArrowDown" + ); + + await gCUITestUtils.hideMainMenu(); +}); + +add_task(async function testSpaceDownAfterTabNavigation() { + await gCUITestUtils.openMainMenu(); + + let buttons = getEnabledNavigableElementsForView(PanelUI.mainView); + let button; + + for (button of buttons) { + if (button.disabled) { + continue; + } + EventUtils.synthesizeKey("KEY_Tab"); + if (button.id == kHelpButtonId) { + break; + } + } + + Assert.equal( + document.commandDispatcher.focusedElement, + button, + "Help button should be focused after tabbing to it." + ); + + // Pressing down space on a button that points to a subview should navigate us + // there, before keyup. + EventUtils.synthesizeKey(" ", { type: "keydown" }); + let helpView = document.getElementById("PanelUI-helpView"); + await BrowserTestUtils.waitForEvent(helpView, "ViewShown"); + + await gCUITestUtils.hideMainMenu(); +}); diff --git a/browser/components/customizableui/test/browser_panel_locationSpecific.js b/browser/components/customizableui/test/browser_panel_locationSpecific.js new file mode 100644 index 0000000000..1668751d9f --- /dev/null +++ b/browser/components/customizableui/test/browser_panel_locationSpecific.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * This test creates multiple panels, one that has been tagged as location specific + * and one that isn't. When the location changes, the specific panel should close. + * The non-specific panel should remain open. + * + */ + +add_task(async function () { + let specificPanel = document.createXULElement("panel"); + specificPanel.setAttribute("locationspecific", "true"); + specificPanel.setAttribute("noautohide", "true"); + specificPanel.style.height = "100px"; + specificPanel.style.width = "100px"; + + let generalPanel = document.createXULElement("panel"); + generalPanel.setAttribute("noautohide", "true"); + generalPanel.style.height = "100px"; + generalPanel.style.width = "100px"; + + let anchor = document.getElementById(CustomizableUI.AREA_NAVBAR); + + anchor.appendChild(specificPanel); + anchor.appendChild(generalPanel); + is(specificPanel.state, "closed", "specificPanel starts as closed"); + is(generalPanel.state, "closed", "generalPanel starts as closed"); + + let specificPanelPromise = BrowserTestUtils.waitForEvent( + specificPanel, + "popupshown" + ); + + specificPanel.openPopupAtScreen(0, 0); + + await specificPanelPromise; + is(specificPanel.state, "open", "specificPanel has been opened"); + + let generalPanelPromise = BrowserTestUtils.waitForEvent( + generalPanel, + "popupshown" + ); + + generalPanel.openPopupAtScreen(100, 0); + + await generalPanelPromise; + is(generalPanel.state, "open", "generalPanel has been opened"); + + let specificPanelHiddenPromise = BrowserTestUtils.waitForEvent( + specificPanel, + "popuphidden" + ); + + // Simulate a location change, and check which panel closes. + let browser = gBrowser.selectedBrowser; + let loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.loadURIString(browser, "http://mochi.test:8888/#0"); + await loaded; + + await specificPanelHiddenPromise; + + is( + specificPanel.state, + "closed", + "specificPanel panel is closed after location change" + ); + is( + generalPanel.state, + "open", + "generalPanel is still open after location change" + ); + + specificPanel.remove(); + generalPanel.remove(); +}); diff --git a/browser/components/customizableui/test/browser_panel_toggle.js b/browser/components/customizableui/test/browser_panel_toggle.js new file mode 100644 index 0000000000..cba441e8e5 --- /dev/null +++ b/browser/components/customizableui/test/browser_panel_toggle.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test opening and closing the menu panel UI. + */ + +// Show and hide the menu panel programmatically without an event (like UITour.sys.mjs would) +add_task(async function () { + await gCUITestUtils.openMainMenu(); + + is( + PanelUI.panel.getAttribute("panelopen"), + "true", + "Check that panel has panelopen attribute" + ); + is(PanelUI.panel.state, "open", "Check that panel state is 'open'"); + + await gCUITestUtils.hideMainMenu(); + + ok( + !PanelUI.panel.hasAttribute("panelopen"), + "Check that panel doesn't have the panelopen attribute" + ); + is(PanelUI.panel.state, "closed", "Check that panel state is 'closed'"); +}); + +// Toggle the menu panel open and closed +add_task(async function () { + await gCUITestUtils.openPanelMultiView(PanelUI.panel, PanelUI.mainView, () => + PanelUI.toggle({ type: "command" }) + ); + + is( + PanelUI.panel.getAttribute("panelopen"), + "true", + "Check that panel has panelopen attribute" + ); + is(PanelUI.panel.state, "open", "Check that panel state is 'open'"); + + await gCUITestUtils.hidePanelMultiView(PanelUI.panel, () => + PanelUI.toggle({ type: "command" }) + ); + + ok( + !PanelUI.panel.hasAttribute("panelopen"), + "Check that panel doesn't have the panelopen attribute" + ); + is(PanelUI.panel.state, "closed", "Check that panel state is 'closed'"); +}); diff --git a/browser/components/customizableui/test/browser_proton_moreTools_panel.js b/browser/components/customizableui/test/browser_proton_moreTools_panel.js new file mode 100644 index 0000000000..d78cf946f8 --- /dev/null +++ b/browser/components/customizableui/test/browser_proton_moreTools_panel.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "DevToolsStartup", () => { + return Cc["@mozilla.org/devtools/startup-clh;1"].getService( + Ci.nsICommandLineHandler + ).wrappedJSObject; +}); + +// Test activating the developer button shows the More Tools panel. +add_task(async function testDevToolsPanelInToolbar() { + // We need to force DevToolsStartup to rebuild the developer tool toggle so that + // proton prefs are applied to the new browser window for this test. + DevToolsStartup.developerToggleCreated = false; + CustomizableUI.destroyWidget("developer-button"); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + CustomizableUI.addWidgetToArea( + "developer-button", + CustomizableUI.AREA_NAVBAR + ); + + // Test the developer tools panel is showing. + let button = document.getElementById("developer-button"); + let devToolsView = PanelMultiView.getViewNode( + document, + "PanelUI-developer-tools" + ); + let devToolsShownPromise = BrowserTestUtils.waitForEvent( + devToolsView, + "ViewShown" + ); + + EventUtils.synthesizeMouseAtCenter(button, {}); + await devToolsShownPromise; + ok(true, "Dev Tools view is showing"); + is( + devToolsView.children.length, + 1, + "Dev tools subview is the only child of panel" + ); + is( + devToolsView.children[0].id, + "PanelUI-developer-tools-view", + "Dev tools child has correct id" + ); + + // Cleanup + await BrowserTestUtils.closeWindow(win); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_proton_toolbar_hide_toolbarbuttons.js b/browser/components/customizableui/test/browser_proton_toolbar_hide_toolbarbuttons.js new file mode 100644 index 0000000000..ef0a44d6b1 --- /dev/null +++ b/browser/components/customizableui/test/browser_proton_toolbar_hide_toolbarbuttons.js @@ -0,0 +1,287 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "HomePage", + "resource:///modules/HomePage.jsm" +); + +const kPrefProtonToolbarVersion = "browser.proton.toolbar.version"; +const kPrefHomeButtonUsed = "browser.engagement.home-button.has-used"; +const kPrefLibraryButtonUsed = "browser.engagement.library-button.has-used"; +const kPrefSidebarButtonUsed = "browser.engagement.sidebar-button.has-used"; + +async function testToolbarButtons(aActions) { + let { + shouldRemoveHomeButton, + shouldRemoveLibraryButton, + shouldRemoveSidebarButton, + shouldUpdateVersion, + } = aActions; + const defaultPlacements = [ + "back-button", + "forward-button", + "stop-reload-button", + "home-button", + "customizableui-special-spring1", + "urlbar-container", + "customizableui-special-spring2", + "downloads-button", + "library-button", + "sidebar-button", + "fxa-toolbar-menu-button", + ]; + let oldState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + + Assert.equal( + Services.prefs.getIntPref(kPrefProtonToolbarVersion), + 0, + "Toolbar proton version is 0" + ); + + let CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + ); + + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + placements: { + "nav-bar": defaultPlacements, + }, + }); + CustomizableUIInternal._updateForNewProtonVersion(); + + let navbarPlacements = + CustomizableUI.getTestOnlyInternalProp("gSavedState").placements["nav-bar"]; + let includesHomeButton = navbarPlacements.includes("home-button"); + let includesLibraryButton = navbarPlacements.includes("library-button"); + let includesSidebarButton = navbarPlacements.includes("sidebar-button"); + + Assert.equal( + !includesHomeButton, + shouldRemoveHomeButton, + "Correctly handles home button" + ); + Assert.equal( + !includesLibraryButton, + shouldRemoveLibraryButton, + "Correctly handles library button" + ); + Assert.equal( + !includesSidebarButton, + shouldRemoveSidebarButton, + "Correctly handles sidebar button" + ); + + let toolbarVersion = Services.prefs.getIntPref(kPrefProtonToolbarVersion); + if (shouldUpdateVersion) { + Assert.ok(toolbarVersion >= 1, "Toolbar proton version updated"); + } else { + Assert.ok(toolbarVersion == 0, "Toolbar proton version not updated"); + } + + // Cleanup + CustomizableUI.setTestOnlyInternalProp("gSavedState", oldState); +} + +/** + * Checks that the home button is removed from the nav-bar under + * these conditions: proton must be enabled, the toolbar engagement + * pref is false, and the homepage is about:home or about:blank. + * Otherwise, the home button should remain if it was previously + * in the navbar. + * Also checks that the library button is removed from the nav-bar + * if proton is enabled and the toolbar engagement pref is false. + */ +add_task(async function testButtonRemoval() { + // Ensure the engagement prefs are set to their default values + await SpecialPowers.pushPrefEnv({ + set: [ + [kPrefHomeButtonUsed, false], + [kPrefLibraryButtonUsed, false], + [kPrefSidebarButtonUsed, false], + ], + }); + + let tests = [ + // Proton enabled without home and library engagement + { + prefs: [], + actions: { + shouldRemoveHomeButton: true, + shouldRemoveLibraryButton: true, + shouldRemoveSidebarButton: true, + shouldUpdateVersion: true, + }, + }, + // Proton enabled with home engagement + { + prefs: [[kPrefHomeButtonUsed, true]], + actions: { + shouldRemoveHomeButton: false, + shouldRemoveLibraryButton: true, + shouldRemoveSidebarButton: true, + shouldUpdateVersion: true, + }, + }, + // Proton enabled with custom homepage + { + prefs: [], + actions: { + shouldRemoveHomeButton: false, + shouldRemoveLibraryButton: true, + shouldRemoveSidebarButton: true, + shouldUpdateVersion: true, + }, + async fn() { + HomePage.safeSet("https://example.com"); + }, + }, + // Proton enabled with library engagement + { + prefs: [[kPrefLibraryButtonUsed, true]], + actions: { + shouldRemoveHomeButton: true, + shouldRemoveLibraryButton: false, + shouldRemoveSidebarButton: true, + shouldUpdateVersion: true, + }, + }, + // Proton enabled with sidebar engagement + { + prefs: [[kPrefSidebarButtonUsed, true]], + actions: { + shouldRemoveHomeButton: true, + shouldRemoveLibraryButton: true, + shouldRemoveSidebarButton: false, + shouldUpdateVersion: true, + }, + }, + ]; + + for (let test of tests) { + await SpecialPowers.pushPrefEnv({ + set: [[kPrefProtonToolbarVersion, 0], ...test.prefs], + }); + if (test.fn) { + await test.fn(); + } + testToolbarButtons(test.actions); + HomePage.reset(); + await SpecialPowers.popPrefEnv(); + } +}); + +/** + * Checks that a null saved state (new profile) does not prevent migration. + */ +add_task(async function testNullSavedState() { + await SpecialPowers.pushPrefEnv({ + set: [[kPrefProtonToolbarVersion, 0]], + }); + let oldState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + + Assert.equal( + Services.prefs.getIntPref(kPrefProtonToolbarVersion), + 0, + "Toolbar proton version is 0" + ); + + let CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + ); + CustomizableUIInternal.initialize(); + + Assert.ok( + Services.prefs.getIntPref(kPrefProtonToolbarVersion) >= 1, + "Toolbar proton version updated" + ); + let navbarPlacements = CustomizableUI.getTestOnlyInternalProp("gAreas") + .get("nav-bar") + .get("defaultPlacements"); + Assert.ok( + !navbarPlacements.includes("home-button"), + "Home button isn't included by default" + ); + Assert.ok( + !navbarPlacements.includes("library-button"), + "Library button isn't included by default" + ); + Assert.ok( + !navbarPlacements.includes("sidebar-button"), + "Sidebar button isn't included by default" + ); + + // Cleanup + CustomizableUI.setTestOnlyInternalProp("gSavedState", oldState); + await SpecialPowers.popPrefEnv(); + // Re-initialize to prevent future test failures + CustomizableUIInternal.initialize(); +}); + +/** + * Checks that a saved state that is missing nav-bar placements does not prevent migration. + */ +add_task(async function testNoNavbarPlacements() { + await SpecialPowers.pushPrefEnv({ + set: [[kPrefProtonToolbarVersion, 0]], + }); + + let oldState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + + Assert.equal( + Services.prefs.getIntPref(kPrefProtonToolbarVersion), + 0, + "Toolbar proton version is 0" + ); + + let CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + ); + + CustomizableUI.setTestOnlyInternalProp("gSavedState", { + placements: { "widget-overflow-fixed-list": [] }, + }); + CustomizableUIInternal._updateForNewProtonVersion(); + + Assert.ok(true, "_updateForNewProtonVersion didn't throw"); + + // Cleanup + CustomizableUI.setTestOnlyInternalProp("gSavedState", oldState); + + await SpecialPowers.popPrefEnv(); +}); + +/** + * Checks that a saved state that is missing the placements value does not prevent migration. + */ +add_task(async function testNullPlacements() { + await SpecialPowers.pushPrefEnv({ + set: [[kPrefProtonToolbarVersion, 0]], + }); + + let oldState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + + Assert.equal( + Services.prefs.getIntPref(kPrefProtonToolbarVersion), + 0, + "Toolbar proton version is 0" + ); + + let CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" + ); + + CustomizableUI.setTestOnlyInternalProp("gSavedState", {}); + CustomizableUIInternal._updateForNewProtonVersion(); + + Assert.ok(true, "_updateForNewProtonVersion didn't throw"); + + // Cleanup + CustomizableUI.setTestOnlyInternalProp("gSavedState", oldState); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/customizableui/test/browser_registerArea.js b/browser/components/customizableui/test/browser_registerArea.js new file mode 100644 index 0000000000..2900c9eb8b --- /dev/null +++ b/browser/components/customizableui/test/browser_registerArea.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that a toolbar area can be registered with overflowable: false + * as one of its properties, and this results in a non-overflowable + * toolbar. + */ +add_task(async function test_overflowable_false() { + registerCleanupFunction(removeCustomToolbars); + + const kToolbarId = "no-overflow-toolbar"; + createToolbarWithPlacements(kToolbarId, ["spring"], { + overflowable: false, + }); + + let node = CustomizableUI.getWidget(kToolbarId).forWindow(window).node; + Assert.ok( + !node.hasAttribute("overflowable"), + "Toolbar should not be overflowable" + ); + Assert.ok( + !node.overflowable, + "OverflowableToolbar instance should not have been created." + ); +}); diff --git a/browser/components/customizableui/test/browser_reload_tab.js b/browser/components/customizableui/test/browser_reload_tab.js new file mode 100644 index 0000000000..b12f5ee0e9 --- /dev/null +++ b/browser/components/customizableui/test/browser_reload_tab.js @@ -0,0 +1,99 @@ +"use strict"; + +/** + * Check that customize mode doesn't break when its tab is reloaded. + */ +add_task(async function reload_tab() { + let initialTab = gBrowser.selectedTab; + let customizeTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + gCustomizeMode.setTab(customizeTab); + let customizationContainer = document.getElementById( + "customization-container" + ); + + is( + customizationContainer.clientWidth, + 0, + "Customization container shouldn't be visible (X)" + ); + is( + customizationContainer.clientHeight, + 0, + "Customization container shouldn't be visible (Y)" + ); + + let customizePromise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + gCustomizeMode.enter(); + await customizePromise; + + let tabReloaded = new Promise(resolve => { + gBrowser.addTabsProgressListener({ + async onLocationChange(aBrowser) { + if (customizeTab.linkedBrowser == aBrowser) { + gBrowser.removeTabsProgressListener(this); + await Promise.resolve(); + resolve(); + } + }, + }); + }); + gBrowser.reloadTab(customizeTab); + await tabReloaded; + + is( + gBrowser.getIcon(customizeTab), + "chrome://browser/skin/customize.svg", + "Tab should have customize icon" + ); + is( + customizeTab.getAttribute("customizemode"), + "true", + "Tab should be in customize mode" + ); + ok( + customizationContainer.clientWidth > 0, + "Customization container should be visible (X)" + ); + ok( + customizationContainer.clientHeight > 0, + "Customization container should be visible (Y)" + ); + + customizePromise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "aftercustomization" + ); + await BrowserTestUtils.switchTab(gBrowser, initialTab); + await customizePromise; + + customizePromise = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + await BrowserTestUtils.switchTab(gBrowser, customizeTab); + await customizePromise; + + is( + gBrowser.getIcon(customizeTab), + "chrome://browser/skin/customize.svg", + "Tab should still have customize icon" + ); + is( + customizeTab.getAttribute("customizemode"), + "true", + "Tab should still be in customize mode" + ); + ok( + customizationContainer.clientWidth > 0, + "Customization container should still be visible (X)" + ); + ok( + customizationContainer.clientHeight > 0, + "Customization container should still be visible (Y)" + ); + + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_remote_attribute.js b/browser/components/customizableui/test/browser_remote_attribute.js new file mode 100644 index 0000000000..543e62e2bc --- /dev/null +++ b/browser/components/customizableui/test/browser_remote_attribute.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * These tests check that the remote attribute is true for remote panels. + * This attribute is needed for Mac to properly render the panel. + */ +add_task(async function check_remote_attribute() { + // The panel is created on the fly, so we can't simply wait for focus + // inside it. + //let pocketPanelShown = BrowserTestUtils.waitForEvent( + // document, + // "popupshown", + // true + //); + let pocketPanelShown = popupShown(document); + // Using Pocket panel as it's an available remote panel. + let pocketButton = document.getElementById("save-to-pocket-button"); + pocketButton.click(); + await pocketPanelShown; + + let pocketPanel = document.getElementById("customizationui-widget-panel"); + is( + pocketPanel.getAttribute("remote"), + "true", + "Pocket panel has remote attribute" + ); + + // Close panel and cleanup. + let pocketPanelHidden = popupHidden(pocketPanel); + pocketPanel.hidePopup(); + await pocketPanelHidden; +}); + +add_task(async function check_remote_attribute_overflow() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + let overflowPanel = win.document.getElementById("widget-overflow"); + overflowPanel.setAttribute("animate", "false"); + + // Force a narrow window to get an overflow toolbar. + win.resizeTo(kForceOverflowWidthPx, win.outerHeight); + let navbar = win.document.getElementById(CustomizableUI.AREA_NAVBAR); + await TestUtils.waitForCondition(() => navbar.hasAttribute("overflowing")); + + // Open the overflow panel view. + let overflowPanelShown = popupShown(overflowPanel); + let overflowPanelButton = win.document.getElementById( + "nav-bar-overflow-button" + ); + overflowPanelButton.click(); + await overflowPanelShown; + + // Using Pocket panel as it's an available remote panel. + let pocketButton = win.document.getElementById("save-to-pocket-button"); + pocketButton.click(); + await BrowserTestUtils.waitForEvent(win.document, "ViewShown"); + + is( + overflowPanel.getAttribute("remote"), + "true", + "Pocket overflow panel has remote attribute" + ); + + // Close panel and cleanup. + let overflowPanelHidden = popupHidden(overflowPanel); + overflowPanel.hidePopup(); + await overflowPanelHidden; + overflowPanel.removeAttribute("animate"); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/customizableui/test/browser_remote_tabs_button.js b/browser/components/customizableui/test/browser_remote_tabs_button.js new file mode 100644 index 0000000000..094335d4b1 --- /dev/null +++ b/browser/components/customizableui/test/browser_remote_tabs_button.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +"use strict"; + +let { Service } = ChromeUtils.importESModule( + "resource://services-sync/service.sys.mjs" +); +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +let getState; +let originalSync; +let syncWasCalled = false; + +// TODO: This test should probably be re-written, we don't really test much here. +add_task(async function testSyncRemoteTabsButtonFunctionality() { + info("Test the Sync Remote Tabs button in the panel"); + storeInitialValues(); + mockFunctions(); + + // Force UI update. + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + + // add the sync remote tabs button to the panel + CustomizableUI.addWidgetToArea( + "sync-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + // check the button's functionality + await document.getElementById("nav-bar").overflowable.show(); + info("The panel menu was opened"); + + let syncRemoteTabsBtn = document.getElementById("sync-button"); + ok( + syncRemoteTabsBtn, + "The sync remote tabs button was added to the Panel Menu" + ); + // click the button - the panel should open. + syncRemoteTabsBtn.click(); + let remoteTabsPanel = document.getElementById("PanelUI-remotetabs"); + let viewShown = BrowserTestUtils.waitForEvent(remoteTabsPanel, "ViewShown"); + await viewShown; + ok(remoteTabsPanel.getAttribute("visible"), "Sync Panel is in view"); + + // Find and click the "setup" button. + let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow"); + syncNowButton.click(); + info("The sync now button was clicked"); + + await TestUtils.waitForCondition(() => syncWasCalled); + + // We need to stop the Syncing animation manually otherwise the button + // will be disabled at the beginning of a next test. + gSync._onActivityStop(); +}); + +add_task(async function asyncCleanup() { + // reset the panel UI to the default state + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "The panel UI is in default state again."); + + if (isOverflowOpen()) { + let panelHidePromise = promiseOverflowHidden(window); + PanelUI.overflowPanel.hidePopup(); + await panelHidePromise; + } + + restoreValues(); +}); + +function mockFunctions() { + // mock UIState.get() + UIState.get = () => ({ + status: UIState.STATUS_SIGNED_IN, + lastSync: new Date(), + email: "user@mozilla.com", + }); + + Service.sync = mocked_sync; +} + +function mocked_sync() { + syncWasCalled = true; +} + +function restoreValues() { + UIState.get = getState; + Service.sync = originalSync; +} + +function storeInitialValues() { + getState = UIState.get; + originalSync = Service.sync; +} diff --git a/browser/components/customizableui/test/browser_remove_customized_specials.js b/browser/components/customizableui/test/browser_remove_customized_specials.js new file mode 100644 index 0000000000..1f123d10cb --- /dev/null +++ b/browser/components/customizableui/test/browser_remove_customized_specials.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that after a reset, we can still drag special nodes in customize mode + */ +add_task(async function () { + await startCustomizing(); + CustomizableUI.addWidgetToArea("spring", "nav-bar", 5); + await gCustomizeMode.reset(); + let springs = document.querySelectorAll("#nav-bar toolbarspring"); + let lastSpring = springs[springs.length - 1]; + let expectedPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar"); + info("Placements before drag: " + expectedPlacements.join(",")); + let lastItem = document.getElementById( + expectedPlacements[expectedPlacements.length - 1] + ); + await waitForElementShown(lastItem); + simulateItemDrag(lastSpring, lastItem, "end"); + expectedPlacements.splice(expectedPlacements.indexOf(lastSpring.id), 1); + expectedPlacements.push(lastSpring.id); + let actualPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar"); + // Log these separately because Assert.deepEqual truncates the stringified versions... + info("Actual placements: " + actualPlacements.join(",")); + info("Expected placements: " + expectedPlacements.join(",")); + Assert.deepEqual( + expectedPlacements, + actualPlacements, + "Should be able to move spring" + ); + await gCustomizeMode.reset(); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_reset_builtin_widget_currentArea.js b/browser/components/customizableui/test/browser_reset_builtin_widget_currentArea.js new file mode 100644 index 0000000000..fa9e497734 --- /dev/null +++ b/browser/components/customizableui/test/browser_reset_builtin_widget_currentArea.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that if we move a non-default, but builtin, widget to another area, +// and then reset things, the currentArea is updated correctly. +add_task(async function reset_should_not_keep_currentArea() { + CustomizableUI.addWidgetToArea( + "save-page-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + // We can't check currentArea directly; check areaType which is based on it: + is( + CustomizableUI.getWidget("save-page-button").areaType, + CustomizableUI.TYPE_PANEL, + "Button should know it's in the overflow panel" + ); + CustomizableUI.reset(); + ok( + !CustomizableUI.getWidget("save-page-button").areaType, + "Button should know it's not in the overflow panel anymore" + ); +}); + +registerCleanupFunction(() => CustomizableUI.reset()); diff --git a/browser/components/customizableui/test/browser_reset_dom_events.js b/browser/components/customizableui/test/browser_reset_dom_events.js new file mode 100644 index 0000000000..2922fe481d --- /dev/null +++ b/browser/components/customizableui/test/browser_reset_dom_events.js @@ -0,0 +1,34 @@ +"use strict"; + +const widgetId = "import-button"; +const listener = { + _beforeCount: 0, + _afterCount: 0, + onWidgetBeforeDOMChange(node) { + if (node.id == widgetId) { + this._beforeCount++; + } + }, + onWidgetAfterDOMChange(node) { + if (node.id == widgetId) { + this._afterCount++; + } + }, +}; + +add_task(async function test_reset_dom_events() { + await startCustomizing(); + + CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_BOOKMARKS); + CustomizableUI.addListener(listener); + + info("Resetting"); + await gCustomizeMode.reset(); + + is(listener._beforeCount, 1, "Should've been notified of the mutation"); + is(listener._afterCount, 1, "Should've been notified of the mutation"); + + CustomizableUI.removeListener(listener); + + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_screenshot_button_disabled.js b/browser/components/customizableui/test/browser_screenshot_button_disabled.js new file mode 100644 index 0000000000..b8eca2b3d3 --- /dev/null +++ b/browser/components/customizableui/test/browser_screenshot_button_disabled.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +"use strict"; + +add_task(async function testScreenshotButtonPrefDisabled() { + info("Test the Screenshots widget not available"); + + Assert.ok( + Services.prefs.getBoolPref("extensions.screenshots.disabled"), + "Sceenshots feature is disabled" + ); + + CustomizableUI.addWidgetToArea( + "screenshot-button", + CustomizableUI.AREA_NAVBAR + ); + + let screenshotBtn = document.getElementById("screenshot-button"); + Assert.ok(!screenshotBtn, "Screenshot button is unavailable"); +}); diff --git a/browser/components/customizableui/test/browser_sidebar_toggle.js b/browser/components/customizableui/test/browser_sidebar_toggle.js new file mode 100644 index 0000000000..5742f368ee --- /dev/null +++ b/browser/components/customizableui/test/browser_sidebar_toggle.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +registerCleanupFunction(async function () { + await resetCustomization(); + + // Ensure sidebar is hidden after each test: + if (!document.getElementById("sidebar-box").hidden) { + SidebarUI.hide(); + } +}); + +var showSidebar = async function (win = window) { + let button = win.document.getElementById("sidebar-button"); + let sidebarFocusedPromise = BrowserTestUtils.waitForEvent( + win.document, + "SidebarFocused" + ); + EventUtils.synthesizeMouseAtCenter(button, {}, win); + await sidebarFocusedPromise; + ok(win.SidebarUI.isOpen, "Sidebar is opened"); + ok(button.hasAttribute("checked"), "Toolbar button is checked"); +}; + +var hideSidebar = async function (win = window) { + let button = win.document.getElementById("sidebar-button"); + EventUtils.synthesizeMouseAtCenter(button, {}, win); + ok(!win.SidebarUI.isOpen, "Sidebar is closed"); + ok(!button.hasAttribute("checked"), "Toolbar button isn't checked"); +}; + +// Check the sidebar widget shows the default items +add_task(async function () { + CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar"); + + await showSidebar(); + is(SidebarUI.currentID, "viewBookmarksSidebar", "Default sidebar selected"); + await SidebarUI.show("viewHistorySidebar"); + + await hideSidebar(); + await showSidebar(); + is(SidebarUI.currentID, "viewHistorySidebar", "Selected sidebar remembered"); + + await hideSidebar(); + let otherWin = await BrowserTestUtils.openNewBrowserWindow(); + await showSidebar(otherWin); + is( + otherWin.SidebarUI.currentID, + "viewHistorySidebar", + "Selected sidebar remembered across windows" + ); + await hideSidebar(otherWin); + + await BrowserTestUtils.closeWindow(otherWin); +}); diff --git a/browser/components/customizableui/test/browser_switch_to_customize_mode.js b/browser/components/customizableui/test/browser_switch_to_customize_mode.js new file mode 100644 index 0000000000..55e80d3517 --- /dev/null +++ b/browser/components/customizableui/test/browser_switch_to_customize_mode.js @@ -0,0 +1,53 @@ +"use strict"; + +add_task(async function () { + await startCustomizing(); + is(gBrowser.tabs.length, 2, "Should have 2 tabs"); + + let paletteKidCount = document.getElementById( + "customization-palette" + ).childElementCount; + let nonCustomizingTab = gBrowser.tabContainer.querySelector( + "tab:not([customizemode=true])" + ); + let finishedCustomizing = BrowserTestUtils.waitForEvent( + gNavToolbox, + "aftercustomization" + ); + await BrowserTestUtils.switchTab(gBrowser, nonCustomizingTab); + await finishedCustomizing; + + let startedCount = 0; + let handler = e => startedCount++; + gNavToolbox.addEventListener("customizationstarting", handler); + await startCustomizing(); + CustomizableUI.removeWidgetFromArea("stop-reload-button"); + await gCustomizeMode.reset().catch(e => { + ok( + false, + "Threw an exception trying to reset after making modifications in customize mode: " + + e + ); + }); + + let newKidCount = document.getElementById( + "customization-palette" + ).childElementCount; + is( + newKidCount, + paletteKidCount, + "Should have just as many items in the palette as before." + ); + await endCustomizing(); + is(startedCount, 1, "Should have only started once"); + gNavToolbox.removeEventListener("customizationstarting", handler); + let customizableToolbars = document.querySelectorAll( + "toolbar[customizable=true]:not([autohide=true])" + ); + for (let toolbar of customizableToolbars) { + ok( + !toolbar.hasAttribute("customizing"), + "Toolbar " + toolbar.id + " is no longer customizing" + ); + } +}); diff --git a/browser/components/customizableui/test/browser_synced_tabs_menu.js b/browser/components/customizableui/test/browser_synced_tabs_menu.js new file mode 100644 index 0000000000..ff60167fea --- /dev/null +++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js @@ -0,0 +1,523 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(2); + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +let { SyncedTabs } = ChromeUtils.importESModule( + "resource://services-sync/SyncedTabs.sys.mjs" +); +let { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + UITour: "resource:///modules/UITour.sys.mjs", +}); + +const DECKINDEX_TABS = 0; +const DECKINDEX_FETCHING = 1; +const DECKINDEX_TABSDISABLED = 2; +const DECKINDEX_NOCLIENTS = 3; + +const SAMPLE_TAB_URL = "https://example.com/"; + +var initialLocation = gBrowser.currentURI.spec; +var newTab = null; + +// A helper to notify there are new tabs. Returns a promise that is resolved +// once the UI has been updated. +function updateTabsPanel() { + let promiseTabsUpdated = promiseObserverNotified( + "synced-tabs-menu:test:tabs-updated" + ); + Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED); + return promiseTabsUpdated; +} + +// This is the mock we use for SyncedTabs.jsm - tests may override various +// functions. +let mockedInternal = { + get isConfiguredToSyncTabs() { + return true; + }, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + hasSyncedThisSession: false, +}; + +add_setup(async function () { + const getSignedInUser = FxAccounts.config.getSignedInUser; + FxAccounts.config.getSignedInUser = async () => + Promise.resolve({ uid: "uid", email: "foo@bar.com" }); + Services.prefs.setCharPref( + "identity.fxaccounts.remote.root", + "https://example.com/" + ); + + let oldInternal = SyncedTabs._internal; + SyncedTabs._internal = mockedInternal; + + let origNotifyStateUpdated = UIState._internal.notifyStateUpdated; + // Sync start-up will interfere with our tests, don't let UIState send UI updates. + UIState._internal.notifyStateUpdated = () => {}; + + // Force gSync initialization + gSync.init(); + + registerCleanupFunction(() => { + FxAccounts.config.getSignedInUser = getSignedInUser; + Services.prefs.clearUserPref("identity.fxaccounts.remote.root"); + UIState._internal.notifyStateUpdated = origNotifyStateUpdated; + SyncedTabs._internal = oldInternal; + }); +}); + +// The test expects the about:preferences#sync page to open in the current tab +async function openPrefsFromMenuPanel(expectedPanelId, entryPoint) { + info("Check Sync button functionality"); + CustomizableUI.addWidgetToArea( + "sync-button", + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + + await waitForOverflowButtonShown(); + + // check the button's functionality + await document.getElementById("nav-bar").overflowable.show(); + + if (entryPoint == "uitour") { + UITour.tourBrowsersByWindow.set(window, new Set()); + UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser); + } + + let syncButton = document.getElementById("sync-button"); + ok(syncButton, "The Sync button was added to the Panel Menu"); + + let tabsUpdatedPromise = promiseObserverNotified( + "synced-tabs-menu:test:tabs-updated" + ); + syncButton.click(); + let syncPanel = document.getElementById("PanelUI-remotetabs"); + let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown"); + await Promise.all([tabsUpdatedPromise, viewShownPromise]); + ok(syncPanel.getAttribute("visible"), "Sync Panel is in view"); + + // Sync is not configured - verify that state is reflected. + let subpanel = document.getElementById(expectedPanelId); + ok(!subpanel.hidden, "sync setup element is visible"); + + // Find and click the "setup" button. + let setupButton = subpanel.querySelector(".PanelUI-remotetabs-button"); + setupButton.click(); + + await new Promise(resolve => { + let handler = async e => { + if ( + e.originalTarget != gBrowser.selectedBrowser.contentDocument || + e.target.location.href == "about:blank" + ) { + info("Skipping spurious 'load' event for " + e.target.location.href); + return; + } + gBrowser.selectedBrowser.removeEventListener("load", handler, true); + resolve(); + }; + gBrowser.selectedBrowser.addEventListener("load", handler, true); + }); + newTab = gBrowser.selectedTab; + + is( + gBrowser.currentURI.spec, + "about:preferences?entrypoint=" + entryPoint + "#sync", + "Firefox Sync preference page opened with `menupanel` entrypoint" + ); + ok(!isOverflowOpen(), "The panel closed"); + + if (isOverflowOpen()) { + await hideOverflow(); + } +} + +function hideOverflow() { + let panelHidePromise = promiseOverflowHidden(window); + PanelUI.overflowPanel.hidePopup(); + return panelHidePromise; +} + +async function asyncCleanup() { + // reset the panel UI to the default state + await resetCustomization(); + ok(CustomizableUI.inDefaultState, "The panel UI is in default state again."); + + // restore the tabs + BrowserTestUtils.addTab(gBrowser, initialLocation); + gBrowser.removeTab(newTab); + UITour.tourBrowsersByWindow.delete(window); +} + +// When Sync is not setup. +add_task(async function () { + gSync.updateAllUI({ status: UIState.STATUS_NOT_CONFIGURED }); + await openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs"); +}); +add_task(asyncCleanup); + +// When an account is connected by Sync is not enabled. +add_task(async function () { + gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, syncEnabled: false }); + await openPrefsFromMenuPanel( + "PanelUI-remotetabs-syncdisabled", + "synced-tabs" + ); +}); +add_task(asyncCleanup); + +// When Sync is configured in an unverified state. +add_task(async function () { + gSync.updateAllUI({ + status: UIState.STATUS_NOT_VERIFIED, + email: "foo@bar.com", + }); + await openPrefsFromMenuPanel("PanelUI-remotetabs-unverified", "synced-tabs"); +}); +add_task(asyncCleanup); + +// When Sync is configured in a "needs reauthentication" state. +add_task(async function () { + gSync.updateAllUI({ + status: UIState.STATUS_LOGIN_FAILED, + email: "foo@bar.com", + }); + await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs"); +}); + +// Test the Connect Another Device button +add_task(async function () { + gSync.updateAllUI({ + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + lastSync: new Date(), + }); + + let button = document.getElementById( + "PanelUI-remotetabs-connect-device-button" + ); + ok(button, "found the button"); + + await document.getElementById("nav-bar").overflowable.show(); + let expectedUrl = + "https://example.com/connect_another_device?context=" + + "fx_desktop_v3&entrypoint=synced-tabs&service=sync&uid=uid&email=foo%40bar.com"; + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedUrl); + button.click(); + // the panel should have been closed. + ok(!isOverflowOpen(), "click closed the panel"); + await promiseTabOpened; + + gBrowser.removeTab(gBrowser.selectedTab); +}); + +// Test the "Sync Now" button +add_task(async function () { + gSync.updateAllUI({ + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + email: "foo@bar.com", + lastSync: new Date(), + }); + + await document.getElementById("nav-bar").overflowable.show(); + let tabsUpdatedPromise = promiseObserverNotified( + "synced-tabs-menu:test:tabs-updated" + ); + let syncPanel = document.getElementById("PanelUI-remotetabs"); + let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown"); + let syncButton = document.getElementById("sync-button"); + syncButton.click(); + await Promise.all([tabsUpdatedPromise, viewShownPromise]); + ok(syncPanel.getAttribute("visible"), "Sync Panel is in view"); + + let subpanel = document.getElementById("PanelUI-remotetabs-main"); + ok(!subpanel.hidden, "main pane is visible"); + let deck = document.getElementById("PanelUI-remotetabs-deck"); + + // The widget is still fetching tabs, as we've neutered everything that + // provides them + is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible"); + + // Tell the widget there are tabs available, but with zero clients. + mockedInternal.getTabClients = () => { + return Promise.resolve([]); + }; + mockedInternal.hasSyncedThisSession = true; + await updateTabsPanel(); + // The UI should be showing the "no clients" pane. + is( + deck.selectedIndex, + DECKINDEX_NOCLIENTS, + "no-clients deck entry is visible" + ); + + // Tell the widget there are tabs available - we have 3 clients, one with no + // tabs. + mockedInternal.getTabClients = () => { + return Promise.resolve([ + { + id: "guid_mobile", + type: "client", + name: "My Phone", + lastModified: 1492201200, + tabs: [], + }, + { + id: "guid_desktop", + type: "client", + name: "My Desktop", + lastModified: 1492201200, + tabs: [ + { + title: "http://example.com/10", + lastUsed: 10, // the most recent + }, + { + title: "http://example.com/1", + lastUsed: 1, // the least recent. + }, + { + title: "http://example.com/5", + lastUsed: 5, + }, + ], + }, + { + id: "guid_second_desktop", + name: "My Other Desktop", + lastModified: 1492201200, + tabs: [ + { + title: "http://example.com/6", + lastUsed: 6, + }, + ], + }, + ]); + }; + await updateTabsPanel(); + + // The UI should be showing tabs! + is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible"); + let tabList = document.getElementById("PanelUI-remotetabs-tabslist"); + let node = tabList.firstElementChild; + // First entry should be the client with the most-recent tab. + is(node.nodeName, "vbox"); + let currentClient = node; + node = node.firstElementChild; + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Desktop", "correct client"); + // Next entry is the most-recent tab + node = node.nextElementSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/10"); + + // Next entry is the next-most-recent tab + node = node.nextElementSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/5"); + + // Next entry is the least-recent tab from the first client. + node = node.nextElementSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/1"); + node = node.nextElementSibling; + is(node, null, "no more siblings"); + + // Next is a toolbarseparator between the clients. + node = currentClient.nextElementSibling; + is(node.nodeName, "toolbarseparator"); + + // Next is the container for client 2. + node = node.nextElementSibling; + is(node.nodeName, "vbox"); + currentClient = node; + + // Next is the client with 1 tab. + node = node.firstElementChild; + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Other Desktop", "correct client"); + // Its single tab + node = node.nextElementSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/6"); + node = node.nextElementSibling; + is(node, null, "no more siblings"); + + // Next is a toolbarseparator between the clients. + node = currentClient.nextElementSibling; + is(node.nodeName, "toolbarseparator"); + + // Next is the container for client 3. + node = node.nextElementSibling; + is(node.nodeName, "vbox"); + currentClient = node; + + // Next is the client with no tab. + node = node.firstElementChild; + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Phone", "correct client"); + // There is a single node saying there's no tabs for the client. + node = node.nextElementSibling; + is(node.nodeName, "label", "node is a label"); + is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client"); + + node = node.nextElementSibling; + is(node, null, "no more siblings"); + is(currentClient.nextElementSibling, null, "no more clients"); + + // Check accessibility. There should be containers for each client, with an + // aria attribute that identifies the client name. + let clientContainers = [ + ...tabList.querySelectorAll("[aria-labelledby]").values(), + ]; + let labelIds = clientContainers.map(container => + container.getAttribute("aria-labelledby") + ); + let labels = labelIds.map(id => document.getElementById(id).textContent); + Assert.deepEqual(labels.sort(), [ + "My Desktop", + "My Other Desktop", + "My Phone", + ]); + + let didSync = false; + let oldDoSync = gSync.doSync; + gSync.doSync = function () { + didSync = true; + gSync.doSync = oldDoSync; + }; + + let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow"); + is(syncNowButton.disabled, false); + syncNowButton.click(); + ok(didSync, "clicking the button called the correct function"); + + await hideOverflow(); +}); + +// Test the pagination capabilities (Show More/All tabs) +add_task(async function () { + mockedInternal.getTabClients = () => { + return Promise.resolve([ + { + id: "guid_desktop", + type: "client", + name: "My Desktop", + lastModified: 1492201200, + tabs: (function () { + let allTabsDesktop = []; + // We choose 77 tabs, because TABS_PER_PAGE is 25, which means + // on the second to last page we should have 22 items shown + // (because we have to show at least NEXT_PAGE_MIN_TABS=5 tabs on the last page) + for (let i = 1; i <= 77; i++) { + allTabsDesktop.push({ title: "Tab #" + i, url: SAMPLE_TAB_URL }); + } + return allTabsDesktop; + })(), + }, + ]); + }; + + gSync.updateAllUI({ + status: UIState.STATUS_SIGNED_IN, + syncEnabled: true, + lastSync: new Date(), + email: "foo@bar.com", + }); + + await document.getElementById("nav-bar").overflowable.show(); + let tabsUpdatedPromise = promiseObserverNotified( + "synced-tabs-menu:test:tabs-updated" + ); + let syncPanel = document.getElementById("PanelUI-remotetabs"); + let viewShownPromise = BrowserTestUtils.waitForEvent(syncPanel, "ViewShown"); + let syncButton = document.getElementById("sync-button"); + syncButton.click(); + await Promise.all([tabsUpdatedPromise, viewShownPromise]); + + // Check pre-conditions + ok(syncPanel.getAttribute("visible"), "Sync Panel is in view"); + let subpanel = document.getElementById("PanelUI-remotetabs-main"); + ok(!subpanel.hidden, "main pane is visible"); + let deck = document.getElementById("PanelUI-remotetabs-deck"); + is(deck.selectedIndex, DECKINDEX_TABS, "we should be showing tabs"); + + function checkTabsPage(tabsShownCount, showMoreLabel) { + let tabList = document.getElementById("PanelUI-remotetabs-tabslist"); + let node = tabList.firstElementChild.firstElementChild; + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Desktop", "correct client"); + for (let i = 0; i < tabsShownCount; i++) { + node = node.nextElementSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is( + node.getAttribute("label"), + "Tab #" + (i + 1), + "the tab is the correct one" + ); + is( + node.getAttribute("targetURI"), + SAMPLE_TAB_URL, + "url is the correct one" + ); + } + let showMoreButton; + if (showMoreLabel) { + node = showMoreButton = node.nextElementSibling; + is( + node.getAttribute("itemtype"), + "showmorebutton", + "node is a show more button" + ); + is(node.getAttribute("label"), showMoreLabel); + } + node = node.nextElementSibling; + is(node, null, "no more entries"); + + return showMoreButton; + } + + async function checkCanOpenURL() { + let tabList = document.getElementById("PanelUI-remotetabs-tabslist"); + let node = tabList.firstElementChild.firstElementChild.nextElementSibling; + let promiseTabOpened = BrowserTestUtils.waitForLocationChange( + gBrowser, + SAMPLE_TAB_URL + ); + node.click(); + await promiseTabOpened; + } + + let showMoreButton; + function clickShowMoreButton() { + let promise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated"); + showMoreButton.click(); + return promise; + } + + showMoreButton = checkTabsPage(25, "Show More Tabs"); + await clickShowMoreButton(); + + checkTabsPage(77, null); + /* calling this will close the overflow menu */ + await checkCanOpenURL(); +}); diff --git a/browser/components/customizableui/test/browser_tabbar_big_widgets.js b/browser/components/customizableui/test/browser_tabbar_big_widgets.js new file mode 100644 index 0000000000..3722ea9cdc --- /dev/null +++ b/browser/components/customizableui/test/browser_tabbar_big_widgets.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const kButtonId = "test-tabbar-size-with-large-buttons"; + +function test() { + registerCleanupFunction(cleanup); + let titlebar = document.getElementById("titlebar"); + let originalHeight = titlebar.getBoundingClientRect().height; + let button = document.createXULElement("toolbarbutton"); + button.id = kButtonId; + button.setAttribute("style", "min-height: 100px"); + gNavToolbox.palette.appendChild(button); + CustomizableUI.addWidgetToArea(kButtonId, CustomizableUI.AREA_TABSTRIP); + let currentHeight = titlebar.getBoundingClientRect().height; + ok(currentHeight > originalHeight, "Titlebar should have grown"); + CustomizableUI.removeWidgetFromArea(kButtonId); + currentHeight = titlebar.getBoundingClientRect().height; + is( + currentHeight, + originalHeight, + "Titlebar should have gone back to its original size." + ); +} + +function cleanup() { + let btn = document.getElementById(kButtonId); + if (btn) { + btn.remove(); + } +} diff --git a/browser/components/customizableui/test/browser_toolbar_collapsed_states.js b/browser/components/customizableui/test/browser_toolbar_collapsed_states.js new file mode 100644 index 0000000000..23da60a7d5 --- /dev/null +++ b/browser/components/customizableui/test/browser_toolbar_collapsed_states.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Checks that CustomizableUI reports the expected collapsed toolbar IDs. + * + * Note: on macOS, expectations for CustomizableUI.AREA_MENUBAR are + * automatically skipped since that area isn't available on that platform. + * + * @param {string[]} The IDs of the expected collapsed toolbars. + */ +function assertCollapsedToolbarIds(expected) { + if (AppConstants.platform == "macosx") { + let menubarIndex = expected.indexOf(CustomizableUI.AREA_MENUBAR); + if (menubarIndex != -1) { + expected.splice(menubarIndex, 1); + } + } + + let collapsedIds = CustomizableUI.getCollapsedToolbarIds(window); + Assert.equal(collapsedIds.size, expected.length); + for (let expectedId of expected) { + Assert.ok( + collapsedIds.has(expectedId), + `${expectedId} should be collapsed` + ); + } +} + +registerCleanupFunction(async () => { + await CustomizableUI.reset(); +}); + +/** + * Tests that CustomizableUI.getCollapsedToolbarIds will return the IDs of + * toolbars that are collapsed, or menubars that are autohidden. + */ +add_task(async function test_toolbar_collapsed_states() { + // By default, we expect the menubar and the bookmarks toolbar to be + // collapsed. + assertCollapsedToolbarIds([ + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_MENUBAR, + ]); + + let bookmarksToolbar = document.getElementById(CustomizableUI.AREA_BOOKMARKS); + // Make sure we're configured to show the bookmarks toolbar on about:newtab. + setToolbarVisibility(bookmarksToolbar, "newtab"); + + let newTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:newtab", + waitForLoad: false, + }); + // Now that we've opened about:newtab, the bookmarks toolbar should now + // be visible. + assertCollapsedToolbarIds([CustomizableUI.AREA_MENUBAR]); + await BrowserTestUtils.removeTab(newTab); + + // And with about:newtab closed again, the bookmarks toolbar should be + // reported as collapsed. + assertCollapsedToolbarIds([ + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_MENUBAR, + ]); + + // Make sure we're configured to show the bookmarks toolbar on about:newtab. + setToolbarVisibility(bookmarksToolbar, "always"); + assertCollapsedToolbarIds([CustomizableUI.AREA_MENUBAR]); + + setToolbarVisibility(bookmarksToolbar, "never"); + assertCollapsedToolbarIds([ + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_MENUBAR, + ]); + + if (AppConstants.platform != "macosx") { + // We'll still consider the menubar collapsed by default, even if it's being temporarily + // shown via the alt key. + let menubarActive = BrowserTestUtils.waitForEvent( + window, + "DOMMenuBarActive" + ); + EventUtils.synthesizeKey("VK_ALT", {}); + await menubarActive; + assertCollapsedToolbarIds([ + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_MENUBAR, + ]); + let menubarInactive = BrowserTestUtils.waitForEvent( + window, + "DOMMenuBarInactive" + ); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + await menubarInactive; + assertCollapsedToolbarIds([ + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_MENUBAR, + ]); + + let menubar = document.getElementById(CustomizableUI.AREA_MENUBAR); + setToolbarVisibility(menubar, true); + assertCollapsedToolbarIds([CustomizableUI.AREA_BOOKMARKS]); + setToolbarVisibility(menubar, false); + assertCollapsedToolbarIds([ + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_MENUBAR, + ]); + } +}); diff --git a/browser/components/customizableui/test/browser_touchbar_customization.js b/browser/components/customizableui/test/browser_touchbar_customization.js new file mode 100644 index 0000000000..106c202ad9 --- /dev/null +++ b/browser/components/customizableui/test/browser_touchbar_customization.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Checks if the Customize Touch Bar button appears when a Touch Bar is +// initialized. +add_task(async function customizeTouchBarButtonAppears() { + let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService( + Ci.nsITouchBarUpdater + ); + // This value will be reset to its default the next time a window is opened. + updater.setTouchBarInitialized(true); + await startCustomizing(); + let touchbarButton = document.querySelector("#customization-touchbar-button"); + ok(!touchbarButton.hidden, "Customize Touch Bar button is not hidden."); + let touchbarSpacer = document.querySelector("#customization-touchbar-spacer"); + ok(!touchbarSpacer.hidden, "Customize Touch Bar spacer is not hidden."); + await endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_unified_extensions_reset.js b/browser/components/customizableui/test/browser_unified_extensions_reset.js new file mode 100644 index 0000000000..fdee8cf76a --- /dev/null +++ b/browser/components/customizableui/test/browser_unified_extensions_reset.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if Unified Extensions UI is enabled that resetting the toolbars + * puts all browser action buttons into the AREA_ADDONS area. + */ +add_task(async function test_reset_with_unified_extensions_ui() { + const kWebExtensionWidgetIDs = [ + "ext0-browser-action", + "ext1-browser-action", + "ext2-browser-action", + "ext3-browser-action", + "ext4-browser-action", + "ext5-browser-action", + "ext6-browser-action", + "ext7-browser-action", + "ext8-browser-action", + "ext9-browser-action", + "ext10-browser-action", + ]; + + for (let widgetID of kWebExtensionWidgetIDs) { + CustomizableUI.createWidget({ + id: widgetID, + label: "Test extension widget", + defaultArea: CustomizableUI.AREA_NAVBAR, + webExtension: true, + }); + } + + // Now let's put these browser actions in a bunch of different places. + // Regardless of where they go, we're going to expect them in AREA_ADDONS + // after we reset. + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[0], + CustomizableUI.AREA_TABSTRIP + ); + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[1], + CustomizableUI.AREA_TABSTRIP + ); + + // macOS doesn't have AREA_MENUBAR registered, so we'll leave these widgets + // behind in the AREA_NAVBAR there, and put them into the menubar on the + // other platforms. + if (AppConstants.platform != "macosx") { + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[2], + CustomizableUI.AREA_MENUBAR + ); + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[3], + CustomizableUI.AREA_MENUBAR + ); + } + + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[4], + CustomizableUI.AREA_BOOKMARKS + ); + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[5], + CustomizableUI.AREA_BOOKMARKS + ); + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[6], + CustomizableUI.AREA_ADDONS + ); + CustomizableUI.addWidgetToArea( + kWebExtensionWidgetIDs[7], + CustomizableUI.AREA_ADDONS + ); + + CustomizableUI.reset(); + + // Let's force the Unified Extensions panel to register itself now if it + // wasn't already done. Using the getter should be sufficient. + Assert.ok(gUnifiedExtensions.panel, "Should have found the panel."); + + for (let widgetID of kWebExtensionWidgetIDs) { + let { area } = CustomizableUI.getPlacementOfWidget(widgetID); + Assert.equal(area, CustomizableUI.AREA_ADDONS); + // Let's double-check that they're actually in there in the DOM too. + let widget = CustomizableUI.getWidget(widgetID).forWindow(window); + Assert.equal(widget.node.parentElement.id, CustomizableUI.AREA_ADDONS); + CustomizableUI.destroyWidget(widgetID); + } +}); diff --git a/browser/components/customizableui/test/browser_widget_animation.js b/browser/components/customizableui/test/browser_widget_animation.js new file mode 100644 index 0000000000..514e3f763b --- /dev/null +++ b/browser/components/customizableui/test/browser_widget_animation.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +gReduceMotionOverride = false; + +function promiseWidgetAnimationOut(aNode) { + let animationNode = aNode; + if ( + animationNode.tagName != "toolbaritem" && + animationNode.tagName != "toolbarbutton" + ) { + animationNode = animationNode.closest("toolbaritem"); + } + if (animationNode.parentNode.id.startsWith("wrapper-")) { + animationNode = animationNode.parentNode; + } + return new Promise(resolve => { + animationNode.addEventListener( + "animationend", + function cleanupWidgetAnimationOut(e) { + if ( + e.animationName == "widget-animate-out" && + e.target.id == animationNode.id + ) { + animationNode.removeEventListener( + "animationend", + cleanupWidgetAnimationOut + ); + ok(true, "The widget`s animationend should have happened"); + resolve(); + } + } + ); + }); +} + +function promiseOverflowAnimationEnd() { + return new Promise(resolve => { + let overflowButton = document.getElementById("nav-bar-overflow-button"); + overflowButton.addEventListener( + "animationend", + function cleanupOverflowAnimationOut(event) { + if (event.animationName == "overflow-animation") { + overflowButton.removeEventListener( + "animationend", + cleanupOverflowAnimationOut + ); + ok( + true, + "The overflow button`s animationend event should have happened" + ); + resolve(); + } + } + ); + }); +} + +// Right-click on the stop/reload button, use the context menu to move it to the overflow menu. +// The button should animate out, and the overflow menu should animate upon adding. +add_task(async function () { + let stopReloadButton = document.getElementById("stop-reload-button"); + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouseAtCenter(stopReloadButton, { + type: "contextmenu", + button: 2, + }); + await shownPromise; + + contextMenu.activateItem( + contextMenu.querySelector(".customize-context-moveToPanel") + ); + + await Promise.all([ + promiseWidgetAnimationOut(stopReloadButton), + promiseOverflowAnimationEnd(), + ]); + ok(true, "The widget and overflow animations should have both happened."); +}); + +registerCleanupFunction(CustomizableUI.reset); diff --git a/browser/components/customizableui/test/browser_widget_recreate_events.js b/browser/components/customizableui/test/browser_widget_recreate_events.js new file mode 100644 index 0000000000..3eca9231a8 --- /dev/null +++ b/browser/components/customizableui/test/browser_widget_recreate_events.js @@ -0,0 +1,99 @@ +"use strict"; + +const widgetData = { + id: "test-widget", + type: "view", + viewId: "PanelUI-testbutton", + label: "test widget label", + onViewShowing() {}, + onViewHiding() {}, +}; + +async function simulateWidgetOpen() { + let testWidgetButton = document.getElementById("test-widget"); + let testWidgetShowing = BrowserTestUtils.waitForEvent( + document, + "popupshowing", + true + ); + testWidgetButton.click(); + await testWidgetShowing; +} + +async function simulateWidgetClose() { + let panel = document.getElementById("customizationui-widget-panel"); + let panelHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + + panel.hidePopup(); + await panelHidden; +} + +function createPanelView() { + let panelView = document.createXULElement("panelview"); + panelView.id = "PanelUI-testbutton"; + let vbox = document.createXULElement("vbox"); + panelView.appendChild(vbox); + return panelView; +} + +/** + * Check that panel view/hide events are added back, + * if widget is destroyed and created again in one session. + */ +add_task(async function () { + let viewCache = document.getElementById("appMenu-viewCache"); + let panelView = createPanelView(); + viewCache.appendChild(panelView); + + CustomizableUI.createWidget(widgetData); + CustomizableUI.addWidgetToArea("test-widget", "nav-bar"); + + // Simulate clicking and wait for the open + // so we ensure the lazy event creation is done. + await simulateWidgetOpen(); + + let listeners = Services.els.getListenerInfoFor(panelView); + ok( + listeners.some(info => info.type == "ViewShowing"), + "ViewShowing event added" + ); + ok( + listeners.some(info => info.type == "ViewHiding"), + "ViewHiding event added" + ); + + await simulateWidgetClose(); + CustomizableUI.destroyWidget("test-widget"); + + listeners = Services.els.getListenerInfoFor(panelView); + // Ensure the events got removed after destorying the widget. + ok( + !listeners.some(info => info.type == "ViewShowing"), + "ViewShowing event removed" + ); + ok( + !listeners.some(info => info.type == "ViewHiding"), + "ViewHiding event removed" + ); + + CustomizableUI.createWidget(widgetData); + // Simulate clicking and wait for the open + // so we ensure the lazy event creation is done. + // We need to do this again because we destroyed the widget. + await simulateWidgetOpen(); + + listeners = Services.els.getListenerInfoFor(panelView); + ok( + listeners.some(info => info.type == "ViewShowing"), + "ViewShowing event added again" + ); + ok( + listeners.some(info => info.type == "ViewHiding"), + "ViewHiding event added again" + ); + + await simulateWidgetClose(); + CustomizableUI.destroyWidget("test-widget"); + panelView.remove(); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/dummy_history_item.html b/browser/components/customizableui/test/dummy_history_item.html new file mode 100644 index 0000000000..23a6992923 --- /dev/null +++ b/browser/components/customizableui/test/dummy_history_item.html @@ -0,0 +1,2 @@ +<title>Happy History Hero</title> +<p>I am a page for the history books.</p> diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js new file mode 100644 index 0000000000..cf1b86d929 --- /dev/null +++ b/browser/components/customizableui/test/head.js @@ -0,0 +1,536 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + CustomizableUITestUtils: + "resource://testing-common/CustomizableUITestUtils.sys.mjs", +}); + +var EventUtils = {}; +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils +); + +/** + * Instance of CustomizableUITestUtils for the current browser window. + */ +var gCUITestUtils = new CustomizableUITestUtils(window); + +Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true); +registerCleanupFunction(() => + Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck") +); + +var { synthesizeDrop, synthesizeMouseAtCenter } = EventUtils; + +const kForceOverflowWidthPx = 450; + +function createDummyXULButton(id, label, win = window) { + let btn = win.document.createXULElement("toolbarbutton"); + btn.id = id; + btn.setAttribute("label", label || id); + btn.className = "toolbarbutton-1 chromeclass-toolbar-additional"; + win.gNavToolbox.palette.appendChild(btn); + return btn; +} + +var gAddedToolbars = new Set(); + +function createToolbarWithPlacements(id, placements = [], properties = {}) { + gAddedToolbars.add(id); + let tb = document.createXULElement("toolbar"); + tb.id = id; + tb.setAttribute("customizable", "true"); + + properties.type = CustomizableUI.TYPE_TOOLBAR; + properties.defaultPlacements = placements; + CustomizableUI.registerArea(id, properties); + gNavToolbox.appendChild(tb); + CustomizableUI.registerToolbarNode(tb); + return tb; +} + +function createOverflowableToolbarWithPlacements(id, placements) { + gAddedToolbars.add(id); + + let tb = document.createXULElement("toolbar"); + tb.id = id; + tb.setAttribute("customizationtarget", id + "-target"); + + let customizationtarget = document.createXULElement("hbox"); + customizationtarget.id = id + "-target"; + customizationtarget.setAttribute("flex", "1"); + tb.appendChild(customizationtarget); + + let overflowPanel = document.createXULElement("panel"); + overflowPanel.id = id + "-overflow"; + document.getElementById("mainPopupSet").appendChild(overflowPanel); + + let overflowList = document.createXULElement("vbox"); + overflowList.id = id + "-overflow-list"; + overflowPanel.appendChild(overflowList); + + let chevron = document.createXULElement("toolbarbutton"); + chevron.id = id + "-chevron"; + tb.appendChild(chevron); + + CustomizableUI.registerArea(id, { + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: placements, + overflowable: true, + }); + + tb.setAttribute("customizable", "true"); + tb.setAttribute("overflowable", "true"); + tb.setAttribute("default-overflowpanel", overflowPanel.id); + tb.setAttribute("default-overflowtarget", overflowList.id); + tb.setAttribute("default-overflowbutton", chevron.id); + tb.setAttribute("addon-webext-overflowbutton", "unified-extensions-button"); + tb.setAttribute("addon-webext-overflowtarget", "overflowed-extensions-list"); + + gNavToolbox.appendChild(tb); + CustomizableUI.registerToolbarNode(tb); + return tb; +} + +function removeCustomToolbars() { + CustomizableUI.reset(); + for (let toolbarId of gAddedToolbars) { + CustomizableUI.unregisterArea(toolbarId, true); + let tb = document.getElementById(toolbarId); + if (tb.hasAttribute("overflowpanel")) { + let panel = document.getElementById(tb.getAttribute("overflowpanel")); + if (panel) { + panel.remove(); + } + } + tb.remove(); + } + gAddedToolbars.clear(); +} + +function resetCustomization() { + return CustomizableUI.reset(); +} + +function isInDevEdition() { + return AppConstants.MOZ_DEV_EDITION; +} + +function removeNonReleaseButtons(areaPanelPlacements) { + if (isInDevEdition() && areaPanelPlacements.includes("developer-button")) { + areaPanelPlacements.splice( + areaPanelPlacements.indexOf("developer-button"), + 1 + ); + } +} + +function removeNonOriginalButtons() { + CustomizableUI.removeWidgetFromArea("sync-button"); +} + +function assertAreaPlacements(areaId, expectedPlacements) { + let actualPlacements = getAreaWidgetIds(areaId); + placementArraysEqual(areaId, actualPlacements, expectedPlacements); +} + +function placementArraysEqual(areaId, actualPlacements, expectedPlacements) { + info("Actual placements: " + actualPlacements.join(", ")); + info("Expected placements: " + expectedPlacements.join(", ")); + is( + actualPlacements.length, + expectedPlacements.length, + "Area " + areaId + " should have " + expectedPlacements.length + " items." + ); + let minItems = Math.min(expectedPlacements.length, actualPlacements.length); + for (let i = 0; i < minItems; i++) { + if (typeof expectedPlacements[i] == "string") { + is( + actualPlacements[i], + expectedPlacements[i], + "Item " + i + " in " + areaId + " should match expectations." + ); + } else if (expectedPlacements[i] instanceof RegExp) { + ok( + expectedPlacements[i].test(actualPlacements[i]), + "Item " + + i + + " (" + + actualPlacements[i] + + ") in " + + areaId + + " should match " + + expectedPlacements[i] + ); + } else { + ok( + false, + "Unknown type of expected placement passed to " + + " assertAreaPlacements. Is your test broken?" + ); + } + } +} + +function todoAssertAreaPlacements(areaId, expectedPlacements) { + let actualPlacements = getAreaWidgetIds(areaId); + let isPassing = actualPlacements.length == expectedPlacements.length; + let minItems = Math.min(expectedPlacements.length, actualPlacements.length); + for (let i = 0; i < minItems; i++) { + if (typeof expectedPlacements[i] == "string") { + isPassing = isPassing && actualPlacements[i] == expectedPlacements[i]; + } else if (expectedPlacements[i] instanceof RegExp) { + isPassing = isPassing && expectedPlacements[i].test(actualPlacements[i]); + } else { + ok( + false, + "Unknown type of expected placement passed to " + + " assertAreaPlacements. Is your test broken?" + ); + } + } + todo( + isPassing, + "The area placements for " + + areaId + + " should equal the expected placements." + ); +} + +function getAreaWidgetIds(areaId) { + return CustomizableUI.getWidgetIdsInArea(areaId); +} + +function simulateItemDrag(aToDrag, aTarget, aEvent = {}, aOffset = 2) { + let ev = aEvent; + if (ev == "end" || ev == "start") { + let win = aTarget.ownerGlobal; + const dwu = win.windowUtils; + let bounds = dwu.getBoundsWithoutFlushing(aTarget); + if (ev == "end") { + ev = { + clientX: bounds.right - aOffset, + clientY: bounds.bottom - aOffset, + }; + } else { + ev = { clientX: bounds.left + aOffset, clientY: bounds.top + aOffset }; + } + } + ev._domDispatchOnly = true; + synthesizeDrop( + aToDrag.parentNode, + aTarget, + null, + null, + aToDrag.ownerGlobal, + aTarget.ownerGlobal, + ev + ); + // Ensure dnd suppression is cleared. + synthesizeMouseAtCenter(aTarget, { type: "mouseup" }, aTarget.ownerGlobal); +} + +function endCustomizing(aWindow = window) { + if (aWindow.document.documentElement.getAttribute("customizing") != "true") { + return true; + } + let afterCustomizationPromise = BrowserTestUtils.waitForEvent( + aWindow.gNavToolbox, + "aftercustomization" + ); + aWindow.gCustomizeMode.exit(); + return afterCustomizationPromise; +} + +function startCustomizing(aWindow = window) { + if (aWindow.document.documentElement.getAttribute("customizing") == "true") { + return null; + } + let customizationReadyPromise = BrowserTestUtils.waitForEvent( + aWindow.gNavToolbox, + "customizationready" + ); + aWindow.gCustomizeMode.enter(); + return customizationReadyPromise; +} + +function promiseObserverNotified(aTopic) { + return new Promise(resolve => { + Services.obs.addObserver(function onNotification(subject, topic, data) { + Services.obs.removeObserver(onNotification, topic); + resolve({ subject, data }); + }, aTopic); + }); +} + +function openAndLoadWindow(aOptions, aWaitForDelayedStartup = false) { + return new Promise(resolve => { + let win = OpenBrowserWindow(aOptions); + if (aWaitForDelayedStartup) { + Services.obs.addObserver(function onDS(aSubject, aTopic, aData) { + if (aSubject != win) { + return; + } + Services.obs.removeObserver(onDS, "browser-delayed-startup-finished"); + resolve(win); + }, "browser-delayed-startup-finished"); + } else { + win.addEventListener( + "load", + function () { + resolve(win); + }, + { once: true } + ); + } + }); +} + +function promiseWindowClosed(win) { + return new Promise(resolve => { + win.addEventListener( + "unload", + function () { + resolve(); + }, + { once: true } + ); + win.close(); + }); +} + +function promiseOverflowShown(win) { + let panelEl = win.document.getElementById("widget-overflow"); + return promisePanelElementShown(win, panelEl); +} + +function promisePanelElementShown(win, aPanel) { + return new Promise((resolve, reject) => { + let timeoutId = win.setTimeout(() => { + reject("Panel did not show within 20 seconds."); + }, 20000); + function onPanelOpen(e) { + aPanel.removeEventListener("popupshown", onPanelOpen); + win.clearTimeout(timeoutId); + resolve(); + } + aPanel.addEventListener("popupshown", onPanelOpen); + }); +} + +function promiseOverflowHidden(win) { + let panelEl = win.PanelUI.overflowPanel; + return promisePanelElementHidden(win, panelEl); +} + +function promisePanelElementHidden(win, aPanel) { + return new Promise((resolve, reject) => { + let timeoutId = win.setTimeout(() => { + reject("Panel did not hide within 20 seconds."); + }, 20000); + function onPanelClose(e) { + aPanel.removeEventListener("popuphidden", onPanelClose); + win.clearTimeout(timeoutId); + executeSoon(resolve); + } + aPanel.addEventListener("popuphidden", onPanelClose); + }); +} + +function isPanelUIOpen() { + return PanelUI.panel.state == "open" || PanelUI.panel.state == "showing"; +} + +function isOverflowOpen() { + let panel = document.getElementById("widget-overflow"); + return panel.state == "open" || panel.state == "showing"; +} + +function subviewShown(aSubview) { + return new Promise((resolve, reject) => { + let win = aSubview.ownerGlobal; + let timeoutId = win.setTimeout(() => { + reject("Subview (" + aSubview.id + ") did not show within 20 seconds."); + }, 20000); + function onViewShown(e) { + aSubview.removeEventListener("ViewShown", onViewShown); + win.clearTimeout(timeoutId); + resolve(); + } + aSubview.addEventListener("ViewShown", onViewShown); + }); +} + +function subviewHidden(aSubview) { + return new Promise((resolve, reject) => { + let win = aSubview.ownerGlobal; + let timeoutId = win.setTimeout(() => { + reject("Subview (" + aSubview.id + ") did not hide within 20 seconds."); + }, 20000); + function onViewHiding(e) { + aSubview.removeEventListener("ViewHiding", onViewHiding); + win.clearTimeout(timeoutId); + resolve(); + } + aSubview.addEventListener("ViewHiding", onViewHiding); + }); +} + +function waitFor(aTimeout = 100) { + return new Promise(resolve => { + setTimeout(() => resolve(), aTimeout); + }); +} + +/** + * Starts a load in an existing tab and waits for it to finish (via some event). + * + * @param aTab The tab to load into. + * @param aUrl The url to load. + * @param aEventType The load event type to wait for. Defaults to "load". + * @return {Promise} resolved when the event is handled. + */ +function promiseTabLoadEvent(aTab, aURL) { + let browser = aTab.linkedBrowser; + + BrowserTestUtils.loadURIString(browser, aURL); + return BrowserTestUtils.browserLoaded(browser); +} + +/** + * Wait for an attribute on a node to change + * + * @param aNode Node on which the mutation is expected + * @param aAttribute The attribute we're interested in + * @param aFilterFn A function to check if the new value is what we want. + * @return {Promise} resolved when the requisite mutation shows up. + */ +function promiseAttributeMutation(aNode, aAttribute, aFilterFn) { + return new Promise((resolve, reject) => { + info("waiting for mutation of attribute '" + aAttribute + "'."); + let obs = new MutationObserver(mutations => { + for (let mut of mutations) { + let attr = mut.attributeName; + let newValue = mut.target.getAttribute(attr); + if (aFilterFn(newValue)) { + ok( + true, + "mutation occurred: attribute '" + + attr + + "' changed to '" + + newValue + + "' from '" + + mut.oldValue + + "'." + ); + obs.disconnect(); + resolve(); + } else { + info( + "Ignoring mutation that produced value " + + newValue + + " because of filter." + ); + } + } + }); + obs.observe(aNode, { attributeFilter: [aAttribute] }); + }); +} + +function popupShown(aPopup) { + return BrowserTestUtils.waitForPopupEvent(aPopup, "shown"); +} + +function popupHidden(aPopup) { + return BrowserTestUtils.waitForPopupEvent(aPopup, "hidden"); +} + +// This is a simpler version of the context menu check that +// exists in contextmenu_common.js. +function checkContextMenu(aContextMenu, aExpectedEntries, aWindow = window) { + let children = [...aContextMenu.children]; + // Ignore hidden nodes: + children = children.filter(n => !n.hidden); + + for (let i = 0; i < children.length; i++) { + let menuitem = children[i]; + try { + if (aExpectedEntries[i][0] == "---") { + is(menuitem.localName, "menuseparator", "menuseparator expected"); + continue; + } + + let selector = aExpectedEntries[i][0]; + ok( + menuitem.matches(selector), + "menuitem should match " + selector + " selector" + ); + let commandValue = menuitem.getAttribute("command"); + let relatedCommand = commandValue + ? aWindow.document.getElementById(commandValue) + : null; + let menuItemDisabled = relatedCommand + ? relatedCommand.getAttribute("disabled") == "true" + : menuitem.getAttribute("disabled") == "true"; + is( + menuItemDisabled, + !aExpectedEntries[i][1], + "disabled state for " + selector + ); + } catch (e) { + ok(false, "Exception when checking context menu: " + e); + } + } +} + +function waitForOverflowButtonShown(win = window) { + info("Waiting for overflow button to show"); + let ov = win.document.getElementById("nav-bar-overflow-button"); + return waitForElementShown(ov.icon); +} +function waitForElementShown(element) { + return BrowserTestUtils.waitForCondition(() => { + info("Checking if element has non-0 size"); + // We intentionally flush layout to ensure the element is actually shown. + let rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }); +} + +/** + * Opens the history panel through the history toolbarbutton in the + * navbar and returns a promise that resolves as soon as the panel is open + * is showing. + */ +async function openHistoryPanel(doc = document) { + await waitForOverflowButtonShown(); + await doc.getElementById("nav-bar").overflowable.show(); + info("Menu panel was opened"); + + let historyButton = doc.getElementById("history-panelmenu"); + Assert.ok(historyButton, "History button appears in Panel Menu"); + + historyButton.click(); + + let historyPanel = doc.getElementById("PanelUI-history"); + return BrowserTestUtils.waitForEvent(historyPanel, "ViewShown"); +} + +/** + * Closes the history panel and returns a promise that resolves as sooon + * as the panel is closed. + */ +async function hideHistoryPanel(doc = document) { + let historyView = doc.getElementById("PanelUI-history"); + let historyPanel = historyView.closest("panel"); + let promise = BrowserTestUtils.waitForEvent(historyPanel, "popuphidden"); + historyPanel.hidePopup(); + return promise; +} diff --git a/browser/components/customizableui/test/support/test_967000_charEncoding_page.html b/browser/components/customizableui/test/support/test_967000_charEncoding_page.html new file mode 100644 index 0000000000..7932b16f12 --- /dev/null +++ b/browser/components/customizableui/test/support/test_967000_charEncoding_page.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="iso-8859-1"> + <title>Test page</title> + </head> + + <body> + This is a test page + </body> +</html> diff --git a/browser/components/customizableui/test/unit/test_unified_extensions_migration.js b/browser/components/customizableui/test/unit/test_unified_extensions_migration.js new file mode 100644 index 0000000000..b692c1d76f --- /dev/null +++ b/browser/components/customizableui/test/unit/test_unified_extensions_migration.js @@ -0,0 +1,368 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// We're in an xpcshell test but have an eslint browser test env applied; +// We definitely do need to manually import CustomizableUI. +// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix +const { CustomizableUI } = ChromeUtils.importESModule( + "resource:///modules/CustomizableUI.sys.mjs" +); + +do_get_profile(); + +// Make Cu.isInAutomation true. This is necessary so that we can use +// CustomizableUIInternal. +Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + true +); + +const CustomizableUIInternal = CustomizableUI.getTestOnlyInternalProp( + "CustomizableUIInternal" +); + +// Migration 19 was the Unified Extensions migration version introduced +// in 109, so we'll run tests by artificially setting the migration version +// to one value earlier. +const PRIOR_MIGRATION_VERSION = 18; + +/** + * Writes customization state into CustomizableUI and then performs the forward migration + * for Unified Extensions. + * + * @param {object|null} stateObj An object that will be structure-cloned and + * written into CustomizableUI's internal `gSavedState` state variable. Should + * not include the currentVersion property, as this will be set automatically by + * function if stateObj is not null. + * @returns {object} + * the saved state object (minus the currentVersion property). + */ +function migrateForward(stateObj) { + // We make sure to use structuredClone here so that we don't end up comparing + // SAVED_STATE against itself. + let stateToSave = structuredClone(stateObj); + if (stateToSave) { + stateToSave.currentVersion = PRIOR_MIGRATION_VERSION; + } + + CustomizableUI.setTestOnlyInternalProp("gSavedState", stateToSave); + CustomizableUIInternal._updateForNewVersion(); + + let migratedState = CustomizableUI.getTestOnlyInternalProp("gSavedState"); + if (migratedState) { + delete migratedState.currentVersion; + } + return migratedState; +} + +/** + * Test that attempting a migration on a new profile with no saved + * state exits safely. + */ +add_task(async function test_no_saved_state() { + let migratedState = migrateForward(null); + + Assert.deepEqual( + migratedState, + null, + "gSavedState should not have been modified" + ); +}); + +/** + * Test that attempting a migration on a new profile with no saved + * state exits safely. + */ +add_task(async function test_no_saved_placements() { + let migratedState = migrateForward({}); + + Assert.deepEqual( + migratedState, + {}, + "gSavedState should not have been modified" + ); +}); + +/** + * Test that placements that don't involve any extension buttons are + * not changed during the migration. + */ +add_task(async function test_no_extensions() { + const SAVED_STATE = { + placements: { + "nav-bar": [ + "back-button", + "forward-button", + "spring", + "urlbar-container", + "save-to-pocket-button", + ], + "toolbar-menubar": [ + "home-button", + "menubar-items", + "spring", + "downloads-button", + ], + TabsToolbar: [ + "firefox-view-button", + "tabbrowser-tabs", + "new-tab-button", + "alltabs-button", + "developer-button", + ], + PersonalToolbar: ["personal-bookmarks", "fxa-toolbar-menu-button"], + "widget-overflow-fixed-list": ["privatebrowsing-button", "panic-button"], + }, + }; + + // ADDONS_AREA should end up with an empty array as its set of placements. + const EXPECTED_STATE = structuredClone(SAVED_STATE); + EXPECTED_STATE.placements[CustomizableUI.AREA_ADDONS] = []; + + let migratedState = migrateForward(SAVED_STATE); + + Assert.deepEqual( + migratedState, + EXPECTED_STATE, + "Got the expected state after the migration." + ); +}); + +/** + * Test that if there's an existing set of items in CustomizableUI.AREA_ADDONS, + * and no extension buttons to migrate from the overflow menu, then we don't + * change the state at all. + */ +add_task(async function test_existing_browser_actions_no_movement() { + const SAVED_STATE = { + placements: { + "nav-bar": [ + "back-button", + "forward-button", + "spring", + "urlbar-container", + "save-to-pocket-button", + ], + "toolbar-menubar": [ + "home-button", + "menubar-items", + "spring", + "downloads-button", + ], + TabsToolbar: [ + "firefox-view-button", + "tabbrowser-tabs", + "new-tab-button", + "alltabs-button", + "developer-button", + ], + PersonalToolbar: ["personal-bookmarks", "fxa-toolbar-menu-button"], + "widget-overflow-fixed-list": ["privatebrowsing-button", "panic-button"], + "unified-extensions-area": ["ext0-browser-action", "ext1-browser-action"], + }, + }; + + let migratedState = migrateForward(SAVED_STATE); + + Assert.deepEqual( + migratedState, + SAVED_STATE, + "The saved state should not have changed after migration." + ); +}); + +/** + * Test that we can migrate extension buttons out from the overflow panel + * into the addons panel. + */ +add_task(async function test_migrate_extension_buttons() { + const SAVED_STATE = { + placements: { + "nav-bar": [ + "back-button", + "forward-button", + "spring", + "urlbar-container", + "save-to-pocket-button", + ], + "toolbar-menubar": [ + "home-button", + "menubar-items", + "spring", + "downloads-button", + ], + TabsToolbar: [ + "firefox-view-button", + "tabbrowser-tabs", + "new-tab-button", + "alltabs-button", + "developer-button", + ], + PersonalToolbar: ["personal-bookmarks", "fxa-toolbar-menu-button"], + "widget-overflow-fixed-list": [ + "ext0-browser-action", + "privatebrowsing-button", + "ext1-browser-action", + "panic-button", + "ext2-browser-action", + ], + }, + }; + const EXPECTED_STATE = structuredClone(SAVED_STATE); + EXPECTED_STATE.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] = [ + "privatebrowsing-button", + "panic-button", + ]; + EXPECTED_STATE.placements[CustomizableUI.AREA_ADDONS] = [ + "ext0-browser-action", + "ext1-browser-action", + "ext2-browser-action", + ]; + + let migratedState = migrateForward(SAVED_STATE); + + Assert.deepEqual( + migratedState, + EXPECTED_STATE, + "The saved state should not have changed after migration." + ); +}); + +/** + * Test that we won't overwrite existing placements within the addons panel + * if we migrate things over from the overflow panel. We'll prepend the + * migrated items to the addons panel instead. + */ +add_task(async function test_migrate_extension_buttons_no_overwrite() { + const SAVED_STATE = { + placements: { + "nav-bar": [ + "back-button", + "forward-button", + "spring", + "urlbar-container", + "save-to-pocket-button", + ], + "toolbar-menubar": [ + "home-button", + "menubar-items", + "spring", + "downloads-button", + ], + TabsToolbar: [ + "firefox-view-button", + "tabbrowser-tabs", + "new-tab-button", + "alltabs-button", + "developer-button", + ], + PersonalToolbar: ["personal-bookmarks", "fxa-toolbar-menu-button"], + "widget-overflow-fixed-list": [ + "ext0-browser-action", + "privatebrowsing-button", + "ext1-browser-action", + "panic-button", + "ext2-browser-action", + ], + "unified-extensions-area": ["ext3-browser-action", "ext4-browser-action"], + }, + }; + const EXPECTED_STATE = structuredClone(SAVED_STATE); + EXPECTED_STATE.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] = [ + "privatebrowsing-button", + "panic-button", + ]; + EXPECTED_STATE.placements[CustomizableUI.AREA_ADDONS] = [ + "ext0-browser-action", + "ext1-browser-action", + "ext2-browser-action", + "ext3-browser-action", + "ext4-browser-action", + ]; + + let migratedState = migrateForward(SAVED_STATE); + + Assert.deepEqual( + migratedState, + EXPECTED_STATE, + "The saved state should not have changed after migration." + ); +}); + +/** + * Test that extension buttons from areas other than the overflow panel + * won't be moved. + */ +add_task(async function test_migrate_extension_buttons_elsewhere() { + const SAVED_STATE = { + placements: { + "nav-bar": [ + "back-button", + "ext0-browser-action", + "forward-button", + "ext1-browser-action", + "spring", + "ext2-browser-action", + "urlbar-container", + "ext3-browser-action", + "save-to-pocket-button", + "ext4-browser-action", + ], + "toolbar-menubar": [ + "home-button", + "ext5-browser-action", + "menubar-items", + "ext6-browser-action", + "spring", + "ext7-browser-action", + "downloads-button", + "ext8-browser-action", + ], + TabsToolbar: [ + "firefox-view-button", + "ext9-browser-action", + "tabbrowser-tabs", + "ext10-browser-action", + "new-tab-button", + "ext11-browser-action", + "alltabs-button", + "ext12-browser-action", + "developer-button", + "ext13-browser-action", + ], + PersonalToolbar: [ + "personal-bookmarks", + "ext14-browser-action", + "fxa-toolbar-menu-button", + "ext15-browser-action", + ], + "widget-overflow-fixed-list": [ + "ext16-browser-action", + "privatebrowsing-button", + "ext17-browser-action", + "panic-button", + "ext18-browser-action", + ], + }, + }; + const EXPECTED_STATE = structuredClone(SAVED_STATE); + EXPECTED_STATE.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] = [ + "privatebrowsing-button", + "panic-button", + ]; + EXPECTED_STATE.placements[CustomizableUI.AREA_ADDONS] = [ + "ext16-browser-action", + "ext17-browser-action", + "ext18-browser-action", + ]; + + let migratedState = migrateForward(SAVED_STATE); + + Assert.deepEqual( + migratedState, + EXPECTED_STATE, + "The saved state should not have changed after migration." + ); +}); diff --git a/browser/components/customizableui/test/unit/xpcshell.ini b/browser/components/customizableui/test/unit/xpcshell.ini new file mode 100644 index 0000000000..ec14295805 --- /dev/null +++ b/browser/components/customizableui/test/unit/xpcshell.ini @@ -0,0 +1,6 @@ +[DEFAULT] +head = +skip-if = toolkit == 'android' # bug 1730213 +firefox-appdir = browser + +[test_unified_extensions_migration.js] |