Bug 920116 - Show full stack traces for console logged messages; r=robcee
authorMihai Sucan <mihai.sucan@gmail.com>
Tue, 22 Apr 2014 21:45:04 +0300
changeset 198185 1c12ded4cd82065787ddebedc35f7bfadc2a506c
parent 198132 4c8ee55dbdd1e3b08e585ba7f2cab8ce44cd0bab
child 198186 505186fee60c6a85c8983858de5bd8ef8a3cbaac
push idunknown
push userunknown
push dateunknown
reviewersrobcee
bugs920116
milestone31.0a1
Bug 920116 - Show full stack traces for console logged messages; r=robcee
browser/devtools/webconsole/console-output.js
browser/devtools/webconsole/test/browser.ini
browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
browser/devtools/webconsole/test/browser_console_click_focus.js
browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
browser/devtools/webconsole/test/browser_webconsole_console_api_stackframe.js
browser/devtools/webconsole/test/browser_webconsole_console_custom_styles.js
browser/devtools/webconsole/test/browser_webconsole_jsterm.js
browser/devtools/webconsole/test/head.js
browser/devtools/webconsole/test/test-bug-766001-console-log.js
browser/devtools/webconsole/test/test-console-api-stackframe.html
browser/devtools/webconsole/webconsole.js
browser/locales/en-US/chrome/browser/devtools/webconsole.properties
browser/themes/shared/devtools/webconsole.inc.css
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -865,17 +865,17 @@ Messages.Simple.prototype = Heritage.ext
   /**
    * Render the message body DOM element.
    * @private
    * @return Element
    */
   _renderBody: function()
   {
     let body = this.document.createElementNS(XHTML_NS, "span");
-    body.className = "body devtools-monospace";
+    body.className = "message-body-wrapper message-body devtools-monospace";
 
     let anchor, container = body;
     if (this._link || this._linkCallback) {
       container = anchor = this.document.createElementNS(XHTML_NS, "a");
       anchor.href = this._link || "#";
       anchor.draggable = false;
       this._addLinkCallback(anchor, this._linkCallback);
       body.appendChild(anchor);
@@ -1218,70 +1218,158 @@ Messages.ConsoleGeneric = function(packe
     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
     private: packet.private,
     filterDuplicates: true,
     location: {
       url: packet.filename,
       line: packet.lineNumber,
     },
   };
+
   switch (packet.level) {
     case "count": {
       let counter = packet.counter, label = counter.label;
       if (!label) {
         label = l10n.getStr("noCounterLabel");
       }
       Messages.Extended.call(this, [label+ ": " + counter.count], options);
       break;
     }
     default:
       Messages.Extended.call(this, packet.arguments, options);
       break;
   }
 
   this._repeatID.consoleApiLevel = packet.level;
   this._repeatID.styles = packet.styles;
+  this._stacktrace = 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()
   {
     return this.document.createTextNode(" ");
   },
 
   render: function()
   {
+    let msg = this.document.createElementNS(XHTML_NS, "span");
+    msg.className = "message-body devtools-monospace";
+
+    this._renderBodyPieces(msg);
+
+    let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
+    let location = Messages.Simple.prototype._renderLocation.call(this);
+    if (location) {
+      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) {
+      flex.appendChild(repeatNode);
+    }
+    if (location) {
+      flex.appendChild(location);
+    }
+
+    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;
+  },
+
+  _renderBody: function()
+  {
+    let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
+    body.classList.remove("devtools-monospace", "message-body");
+    return body;
+  },
+
+  _renderBodyPieces: function(container)
+  {
     let lastStyle = null;
-    let result = this.document.createDocumentFragment();
 
     for (let i = 0; i < this._messagePieces.length; i++) {
       let separator = i > 0 ? this._renderBodyPieceSeparator() : null;
       if (separator) {
-        result.appendChild(separator);
+        container.appendChild(separator);
       }
 
       let piece = this._messagePieces[i];
       let style = this._styles[i];
 
       // No long string support.
       if (style && typeof style == "string" ) {
         lastStyle = this.cleanupStyle(style);
       }
 
-      result.appendChild(this._renderBodyPiece(piece, lastStyle));
+      container.appendChild(this._renderBodyPiece(piece, lastStyle));
     }
 
-    this._message = result;
     this._messagePieces = null;
     this._styles = null;
-    return Messages.Simple.prototype.render.call(this);
   },
 
   _renderBodyPiece: function(piece, style)
   {
     let elem = Messages.Extended.prototype._renderBodyPiece.call(this, piece);
     let result = elem;
 
     if (style) {
@@ -1293,16 +1381,51 @@ Messages.ConsoleGeneric.prototype = Heri
         span.appendChild(elem);
         result = span;
       }
     }
 
     return result;
   },
 
+  // no-op for the message location and .repeats elements.
+  // |this.render()| handles customized message output.
+  _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:
    *
    * - no external URL is allowed to load. See RE_CLEANUP_STYLES.
    * - only some of the properties are allowed, based on a whitelist. See
    *   RE_ALLOWED_STYLES.
    *
@@ -1350,17 +1473,17 @@ Messages.ConsoleGeneric.prototype = Heri
  * @constructor
  * @extends Messages.Simple
  * @param object packet
  *        The Console API call packet received from the server.
  */
 Messages.ConsoleTrace = function(packet)
 {
   let options = {
-    className: "consoleTrace cm-s-mozilla",
+    className: "cm-s-mozilla",
     timestamp: packet.timeStamp,
     category: "webdev",
     severity: CONSOLE_API_LEVELS_TO_SEVERITIES[packet.level],
     private: packet.private,
     filterDuplicates: true,
     location: {
       url: packet.filename,
       line: packet.lineNumber,
@@ -1407,16 +1530,23 @@ Messages.ConsoleTrace.prototype = Herita
         }
       }
     }
     this._arguments = null;
 
     return result;
   },
 
