Bug 1398734 - Support devtools.panels.elements sidebar.setPage API method.
MozReview-Commit-ID: FbXWDUO0lFn
--- 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");