Bug 1398734 - Implement devtools inspector extension page sidebar container component. r=gl
authorLuca Greco <lgreco@mozilla.com>
Wed, 03 Oct 2018 18:23:22 +0000
changeset 495194 8a41d62efdd444de64a935f3dbf4da75cbf8c28d
parent 495178 dbe7858573bd0918f2a101a62997a8309a2fc517
child 495195 7fd8b7194516bcf2d4077c281de479a47934e25f
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1398734
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1398734 - Implement devtools inspector extension page sidebar container component. r=gl Differential Revision: https://phabricator.services.mozilla.com/D5420
devtools/client/inspector/extensions/actions/index.js
devtools/client/inspector/extensions/actions/sidebar.js
devtools/client/inspector/extensions/components/ExtensionPage.js
devtools/client/inspector/extensions/components/ExtensionSidebar.js
devtools/client/inspector/extensions/components/moz.build
devtools/client/inspector/extensions/extension-sidebar.js
devtools/client/inspector/extensions/reducers/sidebar.js
devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
--- a/devtools/client/inspector/extensions/actions/index.js
+++ b/devtools/client/inspector/extensions/actions/index.js
@@ -9,12 +9,15 @@ const { createEnum } = require("devtools
 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",
 
+  // Switch the extension sidebar into an extension page container.
+  "EXTENSION_SIDEBAR_PAGE_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
@@ -2,16 +2,17 @@
  * 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_PAGE_UPDATE,
   EXTENSION_SIDEBAR_REMOVE,
 } = require("./index");
 
 module.exports = {
 
   /**
    * Update the sidebar with an object treeview.
    */
@@ -31,16 +32,27 @@ module.exports = {
       type: EXTENSION_SIDEBAR_OBJECT_GRIP_VIEW_UPDATE,
       sidebarId,
       objectValueGrip,
       rootTitle,
     };
   },
 
   /**
+   * Switch the sidebar into the extension page mode.
+   */
+  updateExtensionPage(sidebarId, iframeURL) {
+    return {
+      type: EXTENSION_SIDEBAR_PAGE_UPDATE,
+      sidebarId,
+      iframeURL,
+    };
+  },
+
+  /**
    * Remove the extension sidebar from the inspector store.
    */
   removeExtensionSidebar(sidebarId) {
     return {
       type: EXTENSION_SIDEBAR_REMOVE,
       sidebarId,
     };
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/extensions/components/ExtensionPage.js
@@ -0,0 +1,53 @@
+/* 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 { createRef, 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");
+
+/**
+ * The ExtensionPage React Component is used in the ExtensionSidebar component to provide
+ * a UI viewMode which shows an extension page rendered inside the sidebar panel.
+ */
+class ExtensionPage extends PureComponent {
+  static get propTypes() {
+    return {
+      iframeURL: PropTypes.string.isRequired,
+      onExtensionPageMount: PropTypes.func.isRequired,
+      onExtensionPageUnmount: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.iframeRef = createRef();
+  }
+
+  componentDidMount() {
+    this.props.onExtensionPageMount(this.iframeRef.current);
+  }
+
+  componentWillUnmount() {
+    this.props.onExtensionPageUnmount(this.iframeRef.current);
+  }
+
+  render() {
+    return dom.iframe({
+      className: "inspector-extension-sidebar-page",
+      src: this.props.iframeURL,
+      style: {
+        width: "100%",
+        height: "100%",
+        margin: 0,
+        padding: 0,
+      },
+      ref: this.iframeRef,
+    });
+  }
+}
+
+module.exports = ExtensionPage;
--- a/devtools/client/inspector/extensions/components/ExtensionSidebar.js
+++ b/devtools/client/inspector/extensions/components/ExtensionSidebar.js
@@ -4,16 +4,17 @@
 
 "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 ExtensionPage = createFactory(require("./ExtensionPage"));
 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 WebExtensions APIs)
@@ -24,53 +25,71 @@ const Types = require("../types");
  *
  * TODO: implement the ExtensionPage viewMode.
  */
 class ExtensionSidebar extends PureComponent {
   static get propTypes() {
     return {
       id: PropTypes.string.isRequired,
       extensionsSidebar: PropTypes.object.isRequired,
+      onExtensionPageMount: PropTypes.func.isRequired,
+      onExtensionPageUnmount: PropTypes.func.isRequired,
       // Helpers injected as props by extension-sidebar.js.
       serviceContainer: PropTypes.shape(Types.serviceContainer).isRequired,
     };
   }
 
   render() {
     const {
       id,
       extensionsSidebar,
+      onExtensionPageMount,
+      onExtensionPageUnmount,
       serviceContainer,
     } = this.props;
 
     const {
-      viewMode = "empty-sidebar",
+      iframeURL,
       object,
       objectValueGrip,
-      rootTitle
+      rootTitle,
+      viewMode = "empty-sidebar",
     } = extensionsSidebar[id] || {};
 
     let sidebarContentEl;
 
     switch (viewMode) {
       case "object-treeview":
         sidebarContentEl = ObjectTreeView({ object });
         break;
       case "object-value-grip-view":
         sidebarContentEl = ObjectValueGripView({
           objectValueGrip,
+          rootTitle,
           serviceContainer,
-          rootTitle,
+        });
+        break;
+      case "extension-page":
+        sidebarContentEl = ExtensionPage({
+          iframeURL,
+          onExtensionPageMount,
+          onExtensionPageUnmount,
         });
         break;
       case "empty-sidebar":
         break;
       default:
         throw new Error(`Unknown ExtensionSidebar viewMode: "${viewMode}"`);
     }
 
     const className = "devtools-monospace extension-sidebar inspector-tabpanel";
 
-    return dom.div({ id, className }, sidebarContentEl);
+    return dom.div({
+      id,
+      className,
+      style: {
+        height: "100%",
+      },
+    }, sidebarContentEl);
   }
 }
 
 module.exports = connect(state => state)(ExtensionSidebar);
--- a/devtools/client/inspector/extensions/components/moz.build
+++ b/devtools/client/inspector/extensions/components/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # 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(
+    'ExtensionPage.js',
     'ExtensionSidebar.js',
     'ObjectTreeView.js',
     'ObjectValueGripView.js',
 )