+  render: function()
+  {
+    Messages.Simple.prototype.render.apply(this, arguments);
+    this.element.setAttribute("open", true);
+    return this;
+  },
+
   /**
    * Render the stack frames.
    *
    * @private
    * @return DOMElement
    */
   _renderStack: function()
   {
@@ -1424,31 +1554,32 @@ Messages.ConsoleTrace.prototype = Herita
     cmvar.className = "cm-variable";
     cmvar.textContent = "console";
 
     let cmprop = this.document.createElementNS(XHTML_NS, "span");
     cmprop.className = "cm-property";
     cmprop.textContent = "trace";
 
     let title = this.document.createElementNS(XHTML_NS, "span");
-    title.className = "title devtools-monospace";
+    title.className = "message-body devtools-monospace";
     title.appendChild(cmvar);
     title.appendChild(this.document.createTextNode("."));
     title.appendChild(cmprop);
     title.appendChild(this.document.createTextNode("():"));
 
     let repeatNode = Messages.Simple.prototype._renderRepeatNode.call(this);
     let location = Messages.Simple.prototype._renderLocation.call(this);
     if (location) {
       location.target = "jsdebugger";
     }
 
     let widget = new Widgets.Stacktrace(this, this._stacktrace).render();
 
-    let body = this.document.createElementNS(XHTML_NS, "div");
+    let body = this.document.createElementNS(XHTML_NS, "span");
+    body.className = "message-flex-body";
     body.appendChild(title);
     if (repeatNode) {
       body.appendChild(repeatNode);
     }
     if (location) {
       body.appendChild(location);
     }
     body.appendChild(this.document.createTextNode("\n"));
@@ -1458,17 +1589,17 @@ Messages.ConsoleTrace.prototype = Herita
     frag.appendChild(widget.element);
 
     return frag;
   },
 
   _renderBody: function()
   {
     let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
-    body.classList.remove("devtools-monospace");
+    body.classList.remove("devtools-monospace", "message-body");
     return body;
   },
 
   // no-op for the message location and .repeats elements.
   // |this._renderStack| handles customized message output.
   _renderLocation: function() { },
   _renderRepeatNode: function() { },
 }); // Messages.ConsoleTrace.prototype
@@ -2717,17 +2848,16 @@ Widgets.Stacktrace.prototype = Heritage.
 
     let elem = this.document.createElementNS(XHTML_NS, "li");
     elem.appendChild(fn);
     elem.appendChild(location);
     elem.appendChild(this.document.createTextNode("\n"));
 
     return elem;
   },
-
 }); // Widgets.Stacktrace.prototype
 
 
 function gSequenceId()
 {
   return gSequenceId.n++;
 }
 gSequenceId.n = 0;
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -108,16 +108,17 @@ support-files =
   test-bug_923281_console_log_filter.html
   test-bug_923281_test1.js
   test-bug_923281_test2.js
   test-bug_939783_console_trace_duplicates.html
   test-bug-952277-highlight-nodes-in-vview.html
   test-bug-609872-cd-iframe-parent.html
   test-bug-609872-cd-iframe-child.html
   test-bug-989025-iframe-parent.html
+  test-console-api-stackframe.html
 
 [browser_bug664688_sandbox_update_after_navigation.js]
 [browser_bug_638949_copy_link_location.js]
 [browser_bug_862916_console_dir_and_filter_off.js]
 [browser_bug_865288_repeat_different_objects.js]
 [browser_bug_865871_variables_view_close_on_esc_key.js]
 [browser_bug_869003_inspect_cross_domain_object.js]
 [browser_bug_871156_ctrlw_close_tab.js]
@@ -278,8 +279,9 @@ run-if = os == "mac"
 [browser_webconsole_output_dom_elements_04.js]
 [browser_webconsole_output_events.js]
 [browser_console_variables_view_highlighter.js]
 [browser_webconsole_start_netmon_first.js]
 [browser_webconsole_console_trace_duplicates.js]
 [browser_webconsole_cd_iframe.js]
 [browser_webconsole_autocomplete_crossdomain_iframe.js]
 [browser_webconsole_console_custom_styles.js]
