Bug 1419086 - Render the ObjectInspector in the console sidebar. r=nchevobbe draft
authorMike Park <mikeparkms@gmail.com>
Thu, 14 Dec 2017 14:01:02 -0600
changeset 712776 390394ae47a3ac39c18357a0146a659cde657ce0
parent 712645 5572465c08a9ce0671dcd01c721f9356fcd53a65
child 713112 7058c57946aaee6221ff66dc672f5b05efe371dd
child 713133 5018a2e7dba9b9fb2052a4d3ae7e49c494c31317
child 713204 612c085b0c4670e53eadf9044dedb2dcc62415f1
child 713207 6ef5fa411fbe512cbe6c7b055ba3efde938559af
push id93429
push userbmo:mpark@mozilla.com
push dateMon, 18 Dec 2017 19:15:41 +0000
reviewersnchevobbe
bugs1419086
milestone59.0a1
Bug 1419086 - Render the ObjectInspector in the console sidebar. r=nchevobbe MozReview-Commit-ID: RpJf4N5X02
devtools/client/themes/webconsole.css
devtools/client/webconsole/new-console-output/components/GripMessageBody.js
devtools/client/webconsole/new-console-output/components/SideBar.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_object_in_sidebar.js
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_object_in_sidebar.js
devtools/client/webconsole/new-console-output/test/store/ui.test.js
devtools/client/webconsole/new-console-output/utils/moz.build
devtools/client/webconsole/new-console-output/utils/object-inspector.js
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -1235,9 +1235,13 @@ body #output-container {
 .webconsole-sidebar-toolbar .sidebar-close-button {
   padding: 4px 0;
   margin: 0;
   margin-inline-end: -3px;
 }
 
 .sidebar-close-button::before {
   background-image: var(--close-button-image);
+}
+
+.sidebar-contents .object-inspector {
+  min-width: 100%;
 }
\ No newline at end of file
--- a/devtools/client/webconsole/new-console-output/components/GripMessageBody.js
+++ b/devtools/client/webconsole/new-console-output/components/GripMessageBody.js
@@ -8,28 +8,25 @@
 
 // If this is being run from Mocha, then the browser loader hasn't set up
 // define. We need to do that before loading Rep.
 if (typeof define === "undefined") {
   require("amd-loader");
 }
 
 // React
