Bug 583476 - WebConsole fails to activate when window.console is already defined r=sdwilsh a=betaN
authorMihai Sucan <msucan@mozilla.com>
Mon, 13 Sep 2010 10:15:59 -0700
changeset 53703 fdc34f208b336f6bef9721c03b3cf12e01e4bd70
parent 53702 e931499f28ebe939db16699ee1284ace466f0e7e
child 53704 07612386060bad4c29db8fe420cf36ee7b716b3e
push idunknown
push userunknown
push dateunknown
reviewerssdwilsh, betaN
bugs583476
milestone2.0b6pre
Bug 583476 - WebConsole fails to activate when window.console is already defined r=sdwilsh a=betaN
toolkit/components/console/hudservice/HUDService.jsm
toolkit/components/console/hudservice/tests/browser/Makefile.in
toolkit/components/console/hudservice/tests/browser/browser_warn_user_about_replaced_api.js
toolkit/components/console/hudservice/tests/browser/browser_webconsole_consoleonpage.js
toolkit/components/console/hudservice/tests/browser/test-own-console.html
--- a/toolkit/components/console/hudservice/HUDService.jsm
+++ b/toolkit/components/console/hudservice/HUDService.jsm
@@ -1347,18 +1347,16 @@ HUD_SERVICE.prototype =
     var window = aContext.linkedBrowser.contentWindow;
     var browser = gBrowser.getBrowserForDocument(window.top.document);
     var tabId = gBrowser.getNotificationBox(browser).getAttribute("id");
     var hudId = "hud_" + tabId;
     var displayNode = this.getHeadsUpDisplay(hudId);
 
     this.unregisterActiveContext(hudId);
     this.unregisterDisplay(hudId);
-    window.wrappedJSObject.console = null;
-
   },
 
   /**
    * Clear the specified HeadsUpDisplay
    *
    * @param string aId
    * @returns void
    */
@@ -1706,16 +1704,23 @@ HUD_SERVICE.prototype =
     for (var i = 0; i < len; i++) {
       if (splitters[i].getAttribute("class") == "hud-splitter") {
         splitters[i].parentNode.removeChild(splitters[i]);
         break;
       }
     }
     // remove the DOM Nodes
     parent.removeChild(outputNode);
+
+    this.windowRegistry[aId].forEach(function(aContentWindow) {
+      if (aContentWindow.wrappedJSObject.console instanceof HUDConsole) {
+        delete aContentWindow.wrappedJSObject.console;
+      }
+    });
+
     // remove our record of the DOM Nodes from the registry
     delete this._headsUpDisplays[aId];
     // remove the HeadsUpDisplay object from memory
     this.deleteHeadsUpDisplay(aId);
     // remove the related storage object
     this.storage.removeDisplay(aId);
     // remove the related window objects
     delete this.windowRegistry[aId];
@@ -1964,18 +1969,18 @@ HUD_SERVICE.prototype =
   /**
    * Get OutputNode by Id
    *
    * @param string aId
    * @returns nsIDOMNode
    */
   getConsoleOutputNode: function HS_getConsoleOutputNode(aId)
   {
-    let displayNode = this.getHeadsUpDisplay(aHUDId);
-    return displayNode.querySelectorAll(".hud-output-node")[0];
+    let displayNode = this.getHeadsUpDisplay(aId);
+    return displayNode.querySelector(".hud-output-node");
   },
 
   /**
    * Inform user that the Web Console API has been replaced by a script
    * in a content page.
    *
    * @param string aHUDId
    * @returns void
@@ -2557,28 +2562,34 @@ HUD_SERVICE.prototype =
   messageFactory:
   function messageFactory(aMessage, aLevel, aOutputNode, aActivityObject)
   {
     // generate a LogMessage object
     return new LogMessage(aMessage, aLevel, aOutputNode,  aActivityObject);
   },
 
   /**
-   * Initialize the JSTerm object to create a JS Workspace
+   * Initialize the JSTerm object to create a JS Workspace by attaching the UI
+   * into the given parent node, using the mixin.
    *
-   * @param nsIDOMWindow aContext
-   * @param nsIDOMNode aParentNode
-   * @returns void
+   * @param nsIDOMWindow aContext the context used for evaluating user input
+   * @param nsIDOMNode aParentNode where to attach the JSTerm
+   * @param object aConsole
+   *        Console object used within the JSTerm instance to report errors
+   *        and log data (by calling console.error(), console.log(), etc).
    */
