Bug 939783 - console.trace() group traces even if part of trace is different; r=robcee
authorMihai Sucan <mihai.sucan@gmail.com>
Thu, 23 Jan 2014 23:37:32 +0200
changeset 181107 2552d554372d96baf3e26ddaf66468886b51cbb9
parent 181106 f9a4e354878bc5503def366a4c6275f4967798fd
child 181108 3977d57df6568053fa6d19e092738ca5d183078b
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobcee
bugs939783
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 939783 - console.trace() group traces even if part of trace is different; r=robcee
browser/devtools/scratchpad/test/browser_scratchpad_wrong_window_focus.js
browser/devtools/webconsole/console-output.js
browser/devtools/webconsole/test/browser.ini
browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
browser/devtools/webconsole/test/browser_console_error_source_click.js
browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
browser/devtools/webconsole/test/browser_webconsole_bug_766001_JS_Console_in_Debugger.js
browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
browser/devtools/webconsole/test/browser_webconsole_console_trace_duplicates.js
browser/devtools/webconsole/test/browser_webconsole_scratchpad_panel_link.js
browser/devtools/webconsole/test/browser_webconsole_view_source.js
browser/devtools/webconsole/test/head.js
browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.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/scratchpad/test/browser_scratchpad_wrong_window_focus.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_wrong_window_focus.js
@@ -43,17 +43,17 @@ function test()
 }
 
 function testFocus(sw, hud) {
   let sp = sw.Scratchpad;
 
   function onMessage(event, messages) {
     let msg = [...messages][0];
 
-    var loc = msg.querySelector(".location");
+    var loc = msg.querySelector(".message-location");
     ok(loc, "location element exists");
     is(loc.textContent.trim(), sw.Scratchpad.uniqueName + ":1",
         "location value is correct");
 
     sw.addEventListener("focus", function onFocus() {
       sw.removeEventListener("focus", onFocus, true);
 
       let win = Services.wm.getMostRecentWindow("devtools:scratchpad");
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -141,16 +141,28 @@ ConsoleOutput.prototype = {
    * Getter for the debugger WebConsoleClient.
    * @type object
    */
   get webConsoleClient() {
     return this.owner.webConsoleClient;
   },
 
   /**
+   * Release an actor.
+   *
+   * @private
+   * @param string actorId
+   *        The actor ID you want to release.
+   */
+  _releaseObject: function(actorId)
+  {
+    this.owner._releaseObject(actorId);
+  },
+
+  /**
    * Add a message to output.
    *
    * @param object ...args
    *        Any number of Message objects.
    * @return this
    */
   addMessage: function(...args)
   {
@@ -842,17 +854,17 @@ Messages.Simple.prototype = Heritage.ext
   _renderRepeatNode: function()
   {
     if (!this._filterDuplicates) {
       return null;
     }
 
     let repeatNode = this.document.createElementNS(XHTML_NS, "span");
     repeatNode.setAttribute("value", "1");
-    repeatNode.className = "repeats";
+    repeatNode.className = "message-repeats";
     repeatNode.textContent = 1;
     repeatNode._uid = this.getRepeatID();
     return repeatNode;
   },
 
   /**
    * Render the message source location DOM element.
    * @private
@@ -1072,16 +1084,144 @@ Messages.ConsoleGeneric = function(packe
 Messages.ConsoleGeneric.prototype = Heritage.extend(Messages.Extended.prototype,
 {
   _renderBodyPieceSeparator: function()
   {
     return this.document.createTextNode(" ");
   },
 }); // Messages.ConsoleGeneric.prototype
 
+/**
+ * The ConsoleTrace message is used for console.trace() calls.
+ *
+ * @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",
+    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,
+    },
+  };
+
+  this._renderStack = this._renderStack.bind(this);
+  Messages.Simple.call(this, this._renderStack, options);
+
+  this._repeatID.consoleApiLevel = packet.level;
+  this._stacktrace = this._repeatID.stacktrace = packet.stacktrace;
+  this._arguments = packet.arguments;
+};
+
+Messages.ConsoleTrace.prototype = Heritage.extend(Messages.Simple.prototype,
+{
+  /**
+   * Holds the stackframes received from the server.
+   *
+   * @private
+   * @type array
+   */
+  _stacktrace: null,
+
+  /**
+   * Holds the arguments the content script passed to the console.trace()
+   * method. This array is cleared when the message is initialized, and
+   * associated actors are released.
+   *
+   * @private
+   * @type array
+   */
+  _arguments: null,
+
+  init: function()
+  {
+    let result = Messages.Simple.prototype.init.apply(this, arguments);
+
+    // We ignore console.trace() arguments. Release object actors.
+    if (Array.isArray(this._arguments)) {
+      for (let arg of this._arguments) {
+        if (WebConsoleUtils.isActorGrip(arg)) {
+          this.output._releaseObject(arg.actor);
+        }
+      }
+    }
+    this._arguments = null;
+
+    return result;
+  },
+
+  /**
+   * Render the stack frames.
+   *
+   * @private
+   * @return DOMElement
+   */
+  _renderStack: function()
+  {
+    let cmvar = this.document.createElementNS(XHTML_NS, "span");
+    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.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");
+    body.appendChild(title);
+    if (repeatNode) {
+      body.appendChild(repeatNode);
+    }
+    if (location) {
+      body.appendChild(location);
+    }
+    body.appendChild(this.document.createTextNode("\n"));
+
+    let frag = this.document.createDocumentFragment();
+    frag.appendChild(body);
+    frag.appendChild(widget.element);
+
+    return frag;
+  },
+
+  _renderBody: function()
+  {
+    let body = Messages.Simple.prototype._renderBody.apply(this, arguments);
+    body.classList.remove("devtools-monospace");
+    return body;
+  },
+
+  // no-op for the message location and .repeats elements.
+  // |this._renderStack| handles customized message output.
+  _renderLocation: function() { },
+  _renderRepeatNode: function() { },
+}); // Messages.ConsoleTrace.prototype
 
 let Widgets = {};
 
 /**
  * The base widget class.
  *
  * @constructor
  * @param object message
@@ -1349,16 +1489,101 @@ Widgets.LongString.prototype = Heritage.
       category: "output",
       severity: "warning",
     });
     this.output.addMessage(msg);
   },
 }); // Widgets.LongString.prototype
 
 
+/**
+ * The stacktrace widget.
+ *
+ * @constructor
+ * @extends Widgets.BaseWidget
+ * @param object message
+ *        The owning message.
+ * @param array stacktrace
+ *        The stacktrace to display, array of frames as supplied by the server,
+ *        over the remote protocol.
+ */
+Widgets.Stacktrace = function(message, stacktrace)
+{
+  Widgets.BaseWidget.call(this, message);
+  this.stacktrace = stacktrace;
+};
+
+Widgets.Stacktrace.prototype = Heritage.extend(Widgets.BaseWidget.prototype,
+{
+  /**
+   * The stackframes received from the server.
+   * @type array
+   */
+  stacktrace: null,
+
+  render: function()
+  {
+    if (this.element) {
+      return this;
+    }
+
+    let result = this.element = this.document.createElementNS(XHTML_NS, "ul");
+    result.className = "stacktrace devtools-monospace";
+
+    for (let frame of this.stacktrace) {
+      result.appendChild(this._renderFrame(frame));
+    }
+
+    return this;
+  },
+
+  /**
+   * Render a frame object received from the server.
+   *
+   * @param object frame
+   *        The stack frame to display. This object should have the following
+   *        properties: functionName, filename and lineNumber.
+   * @return DOMElement
+   *         The DOM element to display for the given frame.
+   */
+  _renderFrame: function(frame)
+  {
+    let fn = this.document.createElementNS(XHTML_NS, "span");
+    fn.className = "function";
+    if (frame.functionName) {
+      let span = this.document.createElementNS(XHTML_NS, "span");
+      span.className = "cm-variable";
+      span.textContent = frame.functionName;
+      fn.appendChild(span);
+      fn.appendChild(this.document.createTextNode("()"));
+    } else {
+      fn.classList.add("cm-comment");
+      fn.textContent = l10n.getStr("stacktrace.anonymousFunction");
+    }
+
+    let location = this.output.owner.createLocationNode(frame.filename,
+                                                        frame.lineNumber,
+                                                        "jsdebugger");
+
+    // .devtools-monospace sets font-size to 80%, however .body already has
+    // .devtools-monospace. If we keep it here, the location would be rendered
+    // smaller.
+    location.classList.remove("devtools-monospace");
+
+    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;
 
 exports.ConsoleOutput = ConsoleOutput;
 exports.Messages = Messages;
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -98,16 +98,17 @@ support-files =
   test_bug_770099_bad_policy_uri.html^headers^
   test_bug_770099_violation.html
   test_bug_770099_violation.html^headers^
   test-autocomplete-in-stackframe.html
   testscript.js
   test-bug_923281_console_log_filter.html
   test-bug_923281_test1.js
   test-bug_923281_test2.js
+  test-bug_939783_console_trace_duplicates.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]
@@ -252,8 +253,9 @@ run-if = os == "mac"
 [browser_webconsole_expandable_timestamps.js]
 [browser_webconsole_autocomplete_in_debugger_stackframe.js]
 [browser_webconsole_autocomplete_popup_close_on_tab_switch.js]
 [browser_webconsole_output_01.js]
 [browser_webconsole_output_02.js]
 [browser_webconsole_output_03.js]
 [browser_webconsole_output_04.js]
 [browser_webconsole_output_events.js]
+[browser_webconsole_console_trace_duplicates.js]
--- a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
+++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -65,17 +65,17 @@ function test()
       onMessageFound(results);
     });
   }
 
   function onMessageFound(results)
   {
     let msg = [...results[0].matched][0];
     ok(msg, "message element found");
-    let locationNode = msg.querySelector(".location");
+    let locationNode = msg.querySelector(".message-location");
     ok(locationNode, "message location element found");
 
     let title = locationNode.getAttribute("title");
     info("location node title: " + title);
     isnot(title.indexOf(" -> "), -1, "error comes from a subscript");
 
     let viewSource = browserconsole.viewSource;
     let URL = null;
--- a/browser/devtools/webconsole/test/browser_console_error_source_click.js
+++ b/browser/devtools/webconsole/test/browser_console_error_source_click.js
@@ -56,17 +56,17 @@ function test()
     let viewSourceCalled = false;
     hud.viewSource = () => viewSourceCalled = true;
 
     for (let result of results) {
       viewSourceCalled = false;
 
       let msg = [...results[0].matched][0];
       ok(msg, "message element found for: " + result.text);
-      let locationNode = msg.querySelector(".location");
+      let locationNode = msg.querySelector(".message-location");
       ok(locationNode, "message location element found");
 
       EventUtils.synthesizeMouse(locationNode, 2, 2, {}, hud.iframeWindow);
 
       ok(viewSourceCalled, "view source opened");
     }
 
     hud.viewSource = viewSource;
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
@@ -1,49 +1,46 @@
 /* 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/. */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-585956-console-trace.html";
 
 function test() {
-  addTab("data:text/html;charset=utf8,<p>hello");
-  browser.addEventListener("load", tabLoaded, true);
+  Task.spawn(runner).then(finishTest);
 
-  function tabLoaded() {
-    browser.removeEventListener("load", tabLoaded, true);
+  function* runner() {
+    let {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+    let hud = yield openConsole(tab);
 
-    openConsole(null, function(hud) {
-      content.location = TEST_URI;
+    content.location = TEST_URI;
 
-      waitForMessages({
-        webconsole: hud,
-        messages: [{
-          name: "console.trace output",
-          consoleTrace: {
-            file: "test-bug-585956-console-trace.html",
-            fn: "window.foobar585956c",
-          },
-        }],
-      }).then(performChecks);
+    let [result] = yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "console.trace output",
+        consoleTrace: {
+          file: "test-bug-585956-console-trace.html",
+          fn: "window.foobar585956c",
+        },
+      }],
     });
-  }
 
-  function performChecks(results) {
-    let node = [...results[0].matched][0];
+    let node = [...result.matched][0];
+    ok(node, "found trace log node");
+
+    let obj = node._messageObject;
+    ok(obj, "console.trace message object");
 
     // The expected stack trace object.
     let stacktrace = [
       { filename: TEST_URI, lineNumber: 9, functionName: "window.foobar585956c", language: 2 },
       { filename: TEST_URI, lineNumber: 14, functionName: "foobar585956b", language: 2 },
       { filename: TEST_URI, lineNumber: 18, functionName: "foobar585956a", language: 2 },
       { filename: TEST_URI, lineNumber: 21, functionName: null, language: 2 }
     ];
 
-    ok(node, "found trace log node");
-    ok(node._stacktrace, "found stacktrace object");
-    is(node._stacktrace.toSource(), stacktrace.toSource(), "stacktrace is correct");
+    ok(obj._stacktrace, "found stacktrace object");
+    is(obj._stacktrace.toSource(), stacktrace.toSource(), "stacktrace is correct");
     isnot(node.textContent.indexOf("bug-585956"), -1, "found file name");
-
-    finishTest();
   }
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_642108_pruneTest.js
@@ -69,17 +69,17 @@ function testCSSPruning(hudRef) {
       }],
     }).then(([result]) => {
       is(countMessageNodes(), LOG_LIMIT, "number of messages");
 
       is(Object.keys(hudRef.ui._repeatNodes).length, LOG_LIMIT,
          "repeated nodes pruned from repeatNodes");
 
       let msg = [...result.matched][0];
-      let repeats = msg.querySelector(".repeats");
+      let repeats = msg.querySelector(".message-repeats");
       is(repeats.getAttribute("value"), 1,
          "repeated nodes pruned from repeatNodes (confirmed)");
 
       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
@@ -29,18 +29,18 @@ function test() {
         text: "Blah Blah",
         category: CATEGORY_WEBDEV,
         severity: SEVERITY_LOG,
       }],
     });
 
     let exceptionMsg = [...exceptionRule.matched][0];
     let consoleMsg = [...consoleRule.matched][0];
-    let nodes = [exceptionMsg.querySelector(".location"),
-                 consoleMsg.querySelector(".location")];
+    let nodes = [exceptionMsg.querySelector(".message-location"),
+                 consoleMsg.querySelector(".message-location")];
     ok(nodes[0], ".location node for the exception message");
     ok(nodes[1], ".location node for the console message");
 
     for (let i = 0; i < nodes.length; i++) {
       yield checkClickOnNode(i, nodes[i]);
       yield gDevTools.showToolbox(hud.target, "webconsole");
     }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
@@ -36,20 +36,20 @@ function testViewSource(aHud)
     {
       text: "'color'",
       category: CATEGORY_CSS,
       severity: SEVERITY_WARNING,
     }],
   }).then(([error1Rule, error2Rule]) => {
     let error1Msg = [...error1Rule.matched][0];
     let error2Msg = [...error2Rule.matched][0];
-    nodes = [error1Msg.querySelector(".location"),
-             error2Msg.querySelector(".location")];
-    ok(nodes[0], ".location node for the first error");
-    ok(nodes[1], ".location node for the second error");
+    nodes = [error1Msg.querySelector(".message-location"),
+             error2Msg.querySelector(".message-location")];
+    ok(nodes[0], ".message-location node for the first error");
+    ok(nodes[1], ".message-location node for the second error");
 
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
     toolbox.once("styleeditor-selected", (event, panel) => {
       StyleEditorUI = panel.UI;
 
       let count = 0;
       StyleEditorUI.on("editor-added", function() {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_trace_duplicates.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+function test() {
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.html";
+
+  Task.spawn(runner).then(finishTest);
+
+  function* runner() {
+    const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello");
+    const hud = yield openConsole(tab);
+
+    content.location = TEST_URI;
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "console.trace output for foo1()",
+        text: "foo1()",
+        repeats: 2,
+        consoleTrace: {
+          file: "test-bug_939783_console_trace_duplicates.html",
+          fn: "foo3()",
+        },
+      }, {
+        name: "console.trace output for foo1b()",
+        text: "foo1b()",
+        consoleTrace: {
+          file: "test-bug_939783_console_trace_duplicates.html",
+          fn: "foo3()",
+        },
+      }],
+    });
+  }
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_scratchpad_panel_link.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_scratchpad_panel_link.js
@@ -48,17 +48,17 @@ function runTests(aToolbox)
       webconsole: hud,
       messages: [{ text: "foobar-from-scratchpad" }]
     });
 
     info("Clicking link to switch to and focus Scratchpad");
 
     let [matched] = [...messages[0].matched];
     ok(matched, "Found logged message from Scratchpad");
-    let anchor = matched.querySelector("a.location");
+    let anchor = matched.querySelector("a.message-location");
 
     aToolbox.on("scratchpad-selected", function selected() {
       aToolbox.off("scratchpad-selected", selected);
 
       is(aToolbox.getCurrentPanel(), scratchpadPanel,
         "Clicking link switches to Scratchpad panel");
       
       is(Services.ww.activeWindow, aToolbox.frame.ownerGlobal,
--- a/browser/devtools/webconsole/test/browser_webconsole_view_source.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_view_source.js
@@ -42,17 +42,17 @@ function testViewSource(hud) {
         }],
       }).then(onMessage);
     });
   });
 
   function onMessage([result]) {
     let msg = [...result.matched][0];
     ok(msg, "error message");
-    let locationNode = msg.querySelector(".location");
+    let locationNode = msg.querySelector(".message-location");
     ok(locationNode, "location node");
 
     Services.ww.registerNotification(observer);
 
     containsValue = Sources.containsValue;
     Sources.containsValue = () => {
       containsValueInvoked = true;
       return false;
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -1,26 +1,22 @@
 /* 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/. */
 
-let WebConsoleUtils, TargetFactory, require;
 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/commonjs/sdk/core/promise.js", {});
 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");
 
-(() => {
-  let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-  let utils = devtools.require("devtools/toolkit/webconsole/utils");
-  TargetFactory = devtools.TargetFactory;
-  WebConsoleUtils = utils.Utils;
-  require = devtools.require;
-})();
 // promise._reportErrors = true; // please never leave me.
 
 let gPendingOutputTest = 0;
 
 // The various categories of messages.
 const CATEGORY_NETWORK = 0;
 const CATEGORY_CSS = 1;
 const CATEGORY_JS = 2;
@@ -278,17 +274,17 @@ function dumpConsoles()
  * Dump to output debug information for the given webconsole message.
  *
  * @param nsIDOMNode aMessage
  *        The message element you want to display.
  */
 function dumpMessageElement(aMessage)
 {
   let text = aMessage.textContent;
-  let repeats = aMessage.querySelector(".repeats");
+  let repeats = aMessage.querySelector(".message-repeats");
   if (repeats) {
     repeats = repeats.getAttribute("value");
   }
   console.debug("id", aMessage.getAttribute("id"),
                 "date", aMessage.timestamp,
                 "class", aMessage.className,
                 "category", aMessage.category,
                 "severity", aMessage.severity,
@@ -927,54 +923,54 @@ function waitForMessages(aOptions)
     return result;
   }
 
   function checkConsoleTrace(aRule, aElement)
   {
     let elemText = aElement.textContent;
     let trace = aRule.consoleTrace;
 
-    if (!checkText("Stack trace from ", elemText)) {
+    if (!checkText("console.trace():", elemText)) {
       return false;
     }
 
-    let clickable = aElement.querySelector(".body a");
-    if (!clickable) {
-      ok(false, "console.trace() message is missing .hud-clickable");
-      displayErrorContext(aRule, aElement);
-      return false;
-    }
-    aRule.clickableElements = [clickable];
-
-    if (trace.file &&
-        !checkText("from " + trace.file + ", ", elemText)) {
-      ok(false, "console.trace() message is missing the file name: " +
-                trace.file);
-      displayErrorContext(aRule, aElement);
-      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 &&
-        !checkText(", function " + trace.fn + ", ", elemText)) {
-      ok(false, "console.trace() message is missing the function name: " +
-                trace.fn);
-      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 &&
-        !checkText(", line " + trace.line + ".", elemText)) {
-      ok(false, "console.trace() message is missing the line number: " +
-                trace.line);
-      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;
 
     return true;
   }
 
   function checkConsoleTime(aRule, aElement)
   {
     let elemText = aElement.textContent;
     let time = aRule.consoleTime;
@@ -1033,17 +1029,17 @@ function waitForMessages(aOptions)
     aRule.category = CATEGORY_WEBDEV;
     aRule.severity = SEVERITY_LOG;
 
     return true;
   }
 
   function checkSource(aRule, aElement)
   {
-    let location = aElement.querySelector(".location");
+    let location = aElement.querySelector(".message-location");
     if (!location) {
       return false;
     }
 
     if (!checkText(aRule.source.url, location.getAttribute("title"))) {
       return false;
     }
 
@@ -1085,26 +1081,33 @@ function waitForMessages(aOptions)
     if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) {
       return false;
     }
 
     if (aRule.source && !checkSource(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 && (!aElement._messageObject ||
-                       !(aElement._messageObject instanceof aRule.type))) {
-      return false;
+    if (aRule.type) {
+      if (!aElement._messageObject ||
+          !(aElement._messageObject instanceof aRule.type)) {
+        if (partialMatch) {
+          ok(false, "message type for rule: " + displayRule(aRule));
+          displayErrorContext(aRule, aElement);
+        }
+        return false;
+      }
+      partialMatch = true;
     }
 
-    let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime ||
-                          aRule.consoleTimeEnd || aRule.type);
-
     if ("category" in aRule && aElement.category != aRule.category) {
       if (partialMatch) {
         is(aElement.category, aRule.category,
            "message category for rule: " + displayRule(aRule));
         displayErrorContext(aRule, aElement);
       }
       return false;
     }
@@ -1119,17 +1122,17 @@ function waitForMessages(aOptions)
     }
 
     if (aRule.category == CATEGORY_NETWORK && "url" in aRule &&
         !checkText(aRule.url, aElement.url)) {
       return false;
     }
 
     if ("repeats" in aRule) {
-      let repeats = aElement.querySelector(".repeats");
+      let repeats = aElement.querySelector(".message-repeats");
       if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
         return false;
       }
     }
 
     if ("groupDepth" in aRule) {
       let timestamp = aElement.querySelector(".timestamp");
       let indent = (GROUP_INDENT * aRule.groupDepth) + "px";
@@ -1175,17 +1178,17 @@ function waitForMessages(aOptions)
     aRule.matched.add(aElement);
 
     return aRule.matched.size == count;
   }
 
   function onMessagesAdded(aEvent, aNewElements)
   {
     for (let elem of aNewElements) {
-      let location = elem.querySelector(".location");
+      let location = elem.querySelector(".message-location");
       if (location) {
         let url = location.title;
         // Prevent recursion with the browser console and any potential
         // messages coming from head.js.
         if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) {
           continue;
         }
       }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug_939783_console_trace_duplicates.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Web Console test for bug 939783 - different console.trace() calls
+      wrongly filtered as duplicates</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+<script type="application/javascript">
+function foo1() {
+  foo2();
+}
+
+function foo1b() {
+  foo2();
+}
+
+function foo2() {
+  foo3();
+}
+
+function foo3() {
+  console.trace();
+}
+
+foo1(); foo1();
+foo1b();
+
+</script>
+  </head>
+  <body>
+    <p>Web Console test for bug 939783 - different console.trace() calls
+      wrongly filtered as duplicates</p>
+  </body>
+</html>
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -1051,17 +1051,17 @@ WebConsoleFrame.prototype = {
    * @param nsIDOMNode aOriginal
    *        The Original Node. The one being merged into.
    * @param nsIDOMNode aFiltered
    *        The node being filtered out because it is repeated.
    */
   mergeFilteredMessageNode:
   function WCF_mergeFilteredMessageNode(aOriginal, aFiltered)
   {
-    let repeatNode = aOriginal.getElementsByClassName("repeats")[0];
+    let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0];
     if (!repeatNode) {
       return; // no repeat node, return early.
     }
 
     let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
     repeatNode.setAttribute("value", occurrences);
     repeatNode.textContent = occurrences;
     let str = l10n.getStr("messageRepeats.tooltip2");
@@ -1076,17 +1076,17 @@ WebConsoleFrame.prototype = {
    * @param nsIDOMNode aNode
    *        The message node to be filtered or not.
    * @returns nsIDOMNode|null
    *          Returns the duplicate node if the message was filtered, null
    *          otherwise.
    */
   _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode)
   {
-    let repeatNode = aNode.getElementsByClassName("repeats")[0];
+    let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
     if (!repeatNode) {
       return null;
     }
 
     let uid = repeatNode._uid;
     let dupeNode = null;
 
     if (aNode.category == CATEGORY_CSS ||
@@ -1100,17 +1100,17 @@ WebConsoleFrame.prototype = {
               aNode.category == CATEGORY_JS) &&
              aNode.category != CATEGORY_NETWORK &&
              !aNode.classList.contains("inlined-variables-view")) {
       let lastMessage = this.outputNode.lastChild;
       if (!lastMessage) {
         return null;
       }
 
-      let lastRepeatNode = lastMessage.getElementsByClassName("repeats")[0];
+      let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0];
       if (lastRepeatNode && lastRepeatNode._uid == uid) {
         dupeNode = lastMessage;
       }
     }
 
     if (dupeNode) {
       this.mergeFilteredMessageNode(dupeNode, aNode);
       return dupeNode;
@@ -1186,58 +1186,31 @@ WebConsoleFrame.prototype = {
       case "error":
       case "exception":
       case "assert":
       case "debug": {
         let msg = new Messages.ConsoleGeneric(aMessage);
         node = msg.init(this.output).render().element;
         break;
       }
+      case "trace": {
+        let msg = new Messages.ConsoleTrace(aMessage);
+        node = msg.init(this.output).render().element;
+        break;
+      }
       case "dir": {
         body = { arguments: args };
         let clipboardArray = [];
         args.forEach((aValue) => {
           clipboardArray.push(VariablesView.getString(aValue));
         });
         clipboardText = clipboardArray.join(" ");
         break;
       }
 
-      case "trace": {
-        let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename);
-        let functionName = aMessage.functionName ||
-                           l10n.getStr("stacktrace.anonymousFunction");
-
-        body = this.document.createElementNS(XHTML_NS, "a");
-        body.setAttribute("aria-haspopup", true);
-        body.href = "#";
-        body.draggable = false;
-        body.textContent = l10n.getFormatStr("stacktrace.outputMessage",
-                                             [filename, functionName,
-                                              sourceLine]);
-
-        this._addMessageLinkCallback(body, () => {
-          this.jsterm.openVariablesView({
-            rawObject: aMessage.stacktrace,
-            autofocus: true,
-          });
-        });
-
-        clipboardText = body.textContent + "\n";
-
-        aMessage.stacktrace.forEach(function(aFrame) {
-          clipboardText += aFrame.filename + " :: " +
-                           aFrame.functionName + " :: " +
-                           aFrame.lineNumber + "\n";
-        });
-
-        clipboardText = clipboardText.trimRight();
-        break;
-      }
-
       case "group":
       case "groupCollapsed":
         clipboardText = body = aMessage.groupName;
         this.groupDepth++;
         break;
 
       case "groupEnd":
         if (this.groupDepth > 0) {
@@ -1276,17 +1249,16 @@ WebConsoleFrame.prototype = {
     }
 
     // Release object actors for arguments coming from console API methods that
     // we ignore their arguments.
     switch (level) {
       case "group":
       case "groupCollapsed":
       case "groupEnd":
-      case "trace":
       case "time":
       case "timeEnd":
         for (let actor of objectActors) {
           this._releaseObject(actor);
         }
         objectActors.clear();
     }
 
@@ -1302,25 +1274,21 @@ WebConsoleFrame.prototype = {
         node.setAttribute("private", true);
       }
     }
 
     if (objectActors.size > 0) {
       node._objectActors = objectActors;
 
       if (!node._messageObject) {
-        let repeatNode = node.getElementsByClassName("repeats")[0];
+        let repeatNode = node.getElementsByClassName("message-repeats")[0];
         repeatNode._uid += [...objectActors].join("-");
       }
     }
 
-    if (level == "trace") {
-      node._stacktrace = aMessage.stacktrace;
-    }
-
     return node;
   },
 
   /**
    * Handle ConsoleAPICall objects received from the server. This method outputs
    * the window.console API call.
    *
    * @param object aMessage
@@ -2383,17 +2351,17 @@ WebConsoleFrame.prototype = {
       for (let actor of aNode._objectActors) {
         this._releaseObject(actor);
       }
       aNode._objectActors.clear();
     }
 
     if (aNode.category == CATEGORY_CSS ||
         aNode.category == CATEGORY_SECURITY) {
-      let repeatNode = aNode.getElementsByClassName("repeats")[0];
+      let repeatNode = aNode.getElementsByClassName("message-repeats")[0];
       if (repeatNode && repeatNode._uid) {
         delete this._repeatNodes[repeatNode._uid];
       }
     }
     else if (aNode._connectionId &&
              aNode.category == CATEGORY_NETWORK) {
       delete this._networkRequests[aNode._connectionId];
       this._releaseObject(aNode._connectionId);
@@ -2496,17 +2464,17 @@ WebConsoleFrame.prototype = {
     // Add the message repeats node only when needed.
     let repeatNode = null;
     if (aCategory != CATEGORY_INPUT &&
         aCategory != CATEGORY_OUTPUT &&
         aCategory != CATEGORY_NETWORK &&
         !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) {
       repeatNode = this.document.createElementNS(XHTML_NS, "span");
       repeatNode.setAttribute("value", "1");
-      repeatNode.className = "repeats";
+      repeatNode.className = "message-repeats";
       repeatNode.textContent = 1;
       repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
                          aSourceURL, aSourceLine].join(":");
     }
 
     // Create the timestamp.
     let timestampNode = this.document.createElementNS(XHTML_NS, "span");
     timestampNode.className = "timestamp devtools-monospace";
@@ -2562,21 +2530,28 @@ WebConsoleFrame.prototype = {
    * Creates the anchor that displays the textual location of an incoming
    * message.
    *
    * @param string aSourceURL
    *        The URL of the source file responsible for the error.
    * @param number aSourceLine [optional]
    *        The line number on which the error occurred. If zero or omitted,
    *        there is no line number associated with this message.
+   * @param string aTarget [optional]
+   *        Tells which tool to open the link with, on click. Supported tools:
+   *        jsdebugger, styleeditor, scratchpad.
    * @return nsIDOMNode
    *         The new anchor element, ready to be added to the message node.
    */
-  createLocationNode: function WCF_createLocationNode(aSourceURL, aSourceLine)
+  createLocationNode:
+  function WCF_createLocationNode(aSourceURL, aSourceLine, aTarget)
   {
+    if (!aSourceURL) {
+      aSourceURL = "";
+    }
     let locationNode = this.document.createElementNS(XHTML_NS, "a");
     let filenameNode = this.document.createElementNS(XHTML_NS, "span");
 
     // Create the text, which consists of an abbreviated version of the URL
     // Scratchpad URLs should not be abbreviated.
     let filename;
     let fullURL;
     let isScratchpad = false;
@@ -2587,40 +2562,49 @@ WebConsoleFrame.prototype = {
       isScratchpad = true;
     }
     else {
       fullURL = aSourceURL.split(" -> ").pop();
       filename = WebConsoleUtils.abbreviateSourceURL(fullURL);
     }
 
     filenameNode.className = "filename";
-    filenameNode.textContent = " " + filename;
+    filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation"));
     locationNode.appendChild(filenameNode);
 
-    locationNode.href = isScratchpad ? "#" : fullURL;
+    locationNode.href = isScratchpad || !fullURL ? "#" : fullURL;
     locationNode.draggable = false;
+    locationNode.target = aTarget;
     locationNode.setAttribute("title", aSourceURL);
-    locationNode.className = "location theme-link devtools-monospace";
+    locationNode.className = "message-location theme-link devtools-monospace";
 
     // Make the location clickable.
-    this._addMessageLinkCallback(locationNode, () => {
-      if (isScratchpad) {
+    let onClick = () => {
+      let target = locationNode.target;
+      if (target == "scratchpad" || isScratchpad) {
         this.owner.viewSourceInScratchpad(aSourceURL);
+        return;
       }
-      else if (locationNode.parentNode.category == CATEGORY_CSS) {
+
+      let category = locationNode.parentNode.category;
+      if (target == "styleeditor" || category == CATEGORY_CSS) {
         this.owner.viewSourceInStyleEditor(fullURL, aSourceLine);
       }
-      else if (locationNode.parentNode.category == CATEGORY_JS ||
-               locationNode.parentNode.category == CATEGORY_WEBDEV) {
+      else if (target == "jsdebugger" ||
+               category == CATEGORY_JS || category == CATEGORY_WEBDEV) {
         this.owner.viewSourceInDebugger(fullURL, aSourceLine);
       }
       else {
         this.owner.viewSource(fullURL, aSourceLine);
       }
-    });
+    };
+
+    if (fullURL) {
+      this._addMessageLinkCallback(locationNode, onClick);
+    }
 
     if (aSourceLine) {
       let lineNumberNode = this.document.createElementNS(XHTML_NS, "span");
       lineNumberNode.className = "line-number";
       lineNumberNode.textContent = ":" + aSourceLine;
       locationNode.appendChild(lineNumberNode);
       locationNode.sourceLine = aSourceLine;
     }
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -97,25 +97,23 @@ gcliterm.instanceLabel=Instance of %S
 # The 2nd line, from "function" to the end of the line, is a link to the
 # JavaScript debugger.
 reflow.messageWithNoLink=reflow: %Sms
 reflow.messageWithLink=reflow: %Sms\u0020
 reflow.messageLinkText=function %1$S, %2$S line %3$S
 
 # LOCALIZATION NOTE (stacktrace.anonymousFunction): this string is used to
 # display JavaScript functions that have no given name - they are said to be
-# anonymous. See also stacktrace.outputMessage.
+# anonymous. Test console.trace() in the webconsole.
 stacktrace.anonymousFunction=<anonymous>
 
-# LOCALIZATION NOTE (stacktrace.outputMessage): this string is used in the Web
-# Console output to identify a web developer call to console.trace(). The
-# stack trace of JavaScript function calls is displayed. In this minimal
-# message we only show the last call. Parameters: %1$S is the file name, %2$S
-# is the function name, %3$S is the line number.
-stacktrace.outputMessage=Stack trace from %1$S, function %2$S, line %3$S.
+# LOCALIZATION NOTE (unknownLocation): this string is used to
+# display messages with sources that have an unknown location, eg. from
+# console.trace() calls.
+unknownLocation=<unknown>
 
 # LOCALIZATION NOTE (timerStarted): this string is used to display the result
 # of the console.time() call. Parameters: %S is the name of the timer.
 timerStarted=%S: timer started
 
 # LOCALIZATION NOTE (timeEnd): this string is used to display the result of
 # the console.timeEnd() call. Parameters: %1$S is the name of the timer, %2$S
 # is the number of milliseconds.
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -41,59 +41,59 @@ a {
 .message > .body {
   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 {
+.message-repeats {
   -moz-user-select: none;
   flex: 0 0 auto;
   margin: 2px 6px;
   padding: 0 6px;
   height: 1.25em;
   color: white;
   background-color: red;
   border-radius: 40px;
   font: message-box;
   font-size: 0.9em;
   font-weight: 600;
 }
 
-.message > .repeats[value="1"] {
+.message-repeats[value="1"] {
   display: none;
 }
 
-.message > .location {
+.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;
   color: -moz-nativehyperlinktext;
   text-decoration: none;
+  white-space: nowrap;
 }
 
-.message > .location:hover,
-.message > .location:focus {
+.message-location:hover,
+.message-location:focus {
   text-decoration: underline;
 }
 
-.message > .location > .filename {
+.message-location > .filename {
   text-overflow: ellipsis;
   text-align: end;
   overflow: hidden;
-  white-space: nowrap;
 }
 
-.message > .location > .line-number {
+.message-location > .line-number {
   flex: 0 0 auto;
 }
 
 .jsterm-input-container {
   border-top-width: 1px;
   border-top-style: solid;
 }
 
@@ -331,16 +331,47 @@ 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 {
+  list-style: none;
+  padding: 0 1em 0 1.5em;
+  margin: 0;
+  max-height: 10em;
+  overflow-y: auto;
+
+  border: 1px solid rgba(128, 128, 128, .5);
+  border-radius: 3px;
+}
+
+.stacktrace li {
+  display: flex;
+  margin: 0;
+}
+
+.stacktrace .function {
+  display: block;
+  flex: 1 1 auto;
+}
+
 /* Replace these values with CSS variables as available */
 .theme-dark .jsterm-input-container {
   background-color: #252c33; /* tabToolbarBackgroundColor */
   border-color: #131c26; /* mainBackgroundColor */
 }
 
 .theme-dark .jsterm-input-node {
   color: #8fa1b2; /* textColor */
@@ -353,16 +384,20 @@ a {
 .theme-dark .navigation-marker .url {
   background: #131c26; /* mainBackgroundColor */
 }
 
 .theme-dark .inlined-variables-view iframe {
   border-color: #333;
 }
 
+.theme-dark .stacktrace {
+  border-color: #333;
+}
+
 .theme-light .jsterm-input-container {
   background-color: #fff; /* mainBackgroundColor */
   border-color: ThreeDShadow;
 }
 
 .theme-light .jsterm-input-node {
   color: black; /* textColor */
 }
@@ -374,16 +409,20 @@ a {
 .theme-light .navigation-marker .url {
   background: #fff; /* mainBackgroundColor */
 }
 
 .theme-light .inlined-variables-view iframe {
   border-color: #ccc;
 }
 
+.theme-light .stacktrace {
+  border-color: #ccc;
+}
+
 @media (max-width: 500px) {
   .message > .timestamp {
     display: none;
   }
   .toolbarbutton-text {
     display: none;
   }
   .hud-console-filter-toolbar .webconsole-filter-button {