Bug 659907 - Expand console object with a dir method that displays an interactive listing of all the properties of an object.; f=rcampbell r=mihai.sucan,bzbarsky sr=bzbarsky
authorPanos Astithas <past@mozilla.com>
Thu, 09 Jun 2011 16:27:30 +0300
changeset 73751 a8c39fc1b57b33f9d127a0afe06b96b54c907e14
parent 73750 327bd5e0d8043aacfaccd177b142eb3306e8227b
child 73752 6eee63caf1c62c71c73fcf7577c54d4fd6d010db
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewersmihai, bzbarsky
bugs659907
milestone8.0a1
Bug 659907 - Expand console object with a dir method that displays an interactive listing of all the properties of an object.; f=rcampbell r=mihai.sucan,bzbarsky sr=bzbarsky
browser/devtools/webconsole/HUDService.jsm
browser/devtools/webconsole/test/browser/Makefile.in
browser/devtools/webconsole/test/browser/browser_webconsole_bug_659907_console_dir.js
browser/devtools/webconsole/test/browser/test-console-extras.html
dom/base/ConsoleAPI.js
dom/tests/browser/browser_ConsoleAPITests.js
dom/tests/mochitest/general/test_consoleAPI.html
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -153,16 +153,17 @@ const SEVERITY_LOG = 3;
 // A mapping from the console API log event levels to the Web Console
 // severities.
 const LEVELS = {
   error: SEVERITY_ERROR,
   warn: SEVERITY_WARNING,
   info: SEVERITY_INFO,
   log: SEVERITY_LOG,
   trace: SEVERITY_LOG,
+  dir: SEVERITY_LOG
 };
 
 // The lowest HTTP response code (inclusive) that is considered an error.
 const MIN_HTTP_ERROR_CODE = 400;
 // The highest HTTP response code (exclusive) that is considered an error.
 const MAX_HTTP_ERROR_CODE = 600;
 
 // HTTP status codes.
@@ -1230,16 +1231,20 @@ function pruneConsoleOutputIfNecessary(a
     if (messageNodes[i].classList.contains("webconsole-msg-cssparser")) {
       let desc = messageNodes[i].childNodes[2].textContent;
       let location = "";
       if (messageNodes[i].childNodes[4]) {
         location = messageNodes[i].childNodes[4].getAttribute("title");
       }
       delete hudRef.cssNodes[desc + location];
     }
+    else if (messageNodes[i].classList.contains("webconsole-msg-inspector")) {
+      hudRef.pruneConsoleDirNode(messageNodes[i]);
+      continue;
+    }
     messageNodes[i].parentNode.removeChild(messageNodes[i]);
   }
 
   if (!scrolledToBottom && removeNodes > 0 &&
       oldScrollHeight != scrollBox.scrollHeight) {
     scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
   }
 
@@ -1964,28 +1969,36 @@ HUD_SERVICE.prototype =
           clipboardText += aFrame.filename + " :: " +
                            aFrame.functionName + " :: " +
                            aFrame.lineNumber + "\n";
         });
 
         clipboardText = clipboardText.trimRight();
         break;
 
+      case "dir":
+        body = unwrap(args[0]);
+        clipboardText = body.toString();
+        sourceURL = aMessage.filename;
+        sourceLine = aMessage.lineNumber;
+        break;
+
       default:
         Cu.reportError("Unknown Console API log level: " + level);
         return;
     }
 
     let node = ConsoleUtils.createMessageNode(hud.outputNode.ownerDocument,
                                               CATEGORY_WEBDEV,
                                               LEVELS[level],
                                               body,
                                               sourceURL,
                                               sourceLine,
-                                              clipboardText);
+                                              clipboardText,
+                                              level);
 
     // Make the node bring up the property panel, to allow the user to inspect
     // the stack trace.
     if (level == "trace") {
       node._stacktrace = args;
 
       let linkNode = node.querySelector(".webconsole-msg-body");
       linkNode.classList.add("hud-clickable");
@@ -2009,16 +2022,24 @@ HUD_SERVICE.prototype =
                                                        this);
           propPanel.panel.setAttribute("hudId", aHUDId);
           this._panelOpen = true;
         }
       }, false);
     }
 
     ConsoleUtils.outputMessageNode(node, aHUDId);
