Bug 1300584 - Implements devtools.inspectedWindow.eval.
MozReview-Commit-ID: 6Z76W8tKt9x
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);
+});