+[browser_webconsole_console_api_stackframe.js]
--- a/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
+++ b/browser/devtools/webconsole/test/browser_bug_638949_copy_link_location.js
@@ -62,16 +62,17 @@ function onConsoleMessage(aResults) {
   output.focus();
   let message = [...aResults[0].matched][0];
 
   goUpdateCommand(COMMAND_NAME);
   ok(!isEnabled(), COMMAND_NAME + "is disabled");
 
   // Test that the "Copy Link Location" menu item is hidden for non-network
   // messages.
+  message.scrollIntoView();
   waitForContextMenu(menu, message, () => {
     let isHidden = menu.querySelector(CONTEXT_MENU_ID).hidden;
     ok(isHidden, CONTEXT_MENU_ID + " is hidden");
   }, testWithNetActivity);
 }
 
 function testWithNetActivity() {
   HUD.jsterm.clearOutput();
@@ -102,15 +103,16 @@ function onNetworkMessage(aResults) {
 
   waitForClipboard((aData) => { return aData.trim() == message.url; },
     () => { goDoCommand(COMMAND_NAME) },
     testMenuWithNetActivity, testMenuWithNetActivity);
 
   function testMenuWithNetActivity() {
     // Test that the "Copy Link Location" menu item is visible for network-related
     // messages.
+    message.scrollIntoView();
     waitForContextMenu(menu, message, () => {
       let isVisible = !menu.querySelector(CONTEXT_MENU_ID).hidden;
       ok(isVisible, CONTEXT_MENU_ID + " is visible");
     }, finishTest);
   }
 }
 
--- a/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
+++ b/browser/devtools/webconsole/test/browser_bug_865288_repeat_different_objects.js
@@ -48,17 +48,17 @@ function checkMessages([result])
   let m = -1;
 
   function nextMessage()
   {
     let msg = msgs[++m];
     if (msg) {
       ok(msg, "message element #" + m);
 
-      let clickable = msg.querySelector(".body a");
+      let clickable = msg.querySelector(".message-body a");
       ok(clickable, "clickable object #" + m);
 
       msg.scrollIntoView(false);
       clickObject(clickable);
     }
     else {
       finishTest();
     }
--- a/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
+++ b/browser/devtools/webconsole/test/browser_bug_865871_variables_view_close_on_esc_key.js
@@ -18,17 +18,17 @@ function test()
     let {tab} = yield loadTab(TEST_URI);
     hud = yield openConsole(tab);
     let jsterm = hud.jsterm;
 
     let msg = yield execute("fooObj");
     ok(msg, "output message found");
 
     let anchor = msg.querySelector("a");
-    let body = msg.querySelector(".body");
+    let body = msg.querySelector(".message-body");
     ok(anchor, "object anchor");
     ok(body, "message body");
     ok(body.textContent.contains('testProp: "testValue"'), "message text check");
 
     msg.scrollIntoView();
     executeSoon(() => {
       EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
     });
@@ -54,17 +54,17 @@ function test()
     });
     yield jsterm.once("sidebar-closed");
 
     jsterm.clearOutput();
 
     msg = yield execute("window.location");
     ok(msg, "output message found");
 
