Bug 962647 - Nodes searched in the inspector selector-search box now appear correctly in the markup-view. r=harth, a=sledru
☠☠ backed out by 74d3aa838c44 ☠ ☠
authorPatrick Brosset <pbrosset@mozilla.com>
Tue, 25 Feb 2014 16:33:57 +0100
changeset 183070 80c4933d97acd6294ca6cf888d138c0dd3a22994
parent 183069 32b887b8aab554e2e61d3c40f0727bbd7f6a4f96
child 183071 a39943a3a1ad7b801fb3ebbc9b9c9895d8ff04bf
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth, sledru
bugs962647
milestone29.0a2
Bug 962647 - Nodes searched in the inspector selector-search box now appear correctly in the markup-view. r=harth, a=sledru
browser/devtools/inspector/inspector-panel.js
browser/devtools/inspector/selector-search.js
browser/devtools/markupview/markup-view.js
browser/devtools/markupview/test/browser.ini
browser/devtools/markupview/test/browser_inspector_markup_962647_search.html
browser/devtools/markupview/test/browser_inspector_markup_962647_search.js
browser/devtools/markupview/test/head.js
toolkit/devtools/server/actors/inspector.js
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -253,35 +253,23 @@ InspectorPanel.prototype = {
   markDirty: function InspectorPanel_markDirty() {
     this.isDirty = true;
   },
 
   /**
    * Hooks the searchbar to show result and auto completion suggestions.
    */
   setupSearchBox: function InspectorPanel_setupSearchBox() {
-    let searchDoc;
-    if (this.target.isLocalTab) {
-      searchDoc = this.browser.contentDocument;
-    } else if (this.target.window) {
-      searchDoc = this.target.window.document;
-    } else {
-      searchDoc = null;
-    }
     // Initiate the selectors search object.
-    let setNodeFunction = function(eventName, node) {
-      this.selection.setNodeFront(node, "selectorsearch");
-    }.bind(this);
     if (this.searchSuggestions) {
       this.searchSuggestions.destroy();
       this.searchSuggestions = null;
     }
     this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox);
-    this.searchSuggestions.on("node-selected", setNodeFunction);
+    this.searchSuggestions = new SelectorSearch(this, this.searchBox);
   },
 
   /**
    * Build the sidebar.
    */
   setupSidebar: function InspectorPanel_setupSidebar() {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this, "inspector");
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,39 +1,34 @@
 /* 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 EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("sdk/core/promise");
 
 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
 /**
  * Converts any input box on a page to a CSS selector search and suggestion box.
  *
  * @constructor
  * @param InspectorPanel aInspector
  *        The InspectorPanel whose `walker` attribute should be used for
  *        document traversal.
- * @param nsIDOMDocument aContentDocument
- *        The content document which inspector is attached to, or null if
- *        a remote document.
  * @param nsiInputElement aInputNode
  *        The input element to which the panel will be attached and from where
  *        search input will be taken.
  */
-function SelectorSearch(aInspector, aContentDocument, aInputNode) {
+function SelectorSearch(aInspector, aInputNode) {
   this.inspector = aInspector;
-  this.doc = aContentDocument;
   this.searchBox = aInputNode;
   this.panelDoc = this.searchBox.ownerDocument;
 
   // initialize variables.
   this._lastSearched = null;
   this._lastValidSearch = "";
   this._lastToLastValidSearch = null;
   this._searchResults = null;
@@ -50,29 +45,27 @@ function SelectorSearch(aInspector, aCon
   let options = {
     panelId: "inspector-searchbox-panel",
     listBoxId: "searchbox-panel-listbox",
     autoSelect: true,
     position: "before_start",
     direction: "ltr",
     theme: "auto",
     onClick: this._onListBoxKeypress,
-    onKeypress: this._onListBoxKeypress,
+    onKeypress: this._onListBoxKeypress
   };
   this.searchPopup = new AutocompletePopup(this.panelDoc, options);
 
   // event listeners.
   this.searchBox.addEventListener("command", this._onHTMLSearch, true);
   this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
 
   // For testing, we need to be able to wait for the most recent node request
   // to finish.  Tests can watch this promise for that.
   this._lastQuery = promise.resolve(null);
-
-  EventEmitter.decorate(this);
 }
 
 exports.SelectorSearch = SelectorSearch;
 
 SelectorSearch.prototype = {
 
   get walker() this.inspector.walker,
 
@@ -160,41 +153,39 @@ SelectorSearch.prototype = {
       }
     }
     return this._state;
   },
 
   /**
    * Removes event listeners and cleans up references.
    */
-  destroy: function SelectorSearch_destroy() {
+  destroy: function() {
     // event listeners.
     this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
     this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
     this.searchPopup.destroy();
     this.searchPopup = null;
     this.searchBox = null;
-    this.doc = null;
     this.panelDoc = null;
     this._searchResults = null;
     this._searchSuggestions = null;
-    EventEmitter.decorate(this);
   },
 
   _selectResult: function(index) {
     return this._searchResults.item(index).then(node => {
-      this.emit("node-selected", node);
+      this.inspector.selection.setNodeFront(node, "selectorsearch");
     });
   },
 
   /**
    * The command callback for the input box. This function is automatically
    * invoked as the user is typing if the input box type is search.
    */
-  _onHTMLSearch: function SelectorSearch__onHTMLSearch() {
+  _onHTMLSearch: function() {
     let query = this.searchBox.value;
     if (query == this._lastSearched) {
       return;
     }
     this._lastSearched = query;
     this._searchResults = [];
     this._searchIndex = 0;
 
@@ -251,34 +242,34 @@ SelectorSearch.prototype = {
             this.searchPopup.hidePopup();
           }
           this.searchBox.classList.remove("devtools-no-search-result");
 
           return this._selectResult(0);
         }
         return this._selectResult(0).then(() => {
           this.searchBox.classList.remove("devtools-no-search-result");
-        }).then( () => this.showSuggestions());
+        }).then(() => this.showSuggestions());
       }
       if (query.match(/[\s>+]$/)) {
         this._lastValidSearch = query + "*";
       }
       else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
         let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
         this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
       }
       this.searchBox.classList.add("devtools-no-search-result");
       return this.showSuggestions();
     });
   },
 
   /**
    * Handles keypresses inside the input box.
    */
