From 9cd64a3229a7a434f95a09071779d3faa9345dd7 Mon Sep 17 00:00:00 2001 From: Patrick Brosset Date: Tue, 11 Mar 2014 12:21:20 +0200 Subject: [PATCH] Bug 757866 - Highlight and select DOM nodes in the web console output; r=msucan --- browser/devtools/framework/toolbox.js | 26 +-- browser/devtools/webconsole/console-output.js | 186 +++++++++++++++++- browser/devtools/webconsole/test/browser.ini | 5 + .../test/browser_webconsole_output_04.js | 2 +- ...owser_webconsole_output_dom_elements_01.js | 99 ++++++++++ ...owser_webconsole_output_dom_elements_02.js | 95 +++++++++ ...owser_webconsole_output_dom_elements_03.js | 67 +++++++ ...owser_webconsole_output_dom_elements_04.js | 106 ++++++++++ browser/devtools/webconsole/test/head.js | 41 +++- .../test-console-output-dom-elements.html | 68 +++++++ browser/devtools/webconsole/webconsole.js | 4 + .../browser/devtools/webconsole.properties | 5 + .../themes/shared/devtools/webconsole.inc.css | 17 ++ toolkit/devtools/server/actors/highlighter.js | 2 +- toolkit/devtools/server/actors/inspector.js | 46 ++++- 15 files changed, 742 insertions(+), 27 deletions(-) create mode 100644 browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js create mode 100644 browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js create mode 100644 browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js create mode 100644 browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js create mode 100644 browser/devtools/webconsole/test/test-console-output-dom-elements.html diff --git a/browser/devtools/framework/toolbox.js b/browser/devtools/framework/toolbox.js index d34b38216211..56ce0598ee7b 100644 --- a/browser/devtools/framework/toolbox.js +++ b/browser/devtools/framework/toolbox.js @@ -465,7 +465,7 @@ Toolbox.prototype = { fireCustomKey: function(toolId) { let toolDefinition = gDevTools.getToolDefinition(toolId); - if (toolDefinition.onkey && + if (toolDefinition.onkey && ((this.currentToolId === toolId) || (toolId == "webconsole" && this.splitConsole))) { toolDefinition.onkey(this.getCurrentPanel(), this); @@ -1093,27 +1093,17 @@ Toolbox.prototype = { * Returns a promise that resolves when the fronts are initialized */ initInspector: function() { - let deferred = promise.defer(); - - if (!this._inspector) { - this._inspector = InspectorFront(this._target.client, this._target.form); - this._inspector.getWalker().then(walker => { - this._walker = walker; + if (!this._initInspector) { + this._initInspector = Task.spawn(function*() { + this._inspector = InspectorFront(this._target.client, this._target.form); + this._walker = yield this._inspector.getWalker(); this._selection = new Selection(this._walker); if (this.highlighterUtils.isRemoteHighlightable) { - this._inspector.getHighlighter().then(highlighter => { - this._highlighter = highlighter; - deferred.resolve(); - }); - } else { - deferred.resolve(); + this._highlighter = yield this._inspector.getHighlighter(); } - }); - } else { - deferred.resolve(); + }.bind(this)); } - - return deferred.promise; + return this._initInspector; }, /** diff --git a/browser/devtools/webconsole/console-output.js b/browser/devtools/webconsole/console-output.js index 525f9d8a2588..14a3465f96a7 100644 --- a/browser/devtools/webconsole/console-output.js +++ b/browser/devtools/webconsole/console-output.js @@ -9,6 +9,8 @@ const {Cc, Ci, Cu} = require("chrome"); loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); loader.lazyImporter(this, "escapeHTML", "resource:///modules/devtools/VariablesView.jsm"); +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); +loader.lazyImporter(this, "Task","resource://gre/modules/Task.jsm"); const Heritage = require("sdk/core/heritage"); const XHTML_NS = "http://www.w3.org/1999/xhtml"; @@ -131,7 +133,7 @@ ConsoleOutput.prototype = { * @type DOMDocument */ get document() { - return this.owner.document; + return this.owner ? this.owner.document : null; }, /** @@ -150,6 +152,14 @@ ConsoleOutput.prototype = { return this.owner.webConsoleClient; }, + /** + * Getter for the current toolbox debuggee target. + * @type Target + */ + get toolboxTarget() { + return this.owner.owner.target; + }, + /** * Release an actor. * @@ -507,6 +517,14 @@ Messages.BaseMessage.prototype = { { this.output.openLink(event.target.href); }, + + destroy: function() + { + // Destroy all widgets that have registered themselves in this.widgets + for (let widget of this.widgets) { + widget.destroy(); + } + } }; // Messages.BaseMessage.prototype @@ -2017,6 +2035,7 @@ Widgets.ObjectRenderers.add({ case Ci.nsIDOMNode.TEXT_NODE: case Ci.nsIDOMNode.COMMENT_NODE: case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: + case Ci.nsIDOMNode.ELEMENT_NODE: return true; default: return false; @@ -2045,6 +2064,9 @@ Widgets.ObjectRenderers.add({ case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: this._renderDocumentFragmentNode(); break; + case Ci.nsIDOMNode.ELEMENT_NODE: + this._renderElementNode(); + break; default: throw new Error("Unsupported nodeType: " + preview.nodeType); } @@ -2138,6 +2160,168 @@ Widgets.ObjectRenderers.add({ this._text(" ]"); }, + + _renderElementNode: function() + { + let doc = this.document; + let {attributes, nodeName} = this.objectActor.preview; + + this.element = this.el("span." + "kind-" + this.objectActor.preview.kind + ".elementNode"); + + let openTag = this.el("span.cm-tag"); + openTag.textContent = "<"; + this.element.appendChild(openTag); + + let tagName = this._anchor(nodeName, { + className: "cm-tag", + appendTo: openTag + }); + + if (this.options.concise) { + if (attributes.id) { + tagName.appendChild(this.el("span.cm-attribute", "#" + attributes.id)); + } + if (attributes.class) { + tagName.appendChild(this.el("span.cm-attribute", "." + attributes.class.split(" ").join("."))); + } + } else { + for (let name of Object.keys(attributes)) { + let attr = this._renderAttributeNode(" " + name, attributes[name]); + this.element.appendChild(attr); + } + } + + let closeTag = this.el("span.cm-tag"); + closeTag.textContent = ">"; + this.element.appendChild(closeTag); + + // Register this widget in the owner message so that it gets destroyed when + // the message is destroyed. + this.message.widgets.add(this); + + this.linkToInspector(); + }, + + /** + * If the DOMNode being rendered can be highlit in the page, this function + * will attach mouseover/out event listeners to do so, and the inspector icon + * to open the node in the inspector. + * @return a promise (always the same) that resolves when the node has been + * linked to the inspector, or rejects if it wasn't (either if no toolbox + * could be found to access the inspector, or if the node isn't present in the + * inspector, i.e. if the node is in a DocumentFragment or not part of the + * tree, or not of type Ci.nsIDOMNode.ELEMENT_NODE). + */ + linkToInspector: function() + { + if (this._linkedToInspector) { + return this._linkedToInspector; + } + + this._linkedToInspector = Task.spawn(function*() { + // Checking the node type + if (this.objectActor.preview.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { + throw null; + } + + // Checking the presence of a toolbox + let target = this.message.output.toolboxTarget; + this.toolbox = gDevTools.getToolbox(target); + if (!this.toolbox) { + throw null; + } + + // Checking that the inspector supports the node + yield this.toolbox.initInspector(); + this._nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this.objectActor.actor); + if (!this._nodeFront) { + throw null; + } + + // At this stage, the message may have been cleared already + if (!this.document) { + throw null; + } + + this.highlightDomNode = this.highlightDomNode.bind(this); + this.element.addEventListener("mouseover", this.highlightDomNode, false); + this.unhighlightDomNode = this.unhighlightDomNode.bind(this); + this.element.addEventListener("mouseout", this.unhighlightDomNode, false); + + this._openInspectorNode = this._anchor("", { + className: "open-inspector", + onClick: this.openNodeInInspector.bind(this) + }); + this._openInspectorNode.title = l10n.getStr("openNodeInInspector"); + }.bind(this)); + + return this._linkedToInspector; + }, + + /** + * Highlight the DOMNode corresponding to the ObjectActor in the page. + * @return a promise that resolves when the node has been highlighted, or + * rejects if the node cannot be highlighted (detached from the DOM) + */ + highlightDomNode: function() + { + return Task.spawn(function*() { + yield this.linkToInspector(); + let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); + if (isAttached) { + yield this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront); + } else { + throw null; + } + }.bind(this)); + }, + + /** + * Unhighlight a previously highlit node + * @see highlightDomNode + * @return a promise that resolves when the highlighter has been hidden + */ + unhighlightDomNode: function() + { + return this.linkToInspector().then(() => { + return this.toolbox.highlighterUtils.unhighlight(); + }); + }, + + /** + * Open the DOMNode corresponding to the ObjectActor in the inspector panel + * @return a promise that resolves when the inspector has been switched to + * and the node has been selected, or rejects if the node cannot be selected + * (detached from the DOM). Note that in any case, the inspector panel will + * be switched to. + */ + openNodeInInspector: function() + { + return Task.spawn(function*() { + yield this.linkToInspector(); + yield this.toolbox.selectTool("inspector"); + + let isAttached = yield this.toolbox.walker.isInDOMTree(this._nodeFront); + if (isAttached) { + let onReady = this.toolbox.inspector.once("inspector-updated"); + yield this.toolbox.selection.setNodeFront(this._nodeFront, "console"); + yield onReady; + } else { + throw null; + } + }.bind(this)); + }, + + destroy: function() + { + if (this.toolbox && this._nodeFront) { + this.element.removeEventListener("mouseover", this.highlightDomNode, false); + this.element.removeEventListener("mouseout", this.unhighlightDomNode, false); + this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, true); + this.toolbox = null; + this._nodeFront = null; + } + }, }); // Widgets.ObjectRenderers.byKind.DOMNode /** diff --git a/browser/devtools/webconsole/test/browser.ini b/browser/devtools/webconsole/test/browser.ini index d68628e76663..9de382ece30f 100644 --- a/browser/devtools/webconsole/test/browser.ini +++ b/browser/devtools/webconsole/test/browser.ini @@ -69,6 +69,7 @@ support-files = test-console-output-02.html test-console-output-03.html test-console-output-04.html + test-console-output-dom-elements.html test-console-output-events.html test-consoleiframes.html test-data.json @@ -266,6 +267,10 @@ run-if = os == "mac" [browser_webconsole_output_02.js] [browser_webconsole_output_03.js] [browser_webconsole_output_04.js] +[browser_webconsole_output_dom_elements_01.js] +[browser_webconsole_output_dom_elements_02.js] +[browser_webconsole_output_dom_elements_03.js] +[browser_webconsole_output_dom_elements_04.js] [browser_webconsole_output_events.js] [browser_console_variables_view_highlighter.js] [browser_webconsole_start_netmon_first.js] diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_04.js b/browser/devtools/webconsole/test/browser_webconsole_output_04.js index c76fc8923111..579529a2b253 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_output_04.js +++ b/browser/devtools/webconsole/test/browser_webconsole_output_04.js @@ -29,7 +29,7 @@ let inputTests = [ // 2 { input: "testDocumentFragment()", - output: 'DocumentFragment [ , ]', + output: 'DocumentFragment [ , ]', printOutput: "[object DocumentFragment]", inspectable: true, variablesViewLabel: "DocumentFragment[2]", diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js new file mode 100644 index 000000000000..c6f8393363f7 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_01.js @@ -0,0 +1,99 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the webconsole output for various types of DOM Nodes. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +let inputTests = [ + { + input: "testBodyNode()", + output: '', + printOutput: "[object HTMLBodyElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocumentElement()", + output: '', + printOutput: "[object HTMLHtmlElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocument()", + output: 'HTMLDocument \u2192 ' + TEST_URI, + printOutput: "[object HTMLDocument]", + inspectable: true, + noClick: true, + inspectorIcon: false + }, + + { + input: "testNode()", + output: '

', + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testNodeList()", + output: 'NodeList [ , , , , <body#body-id.body-class>, <p>, <iframe>, <script> ]', + printOutput: "[object NodeList]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testNodeInIframe()", + output: '<p>', + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: true + }, + + { + input: "testDocumentFragment()", + output: 'DocumentFragment [ <span.foo>, <div#fragdiv> ]', + printOutput: "[object DocumentFragment]", + inspectable: true, + noClick: true, + inspectorIcon: false + }, + + { + input: "testNodeInDocumentFragment()", + output: '<span class="foo" data-lolz="hehe">', + printOutput: "[object HTMLSpanElement]", + inspectable: true, + noClick: true, + inspectorIcon: false + }, + + { + input: "testUnattachedNode()", + output: '<p class="such-class" data-data="such-data">', + printOutput: "[object HTMLParagraphElement]", + inspectable: true, + noClick: true, + inspectorIcon: false + } +]; + +function test() { + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + yield checkOutputForInputs(hud, inputTests); + }).then(finishTest); +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js new file mode 100644 index 000000000000..2e1403cf636b --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_02.js @@ -0,0 +1,95 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test the inspector links in the webconsole output for DOM Nodes actually +// open the inspector and select the right node + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +const TEST_DATA = [ + { + // The first test shouldn't be returning the body element as this is the + // default selected node, so re-selecting it won't fire the inspector-updated + // event + input: "testNode()", + output: '<p some-attribute="some-value">' + }, + { + input: "testBodyNode()", + output: '<body id="body-id" class="body-class">' + }, + { + input: "testNodeInIframe()", + output: '<p>' + }, + { + input: "testDocumentElement()", + output: '<html lang="en-US" dir="ltr">' + } +]; + +function test() { + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let toolbox = gDevTools.getToolbox(hud.target); + + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + yield toolbox.loadTool("inspector"); + let inspector = toolbox.getPanel("inspector"); + + info("Iterating over the test data"); + for (let data of TEST_DATA) { + let [result] = yield jsEval(data.input, hud, {text: data.output}); + let {widget, msg} = yield getWidgetAndMessage(result); + + let inspectorIcon = msg.querySelector(".open-inspector"); + ok(inspectorIcon, "Inspector icon found in the ElementNode widget"); + + info("Clicking on the inspector icon and waiting for the inspector to be selected"); + let onInspectorSelected = toolbox.once("inspector-selected"); + let onInspectorUpdated = inspector.once("inspector-updated"); + + EventUtils.synthesizeMouseAtCenter(inspectorIcon, {}, + inspectorIcon.ownerDocument.defaultView); + yield onInspectorSelected; + yield onInspectorUpdated; + ok(true, "Inspector selected and new node got selected"); + + let rawNode = content.wrappedJSObject[data.input.replace(/\(\)/g, "")](); + is(rawNode, inspector.selection.node.wrappedJSObject, + "The current inspector selection is correct"); + + info("Switching back to the console"); + yield toolbox.selectTool("webconsole"); + } + }).then(finishTest); +} + +function jsEval(input, hud, message) { + info("Executing '" + input + "' in the web console"); + + hud.jsterm.clearOutput(); + hud.jsterm.execute(input); + + return waitForMessages({ + webconsole: hud, + messages: [message] + }); +} + +function* getWidgetAndMessage(result) { + info("Getting the output ElementNode widget"); + + let msg = [...result.matched][0]; + let widget = [...msg._messageObject.widgets][0]; + ok(widget, "ElementNode widget found in the output"); + + info("Waiting for the ElementNode widget to be linked to the inspector"); + yield widget.linkToInspector(); + + return {widget: widget, msg: msg}; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js new file mode 100644 index 000000000000..cc420d492f0b --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_03.js @@ -0,0 +1,67 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that inspector links in webconsole outputs for DOM Nodes highlight +// the actual DOM Nodes on hover + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +function test() { + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let toolbox = gDevTools.getToolbox(hud.target); + + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + yield toolbox.loadTool("inspector"); + let inspector = toolbox.getPanel("inspector"); + + info("Executing 'testNode()' in the web console to output a DOM Node"); + let [result] = yield jsEval("testNode()", hud, { + text: '<p some-attribute="some-value">' + }); + + let elementNodeWidget = yield getWidget(result); + + let nodeFront = yield hoverOverWidget(elementNodeWidget, toolbox); + let attrs = nodeFront.attributes; + is(nodeFront.tagName, "P", "The correct node was highlighted"); + is(attrs[0].name, "some-attribute", "The correct node was highlighted"); + is(attrs[0].value, "some-value", "The correct node was highlighted"); + }).then(finishTest); +} + +function jsEval(input, hud, message) { + hud.jsterm.execute(input); + return waitForMessages({ + webconsole: hud, + messages: [message] + }); +} + +function* getWidget(result) { + info("Getting the output ElementNode widget"); + + let msg = [...result.matched][0]; + let elementNodeWidget = [...msg._messageObject.widgets][0]; + ok(elementNodeWidget, "ElementNode widget found in the output"); + + info("Waiting for the ElementNode widget to be linked to the inspector"); + yield elementNodeWidget.linkToInspector(); + + return elementNodeWidget; +} + +function* hoverOverWidget(widget, toolbox) { + info("Hovering over the output to highlight the node"); + + let onHighlight = toolbox.once("node-highlight"); + EventUtils.sendMouseEvent({type: "mouseover"}, widget.element, + widget.element.ownerDocument.defaultView); + let nodeFront = yield onHighlight; + ok(true, "The highlighter was shown on a node"); + return nodeFront; +} diff --git a/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js new file mode 100644 index 000000000000..7cb0847e7685 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_webconsole_output_dom_elements_04.js @@ -0,0 +1,106 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test that inspector links in the webconsole output for DOM Nodes do not try +// to highlight or select nodes once they have been detached + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-output-dom-elements.html"; + +const TEST_DATA = [ + { + // The first test shouldn't be returning the body element as this is the + // default selected node, so re-selecting it won't fire the inspector-updated + // event + input: "testNode()", + output: '<p some-attribute="some-value">' + }, + { + input: "testBodyNode()", + output: '<body id="body-id" class="body-class">' + }, + { + input: "testNodeInIframe()", + output: '<p>' + }, + { + input: "testDocumentElement()", + output: '<html lang="en-US" dir="ltr">' + } +]; + +const PREF = "devtools.webconsole.persistlog"; + +function test() { + Services.prefs.setBoolPref(PREF, true); + registerCleanupFunction(() => Services.prefs.clearUserPref(PREF)); + + Task.spawn(function*() { + let {tab} = yield loadTab(TEST_URI); + let hud = yield openConsole(tab); + let toolbox = gDevTools.getToolbox(hud.target); + + info("Executing the test data"); + let widgets = []; + for (let data of TEST_DATA) { + let [result] = yield jsEval(data.input, hud, {text: data.output}); + let {widget} = yield getWidgetAndMessage(result); + widgets.push(widget); + } + + info("Reloading the page"); + yield reloadPage(); + + info("Iterating over the ElementNode widgets"); + for (let widget of widgets) { + // Verify that openNodeInInspector rejects since the associated dom node + // doesn't exist anymore + yield widget.openNodeInInspector().then(() => { + ok(false, "The openNodeInInspector promise resolved"); + }, () => { + ok(true, "The openNodeInInspector promise rejected as expected"); + }); + yield toolbox.selectTool("webconsole"); + + // Verify that highlightDomNode rejects too, for the same reason + yield widget.highlightDomNode().then(() => { + ok(false, "The highlightDomNode promise resolved"); + }, () => { + ok(true, "The highlightDomNode promise rejected as expected"); + }); + } + }).then(finishTest); +} + +function jsEval(input, hud, message) { + info("Executing '" + input + "' in the web console"); + hud.jsterm.execute(input); + return waitForMessages({ + webconsole: hud, + messages: [message] + }); +} + +function* getWidgetAndMessage(result) { + info("Getting the output ElementNode widget"); + + let msg = [...result.matched][0]; + let widget = [...msg._messageObject.widgets][0]; + ok(widget, "ElementNode widget found in the output"); + + info("Waiting for the ElementNode widget to be linked to the inspector"); + yield widget.linkToInspector(); + + return {widget: widget, msg: msg}; +} + +function reloadPage() { + let def = promise.defer(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + def.resolve(); + }, true); + content.location.reload(); + return def.promise; +} diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index 26983f8770e3..d94ad1f2ce92 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -1314,6 +1314,10 @@ function whenDelayedStartupFinished(aWindow, aCallback) * - variablesViewLabel: string|RegExp, optional, the expected variables * view label when the object is inspected. If this is not provided, then * |output| is used. + * + * - inspectorIcon: boolean, when true, the test runner expects the + * result widget to contain an inspectorIcon element (className + * open-inspector). */ function checkOutputForInputs(hud, inputTests) { @@ -1338,12 +1342,12 @@ function checkOutputForInputs(hud, inputTests) yield checkJSEval(entry); } - function checkConsoleLog(entry) + function* checkConsoleLog(entry) { hud.jsterm.clearOutput(); hud.jsterm.execute("console.log(" + entry.input + ")"); - return waitForMessages({ + let [result] = yield waitForMessages({ webconsole: hud, messages: [{ name: "console.log() output: " + entry.output, @@ -1352,6 +1356,11 @@ function checkOutputForInputs(hud, inputTests) severity: SEVERITY_LOG, }], }); + + if (typeof entry.inspectorIcon == "boolean") { + let msg = [...result.matched][0]; + yield checkLinkToInspector(entry, msg); + } } function checkPrintOutput(entry) @@ -1385,10 +1394,13 @@ function checkOutputForInputs(hud, inputTests) }], }); + let msg = [...result.matched][0]; if (!entry.noClick) { - let msg = [...result.matched][0]; yield checkObjectClick(entry, msg); } + if (typeof entry.inspectorIcon == "boolean") { + yield checkLinkToInspector(entry, msg); + } } function checkObjectClick(entry, msg) @@ -1413,6 +1425,29 @@ function checkOutputForInputs(hud, inputTests) return promise.resolve(null); } + function checkLinkToInspector(entry, msg) + { + let elementNodeWidget = [...msg._messageObject.widgets][0]; + if (!elementNodeWidget) { + ok(!entry.inspectorIcon, "The message has no ElementNode widget"); + return; + } + + return elementNodeWidget.linkToInspector().then(() => { + // linkToInspector resolved, check for the .open-inspector element + if (entry.inspectorIcon) { + ok(msg.querySelectorAll(".open-inspector").length, + "The ElementNode widget is linked to the inspector"); + } else { + ok(!msg.querySelectorAll(".open-inspector").length, + "The ElementNode widget isn't linked to the inspector"); + } + }, () => { + // linkToInspector promise rejected, node not linked to inspector + ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector"); + }); + } + function onVariablesViewOpen(entry, deferred, event, view, options) { let label = entry.variablesViewLabel || entry.output; diff --git a/browser/devtools/webconsole/test/test-console-output-dom-elements.html b/browser/devtools/webconsole/test/test-console-output-dom-elements.html new file mode 100644 index 000000000000..3d2ef95f511b --- /dev/null +++ b/browser/devtools/webconsole/test/test-console-output-dom-elements.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html dir="ltr" lang="en-US"> +<head> + <meta charset="utf-8"> + <title>Test the web console output - 05 + + + +

hello world!

+ + + + diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 87576ffd89b1..5390071645f8 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -2362,6 +2362,10 @@ WebConsoleFrame.prototype = { */ removeOutputMessage: function WCF_removeOutputMessage(aNode) { + if (aNode._messageObject) { + aNode._messageObject.destroy(); + } + if (aNode._objectActors) { for (let actor of aNode._objectActors) { this._releaseObject(actor); diff --git a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties index f43d04799d5f..b05301894b0c 100644 --- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -214,6 +214,11 @@ emptyPropertiesList=No properties to display # example: 3 repeats messageRepeats.tooltip2=#1 repeat;#1 repeats +# LOCALIZATION NOTE (openNodeInInspector): the text that is displayed in a +# tooltip when hovering over the inspector icon next to a DOM Node in the console +# output +openNodeInInspector=Click to select the node in the inspector + # LOCALIZATION NOTE (cdFunctionInvalidArgument): the text that is displayed when # cd() is invoked with an invalid argument. cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument. diff --git a/browser/themes/shared/devtools/webconsole.inc.css b/browser/themes/shared/devtools/webconsole.inc.css index 1ac7ce01aa7a..0bd292b4adfb 100644 --- a/browser/themes/shared/devtools/webconsole.inc.css +++ b/browser/themes/shared/devtools/webconsole.inc.css @@ -381,6 +381,23 @@ a { text-decoration: underline; } +/* Open DOMNode in inspector button */ +.open-inspector { + background: url("chrome://browser/skin/devtools/vview-open-inspector.png") no-repeat 0 0; + padding-left: 16px; + margin-left: 5px; + cursor: pointer; +} + +.elementNode:hover .open-inspector, +.open-inspector:hover { + background-position: -32px 0; +} + +.open-inspector:active { + background-position: -16px 0; +} + /* Replace these values with CSS variables as available */ .theme-dark .jsterm-input-container { background-color: #252c33; /* tabToolbarBackgroundColor */ diff --git a/toolkit/devtools/server/actors/highlighter.js b/toolkit/devtools/server/actors/highlighter.js index aa326a65c11d..6e1e026922f4 100644 --- a/toolkit/devtools/server/actors/highlighter.js +++ b/toolkit/devtools/server/actors/highlighter.js @@ -83,7 +83,7 @@ let HighlighterActor = protocol.ActorClass({ * outline highlighter for instance does not scrollIntoView */ showBoxModel: method(function(node, options={}) { - if (this._isNodeValidForHighlighting(node.rawNode)) { + if (node && this._isNodeValidForHighlighting(node.rawNode)) { this._boxModelHighlighter.show(node.rawNode, options); } else { this._boxModelHighlighter.hide(); diff --git a/toolkit/devtools/server/actors/inspector.js b/toolkit/devtools/server/actors/inspector.js index 01628e6a44b5..fd2b783ad45c 100644 --- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -2050,14 +2050,54 @@ var WalkerActor = protocol.ActorClass({ this.releaseNode(documentActor, { force: true }); }, + /** + * Check if a node is attached to the DOM tree of the current page. + * @param {nsIDomNode} rawNode + * @return {Boolean} false if the node is removed from the tree or within a + * document fragment + */ + _isInDOMTree: function(rawNode) { + let walker = documentWalker(rawNode, this.rootWin); + let current = walker.currentNode; + + // Reaching the top of tree + while (walker.parentNode()) { + current = walker.currentNode; + } + + // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't + // attached + if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE || + current !== this.rootDoc) { + return false; + } + + // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc + return true; + }, + + /** + * @see _isInDomTree + */ + isInDOMTree: method(function(node) { + return node ? this._isInDOMTree(node.rawNode) : false; + }, { + request: { node: Arg(0, "domnode") }, + response: { attached: RetVal("boolean") } + }), + /** * Given an ObjectActor (identified by its ID), commonly used in the debugger, * webconsole and variablesView, return the corresponding inspector's NodeActor */ getNodeActorFromObjectActor: method(function(objectActorID) { - let debuggerObject = this.conn.poolFor(objectActorID).get(objectActorID).obj; + let debuggerObject = this.conn.getActor(objectActorID).obj; let rawNode = debuggerObject.unsafeDereference(); + if (!this._isInDOMTree(rawNode)) { + return null; + } + // This is a special case for the document object whereby it is considered // as document.documentElement (the node) if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { @@ -2070,7 +2110,7 @@ var WalkerActor = protocol.ActorClass({ objectActorID: Arg(0, "string") }, response: { - nodeFront: RetVal("disconnectedNode") + nodeFront: RetVal("nullable:disconnectedNode") } }), }); @@ -2208,7 +2248,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, { getNodeActorFromObjectActor: protocol.custom(function(objectActorID) { return this._getNodeActorFromObjectActor(objectActorID).then(response => { - return response.node; + return response ? response.node : null; }); }, { impl: "_getNodeActorFromObjectActor"