Bug 1300587 - Implements devtools_panel context and devtools.panel.create API method. draft
authorLuca Greco <lgreco@mozilla.com>
Thu, 09 Feb 2017 18:23:18 +0100
changeset 485260 f9ee8c4dc011772b4f5a3cc4eb57bf2d00ee5624
parent 485259 b4cb624bde1a876bade641e1f91e2c8d4df8f3c7
child 485261 34b50fe5f69b0d55bac53498d4745511eb72b905
push id45684
push userluca.greco@alcacoop.it
push dateThu, 16 Feb 2017 12:27:56 +0000
bugs1300587
milestone54.0a1
Bug 1300587 - Implements devtools_panel context and devtools.panel.create API method. MozReview-Commit-ID: GVpwEkHqP3o
browser/components/extensions/.eslintrc.js
browser/components/extensions/ext-c-devtools-panels.js
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/extensions-browser.manifest
browser/components/extensions/jar.mn
browser/components/extensions/schemas/devtools_panels.json
browser/components/extensions/schemas/jar.mn
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_devtools_panel.js
--- a/browser/components/extensions/.eslintrc.js
+++ b/browser/components/extensions/.eslintrc.js
@@ -8,15 +8,16 @@ module.exports = {  // eslint-disable-li
     "IconDetails": true,
     "Tab": true,
     "TabContext": true,
     "Window": true,
     "WindowEventManager": true,
     "browserActionFor": true,
     "getCookieStoreIdForTab": true,
     "getDevToolsTargetForContext": true,
+    "getTargetTabIdForToolbox": true,
     "makeWidgetId": true,
     "pageActionFor": true,
     "sidebarActionFor": true,
     "tabTracker": true,
     "windowTracker": true,
   },
 };
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -0,0 +1,148 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+const {
+  promiseDocumentLoaded,
+  SingletonEventManager,
+} = ExtensionUtils;
+
+const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+
+/**
+ * Represents an addon devtools panel in the child process.
+ *
+ * @param {DevtoolsExtensionContext}
+ *   A devtools extension context running in a child process.
+ * @param {object} panelOptions
+ * @param {string} panelOptions.id
+ *   The id of the addon devtools panel registered in the main process.
+ */
+class ChildDevToolsPanel extends EventEmitter {
+  constructor(context, {id}) {
+    super();
+
+    this.context = context;
+    this.context.callOnClose(this);
+
+    this.id = id;
+    this._panelContext = null;
+
+    this.mm = context.messageManager;
+    this.mm.addMessageListener("Extension:DevToolsPanelShown", this);
+    this.mm.addMessageListener("Extension:DevToolsPanelHidden", this);
+  }
+
+  get panelContext() {
+    if (this._panelContext) {
+      return this._panelContext;
+    }
+
+    for (let view of this.context.extension.devtoolsViews) {
+      if (view.viewType === "devtools_panel" &&
+          view.devtoolsToolboxInfo.toolboxPanelId === this.id) {
+        this._panelContext = view;
+        return view;
+      }
+    }
+
+    return null;
+  }
+
+  receiveMessage({name, data}) {
+    // Filter out any message received while the panel context do not yet
+    // exist.
+    if (!this.panelContext || !this.panelContext.contentWindow) {
+      return;
+    }
+
+    // Filter out any message that is not related to the id of this
+    // toolbox panel.
+    if (!data || data.toolboxPanelId !== this.id) {
+      return;
+    }
+
+    switch (name) {
+      case "Extension:DevToolsPanelShown":
+        this.onParentPanelShown();
+        break;
+      case "Extension:DevToolsPanelHidden":
+        this.onParentPanelHidden();
+        break;
+    }
+  }
+
+  onParentPanelShown() {
+    const {document} = this.panelContext.contentWindow;
+
+    // Ensure that the onShown event is fired when the panel document has
+    // been fully loaded.
+    promiseDocumentLoaded(document).then(() => {
+      this.emit("shown", this.panelContext.contentWindow);
+    });
+  }
+
+  onParentPanelHidden() {
+    this.emit("hidden");
+  }
+
+  api() {
+    return {
+      onShown: new SingletonEventManager(
+        this.context, "devtoolsPanel.onShown", fire => {
+          const listener = (eventName, panelContentWindow) => {
+            fire.asyncWithoutClone(panelContentWindow);
+          };
+          this.on("shown", listener);
+          return () => {
+            this.off("shown", listener);
+          };
+        }).api(),
+
+      onHidden: new SingletonEventManager(
+        this.context, "devtoolsPanel.onHidden", fire => {
+          const listener = () => {
+            fire.async();
+          };
+          this.on("hidden", listener);
+          return () => {
+            this.off("hidden", listener);
+          };
+        }).api(),
+
+      // TODO(rpl): onSearch event and createStatusBarButton method
+    };
+  }
+
+  close() {
+    this.mm.removeMessageListener("Extension:DevToolsPanelShown", this);
+    this.mm.removeMessageListener("Extension:DevToolsPanelHidden", this);
+
+    this._panelContext = null;
+    this.context = null;
+  }
+}
+
+extensions.registerSchemaAPI("devtools.panels", "devtools_child", context => {
+  return {
+    devtools: {
+      panels: {
+        create(title, icon, url) {
+          return context.cloneScope.Promise.resolve().then(async () => {
+            const panelId = await context.childManager.callParentAsyncFunction(
+              "devtools.panels.create", [title, icon, url]);
+
+            const devtoolsPanel = new ChildDevToolsPanel(context, {id: panelId});
+
+            const devtoolsPanelAPI = Cu.cloneInto(devtoolsPanel.api(),
+                                                  context.cloneScope,
+                                                  {cloneFunctions: true});
+            return devtoolsPanelAPI;
+          });
+        },
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -0,0 +1,248 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
+                                  "resource:///modules/E10SUtils.jsm");
+
+const {
+  watchExtensionProxyContextLoad,
+} = ExtensionParent;
+
+const {
+  IconDetails,
+  promiseEvent,
+} = ExtensionUtils;
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Represents an addon devtools panel in the main process.
+ *
+ * @param {ExtensionChildProxyContext} context
+ *        A devtools extension proxy context running in a main process.
+ * @param {object} options
+ * @param {string} options.id
+ *        The id of the addon devtools panel.
+ * @param {string} options.icon
+ *        The icon of the addon devtools panel.
+ * @param {string} options.title
+ *        The title of the addon devtools panel.
+ * @param {string} options.url
+ *        The url of the addon devtools panel, relative to the extension base URL.
+ */
+class ParentDevToolsPanel {
+  constructor(context, panelOptions) {
+    const toolbox = context.devToolsToolbox;
+    if (!toolbox) {
+      // This should never happen when this constructor is called with a valid
+      // devtools extension context.
+      throw Error("Missing mandatory toolbox");
+    }
+
+    this.extension = context.extension;
+    this.viewType = "devtools_panel";
+
+    this.visible = false;
+    this.toolbox = toolbox;
+
+    this.context = context;
+
+    this.panelOptions = panelOptions;
+
+    this.context.callOnClose(this);
+
+    this.id = this.panelOptions.id;
+
+    this.onToolboxPanelSelect = this.onToolboxPanelSelect.bind(this);
+    this.onToolboxReady = this.onToolboxReady.bind(this);
+
+    this.panelAdded = false;
+
+    if (this.toolbox.isReady) {
+      this.onToolboxReady();
+    } else {
+      this.toolbox.once("ready", this.onToolboxReady);
+    }
+
+    this.waitTopLevelContext = new Promise(resolve => {
+      this._resolveTopLevelContext = resolve;
+    });
+  }
+
+  addPanel() {
+    const {icon, title} = this.panelOptions;
+    const extensionName = this.context.extension.name;
+
+    this.toolbox.addAdditionalTool({
+      id: this.id,
+      url: "about:blank",
+      icon: icon,
+      label: title,
+      tooltip: `DevTools Panel added by "${extensionName}" add-on.`,
+      invertIconForLightTheme: true,
+      visibilityswitch:  `devtools.webext-${this.id}.enabled`,
+      isTargetSupported: target => target.isLocalTab,
+      build: (window, toolbox) => {
+        if (toolbox !== this.toolbox) {
+          throw new Error("Unexpected toolbox received on addAdditionalTool build property");
+        }
+
+        const destroy = this.buildPanel(window, toolbox);
+
+        return {toolbox, destroy};
+      },
+    });
+  }
+
+  buildPanel(window, toolbox) {
+    const {url} = this.panelOptions;
+    const {document} = window;
+
+    const browser = document.createElementNS(XUL_NS, "browser");
+    browser.setAttribute("type", "content");
+    browser.setAttribute("disableglobalhistory", "true");
+    browser.setAttribute("style", "width: 100%; height: 100%;");
+    browser.setAttribute("transparent", "true");
+    browser.setAttribute("class", "webextension-devtoolsPanel-browser");
+    browser.setAttribute("webextension-view-type", "devtools_panel");
+    browser.setAttribute("flex", "1");
+
+    this.browser = browser;
+
+    const {extension} = this.context;
+
+    let awaitFrameLoader = Promise.resolve();
+    if (extension.remote) {
+      browser.setAttribute("remote", "true");
+      browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
+      awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
+    }
+
+    let hasTopLevelContext = false;
+
+    // Listening to new proxy contexts.
+    const 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;
+
+      if (!hasTopLevelContext) {
+        hasTopLevelContext = true;
+
+        // Resolve the promise when the root devtools_panel context has been created.
+        awaitFrameLoader.then(() => this._resolveTopLevelContext(context));
+      }
+    });
+
+    document.body.setAttribute("style", "margin: 0; padding: 0;");
+    document.body.appendChild(browser);
+
+    extensions.emit("extension-browser-inserted", browser, {
+      devtoolsToolboxInfo: {
+        toolboxPanelId: this.id,
+        inspectedWindowTabId: getTargetTabIdForToolbox(toolbox),
+      },
+    });
+
+    browser.loadURI(url);
+
+    toolbox.on("select", this.onToolboxPanelSelect);
+
+    // Return a cleanup method that is when the panel is destroyed, e.g.
+    // - when addon devtool panel has been disabled by the user from the toolbox preferences,
+    //   its ParentDevToolsPanel instance is still valid, but the built devtools panel is removed from
+    //   the toolbox (and re-built again if the user re-enable it from the toolbox preferences panel)
+    // - when the creator context has been destroyed, the ParentDevToolsPanel close method is called,
+    //   it remove the tool definition from the toolbox, which will call this destroy method.
+    return () => {
+      unwatchExtensionProxyContextLoad();
+      browser.remove();
+      toolbox.off("select", this.onToolboxPanelSelect);
+    };
+  }
+
+  onToolboxReady() {
+    if (!this.panelAdded) {
+      this.panelAdded = true;
+      this.addPanel();
+    }
+  }
+
+  onToolboxPanelSelect(what, id) {
+    if (!this.waitTopLevelContext || !this.panelAdded) {
+      return;
+    }
+
+    if (!this.visible && id === this.id) {
+      // Wait that the panel is fully loaded and emit show.
+      this.waitTopLevelContext.then(() => {
+        this.visible = true;
+        this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelShown", {
+          toolboxPanelId: this.id,
+        });
+      });
+    } else if (this.visible && id !== this.id) {
+      this.visible = false;
+      this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelHidden", {
+        toolboxPanelId: this.id,
+      });
+    }
+  }
+
+  close() {
+    const {toolbox} = this;
+
+    if (!toolbox) {
+      throw new Error("Unable to destroy a closed devtools panel");
+    }
+
+    toolbox.off("ready", this.onToolboxReady);
+
+    // Explicitly remove the panel if it is registered and the toolbox is not
+    // closing itself.
+    if (toolbox.isToolRegistered(this.id) && !toolbox._destroyer) {
+      toolbox.removeAdditionalTool(this.id);
+    }
+
+    this.context = null;
+    this.toolbox = null;
+  }
+}
+
+extensions.registerSchemaAPI("devtools.panels", "devtools_parent", context => {
+  // An incremental "per context" id used in the generated devtools panel id.
+  let nextPanelId = 0;
+
+  return {
+    devtools: {
+      panels: {
+        create(title, icon, url) {
+          // Get a fallback icon from the manifest data.
+          if (icon === "" && context.extension.manifest.icons) {
+            const iconInfo = IconDetails.getPreferredIcon(context.extension.manifest.icons,
+                                                          context.extension, 128);
+            icon = iconInfo ? iconInfo.icon : "";
+          }
+
+          icon = context.extension.baseURI.resolve(icon);
+          url = context.extension.baseURI.resolve(url);
+
+          const baseId = `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
+          const id = `${makeWidgetId(baseId)}-devtools-panel`;
+
+          new ParentDevToolsPanel(context, {title, icon, url, id});
+
+          // Resolved to the devtools panel id into the child addon process,
+          // where it will be used to identify the messages related
+          // to the panel API onShown/onHidden events.
+          return Promise.resolve(id);
+        },
+      },
+    },
+  };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -3,45 +3,48 @@ category webextension-scripts bookmarks 
 category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
 category webextension-scripts browsingData chrome://browser/content/ext-browsingData.js
 category webextension-scripts commands chrome://browser/content/ext-commands.js
 category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
 category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
 category webextension-scripts devtools chrome://browser/content/ext-devtools.js
 category webextension-scripts devtools-inspectedWindow chrome://browser/content/ext-devtools-inspectedWindow.js
 category webextension-scripts devtools-network chrome://browser/content/ext-devtools-network.js
+category webextension-scripts devtools-panels chrome://browser/content/ext-devtools-panels.js
 category webextension-scripts history chrome://browser/content/ext-history.js
 category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
 category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
 category webextension-scripts sessions chrome://browser/content/ext-sessions.js
 category webextension-scripts sidebarAction chrome://browser/content/ext-sidebarAction.js
 category webextension-scripts tabs chrome://browser/content/ext-tabs.js
 category webextension-scripts theme chrome://browser/content/ext-theme.js
 category webextension-scripts url-overrides chrome://browser/content/ext-url-overrides.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
 category webextension-scripts windows chrome://browser/content/ext-windows.js
 
 # scripts specific for devtools extension contexts.
 category webextension-scripts-devtools devtools-inspectedWindow chrome://browser/content/ext-c-devtools-inspectedWindow.js
+category webextension-scripts-devtools devtools-panels chrome://browser/content/ext-c-devtools-panels.js
 
 # scripts that must run in the same process as addon code.
 category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
 category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
 category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
 
 # schemas
 category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
 category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
 category webextension-schemas browsing_data chrome://browser/content/schemas/browsing_data.json
 category webextension-schemas commands chrome://browser/content/schemas/commands.json
 category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
 category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
 category webextension-schemas devtools chrome://browser/content/schemas/devtools.json
 category webextension-schemas devtools_inspected_window chrome://browser/content/schemas/devtools_inspected_window.json
 category webextension-schemas devtools_network chrome://browser/content/schemas/devtools_network.json
+category webextension-schemas devtools_panels chrome://browser/content/schemas/devtools_panels.json
 category webextension-schemas history chrome://browser/content/schemas/history.json
 category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
 category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
 category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
 category webextension-schemas sidebar_action chrome://browser/content/schemas/sidebar_action.json
 category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
 category webextension-schemas theme chrome://browser/content/schemas/theme.json
 category webextension-schemas url_overrides chrome://browser/content/schemas/url_overrides.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -16,22 +16,24 @@ browser.jar:
     content/browser/ext-browserAction.js
     content/browser/ext-browsingData.js
     content/browser/ext-commands.js
     content/browser/ext-contextMenus.js
     content/browser/ext-desktop-runtime.js
     content/browser/ext-devtools.js
     content/browser/ext-devtools-inspectedWindow.js
     content/browser/ext-devtools-network.js
+    content/browser/ext-devtools-panels.js
     content/browser/ext-history.js
     content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-sessions.js
     content/browser/ext-sidebarAction.js
     content/browser/ext-tabs.js
     content/browser/ext-theme.js
     content/browser/ext-url-overrides.js
     content/browser/ext-utils.js
     content/browser/ext-windows.js
     content/browser/ext-c-contextMenus.js
     content/browser/ext-c-devtools-inspectedWindow.js
+    content/browser/ext-c-devtools-panels.js
     content/browser/ext-c-omnibox.js
     content/browser/ext-c-tabs.js
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/devtools_panels.json
@@ -0,0 +1,407 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+  {
+    "namespace": "devtools.panels",
+    "allowedContexts": ["devtools", "devtools_only"],
+    "defaultContexts": ["devtools", "devtools_only"],
+    "description": "Use the <code>chrome.devtools.panels</code> API to integrate your extension into Developer Tools window UI: create your own panels, access existing panels, and add sidebars.",
+    "nocompile": true,
+    "types": [
+      {
+        "id": "ElementsPanel",
+        "type": "object",
+        "description": "Represents the Elements panel.",
+        "events": [
+          {
+            "name": "onSelectionChanged",
+            "unsupported": true,
+            "description": "Fired when an object is selected in the panel."
+          }
+        ],
+        "functions": [
+          {
+            "name": "createSidebarPane",
+            "unsupported": true,
+            "type": "function",
+            "description": "Creates a pane within panel's sidebar.",
+            "parameters": [
+              {
+                "name": "title",
+                "type": "string",
+                "description": "Text that is displayed in sidebar caption."
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "description": "A callback invoked when the sidebar is created.",
+                "optional": true,
+                "parameters": [
+                  {
+                    "name": "result",
+                    "description": "An ExtensionSidebarPane object for created sidebar pane.",
+                    "$ref": "ExtensionSidebarPane"
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "id": "SourcesPanel",
+        "type": "object",
+        "description": "Represents the Sources panel.",
+        "events": [
+          {
+            "name": "onSelectionChanged",
+            "unsupported": true,
+            "description": "Fired when an object is selected in the panel."
+          }
+        ],
+        "functions": [
+          {
+            "name": "createSidebarPane",
+            "unsupported": true,
+            "type": "function",
+            "description": "Creates a pane within panel's sidebar.",
+            "parameters": [
+              {
+                "name": "title",
+                "type": "string",
+                "description": "Text that is displayed in sidebar caption."
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "description": "A callback invoked when the sidebar is created.",
+                "optional": true,
+                "parameters": [
+                  {
+                    "name": "result",
+                    "description": "An ExtensionSidebarPane object for created sidebar pane.",
+                    "$ref": "ExtensionSidebarPane"
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "id": "ExtensionPanel",
+        "type": "object",
+        "description": "Represents a panel created by extension.",
+        "functions": [
+          {
+            "name": "createStatusBarButton",
+            "unsupported": true,
+            "description": "Appends a button to the status bar of the panel.",
+            "type": "function",
+            "parameters": [
+              {
+                "name": "iconPath",
+                "type": "string",
+                "description": "Path to the icon of the button. The file should contain a 64x24-pixel image composed of two 32x24 icons. The left icon is used when the button is inactive; the right icon is displayed when the button is pressed."
+              },
+              {
+                "name": "tooltipText",
+                "type": "string",
+                "description": "Text shown as a tooltip when user hovers the mouse over the button."
+              },
+              {
+                "name": "disabled",
+                "type": "boolean",
+                "description": "Whether the button is disabled."
+              }
+            ],
+            "returns": { "$ref": "Button" }
+          }
+        ],
+        "events": [
+          {
+            "name": "onSearch",
+            "unsupported": true,
+            "description": "Fired upon a search action (start of a new search, search result navigation, or search being canceled).",
+            "parameters": [
+              {
+                "name": "action",
+                "type": "string",
+                "description": "Type of search action being performed."
+              },
+              {
+                "name": "queryString",
+                "type": "string",
+                "optional": true,
+                "description": "Query string (only for 'performSearch')."
+              }
+            ]
+          },
+          {
+            "name": "onShown",
+            "type": "function",
+            "description": "Fired when the user switches to the panel.",
+            "parameters": [
+              {
+                "name": "window",
+                "type": "object",
+                "isInstanceOf": "global",
+                "additionalProperties": { "type": "any" },
+                "description": "The JavaScript <code>window</code> object of panel's page."
+              }
+            ]
+          },
+          {
+            "name": "onHidden",
+            "type": "function",
+            "description": "Fired when the user switches away from the panel."
+          }
+        ]
+      },
+      {
+        "id": "ExtensionSidebarPane",
+        "type": "object",
+        "description": "A sidebar created by the extension.",
+        "functions": [
+          {
+            "name": "setHeight",
+            "unsupported": true,
+            "type": "function",
+            "description": "Sets the height of the sidebar.",
+            "parameters": [
+              {
+                "name": "height",
+                "type": "string",
+                "description": "A CSS-like size specification, such as <code>'100px'</code> or <code>'12ex'</code>."
+              }
+            ]
+          },
+          {
+            "name": "setExpression",
+            "unsupported": true,
+            "type": "function",
+            "description": "Sets an expression that is evaluated within the inspected page. The result is displayed in the sidebar pane.",
+            "parameters": [
+              {
+                "name": "expression",
+                "type": "string",
+                "description": "An expression to be evaluated in context of the inspected page. JavaScript objects and DOM nodes are displayed in an expandable tree similar to the console/watch."
+              },
+              {
+                "name": "rootTitle",
+                "type": "string",
+                "optional": true,
+                "description": "An optional title for the root of the expression tree."
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "optional": true,
+                "description": "A callback invoked after the sidebar pane is updated with the expression evaluation results."
+              }
+            ]
+          },
+          {
+            "name": "setObject",
+            "unsupported": true,
+            "type": "function",
+            "description": "Sets a JSON-compliant object to be displayed in the sidebar pane.",
+            "parameters": [
+              {
+                "name": "jsonObject",
+                "type": "string",
+                "description": "An object to be displayed in context of the inspected page. Evaluated in the context of the caller (API client)."
+              },
+              {
+                "name": "rootTitle",
+                "type": "string",
+                "optional": true,
+                "description": "An optional title for the root of the expression tree."
+              },
+              {
+                "name": "callback",
+                "type": "function",
+                "optional": true,
+                "description": "A callback invoked after the sidebar is updated with the object."
+              }
+            ]
+          },
+          {
+            "name": "setPage",
+            "unsupported": true,
+            "type": "function",
+            "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."
+              }
+            ]
+          }
+        ],
+        "events": [
+          {
+            "name": "onShown",
+            "unsupported": true,
+            "type": "function",
+            "description": "Fired when the sidebar pane becomes visible as a result of user switching to the panel that hosts it.",
+            "parameters": [
+              {
+                "name": "window",
+                "type": "object",
+                "isInstanceOf": "global",
+                "additionalProperties": { "type": "any" },
+                "description": "The JavaScript <code>window</code> object of the sidebar page, if one was set with the <code>setPage()</code> method."
+              }
+            ]
+          },
+          {
+            "name": "onHidden",
+            "unsupported": true,
+            "type": "function",
+            "description": "Fired when the sidebar pane becomes hidden as a result of the user switching away from the panel that hosts the sidebar pane."
+          }
+        ]
+      },
+      {
+        "id": "Button",
+        "type": "object",
+        "description": "A button created by the extension.",
+        "functions": [
+          {
+            "name": "update",
+            "unsupported": true,
+            "type": "function",
+            "description": "Updates the attributes of the button. If some of the arguments are omitted or <code>null</code>, the corresponding attributes are not updated.",
+            "parameters": [
+              {
+                "name": "iconPath",
+                "type": "string",
+                "optional": true,
+                "description": "Path to the new icon of the button."
+              },
+              {
+                "name": "tooltipText",
+                "type": "string",
+                "optional": true,
+                "description": "Text shown as a tooltip when user hovers the mouse over the button."
+              },
+              {
+                "name": "disabled",
+                "type": "boolean",
+                "optional": true,
+                "description": "Whether the button is disabled."
+              }
+            ]
+          }
+        ],
+        "events": [
+          {
+            "name": "onClicked",
+            "unsupported": true,
+            "type": "function",
+            "description": "Fired when the button is clicked."
+          }
+        ]
+      }
+    ],
+    "properties": {
+      "elements": {
+        "$ref": "ElementsPanel",
+        "description": "Elements panel."
+      },
+      "sources": {
+        "$ref": "SourcesPanel",
+        "description": "Sources panel."
+      }
+    },
+    "functions": [
+      {
+        "name": "create",
+        "type": "function",
+        "description": "Creates an extension panel.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "title",
+            "type": "string",
+            "description": "Title that is displayed next to the extension icon in the Developer Tools toolbar."
+          },
+          {
+            "name": "iconPath",
+            "type": "string",
+            "description": "Path of the panel's icon relative to the extension directory."
+          },
+          {
+            "name": "pagePath",
+            "type": "string",
+            "description": "Path of the panel's HTML page relative to the extension directory."
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "optional": true,
+            "description": "A function that is called when the panel is created.",
+            "parameters": [
+              {
+                "name": "panel",
+                "description": "An ExtensionPanel object representing the created panel.",
+                "$ref": "ExtensionPanel"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "setOpenResourceHandler",
+        "unsupported": true,
+        "type": "function",
+        "description": "Specifies the function to be called when the user clicks a resource link in the Developer Tools window. To unset the handler, either call the method with no parameters or pass null as the parameter.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "callback",
+            "type": "function",
+            "optional": true,
+            "description": "A function that is called when the user clicks on a valid resource link in Developer Tools window. Note that if the user clicks an invalid URL or an XHR, this function is not called.",
+            "parameters": [
+              {
+                "name": "resource",
+                "$ref": "devtools.inspectedWindow.Resource",
+                "description": "A $(ref:devtools.inspectedWindow.Resource) object for the resource that was clicked."
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "openResource",
+        "unsupported": true,
+        "type": "function",
+        "description": "Requests DevTools to open a URL in a Developer Tools panel.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "url",
+            "type": "string",
+            "description": "The URL of the resource to open."
+          },
+          {
+            "name": "lineNumber",
+            "type": "integer",
+            "description": "Specifies the line number to scroll to when the resource is loaded."
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "optional": true,
+            "description": "A function that is called when the resource has been successfully loaded."
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -7,16 +7,17 @@ browser.jar:
     content/browser/schemas/browser_action.json
     content/browser/schemas/browsing_data.json
     content/browser/schemas/commands.json
     content/browser/schemas/context_menus.json
     content/browser/schemas/context_menus_internal.json
     content/browser/schemas/devtools.json
     content/browser/schemas/devtools_inspected_window.json
     content/browser/schemas/devtools_network.json
+    content/browser/schemas/devtools_panels.json
     content/browser/schemas/history.json
     content/browser/schemas/omnibox.json
     content/browser/schemas/page_action.json
     content/browser/schemas/sessions.json
     content/browser/schemas/sidebar_action.json
     content/browser/schemas/tabs.json
     content/browser/schemas/theme.json
     content/browser/schemas/url_overrides.json
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -50,16 +50,17 @@ support-files =
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_uninstall.js]
 [browser_ext_contextMenus_urlPatterns.js]
 [browser_ext_currentWindow.js]
 [browser_ext_devtools_inspectedWindow.js]
 [browser_ext_devtools_inspectedWindow_reload.js]
 [browser_ext_devtools_network.js]
 [browser_ext_devtools_page.js]
+[browser_ext_devtools_panel.js]
 [browser_ext_getViews.js]
 [browser_ext_incognito_views.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_omnibox.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_panel.js
@@ -0,0 +1,145 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "devtools",
+                                  "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+                                  "resource://devtools/client/framework/gDevTools.jsm");
+
+/**
+ * This test file ensures that:
+ *
+ * - ensures that devtools.panel.create is able to create a devtools panel
+ */
+
+add_task(function* test_devtools_page_panels_create() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+
+  async function devtools_page() {
+    const result = {
+      devtoolsPageTabId: browser.devtools.inspectedWindow.tabId,
+      panelCreated: 0,
+      panelShown: 0,
+      panelHidden: 0,
+    };
+
+    try {
+      const panel = await browser.devtools.panels.create(
+        "Test Panel", "fake-icon.png", "devtools_panel.html"
+      );
+
+      result.panelCreated++;
+
+      panel.onShown.addListener(contentWindow => {
+        result.panelShown++;
+        browser.test.assertEq("complete", contentWindow.document.readyState,
+                              "Got the expected 'complete' panel document readyState");
+        browser.test.assertEq("test_panel_global", contentWindow.TEST_PANEL_GLOBAL,
+                              "Got the expected global in the panel contentWindow");
+        browser.test.sendMessage("devtools_panel_shown", result);
+      });
+
+      panel.onHidden.addListener(() => {
+        result.panelHidden++;
+
+        browser.test.sendMessage("devtools_panel_hidden", result);
+      });
+
+      browser.test.sendMessage("devtools_panel_created");
+    } catch (err) {
+      // Make the test able to fail fast when it is going to be a failure.
+      browser.test.sendMessage("devtools_panel_created");
+      throw err;
+    }
+  }
+
+  function devtools_panel() {
+    // Set a property in the global and check that it is defined
+    // and accessible from the devtools_page when the panel.onShown
+    // event has been received.
+    window.TEST_PANEL_GLOBAL = "test_panel_global";
+    browser.test.sendMessage("devtools_panel_inspectedWindow_tabId",
+                             browser.devtools.inspectedWindow.tabId);
+  }
+
+  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,
+      "devtools_panel.html":  `<!DOCTYPE html>
+      <html>
+       <head>
+         <meta charset="utf-8">
+       </head>
+       <body>
+         DEVTOOLS PANEL
+         <script src="devtools_panel.js"></script>
+       </body>
+      </html>`,
+      "devtools_panel.js": devtools_panel,
+    },
+  });
+
+  yield extension.startup();
+
+  let target = devtools.TargetFactory.forTab(tab);
+
+  const toolbox = yield gDevTools.showToolbox(target, "webconsole");
+  info("developer toolbox opened");
+
+  yield extension.awaitMessage("devtools_panel_created");
+
+  const toolboxAdditionalTools = toolbox.getAdditionalTools();
+
+  is(toolboxAdditionalTools.length, 1,
+     "Got the expected number of toolbox specific panel registered.");
+
+  const panelId = toolboxAdditionalTools[0].id;
+
+  yield gDevTools.showToolbox(target, panelId);
+  const {devtoolsPageTabId} = yield extension.awaitMessage("devtools_panel_shown");
+  const devtoolsPanelTabId = yield extension.awaitMessage("devtools_panel_inspectedWindow_tabId");
+  is(devtoolsPanelTabId, devtoolsPageTabId,
+     "Got the same devtools.inspectedWindow.tabId from devtools page and panel");
+  info("Addon Devtools Panel shown");
+
+  yield gDevTools.showToolbox(target, "webconsole");
+  const results = yield extension.awaitMessage("devtools_panel_hidden");
+  info("Addon Devtools Panel hidden");
+
+  is(results.panelCreated, 1, "devtools.panel.create callback has been called once");
+  is(results.panelShown, 1, "panel.onShown listener has been called once");
+  is(results.panelHidden, 1, "panel.onHidden listener has been called once");
+
+  yield gDevTools.showToolbox(target, panelId);
+  yield extension.awaitMessage("devtools_panel_shown");
+  info("Addon Devtools Panel shown - second cycle");
+
+  yield gDevTools.showToolbox(target, "webconsole");
+  const secondCycleResults = yield extension.awaitMessage("devtools_panel_hidden");
+  info("Addon Devtools Panel hidden - second cycle");
+
+  is(secondCycleResults.panelCreated, 1, "devtools.panel.create callback has been called once");
+  is(secondCycleResults.panelShown, 2, "panel.onShown listener has been called twice");
+  is(secondCycleResults.panelHidden, 2, "panel.onHidden listener has been called twice");
+
+  yield gDevTools.closeToolbox(target);
+
+  yield target.destroy();
+
+  yield extension.unload();
+
+  yield BrowserTestUtils.removeTab(tab);
+});