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 203160 b37bfa44405ebff9f22533974faa2e8b60892344
parent 203159 518ad95704fc2214eb2b9e5d016df007f32c9df9
child 203161 4567aa2ed95bbe6634cabd8d170e2c283206c4e2
push id8520
push usercbook@mozilla.com
push dateWed, 03 Sep 2014 10:04:12 +0000
treeherderfx-team@4567aa2ed95b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker
bugs1059360
milestone35.0a1
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