new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/reps/grip-map.js
@@ -0,0 +1,193 @@
+/* -*- 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) {
+ // Dependencies
+ const React = require("devtools/client/shared/vendor/react");
+ const { createFactories, isGrip } = require("./rep-utils");
+ const { Caption } = createFactories(require("./caption"));
+ const { PropRep } = createFactories(require("./prop-rep"));
+
+ // Shortcuts
+ const { span } = React.DOM;
+ /**
+ * Renders an map. A map is represented by a list of its
+ * entries enclosed in curly brackets.
+ */
+ const GripMap = React.createClass({
+ displayName: "GripMap",
+
+ propTypes: {
+ object: React.PropTypes.object,
+ mode: React.PropTypes.string,
+ },
+
+ getTitle: function (object) {
+ let title = object && object.class ? object.class : "Map";
+ if (this.props.objectLink) {
+ return this.props.objectLink({
+ object: object
+ }, title);
+ }
+ return title;
+ },
+
+ safeEntriesIterator: function (object, max) {
+ max = (typeof max === "undefined") ? 3 : max;
+ try {
+ return this.entriesIterator(object, max);
+ } catch (err) {
+ console.error(err);
+ }
+ return [];
+ },
+
+ entriesIterator: function (object, max) {
+ // Entry filter. Show only interesting entries to the user.
+ let isInterestingEntry = this.props.isInterestingEntry || ((type, value) => {
+ return (
+ type == "boolean" ||
+ type == "number" ||
+ (type == "string" && value.length != 0)
+ );
+ });
+
+ let mapEntries = object.preview && object.preview.entries
+ ? object.preview.entries : [];
+
+ let indexes = this.getEntriesIndexes(mapEntries, max, isInterestingEntry);
+ if (indexes.length < max && indexes.length < mapEntries.length) {
+ // There are not enough entries yet, so we add uninteresting entries.
+ indexes = indexes.concat(
+ this.getEntriesIndexes(mapEntries, max - indexes.length, (t, value, name) => {
+ return !isInterestingEntry(t, value, name);
+ })
+ );
+ }
+
+ let entries = this.getEntries(mapEntries, indexes);
+ if (entries.length < mapEntries.length) {
+ // There are some undisplayed entries. Then display "more…".
+ let objectLink = this.props.objectLink || span;
+
+ entries.push(Caption({
+ key: "more",
+ object: objectLink({
+ object: object
+ }, `${mapEntries.length - max} more…`)
+ }));
+ }
+
+ return entries;
+ },
+
+ /**
+ * Get entries ordered by index.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Array} indexes Indexes of entries.
+ * @return {Array} Array of PropRep.
+ */
+ getEntries: function (entries, indexes) {
+ // Make indexes ordered by ascending.
+ indexes.sort(function (a, b) {
+ return a - b;
+ });
+
+ return indexes.map((index, i) => {
+ let [key, entryValue] = entries[index];
+ let value = entryValue.value !== undefined ? entryValue.value : entryValue;
+
+ return PropRep({
+ // key,
+ name: key,
+ equal: ": ",
+ object: value,
+ // Do not add a trailing comma on the last entry
+ // if there won't be a "more..." item.
+ delim: (i < indexes.length - 1 || indexes.length < entries.length) ? ", " : "",
+ mode: "tiny",
+ objectLink: this.props.objectLink,
+ });
+ });
+ },
+
+ /**
+ * Get the indexes of entries in the map.
+ *
+ * @param {Array} entries Entries array.
+ * @param {Number} max The maximum length of indexes array.
+ * @param {Function} filter Filter the entry you want.
+ * @return {Array} Indexes of filtered entries in the map.
+ */
+ getEntriesIndexes: function (entries, max, filter) {
+ return entries
+ .reduce((indexes, [key, entry], i) => {
+ if (indexes.length < max) {
+ let value = (entry && entry.value !== undefined) ? entry.value : entry;
+ // Type is specified in grip's "class" field and for primitive
+ // values use typeof.
+ let type = (value && value.class ? value.class : typeof value).toLowerCase();
+
+ if (filter(type, value, key)) {
+ indexes.push(i);
+ }
+ }
+
+ return indexes;
+ }, []);
+ },
+
+ render: function () {
+ let object = this.props.object;
+ let props = this.safeEntriesIterator(object,
+ (this.props.mode == "long") ? 100 : 3);
+
+ let objectLink = this.props.objectLink || span;
+ if (this.props.mode == "tiny") {
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, "")
+ )
+ );
+ }
+
+ return (
+ span({className: "objectBox objectBox-object"},
+ this.getTitle(object),
+ objectLink({
+ className: "objectLeftBrace",
+ object: object
+ }, " { "),
+ props,
+ objectLink({
+ className: "objectRightBrace",
+ object: object
+ }, " }")
+ )
+ );
+ },
+ });
+
+ function supportsObject(grip, type) {
+ if (!isGrip(grip)) {
+ return false;
+ }
+ return (grip.preview && grip.preview.kind == "MapLike");
+ }
+
+ // Exports from this module
+ exports.GripMap = {
+ rep: GripMap,
+ supportsObject: supportsObject
+ };
+});
--- a/devtools/client/shared/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -8,16 +8,17 @@ DevToolsModules(
'array.js',
'attribute.js',
'caption.js',
'date-time.js',
'document.js',
'event.js',
'function.js',
'grip-array.js',
+ 'grip-map.js',
'grip.js',
'null.js',
'number.js',
'object-with-text.js',
'object-with-url.js',
'object.js',
'prop-rep.js',
'regexp.js',
--- a/devtools/client/shared/components/reps/prop-rep.js
+++ b/devtools/client/shared/components/reps/prop-rep.js
@@ -4,43 +4,60 @@
* 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) {
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
-
const { span } = React.DOM;
/**
- * Property for Obj (local JS objects) and Grip (remote JS objects)
- * reps. It's used to render object properties.
+ * Property for Obj (local JS objects), Grip (remote JS objects)
+ * and GripMap (remote JS maps and weakmaps) reps.
+ * It's used to render object properties.
*/
let PropRep = React.createFactory(React.createClass({
displayName: "PropRep",
propTypes: {
// Property name.
- name: React.PropTypes.string,
+ name: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.object,
+ ]).isRequired,
// Equal character rendered between property name and value.
equal: React.PropTypes.string,
// Delimiter character used to separate individual properties.
delim: React.PropTypes.string,
+ mode: React.PropTypes.string,
},
render: function () {
+ const { Grip } = require("./grip");
let { Rep } = createFactories(require("./rep"));
+ let key;
+ // The key can be a simple string, for plain objects,
+ // or another object for maps and weakmaps.
+ if (typeof this.props.name === "string") {
+ key = span({"className": "nodeName"}, this.props.name);
+ } else {
+ key = Rep({
+ object: this.props.name,
+ mode: this.props.mode || "tiny",
+ defaultRep: Grip,
+ objectLink: this.props.objectLink,
+ });
+ }
+
return (
span({},
- span({
- "className": "nodeName"},
- this.props.name),
+ key,
span({
"className": "objectEqual"
}, this.props.equal),
Rep(this.props),
span({
"className": "objectComma"
}, this.props.delim)
)
--- a/devtools/client/shared/components/reps/rep.js
+++ b/devtools/client/shared/components/reps/rep.js
@@ -30,16 +30,17 @@ define(function (require, exports, modul
const { Func } = require("./function");
const { RegExp } = require("./regexp");
const { StyleSheet } = require("./stylesheet");
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");
// List of all registered template.
// XXX there should be a way for extensions to register a new
// or modify an existing rep.
let reps = [
RegExp,
StyleSheet,
@@ -49,16 +50,17 @@ define(function (require, exports, modul
Attribute,
Func,
ArrayRep,
Document,
Window,
ObjectWithText,
ObjectWithURL,
GripArray,
+ GripMap,
Grip,
Undefined,
Null,
StringRep,
Number,
SymbolRep,
];
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -10,16 +10,17 @@ support-files =
[test_reps_array.html]
[test_reps_attribute.html]
[test_reps_date-time.html]
[test_reps_document.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_null.html]
[test_reps_number.html]
[test_reps_object.html]
[test_reps_object-with-text.html]
[test_reps_object-with-url.html]
[test_reps_regexp.html]
[test_reps_string.html]
[test_reps_stylesheet.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-map.html
@@ -0,0 +1,403 @@
+
+<!DOCTYPE HTML>
+<html>
+<!--
+Test GripMap rep
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Rep test - GripMap</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 { GripMap } = browserRequire("devtools/client/shared/components/reps/grip-map");
+
+ const componentUnderTest = GripMap;
+
+ try {
+ yield testEmptyMap();
+ yield testSymbolKeyedMap();
+ yield testWeakMap();
+
+ // // Test entries iterator
+ yield testMaxEntries();
+ yield testMoreThanMaxEntries();
+ yield testUninterestingEntries();
+ } catch (e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+
+ function testEmptyMap() {
+ // Test object: `new Map()`
+ const testName = "testEmptyMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testEmptyMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `Map { }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testSymbolKeyedMap() {
+ // Test object:
+ // `new Map([[Symbol("a"), "value-a"], [Symbol("b"), "value-b"]])`
+ const testName = "testSymbolKeyedMap";
+
+ const defaultOutput = `Map { Symbol(a): "value-a", Symbol(b): "value-b" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testWeakMap() {
+ // Test object: `new WeakMap([[{a: "key-a"}, "value-a"]])`
+ const testName = "testWeakMap";
+
+ // Test that correct rep is chosen
+ const gripStub = getGripStub("testWeakMap");
+ const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
+ is(renderedRep.type, GripMap.rep, `Rep correctly selects ${GripMap.rep.displayName}`);
+
+ // Test rendering
+ const defaultOutput = `WeakMap { Object: "value-a" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "WeakMap",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMaxEntries() {
+ // Test object:
+ // `new Map([["key-a","value-a"], ["key-b","value-b"], ["key-c","value-c"]])`
+ const testName = "testMaxEntries";
+
+ const defaultOutput = `Map { key-a: "value-a", key-b: "value-b", key-c: "value-c" }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: "Map",
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: defaultOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testMoreThanMaxEntries() {
+ // Test object = `new Map(
+ // [["key-0", "value-0"], ["key-1", "value-1"]], …, ["key-100", "value-100"]]}`
+ const testName = "testMoreThanMaxEntries";
+
+ const defaultOutput =
+ `Map { key-0: "value-0", key-1: "value-1", key-2: "value-2", 98 more… }`;
+
+ // Generate string with 101 entries, which is the max limit for 'long' mode.
+ let longString = Array.from({length: 100}).map((_, i) => `key-${i}: "value-${i}"`);
+ const longOutput = `Map { ${longString.join(", ")}, 1 more… }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function testUninterestingEntries() {
+ // Test object:
+ // `new Map([["key-a",null], ["key-b",undefined], ["key-c","value-c"], ["key-d",4]])`
+ const testName = "testUninterestingEntries";
+
+ const defaultOutput =
+ `Map { key-a: null, key-c: "value-c", key-d: 4, 1 more… }`;
+ const longOutput =
+ `Map { key-a: null, key-b: undefined, key-c: "value-c", key-d: 4 }`;
+
+ const modeTests = [
+ {
+ mode: undefined,
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "tiny",
+ expectedOutput: `Map`,
+ },
+ {
+ mode: "short",
+ expectedOutput: defaultOutput,
+ },
+ {
+ mode: "long",
+ expectedOutput: longOutput,
+ }
+ ];
+
+ testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
+ }
+
+ function getGripStub(functionName) {
+ switch (functionName) {
+ case "testEmptyMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj97",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 0,
+ "entries": []
+ }
+ };
+
+ case "testSymbolKeyedMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj118",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 2,
+ "entries": [
+ [
+ {
+ "type": "symbol",
+ "name": "a"
+ },
+ "value-a"
+ ],
+ [
+ {
+ "type": "symbol",
+ "name": "b"
+ },
+ "value-b"
+ ]
+ ]
+ }
+ };
+
+ case "testWeakMap":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj115",
+ "class": "WeakMap",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 1,
+ "entries": [
+ [
+ {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj116",
+ "class": "Object",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 1
+ },
+ "value-a"
+ ]
+ ]
+ }
+ };
+
+ case "testMaxEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj109",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 3,
+ "entries": [
+ [
+ "key-a",
+ "value-a"
+ ],
+ [
+ "key-b",
+ "value-b"
+ ],
+ [
+ "key-c",
+ "value-c"
+ ]
+ ]
+ }
+ };
+
+ case "testMoreThanMaxEntries": {
+ let entryNb = 101;
+ return {
+ "type": "object",
+ "class": "Map",
+ "actor": "server1.conn0.obj332",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": entryNb,
+ // Generate 101 entries, which is more that the maximum
+ // limit in case of the 'long' mode.
+ "entries": Array.from({length: entryNb}).map((_, i) => {
+ return [`key-${i}`, `value-${i}`];
+ })
+ }
+ };
+ }
+
+ case "testUninterestingEntries":
+ return {
+ "type": "object",
+ "actor": "server1.conn1.child1/obj111",
+ "class": "Map",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "ownPropertyLength": 0,
+ "preview": {
+ "kind": "MapLike",
+ "size": 4,
+ "entries": [
+ [
+ "key-a",
+ {
+ "type": "null"
+ }
+ ],
+ [
+ "key-b",
+ {
+ "type": "undefined"
+ }
+ ],
+ [
+ "key-c",
+ "value-c"
+ ],
+ [
+ "key-d",
+ 4
+ ]
+ ]
+ }
+ };
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>