+
+    if (level == "dir") {
+      // Initialize the inspector message node, by setting the PropertyTreeView
+      // object on the tree view. This has to be done *after* the node is
+      // shown, because the tree binding must be attached first.
+      let tree = node.querySelector("tree");
+      tree.view = node.propertyTreeView;
+    }
   },
 
   /**
    * Inform user that the Web Console API has been replaced by a script
    * in a content page.
    *
    * @param string aHUDId
    *        The ID of the Web Console to which to send the message.
@@ -3782,16 +3803,34 @@ HeadsUpDisplay.prototype = {
     clearButton.setAttribute("label", this.getStr("btnClear"));
     clearButton.classList.add("webconsole-clear-console-button");
     clearButton.addEventListener("command", HUD_clearButton_onCommand, false);
 
     aToolbar.appendChild(clearButton);
   },
 
   /**
+   * Destroy the property inspector message node. This performs the necessary
+   * cleanup for the tree widget and removes it from the DOM.
+   *
+   * @param nsIDOMNode aMessageNode
+   *        The message node that contains the property inspector from a
+   *        console.dir call.
+   */
+  pruneConsoleDirNode: function HUD_pruneConsoleDirNode(aMessageNode)
+  {
+    aMessageNode.parentNode.removeChild(aMessageNode);
+    let tree = aMessageNode.querySelector("tree");
+    tree.parentNode.removeChild(tree);
+    aMessageNode.propertyTreeView = null;
+    tree.view = null;
+    tree = null;
+  },
+
+  /**
    * Create the Web Console UI.
    *
    * @return nsIDOMNode
    *         The Web Console container element (HUDBox).
    */
   createHUD: function HUD_createHUD()
   {
     if (!this.HUDBox) {
@@ -4789,18 +4828,25 @@ JSTerm.prototype = {
     return type.toLowerCase();
   },
 
   clearOutput: function JST_clearOutput()
   {
     let hud = HUDService.getHudReferenceById(this.hudId);
     hud.cssNodes = {};
 
-    while (hud.outputNode.firstChild) {
-      hud.outputNode.removeChild(hud.outputNode.firstChild);
+    let node = hud.outputNode;
+    while (node.firstChild) {
+      if (node.firstChild.classList &&
+          node.firstChild.classList.contains("webconsole-msg-inspector")) {
+        hud.pruneConsoleDirNode(node.firstChild);
+      }
+      else {
+        hud.outputNode.removeChild(node.firstChild);
+      }
     }
 
     hud.HUDBox.lastTimestamp = 0;
   },
 
   /**
    * Updates the size of the input field (command line) to fit its contents.
    *
@@ -5395,24 +5441,26 @@ ConsoleUtils = {
    *        The URL of the source file that emitted 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 aClipboardText [optional]
    *        The text that should be copied to the clipboard when this node is
    *        copied. If omitted, defaults to the body text. If `aBody` is not
    *        a string, then the clipboard text must be supplied.
+   * @param number aLevel [optional]
+   *        The level of the console API message.
    * @return nsIDOMNode
    *         The message node: a XUL richlistitem ready to be inserted into
    *         the Web Console output node.
    */
   createMessageNode:
   function ConsoleUtils_createMessageNode(aDocument, aCategory, aSeverity,
                                           aBody, aSourceURL, aSourceLine,
-                                          aClipboardText) {
+                                          aClipboardText, aLevel) {
     if (aBody instanceof Ci.nsIDOMNode && aClipboardText == null) {
       throw new Error("HUDService.createMessageNode(): DOM node supplied " +
                       "without any clipboard text");
     }
 
     // Make the icon container, which is a vertical box. Its purpose is to
     // ensure that the icon stays anchored at the top of the message even for
     // long multi-line messages.
@@ -5430,22 +5478,25 @@ ConsoleUtils = {
     spacer.setAttribute("flex", "1");
     iconContainer.appendChild(spacer);
 
     // Create the message body, which contains the actual text of the message.
     let bodyNode = aDocument.createElementNS(XUL_NS, "description");
     bodyNode.setAttribute("flex", "1");
     bodyNode.classList.add("webconsole-msg-body");
 
+    // Store the body text, since it is needed later for the property tree
+    // case.
+    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 : ""));
-    aBody = aBody instanceof Ci.nsIDOMNode ?
+    aBody = aBody instanceof Ci.nsIDOMNode && !(aLevel == "dir") ?
             aBody : aDocument.createTextNode(aBody);
 
     bodyNode.appendChild(aBody);
 
     let repeatContainer = aDocument.createElementNS(XUL_NS, "hbox");
     repeatContainer.setAttribute("align", "start");
     let repeatNode = aDocument.createElementNS(XUL_NS, "label");
     repeatNode.setAttribute("value", "1");
@@ -5470,22 +5521,57 @@ ConsoleUtils = {
     // Create the containing node and append all its elements to it.
     let node = aDocument.createElementNS(XUL_NS, "richlistitem");
     node.clipboardText = aClipboardText;
     node.classList.add("hud-msg-node");
 
     node.timestamp = timestamp;
     ConsoleUtils.setMessageType(node, aCategory, aSeverity);
 
-    node.appendChild(timestampNode);  // childNode[0]
-    node.appendChild(iconContainer);  // childNode[1]
-    node.appendChild(bodyNode);       // childNode[2]
-    node.appendChild(repeatContainer);  // childNode[3]
+    node.appendChild(timestampNode);
+    node.appendChild(iconContainer);
+    // Display the object tree after the message node.
+    if (aLevel == "dir") {
+      // Make the body container, which is a vertical box, for grouping the text
+      // and tree widgets.
+      let bodyContainer = aDocument.createElement("vbox");
+      bodyContainer.setAttribute("flex", "1");
+      bodyContainer.appendChild(bodyNode);
+      // Create the tree.
+      let tree = createElement(aDocument, "tree", {
+        flex: 1,
+        hidecolumnpicker: "true"
+      });
+
+      let treecols = aDocument.createElement("treecols");
+      let treecol = createElement(aDocument, "treecol", {
+        primary: "true",
+        flex: 1,
+        hideheader: "true",
+        ignoreincolumnpicker: "true"
+      });
+      treecols.appendChild(treecol);
+      tree.appendChild(treecols);
+
+      tree.appendChild(aDocument.createElement("treechildren"));
+
+      bodyContainer.appendChild(tree);
+      node.appendChild(bodyContainer);
+      node.classList.add("webconsole-msg-inspector");
+      // Create the treeView object.
+      let treeView = node.propertyTreeView = new PropertyTreeView();
+      treeView.data = body;
+      tree.setAttribute("rows", treeView.rowCount);
+    }
+    else {
+      node.appendChild(bodyNode);
+    }
+    node.appendChild(repeatContainer);
     if (locationNode) {
-      node.appendChild(locationNode); // childNode[4]
+      node.appendChild(locationNode);
     }
 
     node.setAttribute("id", "console-msg-" + HUDService.sequenceId());
 
     return node;
   },
 
   /**
@@ -5678,17 +5764,17 @@ ConsoleUtils = {
    * @return boolean
    *         true if the message is filtered, false otherwise.
    */
   filterRepeatedConsole:
   function ConsoleUtils_filterRepeatedConsole(aNode, aOutput) {
     let lastMessage = aOutput.lastChild;
 
     // childNodes[2] is the description element
-    if (lastMessage &&
+    if (lastMessage && !aNode.classList.contains("webconsole-msg-inspector") &&
         aNode.childNodes[2].textContent ==
         lastMessage.childNodes[2].textContent) {
       this.mergeFilteredMessageNode(lastMessage, aNode);
       return true;
     }
 
     return false;
   },
@@ -5712,17 +5798,17 @@ ConsoleUtils = {
     if (aNode.classList.contains("webconsole-msg-cssparser")) {
       isRepeated = this.filterRepeatedCSS(aNode, outputNode, aHUDId);
     }
 
     if (!isRepeated &&
         (aNode.classList.contains("webconsole-msg-console") ||
          aNode.classList.contains("webconsole-msg-exception") ||
          aNode.classList.contains("webconsole-msg-error"))) {
-      isRepeated = this.filterRepeatedConsole(aNode, outputNode, aHUDId);
+      isRepeated = this.filterRepeatedConsole(aNode, outputNode);
     }
 
     if (!isRepeated) {
       outputNode.appendChild(aNode);
     }
 
     HUDService.regroupOutput(outputNode);
 
--- a/browser/devtools/webconsole/test/browser/Makefile.in
+++ b/browser/devtools/webconsole/test/browser/Makefile.in
@@ -137,16 +137,17 @@ include $(topsrcdir)/config/rules.mk
 	browser_webconsole_position_ui.js \
 	browser_webconsole_bug_642615_autocomplete.js \
 	browser_webconsole_bug_585991_autocomplete_popup.js \
 	browser_webconsole_bug_585991_autocomplete_keys.js \
 	browser_webconsole_bug_663443_panel_title.js \
 	browser_webconsole_bug_660806_history_nav.js \
 	browser_webconsole_bug_651501_document_body_autocomplete.js \
 	browser_webconsole_bug_653531_highlighter_console_helper.js \
+	browser_webconsole_bug_659907_console_dir.js \
 	head.js \
 	$(NULL)
 
 _BROWSER_TEST_PAGES = \
 	test-console.html \
 	test-network.html \
 	test-network-request.html \
 	test-mutation.html \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser/browser_webconsole_bug_659907_console_dir.js
@@ -0,0 +1,45 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that console.dir works as intended.
+
+function test() {
+  addTab("data:text/html,Web Console test for bug 659907: Expand console " +
+         "object with a dir method");
+  browser.addEventListener("load", onLoad, true);
+}
+
+function onLoad(aEvent) {
+  browser.removeEventListener(aEvent.type, arguments.callee, true);
+
+  openConsole();
+  let hudId = HUDService.getHudIdByWindow(content);
+  let hud = HUDService.hudReferences[hudId];
+  outputNode = hud.outputNode;
+  content.console.dir(content.document);
+  findLogEntry("[object HTMLDocument");
+  let msg = outputNode.querySelectorAll(".webconsole-msg-inspector");
+  is(msg.length, 1, "one message node displayed");
+  let rows = msg[0].propertyTreeView._rows;
+  let foundQSA = false;
+  let foundLocation = false;
+  let foundWrite = false;
+  for (let i = 0; i < rows.length; i++) {
+    if (rows[i].display == "querySelectorAll: function querySelectorAll()") {
+      foundQSA = true;
+    }
+    else if (rows[i].display  == "location: Object") {
+      foundLocation = true;
+    }
+    else if (rows[i].display  == "write: function write()") {
+      foundWrite = true;
+    }
+  }
+  ok(foundQSA, "found document.querySelectorAll");
+  ok(foundLocation, "found document.location");
+  ok(foundWrite, "found document.write");
+  finishTest();
+}
--- a/browser/devtools/webconsole/test/browser/test-console-extras.html
+++ b/browser/devtools/webconsole/test/browser/test-console-extras.html
@@ -4,17 +4,16 @@
     <script type="text/javascript">
       function test() {
         console.log("start");
         console.time();
         console.timeEnd()
         console.exception()
         console.assert()
         console.clear()
-        console.dir()
         console.dirxml()
         console.group()
         console.groupCollapsed()
         console.groupEnd()
         console.profile()
         console.profileEnd()
         console.count()
         console.table()
--- a/dom/base/ConsoleAPI.js
+++ b/dom/base/ConsoleAPI.js
@@ -78,23 +78,28 @@ ConsoleAPI.prototype = {
         self.notifyObservers(id, "error", arguments);
       },
       debug: function CA_debug() {
         self.notifyObservers(id, "log", arguments);
       },
       trace: function CA_trace() {
         self.notifyObservers(id, "trace", self.getStackTrace());
       },
+      // Displays an interactive listing of all the properties of an object.
+      dir: function CA_dir() {
+        self.notifyObservers(id, "dir", arguments);
+      },
       __exposedProps__: {
         log: "r",
         info: "r",
         warn: "r",
         error: "r",
         debug: "r",
         trace: "r",
+        dir: "r"
       }
     };
 
     // We need to return an actual content object here, instead of a wrapped
     // chrome object. This allows things like console.log.bind() to work.
     let contentObj = Cu.createObjectIn(aWindow);
     function genPropDesc(fun) {
       return { enumerable: true, configurable: true, writable: true,
@@ -102,16 +107,17 @@ ConsoleAPI.prototype = {
     }
     const properties = {
       log: genPropDesc('log'),
       info: genPropDesc('info'),
       warn: genPropDesc('warn'),
       error: genPropDesc('error'),
       debug: genPropDesc('debug'),
       trace: genPropDesc('trace'),
+      dir: genPropDesc('dir'),
       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
                           value: function() {} },
       __mozillaConsole__: { value: true }
     };
 
     Object.defineProperties(contentObj, properties);
     Cu.makeObjectPropsNormal(contentObj);
 
--- a/dom/tests/browser/browser_ConsoleAPITests.js
+++ b/dom/tests/browser/browser_ConsoleAPITests.js
@@ -159,30 +159,34 @@ function observeConsoleTest() {
   win.console.log("arg");
 
   expect("info", "arg", "extra arg");
   win.console.info("arg", "extra arg");
 
   expect("warn", "arg", "extra arg", 1);
   win.console.warn("arg", "extra arg", 1);
 
+  expect("dir", win.toString());
+  win.console.dir(win);
+
   expect("error", "arg");
   win.console.error("arg");
 }
 
 function consoleAPISanityTest() {
   let win = XPCNativeWrapper.unwrap(gWindow);
   ok(win.console, "we have a console attached");
   ok(win.console, "we have a console attached, 2nd attempt");
 
   ok(win.console.log, "console.log is here");
   ok(win.console.info, "console.info is here");
   ok(win.console.warn, "console.warn is here");
   ok(win.console.error, "console.error is here");
   ok(win.console.trace, "console.trace is here");
+  ok(win.console.dir, "console.dir is here");
 }
 
 var ConsoleObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   init: function CO_init() {
     Services.obs.addObserver(this, "console-api-log-event", false);
   },
--- a/dom/tests/mochitest/general/test_consoleAPI.html
+++ b/dom/tests/mochitest/general/test_consoleAPI.html
@@ -22,16 +22,17 @@ function doTest() {
 
   var expectedProps = {
     "log": "function",
     "info": "function",
     "warn": "function",
     "error": "function",
     "debug": "function",
     "trace": "function",
+    "dir": "function",
     "__noSuchMethod__": "function"
   };
 
   var foundProps = 0;
   for (var prop in console) {
     foundProps++;
     is(typeof(console[prop]), expectedProps[prop], "expect console prop " + prop + " exists");
   }