Bug 622577 - Most content is not rendered after these steps on bed4less.nl [r=mfinkle]
authorVivien Nicolas <21@vingtetun.org>
Tue, 04 Jan 2011 15:56:29 +0100
changeset 67204 a58bc442244ebdbd2028c9ee7c06317d8194c6d6
parent 67203 96527fcfe62cb0cf6590155cc38f334d95cac05f
child 67205 90d68c658bee1814552a2867349f4ebfde3b1b13
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs622577
Bug 622577 - Most content is not rendered after these steps on bed4less.nl [r=mfinkle]
mobile/chrome/content/browser-ui.js
mobile/chrome/content/forms.js
mobile/chrome/tests/browser_forms.html
mobile/chrome/tests/browser_forms.js
mobile/chrome/tests/head.js
mobile/chrome/tests/remote_forms.js
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -1981,55 +1981,59 @@ var FormHelperUI = {
 
           self._zoom(this._currentElementRect, this._currentCaretRect);
         }, 0, this);
         break;
     }
   },
 
   receiveMessage: function formHelperReceiveMessage(aMessage) {
+    if (!this._open && aMessage.name != "FormAssist:Show" && aMessage.name != "FormAssist:Hide")
+      return;
+
     let json = aMessage.json;
     switch (aMessage.name) {
       case "FormAssist:Show":
         // if the user has manually disabled the Form Assistant UI we still
         // want to show a UI for <select /> element but not managed by
         // FormHelperUI
         this.enabled ? this.show(json.current, json.hasPrevious, json.hasNext)
                      : SelectHelperUI.show(json.current.choices);
         break;
 
+      case "FormAssist:Hide":
+        this.enabled ? this.hide()
+                     : SelectHelperUI.hide();
+        break;
+
       case "FormAssist:Resize":
         let element = json.current;
         this._zoom(Rect.fromRect(element.rect), Rect.fromRect(element.caretRect));
         break;
 
-      case "FormAssist:Hide":
-        this.hide();
-        break;
-
       case "FormAssist:AutoComplete":
         this._updateAutocompleteFor(json.current);
         this._container.contentHasChanged();
         break;
 
        case "FormAssist:Update":
         Browser.hideSidebars();
         Browser.hideTitlebar();
         this._zoom(null, Rect.fromRect(json.caretRect));
         break;
 
       case "DOMWillOpenModalDialog":
-        if (this._open && aMessage.target == Browser.selectedBrowser) {
+        if (aMessage.target == Browser.selectedBrowser) {
           this._container.style.display = "none";
           this._container._spacer.hidden = true;
         }
         break;
 
       case "DOMModalDialogClosed":
-        if (this._open && aMessage.target == Browser.selectedBrowser) {
+        if (aMessage.target == Browser.selectedBrowser) {
           this._container.style.display = "-moz-box";
           this._container._spacer.hidden = false;
         }
         break;
     }
   },
 
   goToPrevious: function formHelperGoToPrevious() {
--- a/mobile/chrome/content/forms.js
+++ b/mobile/chrome/content/forms.js
@@ -66,18 +66,18 @@ function FormAssistant() {
   addMessageListener("FormAssist:Next", this);
   addMessageListener("FormAssist:ChoiceSelect", this);
   addMessageListener("FormAssist:ChoiceChange", this);
   addMessageListener("FormAssist:AutoComplete", this);
   addMessageListener("Content:SetWindowSize", this);
 
   addEventListener("keyup", this, false);
   addEventListener("focus", this, true);
-  addEventListener("DOMWindowCreated", this, false);
   addEventListener("pageshow", this, false);
+  addEventListener("pagehide", this, false);
 
   this._enabled = Services.prefs.getBoolPref("formhelper.enabled");
 };
 
 FormAssistant.prototype = {
   _selectWrapper: null,
   _currentIndex: -1,
   _elements: [],
@@ -88,46 +88,41 @@ FormAssistant.prototype = {
 
   get currentIndex() {
     return this._currentIndex;
   },
 
   set currentIndex(aIndex) {
     let element = this._elements[aIndex];
     if (element) {
-      this.focusSync = false;
+      this._currentIndex = aIndex;
       gFocusManager.setFocus(element, Ci.nsIFocusManager.FLAG_NOSCROLL);
-      this.focusSync = true;
-      this._currentIndex = aIndex;
       sendAsyncMessage("FormAssist:Show", this._getJSON());
     }
     return element;
   },
 
   _open: false,
   open: function formHelperOpen(aElement) {
     // if the click is on an option element we want to check if the parent is a valid target
-    if (aElement instanceof HTMLOptionElement && aElement.parentNode instanceof HTMLSelectElement) {
+    if (aElement instanceof HTMLOptionElement && aElement.parentNode instanceof HTMLSelectElement && !aElement.disabled) {
       aElement = aElement.parentNode;
     }
 
     // bug 526045 - the form assistant will close if a click happen:
     // * outside of the scope of the form helper
     // * hover a button of type=[image|submit]
     // * hover a disabled element
     if (!this._isValidElement(aElement)) {
       let passiveButtons = { button: true, checkbox: true, file: true, radio: true, reset: true };
       if ((aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) &&
           passiveButtons[aElement.type] && !aElement.disabled)
         return false;
 
-      sendAsyncMessage("FormAssist:Hide", { });
-      this._currentIndex = -1;
-      this._elements = [];
-      return this._open = false;
+      return this.close();
     }
 
     // Look for a top editable element
     if (this._isEditable(aElement))
       aElement = this._getTopLevelEditable(aElement);
 
     // Checking if the element is the current focused one while the form assistant is open
     // allow the user to reposition the caret into an input element
@@ -139,46 +134,53 @@ FormAssistant.prototype = {
         aElement.blur();
         aElement.focus();
       }
 
       // If the element is a <select/> element and the user has manually click
       // it we need to inform the UI of such a change to keep in sync with the
       // new selected options once the event is finished
       if (aElement instanceof HTMLSelectElement) {
-        let self = this;
-        let timer = new Util.Timeout(function() {
+        this._executeDelayed(function(self) {
           sendAsyncMessage("FormAssist:Show", self._getJSON());
         });
-        timer.once(0);
       }
 
       return false;
     }
 
-    // If form assistant is disabled but the element of a type of choice list
+    // If form assistant is disabled but the element is a type of choice list
     // we still want to show the simple select list
     this._enabled = Services.prefs.getBoolPref("formhelper.enabled");
-    if (!this._enabled && !this._isSelectElement(aElement)) {
-      sendAsyncMessage("FormAssist:Hide", { });
-      return this._open = false;
-    }
+    if (!this._enabled && !this._isSelectElement(aElement))
+      return this.close();
 
     if (this._enabled) {
       this._elements = [];
       this.currentIndex = this._getAllElements(aElement);
     }
     else {
       this._elements = [aElement];
       this.currentIndex = 0;
     }
 
     return this._open = true;
   },
 
+  close: function close() {
+    if (this._open) {
+      this._currentIndex = -1;
+      this._elements = [];
+      sendAsyncMessage("FormAssist:Hide", { });
+      this._open = false;
+    }
+
+    return this._open;
+  },
+
   receiveMessage: function receiveMessage(aMessage) {
     let currentElement = this.currentElement;
     if ((!this._enabled && !getWrapperForElement(currentElement)) || !currentElement)
       return;
 
     let json = aMessage.json;
     switch (aMessage.name) {
       case "FormAssist:Previous":
@@ -205,26 +207,24 @@ FormAssistant.prototype = {
         // ChoiceChange could happened once we have move to an other element or
         // to nothing, so we should keep the used wrapper in mind
         this._selectWrapper.fireOnChange();
 
         // New elements can be shown when a select is updated so we need to
         // reconstruct the inner elements array and to take care of possible
         // focus change, this is why we use "self.currentElement" instead of 
         // using directly "currentElement".
-        let self = this;
-        let timer = new Util.Timeout(function() {
+        this._executeDelayed(function(self) {
           let currentElement = self.currentElement;
           if (!currentElement)
             return;
 
           self._elements = [];
           self._currentIndex = self._getAllElements(currentElement);
         });
-        timer.once(0);
         break;
       }
 
       case "FormAssist:AutoComplete": {
         currentElement.value = json.value;
 
         let event = currentElement.ownerDocument.createEvent("Events");
         event.initEvent("DOMAutoComplete", true, true);
@@ -251,53 +251,54 @@ FormAssistant.prototype = {
         return true;
       }
     }
     return false;
   },
 
   focusSync: false,
   handleEvent: function formHelperHandleEvent(aEvent) {
-    if (!this._enabled || (!this.currentElement && aEvent.type != "focus") || (aEvent.type == "focus" && !this.focusSync))
+    // focus changes should be taken into account only if the user has done a
+    // manual operation like manually clicking
+    let shouldIgnoreFocus = (aEvent.type == "focus" && !this._open && !this.focusSync);
+    if ((!this._open && aEvent.type != "focus") || shouldIgnoreFocus)
       return;
 
     switch (aEvent.type) {
-      case "DOMWindowCreated":
-        this.focusSync = false;
-        break;
+      case "pagehide":
       case "pageshow":
-        this.focusSync = true;
+        // When reacting to a page show/hide, if the focus is different this
+        // could mean the web page has dramatically changed because of
+        // an Ajax change based on fragment identifier
+        if (gFocusManager.focusedElement != this.currentElement)
+          this.close();
         break;
       case "focus":
         let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {}) || aEvent.target;
 
         // If a body element is editable and the body is the child of an
-        // iframe we can assume this is an advanced HTML editor, so let's 
+        // iframe we can assume this is an advanced HTML editor, so let's
         // redirect the form helper selection to the iframe element
         if (focusedElement && this._isEditable(focusedElement)) {
           let editableElement = this._getTopLevelEditable(focusedElement);
           if (this._isValidElement(editableElement)) {
-            let self = this;
-            let timer = new Util.Timeout(function() {
+            this._executeDelayed(function(self) {
               self.open(editableElement);
             });
-            timer.once(0);
           }
           return;
         }
 
         // if an element is focused while we're closed but the element can be handle
-        // by the assistant, try to activate it
+        // by the assistant, try to activate it (only during mouseup)
         if (!this.currentElement) {
           if (focusedElement && this._isValidElement(focusedElement)) {
-            let self = this;
-            let timer = new Util.Timeout(function() {
+            this._executeDelayed(function(self) {
               self.open(focusedElement);
             });
-            timer.once(0);
           }
           return;
         }
 
         let focusedIndex = this._getIndexForElement(focusedElement);
         if (focusedIndex != -1 && this.currentIndex != focusedIndex)
           this.currentIndex = focusedIndex;
         break;
@@ -345,16 +346,24 @@ FormAssistant.prototype = {
         }
 
         let caretRect = this._getCaretRect();
         if (!caretRect.isEmpty())
           sendAsyncMessage("FormAssist:Update", { caretRect: caretRect });
     }
   },
 
+  _executeDelayed: function formHelperExecuteSoon(aCallback) {
+    let self = this;
+    let timer = new Util.Timeout(function() {
+      aCallback(self);
+    });
+    timer.once(0);
+  },
+
   _filterEditables: function formHelperFilterEditables(aNodes) {
     let result = [];
     for (let i = 0; i < aNodes.length; i++) {
       let node = aNodes[i];
 
       // Avoid checking the top level editable element of each node
       if (this._isEditable(node)) {
         let editableElement = this._getTopLevelEditable(node);
@@ -362,17 +371,17 @@ FormAssistant.prototype = {
           result.push(editableElement);
       }
       else {
         result.push(node);
       }
     }
     return result;
   },
-  
+
   _isEditable: function formHelperIsEditable(aElement) {
     let canEdit = false;
 
     if (aElement.isContentEditable || aElement.designMode == "on") {
       canEdit = true;
     } else if (aElement instanceof HTMLIFrameElement && (aElement.contentDocument.body.isContentEditable || aElement.contentDocument.designMode == "on")) {
       canEdit = true;
     } else {
--- a/mobile/chrome/tests/browser_forms.html
+++ b/mobile/chrome/tests/browser_forms.html
@@ -15,17 +15,17 @@
     <input id="root" type="text" tabindex="1">text</input>
     <button type="submit" tabindex="5"></button>
     <div role="button" tabindex="6">click here</div><!-- XXX click!!! button -->
     <input type="password" tabindex="-1"></input>
     <input type="image" tabindex="7"></input>
     <div contenteditable="true" tabindex="8"></div>
     <textarea id="next">textarea</textarea>
     <select id="select">
-      <option>option 1</option>
+      <option id="option">option 1</option>
       <option>option 2</option>
     </select>
     <input id="dumb" type="dumb">dumb type</input>
 
     <!-- These element should not work -->
     <div>div</div>
     <input type="text" disabled="true"></input>
     <input type="hidden"></input>
--- a/mobile/chrome/tests/browser_forms.js
+++ b/mobile/chrome/tests/browser_forms.js
@@ -27,35 +27,95 @@ function onTabLoaded() {
 function testMouseEvents() {
   // Sending a synthesized event directly on content should not work - we
   // don't want web content to be able to open the form helper without the
   // user consent, so we have to pass throught the canvas tile-container
   AsyncTests.waitFor("Test:Click", {}, function(json) {
     is(json.result, false, "Form Assistant should stay closed");
   });
 
+  AsyncTests.waitFor("Test:Focus", { value: "#root" }, function(json) {
+    is(json.result, false, "Form Assistant should stay closed");
+  });
+
+  AsyncTests.waitFor("Test:FocusRedirect", { value: "*[tabindex='0']" }, function(json) {
+    is(json.result, false, "Form Assistant should stay closed");
+    testOpenUIWithSyncFocus();
+  });
+};
+
+function testOpenUIWithSyncFocus() {
+  AsyncTests.waitFor("Test:Open", { value: "*[tabindex='0']" }, function(json) {
+    ok(FormHelperUI._open, "Form Assistant should be open");
+    testOpenUI();
+  });
+};
+
+function testOpenUI() {
   AsyncTests.waitFor("Test:Open", { value: "*[tabindex='0']" }, function(json) {
     ok(FormHelperUI._open, "Form Assistant should be open");
+    testOpenUIWithFocusRedirect();
+  });
+};
+
+function testOpenUIWithFocusRedirect() {
+  AsyncTests.waitFor("Test:OpenWithFocusRedirect", { value: "*[tabindex='0']" }, function(json) {
+    ok(FormHelperUI._open, "Form Assistant should be open");
+    testShowUIForSelect();
+  });
+};
+
+function testShowUIForSelect() {
+  AsyncTests.waitFor("Test:CanShowUI", { value: "#select"}, function(json) {
+    ok(json.result, "canShowUI for select element'");
+  });
+
+  AsyncTests.waitFor("Test:CanShowUI", { value: "#select", disabled: true }, function(json) {
+    is(json.result, false, "!canShowUI for disabled select element'");
+  });
+
+  AsyncTests.waitFor("Test:CanShowUI", { value: "#option"}, function(json) {
+    ok(json.result, "canShowUI for option element'");
+  });
+
+  AsyncTests.waitFor("Test:CanShowUISelect", { value: "#option", disabled: true }, function(json) {
+    is(json.result, false, "!canShowUI for option element with a disabled parent select element'");
+  });
+
+  AsyncTests.waitFor("Test:CanShowUI", { value: "#option", disabled: true }, function(json) {
+    is(json.result, false, "!canShowUI for disabled option element'");
     testShowUIForElements();
   });
-};
+}
 
 function testShowUIForElements() {
   AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='1']" }, function(json) {
     ok(json.result, "canShowUI for input type='text'");
   });
 
+  AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='1']", disabled: true }, function(json) {
+    is(json.result, false, "!canShowUI for disabled input type='text'");
+  });
+
   AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='2']" }, function(json) {
     ok(json.result, "canShowUI for input type='password'");
   });
 
+  AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='2']", disabled: true }, function(json) {
+    is(json.result, false, "!canShowUI for disabled input type='password'");
+  });
+
   AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='8']" }, function(json) {
     ok(json.result, "canShowUI for contenteditable div");
   });
 
+  AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='8']", disabled: true }, function(json) {
+    is(json.result, false, "!canShowUI for disabled contenteditable div");
+  });
+
   AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='3']" }, function(json) {
     is(json.result, false, "!canShowUI for input type='submit'");
   });
 
   AsyncTests.waitFor("Test:CanShowUI", { value: "*[tabindex='4']" }, function(json) {
     is(json.result, false, "!canShowUI for input type='file'");
   });
 
