Bug 1253486, [e10s only] hide select popups when the select element is removed, r=mconley
authorNeil Deakin <neil@mozilla.com>
Wed, 13 Apr 2016 13:15:55 -0400
changeset 330916 b00232b9f65b6195f4f01167747325fd6c140a9c
parent 330915 ac43dab284afd189ed2b0f4c1906a45e0c1736dd
child 330917 9424f63828ebaf3563cad850ce1cc5f865231f70
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1253486
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1253486, [e10s only] hide select popups when the select element is removed, r=mconley
browser/base/content/test/general/browser_selectpopup.js
layout/forms/nsListControlFrame.cpp
toolkit/content/widgets/remote-browser.xml
toolkit/modules/SelectContentHelper.jsm
toolkit/modules/SelectParentHelper.jsm
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -21,57 +21,67 @@ const PAGECONTENT =
   "  </optgroup>" +
   "  <option value='Six' disabled='true'>Six</option>" +
   "  <optgroup label='Third Group'>" +
   "    <option value='Seven'>   Seven  </option>" +
   "    <option value='Eight'>&nbsp;&nbsp;Eight&nbsp;&nbsp;</option>" +
   "  </optgroup></select><input />Text" +
   "</body></html>";
 
-function openSelectPopup(selectPopup, withMouse)
+const PAGECONTENT_SMALL =
+  "<html>" +
+  "<body><select id='one'>" +
+  "  <option value='One'>One</option>" +
+  "  <option value='Two'>Two</option>" +
+  "</select><select id='two'>" +
+  "  <option value='Three'>Three</option>" +
+  "  <option value='Four'>Four</option>" +
+  "</select><select id='three'>" +
+  "  <option value='Five'>Five</option>" +
+  "  <option value='Six'>Six</option>" +
+  "</select></body></html>";
+
+function openSelectPopup(selectPopup, withMouse, selector = "select")
 {
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
 
   if (withMouse) {
     return Promise.all([popupShownPromise,
-                        BrowserTestUtils.synthesizeMouseAtCenter("select", { }, gBrowser.selectedBrowser)]);
+                        BrowserTestUtils.synthesizeMouseAtCenter(selector, { }, gBrowser.selectedBrowser)]);
   }
 
   setTimeout(() => EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, code: "ArrowDown" }), 1500);
   return popupShownPromise;
 }
 
 function hideSelectPopup(selectPopup, withEscape)
 {
-  let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+  let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
 
   if (withEscape) {
     EventUtils.synthesizeKey("KEY_Escape", { code: "Escape" });
   }
   else {
     EventUtils.synthesizeKey("KEY_Enter", { code: "Enter" });
   }
 
-  return popupShownPromise;
+  return popupHiddenPromise;
 }
 
 function getChangeEvents()
 {
   return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     return content.wrappedJSObject.gChangeEvents;
   });
 }
 
 function doSelectTests(contentType, dtd)
 {
-  let tab = gBrowser.selectedTab = gBrowser.addTab();
-  let browser = gBrowser.getBrowserForTab(tab);
-  yield promiseTabLoadEvent(tab, "data:" + contentType + "," + escape(dtd + "\n" + PAGECONTENT));
-
-  yield SimpleTest.promiseFocus(browser.contentWindow);
+  const pageUrl = "data:" + contentType + "," + escape(dtd + "\n" + PAGECONTENT);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
   let menulist = document.getElementById("ContentSelectDropdown");
   let selectPopup = menulist.menupopup;
 
   yield openSelectPopup(selectPopup);
 
   let isWindows = navigator.platform.indexOf("Win") >= 0;
 
@@ -133,19 +143,62 @@ function doSelectTests(contentType, dtd)
   is((yield getChangeEvents()), isWindows ? 2 : 1, "Open and close with change - number of change events");
   EventUtils.synthesizeKey("VK_TAB", { });
   EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
   is((yield getChangeEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of change events");
 
   is(selectPopup.lastChild.previousSibling.label, "Seven", "Spaces collapsed");
   is(selectPopup.lastChild.label, "\xA0\xA0Eight\xA0\xA0", "Non-breaking spaces not collapsed");
 
-  gBrowser.removeCurrentTab();
+  yield BrowserTestUtils.removeTab(tab);
 }
 
 add_task(function*() {
   yield doSelectTests("text/html", "");
 });
 
 add_task(function*() {
   yield doSelectTests("application/xhtml+xml", XHTML_DTD);
 });
 
+// This test opens a select popup and removes the content node of a popup while
+// The popup should close if its node is removed.
+add_task(function*() {
+  const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+  let menulist = document.getElementById("ContentSelectDropdown");
+  let selectPopup = menulist.menupopup;
+
+  // First, try it when a different <select> element than the one that is open is removed
+  yield openSelectPopup(selectPopup, true, "#one");
+
+  yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.document.body.removeChild(content.document.getElementById("two"));
+  });
+
+  // Wait a bit just to make sure the popup won't close.
+  yield new Promise(resolve => setTimeout(resolve, 1000));
+  
+  is(selectPopup.state, "open", "Different popup did not affect open popup");
+
+  yield hideSelectPopup(selectPopup);
+
+  // Next, try it when the same <select> element than the one that is open is removed
+  yield openSelectPopup(selectPopup, true, "#three");
+
+  let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+  yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.document.body.removeChild(content.document.getElementById("three"));
+  });
+  yield popupHiddenPromise;
+
+  ok(true, "Popup hidden when select is removed");
+
+  // Finally, try it when the tab is closed while the select popup is open.
+  yield openSelectPopup(selectPopup, true, "#one");
+
+  popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
+  yield BrowserTestUtils.removeTab(tab);
+  yield popupHiddenPromise;
+
+  ok(true, "Popup hidden when tab is closed");
+});
--- a/layout/forms/nsListControlFrame.cpp
+++ b/layout/forms/nsListControlFrame.cpp
@@ -136,16 +136,24 @@ nsListControlFrame::DestroyFrom(nsIFrame
                                       mEventListener, false);
   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"),
                                       mEventListener, false);
   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mouseup"),
                                       mEventListener, false);
   mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousemove"),
                                       mEventListener, false);
 
