Bug 1398734 - Support devtools.panels.elements sidebar.setPage API method. draft
authorLuca Greco <lgreco@mozilla.com>
Sun, 10 Sep 2017 19:23:28 +0200
changeset 662510 bf65a41ceda74da939cde5da52c8ad6b6f053b91
parent 662509 7a59dba8b4e1be70673c7ae6585ecf3930884fb8
child 730893 237073bdc6a05c798e24d6cef1fa7383a975d2c8
push id79108
push userluca.greco@alcacoop.it
push dateMon, 11 Sep 2017 19:51:56 +0000
bugs1398734
milestone57.0a1
Bug 1398734 - Support devtools.panels.elements sidebar.setPage API method. MozReview-Commit-ID: FbXWDUO0lFn
browser/components/extensions/ext-c-devtools-panels.js
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/schemas/devtools_panels.json
browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
--- a/browser/components/extensions/ext-c-devtools-panels.js
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -210,16 +210,25 @@ class ChildDevToolsInspectorSidebar exte
             fire.async();
           };
           this.on("hidden", listener);
           return () => {
             this.off("hidden", listener);
           };
         }).api(),
 
+      setPage(extensionPageURL) {
+        return context.cloneScope.Promise.resolve().then(() => {
+          return context.childManager.callParentAsyncFunction(
+            "devtools.panels.elements.Sidebar.setPage",
+            [id, extensionPageURL]
+          );
+        });
+      },
+
       setObject(jsonObject, rootTitle) {
         return context.cloneScope.Promise.resolve().then(() => {
           return context.childManager.callParentAsyncFunction(
             "devtools.panels.elements.Sidebar.setObject",
             [id, jsonObject, rootTitle]
           );
         });
       },
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -295,84 +295,189 @@ class ParentDevToolsInspectorSidebar {
     this.context = context;
     this.sidebarOptions = sidebarOptions;
 
     this.context.callOnClose(this);
 
     this.id = this.sidebarOptions.id;
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
     this.onSidebarCreated = this.onSidebarCreated.bind(this);
+    this.onExtensionPageMount = this.onExtensionPageMount.bind(this);
+    this.onExtensionPageUnmount = this.onExtensionPageUnmount.bind(this);
+    this.onToolboxHostWillChange = this.onToolboxHostWillChange.bind(this);
+    this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
 
     this.toolbox.once(`extension-sidebar-created-${this.id}`, this.onSidebarCreated);
-    this.toolbox.on(`inspector-sidebar-select`, this.onSidebarSelect);
+    this.toolbox.on("inspector-sidebar-select", this.onSidebarSelect);
+    this.toolbox.on("host-will-change", this.onToolboxHostWillChange);
+    this.toolbox.on("host-changed", this.onToolboxHostChanged);
 
     // Set by setObject if the sidebar has not been created yet.
     this._initializeSidebar = null;
 
     this.toolbox.registerInspectorExtensionSidebar(this.id, {
       title: sidebarOptions.title,
     });
   }
 
   close() {
     if (this.destroyed) {
       throw new Error("Unable to close a destroyed DevToolsSelectionObserver");
     }
 
+    if (this.extensionSidebar) {
+      this.extensionSidebar.off("extension-page-mount", this.onExtensionPageMount);
+      this.extensionSidebar.off("extension-page-unmount", this.onExtensionPageUnmount);
+    }
+
+    if (this.browser) {
+      this.destroyBrowserElement();
+      this.browser = null;
+      this.containerEl = null;
+    }
+
     this.toolbox.off(`extension-sidebar-created-${this.id}`, this.onSidebarCreated);
-    this.toolbox.off(`inspector-sidebar-select`, this.onSidebarSelect);
+    this.toolbox.off("inspector-sidebar-select", this.onSidebarSelect);
+    this.toolbox.off("host-changed", this.onToolboxHostChanged);
+    this.toolbox.off("host-will-change", this.onToolboxHostWillChange);
 
     this.toolbox.unregisterInspectorExtensionSidebar(this.id);
     this.extensionSidebar = null;
     this._initializeSidebar = null;
 
     this.destroyed = true;
   }
 
+  onToolboxHostWillChange() {
+    if (this.browser) {
+      this.destroyBrowserElement();
+    }
+  }
+
+  onToolboxHostChanged() {
+    if (this.containerEl && this.extensionPageURL) {
+      this.createBrowserElement(this.containerEl);
+    }
+  }
+
   onSidebarCreated(evt, sidebar) {
     this.extensionSidebar = sidebar;
 
+    sidebar.on("extension-page-mount", this.onExtensionPageMount);
+    sidebar.on("extension-page-unmount", this.onExtensionPageUnmount);
+
     if (typeof this._initializeSidebar === "function") {
       this._initializeSidebar();
       this._initializeSidebar = null;
     }
   }
 
   onSidebarSelect(what, id) {
     if (!this.extensionSidebar) {
       return;
     }
 
     if (!this.visible && id === this.id) {
-      // TODO: Wait for top level context if extension page
       this.visible = true;
       this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsInspectorSidebarShown", {
         inspectorSidebarId: this.id,
       });
     } else if (this.visible && id !== this.id) {
       this.visible = false;
       this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsInspectorSidebarHidden", {
         inspectorSidebarId: this.id,
       });
     }
   }
 