-  initializeJSTerm: function HS_initializeJSTerm(aContext, aParentNode)
+  initializeJSTerm: function HS_initializeJSTerm(aContext, aParentNode, aConsole)
   {
     // create Initial JS Workspace:
     var context = Cu.getWeakReference(aContext);
+
+    // Attach the UI into the target parent node using the mixin.
     var firefoxMixin = new JSTermFirefoxMixin(context, aParentNode);
-    var jsTerm = new JSTerm(context, aParentNode, firefoxMixin);
+    var jsTerm = new JSTerm(context, aParentNode, firefoxMixin, aConsole);
+
     // TODO: injection of additional functionality needs re-thinking/api
     // see bug 559748
   },
 
   /**
    * Passed a HUDId, the corresponding window is returned
    *
    * @param string aHUDId
@@ -2693,57 +2704,55 @@ HUD_SERVICE.prototype =
     let hudId = "hud_" + nBoxId;
 
     if (!this.canActivateContext(hudId)) {
       return;
     }
 
     this.registerDisplay(hudId, aContentWindow);
 
-    // check if aContentWindow has a console Object
-    let _console = aContentWindow.wrappedJSObject.console;
-    if (!_console) {
-      // no console exists. does the HUD exist?
-      let hudNode;
-      let childNodes = nBox.childNodes;
-
-      for (var i = 0; i < childNodes.length; i++) {
-        let id = childNodes[i].getAttribute("id");
-        if (id.split("_")[0] == "hud") {
-          hudNode = childNodes[i];
-          break;
-        }
-      }
-
-      if (!hudNode) {
-        // get nBox object and call new HUD
-        let config = { parentNode: nBox,
-                       contentWindow: aContentWindow
-                     };
-
-        let _hud = new HeadsUpDisplay(config);
-
-        let hudWeakRef = Cu.getWeakReference(_hud);
-        HUDService.registerHUDWeakReference(hudWeakRef, hudId);
-      }
-      else {
-        // only need to attach a console object to the window object
-        let config = { hudNode: hudNode,
-                       consoleOnly: true,
-                       contentWindow: aContentWindow
-                     };
-
-        let _hud = new HeadsUpDisplay(config);
-
-        let hudWeakRef = Cu.getWeakReference(_hud);
-        HUDService.registerHUDWeakReference(hudWeakRef, hudId);
-
-        aContentWindow.wrappedJSObject.console = _hud.console;
+    let hudNode;
+    let childNodes = nBox.childNodes;
+
+    for (let i = 0; i < childNodes.length; i++) {
+      let id = childNodes[i].getAttribute("id");
+      // `id` is a string with the format "hud_<number>".
+      if (id.split("_")[0] == "hud") {
+        hudNode = childNodes[i];
+        break;
       }
     }
+
+    let hud;
+    // If there is no HUD for this tab create a new one.
+    if (!hudNode) {
+      // get nBox object and call new HUD
+      let config = { parentNode: nBox,
+                     contentWindow: aContentWindow,
+                   };
+
+      hud = new HeadsUpDisplay(config);
+
+      let hudWeakRef = Cu.getWeakReference(hud);
+      HUDService.registerHUDWeakReference(hudWeakRef, hudId);
+    }
+    else {
+      hud = this.hudWeakReferences[hudId].get();
+      hud.reattachConsole(aContentWindow.top);
+    }
+
+    // Check if aContentWindow has a console object. If so, don't attach
+    // our console, but warn the user about this.
+    if (aContentWindow.wrappedJSObject.console) {
+      this.logWarningAboutReplacedAPI(hudId);
+    }
+    else {
+      aContentWindow.wrappedJSObject.console = hud.console;
+    }
+
     // capture JS Errors
     this.setOnErrorHandler(aContentWindow);
 
     // register the controller to handle "select all" properly
     this.createController(xulWindow);
   },
 
   /**
@@ -2777,33 +2786,16 @@ function HeadsUpDisplay(aConfig)
   //                  // or
   //                  parentNodeId: "myHUDParent123",
   //
   //                  placement: "appendChild"
   //                  // or
   //                  placement: "insertBefore",
   //                  placementChildNodeIndex: 0,
   //                }
-  //
-  // or, just create a new console - as there is already a HUD in place
-  // config: { hudNode: existingHUDDOMNode,
-  //           consoleOnly: true,
-  //           contentWindow: aWindow
-  //         }
-
-  if (aConfig.consoleOnly) {
-    this.HUDBox = aConfig.hudNode;
-    this.parentNode = aConfig.hudNode.parentNode;
-    this.notificationBox = this.parentNode;
-    this.contentWindow = aConfig.contentWindow;
-    this.uriSpec = aConfig.contentWindow.location.href;
-    this.reattachConsole();
-    this.HUDBox.querySelectorAll(".jsterm-input-node")[0].focus();
-    return;
-  }
 
   this.HUDBox = null;
 
   if (aConfig.parentNode) {
     // TODO: need to replace these DOM calls with internal functions
     // that operate on each application's node structure
     // better yet, we keep these functions in a "bridgeModule" or the HUDService
     // to keep a registry of nodeGetters for each application
@@ -2881,21 +2873,20 @@ function HeadsUpDisplay(aConfig)
   let hudBox = this.createHUD();
 
   let splitter = this.chromeDocument.createElement("splitter");
   splitter.setAttribute("class", "hud-splitter");
 
   this.notificationBox.insertBefore(splitter,
                                     this.notificationBox.childNodes[1]);
 
-  let console = this.createConsole();
-
   this.HUDBox.lastTimestamp = 0;
 
-  this.contentWindow.wrappedJSObject.console = console;
+  // Create the console object that is attached to the window later.
+  this._console = this.createConsole();
 
   // create the JSTerm input element
   try {
     this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
     this.HUDBox.querySelectorAll(".jsterm-input-node")[0].focus();
   }
   catch (ex) {
     Cu.reportError(ex);
@@ -2941,44 +2932,46 @@ HeadsUpDisplay.prototype = {
   createConsoleInput:
   function HUD_createConsoleInput(aWindow, aParentNode, aExistingConsole)
   {
     var context = Cu.getWeakReference(aWindow);
 
     if (appName() == "FIREFOX") {
       let outputCSSClassOverride = "hud-msg-node";
       let mixin = new JSTermFirefoxMixin(context, aParentNode, aExistingConsole, outputCSSClassOverride);
-      this.jsterm = new JSTerm(context, aParentNode, mixin);
+      this.jsterm = new JSTerm(context, aParentNode, mixin, this.console);
     }
     else {
       throw new Error("Unsupported Gecko Application");
     }
   },
 
   /**
    * Re-attaches a console when the contentWindow is recreated
    *
+   * @param nsIDOMWindow aContentWindow
    * @returns void
    */
