Bug 1403130 - Support ObjectInspector-based object value grip view in ExtensionSidebar draft
authorLuca Greco <lgreco@mozilla.com>
Thu, 12 Oct 2017 16:20:39 +0200
changeset 748787 83e975ea1db1f255ec6df8e6a807f0d9fded67b0
parent 748479 0dcc8b1903b1ba060a8e16b1bf73432e575d80b2
child 748788 da58e1c82d7fa5203b9a70f5d6052c9c67fadacf
push id97231
push userluca.greco@alcacoop.it
push dateTue, 30 Jan 2018 11:06:14 +0000
bugs1403130
milestone59.0a1
Bug 1403130 - Support ObjectInspector-based object value grip view in ExtensionSidebar MozReview-Commit-ID: DxU886yOCPu
devtools/client/inspector/extensions/actions/index.js
devtools/client/inspector/extensions/actions/sidebar.js
devtools/client/inspector/extensions/components/ExtensionSidebar.js
devtools/client/inspector/extensions/components/ObjectValueGripView.js
devtools/client/inspector/extensions/components/moz.build
devtools/client/inspector/extensions/extension-sidebar.js
devtools/client/inspector/extensions/moz.build
devtools/client/inspector/extensions/reducers/sidebar.js
devtools/client/inspector/extensions/test/browser.ini
devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
devtools/client/inspector/extensions/test/head.js
devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
devtools/client/inspector/extensions/types.js
--- a/devtools/client/inspector/extensions/actions/index.js
+++ b/devtools/client/inspector/extensions/actions/index.js
@@ -6,12 +6,15 @@
 
 const { createEnum } = require("devtools/client/shared/enum");
 
 createEnum([
 
   // Update the extension sidebar with an object TreeView.
   "EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE",
 
+  // Update the extension sidebar with an object value grip preview.
+  "EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE",
+
   // Remove an extension sidebar from the inspector store.
   "EXTENSION_SIDEBAR_REMOVE"
 
 ], module.exports);
--- a/devtools/client/inspector/extensions/actions/sidebar.js
+++ b/devtools/client/inspector/extensions/actions/sidebar.js
@@ -1,16 +1,17 @@
 /* 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 {
   EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+  EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
   EXTENSION_SIDEBAR_REMOVE,
 } = require("./index");
 
 module.exports = {
 
   /**
    * Update the sidebar with an object treeview.
    */
@@ -18,16 +19,28 @@ module.exports = {
     return {
       type: EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
       sidebarId,
       object,
     };
   },
 
   /**
+   * Update the sidebar with an object actor preview.
+   */
+  updateObjectValueGripView(sidebarId, objectValueGrip, rootTitle) {
+    return {
+      type: EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
+      sidebarId,
+      objectValueGrip,
+      rootTitle,
+    };
+  },
+
+  /**
    * Remove the extension sidebar from the inspector store.
    */
   removeExtensionSidebar(sidebarId) {
     return {
       type: EXTENSION_SIDEBAR_REMOVE,
       sidebarId,
     };
   }
--- a/devtools/client/inspector/extensions/components/ExtensionSidebar.js
+++ b/devtools/client/inspector/extensions/components/ExtensionSidebar.js
@@ -5,48 +5,67 @@
 "use strict";
 
 const { createFactory, PureComponent } = 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 ObjectTreeView = createFactory(require("./ObjectTreeView"));
