Bug 1059360 - Highlight nodes that match selectors in the computed-view on mouse-over. r=miker
authorPatrick Brosset <pbrosset@mozilla.com>
Tue, 02 Sep 2014 04:13:00 +0200
changeset 203243 b37bfa44405ebff9f22533974faa2e8b60892344
parent 203242 518ad95704fc2214eb2b9e5d016df007f32c9df9
child 203244 4567aa2ed95bbe6634cabd8d170e2c283206c4e2
push id27424
push userryanvm@gmail.com
push dateWed, 03 Sep 2014 19:35:53 +0000
treeherdermozilla-central@bfef88becbba [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1059360
milestone35.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 1059360 - Highlight nodes that match selectors in the computed-view on mouse-over. r=miker
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/test/browser.ini
browser/devtools/styleinspector/test/browser_computedview_getNodeInfo.js
browser/devtools/styleinspector/test/browser_computedview_original-source-link.js
browser/devtools/styleinspector/test/browser_computedview_style-editor-link.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
browser/devtools/styleinspector/test/head.js
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -319,48 +319,93 @@ CssHtmlTree.prototype = {
    * @param {DOMNode} node The node which we want information about
    * @return {Object} The type information object contains the following props:
    * - type {String} One of the VIEW_NODE_XXX_TYPE const in
    *   style-inspector-overlays
    * - value {Object} Depends on the type of the node
    * returns null of the node isn't anything we care about
    */
   getNodeInfo: function(node) {
-    let type, value;
+    if (!node) {
+      return null;
+    }
+
     let classes = node.classList;
 
-    if (classes.contains("property-name") ||
-        classes.contains("property-value") ||
-        (classes.contains("theme-link") && !classes.contains("link"))) {
-      // Go up to the common parent to find the property and value
-      let parent = node.parentNode;
-      while (!parent.classList.contains("property-view")) {
-        parent = parent.parentNode;
+    // Check if the node isn't a selector first since this doesn't require
+    // walking the DOM
+    if (classes.contains("matched") ||
+        classes.contains("bestmatch") ||
+        classes.contains("parentmatch")) {
+      let selectorText = "";
+      for (let child of node.childNodes) {
+        if (child.nodeType === node.TEXT_NODE) {
+          selectorText += child.textContent;
+        }
+      }
+      return {
+        type: overlays.VIEW_NODE_SELECTOR_TYPE,
+        value: selectorText.trim()
       }
+    }
+
+    // Walk up the nodes to find out where node is
+    let propertyView;
+    let propertyContent;
+    let parent = node;
+    while (parent.parentNode) {
+      if (parent.classList.contains("property-view")) {
+        propertyView = parent;
+        break;
+      }
+      if (parent.classList.contains("property-content")) {
+        propertyContent = parent;
+        break;
+      }
+      parent = parent.parentNode;
+    }
+    if (!propertyView && !propertyContent) {
+      return null;
+    }
+
+    let value, type;
+
+    // Get the property and value for a node that's a property name or value
+    let isHref = classes.contains("theme-link") && !classes.contains("link");
+    if (propertyView && (classes.contains("property-name") ||
+                         classes.contains("property-value") ||
+                         isHref)) {
       value = {
         property: parent.querySelector(".property-name").textContent,
         value: parent.querySelector(".property-value").textContent
       };
     }
+    if (propertyContent && (classes.contains("other-property-value") ||
+                            isHref)) {
+      let view = propertyContent.previousSibling;
+      value = {
+        property: view.querySelector(".property-name").textContent,
+        value: node.textContent
+      };
+    }
 
+    // Get the type
     if (classes.contains("property-name")) {
       type = overlays.VIEW_NODE_PROPERTY_TYPE;
-    } else if (classes.contains("property-value")) {
+    } else if (classes.contains("property-value") ||
+               classes.contains("other-property-value")) {
       type = overlays.VIEW_NODE_VALUE_TYPE;
-    } else if (classes.contains("theme-link")) {
+    } else if (isHref) {
       type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
       value.url = node.href;
     } else {
       return null;
     }
 
-    return {
-      type: type,
-      value: value
-    };
+    return {type, value};
   },
 
   _createPropertyViews: function()
   {
     if (this._createViewsPromise) {
       return this._createViewsPromise;
     }
 
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1243,16 +1243,20 @@ CssRuleView.prototype = {
    * @param {DOMNode} node The node which we want information about
    * @return {Object} The type information object contains the following props:
    * - type {String} One of the VIEW_NODE_XXX_TYPE const in
    *   style-inspector-overlays
    * - value {Object} Depends on the type of the node
    * returns null of the node isn't anything we care about
    */
   getNodeInfo: function(node) {
+    if (!node) {
+      return null;
+    }
+
     let type, value;
     let classes = node.classList;
     let prop = getParentTextProperty(node);
 
     if (classes.contains("ruleview-propertyname") && prop) {
       type = overlays.VIEW_NODE_PROPERTY_TYPE;
       value = {
         property: node.textContent,
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -20,16 +20,17 @@ support-files =
   doc_sourcemaps.scss
   doc_style_editor_link.css
   doc_test_image.png
   doc_urls_clickable.css
   doc_urls_clickable.html
   head.js
 
 [browser_computedview_browser-styles.js]
+[browser_computedview_getNodeInfo.js]
 [browser_computedview_keybindings_01.js]
 [browser_computedview_keybindings_02.js]
 [browser_computedview_matched-selectors-toggle.js]
 [browser_computedview_matched-selectors_01.js]
 [browser_computedview_matched-selectors_02.js]
 [browser_computedview_media-queries.js]
 [browser_computedview_no-results-placeholder.js]
 [browser_computedview_original-source-link.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_computedview_getNodeInfo.js
@@ -0,0 +1,177 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test various output of the computed-view's getNodeInfo method.
+// This method is used by the style-inspector-overlay on mouseover to decide
+// which tooltip or highlighter to show when hovering over a value/name/selector
+// if any.
+// For instance, browser_ruleview_selector-highlighter_01.js and
+// browser_ruleview_selector-highlighter_02.js test that the selector
+// highlighter appear when hovering over a selector in the rule-view.
+// Since the code to make this work for the computed-view is 90% the same, there
+// is no need for testing it again here.
+// This test however serves as a unit test for getNodeInfo.
+
+const {
+  VIEW_NODE_SELECTOR_TYPE,
+  VIEW_NODE_PROPERTY_TYPE,
+  VIEW_NODE_VALUE_TYPE,
+  VIEW_NODE_IMAGE_URL_TYPE
+} = devtools.require("devtools/styleinspector/style-inspector-overlays");
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  body {',
+  '    background: red;',
+  '    color: white;',
+  '  }',
+  '  div {',
+  '    background: green;',
+  '  }',
+  '  div div {',
+  '    background-color: yellow;',
+  '    background-image: url(chrome://global/skin/icons/warning-64.png);',
+  '    color: red;',
+  '  }',
+  '</style>',
+  '<div><div id="testElement">Test element</div></div>'
+].join("\n");
+
+// Each item in this array must have the following properties:
+// - desc {String} will be logged for information
+// - getHoveredNode {Generator Function} received the computed-view instance as
+//   argument and must return the node to be tested
+// - assertNodeInfo {Function} should check the validity of the nodeInfo
+//   argument it receives
+const TEST_DATA = [
+  {
+    desc: "Testing a null node",
+    getHoveredNode: function*() {
+      return null;
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo, null);
+    }
+  },
+  {
+    desc: "Testing a useless node",
+    getHoveredNode: function*(view) {
+      return view.element;
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo, null);
+    }
+  },
+  {
+    desc: "Testing a property name",
+    getHoveredNode: function*(view) {
+      return getComputedViewProperty(view, "color").nameSpan;
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_PROPERTY_TYPE);
+      ok("property" in nodeInfo.value);
+      ok("value" in nodeInfo.value);
+      is(nodeInfo.value.property, "color");
+      is(nodeInfo.value.value, "#F00");
+    }
+  },
+  {
+    desc: "Testing a property value",
+    getHoveredNode: function*(view) {
+      return getComputedViewProperty(view, "color").valueSpan;
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
+      ok("property" in nodeInfo.value);
+      ok("value" in nodeInfo.value);
+      is(nodeInfo.value.property, "color");
+      is(nodeInfo.value.value, "#F00");
+    }
+  },
+  {
+    desc: "Testing an image url",
+    getHoveredNode: function*(view) {
+      let {valueSpan} = getComputedViewProperty(view, "background-image");
+      return valueSpan.querySelector(".theme-link");
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_IMAGE_URL_TYPE);
+      ok("property" in nodeInfo.value);
+      ok("value" in nodeInfo.value);
+      is(nodeInfo.value.property, "background-image");
+      is(nodeInfo.value.value, "url(\"chrome://global/skin/icons/warning-64.png\")");
+      is(nodeInfo.value.url, "chrome://global/skin/icons/warning-64.png");
+    }
+  },
+  {
+    desc: "Testing a matched rule selector (bestmatch)",
+    getHoveredNode: function*(view) {
+      let content = yield getComputedViewMatchedRules(view, "background-color");
+      return content.querySelector(".bestmatch");
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+      is(nodeInfo.value, "div div");
+    }
+  },
+  {
+    desc: "Testing a matched rule selector (matched)",
+    getHoveredNode: function*(view) {
+      let content = yield getComputedViewMatchedRules(view, "background-color");
+      return content.querySelector(".matched");
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+      is(nodeInfo.value, "div");
+    }
+  },
+  {
+    desc: "Testing a matched rule selector (parentmatch)",
+    getHoveredNode: function*(view) {
+      let content = yield getComputedViewMatchedRules(view, "color");
+      return content.querySelector(".parentmatch");
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_SELECTOR_TYPE);
+      is(nodeInfo.value, "body");
+    }
+  },
+  {
+    desc: "Testing a matched rule value",
+    getHoveredNode: function*(view) {
+      let content = yield getComputedViewMatchedRules(view, "color");
+      return content.querySelector(".other-property-value");
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
+      is(nodeInfo.value.property, "color");
+      is(nodeInfo.value.value, "#F00");
+    }
+  },
+  {
+    desc: "Testing a matched rule stylesheet link",
+    getHoveredNode: function*(view) {
+      let content = yield getComputedViewMatchedRules(view, "color");
+      return content.querySelector(".rule-link .theme-link");
+    },
+    assertNodeInfo: function(nodeInfo) {
+      is(nodeInfo, null);
+    }
+  }
+];
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html;charset=utf-8," + PAGE_CONTENT);
+
+  let {inspector, view} = yield openComputedView();
+  yield selectNode("#testElement", inspector);
+
+  for (let {desc, getHoveredNode, assertNodeInfo} of TEST_DATA) {
+    info(desc);
+    let nodeInfo = view.getNodeInfo(yield getHoveredNode(view));
+    assertNodeInfo(nodeInfo);
+  }
+});
--- a/browser/devtools/styleinspector/test/browser_computedview_original-source-link.js
+++ b/browser/devtools/styleinspector/test/browser_computedview_original-source-link.js
@@ -18,17 +18,17 @@ let test = asyncTest(function*() {
 
   yield addTab(TESTCASE_URI);
   let {toolbox, inspector, view} = yield openComputedView();
 
   info("Select the test node");
   yield selectNode("div", inspector);
 
   info("Expanding the first property");
-  yield expandComputedViewPropertyByIndex(view, inspector, 0);
+  yield expandComputedViewPropertyByIndex(view, 0);
 
   info("Verifying the link text");
   yield verifyLinkText(view, SCSS_LOC);
 
   info("Toggling the pref");
   Services.prefs.setBoolPref(PREF, false);
 
   info("Verifying that the link text has changed after the pref change");
--- a/browser/devtools/styleinspector/test/browser_computedview_style-editor-link.js
+++ b/browser/devtools/styleinspector/test/browser_computedview_style-editor-link.js
@@ -56,17 +56,17 @@ let test = asyncTest(function*() {
   yield testFirstInlineStyleSheet(view, toolbox);
   yield testSecondInlineStyleSheet(view, toolbox);
   yield testExternalStyleSheet(view, toolbox);
 });
 
 function* testInlineStyle(view, inspector) {
   info("Testing inline style");
 
-  yield expandComputedViewPropertyByIndex(view, inspector, 0);
+  yield expandComputedViewPropertyByIndex(view, 0);
 
   let onWindow = waitForWindow();
   info("Clicking on the first rule-link in the computed-view");
   clickLinkByIndex(view, 0);
 
   let win = yield onWindow;
 
   let windowType = win.document.documentElement.getAttribute("windowtype");
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
@@ -30,16 +30,18 @@ let test = asyncTest(function*() {
   yield selectNode("#testElement", inspector);
 
   yield testRuleView(view, inspector.selection.nodeFront);
 
   info("Opening the computed view");
   let {toolbox, inspector, view} = yield openComputedView();
 
   yield testComputedView(view, inspector.selection.nodeFront);
+
+  yield testExpandedComputedViewProperty(view, inspector.selection.nodeFront);
 });
 
 function* testRuleView(ruleView, nodeFront) {
   info("Testing font-family tooltips in the rule view");
 
   let tooltip = ruleView.tooltips.previewTooltip;
   let panel = tooltip.panel;
 
@@ -72,8 +74,44 @@ function* testComputedView(computedView,
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
 }
+
+function* testExpandedComputedViewProperty(computedView, nodeFront) {
+  info("Testing font-family tooltips in expanded properties of the computed view");
+
+  info("Expanding the font-family property to reveal matched selectors");
+  let propertyView = getPropertyView(computedView, "font-family");
+  propertyView.matchedExpanded = true;
+  yield propertyView.refreshMatchedSelectors();
+
+  let valueSpan = propertyView.matchedSelectorsContainer
+    .querySelector(".bestmatch .other-property-value");
+
+  let tooltip = computedView.tooltips.previewTooltip;
+  let panel = tooltip.panel;
+
+  yield assertHoverTooltipOn(tooltip, valueSpan);
+
+  let images = panel.getElementsByTagName("image");
+  is(images.length, 1, "Tooltip contains an image");
+  ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
+
+  let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
+  is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
+}
+
+function getPropertyView(computedView, name) {
+  let propertyView = null;
+  computedView.propertyViews.some(function(view) {
+    if (view.name == name) {
+      propertyView = view;
+      return true;
+    }
+    return false;
+  });
+  return propertyView;
+}
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
@@ -23,21 +23,16 @@ let test = asyncTest(function*() {
 
   info("Opening the rule view");
   let {toolbox, inspector, view} = yield openRuleView();
 
   info("Selecting the test node");
   yield selectNode("#testElement", inspector);
 
   yield testRuleView(view, inspector.selection.nodeFront);
-
-  info("Opening the computed view");
-  let {toolbox, inspector, view} = yield openComputedView();
-
-  yield testComputedView(view, inspector.selection.nodeFront);
 });
 
 function* testRuleView(ruleView, nodeFront) {
   info("Testing font-family tooltips in the rule view");
 
   let tooltip = ruleView.tooltips.previewTooltip;
   let panel = tooltip.panel;
 
@@ -58,26 +53,8 @@ function* testRuleView(ruleView, nodeFro
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
 }
-
-function* testComputedView(computedView, nodeFront) {
-  info("Testing font-family tooltips in the computed view");
-
-  let tooltip = computedView.tooltips.previewTooltip;
-  let panel = tooltip.panel;
-
-  let {valueSpan} = getComputedViewProperty(computedView, "font-family");
-
-  yield assertHoverTooltipOn(tooltip, valueSpan);
-
-  let images = panel.getElementsByTagName("image");
-  is(images.length, 1, "Tooltip contains an image");
-  ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
-
-  let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
-  is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
-}
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -747,42 +747,16 @@ let createNewRuleViewProperty = Task.asy
 
   info("Submitting the new value and waiting for value field focus");
   let onFocus = once(ruleEditor.element, "focus", true);
   EventUtils.synthesizeKey("VK_RETURN", {},
     ruleEditor.element.ownerDocument.defaultView);
   yield onFocus;
 });
 
-// TO BE UNCOMMENTED WHEN THE EYEDROPPER FINALLY LANDS
-// /**
-//  * Given a color swatch in the ruleview, click on it to open the color picker
-//  * and then click on the eyedropper button to start the eyedropper tool
-//  * @param {CssRuleView} view The instance of the rule-view panel
-//  * @param {DOMNode} swatch The color swatch to be clicked on
-//  * @return A promise that resolves when the dropper is opened
-//  */
-// let openRuleViewEyeDropper = Task.async(function*(view, swatch) {
-//   info("Opening the colorpicker tooltip on a colorswatch");
-//   let tooltip = view.colorPicker.tooltip;
-//   let onTooltipShown = tooltip.once("shown");
-//   swatch.click();
-//   yield onTooltipShown;
-
-//   info("Finding the eyedropper icon in the colorpicker document");
-//   let tooltipDoc = tooltip.content.contentDocument;
-//   let dropperButton = tooltipDoc.querySelector("#eyedropper-button");
-//   ok(dropperButton, "Found the eyedropper icon");
-
-//   info("Opening the eyedropper");
-//   let onOpen = tooltip.once("eyedropper-opened");
-//   dropperButton.click();
-//   return yield onOpen;
-// });
-
 /* *********************************************
  * COMPUTED-VIEW
  * *********************************************
  * Computed-view related utility functions.
  * Allows to get properties, links, expand properties, ...
  */
 
 /**
@@ -802,44 +776,77 @@ function getComputedViewProperty(view, n
       prop = {nameSpan: nameSpan, valueSpan: valueSpan};
       break;
     }
   }
   return prop;
 }
 
 /**
+ * Get a reference to the property-content element for a given property name in
+ * the computed-view.
+ * A property-content element always follows (nextSibling) the property itself
+ * and is only shown when the twisty icon is expanded on the property.
+ * A property-content element contains matched rules, with selectors, properties,
+ * values and stylesheet links
+ * @param {CssHtmlTree} view The instance of the computed view panel
+ * @param {String} name The name of the property to retrieve
+ * @return {Promise} A promise that resolves to the property matched rules
+ * container
+ */
+let getComputedViewMatchedRules = Task.async(function*(view, name) {
+  let expander;
+  let propertyContent;
+  for (let property of view.styleDocument.querySelectorAll(".property-view")) {
+    let nameSpan = property.querySelector(".property-name");
+    if (nameSpan.textContent === name) {
+      expander = property.querySelector(".expandable");
+      propertyContent = property.nextSibling;
+      break;
+    }
+  }
+
+  if (!expander.hasAttribute("open")) {
+    // Need to expand the property
+    let onExpand = view.inspector.once("computed-view-property-expanded");
+    expander.click();
+    yield onExpand;
+  }
+
+  return propertyContent;
+});
+
+/**
  * Get the text value of the property corresponding to a given name in the
  * computed-view
  * @param {CssHtmlTree} view The instance of the computed view panel
  * @param {String} name The name of the property to retrieve
  * @return {String} The property value
  */
-function getComputedViewPropertyValue(view, selectorText, propertyName) {
-  return getComputedViewProperty(view, selectorText, propertyName)
+function getComputedViewPropertyValue(view, name, propertyName) {
+  return getComputedViewProperty(view, name, propertyName)
     .valueSpan.textContent;
 }
 
 /**
  * Expand a given property, given its index in the current property list of
  * the computed view
  * @param {CssHtmlTree} view The instance of the computed view panel
- * @param {InspectorPanel} inspector The instance of the inspector panel
  * @param {Number} index The index of the property to be expanded
  * @return a promise that resolves when the property has been expanded, or
  * rejects if the property was not found
  */
-function expandComputedViewPropertyByIndex(view, inspector, index) {
+function expandComputedViewPropertyByIndex(view, index) {
   info("Expanding property " + index + " in the computed view");
   let expandos = view.styleDocument.querySelectorAll(".expandable");
   if (!expandos.length || !expandos[index]) {
     return promise.reject();
   }
 
-  let onExpand = inspector.once("computed-view-property-expanded");
+  let onExpand = view.inspector.once("computed-view-property-expanded");
   expandos[index].click();
   return onExpand;
 }
 
 /**
  * Get a rule-link from the computed-view given its index
  * @param {CssHtmlTree} view The instance of the computed view panel
  * @param {Number} index The index of the link to be retrieved