Bug 1380709 - Fetch Map/Set entries when expanding the Object Inspector. r= draft
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Thu, 20 Jul 2017 16:10:20 +0100
changeset 618445 243293817b7b4cc46edd5350d9466c824df13e7e
parent 618288 a6c502679d442596b8b4410d6413cd310ac46408
child 640069 6281dab2e1a8999ad1723a3282a2cb6ac9a6b4c4
push id71332
push userbmo:nchevobbe@mozilla.com
push dateMon, 31 Jul 2017 13:32:49 +0000
bugs1380709
milestone56.0a1
Bug 1380709 - Fetch Map/Set entries when expanding the Object Inspector. r= MozReview-Commit-ID: 3RSCpxTxeMF
devtools/client/webconsole/new-console-output/actions/messages.js
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/components/grip-message-body.js
devtools/client/webconsole/new-console-output/components/message-container.js
devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
devtools/client/webconsole/new-console-output/constants.js
devtools/client/webconsole/new-console-output/reducers/messages.js
devtools/client/webconsole/new-console-output/selectors/messages.js
devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_object_inspector_entries.js
devtools/client/webconsole/new-console-output/test/store/messages.test.js
devtools/client/webconsole/new-console-output/test/store/release-actors.test.js
devtools/client/webconsole/new-console-output/test/store/search.test.js
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -16,16 +16,17 @@ const {
   MESSAGE_ADD,
   NETWORK_MESSAGE_UPDATE,
   MESSAGES_CLEAR,
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
   MESSAGE_TYPE,
   MESSAGE_TABLE_RECEIVE,
   MESSAGE_OBJECT_PROPERTIES_RECEIVE,
+  MESSAGE_OBJECT_ENTRIES_RECEIVE,
 } = require("../constants");
 
 const defaultIdGenerator = new IdGenerator();
 
 function messageAdd(packet, idGenerator = null) {
   if (idGenerator == null) {
     idGenerator = defaultIdGenerator;
   }
@@ -121,16 +122,28 @@ function networkMessageUpdate(packet, id
  */
 function messageObjectPropertiesLoad(id, client, grip) {
   return async (dispatch) => {
     const response = await client.getPrototypeAndProperties();
     dispatch(messageObjectPropertiesReceive(id, grip.actor, response));
   };
 }
 
+function messageObjectEntriesLoad(id, client, grip) {
+  return (dispatch) => {
+    client.enumEntries(enumResponse => {
+      const {iterator} = enumResponse;
+      iterator.slice(0, iterator.count, sliceResponse => {
+        console.log("sliceResponse", sliceResponse);
+        dispatch(messageObjectEntriesReceive(id, grip.actor, sliceResponse));
+      });
+    });
+  }
+}
+
 /**
  * This action is dispatched when properties of a grip are loaded.
  *
  * @param {string} id - The message id the grip is in.
  * @param {string} actor - The actor id of the grip the properties were loaded from.
  * @param {object} properties - A RDP packet that contains the properties of the grip.
  * @returns {object}
  */
@@ -138,21 +151,41 @@ function messageObjectPropertiesReceive(
   return {
     type: MESSAGE_OBJECT_PROPERTIES_RECEIVE,
     id,
     actor,
     properties
   };
 }
 
+/**
+ * This action is dispatched when entries of a grip are loaded.
+ *
+ * @param {string} id - The message id the grip is in.
+ * @param {string} actor - The actor id of the grip the properties were loaded from.
+ * @param {object} entries - A RDP packet that contains the entries of the grip.
+ * @returns {object}
+ */
+function messageObjectEntriesReceive(id, actor, entries) {
+console.log("messageObjectEntriesReceive", entries);
+  return {
+    type: MESSAGE_OBJECT_ENTRIES_RECEIVE,
+    id,
+    actor,
+    entries
+  };
+}
+
 module.exports = {
   messageAdd,
   messagesClear,
   messageOpen,
   messageClose,
   messageTableDataGet,
   networkMessageUpdate,
   messageObjectPropertiesLoad,
+  messageObjectEntriesLoad,
   // for test purpose only.
   messageTableDataReceive,
   messageObjectPropertiesReceive,
+  messageObjectEntriesReceive,
 };
 
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -11,16 +11,17 @@ const {
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const {
   getAllMessagesById,
   getAllMessagesUiById,
   getAllMessagesTableDataById,
   getAllMessagesObjectPropertiesById,
+  getAllMessagesObjectEntriesById,
   getAllNetworkMessagesUpdateById,
   getVisibleMessages,
   getAllRepeatById,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
 
 const ConsoleOutput = createClass({
 
@@ -33,16 +34,17 @@ const ConsoleOutput = createClass({
       attachRefToHud: PropTypes.func.isRequired,
       openContextMenu: PropTypes.func.isRequired,
       sourceMapService: PropTypes.object,
     }),
     dispatch: PropTypes.func.isRequired,
     timestampsVisible: PropTypes.bool,
     messagesTableData: PropTypes.object.isRequired,
     messagesObjectProperties: PropTypes.object.isRequired,
+    messagesObjectEntries: PropTypes.object.isRequired,
     messagesRepeat: PropTypes.object.isRequired,
     networkMessagesUpdate: PropTypes.object.isRequired,
     visibleMessages: PropTypes.array.isRequired,
   },
 
   componentDidMount() {
     // Do the scrolling in the nextTick since this could hit console startup performances.
     // See https://bugzilla.mozilla.org/show_bug.cgi?id=1355869
@@ -80,16 +82,17 @@ const ConsoleOutput = createClass({
   render() {
     let {
       dispatch,
       visibleMessages,
       messages,
       messagesUi,
       messagesTableData,
       messagesObjectProperties,
+      messagesObjectEntries,
       messagesRepeat,
       networkMessagesUpdate,
       serviceContainer,
       timestampsVisible,
     } = this.props;
 
     let messageNodes = visibleMessages.map((messageId) => MessageContainer({
       dispatch,
@@ -98,16 +101,17 @@ const ConsoleOutput = createClass({
       serviceContainer,
       open: messagesUi.includes(messageId),
       tableData: messagesTableData.get(messageId),
       timestampsVisible,
       repeat: messagesRepeat[messageId],
       networkMessageUpdate: networkMessagesUpdate[messageId],
       getMessage: () => messages.get(messageId),
       loadedObjectProperties: messagesObjectProperties.get(messageId),
+      loadedObjectEntries: messagesObjectEntries.get(messageId),
     }));
 
     return (
       dom.div({
         className: "webconsole-output",
         onContextMenu: this.onContextMenu,
         ref: node => {
           this.outputNode = node;
@@ -131,15 +135,16 @@ function isScrolledToBottom(outputNode, 
 
 function mapStateToProps(state, props) {
   return {
     messages: getAllMessagesById(state),
     visibleMessages: getVisibleMessages(state),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
     messagesObjectProperties: getAllMessagesObjectPropertiesById(state),
+    messagesObjectEntries: getAllMessagesObjectEntriesById(state),
     messagesRepeat: getAllRepeatById(state),
     networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
     timestampsVisible: state.ui.timestampsVisible,
   };
 }
 
 module.exports = connect(mapStateToProps)(ConsoleOutput);
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -40,16 +40,17 @@ GripMessageBody.propTypes = {
   serviceContainer: PropTypes.shape({
     createElement: PropTypes.func.isRequired,
     hudProxyClient: PropTypes.object.isRequired,
   }),
   userProvidedStyle: PropTypes.string,
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
   loadedObjectProperties: PropTypes.object,
+  loadedObjectEntries: PropTypes.object,
   type: PropTypes.string,
   helperType: PropTypes.string,
 };
 
 GripMessageBody.defaultProps = {
   mode: MODE.LONG,
 };
 
@@ -59,16 +60,17 @@ function GripMessageBody(props) {
     messageId,
     grip,
     userProvidedStyle,
     serviceContainer,
     useQuotes,
     escapeWhitespace,
     mode = MODE.LONG,
     loadedObjectProperties,
+    loadedObjectEntries,
   } = props;
 
   let styleObject;
   if (userProvidedStyle && userProvidedStyle !== "") {
     styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
   }
 
   let onDOMNodeMouseOver;
@@ -101,16 +103,21 @@ function GripMessageBody(props) {
         value: grip
       }
     }],
     getObjectProperties: actor => loadedObjectProperties && loadedObjectProperties[actor],
     loadObjectProperties: object => {
       const client = new ObjectClient(serviceContainer.hudProxyClient, object);
       dispatch(actions.messageObjectPropertiesLoad(messageId, client, object));
     },
+    getObjectEntries: actor => loadedObjectEntries && loadedObjectEntries[actor],
+    loadObjectEntries: object => {
+      const client = new ObjectClient(serviceContainer.hudProxyClient, object);
+      dispatch(actions.messageObjectEntriesLoad(messageId, client, object));
+    },
   };
 
   if (typeof grip === "string" || grip.type === "longString") {
     Object.assign(objectInspectorProps, {
       useQuotes,
       escapeWhitespace,
       style: styleObject
     });
--- a/devtools/client/webconsole/new-console-output/components/message-container.js
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -34,16 +34,17 @@ const MessageContainer = createClass({
     open: PropTypes.bool.isRequired,
     serviceContainer: PropTypes.object.isRequired,
     tableData: PropTypes.object,
     timestampsVisible: PropTypes.bool.isRequired,
     repeat: PropTypes.number,
     networkMessageUpdate: PropTypes.object,
     getMessage: PropTypes.func.isRequired,
     loadedObjectProperties: PropTypes.object,
+    loadedObjectEntries: PropTypes.object,
   },
 
   getDefaultProps: function () {
     return {
       open: false,
     };
   },
 
@@ -52,23 +53,26 @@ const MessageContainer = createClass({
     const openChanged = this.props.open !== nextProps.open;
     const tableDataChanged = this.props.tableData !== nextProps.tableData;
     const timestampVisibleChanged =
       this.props.timestampsVisible !== nextProps.timestampsVisible;
     const networkMessageUpdateChanged =
       this.props.networkMessageUpdate !== nextProps.networkMessageUpdate;
     const loadedObjectPropertiesChanged =
       this.props.loadedObjectProperties !== nextProps.loadedObjectProperties;
+    const loadedObjectEntriesChanged =
+      this.props.loadedObjectEntries !== nextProps.loadedObjectEntries;
 
     return repeatChanged
       || openChanged
       || tableDataChanged
       || timestampVisibleChanged
       || networkMessageUpdateChanged
-      || loadedObjectPropertiesChanged;
+      || loadedObjectPropertiesChanged
+      || loadedObjectEntriesChanged;
   },
 
   render() {
     const message = this.props.getMessage();
 
     let MessageComponent = getMessageComponent(message);
     return MessageComponent(Object.assign({message}, this.props));
   }
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -22,32 +22,34 @@ ConsoleApiCall.displayName = "ConsoleApi
 
 ConsoleApiCall.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   serviceContainer: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
   loadedObjectProperties: PropTypes.object,
+  loadedObjectEntries: PropTypes.object,
 };
 
 ConsoleApiCall.defaultProps = {
   open: false,
 };
 
 function ConsoleApiCall(props) {
   const {
     dispatch,
     message,
     open,
     tableData,
     serviceContainer,
     timestampsVisible,
     repeat,
     loadedObjectProperties,
+    loadedObjectEntries,
   } = props;
   const {
     id: messageId,
     indent,
     source,
     type,
     level,
     stacktrace,
@@ -57,16 +59,17 @@ function ConsoleApiCall(props) {
     messageText,
     userProvidedStyles,
   } = message;
 
   let messageBody;
   const messageBodyConfig = {
     dispatch,
     loadedObjectProperties,
+    loadedObjectEntries,
     messageId,
     parameters,
     userProvidedStyles,
     serviceContainer,
     type,
   };
 
   if (type === "trace") {
@@ -124,16 +127,17 @@ function ConsoleApiCall(props) {
     timestampsVisible,
   });
 }
 
 function formatReps(options = {}) {
   const {
     dispatch,
     loadedObjectProperties,
+    loadedObjectEntries,
     messageId,
     parameters,
     serviceContainer,
     userProvidedStyles,
     type,
   } = options;
 
   return (
@@ -143,16 +147,17 @@ function formatReps(options = {}) {
         dispatch,
         messageId,
         grip,
         key,
         userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null,
         serviceContainer,
         useQuotes: false,
         loadedObjectProperties,
+        loadedObjectEntries,
         type,
       }))
       // Interleave spaces.
       .reduce((arr, v, i) => {
         return i + 1 < parameters.length
           ? arr.concat(v, dom.span({}, " "))
           : arr.concat(v);
       }, [])
--- a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -17,25 +17,27 @@ const GripMessageBody = require("devtool
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
   loadedObjectProperties: PropTypes.object,
+  loadedObjectEntries: PropTypes.object,
 };
 
 function EvaluationResult(props) {
   const {
     dispatch,
     message,
     serviceContainer,
     timestampsVisible,
     loadedObjectProperties,
+    loadedObjectEntries,
   } = props;
 
   const {
     source,
     type,
     helperType,
     level,
     id: messageId,
@@ -61,16 +63,17 @@ function EvaluationResult(props) {
     messageBody = GripMessageBody({
       dispatch,
       messageId,
       grip: parameters,
       serviceContainer,
       useQuotes: true,
       escapeWhitespace: false,
       loadedObjectProperties,
+      loadedObjectEntries,
       type,
       helperType,
     });
   }
 
   const topLevelClasses = ["cm-s-mozilla"];
 
   return Message({
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -9,16 +9,17 @@ const actionTypes = {
   BATCH_ACTIONS: "BATCH_ACTIONS",
   MESSAGE_ADD: "MESSAGE_ADD",
   MESSAGES_CLEAR: "MESSAGES_CLEAR",
   MESSAGE_OPEN: "MESSAGE_OPEN",
   MESSAGE_CLOSE: "MESSAGE_CLOSE",
   NETWORK_MESSAGE_UPDATE: "NETWORK_MESSAGE_UPDATE",
   MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
   MESSAGE_OBJECT_PROPERTIES_RECEIVE: "MESSAGE_OBJECT_PROPERTIES_RECEIVE",
+  MESSAGE_OBJECT_ENTRIES_RECEIVE: "MESSAGE_OBJECT_ENTRIES_RECEIVE",
   REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
   FILTER_TOGGLE: "FILTER_TOGGLE",
   FILTER_TEXT_SET: "FILTER_TEXT_SET",
   FILTERS_CLEAR: "FILTERS_CLEAR",
   FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
 };
 
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -27,16 +27,21 @@ const MessageState = Immutable.Record({
   // Map of the form {messageId : tableData}, which represent the data passed
   // as an argument in console.table calls.
   messagesTableDataById: Immutable.Map(),
   // Map of the form {messageId : {[actor]: properties}}, where `properties` is
   // a RDP packet containing the properties of the ${actor} grip.
   // This map is consumed by the ObjectInspector so we only load properties once,
   // when needed (when an ObjectInspector node is expanded), and then caches them.
   messagesObjectPropertiesById: Immutable.Map(),
+  // Map of the form {messageId : {[actor]: entries}}, where `entries` is
+  // a RDP packet containing the entries of the ${actor} grip.
+  // This map is consumed by the ObjectInspector so we only load entries once,
+  // when needed (when an ObjectInspector node is expanded), and then caches them.
+  messagesObjectEntriesById: Immutable.Map(),
   // Map of the form {groupMessageId : groupArray},
   // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
   groupsById: Immutable.Map(),
   // Message id of the current group (no corresponding console.groupEnd yet).
   currentGroup: null,
   // Array of removed actors (i.e. actors logged in removed messages) we keep track of
   // in order to properly release them.
   // This array is not supposed to be consumed by any UI component.
@@ -49,16 +54,17 @@ const MessageState = Immutable.Record({
 });
 
 function messages(state = new MessageState(), action, filtersState, prefsState) {
   const {
     messagesById,
     messagesUiById,
     messagesTableDataById,
     messagesObjectPropertiesById,
+    messagesObjectEntriesById,
     networkMessagesUpdateById,
     groupsById,
     currentGroup,
     repeatById,
     visibleMessages,
   } = state;
 
   const {logLimit} = prefsState;
@@ -202,16 +208,26 @@ function messages(state = new MessageSta
         "messagesObjectPropertiesById",
         messagesObjectPropertiesById.set(
           action.id,
           Object.assign({
             [action.actor]: action.properties
           }, messagesObjectPropertiesById.get(action.id))
         )
       );
+    case constants.MESSAGE_OBJECT_ENTRIES_RECEIVE:
+      return state.set(
+        "messagesObjectEntriesById",
+        messagesObjectEntriesById.set(
+          action.id,
+          Object.assign({
+            [action.actor]: action.entries
+          }, messagesObjectEntriesById.get(action.id))
+        )
+      );
 
     case constants.NETWORK_MESSAGE_UPDATE:
       return state.set(
         "networkMessagesUpdateById",
         Object.assign({}, networkMessagesUpdateById, {
           [action.message.id]: action.message
         })
       );
@@ -351,16 +367,20 @@ function limitTopLevelMessageCount(state
   }
   if (mapHasRemovedIdKey(record.groupsById)) {
     record.set("groupsById", record.groupsById.withMutations(cleanUpCollection));
   }
   if (mapHasRemovedIdKey(record.messagesObjectPropertiesById)) {
     record.set("messagesObjectPropertiesById",
       record.messagesObjectPropertiesById.withMutations(cleanUpCollection));
   }
+  if (mapHasRemovedIdKey(record.messagesObjectEntriesById)) {
+    record.set("messagesObjectEntriesById",
+      record.messagesObjectEntriesById.withMutations(cleanUpCollection));
+  }
   if (objectHasRemovedIdKey(record.repeatById)) {
     record.set("repeatById", cleanUpObject(record.repeatById));
   }
 
   if (objectHasRemovedIdKey(record.networkMessagesUpdateById)) {
     record.set("networkMessagesUpdateById",
       cleanUpObject(record.networkMessagesUpdateById));
   }
@@ -390,16 +410,21 @@ function getAllActorsInMessage(message, 
     return res;
   }, [])];
 
   const loadedProperties = state.messagesObjectPropertiesById.get(message.id);
   if (loadedProperties) {
     actors.push(...Object.keys(loadedProperties));
   }
 
+  const loadedEntries = state.messagesObjectEntriesById.get(message.id);
+  if (loadedEntries) {
+    actors.push(...Object.keys(loadedEntries));
+  }
+
   return actors;
 }
 
 /**
  * Returns total count of top level messages (those which are not
  * within a group).
  */
 function getToplevelMessageCount(record) {
--- a/devtools/client/webconsole/new-console-output/selectors/messages.js
+++ b/devtools/client/webconsole/new-console-output/selectors/messages.js
@@ -20,16 +20,20 @@ function getAllMessagesUiById(state) {
 function getAllMessagesTableDataById(state) {
   return state.messages.messagesTableDataById;
 }
 
 function getAllMessagesObjectPropertiesById(state) {
   return state.messages.messagesObjectPropertiesById;
 }
 
+function getAllMessagesObjectEntriesById(state) {
+  return state.messages.messagesObjectEntriesById;
+}
+
 function getAllGroupsById(state) {
   return state.messages.groupsById;
 }
 
 function getCurrentGroup(state) {
   return state.messages.currentGroup;
 }
 
@@ -51,9 +55,10 @@ module.exports = {
   getAllMessagesUiById,
   getAllMessagesTableDataById,
   getAllGroupsById,
   getCurrentGroup,
   getVisibleMessages,
   getAllRepeatById,
   getAllNetworkMessagesUpdateById,
   getAllMessagesObjectPropertiesById,
+  getAllMessagesObjectEntriesById,
 };
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
@@ -22,25 +22,31 @@ const consoleApiCommands = [
   "console.log('myregex', /a.b.c/)",
   "console.table(['red', 'green', 'blue']);",
   "console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});",
 ];
 
 let consoleApi = new Map(consoleApiCommands.map(
   cmd => [cmd, {keys: [cmd], code: cmd}]));
 
-consoleApi.set("console.map('mymap')", {
-  keys: ["console.map('mymap')"],
+consoleApi.set("console.log('mymap')", {
+  keys: ["console.log('mymap')"],
   code: `
 var map = new Map();
 map.set("key1", "value1");
 map.set("key2", "value2");
 console.log('mymap', map);
 `});
 
+consoleApi.set("console.log('myset')", {
+  keys: ["console.log('myset')"],
+  code: `
+console.log('myset', new Set(["a", "b"]));
+`});
+
 consoleApi.set("console.trace()", {
   keys: ["console.trace()"],
   code: `
 function testStacktraceFiltering() {
   console.trace()
 }
 function foo() {
   testStacktraceFiltering()
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
@@ -553,30 +553,30 @@ stubPreparedMessages.set("console.log('m
   },
   "groupId": null,
   "exceptionDocURL": null,
   "userProvidedStyles": [],
   "notes": null,
   "indent": 0
 }));
 
-stubPreparedMessages.set("console.map('mymap')", new ConsoleMessage({
+stubPreparedMessages.set("console.log('mymap')", new ConsoleMessage({
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
-  "timeStamp": 1493125410207,
+  "timeStamp": 1501506737042,
   "type": "log",
   "helperType": null,
   "level": "log",
   "messageText": null,
   "parameters": [
     "mymap",
     {
       "type": "object",
-      "actor": "server1.conn0.child1/obj36",
+      "actor": "server1.conn0.child1/obj37",
       "class": "Map",
       "extensible": true,
       "frozen": false,
       "sealed": false,
       "ownPropertyLength": 0,
       "preview": {
         "kind": "MapLike",
         "size": 2,
@@ -588,30 +588,73 @@ stubPreparedMessages.set("console.map('m
           [
             "key2",
             "value2"
           ]
         ]
       }
     }
   ],
-  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"mymap\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj36\",\"class\":\"Map\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"kind\":\"MapLike\",\"size\":2,\"entries\":[[\"key1\",\"value1\"],[\"key2\",\"value2\"]]}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"mymap\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj37\",\"class\":\"Map\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"kind\":\"MapLike\",\"size\":2,\"entries\":[[\"key1\",\"value1\"],[\"key2\",\"value2\"]]}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
   "stacktrace": null,
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "line": 5,
     "column": 1
   },
   "groupId": null,
   "exceptionDocURL": null,
   "userProvidedStyles": [],
   "notes": null,
   "indent": 0
 }));
 
+stubPreparedMessages.set("console.log('myset')", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1501506737051,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": null,
+  "parameters": [
+    "myset",
+    {
+      "type": "object",
+      "actor": "server1.conn0.child1/obj38",
+      "class": "Set",
+      "extensible": true,
+      "frozen": false,
+      "sealed": false,
+      "ownPropertyLength": 0,
+      "preview": {
+        "kind": "ArrayLike",
+        "length": 2,
+        "items": [
+          "a",
+          "b"
+        ]
+      }
+    }
+  ],
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":1},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"myset\",{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj38\",\"class\":\"Set\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"kind\":\"ArrayLike\",\"length\":2,\"items\":[\"a\",\"b\"]}}],\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 2,
+    "column": 1
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
 stubPreparedMessages.set("console.trace()", new ConsoleMessage({
   "id": "1",
   "allowRepeating": true,
   "source": "console-api",
   "timeStamp": 1479159910198,
   "type": "trace",
   "helperType": null,
   "level": "log",
@@ -1684,26 +1727,26 @@ stubPackets.set("console.log('myobject',
     "styles": [],
     "timeStamp": 1493125748177,
     "timer": null,
     "workerType": "none",
     "category": "webdev"
   }
 });
 
-stubPackets.set("console.map('mymap')", {
+stubPackets.set("console.log('mymap')", {
   "from": "server1.conn0.child1/consoleActor2",
   "type": "consoleAPICall",
   "message": {
     "addonId": "",
     "arguments": [
       "mymap",
       {
         "type": "object",
-        "actor": "server1.conn0.child1/obj36",
+        "actor": "server1.conn0.child1/obj37",
         "class": "Map",
         "extensible": true,
         "frozen": false,
         "sealed": false,
         "ownPropertyLength": 0,
         "preview": {
           "kind": "MapLike",
           "size": 2,
@@ -1724,17 +1767,58 @@ stubPackets.set("console.map('mymap')", 
     "counter": null,
     "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
     "functionName": "triggerPacket",
     "groupName": "",
     "level": "log",
     "lineNumber": 5,
     "private": false,
     "styles": [],
-    "timeStamp": 1493125410207,
+    "timeStamp": 1501506737042,
+    "timer": null,
+    "workerType": "none",
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.log('myset')", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "myset",
+      {
+        "type": "object",
+        "actor": "server1.conn0.child1/obj38",
+        "class": "Set",
+        "extensible": true,
+        "frozen": false,
+        "sealed": false,
+        "ownPropertyLength": 0,
+        "preview": {
+          "kind": "ArrayLike",
+          "length": 2,
+          "items": [
+            "a",
+            "b"
+          ]
+        }
+      }
+    ],
+    "columnNumber": 1,
+    "counter": null,
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "log",
+    "lineNumber": 2,
+    "private": false,
+    "styles": [],
+    "timeStamp": 1501506737051,
     "timer": null,
     "workerType": "none",
     "category": "webdev"
   }
 });
 
 stubPackets.set("console.trace()", {
   "from": "server1.conn12.child1/consoleActor2",
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -38,15 +38,16 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_webconsole_input_focus.js]
 [browser_webconsole_keyboard_accessibility.js]
 [browser_webconsole_location_debugger_link.js]
 [browser_webconsole_location_scratchpad_link.js]
 [browser_webconsole_location_styleeditor_link.js]
 [browser_webconsole_network_messages_click.js]
 [browser_webconsole_nodes_highlight.js]
 [browser_webconsole_nodes_select.js]
+[browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_shows_reqs_in_netmonitor.js]
 [browser_webconsole_stacktrace_location_debugger_link.js]
 [browser_webconsole_stacktrace_location_scratchpad_link.js]
 [browser_webconsole_string.js]
 [browser_webconsole_timestamps.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_object_inspector_entries.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+// Check expanding/collapsing object inspector in the console.
+const TEST_URI = "data:text/html;charset=utf8,<h1>test Object Inspector</h1>";
+
+add_task(async function () {
+  let toolbox = await openNewTabAndToolbox(TEST_URI, "webconsole");
+  let hud = toolbox.getCurrentPanel().hud;
+
+  const store = hud.ui.newConsoleOutput.getStore();
+  // Adding logging each time the store is modified in order to check
+  // the store state in case of failure.
+  store.subscribe(() => {
+    const messages = store.getState().messages.messagesById
+      .reduce(function (res, {id, type, parameters, messageText}) {
+        res.push({id, type, parameters, messageText});
+        return res;
+      }, []);
+    info("messages : " + JSON.stringify(messages));
+  });
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, null, function () {
+    content.wrappedJSObject.console.log(
+      "oi-entries-test",
+      new Map([["a", 42], ["b", 10]]),
+      new Map(
+        Array.from({length: 20})
+          .map((el, i) => [String.fromCharCode(65 + i),  i + 1])
+      ),
+    );
+  });
+
+  let node = await waitFor(() => findMessage(hud, "oi-entries-test"));
+  const objectInspectors = [...node.querySelectorAll(".tree")];
+  is(objectInspectors.length, 2, "There is the expected number of object inspectors");
+
+  const [smallMapOi, largeMapOi] = objectInspectors;
+
+  info("Expanding the small map");
+  let onMapOiMutation = waitForNodeMutation(smallMapOi, {
+    childList: true
+  });
+
+  smallMapOi.querySelector(".arrow").click();
+  await onMapOiMutation;
+
+  ok(smallMapOi.querySelector(".arrow").classList.contains("expanded"),
+    "The arrow of the node has the expected class after clicking on it");
+
+  let smallMapOiNodes = smallMapOi.querySelectorAll(".node");
+  // There are 4 nodes: the root, size, entries and the proto.
+  is(smallMapOiNodes.length, 4, "There is the expected number of nodes in the tree");
+
+  info("Expanding the <entries> leaf of the map");
+  let entriesObject = smallMapOiNodes[2];
+  is(entriesObject.textContent, "<entries>", "There is the expected <entries> node");
+  onMapOiMutation = waitForNodeMutation(smallMapOi, {
+    childList: true
+  });
+
+  entriesObject.querySelector(".arrow").click();
+  await onMapOiMutation;
+
+  ok(entriesObject.querySelector(".arrow").classList.contains("expanded"),
+    "The arrow of the node has the expected class after clicking on it");
+
+  smallMapOiNodes = smallMapOi.querySelectorAll(".node");
+  // There are now 6 nodes, the 4 original ones, and the 2 entries.
+  is(smallMapOiNodes.length, 6, "There is the expected number of nodes in the tree");
+
+  info("Expanding the large map");
+  onMapOiMutation = waitForNodeMutation(largeMapOi, {
+    childList: true
+  });
+
+  largeMapOi.querySelector(".arrow").click();
+  await onMapOiMutation;
+
+  ok(largeMapOi.querySelector(".arrow").classList.contains("expanded"),
+    "The arrow of the node has the expected class after clicking on it");
+
+  let largeMapOiNodes = largeMapOi.querySelectorAll(".node");
+  // There are 4 nodes: the root, size, entries and the proto.
+  is(largeMapOiNodes.length, 4, "There is the expected number of nodes in the tree");
+
+  info("Expanding the <entries> leaf of the map");
+  entriesObject = largeMapOiNodes[2];
+  is(entriesObject.textContent, "<entries>", "There is the expected <entries> node");
+  onMapOiMutation = waitForNodeMutation(largeMapOi, {
+    childList: true
+  });
+
+  entriesObject.querySelector(".arrow").click();
+  await onMapOiMutation;
+
+  ok(entriesObject.querySelector(".arrow").classList.contains("expanded"),
+    "The arrow of the node has the expected class after clicking on it");
+
+  largeMapOiNodes = largeMapOi.querySelectorAll(".node");
+  // There are now 24 nodes, the 4 original ones, and the 20 entries.
+  is(largeMapOiNodes.length, 24, "There is the expected number of nodes in the tree");
+});
--- a/devtools/client/webconsole/new-console-output/test/store/messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
@@ -7,16 +7,17 @@ const {
   getAllMessagesById,
   getAllMessagesTableDataById,
   getAllMessagesUiById,
   getAllNetworkMessagesUpdateById,
   getAllRepeatById,
   getCurrentGroup,
   getVisibleMessages,
   getAllMessagesObjectPropertiesById,
+  getAllMessagesObjectEntriesById,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const {
   clonePacket,
   getMessageAt,
   setupActions,
   setupStore,
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
 const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
@@ -864,9 +865,105 @@ describe("Message reducer:", () => {
       });
 
       // This addition will remove the second table message.
       dispatch(actions.messageAdd(stubPackets.get("console.log('foobar', 'test')")));
 
       expect(getAllMessagesObjectPropertiesById(getState()).size).toBe(0);
     });
   });
+
+  describe("messagesObjectEntriesById", () => {
+    it(`adds messagesObjectEntriesById data in response to
+        MESSAGE_OBJECT_ENTRIES_RECEIVE action`, () => {
+      const { dispatch, getState } = setupStore([]);
+
+      // Add 2 log messages with loaded entries.
+      dispatch(actions.messageAdd(stubPackets.get("console.log('myset')")));
+      dispatch(actions.messageAdd(stubPackets.get("console.log('mymap')")));
+
+      let messages = getAllMessagesById(getState());
+
+      const setEntries = Symbol();
+      const mapEntries = Symbol();
+      const mapEntries2 = Symbol();
+
+      const [id1, id2] = [...messages.keys()];
+      dispatch(actions.messageObjectEntriesReceive(id1, "fakeActor1", setEntries));
+      dispatch(actions.messageObjectEntriesReceive(id2, "fakeActor2", mapEntries));
+      dispatch(actions.messageObjectEntriesReceive(id2, "fakeActor3", mapEntries2));
+
+      let loadedEntries = getAllMessagesObjectEntriesById(getState());
+      expect(loadedEntries.size).toBe(2);
+
+      expect(loadedEntries.get(id1)).toEqual({
+        fakeActor1: setEntries,
+      });
+
+      expect(loadedEntries.get(id2)).toEqual({
+        fakeActor2: mapEntries,
+        fakeActor3: mapEntries2,
+      });
+    });
+
+    it("resets messagesObjectEntriesById in response to MESSAGES_CLEAR action", () => {
+      const { dispatch, getState } = setupStore([
+        "console.log('myset')"
+      ]);
+
+      let messages = getAllMessagesById(getState());
+      const entries = Symbol("entries");
+      const message = messages.first();
+      const {actor} = message.parameters[1];
+
+      dispatch(actions.messageObjectEntriesReceive(message.id, actor, entries));
+
+      let loadedEntries = getAllMessagesObjectEntriesById(getState());
+      expect(loadedEntries.size).toBe(1);
+      expect(loadedEntries.get(message.id)).toEqual({
+        [actor]: entries
+      });
+
+      dispatch(actions.messagesClear());
+
+      expect(getAllMessagesObjectEntriesById(getState()).size).toBe(0);
+    });
+
+    it("cleans messagesObjectPropertiesById when messages are pruned", () => {
+      const { dispatch, getState } = setupStore([], null, {
+        logLimit: 2
+      });
+
+      // Add 2 log messages with loaded entries.
+      dispatch(actions.messageAdd(stubPackets.get("console.log('myset')")));
+      dispatch(actions.messageAdd(stubPackets.get("console.log('mymap')")));
+
+      let messages = getAllMessagesById(getState());
+
+      const setEntries = Symbol();
+      const mapEntries = Symbol();
+      const mapEntries2 = Symbol();
+
+      const [id1, id2] = [...messages.keys()];
+      dispatch(actions.messageObjectEntriesReceive(id1, "fakeActor1", setEntries));
+      dispatch(actions.messageObjectEntriesReceive(id2, "fakeActor2", mapEntries));
+      dispatch(actions.messageObjectEntriesReceive(id2, "fakeActor3", mapEntries2));
+
+      let loadedEntries = getAllMessagesObjectEntriesById(getState());
+      expect(loadedEntries.size).toBe(2);
+
+      // This addition will remove the first message.
+      dispatch(actions.messageAdd(stubPackets.get("console.log(undefined)")));
+
+      loadedEntries = getAllMessagesObjectEntriesById(getState());
+      expect(loadedEntries.size).toBe(1);
+      expect(loadedEntries.get(id2)).toEqual({
+        fakeActor2: mapEntries,
+        fakeActor3: mapEntries2,
+      });
+
+      // This addition will remove the second table message.
+      dispatch(actions.messageAdd(stubPackets.get("console.log('foobar', 'test')")));
+
+      expect(getAllMessagesObjectEntriesById(getState()).size).toBe(0);
+    });
+  });
 });
--- a/devtools/client/webconsole/new-console-output/test/store/release-actors.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/release-actors.test.js
@@ -80,29 +80,37 @@ describe("Release actor enhancer:", () =
       dispatch(actions.messageAdd(
         stubPackets.get("console.log('myarray', ['red', 'green', 'blue'])")));
 
       let messages = getAllMessagesById(getState());
       const firstMessage = messages.first();
       const firstMessageActor = firstMessage.parameters[1].actor;
       const arrayProperties = Symbol();
       const arraySubProperties = Symbol();
+      const mapEntries1 = Symbol();
+      const mapEntries2 = Symbol();
       const [id] = [...messages.keys()];
       dispatch(actions.messageObjectPropertiesReceive(
         id, "fakeActor1", arrayProperties));
       dispatch(actions.messageObjectPropertiesReceive(
         id, "fakeActor2", arraySubProperties));
+      dispatch(actions.messageObjectEntriesReceive(
+        id, "mapActor1", mapEntries1));
+      dispatch(actions.messageObjectEntriesReceive(
+        id, "mapActor2", mapEntries2));
 
       const packet = clonePacket(stubPackets.get(
         "console.assert(false, {message: 'foobar'})"));
       const secondMessageActor = packet.message.arguments[0].actor;
       dispatch(actions.messageAdd(packet));
 
       dispatch(actions.messagesClear());
 
-      expect(releasedActors.length).toBe(4);
+      expect(releasedActors.length).toBe(6);
       expect(releasedActors).toInclude(firstMessageActor);
       expect(releasedActors).toInclude("fakeActor1");
       expect(releasedActors).toInclude("fakeActor2");
+      expect(releasedActors).toInclude("mapActor1");
+      expect(releasedActors).toInclude("mapActor2");
       expect(releasedActors).toInclude(secondMessageActor);
     });
   });
 });
--- a/devtools/client/webconsole/new-console-output/test/store/search.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/search.test.js
@@ -83,15 +83,15 @@ describe("Searching in grips", () => {
 function prepareBaseStore() {
   const store = setupStore([
     "console.log('foobar', 'test')",
     "console.warn('danger, will robinson!')",
     "console.table(['red', 'green', 'blue']);",
     "console.count('bar')",
     "console.log('myarray', ['red', 'green', 'blue'])",
     "console.log('myregex', /a.b.c/)",
-    "console.map('mymap')",
+    "console.log('mymap')",
     "console.log('myobject', {red: 'redValue', green: 'greenValue', blue: 'blueValue'});",
     "GET request",
   ]);
 
   return store;
 }