+  createBrowserElement(containerEl) {
+    const {toolbox, context} = this;
+    const {extension} = context;
+    const document = containerEl.ownerDocument;
+
+    const browser = document.createElementNS(XUL_NS, "browser");
+    browser.setAttribute("type", "content");
+    browser.setAttribute("disableglobalhistoory", "true");
+    browser.setAttribute("style", "width: 100%; height: 100%;");
+    browser.setAttribute("transparent", "true");
+    browser.setAttribute("class", "webextension-devtoolsInspectorSidebarPanel-browser");
+    browser.setAttribute("webextension-view-type", "devtools_panel");
+    browser.setAttribute("flex", "1");
+
+    this.browser = browser;
+
+    if (extension.remote) {
+      browser.setAttribute("remote", "true");
+      browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
+    }
+
+    // Listening to new proxy contexts.
+    this.unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(this, context => {
+      // Keep track of the toolbox and target associated to the context, which is
+      // needed by the API methods implementation.
+      context.devToolsToolbox = toolbox;
+    });
+
+    containerEl.appendChild(browser);
+
+    extensions.emit("extension-browser-inserted", browser, {
+      devtoolsToolboxInfo: {
+        toolboxPanelId: this.id,
+        inspectedWindowTabId: getTargetTabIdForToolbox(toolbox),
+      },
+    });
+
+    browser.loadURI(this.extensionPageURL || "about:blank");
+  }
+
+  destroyBrowserElement() {
+    const {unwatchExtensionProxyContextLoad, browser} = this;
+
+    if (unwatchExtensionProxyContextLoad) {
+      unwatchExtensionProxyContextLoad();
+      this.unwatchExtensionProxyContextLoad = null;
+      browser.remove();
+    }
+  }
+
+  onExtensionPageMount(evt, containerEl) {
+    this.containerEl = containerEl;
+    this.createBrowserElement(containerEl);
+  }
+
+  onExtensionPageUnmount(evt, containerEl) {
+    this.containerEl = null;
+    this.destroyBrowserElement();
+  }
+
   setObject(object, rootTitle) {
     // Nest the object inside an object, as the value of the `rootTitle` property.
     if (rootTitle) {
       object = {[rootTitle]: object};
     }
 
     if (this.extensionSidebar) {
       this.extensionSidebar.setObject(object);
     } else {
       // Defer the sidebar.setObject call.
       this._initializeSidebar = () => this.extensionSidebar.setObject(object);
     }
   }