@@ -107,28 +167,45 @@ function testTabIndexNavigation() {
 
   AsyncTests.waitFor("Test:Next", { value: "*[tabindex='0']" }, function(json) {
     is(json.result, true, "Focus should be on element with tab-index : 0");
   });
 
   let ids = ["next", "select", "dumb", "reset", "checkbox", "radio0", "radio4", "last", "last"];
   for (let i = 0; i < ids.length; i++) {
     let id = ids[i];
-    AsyncTests.waitFor("Test:Next", { value: "*[id='" + id + "']" }, function(json) {
+    AsyncTests.waitFor("Test:Next", { value: "#" + id }, function(json) {
       is(json.result, true, "Focus should be on element with #id: " + id + "");
     });
   };
 
   FormHelperUI.hide();
   let container = document.getElementById("content-navigator");
   is(container.hidden, true, "Form Assistant should be close");
 
+  AsyncTests.waitFor("Test:Open", { value: "*[tabindex='0']" }, function(json) {
+    ok(FormHelperUI._open, "Form Assistant should be open");
+    testFocusChanges();
+  });
+};
 
-  loadNestedIFrames();
-};
+function testFocusChanges() {
+  AsyncTests.waitFor("Test:Focus", { value: "*[tabindex='1']" }, function(json) {
+    ok(json.result, "Form Assistant should be open");
+  });
+
+  AsyncTests.waitFor("Test:Focus", { value: "#select" }, function(json) {
+    ok(json.result, "Form Assistant should stay open");
+  });
+
+  AsyncTests.waitFor("Test:Focus", { value: "*[type='hidden']" }, function(json) {
+    ok(json.result, "Form Assistant should stay open");
+    loadNestedIFrames();
+  });
+}
 
 function loadNestedIFrames() {
   AsyncTests.waitFor("Test:Iframe", { }, function(json) {
     is(json.result, true, "Iframe should have loaded");
     navigateIntoNestedIFrames();
   });
 }
 
--- a/mobile/chrome/tests/head.js
+++ b/mobile/chrome/tests/head.js
@@ -53,17 +53,19 @@ EventUtils.synthesizeMouseForContent = f
 let AsyncTests = {
   _tests: {},
   waitFor: function(aMessage, aData, aCallback) {
     messageManager.addMessageListener(aMessage, this);
     if (!this._tests[aMessage])
       this._tests[aMessage] = [];
 
     this._tests[aMessage].push(aCallback || function() {});
-    Browser.selectedBrowser.messageManager.sendAsyncMessage(aMessage, aData || { });
+    setTimeout(function() {
+      Browser.selectedBrowser.messageManager.sendAsyncMessage(aMessage, aData || { });
+    }, 0);
   },
 
   receiveMessage: function(aMessage) {
     let test = this._tests[aMessage.name];
     let callback = test.shift();
     if (callback)
       callback(aMessage.json);
   }
--- a/mobile/chrome/tests/remote_forms.js
+++ b/mobile/chrome/tests/remote_forms.js
@@ -45,25 +45,87 @@ function sendMouseEvent(aEvent, aTarget,
   aTarget.dispatchEvent(event);
 }
 
 AsyncTests.add("Test:Click", function(aMessage, aJson) {
   sendMouseEvent({type: "click"}, "root", content);
   return assistant._open;
 });
 
+AsyncTests.add("Test:Focus", function(aMessage, aJson) {
+  let targetElement = content.document.querySelector(aJson.value);
+  targetElement.focus();
+  assistant._executeDelayed(function() {
+    sendAsyncMessage(aMessage, { result: assistant._open });
+  });
+});
+
+AsyncTests.add("Test:FocusRedirect", function(aMessage, aJson) {
+  let element = content.document.querySelector(aJson.value);
+  element.addEventListener("focus", function(aEvent) {
+    element.removeEventListener("focus", arguments.callee, false);
+    content.document.getElementById("root").focus();
+  }, false);
+  element.focus();
+
+  assistant._executeDelayed(function() {
+    sendAsyncMessage(aMessage, { result: assistant._open });
+  });
+});
+
+// It should be only 2 ways to open the FormAssistant, the first one is
+// by manually synchronizing the focus to the form helper and the other
+// one is by a user click on an authorized element
+AsyncTests.add("Test:OpenUIWithSyncFocus", function(aMessage, aJson) {
+  let element = content.document.querySelector(aJson.value);
+
+  assistant._open = false;
+  assitant.focusSync = true;
+  element.focus();
+  assistant._executeDelayed(function() {
+    assistant.focusSync = false;
+    sendAsyncMessage(aMessage, { result: assistant._open });
+  });
+});
+
 AsyncTests.add("Test:Open", function(aMessage, aJson) {
   let element = content.document.querySelector(aJson.value);
+  assistant._open = false;
   return assistant.open(element);
 });
 
+AsyncTests.add("Test:OpenWithFocusRedirect", function(aMessage, aJson) {
+  let element = content.document.querySelector(aJson.value);
+  assistant._open = false;
+  assistant.focusSync = true;
+  assistant.open(element);
+  assistant._executeDelayed(function() {
+    assistant.focusSync = false;
+    sendAsyncMessage(aMessage, { result: assistant._open });
+  });
+});
+
 AsyncTests.add("Test:CanShowUI", function(aMessage, aJson) {
   let element = content.document.querySelector(aJson.value);
+  element.disabled = aJson.disabled;
   assistant._open = false;
-  return assistant.open(element);
+  let open = assistant.open(element);
+  element.disabled = false;
+  return open;
+});
+
+AsyncTests.add("Test:CanShowUISelect", function(aMessage, aJson) {
+  let select = content.document.getElementById("select");
+  select.disabled = aJson.disabled;
+
+  let element = content.document.querySelector(aJson.value);
+  assistant._open = false;
+  let open = assistant.open(element);
+  select.disabled = false;
+  return open;
 });
 
 AsyncTests.add("Test:Previous", function(aMessage, aJson) {
   let targetElement = content.document.querySelector(aJson.value);
   assistant.currentIndex--;
   return (assistant.currentElement == targetElement);
 });