-  _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
+  _onSearchKeypress: function(aEvent) {
     let query = this.searchBox.value;
     switch(aEvent.keyCode) {
       case aEvent.DOM_VK_ENTER:
       case aEvent.DOM_VK_RETURN:
         if (query == this._lastSearched && this._searchResults) {
           this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
         }
         else {
@@ -344,17 +335,17 @@ SelectorSearch.prototype = {
     if (this._searchResults && this._searchResults.length > 0) {
       this._lastQuery = this._selectResult(this._searchIndex);
     }
   },
 
   /**
    * Handles keypress and mouse click on the suggestions richlistbox.
    */
-  _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
+  _onListBoxKeypress: function(aEvent) {
     switch(aEvent.keyCode || aEvent.button) {
       case aEvent.DOM_VK_ENTER:
       case aEvent.DOM_VK_RETURN:
       case aEvent.DOM_VK_TAB:
       case 0: // left mouse button
         aEvent.stopPropagation();
         aEvent.preventDefault();
         this.searchBox.value = this.searchPopup.selectedItem.label;
@@ -401,21 +392,20 @@ SelectorSearch.prototype = {
         this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
                                  query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
                                  ["",""])[1];
         this._onHTMLSearch();
         break;
     }
   },
 
-  
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
-  _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
+  _showPopup: function(aList, aFirstPart) {
     let total = 0;
     let query = this.searchBox.value;
     let toLowerCase = false;
     let items = [];
     // In case of tagNames, change the case to small.
     if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
       toLowerCase = true;
     }
@@ -455,17 +445,17 @@ SelectorSearch.prototype = {
       this.searchPopup.hidePopup();
     }
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
-  showSuggestions: function SelectorSearch_showSuggestions() {
+  showSuggestions: function() {
     let query = this.searchBox.value;
     let firstPart = "";
     if (this.state == this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
       firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
       query = query.slice(0, query.length - firstPart.length);
     }
@@ -495,10 +485,10 @@ SelectorSearch.prototype = {
       if (this.state == this.States.CLASS) {
         firstPart = "." + firstPart;
       }
       else if (this.state == this.States.ID) {
         firstPart = "#" + firstPart;
       }
       this._showPopup(result.suggestions, firstPart);
     });
-  },
+  }
 };
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -162,16 +162,19 @@ MarkupView.prototype = {
       this._containers.get(nodeFront).hovered = true;
 
       this._hoveredNode = nodeFront;
     }
   },
 
   _onMouseLeave: function() {
     this._hideBoxModel();
+    if (this._hoveredNode) {
+      this._containers.get(this._hoveredNode).hovered = false;
+    }
     this._hoveredNode = null;
   },
 
   _showBoxModel: function(nodeFront, options={}) {
     this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
 
   _hideBoxModel: function() {
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -2,16 +2,17 @@
 support-files =
   browser_inspector_markup_edit.html
   browser_inspector_markup_mutation.html
   browser_inspector_markup_mutation_flashing.html
   browser_inspector_markup_navigation.html
   browser_inspector_markup_subset.html
   browser_inspector_markup_765105_tooltip.png
   browser_inspector_markup_950732.html
+  browser_inspector_markup_962647_search.html
   head.js
 
 [browser_bug896181_css_mixed_completion_new_attribute.js]
 # Bug 916763 - too many intermittent failures
 skip-if = true
 [browser_inspector_markup_edit.js]
 # Bug 904953 - too many intermittent failures on Linux
 skip-if = os == "linux"
@@ -21,8 +22,9 @@ skip-if = os == "linux"
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.js]
 [browser_inspector_markup_765105_tooltip.js]
 [browser_inspector_markup_950732.js]
 [browser_inspector_markup_964014_copy_image_data.js]
 [browser_inspector_markup_968316_highlit_node_on_hover_then_select.js]
 [browser_inspector_markup_968316_highlight_node_after_mouseleave_mousemove.js]
+[browser_inspector_markup_962647_search.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body>
+  <ul>
+    <li>
+      <span>this is an <em>important</em> node</span>
+    </li>
+  </ul>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_962647_search.js
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that searching for nodes using the selector-search input expands and
+// selects the right nodes in the markup-view, even when those nodes are deeply
+// nested (and therefore not attached yet when the markup-view is initialized).
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html";
+
+function test() {
+  waitForExplicitFinish();
+
+  let p = content.document.querySelector("p");
+  Task.spawn(function() {
+    info("loading the test page");
+    yield addTab(TEST_URL);
+
+    info("opening the inspector");
+    let {inspector, toolbox} = yield openInspector();
+
+    ok(!getContainerForRawNode(inspector.markup, getNode("em")),
+      "The <em> tag isn't present yet in the markup-view");
+
+    // Searching for the innermost element first makes sure that the inspector
+    // back-end is able to attach the resulting node to the tree it knows at the
+    // moment. When the inspector is started, the <body> is the default selected
+    // node, and only the parents up to the ROOT are known, and its direct children
+    info("searching for the innermost child: <em>");
+    let updated = inspector.once("inspector-updated");
+    searchUsingSelectorSearch("em", inspector);
+    yield updated;
+
+    ok(getContainerForRawNode(inspector.markup, getNode("em")),
+      "The <em> tag is now imported in the markup-view");
+    is(inspector.selection.node, getNode("em"),
+      "The <em> tag is the currently selected node");
+
+    info("searching for other nodes too");
+    for (let node of ["span", "li", "ul"]) {
+      let updated = inspector.once("inspector-updated");
+      searchUsingSelectorSearch(node, inspector);
+      yield updated;
+      is(inspector.selection.node, getNode(node),
+        "The <" + node + "> tag is the currently selected node");
+    }
+
+    gBrowser.removeCurrentTab();
+  }).then(null, ok.bind(null, false)).then(finish);
+}
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -120,8 +120,30 @@ function mouseLeaveMarkupView(inspector)
 
   EventUtils.synthesizeMouse(btn, 2, 2, {type: "mousemove"},
     inspector.toolbox.doc.defaultView);
   executeSoon(deferred.resolve);
 
   return deferred.promise;
 }
 
+/**
+ * Get the selector-search input box from the inspector panel
+ * @return {DOMNode}
+ */
+function getSelectorSearchBox(inspector) {
+  return inspector.panelWin.document.getElementById("inspector-searchbox");
+}
+
+/**
+ * Using the inspector panel's selector search box, search for a given selector.
+ * The selector input string will be entered in the input field and the <ENTER>
+ * keypress will be simulated.
+ * This function won't wait for any events and is not async. It's up to callers
+ * to subscribe to events and react accordingly.
+ */
+function searchUsingSelectorSearch(selector, inspector) {
+  info("Entering \"" + selector + "\" into the selector-search input field");
+  let field = getSelectorSearchBox(inspector);
+  field.focus();
+  field.value = selector;
+  EventUtils.sendKey("return", inspector.panelWin);
+}
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -707,22 +707,17 @@ var NodeListActor = exports.NodeListActo
       length: this.nodeList.length
     }
   },
 
   /**
    * Get a single node from the node list.
    */
   item: method(function(index) {
-    let node = this.walker._ref(this.nodeList[index]);
-    let newParents = [node for (node of this.walker.ensurePathToRoot(node))];
-    return {
-      node: node,
-      newParents: newParents
-    }
+    return this.walker.attachElement(this.nodeList[index]);
   }, {
     request: { item: Arg(0) },
     response: RetVal("disconnectedNode")
   }),
 
   /**
    * Get a range of the items from the node list.
    */