+
+  setPage(extensionPageURL) {
+    if (this.extensionSidebar) {
+      if (this.browser) {
+        this.browser.loadURI(extensionPageURL);
+      }
+    } else {
+      // Defer the sidebar.setObject call.
+      this.extensionPageURL = extensionPageURL;
+      this._initializeSidebar = () => this.extensionSidebar.setExtensionPage();
+    }
+  }
 }
 
 const sidebarsById = new Map();
 
 this.devtools_panels = class extends ExtensionAPI {
   getAPI(context) {
     // Lazily retrieved inspectedWindow actor front per child context
     // (used by Sidebar.setExpression).
@@ -424,16 +529,20 @@ this.devtools_panels = class extends Ext
               // where it will be used to identify the messages related
               // to the panel API onShown/onHidden events.
               return Promise.resolve(id);
             },
             // The following methods are used internally to allow the sidebar API
             // piece that is running in the child process to asks the parent process
             // to execute the sidebar methods.
             Sidebar: {
+              setPage(sidebarId, extensionPageURL) {
+                const sidebar = sidebarsById.get(sidebarId);
+                return sidebar.setPage(extensionPageURL);
+              },
               setObject(sidebarId, jsonObject, rootTitle) {
                 const sidebar = sidebarsById.get(sidebarId);
                 return sidebar.setObject(jsonObject, rootTitle);
               },
               async setExpression(sidebarId, evalExpression, rootTitle) {
                 const sidebar = sidebarsById.get(sidebarId);
 
                 if (!waitForInspectedWindowFront) {
--- a/browser/components/extensions/schemas/devtools_panels.json
+++ b/browser/components/extensions/schemas/devtools_panels.json
@@ -225,18 +225,18 @@
                 "type": "function",
                 "optional": true,
                 "description": "A callback invoked after the sidebar is updated with the object."
               }
             ]
           },
           {
             "name": "setPage",
-            "unsupported": true,
             "type": "function",
+            "async": true,
             "description": "Sets an HTML page to be displayed in the sidebar pane.",
             "parameters": [
               {
                 "name": "path",
                 "type": "string",
                 "description": "Relative path of an extension page to display within the sidebar."
               }
             ]
--- a/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
@@ -16,55 +16,75 @@ function isActiveSidebarTabTitle(inspect
 
 add_task(async function test_devtools_panels_elements_sidebar() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
 
   async function devtools_page() {
     const sidebar1 = await browser.devtools.panels.elements.createSidebarPane("Test Sidebar 1");
     const sidebar2 = await browser.devtools.panels.elements.createSidebarPane("Test Sidebar 2");
     const sidebar3 = await browser.devtools.panels.elements.createSidebarPane("Test Sidebar 3");
+    const sidebar4 = await browser.devtools.panels.elements.createSidebarPane("Test Sidebar 4");
 
     const onShownListener = (event, sidebarInstance) => {
       browser.test.sendMessage(`devtools_sidebar_${event}`, sidebarInstance);
     };
 
     sidebar1.onShown.addListener(() => onShownListener("shown", "sidebar1"));
     sidebar2.onShown.addListener(() => onShownListener("shown", "sidebar2"));
     sidebar3.onShown.addListener(() => onShownListener("shown", "sidebar3"));
+    sidebar4.onShown.addListener(() => onShownListener("shown", "sidebar4"));
 
     sidebar1.onHidden.addListener(() => onShownListener("hidden", "sidebar1"));
     sidebar2.onHidden.addListener(() => onShownListener("hidden", "sidebar2"));
     sidebar3.onHidden.addListener(() => onShownListener("hidden", "sidebar3"));
+    sidebar4.onHidden.addListener(() => onShownListener("hidden", "sidebar4"));
 
     sidebar1.setObject({propertyName: "propertyValue"}, "Optional Root Object Title");
     sidebar2.setObject({anotherPropertyName: 123});
 
     // Refresh the sidebar content on every inspector selection.
     browser.devtools.panels.elements.onSelectionChanged.addListener(() => {
       sidebar3.setExpression("$0 && $0.tagName", "Selected Element tagName");
     });
 
+    sidebar4.setPage(browser.runtime.getURL("sidebar.html"));
+
     browser.test.sendMessage("devtools_page_loaded");
   }
 
+  function sidebar() {
+    browser.test.sendMessage("sidebar-loaded");
+  }
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       devtools_page: "devtools_page.html",
     },
     files: {
       "devtools_page.html": `<!DOCTYPE html>
       <html>
        <head>
          <meta charset="utf-8">
        </head>
        <body>
          <script src="devtools_page.js"></script>
        </body>
       </html>`,
       "devtools_page.js": devtools_page,
+      "sidebar.html": `<!DOCTYPE html>
+      <html>
+       <head>
+         <meta charset="utf-8">
+       </head>
+       <body>
+         sidebar panel
+         <script src="sidebar.js"></script>
+       </body>
+      </html>`,
+      "sidebar.js": sidebar,
     },
   });
 
   await extension.startup();
 
   let target = devtools.TargetFactory.forTab(tab);
 
   const toolbox = await gDevTools.showToolbox(target, "webconsole");
@@ -152,16 +172,29 @@ add_task(async function test_devtools_pa
   const treeViewStringValues = sidebarPanel3.querySelectorAll("table.treeTable .stringCell");
 
   is(treeViewStringValues.length, 1,
      "The TreeView component contains the expected content of type string.");
 
   is(treeViewStringValues[0].innerText, "\"BODY\"",
      "Got the expected content in the sidebar.setExpression rendered TreeView");
 
+  inspector.sidebar.show(sidebarIds[3]);
+
+  const shownSidebarInstance4 = await extension.awaitMessage("devtools_sidebar_shown");
+  const hiddenSidebarInstance3 = await extension.awaitMessage("devtools_sidebar_hidden");
+
+  is(shownSidebarInstance4, "sidebar4", "Got the shown event on the third extension sidebar");
+  is(hiddenSidebarInstance3, "sidebar3", "Got the hidden event on the second extension sidebar");
+
+  isActiveSidebarTabTitle(inspector, "Test Sidebar 4",
+                          "Got the expected title on the active sidebar tab");
+
+  await extension.awaitMessage("sidebar-loaded");
+
   await extension.unload();
 
   is(Array.from(toolbox._inspectorExtensionSidebars.keys()).length, 0,
      "All the registered sidebars have been unregistered on extension unload");
 
   is(inspector.sidebar.getTabPanel(sidebarIds[0]), undefined,
      "The first registered sidebar has been removed");