Bug 1300590 - Implement support for $0 and inspect bindings in devtools.inspectedWindow.eval. r=ochameau,aswan draft
authorLuca Greco <lgreco@mozilla.com>
Wed, 15 Feb 2017 14:54:50 +0100
changeset 588673 9284d9a3ea6604501a1f6ed79390ddd61ad9b43c
parent 588672 98f1390029f9bd558de991a53c92342ac0addfc4
child 588674 fd986bb89188fe065a55e25ede6d1b201865e350
push id62107
push userluca.greco@alcacoop.it
push dateSat, 03 Jun 2017 17:26:53 +0000
reviewersochameau, aswan
bugs1300590
milestone55.0a1
Bug 1300590 - Implement support for $0 and inspect bindings in devtools.inspectedWindow.eval. r=ochameau,aswan MozReview-Commit-ID: CHuc57tfgzo
browser/components/extensions/ext-devtools-inspectedWindow.js
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
devtools/client/framework/target.js
devtools/client/framework/toolbox.js
devtools/client/inspector/inspector.js
devtools/client/webconsole/jsterm.js
devtools/server/actors/webconsole.js
devtools/server/actors/webextension-inspected-window.js
devtools/server/main.js
devtools/server/tests/browser/browser_navigateEvents.js
devtools/server/tests/mochitest/inspector-helpers.js
devtools/server/tests/mochitest/test_animation_actor-lifetime.html
devtools/server/tests/mochitest/test_inspector-anonymous.html
devtools/server/tests/mochitest/test_inspector-search.html
devtools/shared/client/main.js
devtools/shared/fronts/inspector.js
devtools/shared/specs/webextension-inspected-window.js
devtools/shared/webconsole/client.js
--- a/browser/components/extensions/ext-devtools-inspectedWindow.js
+++ b/browser/components/extensions/ext-devtools-inspectedWindow.js
@@ -20,37 +20,57 @@ this.devtools_inspectedWindow = class ex
       // If there is not yet a front instance, then a lazily cloned target for the context is
       // retrieved using the DevtoolsParentContextsManager helper (which is an asynchronous operation,
       // because the first time that the target has been cloned, it is not ready to be used to create
       // the front instance until it is connected to the remote debugger successfully).
       const clonedTarget = await getDevToolsTargetForContext(context);
       return new WebExtensionInspectedWindowFront(clonedTarget.client, clonedTarget.form);
     }
 