-    body = msg.querySelector(".body");
+    body = msg.querySelector(".message-body");
     ok(body, "message body");
     anchor = msg.querySelector("a");
     ok(anchor, "object anchor");
     ok(body.textContent.contains("Location \u2192 http://example.com/browser/"),
        "message text check");
 
     msg.scrollIntoView();
     executeSoon(() => {
--- a/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
+++ b/browser/devtools/webconsole/test/browser_bug_869003_inspect_cross_domain_object.js
@@ -42,17 +42,17 @@ function consoleOpened(hud)
   }).then(onConsoleMessage);
 }
 
 function onConsoleMessage(aResults)
 {
   let msg = [...aResults[0].matched][0];
   ok(msg, "message element");
 
-  let body = msg.querySelector(".body");
+  let body = msg.querySelector(".message-body");
   ok(body, "message body");
 
   let clickable = aResults[0].clickableElements[0];
   ok(clickable, "clickable object found");
   ok(body.textContent.contains('{ hello: "world!",'), "message text check");
 
   gJSTerm.once("variablesview-fetched", onObjFetch);
 
--- a/browser/devtools/webconsole/test/browser_console_click_focus.js
+++ b/browser/devtools/webconsole/test/browser_console_click_focus.js
@@ -20,17 +20,17 @@ function testInputFocus() {
       webconsole: hud,
       messages: [{
         text: "Dolske Digs Bacon",
         category: CATEGORY_WEBDEV,
         severity: SEVERITY_LOG,
       }],
     }).then(([result]) => {
       let msg = [...result.matched][0];
-      let outputItem = msg.querySelector(".body");
+      let outputItem = msg.querySelector(".message-body");
       ok(outputItem, "found a logged message");
       let inputNode = hud.jsterm.inputNode;
       ok(inputNode.getAttribute("focused"), "input node is focused, first");
 
       let lostFocus = () => {
         inputNode.removeEventListener("blur", lostFocus);
         info("input node lost focus");
       }
--- a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
+++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js
@@ -29,17 +29,17 @@ function performTest(hud)
       text: "fooBug676722",
       category: CATEGORY_WEBDEV,
       severity: SEVERITY_LOG,
       objects: true,
     }],
   }).then(([result]) => {
     let msg = [...result.matched][0];
     ok(msg, "message element");
-    let body = msg.querySelector(".body");
+    let body = msg.querySelector(".message-body");
     ok(body, "message body");
     let clickable = result.clickableElements[0];
     ok(clickable, "the console.log() object anchor was found");
     ok(body.textContent.contains('{ abba: "omgBug676722" }'),
        "clickable node content is correct");
 
     hud.jsterm.once("variablesview-fetched",
       (aEvent, aVar) => {
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
@@ -31,17 +31,17 @@ function tabLoad2(aEvent) {
     webconsole: HUD,
     messages: [{
       text: "test-console.html",
       category: CATEGORY_NETWORK,
       severity: SEVERITY_LOG,
     }],
   }).then(([result]) => {
     let msg = [...result.matched][0];
-    outputItem = msg.querySelector(".body .url");
+    outputItem = msg.querySelector(".message-body .url");
     ok(outputItem, "found a network message");
     document.addEventListener("popupshown", networkPanelShown, false);
 
     // Send the mousedown and click events such that the network panel opens.
     EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
     EventUtils.sendMouseEvent({type: "click"}, outputItem);
   });
 }
@@ -97,17 +97,17 @@ function networkPanelHidden(aEvent) {
     document.removeEventListener("popupshown", networkPanelShowFailure, false);
 
     // Done with the network output. Now test the jsterm output and the property
     // panel.
     HUD.jsterm.execute("document", (msg) => {
       info("jsterm execute 'document' callback");
 
       HUD.jsterm.once("variablesview-open", onVariablesViewOpen);
-      let outputItem = msg.querySelector(".body a");
+      let outputItem = msg.querySelector(".message-body a");
       ok(outputItem, "jsterm output message found");
 
       // Send the mousedown and click events such that the property panel opens.
       EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
       EventUtils.sendMouseEvent({type: "click"}, outputItem);
     });
   });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -61,17 +61,17 @@ function consoleOpened(HUD) {
   completion = JSPropertyProvider(dbgWindow, null, "window._container.");
   ok(completion, "matches available for window._container");
   ok(completion.matches.length, "matches available for window (length)");
 
   jsterm.clearOutput();
 
   jsterm.execute("window._container", (msg) => {
     jsterm.once("variablesview-fetched", testVariablesView.bind(null, HUD));
-    let anchor = msg.querySelector(".body a");
+    let anchor = msg.querySelector(".message-body a");
     EventUtils.synthesizeMouse(anchor, 2, 2, {}, HUD.iframeWindow);
   });
 }
 
 function testVariablesView(aWebconsole, aEvent, aView) {
   findVariableViewProperties(aView, [
     { name: "gen1", isGenerator: true },
     { name: "gen2", isGenerator: true },
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js
@@ -70,17 +70,17 @@ function autocompletePopupHidden()
 }
 
 function testPropertyPanel()
 {
   let jsterm = gHUD.jsterm;
   jsterm.clearOutput();
   jsterm.execute("document", (msg) => {
     jsterm.once("variablesview-fetched", onVariablesViewReady);
-    let anchor = msg.querySelector(".body a");
+    let anchor = msg.querySelector(".message-body a");
     EventUtils.synthesizeMouse(anchor, 2, 2, {}, gHUD.iframeWindow);
   });
 }
 
 function onVariablesViewReady(aEvent, aView)
 {
   findVariableViewProperties(aView, [
     { name: "body", value: "<body>" },
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_764572_output_open_url.js
@@ -53,16 +53,17 @@ function onConsoleMessage(aResults) {
   // Check if the command is disabled non-network messages.
   goUpdateCommand(COMMAND_NAME);
   let controller = top.document.commandDispatcher
                    .getControllerForCommand(COMMAND_NAME);
 
   let isDisabled = !controller || !controller.isCommandEnabled(COMMAND_NAME);
   ok(isDisabled, COMMAND_NAME + " should be disabled.");
 
+  outputNode.selectedItem.scrollIntoView();
   waitForContextMenu(contextMenu, outputNode.selectedItem, () => {
     let isHidden = contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
     ok(isHidden, CONTEXT_MENU_ID + " should be hidden.");
   }, testOnNetActivity);
 }
 
 function testOnNetActivity() {
   HUD.jsterm.clearOutput();
@@ -112,13 +113,14 @@ function onNetworkMessage(aResults) {
   // Try to open the URL.
   goDoCommand(COMMAND_NAME);
 }
 
 function testOnNetActivity_contextmenu(msg) {
   outputNode.focus();
   HUD.ui.output.selectMessage(msg);
 
+  msg.scrollIntoView();
   waitForContextMenu(contextMenu, msg, () => {
     let isShown = !contextMenu.querySelector(CONTEXT_MENU_ID).hidden;
     ok(isShown, CONTEXT_MENU_ID + " should be shown.");
   }, finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
@@ -1,13 +1,17 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Any copyright is dedicated to the Public Domain.
+/* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
- * ***** END LICENSE BLOCK ***** */
+ */
+
+// Test that message source links for js errors and console API calls open in
+// the jsdebugger when clicked.
+
+"use strict";
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test" +
                  "/test-bug-766001-js-console-links.html";
 
 function test() {
   let hud;
 
   requestLongerTimeout(2);
@@ -56,18 +60,16 @@ function test() {
 
     let line = node.sourceLine;
     ok(line, "found source line for index " + index);
 
     executeSoon(() => {
       EventUtils.sendMouseEvent({ type: "click" }, node);
     });
 
-    yield hud.ui.once("source-in-debugger-opened", checkLine.bind(null, url, line));
-  }
+    yield hud.ui.once("source-in-debugger-opened");
 
-  function* checkLine(url, line) {
     let toolbox = yield gDevTools.getToolbox(hud.target);
     let {panelWin: { DebuggerView: view }} = toolbox.getPanel("jsdebugger");
     is(view.Sources.selectedValue, url, "expected source url");
     is(view.editor.getCursor().line, line - 1, "expected source line");
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_api_stackframe.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the console API messages for console.error()/exception()/assert()
+// include a stackframe. See bug 920116.
+
+function test() {
+  let hud;
+
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-api-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: /\b2[123]\b/, // 21,22,23
+    }, {
+      file: TEST_FILE,
+      fn: "secondCall",
+      line: 16,
+    }, {
+      file: TEST_FILE,
+      fn: "firstCall",
+      line: 12,
+    }];
+
+    let results = yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "foo-log",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+        collapsible: false,
+      }, {
+        text: "foo-error",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_ERROR,
+        collapsible: true,
+        stacktrace: stack,
+      }, {
+        text: "foo-exception",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_ERROR,
+        collapsible: true,
+        stacktrace: stack,
+      }, {
+        text: "foo-assert",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_ERROR,
+        collapsible: true,
+        stacktrace: stack,
+      }],
+    });
+
+    let elem = [...results[1].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);
+  }
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_console_custom_styles.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_custom_styles.js
@@ -54,17 +54,17 @@ function test() {
         text: "foobar",
         category: CATEGORY_WEBDEV,
       }],
     });
 
     let msg = [...result.matched][0];
     ok(msg, "message element");
 
-    let span = msg.querySelector(".body span[style]");
+    let span = msg.querySelector(".message-body span[style]");
     ok(span, "span element");
 
     info("span textContent is: " + span.textContent);
     isnot(span.textContent.indexOf("foobar"), -1, "span textContent check");
 
     let outputStyle = span.getAttribute("style").replace(/\s+|;+$/g, "");
     if (check.sameStyleExpected) {
       is(outputStyle, check.style, "span style is correct");
--- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
@@ -25,17 +25,17 @@ function nextTest() {
 function checkResult(msg, desc) {
   waitForMessages({
     webconsole: jsterm.hud.owner,
     messages: [{
       name: desc,
       category: CATEGORY_OUTPUT,
     }],
   }).then(([result]) => {
-    let node = [...result.matched][0].querySelector(".body");
+    let node = [...result.matched][0].querySelector(".message-body");
     if (typeof msg == "string") {
       is(node.textContent.trim(), msg,
         "correct message shown for " + desc);
     }
     else if (typeof msg == "function") {
       ok(msg(node), "correct message shown for " + desc);
     }
 
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -1,13 +1,15 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let {require, TargetFactory} = devtools;
 let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils");
 let {Messages} = require("devtools/webconsole/console-output");
@@ -877,24 +879,28 @@ function openDebugger(aOptions = {})
  *            Provide this if you want to match a console.time() message.
  *            - consoleTimeEnd: same as above, but for console.timeEnd().
  *            - consoleDir: boolean, set to |true| to match a console.dir()
  *            message.
  *            - consoleGroup: boolean, set to |true| to match a console.group()
  *            message.
  *            - longString: boolean, set to |true} to match long strings in the
  *            message.
+ *            - collapsible: boolean, set to |true| to match messages that can
+ *            be collapsed/expanded.
  *            - type: match messages that are instances of the given object. For
  *            example, you can point to Messages.NavigationMarker to match any
  *            such message.
  *            - objects: boolean, set to |true| if you expect inspectable
  *            objects in the message.
  *            - source: object of the shape { url, line }. This is used to
  *            match the source URL and line number of the error message or
  *            console API call.
+ *            - stacktrace: array of objects of the form { file, fn, line } that
+ *            can match frames in the stacktrace associated with the message.
  *            - groupDepth: number used to check the depth of the message in
  *            a group.
  *            - url: URL to match for network requests.
  * @return object
  *         A promise object is returned once the messages you want are found.
  *         The promise is resolved with the array of rule objects you give in
  *         the |messages| property. Each objects is the same as provided, with
  *         additional properties:
@@ -913,73 +919,53 @@ function waitForMessages(aOptions)
   let rules = WebConsoleUtils.cloneObject(aOptions.messages, true);
   let rulesMatched = 0;
   let listenerAdded = false;
   let deferred = promise.defer();
   aOptions.matchCondition = aOptions.matchCondition || "all";
 
   function checkText(aRule, aText)
   {
-    let result;
+    let result = false;
     if (Array.isArray(aRule)) {
       result = aRule.every((s) => checkText(s, aText));
     }
     else if (typeof aRule == "string") {
       result = aText.indexOf(aRule) > -1;
     }
     else if (aRule instanceof RegExp) {
       result = aRule.test(aText);
     }
+    else {
+      result = aRule == aText;
+    }
     return result;
   }
 
   function checkConsoleTrace(aRule, aElement)
   {
     let elemText = aElement.textContent;
     let trace = aRule.consoleTrace;
 
     if (!checkText("console.trace():", elemText)) {
       return false;
     }
 
-    let frame = aElement.querySelector(".stacktrace li:first-child");
-    if (trace.file) {
-      let file = frame.querySelector(".message-location").title;
-      if (!checkText(trace.file, file)) {
-        ok(false, "console.trace() message is missing the file name: " +
-                  trace.file);
-        displayErrorContext(aRule, aElement);
-        return false;
-      }
-    }
-
-    if (trace.fn) {
-      let fn = frame.querySelector(".function").textContent;
-      if (!checkText(trace.fn, fn)) {
-        ok(false, "console.trace() message is missing the function name: " +
-                  trace.fn);
-        displayErrorContext(aRule, aElement);
-        return false;
-      }
-    }
-
-    if (trace.line) {
-      let line = frame.querySelector(".message-location").sourceLine;
-      if (!checkText(trace.line, line)) {
-        ok(false, "console.trace() message is missing the line number: " +
-                  trace.line);
-        displayErrorContext(aRule, aElement);
-        return false;
-      }
-    }
-
     aRule.category = CATEGORY_WEBDEV;
     aRule.severity = SEVERITY_LOG;
     aRule.type = Messages.ConsoleTrace;
 
+    if (!aRule.stacktrace && typeof trace == "object" && trace !== true) {
+      if (Array.isArray(trace)) {
+        aRule.stacktrace = trace;
+      } else {
+        aRule.stacktrace = [trace];
+      }
+    }
+
     return true;
   }
 
   function checkConsoleTime(aRule, aElement)
   {
     let elemText = aElement.textContent;
     let time = aRule.consoleTime;
 
@@ -1053,16 +1039,76 @@ function waitForMessages(aOptions)
 
     if ("line" in aRule.source && location.sourceLine != aRule.source.line) {
       return false;
     }
 
     return true;
   }
 
+  function checkCollapsible(aRule, aElement)
+  {
+    let msg = aElement._messageObject;
+    if (!msg || !!msg.collapsible != aRule.collapsible) {
+      return false;
+    }
+
+    return true;
+  }
+
+  function checkStacktrace(aRule, aElement)
+  {
+    let stack = aRule.stacktrace;
+    let frames = aElement.querySelectorAll(".stacktrace > li");
+    if (!frames.length) {
+      return false;
+    }
+
+    for (let i = 0; i < stack.length; i++) {
+      let frame = frames[i];
+      let expected = stack[i];
+      if (!frame) {
+        ok(false, "expected frame #" + i + " but didnt find it");
+        return false;
+      }
+
+      if (expected.file) {
+        let file = frame.querySelector(".message-location").title;
+        if (!checkText(expected.file, file)) {
+          ok(false, "frame #" + i + " does not match file name: " +
+                    expected.file);
+          displayErrorContext(aRule, aElement);
+          return false;
+        }
+      }
+
+      if (expected.fn) {
+        let fn = frame.querySelector(".function").textContent;
+        if (!checkText(expected.fn, fn)) {
+          ok(false, "frame #" + i + " does not match the function name: " +
+                    expected.fn);
+          displayErrorContext(aRule, aElement);
+          return false;
+        }
+      }
+
+      if (expected.line) {
+        let line = frame.querySelector(".message-location").sourceLine;
+        if (!checkText(expected.line, line)) {
+          ok(false, "frame #" + i + " does not match the line number: " +
+                    expected.line);
+          displayErrorContext(aRule, aElement);
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
   function checkMessage(aRule, aElement)
   {
     let elemText = aElement.textContent;
 
     if (aRule.text && !checkText(aRule.text, elemText)) {
       return false;
     }
 
@@ -1089,16 +1135,20 @@ function waitForMessages(aOptions)
     if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) {
       return false;
     }
 
     if (aRule.source && !checkSource(aRule, aElement)) {
       return false;
     }
 
+    if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) {
+      return false;
+    }
+
     let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime ||
                           aRule.consoleTimeEnd);
 
     // The rule tries to match the newer types of messages, based on their
     // object constructor.
     if (aRule.type) {
       if (!aElement._messageObject ||
           !(aElement._messageObject instanceof aRule.type)) {
@@ -1124,16 +1174,28 @@ function waitForMessages(aOptions)
       if (partialMatch) {
         is(aElement.severity, aRule.severity,
            "message severity for rule: " + displayRule(aRule));
         displayErrorContext(aRule, aElement);
       }
       return false;
     }
 
+    if (aRule.text) {
+      partialMatch = true;
+    }
+
+    if (aRule.stacktrace && !checkStacktrace(aRule, aElement)) {
+      if (partialMatch) {
+        ok(false, "failed to match stacktrace for rule: " + displayRule(aRule));
+        displayErrorContext(aRule, aElement);
+      }
+      return false;
+    }
+
     if (aRule.category == CATEGORY_NETWORK && "url" in aRule &&
         !checkText(aRule.url, aElement.url)) {
       return false;
     }
 
     if ("repeats" in aRule) {
       let repeats = aElement.querySelector(".message-repeats");
       if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
@@ -1161,17 +1223,17 @@ function waitForMessages(aOptions)
           displayErrorContext(aRule, aElement);
         }
         return false;
       }
       aRule.longStrings = longStrings;
     }
 
     if ("objects" in aRule) {
-      let clickables = aElement.querySelectorAll(".body a");
+      let clickables = aElement.querySelectorAll(".message-body a");
       if (aRule.objects != !!clickables[0]) {
         if (partialMatch) {
           is(!!clickables[0], aRule.objects,
              "objects existence check failed for message rule: " +
              displayRule(aRule));
           displayErrorContext(aRule, aElement);
         }
         return false;
@@ -1405,17 +1467,18 @@ function checkOutputForInputs(hud, input
     }
     if (typeof entry.inspectorIcon == "boolean") {
       yield checkLinkToInspector(entry, msg);
     }
   }
 
   function checkObjectClick(entry, msg)
   {
-    let body = msg.querySelector(".body a") || msg.querySelector(".body");
+    let body = msg.querySelector(".message-body a") ||
+               msg.querySelector(".message-body");
     ok(body, "the message body");
 
     let deferred = promise.defer();
 
     entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferred);
     hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
     eventHandlers.add(entry._onVariablesViewOpen);
 
--- a/browser/devtools/webconsole/test/test-bug-766001-console-log.js
+++ b/browser/devtools/webconsole/test/test-bug-766001-console-log.js
@@ -1,8 +1,10 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-window.addEventListener("load", function() {
+function onLoad123() {
   console.log("Blah Blah");
-}, false);
+}
+
+window.addEventListener("load", onLoad123, false);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-console-api-stackframe.html
@@ -0,0 +1,32 @@
+<!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 920116 - stacktraces for console API messages</title>
+    <script>
+      function firstCall() {
+        secondCall();
+      }
+
+      function secondCall() {
+        thirdCall();
+      }
+
+      function thirdCall() {
+        console.log("foo-log");
+        console.error("foo-error");
+        console.exception("foo-exception");
+        console.assert("red" == "blue", "foo-assert");
+      }
+
+      window.onload = firstCall;
+    </script>
+  </head>
+  <body>
+    <p>Hello world!</p>
+  </body>
+</html>
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -1368,17 +1368,17 @@ WebConsoleFrame.prototype = {
 
     let node = this.createMessageNode(aCategory, severity,
                                       errorMessage,
                                       aScriptError.sourceName,
                                       aScriptError.lineNumber, null, null,
                                       aScriptError.timeStamp);
 
     // Select the body of the message node that is displayed in the console
-    let msgBody = node.getElementsByClassName("body")[0];
+    let msgBody = node.getElementsByClassName("message-body")[0];
     // Add the more info link node to messages that belong to certain categories
     this.addMoreInfoLink(msgBody, aScriptError);
 
     if (aScriptError.private) {
       node.setAttribute("private", true);
     }
 
     if (objectActors.size > 0) {
@@ -2456,17 +2456,17 @@ WebConsoleFrame.prototype = {
     iconContainer.className = "icon";
 
     // Apply the current group by indenting appropriately.
     let iconMarginLeft = this.groupDepth * GROUP_INDENT + GROUP_INDENT_DEFAULT;
     iconContainer.style.marginLeft = iconMarginLeft + "px";
 
     // Create the message body, which contains the actual text of the message.
     let bodyNode = this.document.createElementNS(XHTML_NS, "span");
-    bodyNode.className = "body devtools-monospace";
+    bodyNode.className = "message-body-wrapper message-body devtools-monospace";
 
     // Store the body text, since it is needed later for the variables view.
     let body = aBody;
     // If a string was supplied for the body, turn it into a DOM node and an
     // associated clipboard string now.
     aClipboardText = aClipboardText ||
                      (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
                               (aSourceLine ? ":" + aSourceLine : ""));
@@ -2603,17 +2603,19 @@ WebConsoleFrame.prototype = {
     }
 
     filenameNode.className = "filename";
     filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation"));
     locationNode.appendChild(filenameNode);
 
     locationNode.href = isScratchpad || !fullURL ? "#" : fullURL;
     locationNode.draggable = false;
-    locationNode.target = aTarget;
+    if (aTarget) {
+      locationNode.target = aTarget;
+    }
     locationNode.setAttribute("title", aSourceURL);
     locationNode.className = "message-location theme-link devtools-monospace";
 
     // Make the location clickable.
     let onClick = () => {
       let target = locationNode.target;
       if (target == "scratchpad" || isScratchpad) {
         this.owner.viewSourceInScratchpad(aSourceURL);
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -217,8 +217,13 @@ messageRepeats.tooltip2=#1 repeat;#1 rep
 # 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.
+
+# LOCALIZATION NOTE (messageToggleDetails): the text that is displayed when
+# you hover the arrow for expanding/collapsing the message details. For
+# console.error() and other messages we show the stacktrace.
+messageToggleDetails=Show/hide message details.
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -33,20 +33,18 @@ a {
   background: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 0, 1, 0, 0) no-repeat;
   background-position: center 0.5em;
   flex: 0 0 auto;
   margin: 0 6px;
   padding: 0 4px;
   width: 8px;
 }
 
-.message > .body {
+.message > .message-body-wrapper {
   flex: 1 1 100%;
-  white-space: pre-wrap;
-  word-wrap: break-word;
   margin-top: 4px;
 }
 
 /* The red bubble that shows the number of times a message is repeated */
 .message-repeats {
   -moz-user-select: none;
   flex: 0 0 auto;
   margin: 2px 6px;
@@ -66,17 +64,17 @@ a {
 
 .message-location {
   -moz-margin-start: 6px;
   display: flex;
   flex: 0 0 auto;
   align-self: flex-start;
   justify-content: flex-end;
   width: 10em;
-  margin-top: 4px;
+  margin: 3px 0;
   color: -moz-nativehyperlinktext;
   text-decoration: none;
   white-space: nowrap;
 }
 
 .message-location:hover,
 .message-location:focus {
   text-decoration: underline;
@@ -87,16 +85,30 @@ a {
   text-align: end;
   overflow: hidden;
 }
 
 .message-location > .line-number {
   flex: 0 0 auto;
 }
 
+.message-flex-body {
+  display: flex;
+}
+
+.message-body {
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+
+.message-flex-body > .message-body {
+  display: block;
+  flex: 1 1 auto;
+}
+
 .jsterm-input-container {
   border-top-width: 1px;
   border-top-style: solid;
 }
 
 #output-wrapper {
   direction: ltr;
   overflow: auto;
@@ -148,25 +160,33 @@ a {
 }
 
 /* Network styles */
 .webconsole-filter-button[category="net"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#444444, #000000);
   border-color: #777;
 }
 
+.theme-light .message[severity=error] {
+  background-color: rgba(255, 150, 150, 0.3);
+}
+
+.theme-dark .message[severity=error] {
+  background-color: rgba(255, 100, 100, 0.3);
+}
+
 .message[category=network] > .icon {
   -moz-border-start: solid #000 6px;
 }
 
 .message[category=network][severity=error] > .icon {
   background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 0, 16, 8, 8);
 }
 
-.message[category=network] > .body {
+.message[category=network] > .message-body {
   display: flex;
 }
 
 .message[category=network] .method {
   flex: 0 0 auto;
 }
 
 .message[category=network]:not(.navigation-marker) .url {
@@ -279,17 +299,17 @@ a {
   background: -moz-image-rect(url("chrome://browser/skin/devtools/commandline-icon.png"), 0, 32, 16, 16) no-repeat;
 }
 
 :-moz-any(.jsterm-input-node,
           .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
-.inlined-variables-view .body {
+.inlined-variables-view .message-body {
   display: flex;
   flex-direction: column;
 }
 .inlined-variables-view iframe {
   display: block;
   flex: 1;
   margin-top: 5px;
   margin-bottom: 15px;
@@ -335,35 +355,43 @@ a {
   font-size: 0.9em;
 }
 
 .navigation-marker .url {
   -moz-padding-end: 9px;
   text-decoration: none;
 }
 
-.consoleTrace .body > div {
-  display: flex;
-  margin-bottom: 5px;
-}
-
-.consoleTrace .title {
-  display: block;
-  flex: 1 1 auto;
+.stacktrace {
+  display: none;
+  list-style: none;
+  padding: 0 1em 0 1.5em;
+  margin: 5px 0 0 0;
+  max-height: 10em;
+  overflow-y: auto;
+  border: 1px solid rgb(200,200,200);
+  border-radius: 3px;
 }
 
-.stacktrace {
-  list-style: none;
-  padding: 0 1em 0 1.5em;
-  margin: 0;
-  max-height: 10em;
-  overflow-y: auto;
+.theme-light .message[severity=error] .stacktrace {
+  background-color: rgba(255, 255, 255, 0.5);
+}
+
+.theme-dark .message[severity=error] .stacktrace {
+  background-color: rgba(0, 0, 0, 0.5);
+}
 
-  border: 1px solid rgba(128, 128, 128, .5);
-  border-radius: 3px;
+.message[open] .stacktrace {
+  display: block;
+}
+
+.message .theme-twisty {
+  display: inline-block;
+  vertical-align: middle;
+  margin: 2px 3px 0 0;
 }
 
 .stacktrace li {
   display: flex;
   margin: 0;
 }
 
 .stacktrace .function {