Bug 1403130 - Allow DOMNodes and cyclic objects to be rendered with the sidebar.setExpression API method. draft
authorLuca Greco <lgreco@mozilla.com>
Thu, 12 Oct 2017 15:55:47 +0200
changeset 753018 354817de873526583de30b737a80fbcec5ffcc9a
parent 753017 c7a59fa1fb180a5f97fdd6262bf469139c11977f
push id98446
push userluca.greco@alcacoop.it
push dateFri, 09 Feb 2018 13:16:08 +0000
bugs1403130
milestone60.0a1
Bug 1403130 - Allow DOMNodes and cyclic objects to be rendered with the sidebar.setExpression API method. MozReview-Commit-ID: AjHn7KfVhas
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
--- 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-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -79,16 +79,18 @@ skip-if = (os == 'win' && !debug) # bug 
 [browser_ext_devtools_inspectedWindow.js]
 [browser_ext_devtools_inspectedWindow_eval_bindings.js]
 [browser_ext_devtools_inspectedWindow_reload.js]
 [browser_ext_devtools_network.js]
 [browser_ext_devtools_page.js]
 [browser_ext_devtools_panel.js]
 [browser_ext_devtools_panels_elements.js]
 [browser_ext_devtools_panels_elements_sidebar.js]
+support-files =
+  ../../../../../devtools/client/inspector/extensions/test/head_devtools_inspector_sidebar.js
 [browser_ext_find.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_ext_geckoProfiler_symbolicate.js]
 [browser_ext_getViews.js]
 [browser_ext_history_redirect.js]
 [browser_ext_identity_indication.js]
 [browser_ext_incognito_views.js]
 [browser_ext_incognito_popup.js]
--- 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);
 });