Bug 984455 - Bookmarks menu and toolbar context menus can be broken after underflowing from nav-bar chevron. r=mak,mdeboer,Gijs. a=sledru.
authorMike Conley <mconley@mozilla.com>
Tue, 08 Apr 2014 10:40:25 -0400
changeset 183664 3f2d6f68c415
parent 183663 552251cb84b9
child 183665 c19e0e0a8535
push id3439
push usermconley@mozilla.com
push date2014-04-08 14:42 +0000
Treeherderresults
reviewersmak, mdeboer, Gijs, sledru
bugs984455
milestone29.0
Bug 984455 - Bookmarks menu and toolbar context menus can be broken after underflowing from nav-bar chevron. r=mak,mdeboer,Gijs. a=sledru.
browser/base/content/browser-places.js
browser/base/content/browser.js
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_880164_customization_context_menus.js
browser/components/customizableui/test/browser_884402_customize_from_overflow.js
browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js
browser/components/customizableui/test/head.js
browser/components/places/content/browserPlacesViews.js
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -879,50 +879,63 @@ let PlacesToolbarHelper = {
     return document.getElementById("bookmarks-toolbar-placeholder");
   },
 
   init: function PTH_init(forceToolbarOverflowCheck) {
     let viewElt = this._viewElt;
     if (!viewElt || viewElt._placesView)
       return;
 
+    // CustomizableUI.addListener is idempotent, so we can safely
+    // call this multiple times.
+    CustomizableUI.addListener(this);
+
     // If the bookmarks toolbar item is:
     // - not in a toolbar, or;
     // - the toolbar is collapsed, or;
     // - the toolbar is hidden some other way:
     // don't initialize.  Also, there is no need to initialize the toolbar if
     // customizing, because that will happen when the customization is done.
     let toolbar = this._getParentToolbar(viewElt);
     if (!toolbar || toolbar.collapsed || this._isCustomizing ||
         getComputedStyle(toolbar, "").display == "none")
       return;
 
     new PlacesToolbar(this._place);
     if (forceToolbarOverflowCheck) {
       viewElt._placesView.updateOverflowStatus();
     }
-    this.customizeChange();
+    this._setupPlaceholder();
+  },
+
+  uninit: function PTH_uninit() {
+    CustomizableUI.removeListener(this);
   },
 
   customizeStart: function PTH_customizeStart() {
     try {
       let viewElt = this._viewElt;
       if (viewElt && viewElt._placesView)
         viewElt._placesView.uninit();
     } finally {
       this._isCustomizing = true;
     }
     this._shouldWrap = this._getShouldWrap();
   },
 
   customizeChange: function PTH_customizeChange() {
+    this._setupPlaceholder();
+  },
+
+  _setupPlaceholder: function PTH_setupPlaceholder() {
     let placeholder = this._placeholder;
     if (!placeholder) {
       return;
     }
+
     let shouldWrapNow = this._getShouldWrap();
     if (this._shouldWrap != shouldWrapNow) {
       if (shouldWrapNow) {
         placeholder.setAttribute("wrap", "true");
       } else {
         placeholder.removeAttribute("wrap");
       }
       this._shouldWrap = shouldWrapNow;
@@ -953,17 +966,50 @@ let PlacesToolbarHelper = {
   _getParentToolbar: function(element) {
     while (element) {
       if (element.localName == "toolbar") {
         return element;
       }
       element = element.parentNode;
     }
     return null;
-  }
+  },
+
+  onWidgetUnderflow: function(aNode, aContainer) {
+    // The view gets broken by being removed and reinserted by the overflowable
+    // toolbar, so we have to force an uninit and reinit.
+    let win = aNode.ownerDocument.defaultView;
+    if (aNode.id == "personal-bookmarks" && win == window) {
+      this._resetView();
+    }
+  },
+
+  onWidgetAdded: function(aWidgetId, aArea, aPosition) {
+    if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
+      // It's possible (with the "Add to Menu", "Add to Toolbar" context
+      // options) that the Places Toolbar Items have been moved without
+      // letting us prepare and handle it with with customizeStart and
+      // customizeDone. If that's the case, we need to reset the views
+      // since they're probably broken from the DOM reparenting.
+      this._resetView();
+    }
+  },
+
+  _resetView: function() {
+    if (this._viewElt) {
+      // It's possible that the placesView might not exist, and we need to
+      // do a full init. This could happen if the Bookmarks Toolbar Items are
+      // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
+      // context menu option, outside of customize mode.
+      if (this._viewElt._placesView) {
+        this._viewElt._placesView.uninit();
+      }
+      this.init(true);
+    }
+  },
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// BookmarkingUI
 
 /**
  * Handles the bookmarks menu-button in the toolbar.
  */
@@ -1159,16 +1205,26 @@ let BookmarkingUI = {
   },
 
   _uninitView: function BUI__uninitView() {
     // When an element with a placesView attached is removed and re-inserted,
     // XBL reapplies the binding causing any kind of issues and possible leaks,
     // so kill current view and let popupshowing generate a new one.
     if (this.button._placesView)
       this.button._placesView.uninit();
+
+    // We have to do the same thing for the "special" views underneath the
+    // the bookmarks menu.
+    const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"];
+    for (let viewNodeID of kSpecialViewNodeIDs) {
+      let elem = document.getElementById(viewNodeID);
+      if (elem && elem._placesView) {
+        elem._placesView.uninit();
+      }
+    }
   },
 
   onCustomizeStart: function BUI_customizeStart(aWindow) {
     if (aWindow == window) {
       this._uninitView();
       this._isCustomizing = true;
     }
   },
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1287,16 +1287,18 @@ var gBrowserInit = {
     Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed");
 
     try {
       gBrowser.removeProgressListener(window.XULBrowserWindow);
       gBrowser.removeTabsProgressListener(window.TabsProgressListener);
     } catch (ex) {
     }
 
+    PlacesToolbarHelper.uninit();
+
     BookmarkingUI.uninit();
 
     TabsInTitlebar.uninit();
 
     var enumerator = Services.wm.getEnumerator(null);
     enumerator.getNext();
     if (!enumerator.hasMoreElements()) {
       document.persist("sidebar-box", "sidebarcommand");
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -76,14 +76,18 @@ skip-if = os == "linux"
 [browser_975719_customtoolbars_behaviour.js]
 
 [browser_976792_insertNodeInWindow.js]
 skip-if = os == "linux"
 
 [browser_978084_dragEnd_after_move.js]
 [browser_980155_add_overflow_toolbar.js]
 [browser_981418-widget-onbeforecreated-handler.js]
+
+[browser_984455_bookmarks_items_reparenting.js]
+skip-if = os == "linux"
+
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_981305_separator_insertion.js]
 [browser_987177_destroyWidget_xul.js]
 [browser_987177_xul_wrapper_updating.js]
 [browser_989289_force_icons_mode_attribute.js]
 [browser_panel_toggle.js]
--- a/browser/components/customizableui/test/browser_880164_customization_context_menus.js
+++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const isOSX = (Services.appinfo.OS === "Darwin");
 
 // Right-click on the home button should
 // show a context menu with options to move it.
 add_task(function() {
   let contextMenu = document.getElementById("toolbar-context-menu");
-  let shownPromise = contextMenuShown(contextMenu);
+  let shownPromise = popupShown(contextMenu);
   let homeButton = document.getElementById("home-button");
   EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2 });
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToPanel", true],
     [".customize-context-removeFromToolbar", true],
     ["---"]
@@ -25,27 +25,27 @@ add_task(function() {
   }
   expectedEntries.push(
     ["#toggle_PersonalToolbar", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   );
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenPromise = contextMenuHidden(contextMenu);
+  let hiddenPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield 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(function() {
   let contextMenu = document.getElementById("toolbar-context-menu");
-  let shownPromise = contextMenuShown(contextMenu);
+  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 });
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToPanel", false],
@@ -58,28 +58,28 @@ add_task(function() {
   expectedEntries.push(
     ["#toggle_PersonalToolbar", true],
     ["#toggle_880164_empty_toolbar", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   );
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenPromise = contextMenuHidden(contextMenu);
+  let hiddenPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenPromise;
   removeCustomToolbars();
 });
 
 
 // Right-click on the urlbar-container should
 // show a context menu with disabled options to move it.
 add_task(function() {
   let contextMenu = document.getElementById("toolbar-context-menu");
-  let shownPromise = contextMenuShown(contextMenu);
+  let shownPromise = popupShown(contextMenu);
   let urlBarContainer = document.getElementById("urlbar-container");
   // Need to make sure not to click within an edit field.
   let urlbarRect = urlBarContainer.getBoundingClientRect();
   EventUtils.synthesizeMouse(urlBarContainer, 100, urlbarRect.height - 1, {type: "contextmenu", button: 2 });
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToPanel", false],
@@ -91,17 +91,17 @@ add_task(function() {
   }
   expectedEntries.push(
     ["#toggle_PersonalToolbar", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   );
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenPromise = contextMenuHidden(contextMenu);
+  let hiddenPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenPromise;
 });
 
 // Right-click on the searchbar and moving it to the menu
 // and back should move the search-container instead.
 add_task(function() {
   let searchbar = document.getElementById("searchbar");
@@ -130,47 +130,47 @@ add_task(function() {
 // Right-click on an item within the menu panel should
 // show a context menu with options to move it.
 add_task(function() {
   let shownPanelPromise = promisePanelShown(window);
   PanelUI.toggle({type: "command"});
   yield shownPanelPromise;
 
   let contextMenu = document.getElementById("customizationPanelItemContextMenu");
-  let shownContextPromise = contextMenuShown(contextMenu);
+  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});
   yield shownContextPromise;
 
   is(PanelUI.panel.state, "open", "The PanelUI should still be open.");
 
   let expectedEntries = [
     [".customize-context-moveToToolbar", true],
     [".customize-context-removeFromPanel", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   ];
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenContextPromise;
 
   let hiddenPromise = promisePanelHidden(window);
   PanelUI.toggle({type: "command"});
   yield hiddenPromise;
 });
 
 // Right-click on the home button while in customization mode
 // should show a context menu with options to move it.
 add_task(function() {
   yield startCustomizing();
   let contextMenu = document.getElementById("toolbar-context-menu");
-  let shownPromise = contextMenuShown(contextMenu);
+  let shownPromise = popupShown(contextMenu);
   let homeButton = document.getElementById("wrapper-home-button");
   EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2});
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToPanel", true],
     [".customize-context-removeFromToolbar", true],
     ["---"]
@@ -180,114 +180,114 @@ add_task(function() {
   }
   expectedEntries.push(
     ["#toggle_PersonalToolbar", true],
     ["---"],
     [".viewCustomizeToolbar", false]
   );
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenContextPromise;
 });
 
 // Right-click on an item in the palette should
 // show a context menu with options to move it.
 add_task(function() {
   let contextMenu = document.getElementById("customizationPaletteItemContextMenu");
-  let shownPromise = contextMenuShown(contextMenu);
+  let shownPromise = popupShown(contextMenu);
   let openFileButton = document.getElementById("wrapper-open-file-button");
   EventUtils.synthesizeMouse(openFileButton, 2, 2, {type: "contextmenu", button: 2});
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-addToToolbar", true],
     [".customize-context-addToPanel", true]
   ];
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield 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(function() {
   let contextMenu = document.getElementById("customizationPanelItemContextMenu");
-  let shownPromise = contextMenuShown(contextMenu);
+  let shownPromise = popupShown(contextMenu);
   let newWindowButton = document.getElementById("wrapper-new-window-button");
   EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2});
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToToolbar", true],
     [".customize-context-removeFromPanel", true],
     ["---"],
     [".viewCustomizeToolbar", false]
   ];
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenContextPromise;
   yield endCustomizing();
 });
 
 // Test the toolbarbutton panel context menu in customization mode
 // without opening the panel before customization mode
 add_task(function() {
   this.otherWin = yield openAndLoadWindow(null, true);
 
   yield startCustomizing(this.otherWin);
 
   let contextMenu = this.otherWin.document.getElementById("customizationPanelItemContextMenu");
-  let shownPromise = contextMenuShown(contextMenu);
+  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);
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToToolbar", true],
     [".customize-context-removeFromPanel", true],
     ["---"],
     [".viewCustomizeToolbar", false]
   ];
   checkContextMenu(contextMenu, expectedEntries, this.otherWin);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenContextPromise;
   yield endCustomizing(this.otherWin);
   yield promiseWindowClosed(this.otherWin);
   this.otherWin = null;
 });
 
 // Bug 945191 - Combined buttons show wrong context menu options
 // when they are in the toolbar.
 add_task(function() {
   yield startCustomizing();
   let contextMenu = document.getElementById("customizationPanelItemContextMenu");
-  let shownPromise = contextMenuShown(contextMenu);
+  let shownPromise = popupShown(contextMenu);
   let zoomControls = document.getElementById("wrapper-zoom-controls");
   EventUtils.synthesizeMouse(zoomControls, 2, 2, {type: "contextmenu", button: 2});
   yield shownPromise;
   // Execute the command to move the item from the panel to the toolbar.
   contextMenu.childNodes[0].doCommand();
-  let hiddenPromise = contextMenuHidden(contextMenu);
+  let hiddenPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenPromise;
   yield 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 = contextMenuShown(contextMenu);
+  shownPromise = popupShown(contextMenu);
   EventUtils.synthesizeMouse(zoomControls, 2, 2, {type: "contextmenu", button: 2});
   yield shownPromise;
 
   let expectedEntries = [
     [".customize-context-moveToPanel", true],
     [".customize-context-removeFromToolbar", true],
     ["---"]
   ];
@@ -296,47 +296,47 @@ add_task(function() {
   }
   expectedEntries.push(
     ["#toggle_PersonalToolbar", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   );
   checkContextMenu(contextMenu, expectedEntries);
 
-  hiddenPromise = contextMenuHidden(contextMenu);
+  hiddenPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenPromise;
   yield resetCustomization();
 });
 
 // Bug 947586 - After customization, panel items show wrong context menu options
 add_task(function() {
   yield startCustomizing();
   yield endCustomizing();
 
   yield PanelUI.show();
 
   let contextMenu = document.getElementById("customizationPanelItemContextMenu");
-  let shownContextPromise = contextMenuShown(contextMenu);
+  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});
   yield shownContextPromise;
 
   is(PanelUI.panel.state, "open", "The PanelUI should still be open.");
 
   let expectedEntries = [
     [".customize-context-moveToToolbar", true],
     [".customize-context-removeFromPanel", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   ];
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenContextPromise;
 
   let hiddenPromise = promisePanelHidden(window);
   PanelUI.hide();
   yield hiddenPromise;
 });
 
--- a/browser/components/customizableui/test/browser_884402_customize_from_overflow.js
+++ b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js
@@ -22,17 +22,17 @@ add_task(function() {
   ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
 
   let chevron = document.getElementById("nav-bar-overflow-button");
   let shownPanelPromise = promisePanelElementShown(window, overflowPanel);
   chevron.click();
   yield shownPanelPromise;
 
   let contextMenu = document.getElementById("toolbar-context-menu");
-  let shownContextPromise = contextMenuShown(contextMenu);
+  let shownContextPromise = popupShown(contextMenu);
   let homeButton = document.getElementById("home-button");
   ok(homeButton, "home-button was found");
   is(homeButton.getAttribute("overflowedItem"), "true", "Home button is overflowing");
   EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2});
   yield shownContextPromise;
 
   is(overflowPanel.state, "open", "The widget overflow panel should still be open.");
 
@@ -46,17 +46,17 @@ add_task(function() {
   }
   expectedEntries.push(
     ["#toggle_PersonalToolbar", true],
     ["---"],
     [".viewCustomizeToolbar", true]
   );
   checkContextMenu(contextMenu, expectedEntries);
 
-  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenContextPromise = popupHidden(contextMenu);
   let hiddenPromise = promisePanelElementHidden(window, overflowPanel);
   let moveToPanel = contextMenu.querySelector(".customize-context-moveToPanel");
   if (moveToPanel) {
     moveToPanel.click();
   }
   contextMenu.hidePopup();
   yield hiddenContextPromise;
   yield hiddenPromise;
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js
@@ -0,0 +1,256 @@
+/* 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 gNavBar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+let gOverflowList = document.getElementById(gNavBar.getAttribute("overflowtarget"));
+
+const kBookmarksButton = "bookmarks-menu-button";
+const kBookmarksItems = "personal-bookmarks";
+const kOriginalWindowWidth = window.outerWidth;
+const kSmallWidth = 400;
+
+/**
+ * Helper function that opens the bookmarks menu, and returns a Promise that
+ * resolves as soon as the menu is ready for interaction.
+ */
+function bookmarksMenuPanelShown() {
+  let deferred = Promise.defer();
+  let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup");
+  let onTransitionEnd = (e) => {
+    if (e.target == bookmarksMenuPopup) {
+      bookmarksMenuPopup.removeEventListener("transitionend", onTransitionEnd);
+      deferred.resolve();
+    }
+  }
+  bookmarksMenuPopup.addEventListener("transitionend", onTransitionEnd);
+  return deferred.promise;
+}
+
+/**
+ * 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 hte
+ *        right click on in order to open the context menu.
+ */
+function checkPlacesContextMenu(aItemWithContextMenu) {
+  return Task.spawn(function* () {
+    let contextMenu = document.getElementById("placesContext");
+    let newBookmarkItem = document.getElementById("placesContext_new:bookmark");
+    let shownPromise = popupShown(contextMenu);
+    EventUtils.synthesizeMouseAtCenter(aItemWithContextMenu,
+                                       {type: "contextmenu", button: 2});
+    yield shownPromise;
+
+    ok(!newBookmarkItem.hasAttribute("disabled"),
+       "New bookmark item shouldn't be disabled");
+
+    yield closePopup(contextMenu);
+  });
+}
+
+/**
+ * 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 Task.spawn(function* () {
+    let contextMenu = document.getElementById("placesContext");
+    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();
+    let dropmarker = document.getAnonymousElementByAttribute(bookmarksMenuButton,
+                                                             "anonid", "dropmarker");
+    EventUtils.synthesizeMouseAtCenter(dropmarker, {});
+    info("Waiting for bookmarks menu popup to show after clicking dropmarker.")
+    yield shownPromise;
+
+    for (let menuID in kSpecialItemIDs) {
+      let menuItem = document.getElementById(menuID);
+      let menuPopup = document.getElementById(kSpecialItemIDs[menuID]);
+      let shownPromise = popupShown(menuPopup);
+      EventUtils.synthesizeMouseAtCenter(menuItem, {});
+      yield shownPromise;
+
+      yield checkPlacesContextMenu(menuPopup);
+      yield closePopup(menuPopup);
+    }
+
+    yield 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("VK_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 Task.spawn(function*() {
+    let chevronPopup = document.getElementById("PlacesChevronPopup");
+    let shownPromise = popupShown(chevronPopup);
+    let chevron = document.getElementById("PlacesChevron");
+    EventUtils.synthesizeMouseAtCenter(chevron, {});
+    yield shownPromise;
+    yield waitForCondition(() => {
+      for (let child of chevronPopup.children) {
+        if (child.style.visibility != "hidden")
+          return true;
+      }
+    });
+    yield checkPlacesContextMenu(chevronPopup);
+    yield 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() {
+  window.resizeTo(kSmallWidth, window.outerHeight);
+  return 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() {
+  window.resizeTo(kOriginalWindowWidth, window.outerHeight);
+  return 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(function* testOverflowingBookmarksButtonContextMenu() {
+  ok(!gNavBar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
+  ok(CustomizableUI.inDefaultState, "Should start in default state.");
+
+  // Open the Unsorted and Bookmarks Toolbar context menus and ensure
+  // that they have views attached.
+  yield checkSpecialContextMenus();
+
+  yield overflowEverything();
+  checkOverflowing(kBookmarksButton);
+
+  yield stopOverflowing();
+  checkNotOverflowing(kBookmarksButton);
+
+  yield 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(function* testOverflowingBookmarksItemsContextMenu() {
+  yield PanelUI.ensureReady();
+
+  let bookmarksToolbarItems = document.getElementById(kBookmarksItems);
+  gCustomizeMode.addToToolbar(bookmarksToolbarItems);
+  yield checkPlacesContextMenu(bookmarksToolbarItems);
+
+  yield overflowEverything();
+  checkOverflowing(kBookmarksItems)
+
+  gCustomizeMode.addToPanel(bookmarksToolbarItems);
+
+  yield stopOverflowing();
+
+  gCustomizeMode.addToToolbar(bookmarksToolbarItems);
+  yield 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(function* testOverflowingBookmarksItemsChevronContextMenu() {
+  // If it's not already there, let's move the bookmarks toolbar items to
+  // the nav-bar.
+  let bookmarksToolbarItems = document.getElementById(kBookmarksItems);
+  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";
+  yield waitForCondition(() => !placesChevron.collapsed);
+
+  yield checkBookmarksItemsChevronContextMenu();
+
+  yield overflowEverything();
+  checkOverflowing(kBookmarksItems);
+
+  yield stopOverflowing();
+  checkNotOverflowing(kBookmarksItems);
+
+  yield checkBookmarksItemsChevronContextMenu();
+
+  placesToolbarItems.style.removeProperty("max-width");
+});
+
+add_task(function* asyncCleanup() {
+  window.resizeTo(kOriginalWindowWidth, window.outerHeight);
+  yield resetCustomization();
+});
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -423,47 +423,60 @@ function promiseTabHistoryNavigation(aDi
   }
   gBrowser.addEventListener("pageshow", listener, true);
 
   content.history.go(aDirection);
 
   return deferred.promise;
 }
 
-function contextMenuShown(aContextMenu) {
+function popupShown(aPopup) {
+  return promisePopupEvent(aPopup, "shown");
+}
+
+function popupHidden(aPopup) {
+  return promisePopupEvent(aPopup, "hidden");
+}
+
+/**
+ * Returns a Promise that resolves when aPopup fires an event of type
+ * aEventType. Times out and rejects after 20 seconds.
+ *
+ * @param aPopup the popup to monitor for events.
+ * @param aEventSuffix the _suffix_ for the popup event type to watch for.
+ *
+ * Example usage:
+ *   let popupShownPromise = promisePopupEvent(somePopup, "shown");
+ *   // ... something that opens a popup
+ *   yield popupShownPromise;
+ *
+ *  let popupHiddenPromise = promisePopupEvent(somePopup, "hidden");
+ *  // ... something that hides a popup
+ *  yield popupHiddenPromise;
+ */
+function promisePopupEvent(aPopup, aEventSuffix) {
   let deferred = Promise.defer();
-  let win = aContextMenu.ownerDocument.defaultView;
+  let win = aPopup.ownerDocument.defaultView;
+  let eventType = "popup" + aEventSuffix;
+
   let timeoutId = win.setTimeout(() => {
-    deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds.");
+    deferred.reject("Context menu (" + aPopup.id + ") did not fire "
+                    + eventType + " within 20 seconds.");
   }, 20000);
-  function onPopupShown(e) {
-    aContextMenu.removeEventListener("popupshown", onPopupShown);
+
+  function onPopupEvent(e) {
     win.clearTimeout(timeoutId);
+    aPopup.removeEventListener(eventType, onPopupEvent);
     deferred.resolve();
   };
-  aContextMenu.addEventListener("popupshown", onPopupShown);
+
+  aPopup.addEventListener(eventType, onPopupEvent);
   return deferred.promise;
 }
 
-function contextMenuHidden(aContextMenu) {
-  let deferred = Promise.defer();
-  let win = aContextMenu.ownerDocument.defaultView;
-  let timeoutId = win.setTimeout(() => {
-    deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds.");
-  }, 20000);
-  function onPopupHidden(e) {
-    win.clearTimeout(timeoutId);
-    aContextMenu.removeEventListener("popuphidden", onPopupHidden);
-    deferred.resolve();
-  };
-  aContextMenu.addEventListener("popuphidden", onPopupHidden);
-  return deferred.promise;
-}
-
-
 // This is a simpler version of the context menu check that
 // exists in contextmenu_common.js.
 function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) {
   let childNodes = aContextMenu.childNodes;
   for (let i = 0; i < childNodes.length; i++) {
     let menuitem = childNodes[i];
     try {
       if (aExpectedEntries[i][0] == "---") {
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -967,16 +967,20 @@ PlacesToolbar.prototype = {
   uninit: function PT_uninit() {
     this._removeEventListeners(this._viewElt, this._cbEvents, false);
     this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
                                true);
     this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
     this._removeEventListeners(window, ["resize", "unload"], false);
     this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
 
+    if (this._chevron._placesView) {
+      this._chevron._placesView.uninit();
+    }
+
     PlacesViewBase.prototype.uninit.apply(this, arguments);
   },
 
   _openedMenuButton: null,
   _allowPopupShowing: true,
 
   _rebuild: function PT__rebuild() {
     // Clear out references to existing nodes, since they will be removed