summaryrefslogtreecommitdiffstats
path: root/browser/components/customizableui/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/customizableui/test
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/customizableui/test')
-rw-r--r--browser/components/customizableui/test/CustomizableUITestUtils.sys.mjs156
-rw-r--r--browser/components/customizableui/test/browser.ini210
-rw-r--r--browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js133
-rw-r--r--browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js102
-rw-r--r--browser/components/customizableui/test/browser_1042100_default_placements_update.js231
-rw-r--r--browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js29
-rw-r--r--browser/components/customizableui/test/browser_1087303_button_fullscreen.js55
-rw-r--r--browser/components/customizableui/test/browser_1087303_button_preferences.js59
-rw-r--r--browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js24
-rw-r--r--browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js41
-rw-r--r--browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js109
-rw-r--r--browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js69
-rw-r--r--browser/components/customizableui/test/browser_1701883_restore_defaults_pocket_pref.js28
-rw-r--r--browser/components/customizableui/test/browser_1702200_PanelMultiView_header_separator.js76
-rw-r--r--browser/components/customizableui/test/browser_1795260_searchbar_overflow_toolbar.js29
-rw-r--r--browser/components/customizableui/test/browser_694291_searchbar_preference.js48
-rw-r--r--browser/components/customizableui/test/browser_873501_handle_specials.js89
-rw-r--r--browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js295
-rw-r--r--browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js44
-rw-r--r--browser/components/customizableui/test/browser_877006_missing_view.js46
-rw-r--r--browser/components/customizableui/test/browser_877178_unregisterArea.js70
-rw-r--r--browser/components/customizableui/test/browser_877447_skip_missing_ids.js35
-rw-r--r--browser/components/customizableui/test/browser_878452_drag_to_panel.js90
-rw-r--r--browser/components/customizableui/test/browser_884402_customize_from_overflow.js117
-rw-r--r--browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js73
-rw-r--r--browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js141
-rw-r--r--browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js58
-rw-r--r--browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js74
-rw-r--r--browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js30
-rw-r--r--browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js30
-rw-r--r--browser/components/customizableui/test/browser_901207_searchbar_in_panel.js139
-rw-r--r--browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js49
-rw-r--r--browser/components/customizableui/test/browser_913972_currentset_overflow.js91
-rw-r--r--browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js347
-rw-r--r--browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js46
-rw-r--r--browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js27
-rw-r--r--browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js29
-rw-r--r--browser/components/customizableui/test/browser_934113_menubar_removable.js43
-rw-r--r--browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js94
-rw-r--r--browser/components/customizableui/test/browser_938980_navbar_collapsed.js214
-rw-r--r--browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js45
-rw-r--r--browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js64
-rw-r--r--browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js134
-rw-r--r--browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js30
-rw-r--r--browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js49
-rw-r--r--browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js119
-rw-r--r--browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js26
-rw-r--r--browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js42
-rw-r--r--browser/components/customizableui/test/browser_947914_button_copy.js64
-rw-r--r--browser/components/customizableui/test/browser_947914_button_cut.js58
-rw-r--r--browser/components/customizableui/test/browser_947914_button_find.js37
-rw-r--r--browser/components/customizableui/test/browser_947914_button_history.js68
-rw-r--r--browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js62
-rw-r--r--browser/components/customizableui/test/browser_947914_button_newWindow.js62
-rw-r--r--browser/components/customizableui/test/browser_947914_button_paste.js55
-rw-r--r--browser/components/customizableui/test/browser_947914_button_print.js54
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomIn.js60
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomOut.js61
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomReset.js75
-rw-r--r--browser/components/customizableui/test/browser_947987_removable_default.js94
-rw-r--r--browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js47
-rw-r--r--browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js70
-rw-r--r--browser/components/customizableui/test/browser_956602_remove_special_widget.js37
-rw-r--r--browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js79
-rw-r--r--browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js48
-rw-r--r--browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js60
-rw-r--r--browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js47
-rw-r--r--browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js28
-rw-r--r--browser/components/customizableui/test/browser_970511_undo_restore_default.js274
-rw-r--r--browser/components/customizableui/test/browser_972267_customizationchange_events.js39
-rw-r--r--browser/components/customizableui/test/browser_976792_insertNodeInWindow.js597
-rw-r--r--browser/components/customizableui/test/browser_978084_dragEnd_after_move.js52
-rw-r--r--browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js96
-rw-r--r--browser/components/customizableui/test/browser_981305_separator_insertion.js89
-rw-r--r--browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js65
-rw-r--r--browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js82
-rw-r--r--browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js302
-rw-r--r--browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js56
-rw-r--r--browser/components/customizableui/test/browser_987177_destroyWidget_xul.js35
-rw-r--r--browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js142
-rw-r--r--browser/components/customizableui/test/browser_987492_window_api.js83
-rw-r--r--browser/components/customizableui/test/browser_987640_charEncoding.js78
-rw-r--r--browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js70
-rw-r--r--browser/components/customizableui/test/browser_989751_subviewbutton_class.js91
-rw-r--r--browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js25
-rw-r--r--browser/components/customizableui/test/browser_993322_widget_notoolbar.js59
-rw-r--r--browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js278
-rw-r--r--browser/components/customizableui/test/browser_996364_registerArea_different_properties.js142
-rw-r--r--browser/components/customizableui/test/browser_996635_remove_non_widgets.js54
-rw-r--r--browser/components/customizableui/test/browser_PanelMultiView.js566
-rw-r--r--browser/components/customizableui/test/browser_PanelMultiView_focus.js169
-rw-r--r--browser/components/customizableui/test/browser_PanelMultiView_keyboard.js582
-rw-r--r--browser/components/customizableui/test/browser_addons_area.js76
-rw-r--r--browser/components/customizableui/test/browser_allow_dragging_removable_false.js42
-rw-r--r--browser/components/customizableui/test/browser_backfwd_enabled_post_customize.js79
-rw-r--r--browser/components/customizableui/test/browser_bookmarks_empty_message.js35
-rw-r--r--browser/components/customizableui/test/browser_bookmarks_toolbar_collapsed_restore_default.js35
-rw-r--r--browser/components/customizableui/test/browser_bookmarks_toolbar_shown_newtab.js34
-rw-r--r--browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js81
-rw-r--r--browser/components/customizableui/test/browser_check_tooltips_in_navbar.js21
-rw-r--r--browser/components/customizableui/test/browser_create_button_widget.js87
-rw-r--r--browser/components/customizableui/test/browser_ctrl_click_panel_opening.js56
-rw-r--r--browser/components/customizableui/test/browser_currentset_post_reset.js37
-rw-r--r--browser/components/customizableui/test/browser_customization_context_menus.js633
-rw-r--r--browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js71
-rw-r--r--browser/components/customizableui/test/browser_customizemode_lwthemes.js25
-rw-r--r--browser/components/customizableui/test/browser_customizemode_uidensity.js230
-rw-r--r--browser/components/customizableui/test/browser_disable_commands_customize.js86
-rw-r--r--browser/components/customizableui/test/browser_drag_outside_palette.js53
-rw-r--r--browser/components/customizableui/test/browser_editcontrols_update.js307
-rw-r--r--browser/components/customizableui/test/browser_exit_background_customize_mode.js44
-rw-r--r--browser/components/customizableui/test/browser_flexible_space_area.js48
-rw-r--r--browser/components/customizableui/test/browser_help_panel_cloning.js90
-rw-r--r--browser/components/customizableui/test/browser_hidden_widget_overflow.js115
-rw-r--r--browser/components/customizableui/test/browser_history_after_appMenu.js35
-rw-r--r--browser/components/customizableui/test/browser_history_recently_closed.js198
-rw-r--r--browser/components/customizableui/test/browser_history_recently_closed_middleclick.js106
-rw-r--r--browser/components/customizableui/test/browser_history_restore_session.js52
-rw-r--r--browser/components/customizableui/test/browser_insert_before_moved_node.js51
-rw-r--r--browser/components/customizableui/test/browser_menubar_visibility.js66
-rw-r--r--browser/components/customizableui/test/browser_newtab_button_customizemode.js181
-rw-r--r--browser/components/customizableui/test/browser_open_from_popup.js24
-rw-r--r--browser/components/customizableui/test/browser_open_in_lazy_tab.js42
-rw-r--r--browser/components/customizableui/test/browser_overflow_use_subviews.js88
-rw-r--r--browser/components/customizableui/test/browser_palette_labels.js66
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications.js597
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js139
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js92
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js145
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_modals.js87
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js206
-rw-r--r--browser/components/customizableui/test/browser_panel_keyboard_navigation.js326
-rw-r--r--browser/components/customizableui/test/browser_panel_locationSpecific.js78
-rw-r--r--browser/components/customizableui/test/browser_panel_toggle.js53
-rw-r--r--browser/components/customizableui/test/browser_proton_moreTools_panel.js54
-rw-r--r--browser/components/customizableui/test/browser_proton_toolbar_hide_toolbarbuttons.js287
-rw-r--r--browser/components/customizableui/test/browser_registerArea.js28
-rw-r--r--browser/components/customizableui/test/browser_reload_tab.js99
-rw-r--r--browser/components/customizableui/test/browser_remote_attribute.js73
-rw-r--r--browser/components/customizableui/test/browser_remote_tabs_button.js100
-rw-r--r--browser/components/customizableui/test/browser_remove_customized_specials.js35
-rw-r--r--browser/components/customizableui/test/browser_reset_builtin_widget_currentArea.js26
-rw-r--r--browser/components/customizableui/test/browser_reset_dom_events.js34
-rw-r--r--browser/components/customizableui/test/browser_screenshot_button_disabled.js22
-rw-r--r--browser/components/customizableui/test/browser_sidebar_toggle.js58
-rw-r--r--browser/components/customizableui/test/browser_switch_to_customize_mode.js53
-rw-r--r--browser/components/customizableui/test/browser_synced_tabs_menu.js523
-rw-r--r--browser/components/customizableui/test/browser_tabbar_big_widgets.js32
-rw-r--r--browser/components/customizableui/test/browser_toolbar_collapsed_states.js112
-rw-r--r--browser/components/customizableui/test/browser_touchbar_customization.js21
-rw-r--r--browser/components/customizableui/test/browser_unified_extensions_reset.js91
-rw-r--r--browser/components/customizableui/test/browser_widget_animation.js84
-rw-r--r--browser/components/customizableui/test/browser_widget_recreate_events.js99
-rw-r--r--browser/components/customizableui/test/dummy_history_item.html2
-rw-r--r--browser/components/customizableui/test/head.js536
-rw-r--r--browser/components/customizableui/test/support/test_967000_charEncoding_page.html11
-rw-r--r--browser/components/customizableui/test/unit/test_unified_extensions_migration.js368
-rw-r--r--browser/components/customizableui/test/unit/xpcshell.ini6
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]