Bug 1300584 - Implements devtools.inspectedWindow.eval. r=kmag
authorLuca Greco <lgreco@mozilla.com>
Fri, 02 Dec 2016 15:46:49 -0500
changeset 378252 c20c6b11a1dae5bc59a7f2a3c4ab286778f41b41
parent 378251 df653a73e41432ba67ea7eb65ada4aa84bbf10cb
child 378253 0a699117d21406e40b457572e8884ab80804298d
push id7198
push userjlorenzo@mozilla.com
push dateTue, 18 Apr 2017 12:07:49 +0000
treeherdermozilla-beta@d57aa49c3948 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1300584
milestone54.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 1300584 - Implements devtools.inspectedWindow.eval. r=kmag MozReview-Commit-ID: 6Z76W8tKt9x
browser/components/extensions/ext-devtools-inspectedWindow.js
browser/components/extensions/extensions-browser.manifest
browser/components/extensions/jar.mn
browser/components/extensions/schemas/devtools_inspected_window.json
browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-devtools-inspectedWindow.js
@@ -0,0 +1,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* global getDevToolsTargetForContext */
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+const {
+  SpreadArgs,
+} = ExtensionUtils;
+
+extensions.registerSchemaAPI("devtools.inspectedWindow", "devtools_parent", context => {
+  const {
+    WebExtensionInspectedWindowFront,
+  } = require("devtools/shared/fronts/webextension-inspected-window");
+
+  // Lazily retrieve and store an inspectedWindow actor front per child context.
+  let waitForInspectedWindowFront;
+  async function getInspectedWindowFront() {
+    // 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);
+  }
+
+  // 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]);
+          });
+        },
+      },
+    },
+  };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,16 +1,17 @@
 # scripts
 category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
 category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
 category webextension-scripts browsingData chrome://browser/content/ext-browsingData.js
 category webextension-scripts commands chrome://browser/content/ext-commands.js
 category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
 category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
 category webextension-scripts devtools chrome://browser/content/ext-devtools.js
+category webextension-scripts devtools-inspectedWindow chrome://browser/content/ext-devtools-inspectedWindow.js
 category webextension-scripts history chrome://browser/content/ext-history.js
 category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
 category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
 category webextension-scripts sessions chrome://browser/content/ext-sessions.js
 category webextension-scripts tabs chrome://browser/content/ext-tabs.js
 category webextension-scripts theme chrome://browser/content/ext-theme.js
 category webextension-scripts url-overrides chrome://browser/content/ext-url-overrides.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -14,16 +14,17 @@ browser.jar:
     content/browser/extension.svg
     content/browser/ext-bookmarks.js
     content/browser/ext-browserAction.js
     content/browser/ext-browsingData.js
     content/browser/ext-commands.js
     content/browser/ext-contextMenus.js
     content/browser/ext-desktop-runtime.js
     content/browser/ext-devtools.js
+    content/browser/ext-devtools-inspectedWindow.js
     content/browser/ext-history.js
     content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-sessions.js
     content/browser/ext-tabs.js
     content/browser/ext-theme.js
     content/browser/ext-url-overrides.js
     content/browser/ext-utils.js
--- a/browser/components/extensions/schemas/devtools_inspected_window.json
+++ b/browser/components/extensions/schemas/devtools_inspected_window.json
@@ -88,17 +88,16 @@
       "tabId": {
         "description": "The ID of the tab being inspected. This ID may be used with chrome.tabs.* API.",
         "type": "integer"
       }
     },
     "functions": [
       {
         "name": "eval",
-        "unsupported": true,
         "type": "function",
         "description": "Evaluates a JavaScript expression in the context of the main frame of the inspected page. The expression must evaluate to a JSON-compliant object, otherwise an exception is thrown. The eval function can report either a DevTools-side error or a JavaScript exception that occurs during evaluation. In either case, the <code>result</code> parameter of the callback is <code>undefined</code>. In the case of a DevTools-side error, the <code>isException</code> parameter is non-null and has <code>isError</code> set to true and <code>code</code> set to an error code. In the case of a JavaScript error, <code>isException</code> is set to true and <code>value</code> is set to the string value of thrown object.",
         "async": "callback",
         "parameters": [
           {
             "name": "expression",
             "type": "string",
             "description": "An expression to evaluate."
--- a/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow.js
@@ -10,16 +10,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 /**
  * 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.
  */
 add_task(function* test_devtools_inspectedWindow_tabId() {
   let tab = yield 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");
 
@@ -103,8 +109,130 @@ add_task(function* test_devtools_inspect
   yield gDevTools.closeToolbox(target);
 
   yield target.destroy();
 
   yield extension.unload();
 
   yield BrowserTestUtils.removeTab(tab);
 });
+
+add_task(function* test_devtools_inspectedWindow_eval() {
+  const TEST_TARGET_URL = "http://mochi.test:8888/";
+  let tab = yield 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;
+      }
+
+      try {
+        const [evalResult, errorResult] = await browser.devtools.inspectedWindow.eval(...args);
+        browser.test.sendMessage("inspectedWindow-eval-result", {
+          evalResult,
+          errorResult,
+        });
+      } catch (err) {
+        browser.test.sendMessage("inspectedWindow-eval-result");
+        browser.test.fail(`Error: ${err} :: ${err.stack}`);
+      }
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      devtools_page: "devtools_page.html",
+    },
+    files: {
+      "devtools_page.html": `<!DOCTYPE html>
+      <html>
+       <head>
+         <meta charset="utf-8">
+         <script text="text/javascript" src="devtools_page.js"></script>
+       </head>
+       <body>
+       </body>
+      </html>`,
+      "devtools_page.js": devtools_page,
+    },
+  });
+
+  yield extension.startup();
+
+  let target = devtools.TargetFactory.forTab(tab);
+
+  yield gDevTools.showToolbox(target, "webconsole");
+  info("developer toolbox opened");
+
+  const evalTestCases = [
+    // Successful evaluation results.
+    {
+      args: ["window.location.href"],
+      expectedResults: {evalResult: TEST_TARGET_URL, errorResult: undefined},
+    },
+
+    // Error evaluation results.
+    {
+      args: ["window"],
+      expectedResults: {
+        evalResult: undefined,
+        errorResult: {
+          isError: true,
+          code: "E_PROTOCOLERROR",
+          description: "Inspector protocol error: %s",
+          details: [
+            "TypeError: cyclic object value",
+          ],
+        },
+      },
+    },
+
+    // Exception evaluation results.
+    {
+      args: ["throw new Error('fake eval exception');"],
+      expectedResults: {
+        evalResult: undefined,
+        errorResult: {
+          isException: true,
+          value: /Error: fake eval exception\n.*moz-extension:\/\//,
+        },
+      },
+
+    },
+  ];
+
+  for (let testCase of evalTestCases) {
+    info(`test inspectedWindow.eval with ${JSON.stringify(testCase)}`);
+
+    const {args, expectedResults} = testCase;
+
+    extension.sendMessage(`inspectedWindow-eval-request`, ...args);
+
+    const {evalResult, errorResult} = yield extension.awaitMessage(`inspectedWindow-eval-result`);
+
+    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];
+
+        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`);
+        }
+      }
+    }
+  }
+
+  yield gDevTools.closeToolbox(target);
+
+  yield target.destroy();
+
+  yield extension.unload();
+
+  yield BrowserTestUtils.removeTab(tab);
+});