Bug 1298225 - Format clipboard text of console stack traces into multiple lines r?bgrins draft
authorJarda Snajdr <jsnajdr@gmail.com>
Tue, 30 Aug 2016 12:34:22 +0200
changeset 408528 6a1c4659e71640d4832a539226179fda4f96a058
parent 408409 b7f7ae14590aced450bb0b0469dfb38edd2c0ace
child 530130 48bcd5722c154ec2249062577798863189b6cab2
push id28237
push userbmo:jsnajdr@gmail.com
push dateThu, 01 Sep 2016 08:13:51 +0000
reviewersbgrins
bugs1298225
milestone51.0a1
Bug 1298225 - Format clipboard text of console stack traces into multiple lines r?bgrins MozReview-Commit-ID: HkCFEwjhCwx
devtools/client/shared/components/frame.js
devtools/client/shared/components/stack-trace.js
devtools/client/shared/components/test/mochitest/test_stack-trace.html
devtools/client/webconsole/console-output.js
devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
devtools/client/webconsole/test/test-console.html
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -171,17 +171,18 @@ module.exports = createClass({
       let functionDisplayName = frame.functionDisplayName;
       if (!functionDisplayName && showAnonymousFunctionName) {
         functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction");
       }
 
       if (functionDisplayName) {
         elements.push(
           dom.span({ className: "frame-link-function-display-name" },
-            functionDisplayName)
+            functionDisplayName),
+          " "
         );
       }
     }
 
     let displaySource = showFullSourceUrl ? long : short;
     if (isSourceMapped) {
       displaySource = getSourceMappedFile(displaySource);
     } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
@@ -231,14 +232,14 @@ module.exports = createClass({
     } else {
       sourceEl = dom.span({
         className: "frame-link-source",
       }, sourceInnerEl);
     }
     elements.push(sourceEl);
 
     if (showHost && host) {
-      elements.push(dom.span({ className: "frame-link-host" }, host));
+      elements.push(" ", dom.span({ className: "frame-link-host" }, host));
     }
 
     return dom.span(attributes, ...elements);
   }
 });
--- a/devtools/client/shared/components/stack-trace.js
+++ b/devtools/client/shared/components/stack-trace.js
@@ -37,32 +37,32 @@ const StackTrace = createClass({
   },
 
   render() {
     let { stacktrace, onViewSourceInDebugger } = this.props;
 
     let frames = [];
     stacktrace.forEach(s => {
       if (s.asyncCause) {
-        frames.push(AsyncFrame({
+        frames.push("\t", AsyncFrame({
           asyncCause: s.asyncCause
-        }));
+        }), "\n");
       }
 
-      frames.push(Frame({
+      frames.push("\t", Frame({
         frame: {
           functionDisplayName: s.functionName,
           source: s.filename.split(" -> ").pop(),
           line: s.lineNumber,
           column: s.columnNumber,
         },
         showFunctionName: true,
         showAnonymousFunctionName: true,
         showFullSourceUrl: true,
         onClick: onViewSourceInDebugger
-      }));
+      }), "\n");
     });
 
     return dom.div({ className: "stack-trace" }, frames);
   }
 });
 
 module.exports = StackTrace;
--- a/devtools/client/shared/components/test/mochitest/test_stack-trace.html
+++ b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
@@ -40,17 +40,19 @@ window.onload = function() {
     };
 
     let trace = ReactDOM.render(StackTrace(props), window.document.body);
     yield forceRender(trace);
 
     let traceEl = trace.getDOMNode();
     ok(traceEl, "Rendered StackTrace has an element");
 
-    let frameEls = traceEl.childNodes;
+    // Get the child nodes and filter out the text-only whitespace ones
+    let frameEls = Array.from(traceEl.childNodes)
+      .filter(n => n.className.includes("frame"));
     ok(frameEls, "Rendered StackTrace has frames");
     is(frameEls.length, 3, "StackTrace has 3 frames");
 
     // Check the top frame, function name should be anonymous
     checkFrameString({
       el: frameEls[0],
       functionName: "<anonymous>",
       source: "http://myfile.com/mahscripts.js",
@@ -71,13 +73,21 @@ window.onload = function() {
       functionName: "loadFunc",
       source: "http://myfile.com/loadee.js",
       file: "http://myfile.com/loadee.js",
       line: 10,
       column: null,
       shouldLink: true,
       tooltip: "View source in Debugger → http://myfile.com/loadee.js:10",
     });
+
+    // Check the tabs and newlines in the stack trace textContent
+    let traceText = traceEl.textContent;
+    let traceLines = traceText.split("\n");
+    ok(traceLines.length > 0, "There are newlines in the stack trace text");
+    is(traceLines.pop(), "", "There is a newline at the end of the stack trace text");
+    is(traceLines.length, 3, "The stack trace text has 3 lines");
+    ok(traceLines.every(l => l[0] == "\t"), "Every stack trace line starts with tab");
   });
 }
 </script>
 </body>
 </html>
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -930,18 +930,16 @@ Messages.Simple.prototype = extend(Messa
       twisty.addEventListener("click", this._onClickCollapsible);
       this.element.appendChild(twisty);
       this.collapsible = true;
       this.element.setAttribute("collapsible", true);
     }
 
     this.element.appendChild(body);
 
-    this.element.appendChild(this.document.createTextNode("\n"));
-
     this.element.clipboardText = this.element.textContent;
 
     if (this.private) {
       this.element.setAttribute("private", true);
     }
 
     // TODO: handle object releasing in a more elegant way once all console
     // messages use the new API - bug 778766.