-const { createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-const ObjectClient = require("devtools/shared/client/object-client");
 const {
   MESSAGE_TYPE,
   JSTERM_COMMANDS,
 } = require("../constants");
+const { getObjectInspector } = require("devtools/client/webconsole/new-console-output/utils/object-inspector");
 
 const reps = require("devtools/client/shared/components/reps/reps");
-const { REPS, MODE } = reps;
-const ObjectInspector = createFactory(reps.ObjectInspector);
-const { Grip } = REPS;
+const { MODE } = reps;
 
 GripMessageBody.displayName = "GripMessageBody";
 
 GripMessageBody.propTypes = {
   grip: PropTypes.oneOfType([
     PropTypes.string,
     PropTypes.number,
     PropTypes.object,
@@ -60,74 +57,30 @@ function GripMessageBody(props) {
     mode = MODE.LONG,
   } = props;
 
   let styleObject;
   if (userProvidedStyle && userProvidedStyle !== "") {
     styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
   }
 
-  let onDOMNodeMouseOver;
-  let onDOMNodeMouseOut;
-  let onInspectIconClick;
-  if (serviceContainer) {
-    onDOMNodeMouseOver = serviceContainer.highlightDomElement
-      ? (object) => serviceContainer.highlightDomElement(object)
-      : null;
-    onDOMNodeMouseOut = serviceContainer.unHighlightDomElement;
-    onInspectIconClick = serviceContainer.openNodeInInspector
-      ? (object, e) => {
-        // Stop the event propagation so we don't trigger ObjectInspector expand/collapse.
-        e.stopPropagation();
-        serviceContainer.openNodeInInspector(object);
-      }
-      : null;
-  }
-
-  const objectInspectorProps = {
-    // Auto-expand the ObjectInspector when the message is a console.dir one.
+  let objectInspectorProps = {
     autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
     mode,
-    // TODO: we disable focus since it's not currently working well in ObjectInspector.
-    // Let's remove the property below when problem are fixed in OI.
-    disabledFocus: true,
-    roots: [{
-      path: (grip && grip.actor) || JSON.stringify(grip),
-      contents: {
-        value: grip
-      }
-    }],
-    createObjectClient: object =>
-      new ObjectClient(serviceContainer.hudProxy.client, object),
-    releaseActor: actor => {
-      if (!actor || !serviceContainer.hudProxy.releaseActor) {
-        return;
-      }
-      serviceContainer.hudProxy.releaseActor(actor);
-    },
-    onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger,
-    openLink: serviceContainer.openLink,
   };
 
   if (typeof grip === "string" || (grip && grip.type === "longString")) {
     Object.assign(objectInspectorProps, {
       useQuotes,
       escapeWhitespace,
       style: styleObject
     });
-  } else {
-    Object.assign(objectInspectorProps, {
-      onDOMNodeMouseOver,
-      onDOMNodeMouseOut,
-      onInspectIconClick,
-      defaultRep: Grip,
-    });
   }
 
-  return ObjectInspector(objectInspectorProps);
+  return getObjectInspector(grip, serviceContainer, objectInspectorProps);
 }
 
 // Regular expression that matches the allowed CSS property names.
 const allowedStylesRegex = new RegExp(
   "^(?:-moz-)?(?:background|border|box|clear|color|cursor|display|float|font|line|" +
   "margin|padding|text|transition|outline|white-space|word|writing|" +
   "(?:min-|max-)?width|(?:min-|max-)?height)"
 );
--- a/devtools/client/webconsole/new-console-output/components/SideBar.js
+++ b/devtools/client/webconsole/new-console-output/components/SideBar.js
@@ -2,22 +2,27 @@
  * 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";
 
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { getObjectInspector } = require("devtools/client/webconsole/new-console-output/utils/object-inspector");
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 
+const reps = require("devtools/client/shared/components/reps/reps");
+const { MODE } = reps;
+
 class SideBar extends Component {
   static get propTypes() {
     return {
+      serviceContainer: PropTypes.object,
       dispatch: PropTypes.func.isRequired,
       sidebarVisible: PropTypes.bool,
       grip: PropTypes.object,
     };
   }
 
   constructor(props) {
     super(props);
@@ -27,32 +32,38 @@ class SideBar extends Component {
   onClickSidebarClose() {
     this.props.dispatch(actions.sidebarClose());
   }
 
   render() {
     let {
       sidebarVisible,
       grip,
+      serviceContainer,
     } = this.props;
 
+    let objectInspector = getObjectInspector(grip, serviceContainer, {
+      autoExpandDepth: 1,
+      mode: MODE.SHORT,
+    });
+
     let endPanel = dom.aside({
       className: "sidebar-wrapper"
     },
       dom.header({
         className: "devtools-toolbar webconsole-sidebar-toolbar"
       },
         dom.button({
           className: "devtools-button sidebar-close-button",
           onClick: this.onClickSidebarClose
         })
       ),
       dom.aside({
         className: "sidebar-contents"
-      }, JSON.stringify(grip, null, 2))
+      }, objectInspector)
     );
 
     return (
       sidebarVisible ?
         SplitBox({
           className: "sidebar",
           endPanel,
           endPanelControl: true,
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -344,16 +344,17 @@ skip-if = true #	Bug 1405636
 [browser_webconsole_network_exceptions.js]
 [browser_webconsole_network_messages_expand.js]
 [browser_webconsole_network_messages_openinnet.js]
 [browser_webconsole_network_requests_from_chrome.js]
 [browser_webconsole_nodes_highlight.js]
 [browser_webconsole_nodes_select.js]
 [browser_webconsole_notifications.js]
 skip-if = true #	Bug 1405637
+[browser_webconsole_object_in_sidebar.js]
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_optimized_out_vars.js]
 [browser_webconsole_output_copy.js]
 subsuite = clipboard
 skip-if = true #	Bug 1404364
 # old console skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_object_in_sidebar.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_context_menu_object_in_sidebar.js
@@ -25,32 +25,54 @@ add_task(async function () {
   let string = findMessage(hud, "foo", ".objectBox");
   let bool = findMessage(hud, "false", ".objectBox");
   let nullMessage = findMessage(hud, "null", ".objectBox");
   let undefinedMsg = findMessage(hud, "undefined", ".objectBox");
 
   info("Showing sidebar for {a:1}");
   await showSidebarWithContextMenu(hud, objectA, true);
 
+  let sidebarContents = hud.ui.document.querySelector(".sidebar-contents");
+  let objectInspector = sidebarContents.querySelector(".object-inspector");
+  let oiNodes = objectInspector.querySelectorAll(".node");
+  if (oiNodes.length === 1) {
+    // If this is the case, we wait for the properties to be fetched and displayed.
+    await waitForNodeMutation(objectInspector, {
+      childList: true
+    });
+  }
+
   let sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent;
-  ok(sidebarText.includes('"a":'), "Sidebar is shown for {a:1}");
+  ok(sidebarText.includes("a: 1"), "Sidebar is shown for {a:1}");
 
   info("Showing sidebar for {a:1} again");
   await showSidebarWithContextMenu(hud, objectA, false);
   ok(hud.ui.document.querySelector(".sidebar"),
      "Sidebar is still shown after clicking on same object");
   is(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
      "Sidebar is not updated after clicking on same object");
 
   info("Showing sidebar for {b:1}");
   await showSidebarWithContextMenu(hud, objectB, false);
+
+  sidebarContents = hud.ui.document.querySelector(".sidebar-contents");
+  objectInspector = sidebarContents.querySelector(".object-inspector");
+  oiNodes = objectInspector.querySelectorAll(".node");
+  if (oiNodes.length === 1) {
+    // If this is the case, we wait for the properties to be fetched and displayed.
+    await waitForNodeMutation(objectInspector, {
+      childList: true
+    });
+  }
+
   isnot(hud.ui.document.querySelector(".sidebar-contents").textContent, sidebarText,
         "Sidebar is updated for {b:1}");
   sidebarText = hud.ui.document.querySelector(".sidebar-contents").textContent;
-  ok(sidebarText.includes('"b":'), "Sidebar contents shown for {b:1}");
+
+  ok(sidebarText.includes("b: 1"), "Sidebar contents shown for {b:1}");
 
   info("Checking context menu entry is disabled for number");
   let numberContextMenuEnabled = await isContextMenuEntryEnabled(hud, number);
   ok(!numberContextMenuEnabled, "Context menu entry is disabled for number");
 
   info("Checking context menu entry is disabled for string");
   let stringContextMenuEnabled = await isContextMenuEntryEnabled(hud, string);
   ok(!stringContextMenuEnabled, "Context menu entry is disabled for string");
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_object_in_sidebar.js
@@ -0,0 +1,57 @@
+/* -*- 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/ */
+
+// Test that the ObjectInspector is rendered correctly in the sidebar.
+
+"use strict";
+
+const TEST_URI =
+  "data:text/html;charset=utf8," +
+  "<script>console.log({a:1,b:2,c:3});</script>";
+
+add_task(async function () {
+  // Should be removed when sidebar work is complete
+  await pushPref("devtools.webconsole.sidebarToggle", true);
+
+  let hud = await openNewTabAndConsole(TEST_URI);
+
+  let message = findMessage(hud, "Object");
+  let object = message.querySelector(".object-inspector .objectBox-object");
+
+  await showSidebarWithContextMenu(hud, object, true);
+
+  let sidebarContents = hud.ui.document.querySelector(".sidebar-contents");
+  let objectInspectors = [...sidebarContents.querySelectorAll(".tree")];
+  is(objectInspectors.length, 1, "There is the expected number of object inspectors");
+  let [objectInspector] = objectInspectors;
+  let oiNodes = objectInspector.querySelectorAll(".node");
+  if (oiNodes.length === 1) {
+    // If this is the case, we wait for the properties to be fetched and displayed.
+    await waitForNodeMutation(objectInspector, {
+      childList: true
+    });
+    oiNodes = objectInspector.querySelectorAll(".node");
+  }
+
+  // There are 5 nodes: the root, a, b, c, and proto.
+  is(oiNodes.length, 5, "There is the expected number of nodes in the tree");
+  let propertiesNodes = [...objectInspector.querySelectorAll(".object-label")]
+    .map(el => el.textContent);
+  const arrayPropertiesNames = ["a", "b", "c", "__proto__"];
+  is(JSON.stringify(propertiesNodes), JSON.stringify(arrayPropertiesNames));
+});
+
+async function showSidebarWithContextMenu(hud, node, expectMutation) {
+  let wrapper = hud.ui.document.querySelector(".webconsole-output-wrapper");
+  let onSidebarShown = waitForNodeMutation(wrapper, { childList: true });
+
+  let contextMenu = await openContextMenu(hud, node);
+  let openInSidebar = contextMenu.querySelector("#console-menu-open-sidebar");
+  openInSidebar.click();
+  if (expectMutation) {
+    await onSidebarShown;
+  }
+  await hideContextMenu(hud);
+}
--- a/devtools/client/webconsole/new-console-output/test/store/ui.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/ui.test.js
@@ -1,18 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const expect = require("expect");
 
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
-const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
-const { getAllMessagesById } = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { setupStore, getFirstMessage, getLastMessage } = require("devtools/client/webconsole/new-console-output/test/helpers");
 const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
 describe("Testing UI", () => {
   let store;
 
   beforeEach(() => {
     store = setupStore();
   });
@@ -38,62 +37,58 @@ describe("Testing UI", () => {
   });
 
   describe("Show object in sidebar", () => {
     it("sidebar is shown with correct object", () => {
       const packet = stubPackets.get("inspect({a: 1})");
       const message = stubPreparedMessages.get("inspect({a: 1})");
       store.dispatch(actions.messageAdd(packet));
 
-      const messages = getAllMessagesById(store.getState());
       const actorId = message.parameters[0].actor;
-      const messageId = messages.first().id;
+      const messageId = getFirstMessage(store.getState()).id;
       store.dispatch(actions.showObjectInSidebar(actorId, messageId));
 
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
     });
 
     it("sidebar is not updated for the same object", () => {
       const packet = stubPackets.get("inspect({a: 1})");
       const message = stubPreparedMessages.get("inspect({a: 1})");
       store.dispatch(actions.messageAdd(packet));
 
-      const messages = getAllMessagesById(store.getState());
       const actorId = message.parameters[0].actor;
-      const messageId = messages.first().id;
+      const messageId = getFirstMessage(store.getState()).id;
       store.dispatch(actions.showObjectInSidebar(actorId, messageId));
 
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
       let state = store.getState().ui;
 
       store.dispatch(actions.showObjectInSidebar(actorId, messageId));
       expect(store.getState().ui).toEqual(state);
     });
 
     it("sidebar shown and updated for new object", () => {
       const packet = stubPackets.get("inspect({a: 1})");
       const message = stubPreparedMessages.get("inspect({a: 1})");
       store.dispatch(actions.messageAdd(packet));
 
-      const messages = getAllMessagesById(store.getState());
       const actorId = message.parameters[0].actor;
-      const messageId = messages.first().id;
+      const messageId = getFirstMessage(store.getState()).id;
       store.dispatch(actions.showObjectInSidebar(actorId, messageId));
 
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       expect(store.getState().ui.gripInSidebar).toEqual(message.parameters[0]);
 
       const newPacket = stubPackets.get("new Date(0)");
       const newMessage = stubPreparedMessages.get("new Date(0)");
       store.dispatch(actions.messageAdd(newPacket));
 
-      const newMessages = getAllMessagesById(store.getState());
       const newActorId = newMessage.parameters[0].actor;
-      const newMessageId = newMessages.last().id;
+      const newMessageId = getLastMessage(store.getState()).id;
       store.dispatch(actions.showObjectInSidebar(newActorId, newMessageId));
 
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       expect(store.getState().ui.gripInSidebar).toEqual(newMessage.parameters[0]);
     });
   });
 });
--- a/devtools/client/webconsole/new-console-output/utils/moz.build
+++ b/devtools/client/webconsole/new-console-output/utils/moz.build
@@ -2,9 +2,10 @@
 # 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/.
 
 DevToolsModules(
     'context-menu.js',
     'id-generator.js',
     'messages.js',
+    'object-inspector.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/utils/object-inspector.js
@@ -0,0 +1,82 @@
+/* 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";
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const ObjectClient = require("devtools/shared/client/object-client");
+
+const reps = require("devtools/client/shared/components/reps/reps");
+const { REPS, MODE } = reps;
+const ObjectInspector = createFactory(reps.ObjectInspector);
+const { Grip } = REPS;
+
+/**
+ * Create and return an ObjectInspector for the given grip.
+ *
+ * @param {Object} grip
+ *        The object grip to create an ObjectInspector for.
+ * @param {Object} serviceContainer
+ *        Object containing various utility functions
+ * @param {Object} override
+ *        Object containing props that should override the default props passed to
+ *        ObjectInspector.
+ * @returns {ObjectInspector}
+ *        An ObjectInspector for the given grip.
+ */
+function getObjectInspector(grip, serviceContainer, override) {
+  let onDOMNodeMouseOver;
+  let onDOMNodeMouseOut;
+  let onInspectIconClick;
+  if (serviceContainer) {
+    onDOMNodeMouseOver = serviceContainer.highlightDomElement
+      ? (object) => serviceContainer.highlightDomElement(object)
+      : null;
+    onDOMNodeMouseOut = serviceContainer.unHighlightDomElement;
+    onInspectIconClick = serviceContainer.openNodeInInspector
+      ? (object, e) => {
+        // Stop the event propagation so we don't trigger ObjectInspector expand/collapse.
+        e.stopPropagation();
+        serviceContainer.openNodeInInspector(object);
+      }
+      : null;
+  }
+
+  const objectInspectorProps = {
+    autoExpandDepth: 0,
+    mode: MODE.LONG,
+    // TODO: we disable focus since it's not currently working well in ObjectInspector.
+    // Let's remove the property below when problem are fixed in OI.
+    disabledFocus: true,
+    roots: [{
+      path: (grip && grip.actor) || JSON.stringify(grip),
+      contents: {
+        value: grip
+      }
+    }],
+    createObjectClient: object =>
+      new ObjectClient(serviceContainer.hudProxy.client, object),
+    releaseActor: actor => {
+      if (!actor || !serviceContainer.hudProxy.releaseActor) {
+        return;
+      }
+      serviceContainer.hudProxy.releaseActor(actor);
+    },
+    onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger,
+    openLink: serviceContainer.openLink,
+  };
+
+  if (!(typeof grip === "string" || (grip && grip.type === "longString"))) {
+    Object.assign(objectInspectorProps, {
+      onDOMNodeMouseOver,
+      onDOMNodeMouseOut,
+      onInspectIconClick,
+      defaultRep: Grip,
+    });
+  }
+
+  return ObjectInspector({...objectInspectorProps, ...override});
+}
+
+exports.getObjectInspector = getObjectInspector;