Bug 1184172 - Show stackframe for errors in the webconsole. r=past

This commit is contained in:
Alexandre Poirot 2015-09-08 09:48:38 -07:00
parent 162069381f
commit 419641bcc2
9 changed files with 205 additions and 83 deletions

View File

@ -675,6 +675,7 @@ Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.proto
* handler.
* - location: object that tells the message source: url, line, column
* and lineText.
* - stack: array that tells the message source stack.
* - className: (string) additional element class names for styling
* purposes.
* - private: (boolean) mark this as a private message.
@ -688,6 +689,7 @@ Messages.Simple = function(message, options = {})
this.category = options.category;
this.severity = options.severity;
this.location = options.location;
this.stack = options.stack;
this.timestamp = options.timestamp || Date.now();
this.prefix = options.prefix;
this.private = !!options.private;
@ -697,6 +699,8 @@ Messages.Simple = function(message, options = {})
this._link = options.link;
this._linkCallback = options.linkCallback;
this._filterDuplicates = options.filterDuplicates;
this._onClickCollapsible = this._onClickCollapsible.bind(this);
};
Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
@ -719,6 +723,14 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
*/
location: null,
/**
* Holds the stackframes received from the server.
*
* @private
* @type array
*/
stack: null,
/**
* Message prefix
* @type string|null
@ -810,6 +822,20 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
return this;
},
/**
* Tells if the message can be expanded/collapsed.
* @type boolean
*/
collapsible: false,
/**
* Getter that tells if this message is collapsed - no details are shown.
* @type boolean
*/
get collapsed() {
return this.collapsible && this.element && !this.element.hasAttribute("open");
},
_initRepeatID: function()
{
if (!this._filterDuplicates) {
@ -854,6 +880,9 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
let icon = this.document.createElementNS(XHTML_NS, "span");
icon.className = "icon";
icon.title = l10n.getStr("severity." + this._severityNameCompat);
if (this.stack) {
icon.addEventListener("click", this._onClickCollapsible);
}
let prefixNode;
if (this.prefix) {
@ -886,6 +915,18 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
if (prefixNode) {
this.element.appendChild(prefixNode);
}
if (this.stack) {
let twisty = this.document.createElementNS(XHTML_NS, "a");
twisty.className = "theme-twisty";
twisty.href = "#";
twisty.title = l10n.getStr("messageToggleDetails");
twisty.addEventListener("click", this._onClickCollapsible);
this.element.appendChild(twisty);
this.collapsible = true;
this.element.setAttribute("collapsible", true);
}
this.element.appendChild(body);
if (repeatNode) {
this.element.appendChild(repeatNode);
@ -893,6 +934,7 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
if (location) {
this.element.appendChild(location);
}
this.element.appendChild(this.document.createTextNode("\n"));
this.element.clipboardText = this.element.textContent;
@ -944,6 +986,12 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
container.textContent = this._message;
}
if (this.stack) {
let stack = new Widgets.Stacktrace(this, this.stack).render().element;
body.appendChild(this.document.createTextNode("\n"));
body.appendChild(stack);
}
return body;
},
@ -988,6 +1036,36 @@ Messages.Simple.prototype = Heritage.extend(Messages.BaseMessage.prototype,
line: line,
column: column});
},
/**
* The click event handler for the message expander arrow element. This method
* toggles the display of message details.
*
* @private
* @param nsIDOMEvent ev
* The DOM event object.
* @see this.toggleDetails()
*/
_onClickCollapsible: function(ev)
{
ev.preventDefault();
this.toggleDetails();
},
/**
* Expand/collapse message details.
*/
toggleDetails: function()
{
let twisty = this.element.querySelector(".theme-twisty");
if (this.element.hasAttribute("open")) {
this.element.removeAttribute("open");
twisty.removeAttribute("open");
} else {
this.element.setAttribute("open", true);
twisty.setAttribute("open", true);
}
},
}); // Messages.Simple.prototype
@ -1330,30 +1408,13 @@ Messages.ConsoleGeneric = function(packet)
this._repeatID.consoleApiLevel = packet.level;
this._repeatID.styles = packet.styles;
this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
this.stack = this._repeatID.stacktrace = packet.stacktrace;
this._styles = packet.styles || [];
this._onClickCollapsible = this._onClickCollapsible.bind(this);
};
Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
{
_styles: null,
_stacktrace: null,
/**
* Tells if the message can be expanded/collapsed.
* @type boolean
*/
collapsible: false,
/**
* Getter that tells if this message is collapsed - no details are shown.
* @type boolean
*/
get collapsed() {
return this.collapsible && this.element && !this.element.hasAttribute("open");
},
_renderBodyPieceSeparator: function()
{
@ -1373,25 +1434,9 @@ Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
location.target = "jsdebugger";
}
let stack = null;
let twisty = null;
if (this._stacktrace && this._stacktrace.length > 0) {
stack = new Widgets.Stacktrace(this, this._stacktrace).render().element;
twisty = this.document.createElementNS(XHTML_NS, "a");
twisty.className = "theme-twisty";
twisty.href = "#";
twisty.title = l10n.getStr("messageToggleDetails");
twisty.addEventListener("click", this._onClickCollapsible);
}
let flex = this.document.createElementNS(XHTML_NS, "span");
flex.className = "message-flex-body";
if (twisty) {
flex.appendChild(twisty);
}
flex.appendChild(msg);
if (repeatNode) {
@ -1404,24 +1449,11 @@ Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
let result = this.document.createDocumentFragment();
result.appendChild(flex);
if (stack) {
result.appendChild(this.document.createTextNode("\n"));
result.appendChild(stack);
}
this._message = result;
this._stacktrace = null;
Messages.Simple.prototype.render.call(this);
if (stack) {
this.collapsible = true;
this.element.setAttribute("collapsible", true);
let icon = this.element.querySelector(".icon");
icon.addEventListener("click", this._onClickCollapsible);
}
return this;
},
@ -1484,36 +1516,6 @@ Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
_renderLocation: function() { },
_renderRepeatNode: function() { },
/**
* Expand/collapse message details.
*/
toggleDetails: function()
{
let twisty = this.element.querySelector(".theme-twisty");
if (this.element.hasAttribute("open")) {
this.element.removeAttribute("open");
twisty.removeAttribute("open");
} else {
this.element.setAttribute("open", true);
twisty.setAttribute("open", true);
}
},
/**
* The click event handler for the message expander arrow element. This method
* toggles the display of message details.
*
* @private
* @param nsIDOMEvent ev
* The DOM event object.
* @see this.toggleDetails()
*/
_onClickCollapsible: function(ev)
{
ev.preventDefault();
this.toggleDetails();
},
/**
* Given a style attribute value, return a cleaned up version of the string
* such that:

View File

@ -129,6 +129,7 @@ support-files =
test-bug_1050691_click_function_to_source.html
test-bug_1050691_click_function_to_source.js
test-console-api-stackframe.html
test-exception-stackframe.html
test_bug_1010953_cspro.html^headers^
test_bug_1010953_cspro.html
test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^
@ -383,6 +384,7 @@ skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
[browser_webconsole_autocomplete_crossdomain_iframe.js]
[browser_webconsole_console_custom_styles.js]
[browser_webconsole_console_api_stackframe.js]
[browser_webconsole_exception_stackframe.js]
[browser_webconsole_column_numbers.js]
[browser_console_open_or_focus.js]
[browser_webconsole_bug_922212_console_dirxml.js]

View File

@ -57,7 +57,7 @@ function test() {
let msg = [...result.matched][0];
ok(msg, "message element found");
let locationNode = msg.querySelector(".message-location");
let locationNode = msg.querySelector(".message > .message-location");
ok(locationNode, "message location element found");
let title = locationNode.getAttribute("title");
@ -78,7 +78,7 @@ function test() {
browserconsole.iframeWindow);
info("wait for click on locationNode");
yield clickPromise;
yield clickPromise.promise;
info("view-source url: " + URL);
ok(URL, "we have some source URL after the click");

View File

@ -58,7 +58,7 @@ function test() {
let msg = [...results[0].matched][0];
ok(msg, "message element found for: " + result.text);
let locationNode = msg.querySelector(".message-location");
let locationNode = msg.querySelector(".message > .message-location");
ok(locationNode, "message location element found");
EventUtils.synthesizeMouse(locationNode, 2, 2, {}, hud.iframeWindow);

View File

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the console receive exceptions include a stackframe.
// See bug 1184172.
// On e10s, the exception is triggered in child process
// and is ignored by test harness
if (!Services.appinfo.browserTabsRemoteAutostart) {
expectUncaughtException();
}
function test() {
let hud;
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
"test/test-exception-stackframe.html";
const TEST_FILE = TEST_URI.substr(TEST_URI.lastIndexOf("/"));
Task.spawn(runner).then(finishTest);
function* runner() {
const {tab} = yield loadTab(TEST_URI);
hud = yield openConsole(tab);
const stack = [{
file: TEST_FILE,
fn: "thirdCall",
line: 21,
}, {
file: TEST_FILE,
fn: "secondCall",
line: 17,
}, {
file: TEST_FILE,
fn: "firstCall",
line: 12,
}];
let results = yield waitForMessages({
webconsole: hud,
messages: [{
text: "nonExistingMethodCall is not defined",
category: CATEGORY_JS,
severity: SEVERITY_ERROR,
collapsible: true,
stacktrace: stack,
}],
});
let elem = [...results[0].matched][0];
ok(elem, "message element");
let msg = elem._messageObject;
ok(msg, "message object");
ok(msg.collapsed, "message is collapsed");
msg.toggleDetails();
ok(!msg.collapsed, "message is not collapsed");
msg.toggleDetails();
ok(msg.collapsed, "message is collapsed");
yield closeConsole(tab);
}
}

View File

@ -1079,7 +1079,7 @@ function waitForMessages(options) {
let file = frame.querySelector(".message-location").title;
if (!checkText(expected.file, file)) {
ok(false, "frame #" + i + " does not match file name: " +
expected.file);
expected.file + " != " + file);
displayErrorContext(rule, element);
return false;
}
@ -1089,7 +1089,7 @@ function waitForMessages(options) {
let fn = frame.querySelector(".function").textContent;
if (!checkText(expected.fn, fn)) {
ok(false, "frame #" + i + " does not match the function name: " +
expected.fn);
expected.fn + " != " + fn);
displayErrorContext(rule, element);
return false;
}
@ -1099,7 +1099,7 @@ function waitForMessages(options) {
let line = frame.querySelector(".message-location").sourceLine;
if (!checkText(expected.line, line)) {
ok(false, "frame #" + i + " does not match the line number: " +
expected.line);
expected.line + " != " + line);
displayErrorContext(rule, element);
return false;
}

View File

@ -0,0 +1,30 @@
<!DOCTYPE HTML>
<html dir="ltr" lang="en">
<head>
<meta charset="utf8">
<!--
- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/
-->
<title>Test for bug 1184172 - stacktraces for exceptions</title>
<script>
function firstCall() {
secondCall();
}
// Check anonymous functions
var secondCall = function () {
thirdCall();
}
function thirdCall() {
nonExistingMethodCall();
}
window.onload = firstCall;
</script>
</head>
<body>
<p>Hello world!</p>
</body>
</html>

View File

@ -1528,6 +1528,7 @@ WebConsoleFrame.prototype = {
line: aScriptError.lineNumber,
column: aScriptError.columnNumber
},
stack: aScriptError.stacktrace,
category: category,
severity: severity,
timestamp: aScriptError.timeStamp,

View File

@ -1288,6 +1288,21 @@ WebConsoleActor.prototype =
*/
preparePageErrorForRemote: function WCA_preparePageErrorForRemote(aPageError)
{
let stack = null;
// Convert stack objects to the JSON attributes expected by client code
if (aPageError.stack) {
stack = [];
let s = aPageError.stack;
while (s !== null) {
stack.push({
filename: s.source,
lineNumber: s.line,
columnNumber: s.column,
functionName: s.functionDisplayName
});
s = s.parent;
}
}
let lineText = aPageError.sourceLine;
if (lineText && lineText.length > DebuggerServer.LONG_STRING_INITIAL_LENGTH) {
lineText = lineText.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
@ -1307,6 +1322,7 @@ WebConsoleActor.prototype =
strict: !!(aPageError.flags & aPageError.strictFlag),
info: !!(aPageError.flags & aPageError.infoFlag),
private: aPageError.isFromPrivateWindow,
stacktrace: stack
};
},