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>