+  if (XRE_IsContentProcess() &&
+      Preferences::GetBool("browser.tabs.remote.desktopbehavior", false)) {
+    nsContentUtils::AddScriptRunner(
+      new AsyncEventDispatcher(mContent,
+                               NS_LITERAL_STRING("mozhidedropdown"), true,
+                               true));
+  }
+
   nsFormControlFrame::RegUnRegAccessKey(static_cast<nsIFrame*>(this), false);
   nsHTMLScrollFrame::DestroyFrom(aDestructRoot);
 }
 
 void
 nsListControlFrame::BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                      const nsRect&           aDirtyRect,
                                      const nsDisplayListSet& aLists)
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -396,16 +396,22 @@
       </destructor>
 
       <!-- This is necessary because the destructor doesn't always get called when
            we are removed from a tabbrowser. This will be explicitly called by tabbrowser.
 
            Note: This overrides the destroy() method from browser.xml. -->
       <method name="destroy">
         <body><![CDATA[
+          // Make sure that any open select is closed.
+          if (this._selectParentHelper) {
+            let menulist = document.getElementById(this.getAttribute("selectmenulist"));
+            this._selectParentHelper.hide(menulist, this);
+          }
+
           if (this.mDestroyed)
             return;
           this.mDestroyed = true;
 
           try {
             this.controllers.removeController(this._controller);
           } catch (ex) {
             // This can fail when this browser element is not attached to a
@@ -478,17 +484,17 @@
               event.initEvent("ZoomChangeUsingMouseWheel", true, false);
               this.dispatchEvent(event);
               break;
             }
 
             case "Forms:HideDropDown": {
               if (this._selectParentHelper) {
                 let menulist = document.getElementById(this.getAttribute("selectmenulist"));
-                this._selectParentHelper.hide(menulist);
+                this._selectParentHelper.hide(menulist, this);
               }
               break;
             }
 
             case "DOMFullscreen:RequestExit": {
               let windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                                       .getInterface(Components.interfaces.nsIDOMWindowUtils);
               windowUtils.exitFullscreen();
--- a/toolkit/modules/SelectContentHelper.jsm
+++ b/toolkit/modules/SelectContentHelper.jsm
@@ -31,24 +31,26 @@ this.SelectContentHelper = function (aEl
 
 this.SelectContentHelper.prototype = {
   init: function() {
     this.global.addMessageListener("Forms:SelectDropDownItem", this);
     this.global.addMessageListener("Forms:DismissedDropDown", this);
     this.global.addMessageListener("Forms:MouseOver", this);
     this.global.addMessageListener("Forms:MouseOut", this);
     this.global.addEventListener("pagehide", this);
+    this.global.addEventListener("mozhidedropdown", this);
   },
 
   uninit: function() {
     this.global.removeMessageListener("Forms:SelectDropDownItem", this);
     this.global.removeMessageListener("Forms:DismissedDropDown", this);
     this.global.removeMessageListener("Forms:MouseOver", this);
     this.global.removeMessageListener("Forms:MouseOut", this);
     this.global.removeEventListener("pagehide", this);
+    this.global.removeEventListener("mozhidedropdown", this);
     this.element = null;
     this.global = null;
   },
 
   showDropDown: function() {
     let rect = this._getBoundingContentRect();
 
     this.global.sendAsyncMessage("Forms:ShowDropDown", {
@@ -97,16 +99,22 @@ this.SelectContentHelper.prototype = {
   handleEvent: function(event) {
     switch (event.type) {
       case "pagehide":
         if (this.element.ownerDocument === event.target) {
           this.global.sendAsyncMessage("Forms:HideDropDown", {});
           this.uninit();
         }
         break;
+      case "mozhidedropdown":
+        if (this.element === event.target) {
+          this.global.sendAsyncMessage("Forms:HideDropDown", {});
+          this.uninit();
+        }
+        break;
     }
   }
 
 }
 
 function getComputedDirection(element) {
   return element.ownerDocument.defaultView.getComputedStyle(element).getPropertyValue("direction");
 }
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -21,18 +21,20 @@ this.SelectParentHelper = {
     menulist.hidden = false;
     currentBrowser = browser;
     this._registerListeners(menulist.menupopup);
 
     menulist.menupopup.openPopupAtScreenRect("after_start", rect.left, rect.top, rect.width, rect.height, false, false);
     menulist.selectedItem.scrollIntoView();
   },
 
-  hide: function(menulist) {
-    menulist.menupopup.hidePopup();
+  hide: function(menulist, browser) {
+    if (currentBrowser == browser) {
+      menulist.menupopup.hidePopup();
+    }
   },
 
   handleEvent: function(event) {
     switch (event.type) {
       case "mouseover":
         currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {});
         break;