Bug 1300590 - Implement support for $0 and inspect bindings in devtools.inspectedWindow.eval. r=aswan,ochameau
authorLuca Greco <lgreco@mozilla.com>
Wed, 15 Feb 2017 14:54:50 +0100
changeset 362153 99c6dd76dbc8eeff4f94df562e8f08a5fd30819f
parent 362152 6ddc4c39388edbae82fd505fb8a76c2a6c63b786
child 362154 0403297b318b4a77d9b9b4eccc28e20249ac3d0b
push id44032
push userluca.greco@alcacoop.it
push dateSat, 03 Jun 2017 17:38:27 +0000
treeherderautoland@0403297b318b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan, ochameau
bugs1300590
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1300590 - Implement support for $0 and inspect bindings in devtools.inspectedWindow.eval. r=aswan,ochameau 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");
@@ -2212,16 +2219,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;
     }
@@ -2295,16 +2329,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;
   },