+    function getToolboxOptions() {
+      const options = {};
+      const toolbox = context.devToolsToolbox;
+      const selectedNode = toolbox.selection;
+
+      if (selectedNode && selectedNode.nodeFront) {
+        // If there is a selected node in the inspector, we hand over
+        // its actor id to the eval request in order to provide the "$0" binding.
+        options.toolboxSelectedNodeActorID = selectedNode.nodeFront.actorID;
+      }
+
+      // Provide the console actor ID to implement the "inspect" binding.
+      options.toolboxConsoleActorID = toolbox.target.form.consoleActor;
+
+      return options;
+    }
+
     // 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,
     };
 
     return {
       devtools: {
         inspectedWindow: {
           async eval(expression, options) {
             if (!waitForInspectedWindowFront) {
               waitForInspectedWindowFront = getInspectedWindowFront();
             }
 
             const front = await waitForInspectedWindowFront;
-            return front.eval(callerInfo, expression, options || {}).then(evalResult => {
-              // TODO(rpl): check for additional undocumented behaviors on chrome
-              // (e.g. if we should also print error to the console or set lastError?).
-              return new SpreadArgs([evalResult.value, evalResult.exceptionInfo]);
-            });
+
+            const evalOptions = Object.assign({}, options, getToolboxOptions());
+
+            const evalResult = await front.eval(callerInfo, expression, evalOptions);
+
+            // TODO(rpl): check for additional undocumented behaviors on chrome
+            // (e.g. if we should also print error to the console or set lastError?).
+            return new SpreadArgs([evalResult.value, evalResult.exceptionInfo]);
           },
           async reload(options) {
             const {ignoreCache, userAgent, injectedScript} = options || {};
 
             if (!waitForInspectedWindowFront) {
               waitForInspectedWindowFront = getInspectedWindowFront();
             }
 
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -56,16 +56,17 @@ skip-if = (os == 'win' && !debug) # bug 
 [browser_ext_contextMenus_commands.js]
 [browser_ext_contextMenus_icons.js]
 [browser_ext_contextMenus_onclick.js]
 [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_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_geckoProfiler_symbolicate.js]
 [browser_ext_getViews.js]
 [browser_ext_identity_indication.js]
 [browser_ext_incognito_views.js]
copy from browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
copy to browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
--- a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_bindings.js
@@ -5,122 +5,19 @@
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource://devtools/client/framework/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource://devtools/shared/Loader.jsm");
 
 /**
  * this test file ensures that:
  *
- * - the devtools page gets only a subset of the runtime API namespace.
- * - devtools.inspectedWindow.tabId is the same tabId that we can retrieve
- *   in the background page using the tabs API namespace.
- * - devtools API is available in the devtools page sub-frames when a valid
- *   extension URL has been loaded.
- * - devtools.inspectedWindow.eval:
- *   - returns a serialized version of the evaluation result.
- *   - returns the expected error object when the return value serialization raises a
- *     "TypeError: cyclic object value" exception.
- *   - returns the expected exception when an exception has been raised from the evaluated
- *     javascript code.
+ * - devtools.inspectedWindow.eval provides the expected $0 and inspect bindings
  */
-add_task(async function test_devtools_inspectedWindow_tabId() {
-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
-
-  async function background() {
-    browser.test.assertEq(undefined, browser.devtools,
-                          "No devtools APIs should be available in the background page");
-
-    const tabs = await browser.tabs.query({active: true, lastFocusedWindow: true});
-    browser.test.sendMessage("current-tab-id", tabs[0].id);
-  }
-
-  function devtools_page() {
-    browser.test.assertEq(undefined, browser.runtime.getBackgroundPage,
-      "The `runtime.getBackgroundPage` API method should be missing in a devtools_page context"
-    );
-
-    try {
-      let tabId = browser.devtools.inspectedWindow.tabId;
-      browser.test.sendMessage("inspectedWindow-tab-id", tabId);
-    } catch (err) {
-      browser.test.sendMessage("inspectedWindow-tab-id", undefined);
-      throw err;
-    }
-  }
-
-  function devtools_page_iframe() {
-    try {
-      let tabId = browser.devtools.inspectedWindow.tabId;
-      browser.test.sendMessage("devtools_page_iframe.inspectedWindow-tab-id", tabId);
-    } catch (err) {
-      browser.test.fail(`Error: ${err} :: ${err.stack}`);
-      browser.test.sendMessage("devtools_page_iframe.inspectedWindow-tab-id", undefined);
-    }
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background,
-    manifest: {
-      devtools_page: "devtools_page.html",
-    },
-    files: {
-      "devtools_page.html": `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         <iframe src="/devtools_page_iframe.html"></iframe>
-         <script src="devtools_page.js"></script>
-       </body>
-      </html>`,
-      "devtools_page.js": devtools_page,
-      "devtools_page_iframe.html": `<!DOCTYPE html>
-      <html>
-       <head>
-         <meta charset="utf-8">
-       </head>
-       <body>
-         <script src="devtools_page_iframe.js"></script>
-       </body>
-      </html>`,
-      "devtools_page_iframe.js": devtools_page_iframe,
-    },
-  });
-
-  await extension.startup();
-
-  let backgroundPageCurrentTabId = await extension.awaitMessage("current-tab-id");
-
-  let target = devtools.TargetFactory.forTab(tab);
-
-  await gDevTools.showToolbox(target, "webconsole");
-  info("developer toolbox opened");
-
-  let devtoolsInspectedWindowTabId = await extension.awaitMessage("inspectedWindow-tab-id");
-
-  is(devtoolsInspectedWindowTabId, backgroundPageCurrentTabId,
-     "Got the expected tabId from devtool.inspectedWindow.tabId");
-
-  let devtoolsPageIframeTabId = await extension.awaitMessage("devtools_page_iframe.inspectedWindow-tab-id");
-
-  is(devtoolsPageIframeTabId, backgroundPageCurrentTabId,
-     "Got the expected tabId from devtool.inspectedWindow.tabId called in a devtool_page iframe");
-
-  await gDevTools.closeToolbox(target);
-
-  await target.destroy();
-
-  await extension.unload();
-
-  await BrowserTestUtils.removeTab(tab);
-});
-
-add_task(async function test_devtools_inspectedWindow_eval() {
+add_task(async function test_devtools_inspectedWindow_eval_bindings() {
   const TEST_TARGET_URL = "http://mochi.test:8888/";
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_TARGET_URL);
 
   function devtools_page() {
     browser.test.onMessage.addListener(async (msg, ...args) => {
       if (msg !== "inspectedWindow-eval-request") {
         browser.test.fail(`Unexpected test message received: ${msg}`);
         return;
@@ -154,84 +51,101 @@ add_task(async function test_devtools_in
        </body>
       </html>`,
       "devtools_page.js": devtools_page,
     },
   });
 
   await extension.startup();
 
-  let target = devtools.TargetFactory.forTab(tab);
+  const target = devtools.TargetFactory.forTab(tab);
+  // Open the toolbox on the styleeditor, so that the inspector and the
+  // console panel have not been explicitly activated yet.
+  const toolbox = await gDevTools.showToolbox(target, "styleeditor");
+  info("Developer toolbox opened");
 
-  await gDevTools.showToolbox(target, "webconsole");
-  info("developer toolbox opened");
+  // Test $0 binding with no selected node
+  info("Test inspectedWindow.eval $0 binding with no selected node");
 
-  const evalTestCases = [
-    // Successful evaluation results.
-    {
-      args: ["window.location.href"],
-      expectedResults: {evalResult: TEST_TARGET_URL, errorResult: undefined},
-    },
+  const evalNoSelectedNodePromise = extension.awaitMessage(`inspectedWindow-eval-result`);
+  extension.sendMessage(`inspectedWindow-eval-request`, "$0");
+  const evalNoSelectedNodeResult = await evalNoSelectedNodePromise;
+
+  Assert.deepEqual(evalNoSelectedNodeResult,
+                   {evalResult: undefined, errorResult: undefined},
+                   "Got the expected eval result");
+
+  // Test $0 binding with a selected node in the inspector.
 
-    // Error evaluation results.
-    {
-      args: ["window"],
-      expectedResults: {
-        evalResult: undefined,
-        errorResult: {
-          isError: true,
-          code: "E_PROTOCOLERROR",
-          description: "Inspector protocol error: %s",
-          details: [
-            "TypeError: cyclic object value",
-          ],
-        },
-      },
-    },
+  await gDevTools.showToolbox(target, "inspector");
+  info("Toolbox switched to the inspector panel");
+
+  info("Test inspectedWindow.eval $0 binding with a selected node in the inspector");
+
+  const evalSelectedNodePromise = extension.awaitMessage(`inspectedWindow-eval-result`);
+  extension.sendMessage(`inspectedWindow-eval-request`, "$0 && $0.tagName");
+  const evalSelectedNodeResult = await evalSelectedNodePromise;
+
+  Assert.deepEqual(evalSelectedNodeResult,
+                   {evalResult: "BODY", errorResult: undefined},
+                   "Got the expected eval result");
+
+  // Test that inspect($0) switch the developer toolbox to the inspector.
+
+  await gDevTools.showToolbox(target, "styleeditor");
+
+  info("Toolbox switched back to the styleeditor panel");
+
+  const inspectorPanelSelectedPromise = (async () => {
+    const toolId = await new Promise(resolve => {
+      toolbox.once("select", (evt, toolId) => resolve(toolId));
+    });
 
-    // Exception evaluation results.
-    {
-      args: ["throw new Error('fake eval exception');"],
-      expectedResults: {
-        evalResult: undefined,
-        errorResult: {
-          isException: true,
-          value: /Error: fake eval exception\n.*moz-extension:\/\//,
-        },
-      },
+    if (toolId === "inspector") {
+      const selectedNodeName = toolbox.selection.nodeFront &&
+                               toolbox.selection.nodeFront._form.nodeName;
+      is(selectedNodeName, "HTML", "The expected DOM node has been selected in the inspector");
+    } else {
+      throw new Error(`inspector panel expected, ${toolId} has been selected instead`);
+    }
+  })();
 
-    },
-  ];
+  info("Test inspectedWindow.eval inspect() binding called for a DOM element");
+  const inspectDOMNodePromise = extension.awaitMessage(`inspectedWindow-eval-result`);
+  extension.sendMessage(`inspectedWindow-eval-request`, "inspect(document.documentElement)");
+  await inspectDOMNodePromise;
 
-  for (let testCase of evalTestCases) {
-    info(`test inspectedWindow.eval with ${JSON.stringify(testCase)}`);
+  info("Wait for the toolbox to switch to the inspector and the expected node has been selected");
+  await inspectorPanelSelectedPromise;
+  info("Toolbox has been switched to the inspector as expected");
 
-    const {args, expectedResults} = testCase;
+  info("Test inspectedWindow.eval inspect() binding called for a JS object");
 
-    extension.sendMessage(`inspectedWindow-eval-request`, ...args);
-
-    const {evalResult, errorResult} = await extension.awaitMessage(`inspectedWindow-eval-result`);
+  const splitPanelOpenedPromise = (async () => {
+    await toolbox.once("split-console");
+    let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
 
-    Assert.deepEqual(evalResult, expectedResults.evalResult, "Got the expected eval result");
-
-    if (errorResult) {
-      for (const errorPropName of Object.keys(expectedResults.errorResult)) {
-        const expected = expectedResults.errorResult[errorPropName];
-        const actual = errorResult[errorPropName];
+    const options = await new Promise(resolve => {
+      jsterm.once("variablesview-open", (evt, view, options) => resolve(options));
+    });
 
-        if (expected instanceof RegExp) {
-          ok(expected.test(actual),
-             `Got exceptionInfo.${errorPropName} value ${actual} matches ${expected}`);
-        } else {
-          Assert.deepEqual(actual, expected,
-                           `Got the expected exceptionInfo.${errorPropName} value`);
-        }
-      }
-    }
-  }
+    const objectType = options.objectActor.type;
+    const objectPreviewProperties = options.objectActor.preview.ownProperties;
+    is(objectType, "object", "The inspected object has the expected type");
+    Assert.deepEqual(Object.keys(objectPreviewProperties), ["testkey"],
+                     "The inspected object has the expected preview properties");
+  })();
+
+  const inspectJSObjectPromise = extension.awaitMessage(`inspectedWindow-eval-result`);
+  extension.sendMessage(`inspectedWindow-eval-request`, "inspect({testkey: 'testvalue'})");
+  await inspectJSObjectPromise;
+
+  info("Wait for the split console to be opened and the JS object inspected");
+  await splitPanelOpenedPromise;
+  info("Split console has been opened as expected");
 
   await gDevTools.closeToolbox(target);
 
   await target.destroy();
 
   await extension.unload();
 
   await BrowserTestUtils.removeTab(tab);
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -451,16 +451,20 @@ TabTarget.prototype = {
     };
 
     let onConsoleAttached = (response, consoleClient) => {
       if (!consoleClient) {
         this._remote.reject("Unable to attach to the console");
         return;
       }
       this.activeConsole = consoleClient;
+
+      this._onInspectObject = (event, packet) => this.emit("inspect-object", packet);
+      this.activeConsole.on("inspectObject", this._onInspectObject);
+
       this._remote.resolve(null);
     };
 
     let attachConsole = () => {
       this._client.attachConsole(this._form.consoleActor, [], onConsoleAttached);
     };
 
     if (this.isLocalTab) {
@@ -571,16 +575,19 @@ TabTarget.prototype = {
    */
   _teardownRemoteListeners: function () {
     this.client.removeListener("closed", this.destroy);
     this.client.removeListener("tabNavigated", this._onTabNavigated);
     this.client.removeListener("tabDetached", this._onTabDetached);
     this.client.removeListener("frameUpdate", this._onFrameUpdate);
     this.client.removeListener("newSource", this._onSourceUpdated);
     this.client.removeListener("updatedSource", this._onSourceUpdated);
+    if (this.activeConsole && this._onInspectObject) {
+      this.activeConsole.off("inspectObject", this._onInspectObject);
+    }
   },
 
   /**
    * Handle tabs events.
    */
   handleEvent: function (event) {
     switch (event.type) {
       case "TabClose":
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -62,16 +62,20 @@ loader.lazyRequireGetter(this, "ToolboxB
   "devtools/client/definitions", true);
 loader.lazyRequireGetter(this, "SourceMapURLService",
   "devtools/client/framework/source-map-url-service", true);
 loader.lazyRequireGetter(this, "HUDService",
   "devtools/client/webconsole/hudservice");
 loader.lazyRequireGetter(this, "viewSource",
   "devtools/client/shared/view-source");
 
+loader.lazyGetter(this, "domNodeConstants", () => {
+  return require("devtools/shared/dom-node-constants");
+});
+
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
 
 /**
  * A "Toolbox" is the component that holds all the tools for one specific
  * target. Visually, it's a document that includes the tools tabs and all
  * the iframes where the tool panels will be living in.
@@ -129,16 +133,17 @@ function Toolbox(target, selectedTool, h
   this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
   this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
   this._onToolbarFocus = this._onToolbarFocus.bind(this);
   this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this);
   this._onPickerClick = this._onPickerClick.bind(this);
   this._onPickerKeypress = this._onPickerKeypress.bind(this);
   this._onPickerStarted = this._onPickerStarted.bind(this);
   this._onPickerStopped = this._onPickerStopped.bind(this);
+  this._onInspectObject = this._onInspectObject.bind(this);
   this.selectTool = this.selectTool.bind(this);
 
   this._target.on("close", this.destroy);
 
   if (!selectedTool) {
     selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
   }
   this._defaultToolId = selectedTool;
@@ -147,16 +152,17 @@ function Toolbox(target, selectedTool, h
 
   this._isOpenDeferred = defer();
   this.isOpen = this._isOpenDeferred.promise;
 
   EventEmitter.decorate(this);
 
   this._target.on("navigate", this._refreshHostTitle);
   this._target.on("frame-update", this._updateFrames);
+  this._target.on("inspect-object", this._onInspectObject);
 
   this.on("host-changed", this._refreshHostTitle);
   this.on("select", this._refreshHostTitle);
 
   this.on("ready", this._showDevEditionPromo);
 
   gDevTools.on("tool-registered", this._toolRegistered);
   gDevTools.on("tool-unregistered", this._toolUnregistered);
@@ -417,16 +423,17 @@ Toolbox.prototype = {
         ]);
       }
 
       // Attach the thread
       this._threadClient = yield attachThread(this);
       yield domReady.promise;
 
       this.isReady = true;
+
       let framesPromise = this._listFrames();
 
       Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings);
       Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
                                  this._applyServiceWorkersTestingSettings);
 
       this.textBoxContextMenuPopup =
         this.doc.getElementById("toolbox-textbox-context-popup");
@@ -2213,16 +2220,43 @@ Toolbox.prototype = {
           let autohide = !flags.testing;
           this._highlighter = yield this._inspector.getHighlighter(autohide);
         }
       }.bind(this));
     }
     return this._initInspector;
   },
 
+  _onInspectObject: function (evt, packet) {
+    this.inspectObjectActor(packet.objectActor, packet.inspectFromAnnotation);
+  },
+
+  inspectObjectActor: async function (objectActor, inspectFromAnnotation) {
+    if (objectActor.preview &&
+        objectActor.preview.nodeType === domNodeConstants.ELEMENT_NODE) {
+      // Open the inspector and select the DOM Element.
+      await this.loadTool("inspector");
+      const inspector = await this.getPanel("inspector");
+      const nodeFound = await inspector.inspectNodeActor(objectActor.actor,
+                                                         inspectFromAnnotation);
+      if (nodeFound) {
+        await this.selectTool("inspector");
+      }
+    } else if (objectActor.type !== "null" &&
+               objectActor.type !== "undefined") {
+      // Open then split console and inspect the object in the variables view,
+      // when the objectActor doesn't represent an undefined or null value.
+      await this.openSplitConsole();
+      const panel = this.getPanel("webconsole");
+      const jsterm = panel.hud.jsterm;
+
+      jsterm.inspectObjectActor(objectActor);
+    }
+  },
+
   /**
    * Destroy the inspector/walker/selection fronts
    * Returns a promise that resolves when the fronts are destroyed
    */
   destroyInspector: function () {
     if (this._destroyingInspector) {
       return this._destroyingInspector;
     }
@@ -2296,16 +2330,17 @@ Toolbox.prototype = {
     if (this._destroyer) {
       return this._destroyer;
     }
     let deferred = defer();
     this._destroyer = deferred.promise;
 
     this.emit("destroy");
 
+    this._target.off("inspect-object", this._onInspectObject);
     this._target.off("navigate", this._refreshHostTitle);
     this._target.off("frame-update", this._updateFrames);
     this.off("select", this._refreshHostTitle);
     this.off("host-changed", this._refreshHostTitle);
     this.off("ready", this._showDevEditionPromo);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -2001,16 +2001,34 @@ Inspector.prototype = {
    *         The node to highlight.
    * @param  {Object} options
    *         Options passed to the highlighter actor.
    */
   onShowBoxModelHighlighterForNode(nodeFront, options) {
     let toolbox = this.toolbox;
     toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
+
+  async inspectNodeActor(nodeActor, inspectFromAnnotation) {
+    const nodeFront = await this.walker.getNodeActorFromObjectActor(nodeActor);
+    if (!nodeFront) {
+      console.error("The object cannot be linked to the inspector, the " +
+                    "corresponding nodeFront could not be found.");
+      return false;
+    }
+
+    let isAttached = await this.walker.isInDOMTree(nodeFront);
+    if (!isAttached) {
+      console.error("Selected DOMNode is not attached to the document tree.");
+      return false;
+    }
+
+    await this.selection.setNodeFront(nodeFront, inspectFromAnnotation);
+    return true;
+  },
 };
 
 /**
  * Create a fake toolbox when running the inspector standalone, either in a chrome tab or
  * in a content tab.
  *
  * @param {Target} target to debug
  * @param {Function} createThreadClient
--- a/devtools/client/webconsole/jsterm.js
+++ b/devtools/client/webconsole/jsterm.js
@@ -337,21 +337,17 @@ JSTerm.prototype = {
       switch (helperResult.type) {
         case "clearOutput":
           this.clearOutput();
           break;
         case "clearHistory":
           this.clearHistory();
           break;
         case "inspectObject":
-          this.openVariablesView({
-            label:
-              VariablesView.getString(helperResult.object, { concise: true }),
-            objectActor: helperResult.object,
-          });
+          this.inspectObjectActor(helperResult.object);
           break;
         case "error":
           try {
             errorMessage = l10n.getStr(helperResult.message);
           } catch (ex) {
             errorMessage = helperResult.message;
           }
           break;
@@ -400,16 +396,23 @@ JSTerm.prototype = {
       msg._objectActors.add(response.exception.actor);
     }
 
     if (WebConsoleUtils.isActorGrip(result)) {
       msg._objectActors.add(result.actor);
     }
   },
 
+  inspectObjectActor: function (objectActor) {
+    return this.openVariablesView({
+      objectActor,
+      label: VariablesView.getString(objectActor, {concise: true}),
+    });
+  },
+
   /**
    * Execute a string. Execution happens asynchronously in the content process.
    *
    * @param string [executeString]
    *        The string you want to execute. If this is not provided, the current
    *        user input is used - taken from |this.getInputValue()|.
    * @param function [callback]
    *        Optional function to invoke when the result is displayed.
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -535,16 +535,27 @@ WebConsoleActor.prototype =
    * This is undefined if no evaluations have been completed.
    *
    * @return object
    */
   getLastConsoleInputEvaluation: function () {
     return this._lastConsoleInputEvaluation;
   },
 
+  /**
+   * This helper is used by the WebExtensionInspectedWindowActor to
+   * inspect an object in the developer toolbox.
+   */
+  inspectObject(dbgObj, inspectFromAnnotation) {
+    this.conn.sendActorEvent(this.actorID, "inspectObject", {
+      objectActor: this.createValueGrip(dbgObj),
+      inspectFromAnnotation,
+    });
+  },
+
   // Request handlers for known packet types.
 
   /**
    * Handler for the "startListeners" request.
    *
    * @param object request
    *        The JSON request object received from the Web Console client.
    * @return object
--- a/devtools/server/actors/webextension-inspected-window.js
+++ b/devtools/server/actors/webextension-inspected-window.js
@@ -3,18 +3,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const protocol = require("devtools/shared/protocol");
 
 const {Ci, Cu, Cr} = require("chrome");
 
+const {DebuggerServer} = require("devtools/server/main");
 const Services = require("Services");
 
+loader.lazyGetter(this, "NodeActor", () => require("devtools/server/actors/inspector").NodeActor, true);
+
 const {
   XPCOMUtils,
 } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
 
 const {
   webExtensionInspectedWindowSpec,
 } = require("devtools/shared/specs/webextension-inspected-window");
 
@@ -192,16 +195,17 @@ var WebExtensionInspectedWindowActor = p
      */
     initialize(conn, tabActor) {
       protocol.Actor.prototype.initialize.call(this, conn);
       this.tabActor = tabActor;
     },
 
     destroy(conn) {
       protocol.Actor.prototype.destroy.call(this, conn);
+
       if (this.customizedReload) {
         this.customizedReload.stop(
           new Error("WebExtensionInspectedWindowActor destroyed")
         );
         delete this.customizedReload;
       }
 
       if (this._dbg) {
@@ -227,16 +231,68 @@ var WebExtensionInspectedWindowActor = p
     get window() {
       return this.tabActor.window;
     },
 
     get webNavigation() {
       return this.tabActor.webNavigation;
     },
 
+    createEvalBindings(dbgWindow, options) {
+      const bindings = Object.create(null);
+
+      let selectedDOMNode;
+
+      if (options.toolboxSelectedNodeActorID) {
+        let actor = DebuggerServer.searchAllConnectionsForActor(
+          options.toolboxSelectedNodeActorID
+        );
+        if (actor && actor instanceof NodeActor) {
+          selectedDOMNode = actor.rawNode;
+        }
+      }
+
+      Object.defineProperty(bindings, "$0", {
+        enumerable: true,
+        configurable: true,
+        get: () => {
+          if (selectedDOMNode && !Cu.isDeadWrapper(selectedDOMNode)) {
+            return dbgWindow.makeDebuggeeValue(selectedDOMNode);
+          }
+
+          return undefined;
+        },
+      });
+
+      // This function is used by 'eval' and 'reload' requests, but only 'eval'
+      // passes 'toolboxConsoleActor' from the client side in order to set
+      // the 'inspect' binding.
+      Object.defineProperty(bindings, "inspect", {
+        enumerable: true,
+        configurable: true,
+        value: dbgWindow.makeDebuggeeValue((object) => {
+          const dbgObj = dbgWindow.makeDebuggeeValue(object);
+
+          let consoleActor = DebuggerServer.searchAllConnectionsForActor(
+            options.toolboxConsoleActorID
+          );
+          if (consoleActor) {
+            consoleActor.inspectObject(dbgObj,
+                                       "webextension-devtools-inspectedWindow-eval");
+          } else {
+            // TODO(rpl): evaluate if it would be better to raise an exception
+            // to the caller code instead.
+            console.error("Toolbox Console RDP Actor not found");
+          }
+        }),
+      });
+
+      return bindings;
+    },
+
     /**
      * Reload the target tab, optionally bypass cache, customize the userAgent and/or
      * inject a script in targeted document or any of its sub-frame.
      *
      * @param {webExtensionCallerInfo} callerInfo
      *   the addonId and the url (the addon base url or the url of the actual caller
      *   filename and lineNumber) used to log useful debugging information in the
      *   produced error logs and eval stack trace.
@@ -346,29 +402,17 @@ var WebExtensionInspectedWindowActor = p
      *   Used in the CustomizedReload instances to evaluate the `injectedScript`
      *   javascript code in every sub-frame of the target window during the tab reload.
      *   NOTE: this parameter is not part of the RDP protocol exposed by this actor, when
      *   it is called over the remote debugging protocol the target window is always
      *   `tabActor.window`.
      */
     eval(callerInfo, expression, options, customTargetWindow) {
       const window = customTargetWindow || this.window;
-
-      if (Object.keys(options).length > 0) {
-        return {
-          exceptionInfo: {
-            isError: true,
-            code: "E_PROTOCOLERROR",
-            description: "Inspector protocol error: %s",
-            details: [
-              "The inspectedWindow.eval options are currently not supported",
-            ],
-          },
-        };
-      }
+      options = options || {};
 
       if (!window) {
         return {
           exceptionInfo: {
             isError: true,
             code: "E_PROTOCOLERROR",
             description: "Inspector protocol error: %s",
             details: [
@@ -389,24 +433,41 @@ var WebExtensionInspectedWindowActor = p
             description: "Inspector protocol error: %s",
             details: [
               "This target has a system principal. inspectedWindow.eval denied.",
             ],
           },
         };
       }
 
+      // Raise an error on the unsupported options.
+      if (options.frameURL || options.contextSecurityOrigin ||
+          options.useContentScriptContext) {
+        return {
+          exceptionInfo: {
+            isError: true,
+            code: "E_PROTOCOLERROR",
+            description: "Inspector protocol error: %s",
+            details: [
+              "The inspectedWindow.eval options are currently not supported",
+            ],
+          },
+        };
+      }
+
       const dbgWindow = this.dbg.makeGlobalObjectReference(window);
 
       let evalCalledFrom = callerInfo.url;
       if (callerInfo.lineNumber) {
         evalCalledFrom += `:${callerInfo.lineNumber}`;
       }
-      // TODO(rpl): add $0 and inspect(...) bindings (Bug 1300590)
-      const result = dbgWindow.executeInGlobalWithBindings(expression, {}, {
+
+      const bindings = this.createEvalBindings(dbgWindow, options);
+
+      const result = dbgWindow.executeInGlobalWithBindings(expression, bindings, {
         url: `debugger eval called from ${evalCalledFrom} - eval code`,
       });
 
       let evalResult;
 
       if (result) {
         if ("return" in result) {
           evalResult = result.return;
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -1378,35 +1378,32 @@ var DebuggerServer = {
         for (let connID of Object.getOwnPropertyNames(this._connections)) {
           this._connections[connID].rootActor.removeActorByName(name);
         }
       }
     }
   },
 
   /**
-   * Called when DevTools are unloaded to remove the contend process server script for the
-   * list of scripts loaded for each new content process. Will also remove message
-   * listeners from already loaded scripts.
+   * Searches all active connections for an actor matching an ID.
+   *
+   * ⚠ TO BE USED ONLY FROM SERVER CODE OR TESTING ONLY! ⚠`
+   *
+   * This is helpful for some tests which depend on reaching into the server to check some
+   * properties of an actor, and it is also used by the actors related to the
+   * DevTools WebExtensions API to be able to interact with the actors created for the
+   * panels natively provided by the DevTools Toolbox.
    */
-  removeContentServerScript() {
-    Services.ppmm.removeDelayedProcessScript(CONTENT_PROCESS_DBG_SERVER_SCRIPT);
-    try {
-      Services.ppmm.broadcastAsyncMessage("debug:close-content-server");
-    } catch (e) {
-      // Nothing to do
-    }
-  },
-
-  /**
-   * ⚠ TESTING ONLY! ⚠ Searches all active connections for an actor matching an ID.
-   * This is helpful for some tests which depend on reaching into the server to check some
-   * properties of an actor.
-   */
-  _searchAllConnectionsForActor(actorID) {
+  searchAllConnectionsForActor(actorID) {
+    // NOTE: the actor IDs are generated with the following format:
+    //
+    //   `server${loaderID}.conn${ConnectionID}${ActorPrefix}${ActorID}`
+    //
+    // as an optimization we can come up with a regexp to query only
+    // the right connection via its id.
     for (let connID of Object.getOwnPropertyNames(this._connections)) {
       let actor = this._connections[connID].getActor(actorID);
       if (actor) {
         return actor;
       }
     }
     return null;
   },
--- a/devtools/server/tests/browser/browser_navigateEvents.js
+++ b/devtools/server/tests/browser/browser_navigateEvents.js
@@ -105,17 +105,17 @@ function getServerTabActor(callback) {
 
   // Connect to this tab
   let transport = DebuggerServer.connectPipe();
   client = new DebuggerClient(transport);
   connectDebuggerClient(client).then(form => {
     let actorID = form.actor;
     client.attachTab(actorID, function (response, tabClient) {
       // !Hack! Retrieve a server side object, the BrowserTabActor instance
-      let tabActor = DebuggerServer._searchAllConnectionsForActor(actorID);
+      let tabActor = DebuggerServer.searchAllConnectionsForActor(actorID);
       callback(tabActor);
     });
   });
 
   client.addListener("tabNavigated", function (event, packet) {
     assertEvent("tabNavigated", packet);
   });
 }
--- a/devtools/server/tests/mochitest/inspector-helpers.js
+++ b/devtools/server/tests/mochitest/inspector-helpers.js
@@ -122,17 +122,17 @@ function serverOwnershipSubtree(walker, 
   }
   return {
     name: actor.actorID,
     children: sortOwnershipChildren(children)
   };
 }
 
 function serverOwnershipTree(walker) {
-  let serverWalker = DebuggerServer._searchAllConnectionsForActor(walker.actorID);
+  let serverWalker = DebuggerServer.searchAllConnectionsForActor(walker.actorID);
 
   return {
     root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc),
     orphaned: [...serverWalker._orphaned]
               .map(o => serverOwnershipSubtree(serverWalker, o.rawNode)),
     retained: [...serverWalker._retainedOrphans]
               .map(o => serverOwnershipSubtree(serverWalker, o.rawNode))
   };
--- a/devtools/server/tests/mochitest/test_animation_actor-lifetime.html
+++ b/devtools/server/tests/mochitest/test_animation_actor-lifetime.html
@@ -40,17 +40,17 @@ window.onload = function () {
 
   addAsyncTest(function* testActorLifetime() {
     info("Testing animated node actor");
     let animatedNodeActor = yield gWalker.querySelector(gWalker.rootNode,
       ".animated");
     yield animationsFront.getAnimationPlayersForNode(animatedNodeActor);
 
     let animationsActor = DebuggerServer
-                          ._searchAllConnectionsForActor(animationsFront.actorID);
+                          .searchAllConnectionsForActor(animationsFront.actorID);
 
     is(animationsActor.actors.length, 1,
       "AnimationActor have 1 AnimationPlayerActors");
 
     info("Testing AnimationPlayerActors release");
     let stillNodeActor = yield gWalker.querySelector(gWalker.rootNode,
       ".still");
     yield animationsFront.getAnimationPlayersForNode(stillNodeActor);
--- a/devtools/server/tests/mochitest/test_inspector-anonymous.html
+++ b/devtools/server/tests/mochitest/test_inspector-anonymous.html
@@ -68,17 +68,17 @@ window.onload = function () {
     is(children.nodes.length, 2, "No native anon content for form control");
 
     runNextTest();
   });
 
   addAsyncTest(function* testNativeAnonymousStartingNode() {
     info("Tests attaching an element that a walker can't see.");
 
-    let serverWalker = DebuggerServer._searchAllConnectionsForActor(gWalker.actorID);
+    let serverWalker = DebuggerServer.searchAllConnectionsForActor(gWalker.actorID);
     let docwalker = new _documentWalker(
       gInspectee.querySelector("select"),
       gInspectee.defaultView,
       nodeFilterConstants.SHOW_ALL,
       () => {
         return nodeFilterConstants.FILTER_ACCEPT;
       }
     );
--- a/devtools/server/tests/mochitest/test_inspector-search.html
+++ b/devtools/server/tests/mochitest/test_inspector-search.html
@@ -43,17 +43,17 @@ window.onload = function () {
         inspector = InspectorFront(client, tab);
         resolve();
       });
     });
 
     let walkerFront = yield inspector.getWalker();
     ok(walkerFront, "getWalker() should return an actor.");
 
-    walkerActor = DebuggerServer._searchAllConnectionsForActor(walkerFront.actorID);
+    walkerActor = DebuggerServer.searchAllConnectionsForActor(walkerFront.actorID);
     ok(walkerActor,
       "Got a reference to the walker actor (" + walkerFront.actorID + ")");
 
     walkerSearch = walkerActor.walkerSearch;
 
     runNextTest();
   });
 
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -175,16 +175,17 @@ const UnsolicitedNotifications = {
   "exitedFrame": "exitedFrame",
   "appOpen": "appOpen",
   "appClose": "appClose",
   "appInstall": "appInstall",
   "appUninstall": "appUninstall",
   "evaluationResult": "evaluationResult",
   "newSource": "newSource",
   "updatedSource": "updatedSource",
+  "inspectObject": "inspectObject"
 };
 
 /**
  * Set of pause types that are sent by the server and not as an immediate
  * response to a client request.
  */
 const UnsolicitedPauses = {
   "resumeLimit": "resumeLimit",
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -427,17 +427,17 @@ const NodeFront = FrontClassWithSpec(nod
    * protocol.  If you depend on this you're likely to break soon.
    */
   rawNode: function (rawNode) {
     if (!this.isLocalToBeDeprecated()) {
       console.warn("Tried to use rawNode on a remote connection.");
       return null;
     }
     const { DebuggerServer } = require("devtools/server/main");
-    let actor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
+    let actor = DebuggerServer.searchAllConnectionsForActor(this.actorID);
     if (!actor) {
       // Can happen if we try to get the raw node for an already-expired
       // actor.
       return null;
     }
     return actor.rawNode;
   }
 });
@@ -902,17 +902,17 @@ const WalkerFront = FrontClassWithSpec(w
   // XXX hack during transition to remote inspector: get a proper NodeFront
   // for a given local node.  Only works locally.
   frontForRawNode: function (rawNode) {
     if (!this.isLocal()) {
       console.warn("Tried to use frontForRawNode on a remote connection.");
       return null;
     }
     const { DebuggerServer } = require("devtools/server/main");
-    let walkerActor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
+    let walkerActor = DebuggerServer.searchAllConnectionsForActor(this.actorID);
     if (!walkerActor) {
       throw Error("Could not find client side for actor " + this.actorID);
     }
     let nodeActor = walkerActor._ref(rawNode);
 
     // Pass the node through a read/write pair to create the client side actor.
     let nodeType = types.getType("domnode");
     let returnNode = nodeType.read(
@@ -922,17 +922,17 @@ const WalkerFront = FrontClassWithSpec(w
     for (let extraActor of extras) {
       top = nodeType.read(nodeType.write(extraActor, walkerActor), this);
     }
 
     if (top !== this.rootNode) {
       // Imported an already-orphaned node.
       this._orphaned.add(top);
       walkerActor._orphaned
-        .add(DebuggerServer._searchAllConnectionsForActor(top.actorID));
+        .add(DebuggerServer.searchAllConnectionsForActor(top.actorID));
     }
     return returnNode;
   },
 
   removeNode: custom(Task.async(function* (node) {
     let previousSibling = yield this.previousSibling(node);
     let nextSibling = yield this._removeNode(node);
     return {
--- a/devtools/shared/specs/webextension-inspected-window.js
+++ b/devtools/shared/specs/webextension-inspected-window.js
@@ -31,16 +31,24 @@ types.addDictType("webExtensionCallerInf
 
 /**
  * RDP type related to the inspectedWindow.eval method request.
  */
 types.addDictType("webExtensionEvalOptions", {
   frameURL: "nullable:string",
   contextSecurityOrigin: "nullable:string",
   useContentScriptContext: "nullable:boolean",
+
+  // The actor ID of the node selected in the inspector if any,
+  // used to provide the '$0' binding.
+  toolboxSelectedNodeActorID: "nullable:string",
+
+  // The actor ID of the console actor,
+  // used to provide the 'inspect' binding.
+  toolboxConsoleActorID: "nullable:string",
 });
 
 /**
  * RDP type related to the inspectedWindow.eval method result errors.
  *
  * This type has been modelled on the same data format
  * used in the corresponding chrome API method.
  */
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -29,20 +29,22 @@ function WebConsoleClient(debuggerClient
   this.traits = response.traits || {};
   this.events = [];
   this._networkRequests = new Map();
 
   this.pendingEvaluationResults = new Map();
   this.onEvaluationResult = this.onEvaluationResult.bind(this);
   this.onNetworkEvent = this._onNetworkEvent.bind(this);
   this.onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+  this.onInspectObject = this._onInspectObject.bind(this);
 
   this._client.addListener("evaluationResult", this.onEvaluationResult);
   this._client.addListener("networkEvent", this.onNetworkEvent);
   this._client.addListener("networkEventUpdate", this.onNetworkEventUpdate);
+  this._client.addListener("inspectObject", this.onInspectObject);
   EventEmitter.decorate(this);
 }
 
 exports.WebConsoleClient = WebConsoleClient;
 
 WebConsoleClient.prototype = {
   _longStrings: null,
   traits: null,
@@ -169,16 +171,30 @@ WebConsoleClient.prototype = {
 
     this.emit("networkEventUpdate", {
       packet: packet,
       networkInfo
     });
   },
 
   /**
+   * The "inspectObject" message type handler. We just re-emit it so that
+   * the toolbox can listen to the event and decide how to handle it.
+   *
+   * @private
+   * @param string type
+   *        Message type.
+   * @param object packet
+   *        The message received from the server.
+   */
+  _onInspectObject: function (type, packet) {
+    this.emit("inspectObject", packet);
+  },
+
+  /**
    * Retrieve the cached messages from the server.
    *
    * @see this.CACHED_MESSAGES
    * @param array types
    *        The array of message types you want from the server. See
    *        this.CACHED_MESSAGES for known types.
    * @param function onResponse
    *        The function invoked when the response is received.
@@ -638,16 +654,17 @@ WebConsoleClient.prototype = {
    * @param function onResponse
    *        Function to invoke when the server response is received.
    */
   detach: function (onResponse) {
     this._client.removeListener("evaluationResult", this.onEvaluationResult);
     this._client.removeListener("networkEvent", this.onNetworkEvent);
     this._client.removeListener("networkEventUpdate",
                                 this.onNetworkEventUpdate);
+    this._client.removeListener("inspectObject", this.onInspectObject);
     this.stopListeners(null, onResponse);
     this._longStrings = null;
     this._client = null;
     this.pendingEvaluationResults.clear();
     this.pendingEvaluationResults = null;
     this.clearNetworkRequests();
     this._networkRequests = null;
   },