Bug 962647 - Nodes searched in the inspector selector-search box now appear correctly in the markup-view; r=harth
authorPatrick Brosset <pbrosset@mozilla.com>
Tue, 25 Feb 2014 16:33:57 +0100
changeset 170475 db3625395d32268f8b98d78429193c5680271c1b
parent 170474 5d020fa8466104695256e47f43ecd373812c3832
child 170476 44f836fecd71b516ba3221ac8d5627a0e4e17f06
push id26288
push userryanvm@gmail.com
push dateTue, 25 Feb 2014 20:20:43 +0000
treeherdermozilla-central@22650589a724 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs962647
milestone30.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 962647 - Nodes searched in the inspector selector-search box now appear correctly in the markup-view; r=harth
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
@@ -257,35 +257,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_RETURN:
         if (query == this._lastSearched && this._searchResults) {
           this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
         }
         else {
           this._onHTMLSearch();
@@ -343,17 +334,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_RETURN:
       case aEvent.DOM_VK_TAB:
       case 0: // left mouse button
         aEvent.stopPropagation();
         aEvent.preventDefault();
         this.searchBox.value = this.searchPopup.selectedItem.label;
         this.searchBox.focus();
@@ -399,21 +390,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;
     }
@@ -453,17 +443,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);
     }
@@ -493,10 +483,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
@@ -161,16 +161,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]
 [browser_inspector_markup_edit_2.js]
 [browser_inspector_markup_edit_3.js]
@@ -23,8 +24,9 @@ skip-if = true
 [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
@@ -63,17 +63,16 @@ function openInspector() {
  * HTML node
  * @param {MarkupView} markupView The instance of MarkupView currently loaded into the inspector panel
  * @param {DOMNode} rawNode The DOM node for which the container is required
  * @return {MarkupContainer}
  */
 function getContainerForRawNode(markupView, rawNode) {
   let front = markupView.walker.frontForRawNode(rawNode);
   let container = markupView.getContainer(front);
-  ok(container, "A markup-container object was found");
   return container;
 }
 
 /**
  * Simple DOM node accesor function that takes either a node or a string css
  * selector as argument and returns the corresponding node
  * @param {String|DOMNode} nodeOrSelector
  * @return {DOMNode}
@@ -235,8 +234,31 @@ function redoChange(inspector) {
   if (!canRedo) {
     return promise.reject();
   }
 
   let mutated = inspector.once("markupmutation");
   inspector.markup.undo.redo();
   return mutated;
 }
+
+/**
+ * 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
@@ -676,22 +676,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.
    */