--- a/devtools/client/inspector/extensions/extension-sidebar.js
+++ b/devtools/client/inspector/extensions/extension-sidebar.js
@@ -1,21 +1,22 @@
 /* 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 { createElement, createFactory } = require("devtools/client/shared/vendor/react");
+const EventEmitter = require("devtools/shared/event-emitter");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
-
 const ObjectClient = require("devtools/shared/client/object-client");
 const ExtensionSidebarComponent = createFactory(require("./components/ExtensionSidebar"));
 
 const {
+  updateExtensionPage,
   updateObjectTreeView,
   updateObjectValueGripView,
   removeExtensionSidebar,
 } = require("./actions/sidebar");
 
 /**
  * ExtensionSidebar instances represents Inspector sidebars installed by add-ons
  * using the devtools.panels.elements.createSidebarPane WebExtensions API.
@@ -30,16 +31,17 @@ const {
  * @param {Object} options
  * @param {String} options.id
  *        The unique id of the sidebar.
  * @param {String} options.title
  *        The title of the sidebar.
  */
 class ExtensionSidebar {
   constructor(inspector, {id, title}) {
+    EventEmitter.decorate(this);
     this.inspector = inspector;
     this.store = inspector.store;
     this.id = id;
     this.title = title;
 
     this.destroyed = false;
   }
 
@@ -49,16 +51,22 @@ class ExtensionSidebar {
   get provider() {
     if (!this._provider) {
       this._provider = createElement(Provider, {
         store: this.store,
         key: this.id,
         title: this.title,
       }, ExtensionSidebarComponent({
         id: this.id,
+        onExtensionPageMount: (containerEl) => {
+          this.emit("extension-page-mount", containerEl);
+        },
+        onExtensionPageUnmount: (containerEl) => {
+          this.emit("extension-page-unmount", containerEl);
+        },
         serviceContainer: {
           createObjectClient: (object) => {
             return new ObjectClient(this.inspector.toolbox.target.client, object);
           },
           releaseActor: (actor) => {
             if (!actor) {
               return;
             }
@@ -147,11 +155,19 @@ class ExtensionSidebar {
    */
   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));
   }
+
+  setExtensionPage(iframeURL) {
+    if (this.removed) {
+      throw new Error("Unable to set an object preview on a removed ExtensionSidebar");
+    }
+
+    this.store.dispatch(updateExtensionPage(this.id, iframeURL));
+  }
 }
 
 module.exports = ExtensionSidebar;
--- a/devtools/client/inspector/extensions/reducers/sidebar.js
+++ b/devtools/client/inspector/extensions/reducers/sidebar.js
@@ -2,16 +2,17 @@
  * 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_PAGE_UPDATE,
   EXTENSION_SIDEBAR_REMOVE,
 } = require("../actions/index");
 
 const INITIAL_SIDEBAR = {};
 
 const reducers = {
 
   [EXTENSION_SIDEBAR_OBJECT_TREEVIEW_UPDATE](sidebar, {sidebarId, object}) {
@@ -34,16 +35,27 @@ const reducers = {
       [sidebarId]: {
         viewMode: "object-value-grip-view",
         objectValueGrip,
         rootTitle,
       }
     });
   },
 
+  [EXTENSION_SIDEBAR_PAGE_UPDATE](sidebar, {sidebarId, iframeURL}) {
+    // Update the sidebar to a "object-treeview" which shows
+    // the passed object.
+    return Object.assign({}, sidebar, {
+      [sidebarId]: {
+        viewMode: "extension-page",
+        iframeURL,
+      }
+    });
+  },
+
   [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_inspector_extension_sidebar.js
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -223,16 +223,33 @@ add_task(async function testSidebarDOMNo
   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 testSidebarSetExtensionPage() {
+  const inspectedWindowFront = toolbox.target.getFront("webExtensionInspectedWindow");
+
+  const sidebar = inspector.getPanel(SIDEBAR_ID);
+  const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
+
+  info("Testing sidebar.setExtensionPage");
+
+  const expectedURL = "data:text/html,<!DOCTYPE html><html><body><h1>Extension Page";
+
+  sidebar.setExtensionPage(expectedURL);
+
+  await testSetExtensionPageSidebarPanel(sidebarPanelContent, expectedURL);
+
+  inspectedWindowFront.destroy();
+});
+
 add_task(async function teardownExtensionSidebar() {
   info("Remove the sidebar instance");
 
   toolbox.unregisterInspectorExtensionSidebar(SIDEBAR_ID);
 
   ok(!inspector.sidebar.getTabPanel(SIDEBAR_ID),
      "The rendered extension sidebar has been removed");
 
--- a/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
+++ b/devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
@@ -46,16 +46,34 @@ async function expectNoSuchActorIDs(clie
 
 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.setExtensionPage test case.
+async function testSetExtensionPageSidebarPanel(panelDoc, expectedURL) {
+  const selector = "iframe.inspector-extension-sidebar-page";
+  const iframesCount = await ContentTaskUtils.waitForCondition(() => {
+    return panelDoc.querySelectorAll(selector).length > 0;
+  }, "Wait for the extension page iframe");
+
+  is(iframesCount, 1, "Got the expected number of iframes in the extension panel");
+
+  const iframeWindow = panelDoc.querySelector(selector).contentWindow;
+  await ContentTaskUtils.waitForCondition(() => {
+    return iframeWindow.document.readyState === "complete";
+  }, "Wait for the extension page iframe to complete to load");
+
+  is(iframeWindow.location.href, expectedURL,
+     "Got the expected url in the extension panel iframe");
+}
+
 // Helper function used inside the sidebar.setObjectValueGrip test case.
 async function testSetExpressionSidebarPanel(panel, expected) {
   const {
     nodesLength,
     propertiesNames,
     rootTitle,
   } = expected;