Bug 1307938 - Add a Rep for DOM nodes. r=Honza,pbro; draft
authorNicolas Chevobbe <chevobbe.nicolas@gmail.com>
Mon, 17 Oct 2016 19:29:32 +0200
changeset 432599 e0751da8bbdaa91983f4ba34550cbe9860b8fff6
parent 432230 3e73fd638e687a4d7f46613586e5156b8e2af846
child 535709 bfa7c44c225746ad502fe2bf563fee571377feb1
push id34375
push userchevobbe.nicolas@gmail.com
push dateWed, 02 Nov 2016 12:40:57 +0000
reviewersHonza, pbro
bugs1307938
milestone52.0a1
Bug 1307938 - Add a Rep for DOM nodes. r=Honza,pbro; Add tests based on existing browser_webconsole_output_dom_elements_*.js tests. MozReview-Commit-ID: Wmzg0knuuh
devtools/client/shared/components/reps/element-node.js
devtools/client/shared/components/reps/moz.build
devtools/client/shared/components/reps/rep.js
devtools/client/shared/components/test/mochitest/chrome.ini
devtools/client/shared/components/test/mochitest/test_reps_element-node.html
devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/reps/element-node.js
@@ -0,0 +1,114 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+  // ReactJS
+  const React = require("devtools/client/shared/vendor/react");
+  const { isGrip } = require("./rep-utils");
+
+  // Utils
+  const nodeConstants = require("devtools/shared/dom-node-constants");
+
+  // Shortcuts
+  const { span } = React.DOM;
+
+  /**
+   * Renders DOM element node.
+   */
+  const ElementNode = React.createClass({
+    displayName: "ElementNode",
+
+    propTypes: {
+      object: React.PropTypes.object.isRequired,
+      mode: React.PropTypes.string,
+    },
+
+    getElements: function (grip, mode) {
+      let {attributes, nodeName} = grip.preview;
+      const nodeNameElement = span({
+        className: "tag-name theme-fg-color3"
+      }, nodeName);
+
+      if (mode === "tiny") {
+        let elements = [nodeNameElement];
+        if (attributes.id) {
+          elements.push(
+            span({className: "attr-name theme-fg-color2"}, `#${attributes.id}`));
+        }
+        if (attributes.class) {
+          elements.push(
+            span({className: "attr-name theme-fg-color2"},
+              attributes.class
+                .replace(/(^\s+)|(\s+$)/g, "")
+                .split(" ")
+                .map(cls => `.${cls}`)
+                .join("")
+            )
+          );
+        }
+        return elements;
+      }
+      let attributeElements = Object.keys(attributes)
+        .sort(function getIdAndClassFirst(a1, a2) {
+          if ([a1, a2].includes("id")) {
+            return 3 * (a1 === "id" ? -1 : 1);
+          }
+          if ([a1, a2].includes("class")) {
+            return 2 * (a1 === "class" ? -1 : 1);
+          }
+
+          // `id` and `class` excepted,
+          // we want to keep the same order that in `attributes`.
+          return 0;
+        })
+        .reduce((arr, name, i, keys) => {
+          let value = attributes[name];
+          let attribute = span({},
+            span({className: "attr-name theme-fg-color2"}, `${name}`),
+            `="`,
+            span({className: "attr-value theme-fg-color6"}, `${value}`),
+            `"`
+          );
+
+          return arr.concat([" ", attribute]);
+        }, []);
+
+      return [
+        "<",
+        nodeNameElement,
+        ...attributeElements,
+        ">",
+      ];
+    },
+
+    render: function () {
+      let {object, mode} = this.props;
+      let elements = this.getElements(object, mode);
+      const baseElement = span({className: "objectBox"}, ...elements);
+
+      if (this.props.objectLink) {
+        return this.props.objectLink({object}, baseElement);
+      }
+      return baseElement;
+    },
+  });
+
+  // Registration
+  function supportsObject(object, type) {
+    if (!isGrip(object)) {
+      return false;
+    }
+    return object.preview && object.preview.nodeType === nodeConstants.ELEMENT_NODE;
+  }
+
+  // Exports from this module
+  exports.ElementNode = {
+    rep: ElementNode,
+    supportsObject: supportsObject
+  };
+});
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -6,16 +6,17 @@
 
 DevToolsModules(
     'array.js',
     'attribute.js',
     'caption.js',
     'comment-node.js',
     'date-time.js',
     'document.js',
+    'element-node.js',
     'event.js',
     'function.js',
     'grip-array.js',
     'grip-map.js',
     'grip.js',
     'infinity.js',
     'nan.js',
     'null.js',
--- a/devtools/client/shared/components/reps/rep.js
+++ b/devtools/client/shared/components/reps/rep.js
@@ -29,16 +29,17 @@ define(function (require, exports, modul
   const { DateTime } = require("./date-time");
   const { Document } = require("./document");
   const { Event } = require("./event");
   const { Func } = require("./function");
   const { PromiseRep } = require("./promise");
   const { RegExp } = require("./regexp");
   const { StyleSheet } = require("./stylesheet");
   const { CommentNode } = require("./comment-node");
+  const { ElementNode } = require("./element-node");
   const { TextNode } = require("./text-node");
   const { Window } = require("./window");
   const { ObjectWithText } = require("./object-with-text");
   const { ObjectWithURL } = require("./object-with-url");
   const { GripArray } = require("./grip-array");
   const { GripMap } = require("./grip-map");
   const { Grip } = require("./grip");
 
@@ -46,16 +47,17 @@ define(function (require, exports, modul
   // XXX there should be a way for extensions to register a new
   // or modify an existing rep.
   let reps = [
     RegExp,
     StyleSheet,
     Event,
     DateTime,
     CommentNode,
+    ElementNode,
     TextNode,
     Attribute,
     Func,
     PromiseRep,
     ArrayRep,
     Document,
     Window,
     ObjectWithText,
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -7,16 +7,17 @@ support-files =
 [test_notification_box_01.html]
 [test_notification_box_02.html]
 [test_notification_box_03.html]
 [test_reps_array.html]
 [test_reps_attribute.html]
 [test_reps_comment-node.html]
 [test_reps_date-time.html]
 [test_reps_document.html]
+[test_reps_element-node.html]
 [test_reps_event.html]
 [test_reps_function.html]
 [test_reps_grip.html]
 [test_reps_grip-array.html]
 [test_reps_grip-map.html]
 [test_reps_infinity.html]
 [test_reps_nan.html]
 [test_reps_null.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_element-node.html
@@ -0,0 +1,339 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test Element node rep
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Rep test - Element node</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+"use strict";
+
+window.onload = Task.async(function* () {
+  let { Rep } = browserRequire("devtools/client/shared/components/reps/rep");
+  let { ElementNode } = browserRequire("devtools/client/shared/components/reps/element-node");
+
+  try {
+    yield testBodyNode();
+    yield testDocumentElement();
+    yield testNode();
+    yield testNodeWithLeadingAndTrailingSpacesClassName();
+    yield testNodeWithoutAttributes();
+    yield testLotsOfAttributes();
+    yield testSvgNode();
+    yield testSvgNodeInXHTML();
+  } catch (e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+
+  function testBodyNode() {
+    const stub = getGripStub("testBodyNode");
+    const renderedRep = shallowRenderComponent(Rep, { object: stub });
+    is(renderedRep.type, ElementNode.rep,
+      `Rep correctly selects ${ElementNode.rep.displayName} for body node`);
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent, `<body id="body-id" class="body-class">`,
+      "Element node rep has expected text content for body node");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent, `body#body-id.body-class`,
+      "Element node rep has expected text content for body node in tiny mode");
+  }
+
+  function testDocumentElement() {
+    const stub = getGripStub("testDocumentElement");
+    const renderedRep = shallowRenderComponent(Rep, { object: stub });
+    is(renderedRep.type, ElementNode.rep,
+      `Rep correctly selects ${ElementNode.rep.displayName} for document element node`);
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent, `<html dir="ltr" lang="en-US">`,
+      "Element node rep has expected text content for document element node");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent, `html`,
+      "Element node rep has expected text content for document element in tiny mode");
+  }
+
+  function testNode() {
+    const stub = getGripStub("testNode");
+    const renderedRep = shallowRenderComponent(Rep, { object: stub });
+    is(renderedRep.type, ElementNode.rep,
+      `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent,
+      `<input id="newtab-customize-button" class="bar baz" dir="ltr" ` +
+      `title="Customize your New Tab page" value="foo" type="button">`,
+      "Element node rep has expected text content for element node");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent,
+      `input#newtab-customize-button.bar.baz`,
+      "Element node rep has expected text content for element node in tiny mode");
+  }
+
+  function testNodeWithLeadingAndTrailingSpacesClassName() {
+    const stub = getGripStub("testNodeWithLeadingAndTrailingSpacesClassName");
+    const renderedRep = shallowRenderComponent(Rep, { object: stub });
+    is(renderedRep.type, ElementNode.rep,
+      `Rep correctly selects ${ElementNode.rep.displayName} for element node`);
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent,
+      `<body id="nightly-whatsnew" class="  html-ltr    ">`,
+      "Element node rep output element node with the class trailing spaces");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent,
+      `body#nightly-whatsnew.html-ltr`,
+      "Element node rep does not show leading nor trailing spaces " +
+      "on class attribute in tiny mode");
+  }
+
+  function testNodeWithoutAttributes() {
+    const stub = getGripStub("testNodeWithoutAttributes");
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent, "<p>",
+      "Element node rep has expected text content for element node without attributes");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent, `p`,
+      "Element node rep has expected text content for element node without attributes");
+  }
+
+  function testLotsOfAttributes() {
+    const stub = getGripStub("testLotsOfAttributes");
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent,
+      '<p id="lots-of-attributes" a="" b="" c="" d="" e="" f="" g="" ' +
+      'h="" i="" j="" k="" l="" m="" n="">',
+      "Element node rep has expected text content for node with lots of attributes");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent, `p#lots-of-attributes`,
+      "Element node rep has expected text content for node in tiny mode");
+  }
+
+  function testSvgNode() {
+    const stub = getGripStub("testSvgNode");
+
+    const renderedRep = shallowRenderComponent(Rep, { object: stub });
+    is(renderedRep.type, ElementNode.rep,
+      `Rep correctly selects ${ElementNode.rep.displayName} for SVG element node`);
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent,
+      '<clipPath id="clip" class="svg-element">',
+      "Element node rep has expected text content for SVG element node");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent, `clipPath#clip.svg-element`,
+      "Element node rep has expected text content for SVG element node in tiny mode");
+  }
+
+  function testSvgNodeInXHTML() {
+    const stub = getGripStub("testSvgNodeInXHTML");
+
+    const renderedRep = shallowRenderComponent(Rep, { object: stub });
+    is(renderedRep.type, ElementNode.rep,
+      `Rep correctly selects ${ElementNode.rep.displayName} for XHTML SVG element node`);
+
+    const renderedComponent = renderComponent(ElementNode.rep, { object: stub });
+    is(renderedComponent.textContent,
+      '<svg:circle class="svg-element" cx="0" cy="0" r="5">',
+      "Element node rep has expected text content for XHTML SVG element node");
+
+    const tinyRenderedComponent = renderComponent(
+      ElementNode.rep, { object: stub, mode: "tiny" });
+    is(tinyRenderedComponent.textContent, `svg:circle.svg-element`,
+      "Element node rep has expected text content for XHTML SVG element in tiny mode");
+  }
+
+  function getGripStub(name) {
+    switch (name) {
+      case "testBodyNode":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj30",
+          "class": "HTMLBodyElement",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "body",
+            "attributes": {
+              "class": "body-class",
+              "id": "body-id"
+            },
+            "attributesLength": 2
+          }
+        };
+      case "testDocumentElement":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj40",
+          "class": "HTMLHtmlElement",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "html",
+            "attributes": {
+              "dir": "ltr",
+              "lang": "en-US"
+            },
+            "attributesLength": 2
+          }
+        };
+      case "testNode":
+        return {
+          "type": "object",
+          "actor": "server1.conn2.child1/obj116",
+          "class": "HTMLInputElement",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "input",
+            "attributes": {
+              "id": "newtab-customize-button",
+              "dir": "ltr",
+              "title": "Customize your New Tab page",
+              "class": "bar baz",
+              "value": "foo",
+              "type": "button"
+            },
+            "attributesLength": 6
+          }
+        };
+      case "testNodeWithLeadingAndTrailingSpacesClassName":
+        return {
+          "type": "object",
+          "actor": "server1.conn3.child1/obj59",
+          "class": "HTMLBodyElement",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "body",
+            "attributes": {
+              "id": "nightly-whatsnew",
+              "class": "  html-ltr    "
+            },
+            "attributesLength": 2
+          }
+        };
+      case "testNodeWithoutAttributes":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj32",
+          "class": "HTMLParagraphElement",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "p",
+            "attributes": {},
+            "attributesLength": 1
+          }
+        };
+      case "testLotsOfAttributes":
+        return {
+          "type": "object",
+          "actor": "server1.conn2.child1/obj30",
+          "class": "HTMLParagraphElement",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "p",
+            "attributes": {
+              "id": "lots-of-attributes",
+              "a": "",
+              "b": "",
+              "c": "",
+              "d": "",
+              "e": "",
+              "f": "",
+              "g": "",
+              "h": "",
+              "i": "",
+              "j": "",
+              "k": "",
+              "l": "",
+              "m": "",
+              "n": ""
+            },
+            "attributesLength": 15
+          }
+        };
+      case "testSvgNode":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj42",
+          "class": "SVGClipPathElement",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "clipPath",
+            "attributes": {
+              "id": "clip",
+              "class": "svg-element"
+            },
+            "attributesLength": 0
+          }
+        };
+      case "testSvgNodeInXHTML":
+        return {
+          "type": "object",
+          "actor": "server1.conn3.child1/obj34",
+          "class": "SVGCircleElement",
+          "ownPropertyLength": 0,
+          "preview": {
+            "kind": "DOMNode",
+            "nodeType": 1,
+            "nodeName": "svg:circle",
+            "attributes": {
+              "class": "svg-element",
+              "cx": "0",
+              "cy": "0",
+              "r": "5"
+            },
+            "attributesLength": 3
+          }
+        };
+    }
+    return null;
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -28,18 +28,18 @@ window.onload = Task.async(function* () 
     yield testBasic();
 
     // Test property iterator
     yield testMaxProps();
     yield testMoreThanShortMaxProps();
     yield testMoreThanLongMaxProps();
     yield testRecursiveArray();
     yield testPreviewLimit();
-
     yield testNamedNodeMap();
+    yield testNodeList();
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testBasic() {
     // Test array: `[]`
@@ -238,16 +238,43 @@ window.onload = Task.async(function* () 
         mode: "long",
         expectedOutput: defaultOutput,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
+  function testNodeList() {
+    const testName = "testNodeList";
+    const defaultOutput = "NodeList [ button#btn-1.btn.btn-log, " +
+      "button#btn-2.btn.btn-err, button#btn-3.btn.btn-count ]";
+
+    const modeTests = [
+      {
+        mode: undefined,
+        expectedOutput: defaultOutput,
+      },
+      {
+        mode: "tiny",
+        expectedOutput: `[3]`,
+      },
+      {
+        mode: "short",
+        expectedOutput: defaultOutput,
+      },
+      {
+        mode: "long",
+        expectedOutput: defaultOutput,
+      }
+    ];
+
+    testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+  }
+
   function getGripStub(functionName) {
     switch (functionName) {
       case "testBasic":
         return {
           "type": "object",
           "class": "Array",
           "actor": "server1.conn0.obj35",
           "extensible": true,
@@ -439,15 +466,91 @@ window.onload = Task.async(function* () 
                     "nodeType": 2,
                     "nodeName": "border",
                     "value": "3"
                   }
                 }
               ]
             }
           };
+      case "testNodeList":
+        return {
+          "type": "object",
+          "actor": "server1.conn1.child1/obj51",
+          "class": "NodeList",
+          "extensible": true,
+          "frozen": false,
+          "sealed": false,
+          "ownPropertyLength": 3,
+          "preview": {
+            "kind": "ArrayLike",
+            "length": 3,
+            "items": [
+              {
+                "type": "object",
+                "actor": "server1.conn1.child1/obj52",
+                "class": "HTMLButtonElement",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0,
+                "preview": {
+                  "kind": "DOMNode",
+                  "nodeType": 1,
+                  "nodeName": "button",
+                  "attributes": {
+                    "id": "btn-1",
+                    "class": "btn btn-log",
+                    "type": "button"
+                  },
+                  "attributesLength": 3
+                }
+              },
+              {
+                "type": "object",
+                "actor": "server1.conn1.child1/obj53",
+                "class": "HTMLButtonElement",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0,
+                "preview": {
+                  "kind": "DOMNode",
+                  "nodeType": 1,
+                  "nodeName": "button",
+                  "attributes": {
+                    "id": "btn-2",
+                    "class": "btn btn-err",
+                    "type": "button"
+                  },
+                  "attributesLength": 3
+                }
+              },
+              {
+                "type": "object",
+                "actor": "server1.conn1.child1/obj54",
+                "class": "HTMLButtonElement",
+                "extensible": true,
+                "frozen": false,
+                "sealed": false,
+                "ownPropertyLength": 0,
+                "preview": {
+                  "kind": "DOMNode",
+                  "nodeType": 1,
+                  "nodeName": "button",
+                  "attributes": {
+                    "id": "btn-3",
+                    "class": "btn btn-count",
+                    "type": "button"
+                  },
+                  "attributesLength": 3
+                }
+              }
+            ]
+          }
+        };
     }
   }
 });
 </script>
 </pre>
 </body>
 </html>