Bug 1270215 - Ensure we display properly cased node names all across the devtools. r=jdescottes
authorNicolas Chevobbe <chevobbe.nicolas@gmail.com>
Thu, 12 May 2016 07:18:58 +0200
changeset 369143 50a5ded6bc420fa550faef3c96daf8a154b48137
parent 369021 2013e1255bf82c59a3d90ebc69895f62e9a180f7
child 369144 52c96fb34b7242dc0676ccb12350f31bf478a7dc
push id18759
push userbmo:zer0@mozilla.com
push dateFri, 20 May 2016 14:22:18 +0000
reviewersjdescottes
bugs1270215
milestone49.0a1
Bug 1270215 - Ensure we display properly cased node names all across the devtools. r=jdescottes Add a displayName property on the NodeActor, which compute from Element.prefix + Element.localName. The computation is made by a getNodeDisplayName function which can be imported wherever needed. Edit some tests to ensure we correctly display node names. MozReview-Commit-ID: 6z0G3ynbMoU
devtools/client/inspector/breadcrumbs.js
devtools/client/inspector/inspector-search.js
devtools/client/inspector/markup/markup.js
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/shared/dom-node-preview.js
devtools/client/inspector/test/browser_inspector_breadcrumbs.js
devtools/client/inspector/test/browser_inspector_infobar_01.js
devtools/client/inspector/test/browser_inspector_search-07.js
devtools/client/inspector/test/doc_inspector_breadcrumbs.html
devtools/client/inspector/test/doc_inspector_infobar_01.html
devtools/client/inspector/test/doc_inspector_search-svg.html
devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
devtools/client/webconsole/test/test-console-output-dom-elements.html
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters/box-model.js
devtools/server/actors/inspector.js
devtools/server/actors/styles.js
devtools/shared/fronts/inspector.js
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -100,17 +100,17 @@ HTMLBreadcrumbs.prototype = {
 
   /**
 
    * Build a string that represents the node: tagName#id.class1.class2.
    * @param {NodeFront} node The node to pretty-print
    * @return {String}
    */
   prettyPrintNodeAsText: function (node) {
-    let text = node.tagName.toLowerCase();
+    let text = node.displayName;
     if (node.isPseudoElement) {
       text = node.isBeforePseudoElement ? "::before" : "::after";
     }
 
     if (node.id) {
       text += "#" + node.id;
     }
 
@@ -146,17 +146,17 @@ HTMLBreadcrumbs.prototype = {
     idLabel.className = "breadcrumbs-widget-item-id plain";
 
     let classesLabel = this.chromeDoc.createElement("label");
     classesLabel.className = "breadcrumbs-widget-item-classes plain";
 
     let pseudosLabel = this.chromeDoc.createElement("label");
     pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
 
-    let tagText = node.tagName.toLowerCase();
+    let tagText = node.displayName;
     if (node.isPseudoElement) {
       tagText = node.isBeforePseudoElement ? "::before" : "::after";
     }
     let idText = node.id ? ("#" + node.id) : "";
     let classesText = "";
 
     if (node.className) {
       let classList = node.className.split(/\s+/);
--- a/devtools/client/inspector/inspector-search.js
+++ b/devtools/client/inspector/inspector-search.js
@@ -446,21 +446,16 @@ SelectorAutocompleter.prototype = {
         value = attrPart + value;
       }
 
       let item = {
         preLabel: query,
         label: value
       };
 
-      // In case of tagNames, change the case to small
-      if (value.match(/.*[\.#][^\.#]{0,}$/) == null) {
-        item.label = value.toLowerCase();
-      }
-
       // In case the query's state is tag and the item's state is id or class
       // adjust the preLabel
       if (aState === this.States.TAG && state === this.States.CLASS) {
         item.preLabel = "." + item.preLabel;
       }
       if (aState === this.States.TAG && state === this.States.ID) {
         item.preLabel = "#" + item.preLabel;
       }
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -2707,21 +2707,21 @@ function ElementEditor(container, node) 
       this.container.undo.do(() => {
         doMods.apply();
       }, function () {
         undoMods.apply();
       });
     }
   });
 
-  let tagName = this.getTagName(this.node);
-  this.tag.textContent = tagName;
-  this.closeTag.textContent = tagName;
-
-  let isVoidElement = HTML_VOID_ELEMENTS.includes(tagName);
+  let displayName = this.node.displayName;
+  this.tag.textContent = displayName;
+  this.closeTag.textContent = displayName;
+
+  let isVoidElement = HTML_VOID_ELEMENTS.includes(displayName);
   if (node.isInHTMLDocument && isVoidElement) {
     this.elt.classList.add("void-element");
   }
 
   this.update();
   this.initialized = true;
 }
 
@@ -2740,33 +2740,16 @@ ElementEditor.prototype = {
     flashElementOn(this.getAttributeElement(attrName));
 
     this.animationTimers[attrName] = setTimeout(() => {
       flashElementOff(this.getAttributeElement(attrName));
     }, this.markup.CONTAINER_FLASHING_DURATION);
   },
 
   /**
-   * Returns the name of a node.
-   *
-   * @param  {DOMNode} node
-   *         The node to get the name of.
-   * @return {String} A tag name with correct case
-   */
-  getTagName: function (node) {
-    // Check the node is a SVG element
-    if (node.namespaceURI === "http://www.w3.org/2000/svg") {
-      // nodeName is already in the correct case
-      return node.nodeName;
-    }
-
-    return node.nodeName.toLowerCase();
-  },
-
-  /**
    * Returns information about node in the editor.
    *
    * @param  {DOMNode} node
    *         The node to get information from.
    * @return {Object} An object literal with the following information:
    *         {type: "attribute", name: "rel", value: "index", el: node}
    */
   getInfoAtNode: function (node) {
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -78,17 +78,17 @@ Rule.prototype = {
   },
 
   get inheritedSource() {
     if (this._inheritedSource) {
       return this._inheritedSource;
     }
     this._inheritedSource = "";
     if (this.inherited) {
-      let eltText = this.inherited.tagName.toLowerCase();
+      let eltText = this.inherited.displayName;
       if (this.inherited.id) {
         eltText += "#" + this.inherited.id;
       }
       this._inheritedSource =
         CssLogic._strings.formatStringFromName("rule.inheritedFrom",
                                                [eltText], 1);
     }
     return this._inheritedSource;
--- a/devtools/client/inspector/shared/dom-node-preview.js
+++ b/devtools/client/inspector/shared/dom-node-preview.js
@@ -273,26 +273,26 @@ DomNodePreview.prototype = {
         this.render(this.nodeFront);
         break;
       }
     }
   },
 
   render: function (nodeFront) {
     this.nodeFront = nodeFront;
-    let {tagName, attributes} = nodeFront;
+    let {displayName, attributes} = nodeFront;
 
     if (nodeFront.isPseudoElement) {
       this.pseudoEl.textContent = nodeFront.isBeforePseudoElement
                                    ? "::before"
                                    : "::after";
       this.pseudoEl.style.display = "inline";
       this.tagNameEl.style.display = "none";
     } else {
-      this.tagNameEl.textContent = tagName.toLowerCase();
+      this.tagNameEl.textContent = displayName;
       this.pseudoEl.style.display = "none";
       this.tagNameEl.style.display = "inline";
     }
 
     let idIndex = attributes.findIndex(({name}) => name === "id");
     if (idIndex > -1 && attributes[idIndex].value) {
       this.idEl.querySelector(".attribute-value").textContent =
         attributes[idIndex].value;
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs.js
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js
@@ -2,57 +2,73 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that the breadcrumbs widget content is correct.
 
 const TEST_URI = URL_ROOT + "doc_inspector_breadcrumbs.html";
 const NODES = [
-  {selector: "#i1111", result: "i1 i11 i111 i1111"},
-  {selector: "#i22", result: "i2 i22"},
-  {selector: "#i2111", result: "i2 i21 i211 i2111"},
-  {selector: "#i21", result: "i2 i21 i211 i2111"},
-  {selector: "#i22211", result: "i2 i22 i222 i2221 i22211"},
-  {selector: "#i22", result: "i2 i22 i222 i2221 i22211"},
-  {selector: "#i3", result: "i3"},
+  {selector: "#i1111", ids: "i1 i11 i111 i1111", nodeName: "div",
+    title: "div#i1111"},
+  {selector: "#i22", ids: "i2 i22", nodeName: "div",
+    title: "div#i22"},
+  {selector: "#i2111", ids: "i2 i21 i211 i2111", nodeName: "div",
+    title: "div#i2111"},
+  {selector: "#i21", ids: "i2 i21 i211 i2111", nodeName: "div",
+    title: "div#i21"},
+  {selector: "#i22211", ids: "i2 i22 i222 i2221 i22211", nodeName: "div",
+    title: "div#i22211"},
+  {selector: "#i22", ids: "i2 i22 i222 i2221 i22211", nodeName: "div",
+    title: "div#i22"},
+  {selector: "#i3", ids: "i3", nodeName: "article",
+    title: "article#i3"},
+  {selector: "clipPath", ids: "vector clip", nodeName: "clipPath",
+    title: "clipPath#clip"},
 ];
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(TEST_URI);
   let container = inspector.panelDoc.getElementById("inspector-breadcrumbs");
 
   for (let node of NODES) {
     info("Testing node " + node.selector);
 
     info("Selecting node and waiting for breadcrumbs to update");
     let breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
     yield selectNode(node.selector, inspector);
     yield breadcrumbsUpdated;
 
     info("Performing checks for node " + node.selector);
-    let buttonsLabelIds = node.result.split(" ");
+    let buttonsLabelIds = node.ids.split(" ");
 
     // html > body > …
     is(container.childNodes.length, buttonsLabelIds.length + 2,
       "Node " + node.selector + ": Items count");
 
     for (let i = 2; i < container.childNodes.length; i++) {
       let expectedId = "#" + buttonsLabelIds[i - 2];
       let button = container.childNodes[i];
       let labelId = button.querySelector(".breadcrumbs-widget-item-id");
       is(labelId.textContent, expectedId,
-        "Node #" + node.selector + ": button " + i + " matches");
+        "Node " + node.selector + ": button " + i + " matches");
     }
 
     let checkedButton = container.querySelector("button[checked]");
     let labelId = checkedButton.querySelector(".breadcrumbs-widget-item-id");
     let id = inspector.selection.nodeFront.id;
     is(labelId.textContent, "#" + id,
-      "Node #" + node.selector + ": selection matches");
+      "Node " + node.selector + ": selection matches");
+
+    let labelTag = checkedButton.querySelector(".breadcrumbs-widget-item-tag");
+    is(labelTag.textContent, node.nodeName,
+      "Node " + node.selector + " has the expected tag name");
+
+    is(checkedButton.getAttribute("tooltiptext"), node.title,
+      "Node " + node.selector + " has the expected tooltip");
   }
 
   yield testPseudoElements(inspector, container);
 });
 
 function* testPseudoElements(inspector, container) {
   info("Checking for pseudo elements");
 
--- a/devtools/client/inspector/test/browser_inspector_infobar_01.js
+++ b/devtools/client/inspector/test/browser_inspector_infobar_01.js
@@ -10,44 +10,52 @@ const TEST_URI = URL_ROOT + "doc_inspect
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URI);
 
   let testData = [
     {
       selector: "#top",
       position: "bottom",
-      tag: "DIV",
+      tag: "div",
       id: "top",
       classes: ".class1.class2",
       dims: "500" + " \u00D7 " + "100"
     },
     {
       selector: "#vertical",
       position: "overlap",
-      tag: "DIV",
+      tag: "div",
       id: "vertical",
       classes: ""
       // No dims as they will vary between computers
     },
     {
       selector: "#bottom",
       position: "top",
-      tag: "DIV",
+      tag: "div",
       id: "bottom",
       classes: "",
       dims: "500" + " \u00D7 " + "100"
     },
     {
       selector: "body",
       position: "bottom",
-      tag: "BODY",
+      tag: "body",
       classes: ""
       // No dims as they will vary between computers
     },
+    {
+      selector: "clipPath",
+      position: "bottom",
+      tag: "clipPath",
+      id: "clip",
+      classes: ""
+      // No dims as element is not displayed and we just want to test tag name
+    },
   ];
 
   for (let currTest of testData) {
     yield testPosition(currTest, inspector, testActor);
   }
 });
 
 function* testPosition(test, inspector, testActor) {
--- a/devtools/client/inspector/test/browser_inspector_search-07.js
+++ b/devtools/client/inspector/test/browser_inspector_search-07.js
@@ -6,17 +6,17 @@
 // Test that searching for classes on SVG elements does work (see bug 1219920).
 
 const TEST_URL = URL_ROOT + "doc_inspector_search-svg.html";
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 const TEST_DATA = [{
   key: "c",
-  suggestions: ["circle", ".class1", ".class2"]
+  suggestions: ["circle", "clipPath", ".class1", ".class2"]
 }, {
   key: "VK_BACK_SPACE",
   suggestions: []
 }, {
   key: ".",
   suggestions: [".class1", ".class2"]
 }];
 
--- a/devtools/client/inspector/test/doc_inspector_breadcrumbs.html
+++ b/devtools/client/inspector/test/doc_inspector_breadcrumbs.html
@@ -59,10 +59,16 @@
       <link />
       <link />
       <link />
       <link />
       <link />
       <link />
     </article>
     <div id='pseudo-container'></div>
+    <svg id="vector" viewBox="0 0 10 10">
+      <clipPath id="clip">
+        <rect id="rectangle" x="0" y="0" width="10" height="5"></rect>
+      </clipPath>
+      <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle>
+    </svg>
   </body>
 </html>
--- a/devtools/client/inspector/test/doc_inspector_infobar_01.html
+++ b/devtools/client/inspector/test/doc_inspector_infobar_01.html
@@ -18,16 +18,27 @@
       bottom: 0px;
       background: blue;
     }
 
     #vertical {
       height: 100%;
       background: green;
     }
