Bug 1398729 - Support devtools.panels.elements sidebar.setExpression API method. draft
authorLuca Greco <lgreco@mozilla.com>
Sat, 09 Sep 2017 16:39:17 +0200
changeset 664879 a8562fa11eeffdb3e12120581ceab1b92e3f0005
parent 664878 dea04abd833221d63faa5e23a9b8a32d477f1daf
child 731573 cbe0a64f19e35dc4a1bf9c7e0dc663a61c6a7f74
push id79837
push userluca.greco@alcacoop.it
push dateThu, 14 Sep 2017 14:30:47 +0000
bugs1398729
milestone57.0a1
Bug 1398729 - Support devtools.panels.elements sidebar.setExpression API method. MozReview-Commit-ID: 2tRLF59o8Bg
browser/components/extensions/ext-c-devtools-panels.js
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/schemas/devtools_panels.json
browser/components/extensions/test/browser/browser_ext_devtools_panels_elements_sidebar.js
--- a/browser/components/extensions/ext-c-devtools-panels.js
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -217,16 +217,25 @@ class ChildDevToolsInspectorSidebar exte
       setObject(jsonObject, rootTitle) {
         return context.cloneScope.Promise.resolve().then(() => {
           return context.childManager.callParentAsyncFunction(
             "devtools.panels.elements.Sidebar.setObject",
             [id, jsonObject, rootTitle]
           );
         });
       },
+
+      setExpression(evalExpression, rootTitle) {
+        return context.cloneScope.Promise.resolve().then(() => {
+          return context.childManager.callParentAsyncFunction(
+            "devtools.panels.elements.Sidebar.setExpression",
+            [id, evalExpression, rootTitle]
+          );
+        });
+      },
     };
   }
 }
 
 this.devtools_panels = class extends ExtensionAPI {
   getAPI(context) {
     const themeChangeObserver = ExtensionChildDevToolsUtils.getThemeChangeObserver();
 
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -439,16 +439,27 @@ class ParentDevToolsInspectorSidebar {
     }
   }
 }
 
 const sidebarsById = new Map();
 
 this.devtools_panels = class extends ExtensionAPI {
   getAPI(context) {
+    // Lazily retrieved inspectedWindow actor front per child context
+    // (used by Sidebar.setExpression).
+    let waitForInspectedWindowFront;
+
+    // TODO(rpl): retrive a more detailed callerInfo object, like the filename and
+    // lineNumber of the actual extension called, in the child process.
+    const callerInfo = {
+      addonId: context.extension.id,
+      url: context.extension.baseURI.spec,
+    };
+
     // An incremental "per context" id used in the generated devtools panel id.
     let nextPanelId = 0;
 
     const toolboxSelectionObserver = new DevToolsSelectionObserver(context);
 
     function newBasePanelId() {
       return `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
     }
@@ -487,16 +498,37 @@ this.devtools_panels = class extends Ext
             // 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: {
               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) {
+                  waitForInspectedWindowFront = getInspectedWindowFront(context);
+                }
+
+                const front = await waitForInspectedWindowFront;
+                const evalOptions = Object.assign({}, 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);
+              },
             },
           },
           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 : "";
--- a/browser/components/extensions/schemas/devtools_panels.json
+++ b/browser/components/extensions/schemas/devtools_panels.json
@@ -175,17 +175,16 @@
                 "name": "height",
                 "type": "string",
                 "description": "A CSS-like size specification, such as <code>'100px'</code> or <code>'12ex'</code>."
               }
             ]
           },
           {
             "name": "setExpression",
-            "unsupported": true,
             "async": "callback",
             "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."
--- 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
@@ -1,41 +1,52 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource://devtools/client/framework/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource://devtools/shared/Loader.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ContentTaskUtils",
+                                  "resource://testing-common/ContentTaskUtils.jsm");
 
 function isActiveSidebarTabTitle(inspector, expectedTabTitle, message) {
   const actualTabTitle = inspector.panelDoc.querySelector(".tabs-menu-item.is-active").innerText;
   is(actualTabTitle, expectedTabTitle, message);
 }
 
 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 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"));
+
     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");
+    });
+
     browser.test.sendMessage("devtools_page_loaded");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       devtools_page: "devtools_page.html",
     },
     files: {
@@ -110,25 +121,59 @@ add_task(async function test_devtools_pa
   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");
 
   is(sidebarPanel2.querySelectorAll("table.treeTable .numberCell").length, 1,
      "The TreeView component contains the expected a cell of type number.");
 
+  inspector.sidebar.show(sidebarIds[2]);
+
+  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");
+
+  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");
+
+  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");
+
   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 gDevTools.closeToolbox(target);
 
   await target.destroy();
 
   await BrowserTestUtils.removeTab(tab);
 });