+const ObjectValueGripView = createFactory(require("./ObjectValueGripView"));
+const Types = require("../types");
 
 /**
  * The ExtensionSidebar is a React component with 2 supported viewMode:
- * - an ObjectTreeView UI, used to show the JS objects (used by the sidebar.setObject
- *   and sidebar.setExpression WebExtensions APIs)
- * - an ExtensionPage UI used to show an extension page (used by the sidebar.setPage
- *   WebExtensions APIs).
+ * - an ObjectTreeView UI, used to show the JS objects
+ *   (used by the sidebar.setObject WebExtensions APIs)
+ * - an ObjectValueGripView UI, used to show the objects value grips
+ *   (used by sidebar.setExpression WebExtensions APIs)
+ * - an ExtensionPage UI used to show an extension page
+ *   (used by the sidebar.setPage WebExtensions APIs).
  *
  * TODO: implement the ExtensionPage viewMode.
  */
 class ExtensionSidebar extends PureComponent {
   static get propTypes() {
     return {
       id: PropTypes.string.isRequired,
       extensionsSidebar: PropTypes.object.isRequired,
+      // Helpers injected as props by extension-sidebar.js.
+      serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired,
     };
   }
 
   render() {
-    const { id, extensionsSidebar } = this.props;
+    const {
+      id,
+      extensionsSidebar,
+      serviceContainer,
+    } = this.props;
 
     let {
       viewMode = "empty-sidebar",
-      object
+      object,
+      objectValueGrip,
+      rootTitle
     } = extensionsSidebar[id] || {};
 
     let sidebarContentEl;
 
     switch (viewMode) {
       case "object-treeview":
         sidebarContentEl = ObjectTreeView({ object });
         break;
+      case "object-value-grip-view":
+        sidebarContentEl = ObjectValueGripView({
+          objectValueGrip,
+          serviceContainer,
+          rootTitle,
+        });
+        break;
       case "empty-sidebar":
         break;
       default:
         throw new Error(`Unknown ExtensionSidebar viewMode: "${viewMode}"`);
     }
 
     const className = "devtools-monospace extension-sidebar inspector-tabpanel";
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ObjectValueGripView.js
@@ -0,0 +1,88 @@
+/* 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, PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const Accordion = createFactory(require("devtools/client/inspector/layout/components/Accordion"));
+const reps = require("devtools/client/shared/components/reps/reps");
+const Types = require("../types");
+
+const { REPS, MODE } = reps;
+const { Grip } = REPS;
+
+const ObjectInspector = createFactory(reps.ObjectInspector);
+
+class ObjectValueGripView extends PureComponent {
+  static get propTypes() {
+    return {
+      rootTitle: PropTypes.string,
+      objectValueGrip: PropTypes.oneOfType([
+        PropTypes.string,
+        PropTypes.number,
+        PropTypes.object,
+      ]).isRequired,
+      // Helpers injected as props by extension-sidebar.js.
+      serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired,
+    };
+  }
+
+  render() {
+    const {
+      objectValueGrip,
+      serviceContainer,
+      rootTitle,
+    } = this.props;
+
+    const objectInspectorProps = {
+      autoExpandDepth: 1,
+      mode: MODE.SHORT,
+      // 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: objectValueGrip && objectValueGrip.actor || JSON.stringify(objectValueGrip),
+        contents: {
+          value: objectValueGrip,
+        }
+      }],
+      createObjectClient: serviceContainer.createObjectClient,
+      releaseActor: serviceContainer.releaseActor,
+      // TODO: evaluate if there should also be a serviceContainer.openLink.
+    };
+
+    if (objectValueGrip && objectValueGrip.actor) {
+      Object.assign(objectInspectorProps, {
+        onDOMNodeMouseOver: serviceContainer.highlightDomElement,
+        onDOMNodeMouseOut: serviceContainer.unHighlightDomElement,
+        onInspectIconClick(object, e) {
+          // Stop the event propagation so we don't trigger ObjectInspector
+          // expand/collapse.
+          e.stopPropagation();
+          serviceContainer.openNodeInInspector(object);
+        },
+        defaultRep: Grip,
+      });
+    }
+
+    if (rootTitle) {
+      return Accordion({
+        items: [
+          {
+            component: ObjectInspector,
+            componentProps: objectInspectorProps,
+            header: rootTitle,
+            opened: true,
+          },
+        ],
+      });
+    }
+
+    return ObjectInspector(objectInspectorProps);
+  }
+}
+
+module.exports = ObjectValueGripView;
--- a/devtools/client/inspector/extensions/components/moz.build
+++ b/devtools/client/inspector/extensions/components/moz.build
@@ -2,9 +2,10 @@
 # vim: set filetype=python:
 # 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(
     'ExtensionSidebar.js',
     'ObjectTreeView.js',
+    'ObjectValueGripView.js',
 )
--- a/devtools/client/inspector/extensions/extension-sidebar.js
+++ b/devtools/client/inspector/extensions/extension-sidebar.js
@@ -2,20 +2,22 @@
  * 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 { createElement, createFactory } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
+const ObjectClient = require("devtools/shared/client/object-client");
 const ExtensionSidebarComponent = createFactory(require("./components/ExtensionSidebar"));
 
 const {
   updateObjectTreeView,
+  updateObjectValueGripView,
   removeExtensionSidebar,
 } = require("./actions/sidebar");
 
 /**
  * ExtensionSidebar instances represents Inspector sidebars installed by add-ons
  * using the devtools.panels.elements.createSidebarPane WebExtensions API.
  *
  * The WebExtensions API registers the extensions' sidebars on the toolbox instance
@@ -47,16 +49,60 @@ class ExtensionSidebar {
   get provider() {
     if (!this._provider) {
       this._provider = createElement(Provider, {
         store: this.store,
         key: this.id,
         title: this.title,
       }, ExtensionSidebarComponent({
         id: this.id,
+        serviceContainer: {
+          createObjectClient: (object) => {
+            return new ObjectClient(this.inspector.toolbox.target.client, object);
+          },
+          releaseActor: (actor) => {
+            if (!actor) {
+              return;
+            }
+            this.inspector.toolbox.target.client.release(actor);
+          },
+          highlightDomElement: (grip, options = {}) => {
+            const { highlighterUtils } = this.inspector.toolbox;
+
+            if (!highlighterUtils) {
+              return null;
+            }
+
+            return highlighterUtils.highlightDomValueGrip(grip, options);
+          },
+          unHighlightDomElement: (forceHide = false) => {
+            const { highlighterUtils } = this.inspector.toolbox;
+
+            if (!highlighterUtils) {
+              return null;
+            }
+
+            return highlighterUtils.unhighlight(forceHide);
+          },
+          openNodeInInspector: async (grip) => {
+            const { highlighterUtils } = this.inspector.toolbox;
+
+            if (!highlighterUtils) {
+              return null;
+            }
+
+            let front = await highlighterUtils.gripToNodeFront(grip);
+            let onInspectorUpdated = this.inspector.once("inspector-updated");
+            let onNodeFrontSet = this.inspector.toolbox.selection.setNodeFront(
+              front, "inspector-extension-sidebar"
+            );
+
+            return Promise.all([onNodeFrontSet, onInspectorUpdated]);
+          }
+        },
       }));
     }
 
     return this._provider;
   }
 
   /**
    * Destroy the ExtensionSidebar instance, dispatch a removeExtensionSidebar Redux action
@@ -88,11 +134,24 @@ class ExtensionSidebar {
    */
   setObject(object) {
     if (this.removed) {
       throw new Error("Unable to set an object preview on a removed ExtensionSidebar");
     }
 
     this.store.dispatch(updateObjectTreeView(this.id, object));
   }
+
+  /**
+   * Dispatch an objectPreview action to change the SidebarComponent into an
+   * ObjectPreview React Component, which shows the passed value grip
+   * in the sidebar.
+   */
+  setObjectValueGrip(objectValueGrip, rootTitle) {
+    if (this.removed) {
+      throw new Error("Unable to set an object preview on a removed ExtensionSidebar");
+    }
+
+    this.store.dispatch(updateObjectValueGripView(this.id, objectValueGrip, rootTitle));
+  }
 }
 
 module.exports = ExtensionSidebar;
--- a/devtools/client/inspector/extensions/moz.build
+++ b/devtools/client/inspector/extensions/moz.build
@@ -7,11 +7,12 @@
 DIRS += [
     'actions',
     'components',
     'reducers',
 ]
 
 DevToolsModules(
     'extension-sidebar.js',
+    'types.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/extensions/reducers/sidebar.js
+++ b/devtools/client/inspector/extensions/reducers/sidebar.js
@@ -1,16 +1,17 @@
 /* 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 {
   EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE,
+  EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
   EXTENSION_SIDEBAR_REMOVE,
 } = require("../actions/index");
 
 const INITIAL_SIDEBAR = {};
 
 let reducers = {
 
   [EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE](sidebar, {sidebarId, object}) {
@@ -19,16 +20,30 @@ let reducers = {
     return Object.assign({}, sidebar, {
       [sidebarId]: {
         viewMode: "object-treeview",
         object,
       }
     });
   },
 
+  [EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE](
+    sidebar, {sidebarId, objectValueGrip, rootTitle}
+  ) {
+    // Update the sidebar to a "object-treeview" which shows
+    // the passed object.
+    return Object.assign({}, sidebar, {
+      [sidebarId]: {
+        viewMode: "object-value-grip-view",
+        objectValueGrip,
+        rootTitle,
+      }
+    });
+  },
+
   [EXTENSION_SIDEBAR_REMOVE](sidebar, {sidebarId}) {
     // Remove the sidebar from the Redux store.
     delete sidebar[sidebarId];
     return Object.assign({}, sidebar);
   },
 
 };
 
--- a/devtools/client/inspector/extensions/test/browser.ini
+++ b/devtools/client/inspector/extensions/test/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
+  head_devtools_inspector_sidebar.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_inspector_extension_sidebar.js]
\ No newline at end of file
--- a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -1,95 +1,240 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-add_task(async function () {
-  const {inspector} = await openInspectorForURL("about:blank");
-  const {toolbox} = inspector;
+XPCOMUtils.defineLazyModuleGetter(this, "ContentTaskUtils",
+                                  "resource://testing-common/ContentTaskUtils.jsm");
+loader.lazyGetter(this, "WebExtensionInspectedWindowFront", () => {
+  return require(
+    "devtools/shared/fronts/webextension-inspected-window"
+  ).WebExtensionInspectedWindowFront;
+}, true);
 
-  const sidebarId = "an-extension-sidebar";
-  const sidebarTitle = "Sidebar Title";
-
-  const waitSidebarCreated = toolbox.once(`extension-sidebar-created-${sidebarId}`);
+const FAKE_CALLER_INFO = {
+  url: "moz-extension://fake-webextension-uuid/fake-caller-script.js",
+  lineNumber: 1,
+  addonId: "fake-webextension-uuid",
+};
+const SIDEBAR_ID = "an-extension-sidebar";
+const SIDEBAR_TITLE = "Sidebar Title";
 
-  toolbox.registerInspectorExtensionSidebar(sidebarId, {title: sidebarTitle});
+let toolbox;
+let inspector;
 
-  const sidebar = await waitSidebarCreated;
+add_task(async function setupExtensionSidebar() {
+  const res = await openInspectorForURL("about:blank");
+  inspector = res.inspector;
+  toolbox = res.toolbox;
 
-  is(sidebar, inspector.getPanel(sidebarId),
-     "Got an extension sidebar instance equal to the one saved in the inspector");
+  const onceSidebarCreated = toolbox.once(`extension-sidebar-created-${SIDEBAR_ID}`);
+  toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, {title: SIDEBAR_TITLE});
+
+  const sidebar = await onceSidebarCreated;
 
-  is(sidebar.title, sidebarTitle,
+  // Test sidebar properties.
+  is(sidebar, inspector.getPanel(SIDEBAR_ID),
+     "Got an extension sidebar instance equal to the one saved in the inspector");
+  is(sidebar.title, SIDEBAR_TITLE,
      "Got the expected title in the extension sidebar instance");
-  is(sidebar.provider.props.title, sidebarTitle,
+  is(sidebar.provider.props.title, SIDEBAR_TITLE,
      "Got the expeted title in the provider props");
 
+  // Test sidebar Redux state.
   let inspectorStoreState = inspector.store.getState();
-
   ok("extensionsSidebar" in inspectorStoreState,
      "Got the extensionsSidebar sub-state in the inspector Redux store");
-
   Assert.deepEqual(inspectorStoreState.extensionsSidebar, {},
                    "The extensionsSidebar should be initially empty");
+});
 
+add_task(async function testSidebarSetObject() {
   let object = {
     propertyName: {
       nestedProperty: "propertyValue",
       anotherProperty: "anotherValue",
     },
   };
 
+  let sidebar = inspector.getPanel(SIDEBAR_ID);
   sidebar.setObject(object);
 
-  inspectorStoreState = inspector.store.getState();
-
+  // Test updated sidebar Redux state.
+  let inspectorStoreState = inspector.store.getState();
   is(Object.keys(inspectorStoreState.extensionsSidebar).length, 1,
      "The extensionsSidebar state contains the newly registered extension sidebar state");
-
   Assert.deepEqual(inspectorStoreState.extensionsSidebar, {
-    [sidebarId]: {
+    [SIDEBAR_ID]: {
       viewMode: "object-treeview",
       object,
     },
   }, "Got the expected state for the registered extension sidebar");
 
+  // Select the extension sidebar.
   const waitSidebarSelected = toolbox.once(`inspector-sidebar-select`);
-
-  inspector.sidebar.show(sidebarId);
-
+  inspector.sidebar.show(SIDEBAR_ID);
   await waitSidebarSelected;
 
-  const sidebarPanelContent = inspector.sidebar.getTabPanel(sidebarId);
+  const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
 
+  // Test extension sidebar content.
   ok(sidebarPanelContent, "Got a sidebar panel for the registered extension sidebar");
 
-  is(sidebarPanelContent.querySelectorAll("table.treeTable").length, 1,
-     "The sidebar panel contains a rendered TreeView component");
+  assertTreeView(sidebarPanelContent, {
+    expectedTreeTables: 1,
+    expectedStringCells: 2,
+    expectedNumberCells: 0,
+  });
 
-  is(sidebarPanelContent.querySelectorAll("table.treeTable .stringCell").length, 2,
-     "The TreeView component contains the expected number of string cells.");
-
+  // Test sidebar refreshed on further sidebar.setObject calls.
   info("Change the inspected object in the extension sidebar object treeview");
   sidebar.setObject({aNewProperty: 123});
 
-  is(sidebarPanelContent.querySelectorAll("table.treeTable .stringCell").length, 0,
-     "The TreeView component doesn't contains any string cells anymore.");
+  assertTreeView(sidebarPanelContent, {
+    expectedTreeTables: 1,
+    expectedStringCells: 0,
+    expectedNumberCells: 1,
+  });
+});
+
+add_task(async function testSidebarSetObjectValueGrip() {
+  const inspectedWindowFront = new WebExtensionInspectedWindowFront(
+    toolbox.target.client, toolbox.target.form
+  );
+
+  const sidebar = inspector.getPanel(SIDEBAR_ID);
+  const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+  info("Testing sidebar.setObjectValueGrip with rootTitle");
+
+  let expression = `
+    var obj = Object.create(null);
+    obj.prop1 = 123;
+    obj[Symbol('sym1')] = 456;
+    obj.cyclic = obj;
+    obj;
+  `;
+
+  let evalResult = await inspectedWindowFront.eval(FAKE_CALLER_INFO, expression, {
+    evalResultAsGrip: true,
+    toolboxConsoleActorID: toolbox.target.form.consoleActor
+  });
+
+  sidebar.setObjectValueGrip(evalResult.valueGrip, "Expected Root Title");
+
+  // Wait the ObjectInspector component to be rendered and test its content.
+  await testSetExpressionSidebarPanel(sidebarPanelContent, {
+    nodesLength: 4,
+    propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+    rootTitle: "Expected Root Title",
+  });
+
+  info("Testing sidebar.setObjectValueGrip without rootTitle");
+
+  sidebar.setObjectValueGrip(evalResult.valueGrip);
+
+  // Wait the ObjectInspector component to be rendered and test its content.
+  await testSetExpressionSidebarPanel(sidebarPanelContent, {
+    nodesLength: 4,
+    propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+  });
+
+  inspectedWindowFront.destroy();
+});
+
+add_task(async function testSidebarDOMNodeHighlighting() {
+  const inspectedWindowFront = new WebExtensionInspectedWindowFront(
+    toolbox.target.client, toolbox.target.form
+  );
+
+  const sidebar = inspector.getPanel(SIDEBAR_ID);
+  const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+  let expression = "({ body: document.body })";
 
-  is(sidebarPanelContent.querySelectorAll("table.treeTable .numberCell").length, 1,
-     "The TreeView component contains one number cells.");
+  let evalResult = await inspectedWindowFront.eval(FAKE_CALLER_INFO, expression, {
+    evalResultAsGrip: true,
+    toolboxConsoleActorID: toolbox.target.form.consoleActor
+  });
+
+  sidebar.setObjectValueGrip(evalResult.valueGrip);
+
+  // Wait the DOM node to be rendered inside the component.
+  await waitForObjectInspector(sidebarPanelContent, "node");
+
+  // Get and verify the DOMNode and the "open inspector"" icon
+  // rendered inside the ObjectInspector.
+  assertObjectInspector(sidebarPanelContent, {
+    expectedDOMNodes: 1,
+    expectedOpenInspectors: 1,
+  });
+
+  // Test highlight DOMNode on mouseover.
+  info("Highlight the node by moving the cursor on it");
+
+  let onNodeHighlight = toolbox.once("node-highlight");
+
+  moveMouseOnObjectInspectorDOMNode(sidebarPanelContent);
+
+  let nodeFront = await onNodeHighlight;
+  is(nodeFront.displayName, "body", "The correct node was highlighted");
+
+  // Test unhighlight DOMNode on mousemove.
+  info("Unhighlight the node by moving away from the node");
+  let onNodeUnhighlight = toolbox.once("node-unhighlight");
+
+  moveMouseOnPanelCenter(sidebarPanelContent);
 
+  await onNodeUnhighlight;
+  info("node-unhighlight event was fired when moving away from the node");
+
+  inspectedWindowFront.destroy();
+});
+
+add_task(async function testSidebarDOMNodeOpenInspector() {
+  const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+  // Test DOMNode selected in the inspector when "open inspector"" icon clicked.
+  info("Unselect node in the inspector");
+  let onceNewNodeFront = inspector.selection.once("new-node-front");
+  inspector.selection.setNodeFront(null);
+  let nodeFront = await onceNewNodeFront;
+  is(nodeFront, undefined, "The inspector selection should have been unselected");
+
+  info("Select the ObjectInspector DOMNode in the inspector panel by clicking on it");
+
+  // Once we click the open-inspector icon we expect a new node front to be selected
+  // and the node to have been highlighted and unhighlighted.
+  let onNodeHighlight = toolbox.once("node-highlight");
+  let onNodeUnhighlight = toolbox.once("node-unhighlight");
+  onceNewNodeFront = inspector.selection.once("new-node-front");
+
+  clickOpenInspectorIcon(sidebarPanelContent);
+
+  nodeFront = await onceNewNodeFront;
+  is(nodeFront.displayName, "body", "The correct node has been selected");
+  nodeFront = await onNodeHighlight;
+  is(nodeFront.displayName, "body", "The correct node was highlighted");
+
+  await onNodeUnhighlight;
+});
+
+add_task(async function teardownExtensionSidebar() {
   info("Remove the sidebar instance");
 
-  toolbox.unregisterInspectorExtensionSidebar(sidebarId);
+  toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID);
 
-  ok(!inspector.sidebar.getTabPanel(sidebarId),
+  ok(!inspector.sidebar.getTabPanel(SIDEBAR_ID),
      "The rendered extension sidebar has been removed");
 
-  inspectorStoreState = inspector.store.getState();
+  let inspectorStoreState = inspector.store.getState();
 
   Assert.deepEqual(inspectorStoreState.extensionsSidebar, {},
                    "The extensions sidebar Redux store data has been cleared");
 
   await toolbox.destroy();
+
+  toolbox = null;
+  inspector = null;
 });
--- a/devtools/client/inspector/extensions/test/head.js
+++ b/devtools/client/inspector/extensions/test/head.js
@@ -7,8 +7,15 @@
 /* import-globals-from ../../test/head.js */
 
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
+
+// Import the inspector extensions test helpers (shared between the tests that live
+// in the current devtools test directory and the devtools sidebar tests that live
+// in browser/components/extensions/test/browser).
+Services.scriptloader.loadSubScript(
+  new URL("head_devtools_inspector_sidebar.js", gTestPath).href,
+  this);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
@@ -0,0 +1,128 @@
+/* vim: set 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/. */
+
+/* exported waitForObjectInspector, testSetExpressionSidebarPanel, assertTreeView,
+            assertObjectInspector, moveMouseOnObjectInspectorDOMNode,
+            moveMouseOnPanelCenter, clickOpenInspectorIcon */
+
+"use strict";
+
+function waitForObjectInspector(panelDoc, waitForNodeWithType = "object") {
+  const selector = `.object-inspector .objectBox-${waitForNodeWithType}`;
+  return ContentTaskUtils.waitForCondition(() => {
+    return panelDoc.querySelectorAll(selector).length > 0;
+  });
+}
+
+// Helper function used inside the sidebar.setObjectValueGrip test case.
+async function testSetExpressionSidebarPanel(panel, expected) {
+  const {
+    nodesLength,
+    propertiesNames,
+    rootTitle,
+  } = expected;
+
+  await waitForObjectInspector(panel);
+
+  let objectInspectors = [...panel.querySelectorAll(".tree")];
+  is(objectInspectors.length, 1, "There is the expected number of object inspectors");
+  let [objectInspector] = objectInspectors;
+
+  // Wait the objectInspector to have been fully rendered.
+  await ContentTaskUtils.waitForCondition(() => {
+    return objectInspector.querySelectorAll(".node").length >= nodesLength;
+  });
+
+  let oiNodes = objectInspector.querySelectorAll(".node");
+
+  is(oiNodes.length, nodesLength, "Got the expected number of nodes in the tree");
+  let propertiesNodes = [...objectInspector.querySelectorAll(".object-label")]
+        .map(el => el.textContent);
+  is(JSON.stringify(propertiesNodes), JSON.stringify(propertiesNames),
+     "Got the expected property names");
+
+  if (rootTitle) {
+    // Also check that the ObjectInspector is rendered inside
+    // an Accordion component with the expected title.
+    const accordion = panel.querySelector(".accordion");
+
+    ok(accordion, "Got an Accordion component as expected");
+
+    is(accordion.querySelector("._content").firstChild, objectInspector,
+       "The ObjectInspector should be inside the Accordion content");
+
+    is(accordion.querySelector("._header").textContent.trim(), rootTitle,
+       "The Accordion has the expected label");
+  } else {
+    // Also check that there is no Accordion component rendered
+    // inside the sidebar panel.
+    ok(!panel.querySelector(".accordion"),
+       "Got no Accordion component as expected");
+  }
+}
+
+function assertTreeView(panelDoc, expectedContent) {
+  const {
+    expectedTreeTables,
+    expectedStringCells,
+    expectedNumberCells
+  } = expectedContent;
+
+  if (expectedTreeTables) {
+    is(panelDoc.querySelectorAll("table.treeTable").length, expectedTreeTables,
+       "The panel document contains the expected number of TreeView components");
+  }
+
+  if (expectedStringCells) {
+    is(panelDoc.querySelectorAll("table.treeTable .stringCell").length,
+       expectedStringCells,
+       "The panel document contains the expected number of string cells.");
+  }
+
+  if (expectedNumberCells) {
+    is(panelDoc.querySelectorAll("table.treeTable .numberCell").length,
+       expectedNumberCells,
+       "The panel document contains the expected number of number cells.");
+  }
+}
+
+async function assertObjectInspector(panelDoc, expectedContent) {
+  const {expectedDOMNodes, expectedOpenInspectors} = expectedContent;
+
+  // Get and verify the DOMNode and the "open inspector"" icon
+  // rendered inside the ObjectInspector.
+  let nodes = panelDoc.querySelectorAll(".objectBox-node");
+  let nodeOpenInspectors = panelDoc.querySelectorAll(
+    ".objectBox-node .open-inspector"
+  );
+
+  is(nodes.length, expectedDOMNodes,
+     "Found the expected number of ObjectInspector DOMNodes");
+  is(nodeOpenInspectors.length, expectedOpenInspectors,
+     "Found the expected nuber of open-inspector icons inside the ObjectInspector");
+}
+
+function moveMouseOnObjectInspectorDOMNode(panelDoc, nodeIndex = 0) {
+  let nodes = panelDoc.querySelectorAll(".objectBox-node");
+  let node = nodes[nodeIndex];
+
+  ok(node, "Found the ObjectInspector DOMNode");
+
+  EventUtils.synthesizeMouseAtCenter(node, {type: "mousemove"},
+                                     node.ownerDocument.defaultView);
+}
+
+function moveMouseOnPanelCenter(panelDoc) {
+  EventUtils.synthesizeMouseAtCenter(panelDoc, {type: "mousemove"}, panelDoc.window);
+}
+
+function clickOpenInspectorIcon(panelDoc, nodeIndex = 0) {
+  let nodes = panelDoc.querySelectorAll(".objectBox-node .open-inspector");
+  let node = nodes[nodeIndex];
+
+  ok(node, "Found the ObjectInspector open-inspector icon");
+
+  node.click();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/types.js
@@ -0,0 +1,17 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+// Helpers injected as props by extension-sidebar.js and used by the
+// ObjectInspector component (which is part of the ObjectValueGripView).
+exports.serviceContainer = {
+  createObjectClient: PropTypes.func.isRequired,
+  releaseActor: PropTypes.func.isRequired,
+  highlightDomElement: PropTypes.func.isRequired,
+  unHighlightDomElement: PropTypes.func.isRequired,
+  openNodeInInspector: PropTypes.func.isRequired,
+};