@@ -988,22 +986,26 @@ Messages.Simple.prototype = extend(Messa
 
     // do this before repeatNode is rendered - it has no effect afterwards
     this._repeatID.textContent += "|" + container.textContent;
 
     let repeatNode = this._renderRepeatNode();
     let location = this._renderLocation();
 
     if (repeatNode) {
+      bodyFlex.appendChild(this.document.createTextNode(" "));
       bodyFlex.appendChild(repeatNode);
     }
     if (location) {
+      bodyFlex.appendChild(this.document.createTextNode(" "));
       bodyFlex.appendChild(location);
     }
 
+    bodyFlex.appendChild(this.document.createTextNode("\n"));
+
     if (this.stack) {
       this._attachment = new Widgets.Stacktrace(this, this.stack).render().element;
     }
 
     if (this._attachment) {
       bodyWrapper.appendChild(this._attachment);
     }
 
--- a/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
+++ b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
@@ -1,69 +1,97 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+/* globals goDoCommand */
+
 "use strict";
 
 // Test copying of the entire console message when right-clicked
 // with no other text selected. See Bug 1100562.
 
-function test() {
+add_task(function* () {
   let hud;
   let outputNode;
   let contextMenu;
 
-  const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
-                   "test/test-console.html";
-
-  Task.spawn(runner).then(finishTest);
+  const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console.html";
 
-  function* runner() {
-    const {tab} = yield loadTab(TEST_URI);
-    hud = yield openConsole(tab);
-    outputNode = hud.outputNode;
-    contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
+  const { tab, browser } = yield loadTab(TEST_URI);
+  hud = yield openConsole(tab);
+  outputNode = hud.outputNode;
+  contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
+
+  registerCleanupFunction(() => {
+    hud = outputNode = contextMenu = null;
+  });
 
-    registerCleanupFunction(() => {
-      hud = outputNode = contextMenu = null;
-    });
+  hud.jsterm.clearOutput();
 
-    hud.jsterm.clearOutput();
-    content.console.log("bug 1100562");
+  yield ContentTask.spawn(browser, {}, function* () {
+    let button = content.document.getElementById("testTrace");
+    button.click();
+  });
 
-    let [results] = yield waitForMessages({
-      webconsole: hud,
-      messages: [{
+  let results = yield waitForMessages({
+    webconsole: hud,
+    messages: [
+      {
         text: "bug 1100562",
         category: CATEGORY_WEBDEV,
         severity: SEVERITY_LOG,
-      }]
-    });
+        lines: 1,
+      },
+      {
+        name: "console.trace output",
+        consoleTrace: true,
+        lines: 3,
+      },
+    ]
+  });
 
-    outputNode.focus();
-    let message = [...results.matched][0];
+  outputNode.focus();
 
-    yield waitForContextMenu(contextMenu, message, copyFromPopup,
-                             testContextMenuCopy);
+  for (let result of results) {
+    let message = [...result.matched][0];
 
-    function copyFromPopup() {
+    yield waitForContextMenu(contextMenu, message, () => {
       let copyItem = contextMenu.querySelector("#cMenu_copy");
       copyItem.doCommand();
 
       let controller = top.document.commandDispatcher
                                    .getControllerForCommand("cmd_copy");
       is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
-    }
+    });
+
+    let clipboardText;
+
+    yield waitForClipboardPromise(
+      () => goDoCommand("cmd_copy"),
+      (str) => {
+        clipboardText = str;
+        return message.textContent == clipboardText;
+      }
+    );
+
+    ok(clipboardText, "Clipboard text was found and saved");
 
-    function testContextMenuCopy() {
-      waitForClipboard((str) => {
-        return message.textContent.trim() == str.trim();
-      }, () => {
-        goDoCommand("cmd_copy");
-      }, () => {}, () => {}
-      );
+    let lines = clipboardText.split("\n");
+    ok(lines.length > 0, "There is at least one newline in the message");
+    is(lines.pop(), "", "There is a newline at the end");
+    is(lines.length, result.lines, `There are ${result.lines} lines in the message`);
+
+    // Test the first line for "timestamp message repeat file:line"
+    let firstLine = lines.shift();
+    ok(/^[\d:.]+ .+ \d+ .+:\d+$/.test(firstLine),
+      "The message's first line has the right format");
+
+    // Test the remaining lines (stack trace) for "TABfunctionName sourceURL:line:col"
+    for (let line of lines) {
+      ok(/^\t.+ .+:\d+:\d+$/.test(line), "The stack trace line has the right format");
     }
+  }
 
-    yield closeConsole(tab);
-  }
-}
+  yield closeConsole(tab);
+  yield finishTest();
+});
--- a/devtools/client/webconsole/test/test-console.html
+++ b/devtools/client/webconsole/test/test-console.html
@@ -8,20 +8,27 @@
       };
 
       function test() {
         var str = "Dolske Digs Bacon, Now and Forevermore."
         for (var i=0; i < 5; i++) {
           console.log(str);
         }
       }
+
+      function testTrace() {
+        console.log("bug 1100562");
+        console.trace();
+      }
+
       console.info("INLINE SCRIPT:");
       test();
       console.warn("I'm warning you, he will eat up all yr bacon.");
       console.error("Error Message");
     </script>
   </head>
   <body>
     <h1 id="header">Heads Up Display Demo</h1>
     <button onclick="test();">Log stuff about Dolske</button>
+    <button id="testTrace" onclick="testTrace();">Log stuff with stacktrace</button>
     <div id="myDiv"></div>
   </body>
 </html>