--- 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;
},