Bug 1300587 - Implements devtools_panel context and devtools.panel.create API method. r=kmag
authorLuca Greco <lgreco@mozilla.com>
Thu, 09 Feb 2017 18:23:18 +0100
changeset 343296 de8052439ef29cf0d06f836dac9dc4ad3e2ecdbc
parent 343295 10293eb8e33248ef3758991391835a35cafbc787
child 343297 6d4af96450ed20812649f023bd373110505fabf5
push id31376
push userkwierso@gmail.com
push dateFri, 17 Feb 2017 01:09:40 +0000
treeherdermozilla-central@6cefe01ca774 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1300587
milestone54.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 1300587 - Implements devtools_panel context and devtools.panel.create API method. r=kmag 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);
+});