-  reattachConsole: function HUD_reattachConsole()
+  reattachConsole: function HUD_reattachConsole(aContentWindow)
   {
-    this.hudId = this.HUDBox.getAttribute("id");
-
-    this.outputNode = this.HUDBox.querySelectorAll(".hud-output-node")[0];
-
-    this.chromeWindow = HUDService.
-      getChromeWindowFromContentWindow(this.contentWindow);
-    this.chromeDocument = this.HUDBox.ownerDocument;
-
-    if (this.outputNode) {
-      // createConsole
-      this.createConsole();
+    this.contentWindow = aContentWindow;
+    this.contentDocument = this.contentWindow.document;
+    this.uriSpec = this.contentWindow.location.href;
+
+    if (!this._console) {
+      this._console = this.createConsole();
+    }
+
+    if (!this.jsterm) {
+      this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
     }
     else {
-      throw new Error("Cannot get output node");
+      this.jsterm.context = Cu.getWeakReference(this.contentWindow);
+      this.jsterm.console = this.console;
+      this.jsterm.createSandbox();
     }
   },
 
   /**
    * Shortcut to make XUL nodes
    *
    * @param string aTag
    * @returns nsIDOMNode
@@ -3255,17 +3248,23 @@ HeadsUpDisplay.prototype = {
 
       let nodes = this.notificationBox.insertBefore(this.HUDBox,
         this.notificationBox.childNodes[0]);
 
       return this.HUDBox;
     }
   },
 
-  get console() { return this._console || this.createConsole(); },
+  get console() {
+    if (!this._console) {
+      this._console = this.createConsole();
+    }
+
+    return this._console;
+  },
 
   getLogCount: function HUD_getLogCount()
   {
     return this.outputNode.childNodes.length;
   },
 
   getLogNodes: function HUD_getLogNodes()
   {
@@ -3304,18 +3303,16 @@ HeadsUpDisplay.prototype = {
  */
 function HUDConsole(aHeadsUpDisplay)
 {
   let hud = aHeadsUpDisplay;
   let hudId = hud.hudId;
   let outputNode = hud.outputNode;
   let chromeDocument = hud.chromeDocument;
 
-  aHeadsUpDisplay._console = this;
-
   let sendToHUDService = function console_send(aLevel, aArguments)
   {
     let ts = ConsoleUtils.timestamp();
     let messageNode = hud.makeXULNode("label");
 
     let klass = "hud-msg-node hud-" + aLevel;
 
     messageNode.setAttribute("class", klass);
@@ -3785,35 +3782,36 @@ function JSTermHelper(aJSTerm)
 /**
  * JSTerm
  *
  * JavaScript Terminal: creates input nodes for console code interpretation
  * and 'JS Workspaces'
  */
 
 /**
- * Create a JSTerminal or attach a JSTerm input node to an existing output node
- *
- *
+ * Create a JSTerminal or attach a JSTerm input node to an existing output node,
+ * given by the parent node.
  *
  * @param object aContext
  *        Usually nsIDOMWindow, but doesn't have to be
- * @param nsIDOMNode aParentNode
+ * @param nsIDOMNode aParentNode where to attach the JSTerm
  * @param object aMixin
  *        Gecko-app (or Jetpack) specific utility object
- * @returns void
+ * @param object aConsole
+ *        Console object to use within the JSTerm.
  */
-function JSTerm(aContext, aParentNode, aMixin)
+function JSTerm(aContext, aParentNode, aMixin, aConsole)
 {
   // set the context, attach the UI by appending to aParentNode
 
   this.application = appName();
   this.context = aContext;
   this.parentNode = aParentNode;
   this.mixins = aMixin;
+  this.console = aConsole;
 
   this.xulElementFactory =
     NodeFactory("xul", "xul", aParentNode.ownerDocument);
 
   this.textFactory = NodeFactory("text", "xul", aParentNode.ownerDocument);
 
   this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;
 
@@ -3860,18 +3858,16 @@ JSTerm.prototype = {
   attachUI: function JST_attachUI()
   {
     this.mixins.attachUI();
   },
 
   createSandbox: function JST_setupSandbox()
   {
     // create a JS Sandbox out of this.context
-    this._window.wrappedJSObject.jsterm = {};
-    this.console = this._window.wrappedJSObject.console;
     this.sandbox = new Cu.Sandbox(this._window);
     this.sandbox.window = this._window;
     this.sandbox.console = this.console;
     this.sandbox.__helperFunctions__ = JSTermHelper(this);
     this.sandbox.__proto__ = this._window.wrappedJSObject;
   },
 
   get _window()
--- a/toolkit/components/console/hudservice/tests/browser/Makefile.in
+++ b/toolkit/components/console/hudservice/tests/browser/Makefile.in
@@ -50,16 +50,17 @@ include $(topsrcdir)/config/rules.mk
 	browser_warn_user_about_replaced_api.js \
 	browser_webconsole_bug_585237_line_limit.js \
 	browser_webconsole_bug_586388_select_all.js  \
 	browser_webconsole_bug_588967_input_expansion.js \
 	browser_webconsole_bug_580454_timestamp_l10n.js \
 	browser_webconsole_netlogging.js \
 	browser_webconsole_bug_593003_iframe_wrong_hud.js \
 	browser_webconsole_bug_581231_close_button.js \
+	browser_webconsole_consoleonpage.js \
 	$(NULL)
 
 _BROWSER_TEST_PAGES = \
 	test-console.html \
 	test-network.html \
 	test-network-request.html \
 	test-mutation.html \
 	testscript.js \
@@ -69,15 +70,16 @@ include $(topsrcdir)/config/rules.mk
 	test-property-provider.html \
 	test-error.html \
 	test-duplicate-error.html \
 	test-image.png \
 	test-encoding-ISO-8859-1.html \
 	test-bug-593003-iframe-wrong-hud.html \
 	test-bug-593003-iframe-wrong-hud-iframe.html \
 	test-console-replaced-api.html \
+	test-own-console.html \
 	$(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
 
 libs:: $(_BROWSER_TEST_PAGES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/toolkit/components/console/hudservice/tests/browser/browser_warn_user_about_replaced_api.js
+++ b/toolkit/components/console/hudservice/tests/browser/browser_warn_user_about_replaced_api.js
@@ -15,16 +15,17 @@
  * The Original Code is DevTools test code.
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  David Dahl <ddahl@mozilla.com>
+ *  Mihai Șucan <mihai.sucan@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -53,62 +54,56 @@ function testOpenWebConsole()
 {
   HUDService.activateHUDForContext(gBrowser.selectedTab);
   is(HUDService.displaysIndex().length, 1, "WebConsole was opened");
 
   hudId = HUDService.displaysIndex()[0];
   hud = HUDService.getHeadsUpDisplay(hudId);
 
   HUDService.logWarningAboutReplacedAPI(hudId);
+  testWarning();
 }
 
 function testWarning()
 {
   const successMsg = "Found the warning message";
   const errMsg = "Could not find the warning message about the replaced API";
 
   var display = HUDService.getDisplayByURISpec(content.location.href);
   var outputNode = display.querySelectorAll(".hud-output-node")[0];
-  executeSoon(function () {
-    testLogEntry(outputNode, "disabled", { success: successMsg, err: errMsg });
-  });
+
+  testLogEntry(outputNode, "disabled", { success: successMsg, err: errMsg });
+
+  HUDService.deactivateHUDForContext(gBrowser.selectedTab);
+  executeSoon(finishTest);
 }
 
 function testLogEntry(aOutputNode, aMatchString, aSuccessErrObj)
 {
   var message = aOutputNode.textContent.indexOf(aMatchString);
   if (message > -1) {
     ok(true, aSuccessErrObj.success);
-  return;
+    return;
   }
   ok(false, aSuccessErrObj.err);
 }
 
-function finishTest() {
+function finishTest()
+{
   hud = null;
   hudId = null;
 
-  executeSoon(function() {
-    finish();
-  });
+  finish();
 }
 
-let hud, hudId, tab, browser, filterBox, outputNode;
-let win = gBrowser.selectedBrowser;
+let hud, hudId;
 
-tab = gBrowser.selectedTab;
-browser = gBrowser.getBrowserForTab(tab);
+content.location = TEST_REPLACED_API_URI;
 
-content.location.href = TEST_REPLACED_API_URI;
-
-function test() {
+function test()
+{
   waitForExplicitFinish();
-  browser.addEventListener("DOMContentLoaded", function onLoad(event) {
-    browser.removeEventListener("DOMContentLoaded", onLoad, false);
-    executeSoon(function (){
-      testOpenWebConsole();
-      executeSoon(function (){
-        testWarning();
-      });
-    });
-  }, false);
-  finishTest();
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee,
+      true);
+    testOpenWebConsole();
+  }, true);
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/console/hudservice/tests/browser/browser_webconsole_consoleonpage.js
@@ -0,0 +1,81 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * Contributor(s):
+ *  Julian Viereck <jviereck@mozilla.com>
+ *  Mihai Șucan <mihai.sucan@gmail.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/HUDService.jsm");
+
+let hud;
+let hudId;
+
+function testOpenWebConsole()
+{
+  HUDService.activateHUDForContext(gBrowser.selectedTab);
+  is(HUDService.displaysIndex().length, 1, "WebConsole was opened");
+
+  hudId = HUDService.displaysIndex()[0];
+  hud = HUDService.hudWeakReferences[hudId].get();
+
+  testOwnConsole();
+}
+
+function testConsoleOnPage() {
+  let console = content.wrappedJSObject.console;
+  isnot(console, undefined, "Console object defined on page");
+  is(console.foo, "bar", "Custom console is not overwritten");
+}
+
+function testOwnConsole()
+{
+  // Test console on the page. There is already one so it shouldn't be
+  // overwritten by the WebConsole's console.
+  testConsoleOnPage();
+
+  // Check that the console object is set on the jsterm object although there
+  // is no console object added to the page.
+  ok(hud.jsterm.console, "JSTerm console is defined");
+  ok(hud.jsterm.console === hud._console, "JSTerm console is same as HUD console");
+
+  content.wrappedJSObject.loadIFrame(function(iFrame) {
+    // Test the console in the iFrame.
+    let consoleIFrame = iFrame.wrappedJSObject.contentWindow.console;
+    isnot(consoleIFrame, undefined, "Console object defined in iFrame");
+
+    ok(consoleIFrame === hud._console, "Console on the page is hud console");
+
+    // Close the hud and see which console is still around.
+    HUDService.deactivateHUDForContext(gBrowser.selectedTab);
+
+    executeSoon(function () {
+      consoleIFrame = iFrame.wrappedJSObject.contentWindow.console;
+      is(consoleIFrame, undefined, "Console object was removed from iFrame");
+      testConsoleOnPage();
+
+      hud = hudId = null;
+      gBrowser.removeCurrentTab();
+      finish();
+    });
+  });
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    waitForFocus(testOpenWebConsole, content);
+  }, true);
+
+  content.location =
+    "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-own-console.html";
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/console/hudservice/tests/browser/test-own-console.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+<head>
+<script>
+  window.console = {
+    foo: "bar"
+  }
+
+  function loadIFrame(aCallback) {
+    var iframe = document.body.querySelector("iframe");
+    iframe.addEventListener("load", function() {
+      iframe.removeEventListener("load", arguments.callee, true);
+      aCallback(iframe);
+    }, true);
+
+    iframe.setAttribute("src", "test-console.html");
+  }
+</script>
+</head>
+<body>
+  <iframe></iframe>
+</body>