+
+    svg {
+      width: 10px;
+      height: 10px;
+    }
    </style>
  </head>
  <body>
   <div id="vertical">Vertical</div>
   <div id="top" class="class1 class2">Top</div>
   <div id="bottom">Bottom</div>
+  <svg viewBox="0 0 10 10">
+    <clipPath id="clip">
+      <rect x="0" y="0" width="10" height="5"></rect>
+    </clipPath>
+    <circle cx="5" cy="5" r="5" fill="blue" clip-path="url(#clip)"></circle>
+  </svg>
  </body>
  </html>
--- a/devtools/client/inspector/test/doc_inspector_search-svg.html
+++ b/devtools/client/inspector/test/doc_inspector_search-svg.html
@@ -2,12 +2,15 @@
 <html lang="en">
 <head>
   <meta charset="utf-8">
   <title>Inspector SVG Search Box Test</title>
 </head>
 <body>
   <div class="class1"></div>
   <svg>
+    <clipPath>
+      <rect x="0" y="0" width="10" height="5"></rect>
+    </clipPath>
     <circle cx="0" cy="0" r="50" class="class2" />
   </svg>
 </body>
 </html>
--- a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_01.js
@@ -4,17 +4,18 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejections should be fixed.
 
 "use strict";
 
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed(null);
-thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.toolbox is null");
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
+  "TypeError: this.toolbox is null");
 
 // Test the webconsole output for various types of DOM Nodes.
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/test-console-output-dom-elements.html";
 
 var inputTests = [
   {
@@ -50,19 +51,19 @@ var inputTests = [
     printOutput: "[object HTMLParagraphElement]",
     inspectable: true,
     noClick: true,
     inspectorIcon: true
   },
 
   {
     input: "testNodeList()",
-    output: "NodeList [ <html>, <head>, <meta>, <title>, " +
-            "<body#body-id.body-class>, <p>, <p#lots-of-attributes>, <iframe>, " +
-            "<div.some.classname.here.with.more.classnames.here>, <script> ]",
+    output: "NodeList [ <p>, <p#lots-of-attributes>, <iframe>, " +
+            "<div.some.classname.here.with.more.classnames.here>, " +
+            "<svg>, <clipPath>, <rect>, <script> ]",
     printOutput: "[object NodeList]",
     inspectable: true,
     noClick: true,
     inspectorIcon: true
   },
 
   {
     input: "testNodeInIframe()",
@@ -70,17 +71,18 @@ var inputTests = [
     printOutput: "[object HTMLParagraphElement]",
     inspectable: true,
     noClick: true,
     inspectorIcon: true
   },
 
   {
     input: "testLotsOfAttributes()",
-    output: '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" h="" i="" j="" k="" l="" m="" n="">',
+    output: '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+            'h="" i="" j="" k="" l="" m="" n="">',
     printOutput: "[object HTMLParagraphElement]",
     inspectable: true,
     noClick: true,
     inspectorIcon: true
   },
 
   {
     input: "testDocumentFragment()",
--- a/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_dom_elements_04.js
@@ -15,16 +15,20 @@ const TEST_DATA = [
   {
     // The first test shouldn't be returning the body element as this is the
     // default selected node, so re-selecting it won't fire the
     // inspector-updated event
     input: "testNode()",
     output: '<p some-attribute="some-value">'
   },
   {
+    input: "testSvgNode()",
+    output: '<clipPath>'
+  },
+  {
     input: "testBodyNode()",
     output: '<body class="body-class" id="body-id">'
   },
   {
     input: "testNodeInIframe()",
     output: "<p>"
   },
   {
--- a/devtools/client/webconsole/test/test-console-output-dom-elements.html
+++ b/devtools/client/webconsole/test/test-console-output-dom-elements.html
@@ -16,16 +16,21 @@
     console output message:
     "The character encoding of a framed document was not declared. The document may appear different if viewed without the document framing it."
     This wouldn't be a big deal, but when we look for a "<p>" in our `waitForMessage` helper,
     this extra encoding warning line contains the data URI source, returning a message
     that was unexpected
   -->
   <iframe src="data:text/html;charset=US-ASCII,<p>hello from iframe</p>"></iframe>
   <div class="some       classname      here      with       more classnames here"></div>
+  <svg>
+    <clipPath>
+      <rect x="0" y="0" width="10" height="5"></rect>
+    </clipPath>
+  </svg>
   <script type="text/javascript">
 function testBodyNode() {
   return document.body;
 }
 
 function testDocumentElement() {
   return document.documentElement;
 }
@@ -37,18 +42,22 @@ function testLotsOfAttributes() {
 function testDocument() {
   return document;
 }
 
 function testNode() {
   return document.querySelector("p");
 }
 
+function testSvgNode() {
+  return document.querySelector("clipPath");
+}
+
 function testNodeList() {
-  return document.querySelectorAll("*");
+  return document.querySelectorAll("body *");
 }
 
 function testNodeInIframe() {
   return document.querySelector("iframe").contentWindow.document.querySelector("p");
 }
 
 function testDocumentFragment() {
   var frag = document.createDocumentFragment();
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -153,17 +153,16 @@
   white-space: nowrap;
   direction: ltr;
   text-align: center;
   padding-bottom: 1px;
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-tagname {
   color: hsl(285,100%,75%);
-  text-transform: lowercase;
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-id {
   color: hsl(103,46%,54%);
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-classes,
 :-moz-native-anonymous .box-model-nodeinfobar-pseudo-classes {
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -7,16 +7,17 @@
 const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
   CanvasFrameAnonymousContentHelper,
   getBindingElementAndPseudo, hasPseudoClassLock, getComputedStyle,
   createSVGNode, createNode, isNodeValid } = require("./utils/markup");
 const { getCurrentZoom,
   setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
+const inspector = require("devtools/server/actors/inspector");
 
 // Note that the order of items in this array is important because it is used
 // for drawing the BoxModelHighlighter's path elements correctly.
 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
 const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
 // Width of boxmodelhighlighter guides
 const GUIDE_STROKE_WIDTH = 1;
 // How high is the nodeinfobar (px).
@@ -638,28 +639,28 @@ BoxModelHighlighter.prototype = extend(A
     }
 
     guide.removeAttribute("hidden");
 
     return true;
   },
 
   /**
-   * Update node information (tagName#id.class)
+   * Update node information (displayName#id.class)
    */
   _updateInfobar: function () {
     if (!this.currentNode) {
       return;
     }
 
     let {bindingElement: node, pseudo} =
         getBindingElementAndPseudo(this.currentNode);
 
     // Update the tag, id, classes, pseudo-classes and dimensions
-    let tagName = node.tagName;
+    let displayName = inspector.getNodeDisplayName(node);
 
     let id = node.id ? "#" + node.id : "";
 
     let classList = (node.classList || []).length
                     ? "." + [...node.classList].join(".")
                     : "";
 
     let pseudos = PSEUDO_CLASSES.filter(pseudo => {
@@ -670,17 +671,17 @@ BoxModelHighlighter.prototype = extend(A
       pseudos += ":" + pseudo;
     }
 
     let rect = this._getOuterQuad("border").bounds;
     let dim = parseFloat(rect.width.toPrecision(6)) +
               " \u00D7 " +
               parseFloat(rect.height.toPrecision(6));
 
-    this.getElement("nodeinfobar-tagname").setTextContent(tagName);
+    this.getElement("nodeinfobar-tagname").setTextContent(displayName);
     this.getElement("nodeinfobar-id").setTextContent(id);
     this.getElement("nodeinfobar-classes").setTextContent(classList);
     this.getElement("nodeinfobar-pseudo-classes").setTextContent(pseudos);
     this.getElement("nodeinfobar-dimensions").setTextContent(dim);
 
     this._moveInfobar();
   },
 
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -177,16 +177,30 @@ var gInspectingNode = null;
 
 // We expect this function to be called from the child.js frame script
 // when it receives the node to be inspected over the message manager.
 exports.setInspectingNode = function (val) {
   gInspectingNode = val;
 };
 
 /**
+ * Returns the properly cased version of the node's tag name, which can be
+ * used when displaying said name in the UI.
+ *
+ * @param  {Node} rawNode
+ *         Node for which we want the display name
+ * @return {String}
+ *         Properly cased version of the node tag name
+ */
+const getNodeDisplayName = function (rawNode) {
+  return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
+};
+exports.getNodeDisplayName = getNodeDisplayName;
+
+/**
  * Server side of the node actor.
  */
 var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
   initialize: function (walker, node) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.walker = walker;
     this.rawNode = node;
     this._eventParsers = new EventParsers().parsers;
@@ -238,16 +252,17 @@ var NodeActor = exports.NodeActor = prot
 
     let form = {
       actor: this.actorID,
       baseURI: this.rawNode.baseURI,
       parent: parentNode ? parentNode.actorID : undefined,
       nodeType: this.rawNode.nodeType,
       namespaceURI: this.rawNode.namespaceURI,
       nodeName: this.rawNode.nodeName,
+      displayName: getNodeDisplayName(this.rawNode),
       numChildren: this.numChildren,
       singleTextChild: singleTextChild ? singleTextChild.form() : undefined,
 
       // doctype attributes
       name: this.rawNode.name,
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
@@ -1631,17 +1646,17 @@ var WalkerActor = protocol.ActorClassWit
 
       case "tag":
         if (!query) {
           nodes = this._multiFrameQuerySelectorAll("*");
         } else {
           nodes = this._multiFrameQuerySelectorAll(query);
         }
         for (let node of nodes) {
-          let tag = node.tagName.toLowerCase();
+          let tag = node.localName;
           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
         }
         for (let [tag, count] of sugs.tags) {
           if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
             result.push([tag, count, selectorState]);
           }
         }
 
@@ -1658,17 +1673,17 @@ var WalkerActor = protocol.ActorClassWit
         }
 
         break;
 
       case "null":
         nodes = this._multiFrameQuerySelectorAll(query);
         for (let node of nodes) {
           sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
-          let tag = node.tagName.toLowerCase();
+          let tag = node.localName;
           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
           for (let className of node.classList) {
             sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
           }
         }
         for (let [tag, count] of sugs.tags) {
           tag && result.push([tag, count]);
         }
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -886,17 +886,17 @@ var PageStyleActor = protocol.ActorClass
     let rawNode = node.rawNode;
 
     let selector;
     if (rawNode.id) {
       selector = "#" + CSS.escape(rawNode.id);
     } else if (rawNode.className) {
       selector = "." + [...rawNode.classList].map(c => CSS.escape(c)).join(".");
     } else {
-      selector = rawNode.tagName.toLowerCase();
+      selector = rawNode.localName;
     }
 
     if (pseudoClasses && pseudoClasses.length > 0) {
       selector += pseudoClasses.join("");
     }
 
     let index = sheet.insertRule(selector + " {}", cssRules.length);
 
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -205,16 +205,22 @@ const NodeFront = FrontClassWithSpec(nod
     return this._form.nodeType;
   },
   get namespaceURI() {
     return this._form.namespaceURI;
   },
   get nodeName() {
     return this._form.nodeName;
   },
+  get displayName() {
+    let {displayName, nodeName} = this._form;
+
+    // Keep `nodeName.toLowerCase()` for backward compatibility
+    return displayName || nodeName.toLowerCase();
+  },
   get doctypeString() {
     return "<!DOCTYPE " + this._form.name +
      (this._form.publicId ? " PUBLIC \"" + this._form.publicId + "\"" : "") +
      (this._form.systemId ? " \"" + this._form.systemId + "\"" : "") +
      ">";
   },
 
   get baseURI() {