--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -374,42 +374,52 @@ class ParentDevToolsInspectorSidebar {
this.onSidebarCreated = this.onSidebarCreated.bind(this);
this.toolbox.once(`extension-sidebar-created-${this.id}`, this.onSidebarCreated);
this.toolbox.on(`inspector-sidebar-select`, this.onSidebarSelect);
// Set by setObject if the sidebar has not been created yet.
this._initializeSidebar = null;
+ // Set by _updateLastObjectValueGrip to keep track of the last
+ // object value grip (to release the previous selected actor
+ // on the remote debugging server when the actor changes).
+ this._lastObjectValueGrip = null;
+
this.toolbox.registerInspectorExtensionSidebar(this.id, {
title: sidebarOptions.title,
});
}
close() {
if (this.destroyed) {
throw new Error("Unable to close a destroyed DevToolsSelectionObserver");
}
+ // Release the last selected actor on the remote debugging server.
+ this._updateLastObjectValueGrip(null);
+
this.toolbox.off(`extension-sidebar-created-${this.id}`, this.onSidebarCreated);
this.toolbox.off(`inspector-sidebar-select`, this.onSidebarSelect);
this.toolbox.unregisterInspectorExtensionSidebar(this.id);
this.extensionSidebar = null;
- this._initializeSidebar = null;
+ this._lazySidebarInit = null;
this.destroyed = true;
}
onSidebarCreated(evt, sidebar) {
this.extensionSidebar = sidebar;
- if (typeof this._initializeSidebar === "function") {
- this._initializeSidebar();
- this._initializeSidebar = null;
+ const {_lazySidebarInit} = this;
+ this._lazySidebarInit = null;
+
+ if (typeof _lazySidebarInit === "function") {
+ _lazySidebarInit();
}
}
onSidebarSelect(what, id) {
if (!this.extensionSidebar) {
return;
}
@@ -423,26 +433,59 @@ class ParentDevToolsInspectorSidebar {
this.visible = false;
this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsInspectorSidebarHidden", {
inspectorSidebarId: this.id,
});
}
}
setObject(object, rootTitle) {
+ this._updateLastObjectValueGrip(null);
+
// 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);
+ this._setLazySidebarInit(() => this.extensionSidebar.setObject(object));
+ }
+ }
+
+ _setLazySidebarInit(cb) {
+ this._lazySidebarInit = cb;
+ }
+
+ setObjectValueGrip(objectValueGrip, rootTitle) {
+ this._updateLastObjectValueGrip(objectValueGrip);
+
+ if (this.extensionSidebar) {
+ this.extensionSidebar.setObjectValueGrip(objectValueGrip, rootTitle);
+ } else {
+ // Defer the sidebar.setObjectValueGrip call.
+ this._setLazySidebarInit(() => {
+ this.extensionSidebar.setObjectValueGrip(objectValueGrip, rootTitle);
+ });
+ }
+ }
+
+ _updateLastObjectValueGrip(newObjectValueGrip = null) {
+ const {_lastObjectValueGrip} = this;
+
+ this._lastObjectValueGrip = newObjectValueGrip;
+
+ const oldActor = _lastObjectValueGrip && _lastObjectValueGrip.actor;
+ const newActor = newObjectValueGrip && newObjectValueGrip.actor;
+
+ // Release the previously active actor on the remote debugging server.
+ if (oldActor && oldActor !== newActor) {
+ this.toolbox.target.client.release(oldActor);
}
}
}
const sidebarsById = new Map();
this.devtools_panels = class extends ExtensionAPI {
getAPI(context) {
@@ -508,28 +551,30 @@ this.devtools_panels = class extends Ext
async setExpression(sidebarId, evalExpression, rootTitle) {
const sidebar = sidebarsById.get(sidebarId);
if (!waitForInspectedWindowFront) {
waitForInspectedWindowFront = getInspectedWindowFront(context);
}
const front = await waitForInspectedWindowFront;
- const evalOptions = Object.assign({}, getToolboxEvalOptions(context));
+ const evalOptions = Object.assign({
+ evalResultAsGrip: true,
+ }, getToolboxEvalOptions(context));
const evalResult = await front.eval(callerInfo, evalExpression, evalOptions);
let jsonObject;
if (evalResult.exceptionInfo) {
jsonObject = evalResult.exceptionInfo;
- } else {
- jsonObject = evalResult.value;
+
+ return sidebar.setObject(jsonObject, rootTitle);
}
- return sidebar.setObject(jsonObject, rootTitle);
+ return sidebar.setObjectValueGrip(evalResult.valueGrip, rootTitle);
},
},
},
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);
--- 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
@@ -4,21 +4,65 @@
ChromeUtils.defineModuleGetter(this, "gDevTools",
"resource://devtools/client/framework/gDevTools.jsm");
ChromeUtils.defineModuleGetter(this, "devtools",
"resource://devtools/shared/Loader.jsm");
ChromeUtils.defineModuleGetter(this, "ContentTaskUtils",
"resource://testing-common/ContentTaskUtils.jsm");
+/* globals getExtensionSidebarActors, expectNoSuchActorIDs, testSetExpressionSidebarPanel */
+
+// Import the shared test helpers from the related devtools tests.
+Services.scriptloader.loadSubScript(
+ new URL("head_devtools_inspector_sidebar.js", gTestPath).href,
+ this);
+
function isActiveSidebarTabTitle(inspector, expectedTabTitle, message) {
const actualTabTitle = inspector.panelDoc.querySelector(".tabs-menu-item.is-active").innerText;
is(actualTabTitle, expectedTabTitle, message);
}
+function testSetObjectSidebarPanel(panel, expectedCellType, expectedTitle) {
+ is(panel.querySelectorAll("table.treeTable").length, 1,
+ "The sidebar panel contains a rendered TreeView component");
+
+ is(panel.querySelectorAll(`table.treeTable .${expectedCellType}Cell`).length, 1,
+ `The TreeView component contains the expected a cell of type ${expectedCellType}`);
+
+ if (expectedTitle) {
+ const panelTree = panel.querySelector("table.treeTable");
+ ok(
+ panelTree.innerText.includes(expectedTitle),
+ "The optional root object title has been included in the object tree"
+ );
+ }
+}
+
+async function testSidebarPanelSelect(extension, inspector, tabId, expected) {
+ const {
+ sidebarShown,
+ sidebarHidden,
+ activeSidebarTabTitle,
+ } = expected;
+
+ inspector.sidebar.show(tabId);
+
+ const shown = await extension.awaitMessage("devtools_sidebar_shown");
+ is(shown, sidebarShown, "Got the shown event on the second extension sidebar");
+
+ if (sidebarHidden) {
+ const hidden = await extension.awaitMessage("devtools_sidebar_hidden");
+ is(hidden, sidebarHidden, "Got the hidden event on the first extension sidebar");
+ }
+
+ isActiveSidebarTabTitle(inspector, activeSidebarTabTitle,
+ "Got the expected title on the active sidebar tab");
+}
+
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");
@@ -29,24 +73,31 @@ add_task(async function test_devtools_pa
sidebar1.onShown.addListener(() => onShownListener("shown", "sidebar1"));
sidebar2.onShown.addListener(() => onShownListener("shown", "sidebar2"));
sidebar3.onShown.addListener(() => onShownListener("shown", "sidebar3"));
sidebar1.onHidden.addListener(() => onShownListener("hidden", "sidebar1"));
sidebar2.onHidden.addListener(() => onShownListener("hidden", "sidebar2"));
sidebar3.onHidden.addListener(() => onShownListener("hidden", "sidebar3"));
- 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");
+ const expression = `
+ var obj = Object.create(null);
+ obj.prop1 = 123;
+ obj[Symbol('sym1')] = 456;
+ obj.cyclic = obj;
+ obj;
+ `;
+ sidebar1.setExpression(expression, "sidebar.setExpression rootTitle");
});
+ sidebar2.setObject({anotherPropertyName: 123});
+ sidebar3.setObject({propertyName: "propertyValue"}, "Optional Root Object Title");
+
browser.test.sendMessage("devtools_page_loaded");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
devtools_page: "devtools_page.html",
},
files: {
@@ -75,105 +126,88 @@ add_task(async function test_devtools_pa
const waitInspector = toolbox.once("inspector-selected");
toolbox.selectTool("inspector");
await waitInspector;
const sidebarIds = Array.from(toolbox._inspectorExtensionSidebars.keys());
const inspector = await toolbox.getPanel("inspector");
+ info("Test extension inspector sidebar 1 (sidebar.setExpression)");
+
inspector.sidebar.show(sidebarIds[0]);
const shownSidebarInstance = await extension.awaitMessage("devtools_sidebar_shown");
is(shownSidebarInstance, "sidebar1", "Got the shown event on the first extension sidebar");
isActiveSidebarTabTitle(inspector, "Test Sidebar 1",
"Got the expected title on the active sidebar tab");
const sidebarPanel1 = inspector.sidebar.getTabPanel(sidebarIds[0]);
ok(sidebarPanel1, "Got a rendered sidebar panel for the first registered extension sidebar");
- is(sidebarPanel1.querySelectorAll("table.treeTable").length, 1,
- "The first sidebar panel contains a rendered TreeView component");
+ info("Waiting for the first panel to be rendered");
- is(sidebarPanel1.querySelectorAll("table.treeTable .stringCell").length, 1,
- "The TreeView component contains the expected number of string cells.");
+ // Verify that the panel contains an ObjectInspector, with the expected number of nodes
+ // and with the expected property names.
+ await testSetExpressionSidebarPanel(sidebarPanel1, {
+ nodesLength: 4,
+ propertiesNames: ["cyclic", "prop1", "Symbol(sym1)"],
+ rootTitle: "sidebar.setExpression rootTitle",
+ });
- const sidebarPanel1Tree = sidebarPanel1.querySelector("table.treeTable");
- ok(
- sidebarPanel1Tree.innerText.includes("Optional Root Object Title"),
- "The optional root object title has been included in the object tree"
- );
+ // Retrieve the actors currently rendered into the extension sidebars.
+ const actors = getExtensionSidebarActors(inspector);
- inspector.sidebar.show(sidebarIds[1]);
+ info("Test extension inspector sidebar 2 (sidebar.setObject without a root title)");
- const shownSidebarInstance2 = await extension.awaitMessage("devtools_sidebar_shown");
- const hiddenSidebarInstance1 = await extension.awaitMessage("devtools_sidebar_hidden");
-
- is(shownSidebarInstance2, "sidebar2", "Got the shown event on the second extension sidebar");
- is(hiddenSidebarInstance1, "sidebar1", "Got the hidden event on the first extension sidebar");
-
- isActiveSidebarTabTitle(inspector, "Test Sidebar 2",
- "Got the expected title on the active sidebar tab");
+ await testSidebarPanelSelect(extension, inspector, sidebarIds[1], {
+ sidebarShown: "sidebar2",
+ sidebarHidden: "sidebar1",
+ activeSidebarTabTitle: "Test Sidebar 2",
+ });
const sidebarPanel2 = inspector.sidebar.getTabPanel(sidebarIds[1]);
ok(sidebarPanel2, "Got a rendered sidebar panel for the second registered extension sidebar");
- is(sidebarPanel2.querySelectorAll("table.treeTable").length, 1,
- "The second sidebar panel contains a rendered TreeView component");
+ testSetObjectSidebarPanel(sidebarPanel2, "number");
- is(sidebarPanel2.querySelectorAll("table.treeTable .numberCell").length, 1,
- "The TreeView component contains the expected a cell of type number.");
-
- inspector.sidebar.show(sidebarIds[2]);
+ info("Test extension inspector sidebar 3 (sidebar.setObject with a root title)");
- const shownSidebarInstance3 = await extension.awaitMessage("devtools_sidebar_shown");
- const hiddenSidebarInstance2 = await extension.awaitMessage("devtools_sidebar_hidden");
-
- is(shownSidebarInstance3, "sidebar3", "Got the shown event on the third extension sidebar");
- is(hiddenSidebarInstance2, "sidebar2", "Got the hidden event on the second extension sidebar");
-
- isActiveSidebarTabTitle(inspector, "Test Sidebar 3",
- "Got the expected title on the active sidebar tab");
+ await testSidebarPanelSelect(extension, inspector, sidebarIds[2], {
+ sidebarShown: "sidebar3",
+ sidebarHidden: "sidebar2",
+ activeSidebarTabTitle: "Test Sidebar 3",
+ });
const sidebarPanel3 = inspector.sidebar.getTabPanel(sidebarIds[2]);
ok(sidebarPanel3, "Got a rendered sidebar panel for the third registered extension sidebar");
- info("Waiting for the third panel to be rendered");
- await ContentTaskUtils.waitForCondition(() => {
- return sidebarPanel3.querySelectorAll("table.treeTable").length > 0;
- });
-
- is(sidebarPanel3.querySelectorAll("table.treeTable").length, 1,
- "The third sidebar panel contains a rendered TreeView component");
+ testSetObjectSidebarPanel(sidebarPanel3, "string", "Optional Root Object Title");
- 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");
+ info("Unloading the extension and check that all the sidebar have been removed");
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");
is(inspector.sidebar.getTabPanel(sidebarIds[1]), undefined,
"The second registered sidebar has been removed");
is(inspector.sidebar.getTabPanel(sidebarIds[2]), undefined,
"The third registered sidebar has been removed");
+ await expectNoSuchActorIDs(target.client, actors);
+
await gDevTools.closeToolbox(target);
await target.destroy();
await BrowserTestUtils.removeTab(tab);
});