Bug 757866 - Highlight and select DOM nodes in the web console output; r=msucan

This commit is contained in:
Patrick Brosset 2014-03-11 12:21:20 +02:00
parent 8bb8a20f43
commit 5bb0d4f464
15 changed files with 742 additions and 27 deletions

View File

@ -465,7 +465,7 @@ Toolbox.prototype = {
fireCustomKey: function(toolId) { fireCustomKey: function(toolId) {
let toolDefinition = gDevTools.getToolDefinition(toolId); let toolDefinition = gDevTools.getToolDefinition(toolId);
if (toolDefinition.onkey && if (toolDefinition.onkey &&
((this.currentToolId === toolId) || ((this.currentToolId === toolId) ||
(toolId == "webconsole" && this.splitConsole))) { (toolId == "webconsole" && this.splitConsole))) {
toolDefinition.onkey(this.getCurrentPanel(), this); toolDefinition.onkey(this.getCurrentPanel(), this);
@ -1093,27 +1093,17 @@ Toolbox.prototype = {
* Returns a promise that resolves when the fronts are initialized * Returns a promise that resolves when the fronts are initialized
*/ */
initInspector: function() { initInspector: function() {
let deferred = promise.defer(); if (!this._initInspector) {
this._initInspector = Task.spawn(function*() {
if (!this._inspector) { this._inspector = InspectorFront(this._target.client, this._target.form);
this._inspector = InspectorFront(this._target.client, this._target.form); this._walker = yield this._inspector.getWalker();
this._inspector.getWalker().then(walker => {
this._walker = walker;
this._selection = new Selection(this._walker); this._selection = new Selection(this._walker);
if (this.highlighterUtils.isRemoteHighlightable) { if (this.highlighterUtils.isRemoteHighlightable) {
this._inspector.getHighlighter().then(highlighter => { this._highlighter = yield this._inspector.getHighlighter();
this._highlighter = highlighter;
deferred.resolve();
});
} else {
deferred.resolve();
} }
}); }.bind(this));
} else {
deferred.resolve();
} }
return this._initInspector;
return deferred.promise;
}, },
/** /**

View File

@ -9,6 +9,8 @@ const {Cc, Ci, Cu} = require("chrome");
loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
loader.lazyImporter(this, "escapeHTML", "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 Heritage = require("sdk/core/heritage");
const XHTML_NS = "http://www.w3.org/1999/xhtml"; const XHTML_NS = "http://www.w3.org/1999/xhtml";
@ -131,7 +133,7 @@ ConsoleOutput.prototype = {
* @type DOMDocument * @type DOMDocument
*/ */
get document() { get document() {
return this.owner.document; return this.owner ? this.owner.document : null;
}, },
/** /**
@ -150,6 +152,14 @@ ConsoleOutput.prototype = {
return this.owner.webConsoleClient; return this.owner.webConsoleClient;
}, },
/**
* Getter for the current toolbox debuggee target.
* @type Target
*/
get toolboxTarget() {
return this.owner.owner.target;
},
/** /**
* Release an actor. * Release an actor.
* *
@ -507,6 +517,14 @@ Messages.BaseMessage.prototype = {
{ {
this.output.openLink(event.target.href); 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 }; // Messages.BaseMessage.prototype
@ -2017,6 +2035,7 @@ Widgets.ObjectRenderers.add({
case Ci.nsIDOMNode.TEXT_NODE: case Ci.nsIDOMNode.TEXT_NODE:
case Ci.nsIDOMNode.COMMENT_NODE: case Ci.nsIDOMNode.COMMENT_NODE:
case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
case Ci.nsIDOMNode.ELEMENT_NODE:
return true; return true;
default: default:
return false; return false;
@ -2045,6 +2064,9 @@ Widgets.ObjectRenderers.add({
case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
this._renderDocumentFragmentNode(); this._renderDocumentFragmentNode();
break; break;
case Ci.nsIDOMNode.ELEMENT_NODE:
this._renderElementNode();
break;
default: default:
throw new Error("Unsupported nodeType: " + preview.nodeType); throw new Error("Unsupported nodeType: " + preview.nodeType);
} }
@ -2138,6 +2160,168 @@ Widgets.ObjectRenderers.add({
this._text(" ]"); 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 }); // Widgets.ObjectRenderers.byKind.DOMNode
/** /**

View File

@ -69,6 +69,7 @@ support-files =
test-console-output-02.html test-console-output-02.html
test-console-output-03.html test-console-output-03.html
test-console-output-04.html test-console-output-04.html
test-console-output-dom-elements.html
test-console-output-events.html test-console-output-events.html
test-consoleiframes.html test-consoleiframes.html
test-data.json test-data.json
@ -266,6 +267,10 @@ run-if = os == "mac"
[browser_webconsole_output_02.js] [browser_webconsole_output_02.js]
[browser_webconsole_output_03.js] [browser_webconsole_output_03.js]
[browser_webconsole_output_04.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_webconsole_output_events.js]
[browser_console_variables_view_highlighter.js] [browser_console_variables_view_highlighter.js]
[browser_webconsole_start_netmon_first.js] [browser_webconsole_start_netmon_first.js]

View File

@ -29,7 +29,7 @@ let inputTests = [
// 2 // 2
{ {
input: "testDocumentFragment()", input: "testDocumentFragment()",
output: 'DocumentFragment [ <div#foo1>, <div#foo3> ]', output: 'DocumentFragment [ <div#foo1.bar>, <div#foo3> ]',
printOutput: "[object DocumentFragment]", printOutput: "[object DocumentFragment]",
inspectable: true, inspectable: true,
variablesViewLabel: "DocumentFragment[2]", variablesViewLabel: "DocumentFragment[2]",

View File

@ -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: '<body id="body-id" class="body-class">',
printOutput: "[object HTMLBodyElement]",
inspectable: true,
noClick: true,
inspectorIcon: true
},
{
input: "testDocumentElement()",
output: '<html lang="en-US" dir="ltr">',
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: '<p some-attribute="some-value">',
printOutput: "[object HTMLParagraphElement]",
inspectable: true,
noClick: true,
inspectorIcon: true
},
{
input: "testNodeList()",
output: 'NodeList [ <html>, <head>, <meta>, <title>, <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);
}

View File

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

View File

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

View File

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

View File

@ -1314,6 +1314,10 @@ function whenDelayedStartupFinished(aWindow, aCallback)
* - variablesViewLabel: string|RegExp, optional, the expected variables * - variablesViewLabel: string|RegExp, optional, the expected variables
* view label when the object is inspected. If this is not provided, then * view label when the object is inspected. If this is not provided, then
* |output| is used. * |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) function checkOutputForInputs(hud, inputTests)
{ {
@ -1338,12 +1342,12 @@ function checkOutputForInputs(hud, inputTests)
yield checkJSEval(entry); yield checkJSEval(entry);
} }
function checkConsoleLog(entry) function* checkConsoleLog(entry)
{ {
hud.jsterm.clearOutput(); hud.jsterm.clearOutput();
hud.jsterm.execute("console.log(" + entry.input + ")"); hud.jsterm.execute("console.log(" + entry.input + ")");
return waitForMessages({ let [result] = yield waitForMessages({
webconsole: hud, webconsole: hud,
messages: [{ messages: [{
name: "console.log() output: " + entry.output, name: "console.log() output: " + entry.output,
@ -1352,6 +1356,11 @@ function checkOutputForInputs(hud, inputTests)
severity: SEVERITY_LOG, severity: SEVERITY_LOG,
}], }],
}); });
if (typeof entry.inspectorIcon == "boolean") {
let msg = [...result.matched][0];
yield checkLinkToInspector(entry, msg);
}
} }
function checkPrintOutput(entry) function checkPrintOutput(entry)
@ -1385,10 +1394,13 @@ function checkOutputForInputs(hud, inputTests)
}], }],
}); });
let msg = [...result.matched][0];
if (!entry.noClick) { if (!entry.noClick) {
let msg = [...result.matched][0];
yield checkObjectClick(entry, msg); yield checkObjectClick(entry, msg);
} }
if (typeof entry.inspectorIcon == "boolean") {
yield checkLinkToInspector(entry, msg);
}
} }
function checkObjectClick(entry, msg) function checkObjectClick(entry, msg)
@ -1413,6 +1425,29 @@ function checkOutputForInputs(hud, inputTests)
return promise.resolve(null); 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) function onVariablesViewOpen(entry, deferred, event, view, options)
{ {
let label = entry.variablesViewLabel || entry.output; let label = entry.variablesViewLabel || entry.output;

View File

@ -0,0 +1,68 @@
<!DOCTYPE HTML>
<html dir="ltr" lang="en-US">
<head>
<meta charset="utf-8">
<title>Test the web console output - 05</title>
<!--
- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/
-->
</head>
<body class="body-class" id="body-id">
<p some-attribute="some-value">hello world!</p>
<iframe src="data:text/html,<p>hello from iframe</p>"></iframe>
<script type="text/javascript">
function testBodyNode() {
return document.body;
}
function testDocumentElement() {
return document.documentElement;
}
function testDocument() {
return document;
}
function testNode() {
return document.querySelector("p");
}
function testNodeList() {
return document.querySelectorAll("*");
}
function testNodeInIframe() {
return document.querySelector("iframe").contentWindow.document.querySelector("p");
}
function testDocumentFragment() {
var frag = document.createDocumentFragment();
var span = document.createElement("span");
span.className = 'foo';
span.dataset.lolz = 'hehe';
var div = document.createElement('div')
div.id = 'fragdiv';
frag.appendChild(span);
frag.appendChild(div);
return frag;
}
function testNodeInDocumentFragment() {
var frag = testDocumentFragment();
return frag.firstChild;
}
function testUnattachedNode() {
var p = document.createElement("p");
p.className = "such-class";
p.dataset.data = "such-data";
return p;
}
</script>
</body>
</html>

View File

@ -2362,6 +2362,10 @@ WebConsoleFrame.prototype = {
*/ */
removeOutputMessage: function WCF_removeOutputMessage(aNode) removeOutputMessage: function WCF_removeOutputMessage(aNode)
{ {
if (aNode._messageObject) {
aNode._messageObject.destroy();
}
if (aNode._objectActors) { if (aNode._objectActors) {
for (let actor of aNode._objectActors) { for (let actor of aNode._objectActors) {
this._releaseObject(actor); this._releaseObject(actor);

View File

@ -214,6 +214,11 @@ emptyPropertiesList=No properties to display
# example: 3 repeats # example: 3 repeats
messageRepeats.tooltip2=#1 repeat;#1 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 # LOCALIZATION NOTE (cdFunctionInvalidArgument): the text that is displayed when
# cd() is invoked with an invalid argument. # cd() is invoked with an invalid argument.
cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument. cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument.

View File

@ -381,6 +381,23 @@ a {
text-decoration: underline; 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 */ /* Replace these values with CSS variables as available */
.theme-dark .jsterm-input-container { .theme-dark .jsterm-input-container {
background-color: #252c33; /* tabToolbarBackgroundColor */ background-color: #252c33; /* tabToolbarBackgroundColor */

View File

@ -83,7 +83,7 @@ let HighlighterActor = protocol.ActorClass({
* outline highlighter for instance does not scrollIntoView * outline highlighter for instance does not scrollIntoView
*/ */
showBoxModel: method(function(node, options={}) { showBoxModel: method(function(node, options={}) {
if (this._isNodeValidForHighlighting(node.rawNode)) { if (node && this._isNodeValidForHighlighting(node.rawNode)) {
this._boxModelHighlighter.show(node.rawNode, options); this._boxModelHighlighter.show(node.rawNode, options);
} else { } else {
this._boxModelHighlighter.hide(); this._boxModelHighlighter.hide();

View File

@ -2050,14 +2050,54 @@ var WalkerActor = protocol.ActorClass({
this.releaseNode(documentActor, { force: true }); 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, * Given an ObjectActor (identified by its ID), commonly used in the debugger,
* webconsole and variablesView, return the corresponding inspector's NodeActor * webconsole and variablesView, return the corresponding inspector's NodeActor
*/ */
getNodeActorFromObjectActor: method(function(objectActorID) { getNodeActorFromObjectActor: method(function(objectActorID) {
let debuggerObject = this.conn.poolFor(objectActorID).get(objectActorID).obj; let debuggerObject = this.conn.getActor(objectActorID).obj;
let rawNode = debuggerObject.unsafeDereference(); let rawNode = debuggerObject.unsafeDereference();
if (!this._isInDOMTree(rawNode)) {
return null;
}
// This is a special case for the document object whereby it is considered // This is a special case for the document object whereby it is considered
// as document.documentElement (the <html> node) // as document.documentElement (the <html> node)
if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
@ -2070,7 +2110,7 @@ var WalkerActor = protocol.ActorClass({
objectActorID: Arg(0, "string") objectActorID: Arg(0, "string")
}, },
response: { response: {
nodeFront: RetVal("disconnectedNode") nodeFront: RetVal("nullable:disconnectedNode")
} }
}), }),
}); });
@ -2208,7 +2248,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
getNodeActorFromObjectActor: protocol.custom(function(objectActorID) { getNodeActorFromObjectActor: protocol.custom(function(objectActorID) {
return this._getNodeActorFromObjectActor(objectActorID).then(response => { return this._getNodeActorFromObjectActor(objectActorID).then(response => {
return response.node; return response ? response.node : null;
}); });
}, { }, {
impl: "_getNodeActorFromObjectActor" impl: "_getNodeActorFromObjectActor"