merge m-c to fx-team
authorTim Taubert <tim.taubert@gmx.de>
Sat, 12 May 2012 20:06:48 +0200
changeset 93855 c758cc9b60e5d662c2d8b72f5bd4634baa3f327e
parent 93851 06ff01938bfa343a1970b33657988ead55075673 (current diff)
parent 93854 6d1fa04418301095fa8cc225ed549146444139de (diff)
child 93856 5fbb80b8e84f12e4af6b8694b42b45a01959026b
child 93952 06438370bd3f6f4a53d5d7464c52b280a30877bd
push id9350
push usermbrubeck@mozilla.com
push dateSun, 13 May 2012 15:26:34 +0000
treeherdermozilla-inbound@5fbb80b8e84f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone15.0a1
first release with
nightly linux32
c758cc9b60e5 / 15.0a1 / 20120513030522 / files
nightly linux64
c758cc9b60e5 / 15.0a1 / 20120513030522 / files
nightly mac
c758cc9b60e5 / 15.0a1 / 20120513030522 / files
nightly win32
c758cc9b60e5 / 15.0a1 / 20120513030522 / files
nightly win64
c758cc9b60e5 / 15.0a1 / 20120513030522 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge m-c to fx-team
content/base/src/nsFrameMessageManager.cpp
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -1,11 +1,12 @@
 browser.jar:
 *   content/browser/inspector.html                (highlighter/inspector.html)
     content/browser/NetworkPanel.xhtml            (webconsole/NetworkPanel.xhtml)
+    content/browser/devtools/HUDService-content.js (webconsole/HUDService-content.js)
 *   content/browser/scratchpad.xul                (scratchpad/scratchpad.xul)
     content/browser/scratchpad.js                 (scratchpad/scratchpad.js)
     content/browser/splitview.css                 (shared/splitview.css)
 *   content/browser/styleeditor.xul               (styleeditor/styleeditor.xul)
     content/browser/styleeditor.css               (styleeditor/styleeditor.css)
     content/browser/devtools/csshtmltree.xul      (styleinspector/csshtmltree.xul)
     content/browser/devtools/cssruleview.xul      (styleinspector/cssruleview.xul)
     content/browser/devtools/styleinspector.css   (styleinspector/styleinspector.css)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/HUDService-content.js
@@ -0,0 +1,790 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This code is appended to the browser content script.
+(function _HUDServiceContent() {
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+let tempScope = {};
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", tempScope);
+Cu.import("resource://gre/modules/Services.jsm", tempScope);
+Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", tempScope);
+Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope);
+
+let XPCOMUtils = tempScope.XPCOMUtils;
+let Services = tempScope.Services;
+let gConsoleStorage = tempScope.ConsoleAPIStorage;
+let WebConsoleUtils = tempScope.WebConsoleUtils;
+let l10n = WebConsoleUtils.l10n;
+tempScope = null;
+
+let _alive = true; // Track if this content script should still be alive.
+
+/**
+ * The Web Console content instance manager.
+ */
+let Manager = {
+  get window() content,
+  get console() this.window.console,
+  sandbox: null,
+  hudId: null,
+  _sequence: 0,
+  _messageListeners: ["WebConsole:Init", "WebConsole:EnableFeature",
+                      "WebConsole:DisableFeature", "WebConsole:Destroy"],
+  _messageHandlers: null,
+  _enabledFeatures: null,
+
+  /**
+   * Getter for a unique ID for the current Web Console content instance.
+   */
+  get sequenceId() "HUDContent-" + (++this._sequence),
+
+  /**
+   * Initialize the Web Console manager.
+   */
+  init: function Manager_init()
+  {
+    this._enabledFeatures = [];
+    this._messageHandlers = {};
+
+    this._messageListeners.forEach(function(aName) {
+      addMessageListener(aName, this);
+    }, this);
+
+    // Need to track the owner XUL window to listen to the unload and TabClose
+    // events, to avoid memory leaks.
+    let xulWindow = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIWebNavigation)
+                    .QueryInterface(Ci.nsIDocShell)
+                    .chromeEventHandler.ownerDocument.defaultView;
+
+    xulWindow.addEventListener("unload", this._onXULWindowClose, false);
+
+    let tabContainer = xulWindow.gBrowser.tabContainer;
+    tabContainer.addEventListener("TabClose", this._onTabClose, false);
+
+    // Need to track private browsing change and quit application notifications,
+    // again to avoid memory leaks. The Web Console main process cannot notify
+    // this content script when the XUL window close, tab close, private
+    // browsing change and quit application events happen, so we must call
+    // Manager.destroy() on our own.
+    Services.obs.addObserver(this, "private-browsing-change-granted", false);
+    Services.obs.addObserver(this, "quit-application-granted", false);
+  },
+
+  /**
+   * The message handler. This method forwards all the remote messages to the
+   * appropriate code.
+   */
+  receiveMessage: function Manager_receiveMessage(aMessage)
+  {
+    if (!_alive) {
+      return;
+    }
+
+    if (!aMessage.json || (aMessage.name != "WebConsole:Init" &&
+                           aMessage.json.hudId != this.hudId)) {
+      Cu.reportError("Web Console content script: received message " +
+                     aMessage.name + " from wrong hudId!");
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "WebConsole:Init":
+        this._onInit(aMessage.json);
+        break;
+      case "WebConsole:EnableFeature":
+        this.enableFeature(aMessage.json.feature, aMessage.json);
+        break;
+      case "WebConsole:DisableFeature":
+        this.disableFeature(aMessage.json.feature);
+        break;
+      case "WebConsole:Destroy":
+        this.destroy();
+        break;
+      default: {
+        let handler = this._messageHandlers[aMessage.name];
+        handler && handler(aMessage.json);
+        break;
+      }
+    }
+  },
+
+  /**
+   * Observe notifications from the nsIObserverService.
+   *
+   * @param mixed aSubject
+   * @param string aTopic
+   * @param mixed aData
+   */
+  observe: function Manager_observe(aSubject, aTopic, aData)
+  {
+    if (_alive && (aTopic == "quit-application-granted" ||
+        (aTopic == "private-browsing-change-granted" &&
+         (aData == "enter" || aData == "exit")))) {
+      this.destroy();
+    }
+  },
+
+  /**
+   * The manager initialization code. This method is called when the Web Console
+   * remote process initializes the content process (this code!).
+   *
+   * @param object aMessage
+   *        The object received from the remote process. The WebConsole:Init
+   *        message properties:
+   *        - hudId - (required) the remote Web Console instance ID.
+   *        - features - (optional) array of features you want to enable from
+   *        the start. For each feature you enable you can pass feature-specific
+   *        options in a property on the JSON object you send with the same name
+   *        as the feature. See this.enableFeature() for the list of available
+   *        features.
+   *        - cachedMessages - (optional) an array of cached messages you want
+   *        to receive. See this._sendCachedMessages() for the list of available
+   *        message types.
+   *
+   *        Example message:
+   *        {
+   *          hudId: "foo1",
+   *          features: ["JSTerm", "ConsoleAPI"],
+   *          ConsoleAPI: { ... }, // ConsoleAPI-specific options
+   *          cachedMessages: ["ConsoleAPI"],
+   *        }
+   */
+  _onInit: function Manager_onInit(aMessage)
+  {
+    this.hudId = aMessage.hudId;
+    if (aMessage.features) {
+      aMessage.features.forEach(function(aFeature) {
+        this.enableFeature(aFeature, aMessage[aFeature]);
+      }, this);
+    }
+
+    if (aMessage.cachedMessages) {
+      this._sendCachedMessages(aMessage.cachedMessages);
+    }
+  },
+
+  /**
+   * Add a remote message handler. This is used by other components of the Web
+   * Console content script.
+   *
+   * @param string aName
+   *        Message name to listen for.
+   * @param function aCallback
+   *        Function to execute when the message is received. This function is
+   *        given the JSON object that came from the remote Web Console
+   *        instance.
+   *        Only one callback per message name is allowed!
+   */
+  addMessageHandler: function Manager_addMessageHandler(aName, aCallback)
+  {
+    if (aName in this._messageHandlers) {
+      Cu.reportError("Web Console content script: addMessageHandler() called for an existing message handler: " + aName);
+      return;
+    }
+
+    this._messageHandlers[aName] = aCallback;
+    addMessageListener(aName, this);
+  },
+
+  /**
+   * Remove the message handler for the given name.
+   *
+   * @param string aName
+   *        Message name for the handler you want removed.
+   */
+  removeMessageHandler: function Manager_removeMessageHandler(aName)
+  {
+    if (!(aName in this._messageHandlers)) {
+      return;
+    }
+
+    delete this._messageHandlers[aName];
+    removeMessageListener(aName, this);
+  },
+
+  /**
+   * Send a message to the remote Web Console instance.
+   *
+   * @param string aName
+   *        The name of the message you want to send.
+   * @param object aMessage
+   *        The message object you want to send.
+   */
+  sendMessage: function Manager_sendMessage(aName, aMessage)
+  {
+    aMessage.hudId = this.hudId;
+    if (!("id" in aMessage)) {
+      aMessage.id = this.sequenceId;
+    }
+
+    sendAsyncMessage(aName, aMessage);
+  },
+
+  /**
+   * Enable a feature in the Web Console content script. A feature is generally
+   * a set of observers/listeners that are added in the content process. This
+   * content script exposes the data via the message manager for the features
+   * you enable.
+   *
+   * Supported features:
+   *    - JSTerm - a JavaScript "terminal" which allows code execution.
+   *    - ConsoleAPI - support for routing the window.console API to the remote
+   *    process.
+   *    - PageError - route all the nsIScriptErrors from the nsIConsoleService
+   *    to the remote process.
+   *
+   * @param string aFeature
+   *        One of the supported features: JSTerm, ConsoleAPI.
+   * @param object [aMessage]
+   *        Optional JSON message object coming from the remote Web Console
+   *        instance. This can be used for feature-specific options.
+   */
+  enableFeature: function Manager_enableFeature(aFeature, aMessage)
+  {
+    if (this._enabledFeatures.indexOf(aFeature) != -1) {
+      return;
+    }
+
+    switch (aFeature) {
+      case "JSTerm":
+        JSTerm.init(aMessage);
+        break;
+      case "ConsoleAPI":
+        ConsoleAPIObserver.init(aMessage);
+        break;
+      case "PageError":
+        ConsoleListener.init(aMessage);
+        break;
+      default:
+        Cu.reportError("Web Console content: unknown feature " + aFeature);
+        break;
+    }
+
+    this._enabledFeatures.push(aFeature);
+  },
+
+  /**
+   * Disable a Web Console content script feature.
+   *
+   * @see this.enableFeature
+   * @param string aFeature
+   *        One of the supported features - see this.enableFeature() for the
+   *        list of supported features.
+   */
+  disableFeature: function Manager_disableFeature(aFeature)
+  {
+    let index = this._enabledFeatures.indexOf(aFeature);
+    if (index == -1) {
+      return;
+    }
+    this._enabledFeatures.splice(index, 1);
+
+    switch (aFeature) {
+      case "JSTerm":
+        JSTerm.destroy();
+        break;
+      case "ConsoleAPI":
+        ConsoleAPIObserver.destroy();
+        break;
+      case "PageError":
+        ConsoleListener.destroy();
+        break;
+      default:
+        Cu.reportError("Web Console content: unknown feature " + aFeature);
+        break;
+    }
+  },
+
+  /**
+   * Send the cached messages to the remote Web Console instance.
+   *
+   * @private
+   * @param array aMessageTypes
+   *        An array that lists which kinds of messages you want. Supported
+   *        message types: "ConsoleAPI" and "PageError".
+   */
+  _sendCachedMessages: function Manager__sendCachedMessages(aMessageTypes)
+  {
+    let messages = [];
+
+    while (aMessageTypes.length > 0) {
+      switch (aMessageTypes.shift()) {
+        case "ConsoleAPI":
+          messages.push.apply(messages, ConsoleAPIObserver.getCachedMessages());
+          break;
+        case "PageError":
+          messages.push.apply(messages, ConsoleListener.getCachedMessages());
+          break;
+      }
+    }
+
+    messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });
+
+    this.sendMessage("WebConsole:CachedMessages", {messages: messages});
+  },
+
+  /**
+   * The XUL window "unload" event handler which destroys this content script
+   * instance.
+   * @private
+   */
+  _onXULWindowClose: function Manager__onXULWindowClose()
+  {
+    if (_alive) {
+      Manager.destroy();
+    }
+  },
+
+  /**
+   * The "TabClose" event handler which destroys this content script
+   * instance, if needed.
+   * @private
+   */
+  _onTabClose: function Manager__onTabClose(aEvent)
+  {
+    let tab = aEvent.target;
+    if (_alive && tab.linkedBrowser.contentWindow === Manager.window) {
+      Manager.destroy();
+    }
+  },
+
+  /**
+   * Destroy the Web Console content script instance.
+   */
+  destroy: function Manager_destroy()
+  {
+    Services.obs.removeObserver(this, "private-browsing-change-granted");
+    Services.obs.removeObserver(this, "quit-application-granted");
+
+    _alive = false;
+    let xulWindow = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIWebNavigation)
+                    .QueryInterface(Ci.nsIDocShell)
+                    .chromeEventHandler.ownerDocument.defaultView;
+
+    xulWindow.removeEventListener("unload", this._onXULWindowClose, false);
+    let tabContainer = xulWindow.gBrowser.tabContainer;
+    tabContainer.removeEventListener("TabClose", this._onTabClose, false);
+
+    this._messageListeners.forEach(function(aName) {
+      removeMessageListener(aName, this);
+    }, this);
+
+    this._enabledFeatures.slice().forEach(this.disableFeature, this);
+
+    this.hudId = null;
+    this._messageHandlers = null;
+    Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = null;
+    Cc = Ci = Cu = XPCOMUtils = Services = gConsoleStorage =
+      WebConsoleUtils = l10n = null;
+  },
+};
+
+/**
+ * The JavaScript terminal is meant to allow remote code execution for the Web
+ * Console.
+ */
+let JSTerm = {
+  /**
+   * Evaluation result objects are cached in this object. The chrome process can
+   * request any object based on its ID.
+   */
+  _objectCache: null,
+
+  /**
+   * Initialize the JavaScript terminal feature.
+   */
+  init: function JST_init()
+  {
+    this._objectCache = {};
+
+    Manager.addMessageHandler("JSTerm:GetEvalObject",
+                              this.handleGetEvalObject.bind(this));
+    Manager.addMessageHandler("JSTerm:ClearObjectCache",
+                              this.handleClearObjectCache.bind(this));
+  },
+
+  /**
+   * Handler for the remote "JSTerm:GetEvalObject" message. This allows the
+   * remote Web Console instance to retrieve an object from the content process.
+   *
+   * @param object aRequest
+   *        The message that requests the content object. Properties: cacheId,
+   *        objectId and resultCacheId.
+   *
+   *        Evaluated objects are stored in "buckets" (cache IDs). Each object
+   *        is assigned an ID (object ID). You can request a specific object
+   *        (objectId) from a specific cache (cacheId) and tell where the result
+   *        should be cached (resultCacheId). The requested object can have
+   *        further references to other objects - those references will be
+   *        cached in the "bucket" of your choice (based on resultCacheId). If
+   *        you do not provide any resultCacheId in the request message, then
+   *        cacheId will be used.
+   */
+  handleGetEvalObject: function JST_handleGetEvalObject(aRequest)
+  {
+    if (aRequest.cacheId in this._objectCache &&
+        aRequest.objectId in this._objectCache[aRequest.cacheId]) {
+      let object = this._objectCache[aRequest.cacheId][aRequest.objectId];
+      let resultCacheId = aRequest.resultCacheId || aRequest.cacheId;
+      let message = {
+        id: aRequest.id,
+        cacheId: aRequest.cacheId,
+        objectId: aRequest.objectId,
+        object: this.prepareObjectForRemote(object, resultCacheId),
+        childrenCacheId: resultCacheId,
+      };
+      Manager.sendMessage("JSTerm:EvalObject", message);
+    }
+    else {
+      Cu.reportError("JSTerm:GetEvalObject request " + aRequest.id +
+                     ": stale object.");
+    }
+  },
+
+  /**
+   * Handler for the remote "JSTerm:ClearObjectCache" message. This allows the
+   * remote Web Console instance to clear the cache of objects that it no longer
+   * uses.
+   *
+   * @param object aRequest
+   *        An object that holds one property: the cacheId you want cleared.
+   */
+  handleClearObjectCache: function JST_handleClearObjectCache(aRequest)
+  {
+    if (aRequest.cacheId in this._objectCache) {
+      delete this._objectCache[aRequest.cacheId];
+    }
+  },
+
+  /**
+   * Prepare an object to be sent to the remote Web Console instance.
+   *
+   * @param object aObject
+   *        The object you want to send to the remote Web Console instance.
+   * @param number aCacheId
+   *        Cache ID where you want object references to be stored into. The
+   *        given object may include references to other objects - those
+   *        references will be stored in the given cache ID so the remote
+   *        process can later retrieve them as well.
+   * @return array
+   *         An array that holds one element for each enumerable property and
+   *         method in aObject. Each element describes the property. For details
+   *         see WebConsoleUtils.namesAndValuesOf().
+   */
+  prepareObjectForRemote:
+  function JST_prepareObjectForRemote(aObject, aCacheId)
+  {
+    // Cache the properties that have inspectable values.
+    let propCache = this._objectCache[aCacheId] || {};
+    let result = WebConsoleUtils.namesAndValuesOf(aObject, propCache);
+    if (!(aCacheId in this._objectCache) && Object.keys(propCache).length > 0) {
+      this._objectCache[aCacheId] = propCache;
+    }
+
+    return result;
+  },
+
+  /**
+   * Destroy the JSTerm instance.
+   */
+  destroy: function JST_destroy()
+  {
+    Manager.removeMessageHandler("JSTerm:GetEvalObject");
+    Manager.removeMessageHandler("JSTerm:ClearObjectCache");
+
+    delete this._objectCache;
+  },
+};
+
+/**
+ * The window.console API observer. This allows the window.console API messages
+ * to be sent to the remote Web Console instance.
+ */
+let ConsoleAPIObserver = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+  /**
+   * Initialize the window.console API observer.
+   */
+  init: function CAO_init()
+  {
+    // Note that the observer is process-wide. We will filter the messages as
+    // needed, see CAO_observe().
+    Services.obs.addObserver(this, "console-api-log-event", false);
+
+    Manager.addMessageHandler("ConsoleAPI:ClearCache",
+                              this.handleClearCache.bind(this));
+  },
+
+  /**
+   * The console API message observer. When messages are received from the
+   * observer service we forward them to the remote Web Console instance.
+   *
+   * @param object aMessage
+   *        The message object receives from the observer service.
+   * @param string aTopic
+   *        The message topic received from the observer service.
+   */
+  observe: function CAO_observe(aMessage, aTopic)
+  {
+    if (!_alive || !aMessage || aTopic != "console-api-log-event") {
+      return;
+    }
+
+    let apiMessage = aMessage.wrappedJSObject;
+
+    let msgWindow =
+      WebConsoleUtils.getWindowByOuterId(apiMessage.ID, Manager.window);
+    if (!msgWindow || msgWindow.top != Manager.window) {
+      // Not the same window!
+      return;
+    }
+
+    let messageToChrome = {};
+    this._prepareApiMessageForRemote(apiMessage, messageToChrome);
+    Manager.sendMessage("WebConsole:ConsoleAPI", messageToChrome);
+  },
+
+  /**
+   * Prepare a message from the console APi to be sent to the remote Web Console
+   * instance.
+   *
+   * @param object aOriginalMessage
+   *        The original message received from console-api-log-event.
+   * @param object aRemoteMessage
+   *        The object you want to send to the remote Web Console. This object
+   *        is updated to hold information from the original message. New
+   *        properties added:
+   *        - timeStamp
+   *        Message timestamp (same as the aOriginalMessage.timeStamp property).
+   *        - apiMessage
+   *        An object that copies almost all the properties from
+   *        aOriginalMessage. Arguments might be skipped if it holds references
+   *        to objects that cannot be sent as they are to the remote Web Console
+   *        instance.
+   *        - argumentsToString
+   *        Optional: the aOriginalMessage.arguments object stringified.
+   *
+   *        The apiMessage.arguments property is set to hold data appropriate
+   *        to the message level. A similar approach is used for
+   *        argumentsToString.
+   */
+  _prepareApiMessageForRemote:
+  function CAO__prepareApiMessageForRemote(aOriginalMessage, aRemoteMessage)
+  {
+    aRemoteMessage.apiMessage =
+      WebConsoleUtils.cloneObject(aOriginalMessage, true,
+        function(aKey, aValue, aObject) {
+          // We need to skip the arguments property from the original object.
+          if (aKey == "wrappedJSObject" || aObject === aOriginalMessage &&
+              aKey == "arguments") {
+            return false;
+          }
+          return true;
+        });
+
+    aRemoteMessage.timeStamp = aOriginalMessage.timeStamp;
+
+    switch (aOriginalMessage.level) {
+      case "trace":
+      case "time":
+      case "timeEnd":
+      case "group":
+      case "groupCollapsed":
+        aRemoteMessage.apiMessage.arguments =
+          WebConsoleUtils.cloneObject(aOriginalMessage.arguments, true);
+        break;
+
+      case "log":
+      case "info":
+      case "warn":
+      case "error":
+      case "debug":
+      case "groupEnd":
+        aRemoteMessage.argumentsToString =
+          Array.map(aOriginalMessage.arguments || [],
+                    this._formatObject.bind(this));
+        break;
+
+      case "dir": {
+        aRemoteMessage.objectsCacheId = Manager.sequenceId;
+        aRemoteMessage.argumentsToString = [];
+        let mapFunction = function(aItem) {
+          aRemoteMessage.argumentsToString.push(this._formatObject(aItem));
+          if (WebConsoleUtils.isObjectInspectable(aItem)) {
+            return JSTerm.prepareObjectForRemote(aItem,
+                                                 aRemoteMessage.objectsCacheId);
+          }
+          return aItem;
+        }.bind(this);
+
+        aRemoteMessage.apiMessage.arguments =
+          Array.map(aOriginalMessage.arguments || [], mapFunction);
+        break;
+      }
+      default:
+        Cu.reportError("Unknown Console API log level: " +
+                       aOriginalMessage.level);
+        break;
+    }
+  },
+
+  /**
+   * Format an object's value to be displayed in the Web Console.
+   *
+   * @private
+   * @param object aObject
+   *        The object you want to display.
+   * @return string
+   *         The string you can display for the given object.
+   */
+  _formatObject: function CAO__formatObject(aObject)
+  {
+    return typeof aObject == "string" ?
+           aObject : WebConsoleUtils.formatResult(aObject);
+  },
+
+  /**
+   * Get the cached messages for the current inner window.
+   *
+   * @see this._prepareApiMessageForRemote()
+   * @return array
+   *         The array of cached messages. Each element is a Console API
+   *         prepared to be sent to the remote Web Console instance.
+   */
+  getCachedMessages: function CAO_getCachedMessages()
+  {
+    let innerWindowId = WebConsoleUtils.getInnerWindowId(Manager.window);
+    let messages = gConsoleStorage.getEvents(innerWindowId);
+
+    let result = messages.map(function(aMessage) {
+      let remoteMessage = { _type: "ConsoleAPI" };
+      this._prepareApiMessageForRemote(aMessage.wrappedJSObject, remoteMessage);
+      return remoteMessage;
+    }, this);
+
+    return result;
+  },
+
+  /**
+   * Handler for the "ConsoleAPI:ClearCache" message.
+   */
+  handleClearCache: function CAO_handleClearCache()
+  {
+    let windowId = WebConsoleUtils.getInnerWindowId(Manager.window);
+    gConsoleStorage.clearEvents(windowId);
+  },
+
+  /**
+   * Destroy the ConsoleAPIObserver listeners.
+   */
+  destroy: function CAO_destroy()
+  {
+    Manager.removeMessageHandler("ConsoleAPI:ClearCache");
+    Services.obs.removeObserver(this, "console-api-log-event");
+  },
+};
+
+/**
+ * The nsIConsoleService listener. This is used to send all the page errors
+ * (JavaScript, CSS and more) to the remote Web Console instance.
+ */
+let ConsoleListener = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
+
+  /**
+   * Initialize the nsIConsoleService listener.
+   */
+  init: function CL_init()
+  {
+    Services.console.registerListener(this);
+  },
+
+  /**
+   * The nsIConsoleService observer. This method takes all the script error
+   * messages belonging to the current window and sends them to the remote Web
+   * Console instance.
+   *
+   * @param nsIScriptError aScriptError
+   *        The script error object coming from the nsIConsoleService.
+   */
+  observe: function CL_observe(aScriptError)
+  {
+    if (!_alive || !(aScriptError instanceof Ci.nsIScriptError) ||
+        !aScriptError.outerWindowID) {
+      return;
+    }
+
+    switch (aScriptError.category) {
+      // We ignore chrome-originating errors as we only care about content.
+      case "XPConnect JavaScript":
+      case "component javascript":
+      case "chrome javascript":
+      case "chrome registration":
+      case "XBL":
+      case "XBL Prototype Handler":
+      case "XBL Content Sink":
+      case "xbl javascript":
+        return;
+    }
+
+    let errorWindow =
+      WebConsoleUtils.getWindowByOuterId(aScriptError.outerWindowID,
+                                         Manager.window);
+    if (!errorWindow || errorWindow.top != Manager.window) {
+      return;
+    }
+
+    Manager.sendMessage("WebConsole:PageError", { pageError: aScriptError });
+  },
+
+  /**
+   * Get the cached page errors for the current inner window.
+   *
+   * @return array
+   *         The array of cached messages. Each element is an nsIScriptError
+   *         with an added _type property so the remote Web Console instance can
+   *         tell the difference between various types of cached messages.
+   */
+  getCachedMessages: function CL_getCachedMessages()
+  {
+    let innerWindowId = WebConsoleUtils.getInnerWindowId(Manager.window);
+    let result = [];
+    let errors = {};
+    Services.console.getMessageArray(errors, {});
+
+    (errors.value || []).forEach(function(aError) {
+      if (!(aError instanceof Ci.nsIScriptError) ||
+          aError.innerWindowID != innerWindowId) {
+        return;
+      }
+
+      let remoteMessage = WebConsoleUtils.cloneObject(aError);
+      remoteMessage._type = "PageError";
+      result.push(remoteMessage);
+    });
+
+    return result;
+  },
+
+  /**
+   * Remove the nsIConsoleService listener.
+   */
+  destroy: function CL_destroy()
+  {
+    Services.console.unregisterListener(this);
+  },
+};
+
+Manager.init();
+})();
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -49,17 +49,16 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const CONSOLEAPI_CLASS_ID = "{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/NetworkHelper.jsm");
-Cu.import("resource:///modules/PropertyPanel.jsm");
 
 var EXPORTED_SYMBOLS = ["HUDService", "ConsoleUtils"];
 
 XPCOMUtils.defineLazyServiceGetter(this, "scriptError",
                                    "@mozilla.org/scripterror;1",
                                    "nsIScriptError");
 
 XPCOMUtils.defineLazyServiceGetter(this, "activityDistributor",
@@ -103,16 +102,28 @@ XPCOMUtils.defineLazyGetter(this, "Prope
   try {
     Cu.import("resource:///modules/PropertyPanel.jsm", obj);
   } catch (err) {
     Cu.reportError(err);
   }
   return obj.PropertyPanel;
 });
 
+XPCOMUtils.defineLazyGetter(this, "PropertyPanelAsync", function () {
+  let obj = {};
+  Cu.import("resource:///modules/PropertyPanelAsync.jsm", obj);
+  return obj.PropertyPanel;
+});
+
+XPCOMUtils.defineLazyGetter(this, "PropertyTreeViewAsync", function () {
+  let obj = {};
+  Cu.import("resource:///modules/PropertyPanelAsync.jsm", obj);
+  return obj.PropertyTreeView;
+});
+
 XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function () {
   var obj = {};
   try {
     Cu.import("resource:///modules/AutocompletePopup.jsm", obj);
   }
   catch (err) {
     Cu.reportError(err);
   }
@@ -131,20 +142,24 @@ XPCOMUtils.defineLazyGetter(this, "Scrat
 });
 
 XPCOMUtils.defineLazyGetter(this, "namesAndValuesOf", function () {
   var obj = {};
   Cu.import("resource:///modules/PropertyPanel.jsm", obj);
   return obj.namesAndValuesOf;
 });
 
-XPCOMUtils.defineLazyGetter(this, "gConsoleStorage", function () {
+XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () {
   let obj = {};
-  Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", obj);
-  return obj.ConsoleAPIStorage;
+  Cu.import("resource:///modules/WebConsoleUtils.jsm", obj);
+  return obj.WebConsoleUtils;
+});
+
+XPCOMUtils.defineLazyGetter(this, "l10n", function() {
+  return WebConsoleUtils.l10n;
 });
 
 function LogFactory(aMessagePrefix)
 {
   function log(aMessage) {
     var _msg = aMessagePrefix + " " + aMessage + "\n";
     dump(_msg);
   }
@@ -276,16 +291,19 @@ const MINIMUM_CONSOLE_HEIGHT = 150;
 const MINIMUM_PAGE_HEIGHT = 50;
 
 // The default console height, as a ratio from the content window inner height.
 const DEFAULT_CONSOLE_HEIGHT = 0.33;
 
 // Constant used when checking the typeof objects.
 const TYPEOF_FUNCTION = "function";
 
+// This script is inserted into the content process.
+const CONTENT_SCRIPT_URL = "chrome://browser/content/devtools/HUDService-content.js";
+
 const ERRORS = { LOG_MESSAGE_MISSING_ARGS:
                  "Missing arguments: aMessage, aConsoleNode and aMessageNode are required.",
                  CANNOT_GET_HUD: "Cannot getHeads Up Display with provided ID",
                  MISSING_ARGS: "Missing arguments",
                  LOG_OUTPUT_FAILED: "Log Failure: Could not append messageNode to outputNode",
 };
 
 // The indent of a console group in pixels.
@@ -607,31 +625,16 @@ function createElement(aDocument, aTag, 
  */
 function createAndAppendElement(aParent, aTag, aAttributes)
 {
   let node = createElement(aParent.ownerDocument, aTag, aAttributes);
   aParent.appendChild(node);
   return node;
 }
 
-/**
- * Convenience function to unwrap a wrapped object.
- *
- * @param aObject the object to unwrap
- */
-
-function unwrap(aObject)
-{
-  try {
-    return XPCNativeWrapper.unwrap(aObject);
-  } catch(e) {
-    return aObject;
-  }
-}
-
 ///////////////////////////////////////////////////////////////////////////
 //// NetworkPanel
 
 /**
  * Creates a new NetworkPanel.
  *
  * @param nsIDOMNode aParent
  *        Parent node to append the created panel to.
@@ -640,17 +643,17 @@ function unwrap(aObject)
  */
 function NetworkPanel(aParent, aHttpActivity)
 {
   let doc = aParent.ownerDocument;
   this.httpActivity = aHttpActivity;
 
   // Create the underlaying panel
   this.panel = createElement(doc, "panel", {
-    label: HUDService.getStr("NetworkPanel.label"),
+    label: l10n.getStr("NetworkPanel.label"),
     titlebar: "normal",
     noautofocus: "true",
     noautohide: "true",
     close: "true"
   });
 
   // Create the iframe that displays the NetworkPanel XHTML.
   this.iframe = createAndAppendElement(this.panel, "iframe", {
@@ -726,17 +729,17 @@ NetworkPanel.prototype =
    *        "NetworkPanel." before calling the HUDService.getFormatStr function.
    * @param array aArray
    *        Values used as placeholder for the i10n string.
    * @returns string
    *          The i10n formated string.
    */
   _format: function NP_format(aName, aArray)
   {
-    return HUDService.getFormatStr("NetworkPanel." + aName, aArray);
+    return l10n.getFormatStr("NetworkPanel." + aName, aArray);
   },
 
   /**
    * Returns the content type of the response body. This is based on the
    * response.header["Content-Type"] info. If this value is not available, then
    * the content type is tried to be estimated by the url file ending.
    *
    * @returns string or null
@@ -926,17 +929,17 @@ NetworkPanel.prototype =
   {
     let timing = this.httpActivity.timing;
     let request = this.httpActivity.request;
 
     this._appendTextNode("headUrl", this.httpActivity.url);
     this._appendTextNode("headMethod", this.httpActivity.method);
 
     this._appendTextNode("requestHeadersInfo",
-      ConsoleUtils.timestampString(timing.REQUEST_HEADER/1000));
+      l10n.timestampString(timing.REQUEST_HEADER/1000));
 
     this._appendList("requestHeadersContent", request.header, true);
 
     if ("Cookie" in request.header) {
       this._displayNode("requestCookie");
 
       let cookies = request.header.Cookie.split(";");
       let cookieList = {};
@@ -1271,29 +1274,35 @@ function pruneConsoleOutputIfNecessary(a
   let oldScrollHeight = scrollBox.scrollHeight;
   let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);
 
   // Prune the nodes.
   let messageNodes = outputNode.querySelectorAll(".webconsole-msg-" +
       CATEGORY_CLASS_FRAGMENTS[aCategory]);
   let removeNodes = messageNodes.length - logLimit;
   for (let i = 0; i < removeNodes; i++) {
-    if (messageNodes[i].classList.contains("webconsole-msg-cssparser")) {
+    let node = messageNodes[i];
+    if (node._evalCacheId && !node._panelOpen) {
+      hudRef.jsterm.clearObjectCache(node._evalCacheId);
+    }
+
+    if (node.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");
+      if (node.childNodes[4]) {
+        location = node.childNodes[4].getAttribute("title");
       }
       delete hudRef.cssNodes[desc + location];
     }
-    else if (messageNodes[i].classList.contains("webconsole-msg-inspector")) {
-      hudRef.pruneConsoleDirNode(messageNodes[i]);
+    else if (node.classList.contains("webconsole-msg-inspector")) {
+      hudRef.pruneConsoleDirNode(node);
       continue;
     }
-    messageNodes[i].parentNode.removeChild(messageNodes[i]);
+
+    node.parentNode.removeChild(node);
   }
 
   if (!scrolledToBottom && removeNodes > 0 &&
       oldScrollHeight != scrollBox.scrollHeight) {
     scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
   }
 
   return logLimit;
@@ -1357,107 +1366,31 @@ function HUD_SERVICE()
    * Response headers for requests that haven't finished yet.
    */
   this.openResponseHeaders = {};
 };
 
 HUD_SERVICE.prototype =
 {
   /**
-   * Last value entered
-   */
-  lastInputValue: "",
-
-  /**
-   * L10N shortcut function
-   *
-   * @param string aName
-   * @returns string
-   */
-  getStr: function HS_getStr(aName)
-  {
-    return stringBundle.GetStringFromName(aName);
-  },
-
-  /**
-   * L10N shortcut function
-   *
-   * @param string aName
-   * @returns (format) string
-   */
-  getFormatStr: function HS_getFormatStr(aName, aArray)
-  {
-    return stringBundle.formatStringFromName(aName, aArray, aArray.length);
-  },
-
-  /**
    * getter for UI commands to be used by the frontend
    *
    * @returns object
    */
   get consoleUI() {
     return HeadsUpDisplayUICommands;
   },
 
   /**
    * The sequencer is a generator (after initialization) that returns unique
    * integers
    */
   sequencer: null,
 
   /**
-   * Gets the ID of the outer window of this DOM window
-   *
-   * @param nsIDOMWindow aWindow
-   * @returns integer
-   */
-  getWindowId: function HS_getWindowId(aWindow)
-  {
-    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
-  },
-
-  /**
-   * Gets the ID of the inner window of this DOM window
-   *
-   * @param nsIDOMWindow aWindow
-   * @returns integer
-   */
-  getInnerWindowId: function HS_getInnerWindowId(aWindow)
-  {
-    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
-           getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
-  },
-
-  /**
-   * Gets the top level content window that has an outer window with
-   * the given ID or returns null if no such content window exists
-   *
-   * @param integer aId
-   * @returns nsIDOMWindow
-   */
-  getWindowByWindowId: function HS_getWindowByWindowId(aId)
-  {
-    // In the future (post-Electrolysis), getOuterWindowWithId() could
-    // return null, because the originating window could have gone away
-    // while we were in the process of receiving and/or processing a
-    // message. For future-proofing purposes, we do a null check here.
-
-    let someWindow = Services.wm.getMostRecentWindow(null);
-    let content = null;
-
-    if (someWindow) {
-      let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIDOMWindowUtils);
-      content = windowUtils.getOuterWindowWithId(aId);
-    }
-
-    return content;
-  },
-
-  /**
    * Whether to save the bodies of network requests and responses. Disabled by
    * default to save memory.
    */
   saveRequestAndResponseBodies: false,
 
   /**
    * Tell the HUDService that a HeadsUpDisplay can be activated
    * for the window or context that has 'aContextDOMId' node id
@@ -1565,18 +1498,19 @@ HUD_SERVICE.prototype =
   deactivateHUDForContext: function HS_deactivateHUDForContext(aContext, aAnimated)
   {
     let browser = aContext.linkedBrowser;
     let window = browser.contentWindow;
     let chromeDocument = aContext.ownerDocument;
     let nBox = chromeDocument.defaultView.getNotificationBox(window);
     let hudId = "hud_" + nBox.id;
     let displayNode = chromeDocument.getElementById(hudId);
-
-    if (hudId in this.hudReferences && displayNode) {
+    let hudFound = (hudId in this.hudReferences) && displayNode;
+
+    if (hudFound) {
       if (!aAnimated) {
         this.storeHeight(hudId);
       }
 
       let hud = this.hudReferences[hudId];
       browser.webProgress.removeProgressListener(hud.progressListener);
       delete hud.progressListener;
 
@@ -1594,16 +1528,21 @@ HUD_SERVICE.prototype =
       procInstr.contexts = procInstr.contexts.filter(function(id) {
         return id !== hudId;
       });
       if (procInstr.contexts.length == 0 && procInstr.parentNode) {
         procInstr.parentNode.removeChild(procInstr);
         delete aContext.ownerDocument.gcliCssProcInstr;
       }
     }
+
+    if (hudFound) {
+      let id = WebConsoleUtils.supportsString(hudId);
+      Services.obs.notifyObservers(id, "web-console-destroyed", null);
+    }
   },
 
   /**
    * get a unique ID from the sequence generator
    *
    * @returns integer
    */
   sequenceId: function HS_sequencerId()
@@ -1798,19 +1737,16 @@ HUD_SERVICE.prototype =
 
   /**
    * Register a reference of each HeadsUpDisplay that is created
    */
   registerHUDReference:
   function HS_registerHUDReference(aHUD)
   {
     this.hudReferences[aHUD.hudId] = aHUD;
-
-    let id = ConsoleUtils.supString(aHUD.hudId);
-    Services.obs.notifyObservers(id, "web-console-created", null);
   },
 
   /**
    * Register a new Heads Up Display
    *
    * @returns void
    */
   registerDisplay: function HS_registerDisplay(aHUDId)
@@ -1889,19 +1825,16 @@ HUD_SERVICE.prototype =
     this.unregisterActiveContext(aHUDId);
 
     let popupset = hud.chromeDocument.getElementById("mainPopupSet");
     let panels = popupset.querySelectorAll("panel[hudId=" + aHUDId + "]");
     for (let i = 0; i < panels.length; i++) {
       panels[i].hidePopup();
     }
 
-    let id = ConsoleUtils.supString(aHUDId);
-    Services.obs.notifyObservers(id, "web-console-destroyed", null);
-
     if (Object.keys(this.hudReferences).length == 0) {
       let autocompletePopup = hud.chromeDocument.
                               getElementById("webConsole_autocompletePopup");
       if (autocompletePopup) {
         autocompletePopup.parentNode.removeChild(autocompletePopup);
       }
 
       this.suspend();
@@ -1919,18 +1852,16 @@ HUD_SERVICE.prototype =
     if (Object.keys(this.hudReferences).length > 0) {
       return;
     }
 
     // begin observing HTTP traffic
     this.startHTTPObservation();
 
     HUDWindowObserver.init();
-    HUDConsoleObserver.init();
-    ConsoleAPIObserver.init();
   },
 
   /**
    * Suspend Web Console activity. This is called when all Web Consoles are
    * closed.
    *
    * @returns void
    */
@@ -1945,18 +1876,16 @@ HUD_SERVICE.prototype =
     this.openRequests = {};
     this.openResponseHeaders = {};
 
     delete this.defaultFilterPrefs;
 
     delete this.lastFinishedRequestCallback;
 
     HUDWindowObserver.uninit();
-    HUDConsoleObserver.uninit();
-    ConsoleAPIObserver.shutdown();
   },
 
   /**
    * Shutdown all HeadsUpDisplays on xpcom-shutdown
    *
    * @returns void
    */
   shutdown: function HS_shutdown()
@@ -1982,17 +1911,17 @@ HUD_SERVICE.prototype =
    * Returns the hudId that is corresponding to the hud activated for the
    * passed aContentWindow. If there is no matching hudId null is returned.
    *
    * @param nsIDOMWindow aContentWindow
    * @returns string or null
    */
   getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow)
   {
-    let windowId = this.getWindowId(aContentWindow);
+    let windowId = WebConsoleUtils.getOuterWindowId(aContentWindow);
     return this.getHudIdByWindowId(windowId);
   },
 
   /**
    * Returns the hudReference for a given id.
    *
    * @param string aId
    * @returns Object
@@ -2033,188 +1962,16 @@ HUD_SERVICE.prototype =
    */
   updateFilterText: function HS_updateFiltertext(aTextBoxNode)
   {
     var hudId = aTextBoxNode.getAttribute("hudId");
     this.adjustVisibilityOnSearchStringChange(hudId, aTextBoxNode.value);
   },
 
   /**
-   * Logs a message to the Web Console that originates from the window.console
-   * service ("console-api-log-event" notifications).
-   *
-   * @param string aHUDId
-   *        The ID of the Web Console to which to send the message.
-   * @param object aMessage
-   *        The message reported by the console service.
-   * @return void
-   */
-  logConsoleAPIMessage: function HS_logConsoleAPIMessage(aHUDId, aMessage)
-  {
-    // Pipe the message to createMessageNode().
-    let hud = HUDService.hudReferences[aHUDId];
-    function formatResult(x) {
-      if (typeof(x) == "string") {
-        return x;
-      }
-      if (hud.gcliterm) {
-        return hud.gcliterm.formatResult(x);
-      }
-      if (hud.jsterm) {
-        return hud.jsterm.formatResult(x);
-      }
-      return x;
-    }
-
-    let body = null;
-    let clipboardText = null;
-    let sourceURL = null;
-    let sourceLine = 0;
-    let level = aMessage.level;
-    let args = aMessage.arguments;
-
-    switch (level) {
-      case "log":
-      case "info":
-      case "warn":
-      case "error":
-      case "debug":
-        let mappedArguments = Array.map(args, formatResult);
-        body = Array.join(mappedArguments, " ");
-        sourceURL = aMessage.filename;
-        sourceLine = aMessage.lineNumber;
-        break;
-
-      case "trace":
-        let filename = ConsoleUtils.abbreviateSourceURL(args[0].filename);
-        let functionName = args[0].functionName ||
-                           this.getStr("stacktrace.anonymousFunction");
-        let lineNumber = args[0].lineNumber;
-
-        body = this.getFormatStr("stacktrace.outputMessage",
-                                 [filename, functionName, lineNumber]);
-
-        sourceURL = args[0].filename;
-        sourceLine = args[0].lineNumber;
-
-        clipboardText = "";
-
-        args.forEach(function(aFrame) {
-          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;
-
-      case "group":
-      case "groupCollapsed":
-        clipboardText = body = formatResult(args);
-        sourceURL = aMessage.filename;
-        sourceLine = aMessage.lineNumber;
-        hud.groupDepth++;
-        break;
-
-      case "groupEnd":
-        if (hud.groupDepth > 0) {
-          hud.groupDepth--;
-        }
-        return;
-
-      case "time":
-        if (!args) {
-          return;
-        }
-        if (args.error) {
-          Cu.reportError(this.getStr(args.error));
-          return;
-        }
-        body = this.getFormatStr("timerStarted", [args.name]);
-        clipboardText = body;
-        sourceURL = aMessage.filename;
-        sourceLine = aMessage.lineNumber;
-        break;
-
-      case "timeEnd":
-        if (!args) {
-          return;
-        }
-        body = this.getFormatStr("timeEnd", [args.name, args.duration]);
-        clipboardText = body;
-        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,
-                                              aHUDId,
-                                              sourceURL,
-                                              sourceLine,
-                                              clipboardText,
-                                              level,
-                                              aMessage.timeStamp);
-
-    // 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");
-      linkNode.setAttribute("aria-haspopup", "true");
-
-      node.addEventListener("mousedown", function(aEvent) {
-        this._startX = aEvent.clientX;
-        this._startY = aEvent.clientY;
-      }, false);
-
-      node.addEventListener("click", function(aEvent) {
-        if (aEvent.detail != 1 || aEvent.button != 0 ||
-            (this._startX != aEvent.clientX &&
-             this._startY != aEvent.clientY)) {
-          return;
-        }
-
-        if (!this._panelOpen) {
-          let propPanel = hud.jsterm.openPropertyPanel(null,
-                                                       node._stacktrace,
-                                                       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.
    * @return void
    */
   logWarningAboutReplacedAPI:
@@ -2225,83 +1982,16 @@ HUD_SERVICE.prototype =
     let message = stringBundle.GetStringFromName("ConsoleAPIDisabled");
     let node = ConsoleUtils.createMessageNode(chromeDocument, CATEGORY_JS,
                                               SEVERITY_WARNING, message,
                                               aHUDId);
     ConsoleUtils.outputMessageNode(node, aHUDId);
   },
 
   /**
-   * Reports an error in the page source, either JavaScript or CSS.
-   *
-   * @param nsIScriptError aScriptError
-   *        The error message to report.
-   * @return void
-   */
-  reportPageError: function HS_reportPageError(aScriptError)
-  {
-    if (!aScriptError.outerWindowID) {
-      return;
-    }
-
-    let category;
-
-    switch (aScriptError.category) {
-      // We ignore chrome-originating errors as we only care about content.
-      case "XPConnect JavaScript":
-      case "component javascript":
-      case "chrome javascript":
-      case "chrome registration":
-      case "XBL":
-      case "XBL Prototype Handler":
-      case "XBL Content Sink":
-      case "xbl javascript":
-        return;
-
-      case "CSS Parser":
-      case "CSS Loader":
-        category = CATEGORY_CSS;
-        break;
-
-      default:
-        category = CATEGORY_JS;
-        break;
-    }
-
-    // Warnings and legacy strict errors become warnings; other types become
-    // errors.
-    let severity = SEVERITY_ERROR;
-    if ((aScriptError.flags & aScriptError.warningFlag) ||
-        (aScriptError.flags & aScriptError.strictFlag)) {
-      severity = SEVERITY_WARNING;
-    }
-
-    let window = HUDService.getWindowByWindowId(aScriptError.outerWindowID);
-    if (window) {
-      let hudId = HUDService.getHudIdByWindow(window.top);
-      if (hudId) {
-        let outputNode = this.hudReferences[hudId].outputNode;
-        let chromeDocument = outputNode.ownerDocument;
-
-        let node = ConsoleUtils.createMessageNode(chromeDocument,
-                                                  category,
-                                                  severity,
-                                                  aScriptError.errorMessage,
-                                                  hudId,
-                                                  aScriptError.sourceName,
-                                                  aScriptError.lineNumber,
-                                                  null, null,
-                                                  aScriptError.timeStamp);
-
-        ConsoleUtils.outputMessageNode(node, hudId);
-      }
-    }
-  },
-
-  /**
    * Register a Gecko app's specialized ApplicationHooks object
    *
    * @returns void or throws "UNSUPPORTED APPLICATION" error
    */
   registerApplicationHooks:
   function HS_registerApplications(aAppName, aHooksObject)
   {
     switch(aAppName) {
@@ -2590,17 +2280,17 @@ HUD_SERVICE.prototype =
                                 timing.REQUEST_HEADER) / 1000);
 
                 // Add the request duration.
                 let linkNode = msgObject.linkNode;
                 let statusNode = linkNode.
                   querySelector(".webconsole-msg-status");
 
                 let statusText = httpActivity.response.status;
-                let timeText = self.getFormatStr("NetworkPanel.durationMS",
+                let timeText = l10n.getFormatStr("NetworkPanel.durationMS",
                                                  [ requestDuration ]);
                 let fullStatusText = "[" + statusText + " " + timeText + "]";
                 statusNode.setAttribute("value", fullStatusText);
 
                 let clipboardTextPieces =
                   [ httpActivity.method, httpActivity.url, fullStatusText ];
                 msgObject.messageNode.clipboardText =
                   clipboardTextPieces.join(" ");
@@ -2873,17 +2563,17 @@ HUD_SERVICE.prototype =
    */
   windowInitializer: function HS_WindowInitalizer(aContentWindow)
   {
     var xulWindow = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIWebNavigation)
                       .QueryInterface(Ci.nsIDocShell)
                       .chromeEventHandler.ownerDocument.defaultView;
 
-    let xulWindow = unwrap(xulWindow);
+    let xulWindow = WebConsoleUtils.unwrap(xulWindow);
 
     let docElem = xulWindow.document.documentElement;
     if (!docElem || docElem.getAttribute("windowtype") != "navigator:browser" ||
         !xulWindow.gBrowser) {
       // Do not do anything unless we have a browser window.
       // This may be a view-source window or other type of non-browser window.
       return;
     }
@@ -2938,36 +2628,34 @@ HUD_SERVICE.prototype =
       // get nBox object and call new HUD
       let config = { parentNode: nBox,
                      contentWindow: aContentWindow.top
                    };
 
       hud = new HeadsUpDisplay(config);
 
       HUDService.registerHUDReference(hud);
-      let windowId = this.getWindowId(aContentWindow.top);
+      let windowId = WebConsoleUtils.getOuterWindowId(aContentWindow.top);
       this.windowIds[windowId] = hudId;
 
       hud.progressListener = new ConsoleProgressListener(hudId);
 
       _browser.webProgress.addProgressListener(hud.progressListener,
         Ci.nsIWebProgress.NOTIFY_STATE_ALL);
-
-      hud.displayCachedConsoleMessages();
     }
     else {
       hud = this.hudReferences[hudId];
       if (aContentWindow == aContentWindow.top) {
         // TODO: name change?? doesn't actually re-attach the console
         hud.reattachConsole(aContentWindow);
       }
     }
 
     // Need to detect that the console component has been paved over.
-    let consoleObject = unwrap(aContentWindow).console;
+    let consoleObject = WebConsoleUtils.unwrap(aContentWindow).console;
     if (!("__mozillaConsole__" in consoleObject))
       this.logWarningAboutReplacedAPI(hudId);
 
     // register the controller to handle "select all" properly
     this.createController(xulWindow);
   },
 
   /**
@@ -3119,17 +2807,17 @@ HUD_SERVICE.prototype =
       // copied output.
       if (i > 0 && item.classList.contains("webconsole-new-group")) {
         newGroup = true;
       }
 
       // Ensure the selected item hasn't been filtered by type or string.
       if (!item.classList.contains("hud-filtered-by-type") &&
           !item.classList.contains("hud-filtered-by-string")) {
-        let timestampString = ConsoleUtils.timestampString(item.timestamp);
+        let timestampString = l10n.timestampString(item.timestamp);
         if (newGroup) {
           strings.push("--");
           newGroup = false;
         }
         strings.push("[" + timestampString + "] " + item.clipboardText);
       }
     }
     clipboardHelper.copyString(strings.join("\n"));
@@ -3229,16 +2917,22 @@ function HeadsUpDisplay(aConfig)
     this.parentNode = parentNode;
     this.notificationBox = parentNode;
   }
 
   // create textNode Factory:
   this.textFactory = NodeFactory("text", "xul", this.chromeDocument);
 
   this.chromeWindow = this.chromeDocument.defaultView;
+  this.browser = this.tab.linkedBrowser;
+  this.messageManager = this.browser.messageManager;
+
+  // Track callback functions registered for specific async requests sent to the
+  // content process.
+  this.asyncRequests = {};
 
   // create a panel dynamically and attach to the parentNode
   this.createHUD();
 
   this.HUDBox.lastTimestamp = 0;
   // create the JSTerm input element
   try {
     this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
@@ -3250,19 +2944,30 @@ function HeadsUpDisplay(aConfig)
     }
   }
   catch (ex) {
     Cu.reportError(ex);
   }
 
   // A cache for tracking repeated CSS Nodes.
   this.cssNodes = {};
+
+  this._setupMessageManager();
 }
 
 HeadsUpDisplay.prototype = {
+  /**
+   * Message names that the HUD listens for. These messages come from the remote
+   * Web Console content script.
+   *
+   * @private
+   * @type array
+   */
+  _messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI",
+                      "WebConsole:CachedMessages", "WebConsole:PageError"],
 
   consolePanel: null,
 
   /**
    * The nesting depth of the currently active console group.
    */
   groupDepth: 0,
 
@@ -3444,17 +3149,17 @@ HeadsUpDisplay.prototype = {
   /**
    * Retrieve the Web Console panel title.
    *
    * @return string
    *         The Web Console panel title.
    */
   getPanelTitle: function HUD_getPanelTitle()
   {
-    return this.getFormatStr("webConsoleWindowTitleAndURL", [this.uriSpec]);
+    return l10n.getFormatStr("webConsoleWindowTitleAndURL", [this.uriSpec]);
   },
 
   positions: {
     above: 0, // the childNode index
     below: 2,
     window: null
   },
 
@@ -3549,39 +3254,16 @@ HeadsUpDisplay.prototype = {
       this.jsterm.inputNode.focus();
     }
     if (this.gcliterm) {
       this.gcliterm.inputNode.focus();
     }
   },
 
   /**
-   * L10N shortcut function
-   *
-   * @param string aName
-   * @returns string
-   */
-  getStr: function HUD_getStr(aName)
-  {
-    return stringBundle.GetStringFromName(aName);
-  },
-
-  /**
-   * L10N shortcut function
-   *
-   * @param string aName
-   * @param array aArray
-   * @returns string
-   */
-  getFormatStr: function HUD_getFormatStr(aName, aArray)
-  {
-    return stringBundle.formatStringFromName(aName, aArray, aArray.length);
-  },
-
-  /**
    * The JSTerm object that contains the console's inputNode
    *
    */
   jsterm: null,
 
   /**
    * The GcliTerm object that contains the console's GCLI
    */
@@ -3619,49 +3301,40 @@ HeadsUpDisplay.prototype = {
       throw new Error("Unsupported Gecko Application");
     }
   },
 
   /**
    * Display cached messages that may have been collected before the UI is
    * displayed.
    *
-   * @returns void
-   */
-  displayCachedConsoleMessages: function HUD_displayCachedConsoleMessages()
-  {
-    let innerWindowId = HUDService.getInnerWindowId(this.contentWindow);
-
-    let messages = gConsoleStorage.getEvents(innerWindowId);
-
-    let errors = {};
-    Services.console.getMessageArray(errors, {});
-
-    // Filter the errors to find only those we should display.
-    let filteredErrors = (errors.value || []).filter(function(aError) {
-      return aError instanceof Ci.nsIScriptError &&
-             aError.innerWindowID == innerWindowId;
-    }, this);
-
-    messages.push.apply(messages, filteredErrors);
-    messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });
+   * @private
+   * @param array aRemoteMessages
+   *        Array of cached messages coming from the remote Web Console
+   *        content instance.
+   */
+  _displayCachedConsoleMessages:
+  function HUD__displayCachedConsoleMessages(aRemoteMessages)
+  {
+    if (!aRemoteMessages.length) {
+      return;
+    }
 
     // Turn off scrolling for the moment.
     ConsoleUtils.scroll = false;
     this.outputNode.hidden = true;
 
-    // Display all messages.
-    messages.forEach(function(aMessage) {
-      if (aMessage instanceof Ci.nsIScriptError) {
-        HUDService.reportPageError(aMessage);
-      }
-      else {
-        // In this case the cached message is a console message generated
-        // by the ConsoleAPI, not an nsIScriptError
-        HUDService.logConsoleAPIMessage(this.hudId, aMessage);
+    aRemoteMessages.forEach(function(aMessage) {
+      switch (aMessage._type) {
+        case "PageError":
+          this.reportPageError(aMessage);
+          break;
+        case "ConsoleAPI":
+          this.logConsoleAPIMessage(aMessage);
+          break;
       }
     }, this);
 
     this.outputNode.hidden = false;
     ConsoleUtils.scroll = true;
 
     // Scroll to bottom.
     let numChildren = this.outputNode.childNodes.length;
@@ -3743,17 +3416,17 @@ HeadsUpDisplay.prototype = {
     consoleWrap.setAttribute("flex", "1");
 
     this.filterSpacer = this.makeXULNode("spacer");
     this.filterSpacer.setAttribute("flex", "1");
 
     this.filterBox = this.makeXULNode("textbox");
     this.filterBox.setAttribute("class", "compact hud-filter-box");
     this.filterBox.setAttribute("hudId", this.hudId);
-    this.filterBox.setAttribute("placeholder", this.getStr("stringFilter"));
+    this.filterBox.setAttribute("placeholder", l10n.getStr("stringFilter"));
     this.filterBox.setAttribute("type", "search");
 
     this.setFilterTextBoxEvents();
 
     this.createConsoleMenu(this.consoleWrap);
 
     this.filterPrefs = HUDService.getDefaultFilterPrefs(this.hudId);
 
@@ -3921,38 +3594,38 @@ HeadsUpDisplay.prototype = {
       this.positionConsole("below");
     }).bind(this);
     this._positionConsoleWindow = (function HUD_positionWindow() {
       this.positionConsole("window");
     }).bind(this);
 
     let button = this.makeXULNode("toolbarbutton");
     button.setAttribute("type", "menu");
-    button.setAttribute("label", this.getStr("webConsolePosition"));
-    button.setAttribute("tooltip", this.getStr("webConsolePositionTooltip"));
+    button.setAttribute("label", l10n.getStr("webConsolePosition"));
+    button.setAttribute("tooltip", l10n.getStr("webConsolePositionTooltip"));
 
     let menuPopup = this.makeXULNode("menupopup");
     button.appendChild(menuPopup);
 
     let itemAbove = this.makeXULNode("menuitem");
-    itemAbove.setAttribute("label", this.getStr("webConsolePositionAbove"));
+    itemAbove.setAttribute("label", l10n.getStr("webConsolePositionAbove"));
     itemAbove.setAttribute("type", "checkbox");
     itemAbove.setAttribute("autocheck", "false");
     itemAbove.addEventListener("command", this._positionConsoleAbove, false);
     menuPopup.appendChild(itemAbove);
 
     let itemBelow = this.makeXULNode("menuitem");
-    itemBelow.setAttribute("label", this.getStr("webConsolePositionBelow"));
+    itemBelow.setAttribute("label", l10n.getStr("webConsolePositionBelow"));
     itemBelow.setAttribute("type", "checkbox");
     itemBelow.setAttribute("autocheck", "false");
     itemBelow.addEventListener("command", this._positionConsoleBelow, false);
     menuPopup.appendChild(itemBelow);
 
     let itemWindow = this.makeXULNode("menuitem");
-    itemWindow.setAttribute("label", this.getStr("webConsolePositionWindow"));
+    itemWindow.setAttribute("label", l10n.getStr("webConsolePositionWindow"));
     itemWindow.setAttribute("type", "checkbox");
     itemWindow.setAttribute("autocheck", "false");
     itemWindow.addEventListener("command", this._positionConsoleWindow, false);
     menuPopup.appendChild(itemWindow);
 
     this.positionMenuitems = {
       last: null,
       above: itemAbove,
@@ -3975,38 +3648,38 @@ HeadsUpDisplay.prototype = {
     let id = this.hudId + "-output-contextmenu";
     menuPopup.setAttribute("id", id);
     menuPopup.addEventListener("popupshowing", function() {
       saveBodiesItem.setAttribute("checked",
         HUDService.saveRequestAndResponseBodies);
     }, true);
 
     let saveBodiesItem = this.makeXULNode("menuitem");
-    saveBodiesItem.setAttribute("label", this.getStr("saveBodies.label"));
+    saveBodiesItem.setAttribute("label", l10n.getStr("saveBodies.label"));
     saveBodiesItem.setAttribute("accesskey",
-                                 this.getStr("saveBodies.accesskey"));
+                                 l10n.getStr("saveBodies.accesskey"));
     saveBodiesItem.setAttribute("type", "checkbox");
     saveBodiesItem.setAttribute("buttonType", "saveBodies");
     saveBodiesItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
     menuPopup.appendChild(saveBodiesItem);
 
     menuPopup.appendChild(this.makeXULNode("menuseparator"));
 
     let copyItem = this.makeXULNode("menuitem");
-    copyItem.setAttribute("label", this.getStr("copyCmd.label"));
-    copyItem.setAttribute("accesskey", this.getStr("copyCmd.accesskey"));
+    copyItem.setAttribute("label", l10n.getStr("copyCmd.label"));
+    copyItem.setAttribute("accesskey", l10n.getStr("copyCmd.accesskey"));
     copyItem.setAttribute("key", "key_copy");
     copyItem.setAttribute("command", "cmd_copy");
     copyItem.setAttribute("buttonType", "copy");
     menuPopup.appendChild(copyItem);
 
     let selectAllItem = this.makeXULNode("menuitem");
-    selectAllItem.setAttribute("label", this.getStr("selectAllCmd.label"));
+    selectAllItem.setAttribute("label", l10n.getStr("selectAllCmd.label"));
     selectAllItem.setAttribute("accesskey",
-                               this.getStr("selectAllCmd.accesskey"));
+                               l10n.getStr("selectAllCmd.accesskey"));
     selectAllItem.setAttribute("hudId", this.hudId);
     selectAllItem.setAttribute("buttonType", "selectAll");
     selectAllItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
     menuPopup.appendChild(selectAllItem);
 
     aConsoleWrapper.appendChild(menuPopup);
     aConsoleWrapper.setAttribute("context", id);
   },
@@ -4027,30 +3700,30 @@ HeadsUpDisplay.prototype = {
     let toolbarButton = this.makeXULNode("toolbarbutton");
     aParent.appendChild(toolbarButton);
 
     let toggleFilter = HeadsUpDisplayUICommands.toggleFilter;
     toolbarButton.addEventListener("click", toggleFilter, false);
 
     let name = aDescriptor.name;
     toolbarButton.setAttribute("type", "menu-button");
-    toolbarButton.setAttribute("label", this.getStr("btn" + name));
-    toolbarButton.setAttribute("tooltip", this.getStr("tip" + name));
+    toolbarButton.setAttribute("label", l10n.getStr("btn" + name));
+    toolbarButton.setAttribute("tooltip", l10n.getStr("tip" + name));
     toolbarButton.setAttribute("category", aDescriptor.category);
     toolbarButton.setAttribute("hudId", this.hudId);
     toolbarButton.classList.add("webconsole-filter-button");
 
     let menuPopup = this.makeXULNode("menupopup");
     toolbarButton.appendChild(menuPopup);
 
     let someChecked = false;
     for (let i = 0; i < aDescriptor.severities.length; i++) {
       let severity = aDescriptor.severities[i];
       let menuItem = this.makeXULNode("menuitem");
-      menuItem.setAttribute("label", this.getStr("btn" + severity.name));
+      menuItem.setAttribute("label", l10n.getStr("btn" + severity.name));
       menuItem.setAttribute("type", "checkbox");
       menuItem.setAttribute("autocheck", "false");
       menuItem.setAttribute("hudId", this.hudId);
 
       let prefKey = severity.prefKey;
       menuItem.setAttribute("prefKey", prefKey);
 
       let checked = this.filterPrefs[prefKey];
@@ -4107,17 +3780,17 @@ HeadsUpDisplay.prototype = {
         hud.jsterm.clearOutput(true);
       }
       if (hud.gcliterm) {
         hud.gcliterm.clearOutput();
       }
     }
 
     let clearButton = this.makeXULNode("toolbarbutton");
-    clearButton.setAttribute("label", this.getStr("btnClear"));
+    clearButton.setAttribute("label", l10n.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
@@ -4128,18 +3801,20 @@ HeadsUpDisplay.prototype = {
    *        console.dir call.
    */
   pruneConsoleDirNode: function HUD_pruneConsoleDirNode(aMessageNode)
   {
     aMessageNode.parentNode.removeChild(aMessageNode);
     let tree = aMessageNode.querySelector("tree");
     tree.parentNode.removeChild(tree);
     aMessageNode.propertyTreeView = null;
+    if (tree.view) {
+      tree.view.data = null;
+    }
     tree.view = null;
-    tree = null;
   },
 
   /**
    * Create the Web Console UI.
    *
    * @return nsIDOMNode
    *         The Web Console container element (HUDBox).
    */
@@ -4152,100 +3827,442 @@ HeadsUpDisplay.prototype = {
     }
     return this.HUDBox;
   },
 
   uiInOwnWindow: false,
 
   get console() { return this.contentWindow.wrappedJSObject.console; },
 
-  getLogCount: function HUD_getLogCount()
-  {
-    return this.outputNode.childNodes.length;
-  },
-
-  getLogNodes: function HUD_getLogNodes()
-  {
-    return this.outputNode.childNodes;
+  /**
+   * Logs a message to the Web Console that originates from the remote Web
+   * Console instance.
+   *
+   * @param object aMessage
+   *        The message received from the remote Web Console instance.
+   *        console service. This object needs to hold:
+   *          - hudId - the Web Console ID.
+   *          - apiMessage - a representation of the object sent by the console
+   *          storage service. This object holds the console message level, the
+   *          arguments that were passed to the console method and other
+   *          information.
+   *          - argumentsToString - the array of arguments passed to the console
+   *          method, each converted to a string.
+   */
+  logConsoleAPIMessage: function HUD_logConsoleAPIMessage(aMessage)
+  {
+    let body = null;
+    let clipboardText = null;
+    let sourceURL = null;
+    let sourceLine = 0;
+    let level = aMessage.apiMessage.level;
+    let args = aMessage.apiMessage.arguments;
+    let argsToString = aMessage.argumentsToString;
+
+    switch (level) {
+      case "log":
+      case "info":
+      case "warn":
+      case "error":
+      case "debug":
+        body = argsToString.join(" ");
+        sourceURL = aMessage.apiMessage.filename;
+        sourceLine = aMessage.apiMessage.lineNumber;
+        break;
+
+      case "trace":
+        let filename = WebConsoleUtils.abbreviateSourceURL(args[0].filename);
+        let functionName = args[0].functionName ||
+                           l10n.getStr("stacktrace.anonymousFunction");
+        let lineNumber = args[0].lineNumber;
+
+        body = l10n.getFormatStr("stacktrace.outputMessage",
+                                 [filename, functionName, lineNumber]);
+
+        sourceURL = args[0].filename;
+        sourceLine = args[0].lineNumber;
+
+        clipboardText = "";
+
+        args.forEach(function(aFrame) {
+          clipboardText += aFrame.filename + " :: " +
+                           aFrame.functionName + " :: " +
+                           aFrame.lineNumber + "\n";
+        });
+
+        clipboardText = clipboardText.trimRight();
+        break;
+
+      case "dir":
+        body = {
+          cacheId: aMessage.objectsCacheId,
+          resultString: argsToString[0],
+          remoteObject: args[0],
+          remoteObjectProvider:
+            this.jsterm.remoteObjectProvider.bind(this.jsterm),
+        };
+        clipboardText = body.resultString;
+        sourceURL = aMessage.apiMessage.filename;
+        sourceLine = aMessage.apiMessage.lineNumber;
+        break;
+
+      case "group":
+      case "groupCollapsed":
+        clipboardText = body = args;
+        sourceURL = aMessage.apiMessage.filename;
+        sourceLine = aMessage.apiMessage.lineNumber;
+        this.groupDepth++;
+        break;
+
+      case "groupEnd":
+        if (this.groupDepth > 0) {
+          this.groupDepth--;
+        }
+        return;
+
+      case "time":
+        if (!args) {
+          return;
+        }
+        if (args.error) {
+          Cu.reportError(l10n.getStr(args.error));
+          return;
+        }
+        body = l10n.getFormatStr("timerStarted", [args.name]);
+        clipboardText = body;
+        sourceURL = aMessage.apiMessage.filename;
+        sourceLine = aMessage.apiMessage.lineNumber;
+        break;
+
+      case "timeEnd":
+        if (!args) {
+          return;
+        }
+        body = l10n.getFormatStr("timeEnd", [args.name, args.duration]);
+        clipboardText = body;
+        sourceURL = aMessage.apiMessage.filename;
+        sourceLine = aMessage.apiMessage.lineNumber;
+        break;
+
+      default:
+        Cu.reportError("Unknown Console API log level: " + level);
+        return;
+    }
+
+    let node = ConsoleUtils.createMessageNode(this.chromeDocument,
+                                              CATEGORY_WEBDEV,
+                                              LEVELS[level],
+                                              body,
+                                              this.hudId,
+                                              sourceURL,
+                                              sourceLine,
+                                              clipboardText,
+                                              level,
+                                              aMessage.timeStamp);
+
+    // Make the node bring up the property panel, to allow the user to inspect
+    // the stack trace.
+    if (level == "trace") {
+      node._stacktrace = args;
+
+      this.makeOutputMessageLink(node, function _traceNodeClickCallback() {
+        if (node._panelOpen) {
+          return;
+        }
+
+        let options = {
+          anchor: node,
+          data: { object: node._stacktrace },
+        };
+
+        let propPanel = this.jsterm.openPropertyPanelAsync(options);
+        propPanel.panel.setAttribute("hudId", this.hudId);
+      }.bind(this));
+    }
+
+    ConsoleUtils.outputMessageNode(node, this.hudId);
+
+    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;
+
+      // Make sure the cached evaluated object will be purged when the node is
+      // removed.
+      node._evalCacheId = aMessage.objectsCacheId;
+    }
+  },
+
+  /**
+   * Reports an error in the page source, either JavaScript or CSS.
+   *
+   * @param nsIScriptError aScriptError
+   *        The error message to report.
+   */
+  reportPageError: function HUD_reportPageError(aScriptError)
+  {
+    if (!aScriptError.outerWindowID) {
+      return;
+    }
+
+    let category;
+
+    switch (aScriptError.category) {
+      // We ignore chrome-originating errors as we only care about content.
+      case "XPConnect JavaScript":
+      case "component javascript":
+      case "chrome javascript":
+      case "chrome registration":
+      case "XBL":
+      case "XBL Prototype Handler":
+      case "XBL Content Sink":
+      case "xbl javascript":
+        return;
+
+      case "CSS Parser":
+      case "CSS Loader":
+        category = CATEGORY_CSS;
+        break;
+
+      default:
+        category = CATEGORY_JS;
+        break;
+    }
+
+    // Warnings and legacy strict errors become warnings; other types become
+    // errors.
+    let severity = SEVERITY_ERROR;
+    if ((aScriptError.flags & aScriptError.warningFlag) ||
+        (aScriptError.flags & aScriptError.strictFlag)) {
+      severity = SEVERITY_WARNING;
+    }
+
+    let node = ConsoleUtils.createMessageNode(this.chromeDocument,
+                                              category,
+                                              severity,
+                                              aScriptError.errorMessage,
+                                              this.hudId,
+                                              aScriptError.sourceName,
+                                              aScriptError.lineNumber,
+                                              null,
+                                              null,
+                                              aScriptError.timeStamp);
+
+    ConsoleUtils.outputMessageNode(node, this.hudId);
   },
 
   ERRORS: {
     HUD_BOX_DOES_NOT_EXIST: "Heads Up Display does not exist",
     TAB_ID_REQUIRED: "Tab DOM ID is required",
     PARENTNODE_NOT_FOUND: "parentNode element not found"
   },
 
   /**
+   * Setup the message manager used to communicate with the Web Console content
+   * script. This method loads the content script, adds the message listeners
+   * and initializes the connection to the content script.
+   *
+   * @private
+   */
+  _setupMessageManager: function HUD__setupMessageManager()
+  {
+    this.messageManager.loadFrameScript(CONTENT_SCRIPT_URL, true);
+
+    this._messageListeners.forEach(function(aName) {
+      this.messageManager.addMessageListener(aName, this);
+    }, this);
+
+    let message = {
+      hudId: this.hudId,
+      features: ["ConsoleAPI", "JSTerm", "PageError"],
+      cachedMessages: ["ConsoleAPI", "PageError"],
+    };
+    this.sendMessageToContent("WebConsole:Init", message);
+  },
+
+  /**
+   * Handler for all of the messages coming from the Web Console content script.
+   *
+   * @private
+   * @param object aMessage
+   *        A MessageManager object that holds the remote message.
+   */
+  receiveMessage: function HUD_receiveMessage(aMessage)
+  {
+    if (!aMessage.json || aMessage.json.hudId != this.hudId) {
+      Cu.reportError("JSTerm: received message " + aMessage.name +
+                     " from wrong hudId.");
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "JSTerm:EvalObject":
+        this._receiveMessageWithCallback(aMessage.json);
+        break;
+      case "WebConsole:ConsoleAPI":
+        this.logConsoleAPIMessage(aMessage.json);
+        break;
+      case "WebConsole:PageError":
+        this.reportPageError(aMessage.json.pageError);
+        break;
+      case "WebConsole:CachedMessages":
+        this._displayCachedConsoleMessages(aMessage.json.messages);
+        this._onInitComplete();
+        break;
+    }
+  },
+
+  /**
+   * Callback method for when the Web Console initialization is complete. For
+   * now this method sends the web-console-created notification using the
+   * nsIObserverService.
+   *
+   * @private
+   */
+  _onInitComplete: function HUD__onInitComplete()
+  {
+    let id = WebConsoleUtils.supportsString(this.hudId);
+    Services.obs.notifyObservers(id, "web-console-created", null);
+  },
+
+  /**
+   * Handler for messages that have an associated callback function. The
+   * this.sendMessageToContent() allows one to provide a function to be invoked
+   * when the content script replies to the message previously sent. This is the
+   * method that invokes the callback.
+   *
+   * @see this.sendMessageToContent
+   * @private
+   * @param object aResponse
+   *        Message object received from the content script.
+   */
+  _receiveMessageWithCallback:
+  function HUD__receiveMessageWithCallback(aResponse)
+  {
+    if (aResponse.id in this.asyncRequests) {
+      let request = this.asyncRequests[aResponse.id];
+      request.callback(aResponse, request.message);
+      delete this.asyncRequests[aResponse.id];
+    }
+    else {
+      Cu.reportError("receiveMessageWithCallback response for stale request " +
+                     "ID " + aResponse.id);
+    }
+  },
+
+  /**
+   * Send a message to the content script.
+   *
+   * @param string aName
+   *        The name of the message you want to send.
+   *
+   * @param object aMessage
+   *        The message object you want to send. This object needs to have no
+   *        cyclic references and it needs to be JSON-stringifiable.
+   *
+   * @param function [aCallback]
+   *        Optional function you want to have called when the content script
+   *        replies to your message. Your callback receives two arguments:
+   *        (1) the response object from the content script and (2) the message
+   *        you sent to the content script (which is aMessage here).
+   */
+  sendMessageToContent:
+  function HUD_sendMessageToContent(aName, aMessage, aCallback)
+  {
+    aMessage.hudId = this.hudId;
+    if (!("id" in aMessage)) {
+      aMessage.id = "HUDChrome-" + HUDService.sequenceId();
+    }
+
+    if (aCallback) {
+      this.asyncRequests[aMessage.id] = {
+        name: aName,
+        message: aMessage,
+        callback: aCallback,
+      };
+    }
+
+    this.messageManager.sendAsyncMessage(aName, aMessage);
+  },
+
+  /**
+   * Make a link given an output element.
+   *
+   * @param nsIDOMNode aNode
+   *        The message element you want to make a link for.
+   * @param function aCallback
+   *        The function you want invoked when the user clicks on the message
+   *        element.
+   */
+  makeOutputMessageLink: function HUD_makeOutputMessageLink(aNode, aCallback)
+  {
+    let linkNode;
+    if (aNode.category === CATEGORY_NETWORK) {
+      linkNode = aNode.querySelector(".webconsole-msg-link");
+    }
+    else {
+      linkNode = aNode.querySelector(".webconsole-msg-body");
+      linkNode.classList.add("hud-clickable");
+    }
+
+    linkNode.setAttribute("aria-haspopup", "true");
+
+    aNode.addEventListener("mousedown", function(aEvent) {
+      this._startX = aEvent.clientX;
+      this._startY = aEvent.clientY;
+    }, false);
+
+    aNode.addEventListener("click", function(aEvent) {
+      if (aEvent.detail != 1 || aEvent.button != 0 ||
+          (this._startX != aEvent.clientX &&
+           this._startY != aEvent.clientY)) {
+        return;
+      }
+
+      aCallback(this, aEvent);
+    }, false);
+  },
+
+  /**
    * Destroy the HUD object. Call this method to avoid memory leaks when the Web
    * Console is closed.
    */
   destroy: function HUD_destroy()
   {
+    this.sendMessageToContent("WebConsole:Destroy", {hudId: this.hudId});
+
+    this._messageListeners.forEach(function(aName) {
+      this.messageManager.removeMessageListener(aName, this);
+    }, this);
+
     if (this.jsterm) {
       this.jsterm.destroy();
     }
     if (this.gcliterm) {
       this.gcliterm.destroy();
     }
 
+    delete this.asyncRequests;
+    delete this.messageManager;
+    delete this.browser;
+
     this.positionMenuitems.above.removeEventListener("command",
       this._positionConsoleAbove, false);
     this.positionMenuitems.below.removeEventListener("command",
       this._positionConsoleBelow, false);
     this.positionMenuitems.window.removeEventListener("command",
       this._positionConsoleWindow, false);
 
     this.closeButton.removeEventListener("command",
       this.closeButtonOnCommand, false);
   },
 };
 
-
-//////////////////////////////////////////////////////////////////////////////
-// ConsoleAPIObserver
-//////////////////////////////////////////////////////////////////////////////
-
-let ConsoleAPIObserver = {
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
-
-  init: function CAO_init()
-  {
-    Services.obs.addObserver(this, "quit-application-granted", false);
-    Services.obs.addObserver(this, "console-api-log-event", false);
-  },
-
-  observe: function CAO_observe(aMessage, aTopic, aData)
-  {
-    if (aTopic == "console-api-log-event") {
-      aMessage = aMessage.wrappedJSObject;
-      let windowId = parseInt(aData);
-      let win = HUDService.getWindowByWindowId(windowId);
-      if (!win)
-        return;
-
-      // Find the HUD ID for the topmost window
-      let hudId = HUDService.getHudIdByWindow(win.top);
-      if (!hudId)
-        return;
-
-      HUDService.logConsoleAPIMessage(hudId, aMessage);
-    }
-    else if (aTopic == "quit-application-granted") {
-      HUDService.shutdown();
-    }
-  },
-
-  shutdown: function CAO_shutdown()
-  {
-    Services.obs.removeObserver(this, "quit-application-granted");
-    Services.obs.removeObserver(this, "console-api-log-event");
-  }
-};
-
 /**
  * Creates a DOM Node factory for XUL nodes - as well as textNodes
  * @param aFactoryType "xul" or "text"
  * @param ignored This parameter is currently ignored, and will be removed
  * See bug 594304
  * @param aDocument The document, the factory is to generate nodes from
  * @return DOM Node Factory function
  */
@@ -4407,17 +4424,17 @@ function findCompletionBeginning(aStr)
  *            {
  *              matches: [ string, string, string ],
  *              matchProp: Last part of the inputValue that was used to find
  *                         the matches-strings.
  *            }
  */
 function JSPropertyProvider(aScope, aInputValue)
 {
-  let obj = unwrap(aScope);
+  let obj = WebConsoleUtils.unwrap(aScope);
 
   // Analyse the aInputValue and find the beginning of the last part that
   // should be completed.
   let beginning = findCompletionBeginning(aInputValue);
 
   // There was an error analysing the string.
   if (beginning.err) {
     return null;
@@ -4446,17 +4463,17 @@ function JSPropertyProvider(aScope, aInp
       // If obj is undefined or null, then there is no chance to run completion
       // on it. Exit here.
       if (typeof obj === "undefined" || obj === null) {
         return null;
       }
 
       // Check if prop is a getter function on obj. Functions can change other
       // stuff so we can't execute them to get the next object. Stop here.
-      if (isNonNativeGetter(obj, prop)) {
+      if (WebConsoleUtils.isNonNativeGetter(obj, prop)) {
         return null;
       }
       try {
         obj = obj[prop];
       }
       catch (ex) {
         return null;
       }
@@ -4468,61 +4485,33 @@ function JSPropertyProvider(aScope, aInp
 
   // If obj is undefined or null, then there is no chance to run
   // completion on it. Exit here.
   if (typeof obj === "undefined" || obj === null) {
     return null;
   }
 
   // Skip Iterators and Generators.
-  if (isIteratorOrGenerator(obj)) {
+  if (WebConsoleUtils.isIteratorOrGenerator(obj)) {
     return null;
   }
 
   let matches = [];
   for (let prop in obj) {
     if (prop.indexOf(matchProp) == 0) {
       matches.push(prop);
     }
   }
 
   return {
     matchProp: matchProp,
     matches: matches.sort(),
   };
 }
 
-function isIteratorOrGenerator(aObject)
-{
-  if (aObject === null) {
-    return false;
-  }
-
-  if (typeof aObject == "object") {
-    if (typeof aObject.__iterator__ == "function" ||
-        aObject.constructor && aObject.constructor.name == "Iterator") {
-      return true;
-    }
-
-    try {
-      let str = aObject.toString();
-      if (typeof aObject.next == "function" &&
-          str.indexOf("[object Generator") == 0) {
-        return true;
-      }
-    }
-    catch (ex) {
-      // window.history.next throws in the typeof check above.
-      return false;
-    }
-  }
-
-  return false;
-}
-
 //////////////////////////////////////////////////////////////////////////
 // JSTerm
 //////////////////////////////////////////////////////////////////////////
 
 /**
  * JSTermHelper
  *
  * Defines a set of functions ("helper functions") that are available from the
@@ -4630,34 +4619,34 @@ function JSTermHelper(aJSTerm)
    *
    * @param object aObject
    *        Object to return the property names from.
    * @returns array of string
    */
   aJSTerm.sandbox.keys = function JSTH_keys(aObject)
   {
     try {
-      return Object.keys(unwrap(aObject));
+      return Object.keys(WebConsoleUtils.unwrap(aObject));
     }
     catch (ex) {
       aJSTerm.console.error(ex.message);
     }
   };
 
   /**
    * Returns the values of all properties on aObject.
    *
    * @param object aObject
    *        Object to display the values from.
    * @returns array of string
    */
   aJSTerm.sandbox.values = function JSTH_values(aObject)
   {
     let arrValues = [];
-    let obj = unwrap(aObject);
+    let obj = WebConsoleUtils.unwrap(aObject);
 
     try {
       for (let prop in obj) {
         arrValues.push(obj[prop]);
       }
     }
     catch (ex) {
       aJSTerm.console.error(ex.message);
@@ -4681,17 +4670,18 @@ function JSTermHelper(aJSTerm)
    *
    * @param object aObject
    *        Object to inspect.
    * @returns void
    */
   aJSTerm.sandbox.inspect = function JSTH_inspect(aObject)
   {
     aJSTerm.helperEvaluated = true;
-    let propPanel = aJSTerm.openPropertyPanel(null, unwrap(aObject));
+    let propPanel = aJSTerm.openPropertyPanel(null,
+                                              WebConsoleUtils.unwrap(aObject));
     propPanel.panel.setAttribute("hudId", aJSTerm.hudId);
   };
 
   aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode)
   {
     aJSTerm.helperEvaluated = true;
     let doc = aJSTerm.inputNode.ownerDocument;
     let win = doc.defaultView;
@@ -4742,26 +4732,26 @@ function JSTermHelper(aJSTerm)
    * @param object aObject
    *        Object to print to the output.
    * @returns void
    */
   aJSTerm.sandbox.pprint = function JSTH_pprint(aObject)
   {
     aJSTerm.helperEvaluated = true;
     if (aObject === null || aObject === undefined || aObject === true || aObject === false) {
-      aJSTerm.console.error(HUDService.getStr("helperFuncUnsupportedTypeError"));
+      aJSTerm.console.error(l10n.getStr("helperFuncUnsupportedTypeError"));
       return;
     }
     else if (typeof aObject === TYPEOF_FUNCTION) {
       aJSTerm.writeOutput(aObject + "\n", CATEGORY_OUTPUT, SEVERITY_LOG);
       return;
     }
 
     let output = [];
-    let pairs = namesAndValuesOf(unwrap(aObject));
+    let pairs = namesAndValuesOf(WebConsoleUtils.unwrap(aObject));
 
     pairs.forEach(function(pair) {
       output.push("  " + pair.display);
     });
 
     aJSTerm.writeOutput(output.join("\n"), CATEGORY_OUTPUT, SEVERITY_LOG);
   };
 
@@ -4812,27 +4802,28 @@ function JSTerm(aContext, aParentNode, a
   this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;
 
   let node = aParentNode;
   while (!node.hasAttribute("id")) {
     node = node.parentNode;
   }
   this.hudId = node.getAttribute("id");
 
+  this.history = [];
   this.historyIndex = 0;
   this.historyPlaceHolder = 0;  // this.history.length;
   this.log = LogFactory("*** JSTerm:");
   this.autocompletePopup = new AutocompletePopup(aParentNode.ownerDocument);
   this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this);
   this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this);
   this.init();
 }
 
 JSTerm.prototype = {
-
+  lastInputValue: "",
   propertyProvider: JSPropertyProvider,
 
   COMPLETE_FORWARD: 0,
   COMPLETE_BACKWARD: 1,
   COMPLETE_HINT_ONLY: 2,
 
   init: function JST_init()
   {
@@ -4892,17 +4883,17 @@ JSTerm.prototype = {
    */
   evalInSandbox: function JST_evalInSandbox(aString)
   {
     // The help function needs to be easy to guess, so we make the () optional
     if (aString.trim() === "help" || aString.trim() === "?") {
       aString = "help()";
     }
 
-    let window = unwrap(this.sandbox.window);
+    let window = WebConsoleUtils.unwrap(this.sandbox.window);
     let $ = null, $$ = null;
 
     // We prefer to execute the page-provided implementations for the $() and
     // $$() functions.
     if (typeof window.$ == "function") {
       $ = this.sandbox.$;
       delete this.sandbox.$;
     }
@@ -4937,18 +4928,18 @@ JSTerm.prototype = {
 
     try {
       this.helperEvaluated = false;
       let result = this.evalInSandbox(aExecuteString);
 
       // Hide undefined results coming from helpers.
       let shouldShow = !(result === undefined && this.helperEvaluated);
       if (shouldShow) {
-        let inspectable = this.isResultInspectable(result);
-        let resultString = this.formatResult(result);
+        let inspectable = WebConsoleUtils.isObjectInspectable(result);
+        let resultString = WebConsoleUtils.formatResult(result);
 
         if (inspectable) {
           this.writeOutputJS(aExecuteString, result, resultString);
         }
         else {
           this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG);
         }
       }
@@ -4988,18 +4979,18 @@ JSTerm.prototype = {
     //    result will be inspected by this panel.
     let buttons = [];
 
     // If there is a evalString passed to this function, then add a `Update`
     // button to the panel so that the evalString can be reexecuted to update
     // the content of the panel.
     if (aEvalString !== null) {
       buttons.push({
-        label: HUDService.getStr("update.button"),
-        accesskey: HUDService.getStr("update.accesskey"),
+        label: l10n.getStr("update.button"),
+        accesskey: l10n.getStr("update.accesskey"),
         oncommand: function () {
           try {
             var result = self.evalInSandbox(aEvalString);
 
             if (result !== undefined) {
               // TODO: This updates the value of the tree.
               // However, the states of opened nodes is not saved.
               // See bug 586246.
@@ -5011,29 +5002,91 @@ JSTerm.prototype = {
           }
         }
       });
     }
 
     let doc = self.document;
     let parent = doc.getElementById("mainPopupSet");
     let title = (aEvalString
-        ? HUDService.getFormatStr("jsPropertyInspectTitle", [aEvalString])
-        : HUDService.getStr("jsPropertyTitle"));
+        ? l10n.getFormatStr("jsPropertyInspectTitle", [aEvalString])
+        : l10n.getStr("jsPropertyTitle"));
 
     propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons);
     propPanel.linkNode = aAnchor;
 
     let panel = propPanel.panel;
     panel.openPopup(aAnchor, "after_pointer", 0, 0, false, false);
     panel.sizeTo(350, 450);
     return propPanel;
   },
 
   /**
+   * Opens a new property panel that allows the inspection of the given object.
+   * The object information can be retrieved both async and sync, depending on
+   * the given options.
+   *
+   * @param object aOptions
+   *        Property panel options:
+   *        - title:
+   *        Panel title.
+   *        - anchor:
+   *        The DOM element you want the panel to be anchored to.
+   *        - updateButtonCallback:
+   *        An optional function you want invoked when the user clicks the
+   *        Update button. If this function is not provided the Update button is
+   *        not shown.
+   *        - data:
+   *        An object that represents the object you want to inspect. Please see
+   *        the PropertyPanelAsync documentation - this object is passed to the
+   *        PropertyPanelAsync constructor
+   * @param object aResponse
+   *        The response object to display/inspect inside of the tree.
+   * @param nsIDOMNode aAnchor
+   *        A node to popup the panel next to (using "after_pointer").
+   * @return object
+   *         The new instance of PropertyPanelAsync.
+   */
+  openPropertyPanelAsync: function JST_openPropertyPanelAsync(aOptions)
+  {
+    // The property panel has one button:
+    //    `Update`: reexecutes the string executed on the command line. The
+    //    result will be inspected by this panel.
+    let buttons = [];
+
+    if (aOptions.updateButtonCallback) {
+      buttons.push({
+        label: l10n.getStr("update.button"),
+        accesskey: l10n.getStr("update.accesskey"),
+        oncommand: aOptions.updateButtonCallback,
+      });
+    }
+
+    let parent = this.document.getElementById("mainPopupSet");
+    let title = aOptions.title ?
+                l10n.getFormatStr("jsPropertyInspectTitle", [aOptions.title]) :
+                l10n.getStr("jsPropertyTitle");
+
+    let propPanel = new PropertyPanelAsync(parent, title, aOptions.data, buttons);
+
+    propPanel.panel.openPopup(aOptions.anchor, "after_pointer", 0, 0, false, false);
+    propPanel.panel.sizeTo(350, 450);
+
+    if (aOptions.anchor) {
+      propPanel.panel.addEventListener("popuphiding", function onPopupHide() {
+        propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
+        aOptions.anchor._panelOpen = false;
+      }, false);
+      aOptions.anchor._panelOpen = true;
+    }
+
+    return propPanel;
+  },
+
+  /**
    * Writes a JS object to the JSTerm outputNode. If the user clicks on the
    * written object, openPropertyPanel is called to open up a panel to inspect
    * the object.
    *
    * @param string aEvalString
    *        String that was evaluated to get the aOutputObject.
    * @param object aResultObject
    *        The evaluation result object.
@@ -5094,161 +5147,48 @@ JSTerm.prototype = {
     let node = ConsoleUtils.createMessageNode(this.document,
                                               aCategory, aSeverity,
                                               aOutputMessage, this.hudId);
 
     ConsoleUtils.outputMessageNode(node, this.hudId);
   },
 
   /**
-   * Format the jsterm execution result based on its type.
-   *
-   * @param mixed aResult
-   *        The evaluation result object you want displayed.
-   * @returns string
-   *          The string that can be displayed.
-   */
-  formatResult: function JST_formatResult(aResult)
-  {
-    let output = "";
-    let type = this.getResultType(aResult);
-
-    switch (type) {
-      case "string":
-        output = this.formatString(aResult);
-        break;
-      case "boolean":
-      case "date":
-      case "error":
-      case "number":
-      case "regexp":
-        output = aResult.toString();
-        break;
-      case "null":
-      case "undefined":
-        output = type;
-        break;
-      default:
-        if (aResult.toSource) {
-          try {
-            output = aResult.toSource();
-          } catch (ex) { }
-        }
-        if (!output || output == "({})") {
-          output = aResult.toString();
-        }
-        break;
-    }
-
-    return output;
-  },
-
-  /**
-   * Format a string for output.
-   *
-   * @param string aString
-   *        The string you want to display.
-   * @returns string
-   *          The string that can be displayed.
-   */
-  formatString: function JST_formatString(aString)
-  {
-    function isControlCode(c) {
-      // See http://en.wikipedia.org/wiki/C0_and_C1_control_codes
-      // C0 is 0x00-0x1F, C1 is 0x80-0x9F (inclusive).
-      // We also include DEL (U+007F) and NBSP (U+00A0), which are not strictly
-      // in C1 but border it.
-      return (c <= 0x1F) || (0x7F <= c && c <= 0xA0);
-    }
-
-    function replaceFn(aMatch, aType, aHex) {
-      // Leave control codes escaped, but unescape the rest of the characters.
-      let c = parseInt(aHex, 16);
-      return isControlCode(c) ? aMatch : String.fromCharCode(c);
-    }
-
-    let output = uneval(aString).replace(/\\(x)([0-9a-fA-F]{2})/g, replaceFn)
-                 .replace(/\\(u)([0-9a-fA-F]{4})/g, replaceFn);
-
-    return output;
-  },
-
-  /**
-   * Determine if the jsterm execution result is inspectable or not.
-   *
-   * @param mixed aResult
-   *        The evaluation result object you want to check if it is inspectable.
-   * @returns boolean
-   *          True if the object is inspectable or false otherwise.
-   */
-  isResultInspectable: function JST_isResultInspectable(aResult)
-  {
-    let isEnumerable = false;
-
-    // Skip Iterators and Generators.
-    if (isIteratorOrGenerator(aResult)) {
-      return false;
-    }
-
-    for (let p in aResult) {
-      isEnumerable = true;
-      break;
-    }
-
-    return isEnumerable && typeof(aResult) != "string";
-  },
-
-  /**
-   * Determine the type of the jsterm execution result.
-   *
-   * @param mixed aResult
-   *        The evaluation result object you want to check.
-   * @returns string
-   *          Constructor name or type: string, number, boolean, regexp, date,
-   *          function, object, null, undefined...
-   */
-  getResultType: function JST_getResultType(aResult)
-  {
-    let type = aResult === null ? "null" : typeof aResult;
-    if (type == "object" && aResult.constructor && aResult.constructor.name) {
-      type = aResult.constructor.name;
-    }
-
-    return type.toLowerCase();
-  },
-
-  /**
    * Clear the Web Console output.
    *
    * @param boolean aClearStorage
    *        True if you want to clear the console messages storage associated to
    *        this Web Console.
    */
   clearOutput: function JST_clearOutput(aClearStorage)
   {
     let hud = HUDService.getHudReferenceById(this.hudId);
     hud.cssNodes = {};
 
-    let node = hud.outputNode;
-    while (node.firstChild) {
-      if (node.firstChild.classList &&
-          node.firstChild.classList.contains("webconsole-msg-inspector")) {
-        hud.pruneConsoleDirNode(node.firstChild);
+    let outputNode = hud.outputNode;
+    let node;
+    while ((node = outputNode.firstChild)) {
+      if (node._evalCacheId && !node._panelOpen) {
+        this.clearObjectCache(node._evalCacheId);
+      }
+
+      if (node.classList &&
+          node.classList.contains("webconsole-msg-inspector")) {
+        hud.pruneConsoleDirNode(node);
       }
       else {
-        hud.outputNode.removeChild(node.firstChild);
+        outputNode.removeChild(node);
       }
     }
 
     hud.HUDBox.lastTimestamp = 0;
     hud.groupDepth = 0;
 
     if (aClearStorage) {
-      let windowId = HUDService.getInnerWindowId(hud.contentWindow);
-      gConsoleStorage.clearEvents(windowId);
+      hud.sendMessageToContent("ConsoleAPI:ClearCache", {});
     }
   },
 
   /**
    * Updates the size of the input field (command line) to fit its contents.
    *
    * @returns void
    */
@@ -5377,17 +5317,17 @@ JSTerm.prototype = {
       case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
         // Generate a completion and accept the first proposed value.
         if (this.complete(this.COMPLETE_HINT_ONLY) &&
             this.lastCompletion &&
             this.acceptProposedCompletion()) {
           aEvent.preventDefault();
         }
         else {
-          this.updateCompleteNode(HUDService.getStr("Autocomplete.blank"));
+          this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
           aEvent.preventDefault();
         }
         break;
 
       default:
         break;
     }
   },
@@ -5484,17 +5424,17 @@ JSTerm.prototype = {
       return false;
     }
 
     let multiline = /[\r\n]/.test(node.value);
     return node.selectionStart == node.value.length ? true :
            node.selectionStart == 0 && !multiline;
   },
 
-  history: [],
+  history: null,
 
   // Stores the data for the last completion.
   lastCompletion: null,
 
   /**
    * Completes the current typed text in the inputNode. Completion is performed
    * only if the selection/cursor is at the end of the string. If no completion
    * is found, the current inputNode value and cursor/selection stay.
@@ -5643,16 +5583,57 @@ JSTerm.prototype = {
   updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
   {
     // completion prefix = input, with non-control chars replaced by spaces
     let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
     this.completeNode.value = prefix + aSuffix;
   },
 
   /**
+   * Clear the object cache from the Web Console content instance.
+   *
+   * @param string aCacheId
+   *        The cache ID you want to clear. Multiple objects are cached into one
+   *        group which is given an ID.
+   */
+  clearObjectCache: function JST_clearObjectCache(aCacheId)
+  {
+    let hud = HUDService.getHudReferenceById(this.hudId);
+    hud.sendMessageToContent("JSTerm:ClearObjectCache", {cacheId: aCacheId});
+  },
+
+  /**
+   * The remote object provider allows you to retrieve a given object from
+   * a specific cache and have your callback invoked when the desired object is
+   * received from the Web Console content instance.
+   *
+   * @param string aCacheId
+   *        Retrieve the desired object from this cache ID.
+   * @param string aObjectId
+   *        The ID of the object you want.
+   * @param string aResultCacheId
+   *        The ID of the cache where you want any object references to be
+   *        stored into.
+   * @param function aCallback
+   *        The function you want invoked when the desired object is retrieved.
+   */
+  remoteObjectProvider:
+  function JST_remoteObjectProvider(aCacheId, aObjectId, aResultCacheId,
+                                    aCallback) {
+    let message = {
+      cacheId: aCacheId,
+      objectId: aObjectId,
+      resultCacheId: aResultCacheId,
+    };
+
+    let hud = HUDService.getHudReferenceById(this.hudId);
+    hud.sendMessageToContent("JSTerm:GetEvalObject", message, aCallback);
+  },
+
+  /**
    * Destroy the JSTerm object. Call this method to avoid memory leaks.
    */
   destroy: function JST_destroy()
   {
     this.inputNode.removeEventListener("keypress", this._keyPress, false);
     this.inputNode.removeEventListener("input", this._inputEventHandler, false);
     this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
   },
@@ -5775,50 +5756,16 @@ FirefoxApplicationHooks.prototype = {
  */
 
 ConsoleUtils = {
   /**
    * Flag to turn on and off scrolling.
    */
   scroll: true,
 
-  supString: function ConsoleUtils_supString(aString)
-  {
-    let str = Cc["@mozilla.org/supports-string;1"].
-      createInstance(Ci.nsISupportsString);
-    str.data = aString;
-    return str;
-  },
-
-  /**
-   * Generates a millisecond resolution timestamp.
-   *
-   * @returns integer
-   */
-  timestamp: function ConsoleUtils_timestamp()
-  {
-    return Date.now();
-  },
-
-  /**
-   * Generates a formatted timestamp string for displaying in console messages.
-   *
-   * @param integer [ms] Optional, allows you to specify the timestamp in
-   * milliseconds since the UNIX epoch.
-   * @returns string The timestamp formatted for display.
-   */
-  timestampString: function ConsoleUtils_timestampString(ms)
-  {
-    var d = new Date(ms ? ms : null);
-    let hours = d.getHours(), minutes = d.getMinutes();
-    let seconds = d.getSeconds(), milliseconds = d.getMilliseconds();
-    let parameters = [ hours, minutes, seconds, milliseconds ];
-    return HUDService.getFormatStr("timestampFormat", parameters);
-  },
-
   /**
    * Scrolls a node so that it's visible in its containing XUL "scrollbox"
    * element.
    *
    * @param nsIDOMNode aNode
    *        The node to make visible.
    * @returns void
    */
@@ -5908,18 +5855,20 @@ ConsoleUtils = {
     // 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 && !(aLevel == "dir") ?
-            aBody : aDocument.createTextNode(aBody);
+    if (!(aBody instanceof Ci.nsIDOMNode)) {
+      aBody = aDocument.createTextNode(aLevel == "dir" ?
+                                       aBody.resultString : aBody);
+    }
 
     if (!aBody.nodeType) {
       aBody = aDocument.createTextNode(aBody.toString());
     }
     if (typeof aBody == "string") {
       aBody = aDocument.createTextNode(aBody);
     }
 
@@ -5930,18 +5879,18 @@ ConsoleUtils = {
     let repeatNode = aDocument.createElementNS(XUL_NS, "label");
     repeatNode.setAttribute("value", "1");
     repeatNode.classList.add("webconsole-msg-repeat");
     repeatContainer.appendChild(repeatNode);
 
     // Create the timestamp.
     let timestampNode = aDocument.createElementNS(XUL_NS, "label");
     timestampNode.classList.add("webconsole-timestamp");
-    let timestamp = aTimeStamp || ConsoleUtils.timestamp();
-    let timestampString = ConsoleUtils.timestampString(timestamp);
+    let timestamp = aTimeStamp || Date.now();
+    let timestampString = l10n.timestampString(timestamp);
     timestampNode.setAttribute("value", timestampString);
 
     // Create the source location (e.g. www.example.com:6) that sits on the
     // right side of the message, if applicable.
     let locationNode;
     if (aSourceURL) {
       locationNode = this.createLocationNode(aDocument, aSourceURL,
                                              aSourceLine);
@@ -5981,18 +5930,25 @@ ConsoleUtils = {
       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;
+      let treeView = node.propertyTreeView = new PropertyTreeViewAsync();
+
+      treeView.data = {
+        rootCacheId: body.cacheId,
+        panelCacheId: body.cacheId,
+        remoteObject: body.remoteObject,
+        remoteObjectProvider: body.remoteObjectProvider,
+      };
+
       tree.setAttribute("rows", treeView.rowCount);
     }
     else {
       node.appendChild(bodyNode);
     }
     node.appendChild(repeatContainer);
     if (locationNode) {
       node.appendChild(locationNode);
@@ -6057,17 +6013,17 @@ ConsoleUtils = {
    */
   createLocationNode:
   function ConsoleUtils_createLocationNode(aDocument, aSourceURL,
                                            aSourceLine) {
     let locationNode = aDocument.createElementNS(XUL_NS, "label");
 
     // Create the text, which consists of an abbreviated version of the URL
     // plus an optional line number.
-    let text = ConsoleUtils.abbreviateSourceURL(aSourceURL);
+    let text = WebConsoleUtils.abbreviateSourceURL(aSourceURL);
     if (aSourceLine) {
       text += ":" + aSourceLine;
     }
     locationNode.setAttribute("value", text);
 
     // Style appropriately.
     locationNode.setAttribute("crop", "center");
     locationNode.setAttribute("title", aSourceURL);
@@ -6254,17 +6210,17 @@ ConsoleUtils = {
 
     // Scroll to the new node if it is not filtered, and if the output node is
     // scrolled at the bottom or if the new node is a jsterm input/output
     // message.
     if (!isFiltered && !isRepeated && (scrolledToBottom || isInputOutput)) {
       ConsoleUtils.scrollToVisible(aNode);
     }
 
-    let id = ConsoleUtils.supString(aHUDId);
+    let id = WebConsoleUtils.supportsString(aHUDId);
     let nodeID = aNode.getAttribute("id");
     Services.obs.notifyObservers(id, "web-console-message-created", nodeID);
   },
 
   /**
    * Check if the given output node is scrolled to the bottom.
    *
    * @param nsIDOMNode aOutputNode
@@ -6277,46 +6233,16 @@ ConsoleUtils = {
   {
     let lastNodeHeight = aOutputNode.lastChild ?
                          aOutputNode.lastChild.clientHeight : 0;
     let scrollBox = aOutputNode.scrollBoxObject.element;
 
     return scrollBox.scrollTop + scrollBox.clientHeight >=
            scrollBox.scrollHeight - lastNodeHeight / 2;
   },
-
-  /**
-   * Abbreviates the given source URL so that it can be displayed flush-right
-   * without being too distracting.
-   *
-   * @param string aSourceURL
-   *        The source URL to shorten.
-   * @return string
-   *         The abbreviated form of the source URL.
-   */
-  abbreviateSourceURL: function ConsoleUtils_abbreviateSourceURL(aSourceURL) {
-    // Remove any query parameters.
-    let hookIndex = aSourceURL.indexOf("?");
-    if (hookIndex > -1) {
-      aSourceURL = aSourceURL.substring(0, hookIndex);
-    }
-
-    // Remove a trailing "/".
-    if (aSourceURL[aSourceURL.length - 1] == "/") {
-      aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
-    }
-
-    // Remove all but the last path component.
-    let slashIndex = aSourceURL.lastIndexOf("/");
-    if (slashIndex > -1) {
-      aSourceURL = aSourceURL.substring(slashIndex + 1);
-    }
-
-    return aSourceURL;
-  }
 };
 
 //////////////////////////////////////////////////////////////////////////
 // HeadsUpDisplayUICommands
 //////////////////////////////////////////////////////////////////////////
 
 HeadsUpDisplayUICommands = {
   refreshCommand: function UIC_refreshCommand() {
@@ -6509,41 +6435,38 @@ ConsoleEntry.prototype = {
   }
 };
 
 //////////////////////////////////////////////////////////////////////////
 // HUDWindowObserver
 //////////////////////////////////////////////////////////////////////////
 
 HUDWindowObserver = {
-  QueryInterface: XPCOMUtils.generateQI(
-    [Ci.nsIObserver,]
-  ),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   init: function HWO_init()
   {
-    Services.obs.addObserver(this, "xpcom-shutdown", false);
     Services.obs.addObserver(this, "content-document-global-created", false);
+    Services.obs.addObserver(this, "quit-application-granted", false);
   },
 
   observe: function HWO_observe(aSubject, aTopic, aData)
   {
     if (aTopic == "content-document-global-created") {
       HUDService.windowInitializer(aSubject);
     }
-    else if (aTopic == "xpcom-shutdown") {
-      this.uninit();
+    else if (aTopic == "quit-application-granted") {
+      HUDService.shutdown();
     }
   },
 
   uninit: function HWO_uninit()
   {
     Services.obs.removeObserver(this, "content-document-global-created");
-    Services.obs.removeObserver(this, "xpcom-shutdown");
-    this.initialConsoleCreated = false;
+    Services.obs.removeObserver(this, "quit-application-granted");
   },
 
 };
 
 ///////////////////////////////////////////////////////////////////////////////
 // CommandController
 ///////////////////////////////////////////////////////////////////////////////
 
@@ -6628,54 +6551,16 @@ CommandController.prototype = {
         break;
       case "cmd_selectAll":
         this.selectAll(outputNode);
         break;
     }
   }
 };
 
-///////////////////////////////////////////////////////////////////////////////
-// HUDConsoleObserver
-///////////////////////////////////////////////////////////////////////////////
-
-/**
- * HUDConsoleObserver: Observes nsIConsoleService for global consoleMessages,
- * if a message originates inside a contentWindow we are tracking,
- * then route that message to the HUDService for logging.
- */
-
-HUDConsoleObserver = {
-  QueryInterface: XPCOMUtils.generateQI(
-    [Ci.nsIObserver]
-  ),
-
-  init: function HCO_init()
-  {
-    Services.console.registerListener(this);
-    Services.obs.addObserver(this, "quit-application-granted", false);
-  },
-
-  uninit: function HCO_uninit()
-  {
-    Services.console.unregisterListener(this);
-    Services.obs.removeObserver(this, "quit-application-granted");
-  },
-
-  observe: function HCO_observe(aSubject, aTopic, aData)
-  {
-    if (aTopic == "quit-application-granted") {
-      this.uninit();
-    }
-    else if (aSubject instanceof Ci.nsIScriptError) {
-      HUDService.reportPageError(aSubject);
-    }
-  }
-};
-
 /**
  * A WebProgressListener that listens for location changes, to update HUDService
  * state information on page navigation.
  *
  * @constructor
  * @param string aHudId
  *        The HeadsUpDisplay ID.
  */
@@ -7040,22 +6925,22 @@ GcliTerm.prototype = {
       '    <image class="webconsole-msg-icon"/>' +
       '    <spacer flex="1"/>' +
       '  </vbox>' +
       '  <hbox flex="1" class="gcliterm-msg-body">${output}</hbox>' +
       '  <hbox align="start"><label value="1" class="webconsole-msg-repeat"/></hbox>' +
       '</richlistitem>').firstChild;
 
     let hud = HUDService.getHudReferenceById(this.hudId);
-    let timestamp = ConsoleUtils.timestamp();
+    let timestamp = Date.now();
     template(element, {
       iconContainerStyle: "margin-left=" + (hud.groupDepth * GROUP_INDENT) + "px",
       output: output,
       timestamp: timestamp,
-      timestampString: ConsoleUtils.timestampString(timestamp),
+      timestampString: l10n.timestampString(timestamp),
       clipboardText: output.innerText,
       id: "console-msg-" + HUDService.sequenceId()
     });
 
     ConsoleUtils.setMessageType(element, CATEGORY_OUTPUT, SEVERITY_LOG);
     ConsoleUtils.outputMessageNode(element, this.hudId);
   },
 
@@ -7078,17 +6963,17 @@ GcliTerm.prototype = {
    * Evaluates a string in the sandbox.
    *
    * @param string aString
    *        String to evaluate in the sandbox
    * @return The result of the evaluation
    */
   evalInSandbox: function Gcli_evalInSandbox(aString)
   {
-    let window = unwrap(this.sandbox.window);
+    let window = WebConsoleUtils.unwrap(this.sandbox.window);
     let temp$ = null;
     let temp$$ = null;
 
     // We prefer to execute the page-provided implementations for the $() and
     // $$() functions.
     if (typeof window.$ == "function") {
       temp$ = this.sandbox.$;
       delete this.sandbox.$;
@@ -7136,19 +7021,19 @@ GcliTerm.prototype = {
                     aOptions.clipboardText || undefined);
 
     ConsoleUtils.outputMessageNode(node, this.hudId);
   },
 
   clearOutput: JSTerm.prototype.clearOutput,
   openPropertyPanel: JSTerm.prototype.openPropertyPanel,
 
-  formatResult: JSTerm.prototype.formatResult,
-  getResultType: JSTerm.prototype.getResultType,
-  formatString: JSTerm.prototype.formatString,
+  formatResult: WebConsoleUtils.formatResult,
+  getResultType: WebConsoleUtils.getResultType,
+  formatString: WebConsoleUtils.formatResultString,
 };
 
 /**
  * A fancy version of toString()
  */
 function nameObject(aObj) {
   if (aObj.constructor && aObj.constructor.name) {
     return aObj.constructor.name;
--- a/browser/devtools/webconsole/Makefile.in
+++ b/browser/devtools/webconsole/Makefile.in
@@ -41,18 +41,20 @@ DEPTH		= ../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
 		PropertyPanel.jsm \
+		PropertyPanelAsync.jsm \
 		NetworkHelper.jsm \
 		AutocompletePopup.jsm \
+		WebConsoleUtils.jsm \
 		gcli.jsm \
 		GcliCommands.jsm \
 		GcliTiltCommands.jsm \
 		$(NULL)
 
 EXTRA_PP_JS_MODULES = \
 		HUDService.jsm \
 		$(NULL)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/PropertyPanelAsync.jsm
@@ -0,0 +1,466 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () {
+  let obj = {};
+  Cu.import("resource:///modules/WebConsoleUtils.jsm", obj);
+  return obj.WebConsoleUtils;
+});
+
+var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"];
+
+
+///////////////////////////////////////////////////////////////////////////
+//// PropertyTreeView.
+
+/**
+ * This is an implementation of the nsITreeView interface. For comments on the
+ * interface properties, see the documentation:
+ * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsITreeView
+ */
+var PropertyTreeView = function() {
+  this._rows = [];
+  this._objectCache = {};
+};
+
+PropertyTreeView.prototype = {
+  /**
+   * Stores the visible rows of the tree.
+   * @private
+   */
+  _rows: null,
+
+  /**
+   * Stores the nsITreeBoxObject for this tree.
+   * @private
+   */
+  _treeBox: null,
+
+  /**
+   * Stores cached information about local objects being inspected.
+   * @private
+   */
+  _objectCache: null,
+
+  /**
+   * Use this setter to update the content of the tree.
+   *
+   * @param object aObject
+   *        An object that holds information about the object you want to
+   *        display in the property panel. Object properties:
+   *        - object:
+   *        This is the raw object you want to display. You can only provide
+   *        this object if you want the property panel to work in sync mode.
+   *        - remoteObject:
+   *        An array that holds information on the remote object being
+   *        inspected. Each element in this array describes each property in the
+   *        remote object. See WebConsoleUtils.namesAndValuesOf() for details.
+   *        - rootCacheId:
+   *        The cache ID where the objects referenced in remoteObject are found.
+   *        - panelCacheId:
+   *        The cache ID where any object retrieved by this property panel
+   *        instance should be stored into.
+   *        - remoteObjectProvider:
+   *        A function that is invoked when a new object is needed. This is
+   *        called when the user tries to expand an inspectable property. The
+   *        callback must take four arguments:
+   *          - fromCacheId:
+   *          Tells from where to retrieve the object the user picked (from
+   *          which cache ID).
+   *          - objectId:
+   *          The object ID the user wants.
+   *          - panelCacheId:
+   *          Tells in which cache ID to store the objects referenced by
+   *          objectId so they can be retrieved later.
+   *          - callback:
+   *          The callback function to be invoked when the remote object is
+   *          received. This function takes one argument: the raw message
+   *          received from the Web Console content script.
+   */
+  set data(aData) {
+    let oldLen = this._rows.length;
+
+    this._cleanup();
+
+    if (!aData) {
+      return;
+    }
+
+    if (aData.remoteObject) {
+      this._rootCacheId = aData.rootCacheId;
+      this._panelCacheId = aData.panelCacheId;
+      this._remoteObjectProvider = aData.remoteObjectProvider;
+      this._rows = [].concat(aData.remoteObject);
+      this._updateRemoteObject(this._rows, 0);
+    }
+    else if (aData.object) {
+      this._rows = this._inspectObject(aData.object);
+    }
+    else {
+      throw new Error("First argument must have a .remoteObject or " +
+                      "an .object property!");
+    }
+
+    if (this._treeBox) {
+      this._treeBox.beginUpdateBatch();
+      if (oldLen) {
+        this._treeBox.rowCountChanged(0, -oldLen);
+      }
+      this._treeBox.rowCountChanged(0, this._rows.length);
+      this._treeBox.endUpdateBatch();
+    }
+  },
+
+  /**
+   * Update a remote object so it can be used with the tree view. This method
+   * adds properties to each array element.
+   *
+   * @private
+   * @param array aObject
+   *        The remote object you want prepared for use with the tree view.
+   * @param number aLevel
+   *        The level you want to give to each property in the remote object.
+   */
+  _updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel)
+  {
+    aObject.forEach(function(aElement) {
+      aElement.level = aLevel;
+      aElement.isOpened = false;
+      aElement.children = null;
+    });
+  },
+
+  /**
+   * Inspect a local object.
+   *
+   * @private
+   * @param object aObject
+   *        The object you want to inspect.
+   */
+  _inspectObject: function PTV__inspectObject(aObject)
+  {
+    this._objectCache = {};
+    this._remoteObjectProvider = this._localObjectProvider.bind(this);
+    let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache);
+    this._updateRemoteObject(children, 0);
+    return children;
+  },
+
+  /**
+   * An object provider for when the user inspects local objects (not remote
+   * ones).
+   *
+   * @private
+   * @param string aFromCacheId
+   *        The cache ID from where to retrieve the desired object.
+   * @param string aObjectId
+   *        The ID of the object you want.
+   * @param string aDestCacheId
+   *        The ID of the cache where to store any objects referenced by the
+   *        desired object.
+   * @param function aCallback
+   *        The function you want to receive the object.
+   */
+  _localObjectProvider:
+  function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId,
+                                    aCallback)
+  {
+    let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId],
+                                                  this._objectCache);
+    aCallback({cacheId: aFromCacheId,
+               objectId: aObjectId,
+               object: object,
+               childrenCacheId: aDestCacheId || aFromCacheId,
+    });
+  },
+
+  /** nsITreeView interface implementation **/
+
+  selection: null,
+
+  get rowCount()                     { return this._rows.length; },
+  setTree: function(treeBox)         { this._treeBox = treeBox;  },
+  getCellText: function(idx, column) {
+    let row = this._rows[idx];
+    return row.name + ": " + row.value;
+  },
+  getLevel: function(idx) {
+    return this._rows[idx].level;
+  },
+  isContainer: function(idx) {
+    return !!this._rows[idx].inspectable;
+  },
+  isContainerOpen: function(idx) {
+    return this._rows[idx].isOpened;
+  },
+  isContainerEmpty: function(idx)    { return false; },
+  isSeparator: function(idx)         { return false; },
+  isSorted: function()               { return false; },
+  isEditable: function(idx, column)  { return false; },
+  isSelectable: function(row, col)   { return true; },
+
+  getParentIndex: function(idx)
+  {
+    if (this.getLevel(idx) == 0) {
+      return -1;
+    }
+    for (var t = idx - 1; t >= 0; t--) {
+      if (this.isContainer(t)) {
+        return t;
+      }
+    }
+    return -1;
+  },
+
+  hasNextSibling: function(idx, after)
+  {
+    var thisLevel = this.getLevel(idx);
+    return this._rows.slice(after + 1).some(function (r) r.level == thisLevel);
+  },
+
+  toggleOpenState: function(idx)
+  {
+    let item = this._rows[idx];
+    if (!item.inspectable) {
+      return;
+    }
+
+    if (item.isOpened) {
+      this._treeBox.beginUpdateBatch();
+      item.isOpened = false;
+
+      var thisLevel = item.level;
+      var t = idx + 1, deleteCount = 0;
+      while (t < this._rows.length && this.getLevel(t++) > thisLevel) {
+        deleteCount++;
+      }
+
+      if (deleteCount) {
+        this._rows.splice(idx + 1, deleteCount);
+        this._treeBox.rowCountChanged(idx + 1, -deleteCount);
+      }
+      this._treeBox.invalidateRow(idx);
+      this._treeBox.endUpdateBatch();
+    }
+    else {
+      let levelUpdate = true;
+      let callback = function _onRemoteResponse(aResponse) {
+        this._treeBox.beginUpdateBatch();
+        item.isOpened = true;
+
+        if (levelUpdate) {
+          this._updateRemoteObject(aResponse.object, item.level + 1);
+          item.children = aResponse.object;
+        }
+
+        this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children));
+
+        this._treeBox.rowCountChanged(idx + 1, item.children.length);
+        this._treeBox.invalidateRow(idx);
+        this._treeBox.endUpdateBatch();
+      }.bind(this);
+
+      if (!item.children) {
+        let fromCacheId = item.level > 0 ? this._panelCacheId :
+                                           this._rootCacheId;
+        this._remoteObjectProvider(fromCacheId, item.objectId,
+                                   this._panelCacheId, callback);
+      }
+      else {
+        levelUpdate = false;
+        callback({object: item.children});
+      }
+    }
+  },
+
+  getImageSrc: function(idx, column) { },
+  getProgressMode : function(idx,column) { },
+  getCellValue: function(idx, column) { },
+  cycleHeader: function(col, elem) { },
+  selectionChanged: function() { },
+  cycleCell: function(idx, column) { },
+  performAction: function(action) { },
+  performActionOnCell: function(action, index, column) { },
+  performActionOnRow: function(action, row) { },
+  getRowProperties: function(idx, column, prop) { },
+  getCellProperties: function(idx, column, prop) { },
+  getColumnProperties: function(column, element, prop) { },
+
+  setCellValue: function(row, col, value)               { },
+  setCellText: function(row, col, value)                { },
+  drop: function(index, orientation, dataTransfer)      { },
+  canDrop: function(index, orientation, dataTransfer)   { return false; },
+
+  _cleanup: function PTV__cleanup()
+  {
+    if (this._rows.length) {
+      // Reset the existing _rows children to the initial state.
+      this._updateRemoteObject(this._rows, 0);
+      this._rows = [];
+    }
+
+    delete this._objectCache;
+    delete this._rootCacheId;
+    delete this._panelCacheId;
+    delete this._remoteObjectProvider;
+  },
+};
+
+///////////////////////////////////////////////////////////////////////////
+//// Helper for creating the panel.
+
+/**
+ * Creates a DOMNode and sets all the attributes of aAttributes on the created
+ * element.
+ *
+ * @param nsIDOMDocument aDocument
+ *        Document to create the new DOMNode.
+ * @param string aTag
+ *        Name of the tag for the DOMNode.
+ * @param object aAttributes
+ *        Attributes set on the created DOMNode.
+ * @returns nsIDOMNode
+ */
+function createElement(aDocument, aTag, aAttributes)
+{
+  let node = aDocument.createElement(aTag);
+  for (var attr in aAttributes) {
+    node.setAttribute(attr, aAttributes[attr]);
+  }
+  return node;
+}
+
+/**
+ * Creates a new DOMNode and appends it to aParent.
+ *
+ * @param nsIDOMDocument aDocument
+ *        Document to create the new DOMNode.
+ * @param nsIDOMNode aParent
+ *        A parent node to append the created element.
+ * @param string aTag
+ *        Name of the tag for the DOMNode.
+ * @param object aAttributes
+ *        Attributes set on the created DOMNode.
+ * @returns nsIDOMNode
+ */
+function appendChild(aDocument, aParent, aTag, aAttributes)
+{
+  let node = createElement(aDocument, aTag, aAttributes);
+  aParent.appendChild(node);
+  return node;
+}
+
+///////////////////////////////////////////////////////////////////////////
+//// PropertyPanel
+
+/**
+ * Creates a new PropertyPanel.
+ *
+ * @see PropertyTreeView
+ * @param nsIDOMNode aParent
+ *        Parent node to append the created panel to.
+ * @param string aTitle
+ *        Title for the panel.
+ * @param string aObject
+ *        Object to display in the tree. For details about this object please
+ *        see the PropertyTreeView.data property in this file.
+ * @param array of objects aButtons
+ *        Array with buttons to display at the bottom of the panel.
+ */
+function PropertyPanel(aParent, aTitle, aObject, aButtons)
+{
+  let document = aParent.ownerDocument;
+
+  // Create the underlying panel
+  this.panel = createElement(document, "panel", {
+    label: aTitle,
+    titlebar: "normal",
+    noautofocus: "true",
+    noautohide: "true",
+    close: "true",
+  });
+
+  // Create the tree.
+  let tree = this.tree = createElement(document, "tree", {
+    flex: 1,
+    hidecolumnpicker: "true"
+  });
+
+  let treecols = document.createElement("treecols");
+  appendChild(document, treecols, "treecol", {
+    primary: "true",
+    flex: 1,
+    hideheader: "true",
+    ignoreincolumnpicker: "true"
+  });
+  tree.appendChild(treecols);
+
+  tree.appendChild(document.createElement("treechildren"));
+  this.panel.appendChild(tree);
+
+  // Create the footer.
+  let footer = createElement(document, "hbox", { align: "end" });
+  appendChild(document, footer, "spacer", { flex: 1 });
+
+  // The footer can have butttons.
+  let self = this;
+  if (aButtons) {
+    aButtons.forEach(function(button) {
+      let buttonNode = appendChild(document, footer, "button", {
+        label: button.label,
+        accesskey: button.accesskey || "",
+        class: button.class || "",
+      });
+      buttonNode.addEventListener("command", button.oncommand, false);
+    });
+  }
+
+  appendChild(document, footer, "resizer", { dir: "bottomend" });
+  this.panel.appendChild(footer);
+
+  aParent.appendChild(this.panel);
+
+  // Create the treeView object.
+  this.treeView = new PropertyTreeView();
+  this.treeView.data = aObject;
+
+  // Set the treeView object on the tree view. This has to be done *after* the
+  // panel is shown. This is because the tree binding must be attached first.
+  this.panel.addEventListener("popupshown", function onPopupShow()
+  {
+    self.panel.removeEventListener("popupshown", onPopupShow, false);
+    self.tree.view = self.treeView;
+  }, false);
+
+  this.panel.addEventListener("popuphidden", function onPopupHide()
+  {
+    self.panel.removeEventListener("popuphidden", onPopupHide, false);
+    self.destroy();
+  }, false);
+}
+
+/**
+ * Destroy the PropertyPanel. This closes the panel and removes it from the
+ * browser DOM.
+ */
+PropertyPanel.prototype.destroy = function PP_destroy()
+{
+  this.treeView.data = null;
+  this.panel.parentNode.removeChild(this.panel);
+  this.treeView = null;
+  this.panel = null;
+  this.tree = null;
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/WebConsoleUtils.jsm
@@ -0,0 +1,706 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var EXPORTED_SYMBOLS = ["WebConsoleUtils"];
+
+const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
+
+const TYPES = { OBJECT: 0,
+                FUNCTION: 1,
+                ARRAY: 2,
+                OTHER: 3,
+                ITERATOR: 4,
+                GETTER: 5,
+                GENERATOR: 6,
+                STRING: 7
+              };
+
+var gObjectId = 0;
+
+var WebConsoleUtils = {
+  TYPES: TYPES,
+
+  /**
+   * Convenience function to unwrap a wrapped object.
+   *
+   * @param aObject the object to unwrap.
+   * @return aObject unwrapped.
+   */
+  unwrap: function WCU_unwrap(aObject)
+  {
+    try {
+      return XPCNativeWrapper.unwrap(aObject);
+    }
+    catch (ex) {
+      return aObject;
+    }
+  },
+
+  /**
+   * Wrap a string in an nsISupportsString object.
+   *
+   * @param string aString
+   * @return nsISupportsString
+   */
+  supportsString: function WCU_supportsString(aString)
+  {
+    let str = Cc["@mozilla.org/supports-string;1"].
+              createInstance(Ci.nsISupportsString);
+    str.data = aString;
+    return str;
+  },
+
+  /**
+   * Clone an object.
+   *
+   * @param object aObject
+   *        The object you want cloned.
+   * @param boolean aRecursive
+   *        Tells if you want to dig deeper into the object, to clone
+   *        recursively.
+   * @param function [aFilter]
+   *        Optional, filter function, called for every property. Three
+   *        arguments are passed: key, value and object. Return true if the
+   *        property should be added to the cloned object. Return false to skip
+   *        the property.
+   * @return object
+   *         The cloned object.
+   */
+  cloneObject: function WCU_cloneObject(aObject, aRecursive, aFilter)
+  {
+    if (typeof aObject != "object") {
+      return aObject;
+    }
+
+    let temp;
+
+    if (Array.isArray(aObject)) {
+      temp = [];
+      Array.forEach(aObject, function(aValue, aIndex) {
+        if (!aFilter || aFilter(aIndex, aValue, aObject)) {
+          temp.push(aRecursive ? WCU_cloneObject(aValue) : aValue);
+        }
+      });
+    }
+    else {
+      temp = {};
+      for (let key in aObject) {
+        let value = aObject[key];
+        if (aObject.hasOwnProperty(key) &&
+            (!aFilter || aFilter(key, value, aObject))) {
+          temp[key] = aRecursive ? WCU_cloneObject(value) : value;
+        }
+      }
+    }
+
+    return temp;
+  },
+
+  /**
+   * Gets the ID of the inner window of this DOM window.
+   *
+   * @param nsIDOMWindow aWindow
+   * @return integer
+   *         Inner ID for the given aWindow.
+   */
+  getInnerWindowId: function WCU_getInnerWindowId(aWindow)
+  {
+    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+           getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+  },
+
+  /**
+   * Gets the ID of the outer window of this DOM window.
+   *
+   * @param nsIDOMWindow aWindow
+   * @return integer
+   *         Outer ID for the given aWindow.
+   */
+  getOuterWindowId: function WCU_getOuterWindowId(aWindow)
+  {
+    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+           getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+  },
+
+  /**
+   * Gets the window that has the given outer ID.
+   *
+   * @param integer aOuterId
+   * @param nsIDOMWindow [aHintWindow]
+   *        Optional, the window object used to QueryInterface to
+   *        nsIDOMWindowUtils. If this is not given,
+   *        Services.wm.getMostRecentWindow() is used.
+   * @return nsIDOMWindow|null
+   *         The window object with the given outer ID.
+   */
+  getWindowByOuterId: function WCU_getWindowByOuterId(aOuterId, aHintWindow)
+  {
+    let someWindow = aHintWindow || Services.wm.getMostRecentWindow(null);
+    let content = null;
+
+    if (someWindow) {
+      let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+                                   getInterface(Ci.nsIDOMWindowUtils);
+      content = windowUtils.getOuterWindowWithId(aOuterId);
+    }
+
+    return content;
+  },
+
+  /**
+   * Gets the window that has the given inner ID.
+   *
+   * @param integer aInnerId
+   * @param nsIDOMWindow [aHintWindow]
+   *        Optional, the window object used to QueryInterface to
+   *        nsIDOMWindowUtils. If this is not given,
+   *        Services.wm.getMostRecentWindow() is used.
+   * @return nsIDOMWindow|null
+   *         The window object with the given inner ID.
+   */
+  getWindowByInnerId: function WCU_getWindowByInnerId(aInnerId, aHintWindow)
+  {
+    let someWindow = aHintWindow || Services.wm.getMostRecentWindow(null);
+    let content = null;
+
+    if (someWindow) {
+      let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+                                   getInterface(Ci.nsIDOMWindowUtils);
+      content = windowUtils.getInnerWindowWithId(aInnerId);
+    }
+
+    return content;
+  },
+
+  /**
+   * Abbreviates the given source URL so that it can be displayed flush-right
+   * without being too distracting.
+   *
+   * @param string aSourceURL
+   *        The source URL to shorten.
+   * @return string
+   *         The abbreviated form of the source URL.
+   */
+  abbreviateSourceURL: function WCU_abbreviateSourceURL(aSourceURL)
+  {
+    // Remove any query parameters.
+    let hookIndex = aSourceURL.indexOf("?");
+    if (hookIndex > -1) {
+      aSourceURL = aSourceURL.substring(0, hookIndex);
+    }
+
+    // Remove a trailing "/".
+    if (aSourceURL[aSourceURL.length - 1] == "/") {
+      aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
+    }
+
+    // Remove all but the last path component.
+    let slashIndex = aSourceURL.lastIndexOf("/");
+    if (slashIndex > -1) {
+      aSourceURL = aSourceURL.substring(slashIndex + 1);
+    }
+
+    return aSourceURL;
+  },
+
+  /**
+   * Format the jsterm execution result based on its type.
+   *
+   * @param mixed aResult
+   *        The evaluation result object you want displayed.
+   * @return string
+   *         The string that can be displayed.
+   */
+  formatResult: function WCU_formatResult(aResult)
+  {
+    let output = "";
+    let type = this.getResultType(aResult);
+
+    switch (type) {
+      case "string":
+        output = this.formatResultString(aResult);
+        break;
+      case "boolean":
+      case "date":
+      case "error":
+      case "number":
+      case "regexp":
+        output = aResult.toString();
+        break;
+      case "null":
+      case "undefined":
+        output = type;
+        break;
+      default:
+        if (aResult.toSource) {
+          try {
+            output = aResult.toSource();
+          } catch (ex) { }
+        }
+        if (!output || output == "({})") {
+          output = aResult.toString();
+        }
+        break;
+    }
+
+    return output;
+  },
+
+  /**
+   * Format a string for output.
+   *
+   * @param string aString
+   *        The string you want to display.
+   * @return string
+   *         The string that can be displayed.
+   */
+  formatResultString: function WCU_formatResultString(aString)
+  {
+    function isControlCode(c) {
+      // See http://en.wikipedia.org/wiki/C0_and_C1_control_codes
+      // C0 is 0x00-0x1F, C1 is 0x80-0x9F (inclusive).
+      // We also include DEL (U+007F) and NBSP (U+00A0), which are not strictly
+      // in C1 but border it.
+      return (c <= 0x1F) || (0x7F <= c && c <= 0xA0);
+    }
+
+    function replaceFn(aMatch, aType, aHex) {
+      // Leave control codes escaped, but unescape the rest of the characters.
+      let c = parseInt(aHex, 16);
+      return isControlCode(c) ? aMatch : String.fromCharCode(c);
+    }
+
+    let output = uneval(aString).replace(/\\(x)([0-9a-fA-F]{2})/g, replaceFn)
+                 .replace(/\\(u)([0-9a-fA-F]{4})/g, replaceFn);
+
+    return output;
+  },
+
+  /**
+   * Determine if an object can be inspected or not.
+   *
+   * @param mixed aObject
+   *        The object you want to check if it can be inspected.
+   * @return boolean
+   *         True if the object is inspectable or false otherwise.
+   */
+  isObjectInspectable: function WCU_isObjectInspectable(aObject)
+  {
+    let isEnumerable = false;
+
+    // Skip Iterators and Generators.
+    if (this.isIteratorOrGenerator(aObject)) {
+      return false;
+    }
+
+    try {
+      for (let p in aObject) {
+        isEnumerable = true;
+        break;
+      }
+    }
+    catch (ex) {
+      // Proxy objects can lack an enumerable method.
+    }
+
+    return isEnumerable && typeof(aObject) != "string";
+  },
+
+  /**
+   * Determine the type of the jsterm execution result.
+   *
+   * @param mixed aResult
+   *        The evaluation result object you want to check.
+   * @return string
+   *         Constructor name or type: string, number, boolean, regexp, date,
+   *         function, object, null, undefined...
+   */
+  getResultType: function WCU_getResultType(aResult)
+  {
+    let type = aResult === null ? "null" : typeof aResult;
+    if (type == "object" && aResult.constructor && aResult.constructor.name) {
+      type = aResult.constructor.name;
+    }
+
+    return type.toLowerCase();
+  },
+
+  /**
+   * Figures out the type of aObject and the string to display as the object
+   * value.
+   *
+   * @see TYPES
+   * @param object aObject
+   *        The object to operate on.
+   * @return object
+   *         An object of the form:
+   *         {
+   *           type: TYPES.OBJECT || TYPES.FUNCTION || ...
+   *           display: string for displaying the object
+   *         }
+   */
+  presentableValueFor: function WCU_presentableValueFor(aObject)
+  {
+    let type = this.getResultType(aObject);
+    let presentable;
+
+    switch (type) {
+      case "undefined":
+      case "null":
+        return {
+          type: TYPES.OTHER,
+          display: type
+        };
+
+      case "array":
+        return {
+          type: TYPES.ARRAY,
+          display: "Array"
+        };
+
+      case "string":
+        return {
+          type: TYPES.STRING,
+          display: "\"" + aObject + "\""
+        };
+
+      case "date":
+      case "regexp":
+      case "number":
+      case "boolean":
+        return {
+          type: TYPES.OTHER,
+          display: aObject.toString()
+        };
+
+      case "iterator":
+        return {
+          type: TYPES.ITERATOR,
+          display: "Iterator"
+        };
+
+      case "function":
+        presentable = aObject.toString();
+        return {
+          type: TYPES.FUNCTION,
+          display: presentable.substring(0, presentable.indexOf(')') + 1)
+        };
+
+      default:
+        presentable = String(aObject);
+        let m = /^\[object (\S+)\]/.exec(presentable);
+
+        try {
+          if (type == "object" && typeof aObject.next == "function" &&
+              m && m[1] == "Generator") {
+            return {
+              type: TYPES.GENERATOR,
+              display: m[1]
+            };
+          }
+        }
+        catch (ex) {
+          // window.history.next throws in the typeof check above.
+          return {
+            type: TYPES.OBJECT,
+            display: m ? m[1] : "Object"
+          };
+        }
+
+        if (type == "object" && typeof aObject.__iterator__ == "function") {
+          return {
+            type: TYPES.ITERATOR,
+            display: "Iterator"
+          };
+        }
+
+        return {
+          type: TYPES.OBJECT,
+          display: m ? m[1] : "Object"
+        };
+    }
+  },
+
+  /**
+   * Tells if the given function is native or not.
+   *
+   * @param function aFunction
+   *        The function you want to check if it is native or not.
+   * @return boolean
+   *         True if the given function is native, false otherwise.
+   */
+  isNativeFunction: function WCU_isNativeFunction(aFunction)
+  {
+    return typeof aFunction == "function" && !("prototype" in aFunction);
+  },
+
+  /**
+   * Tells if the given property of the provided object is a non-native getter or
+   * not.
+   *
+   * @param object aObject
+   *        The object that contains the property.
+   * @param string aProp
+   *        The property you want to check if it is a getter or not.
+   * @return boolean
+   *         True if the given property is a getter, false otherwise.
+   */
+  isNonNativeGetter: function WCU_isNonNativeGetter(aObject, aProp)
+  {
+    if (typeof aObject != "object") {
+      return false;
+    }
+    let desc;
+    while (aObject) {
+      try {
+        if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) {
+          break;
+        }
+      }
+      catch (ex) {
+        // Native getters throw here. See bug 520882.
+        if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" ||
+            ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") {
+          return false;
+        }
+        throw ex;
+      }
+      aObject = Object.getPrototypeOf(aObject);
+    }
+    if (desc && desc.get && !this.isNativeFunction(desc.get)) {
+      return true;
+    }
+    return false;
+  },
+
+  /**
+   * Get an array that describes the properties of the given object.
+   *
+   * @param object aObject
+   *        The object to get the properties from.
+   * @param object aObjectCache
+   *        Optional object cache where to store references to properties of
+   *        aObject that are inspectable. See this.isObjectInspectable().
+   * @return array
+   *         An array that describes each property from the given object. Each
+   *         array element is an object (a property descriptor). Each property
+   *         descriptor has the following properties:
+   *         - name - property name
+   *         - value - a presentable property value representation (see
+   *                   this.presentableValueFor())
+   *         - type - value type (see this.presentableValueFor())
+   *         - inspectable - tells if the property value holds further
+   *                         properties (see this.isObjectInspectable()).
+   *         - objectId - optional, available only if aObjectCache is given and
+   *         if |inspectable| is true. You can do
+   *         aObjectCache[propertyDescriptor.objectId] to get the actual object
+   *         referenced by the property of aObject.
+   */
+  namesAndValuesOf: function WCU_namesAndValuesOf(aObject, aObjectCache)
+  {
+    let pairs = [];
+    let value, presentable;
+
+    let isDOMDocument = aObject instanceof Ci.nsIDOMDocument;
+
+    for (let propName in aObject) {
+      // See bug 632275: skip deprecated width and height properties.
+      if (isDOMDocument && (propName == "width" || propName == "height")) {
+        continue;
+      }
+
+      // Also skip non-native getters.
+      if (this.isNonNativeGetter(aObject, propName)) {
+        value = "";
+        presentable = {type: TYPES.GETTER, display: "Getter"};
+      }
+      else {
+        value = aObject[propName];
+        presentable = this.presentableValueFor(value);
+      }
+
+      let pair = {};
+      pair.name = propName;
+      pair.value = presentable.display;
+      pair.inspectable = false;
+      pair.type = presentable.type;
+
+      switch (presentable.type) {
+        case TYPES.GETTER:
+        case TYPES.ITERATOR:
+        case TYPES.GENERATOR:
+        case TYPES.STRING:
+          break;
+        default:
+          try {
+            for (let p in value) {
+              pair.inspectable = true;
+              break;
+            }
+          }
+          catch (ex) { }
+          break;
+      }
+
+      // Store the inspectable object.
+      if (pair.inspectable && aObjectCache) {
+        pair.objectId = ++gObjectId;
+        aObjectCache[pair.objectId] = value;
+      }
+
+      pairs.push(pair);
+    }
+
+    pairs.sort(function(a, b)
+    {
+      // Convert the pair.name to a number for later sorting.
+      let aNumber = parseFloat(a.name);
+      let bNumber = parseFloat(b.name);
+
+      // Sort numbers.
+      if (!isNaN(aNumber) && isNaN(bNumber)) {
+        return -1;
+      }
+      else if (isNaN(aNumber) && !isNaN(bNumber)) {
+        return 1;
+      }
+      else if (!isNaN(aNumber) && !isNaN(bNumber)) {
+        return aNumber - bNumber;
+      }
+      // Sort string.
+      else if (a.name < b.name) {
+        return -1;
+      }
+      else if (a.name > b.name) {
+        return 1;
+      }
+      else {
+        return 0;
+      }
+    });
+
+    return pairs;
+  },
+
+  /**
+   * Check if the given object is an iterator or a generator.
+   *
+   * @param object aObject
+   *        The object you want to check.
+   * @return boolean
+   *         True if the given object is an iterator or a generator, otherwise
+   *         false is returned.
+   */
+  isIteratorOrGenerator: function WCU_isIteratorOrGenerator(aObject)
+  {
+    if (aObject === null) {
+      return false;
+    }
+
+    if (typeof aObject == "object") {
+      if (typeof aObject.__iterator__ == "function" ||
+          aObject.constructor && aObject.constructor.name == "Iterator") {
+        return true;
+      }
+
+      try {
+        let str = aObject.toString();
+        if (typeof aObject.next == "function" &&
+            str.indexOf("[object Generator") == 0) {
+          return true;
+        }
+      }
+      catch (ex) {
+        // window.history.next throws in the typeof check above.
+        return false;
+      }
+    }
+
+    return false;
+  },
+};
+
+//////////////////////////////////////////////////////////////////////////
+// Localization
+//////////////////////////////////////////////////////////////////////////
+
+WebConsoleUtils.l10n = {
+  /**
+   * Generates a formatted timestamp string for displaying in console messages.
+   *
+   * @param integer [aMilliseconds]
+   *        Optional, allows you to specify the timestamp in milliseconds since
+   *        the UNIX epoch.
+   * @return string
+   *         The timestamp formatted for display.
+   */
+  timestampString: function WCU_l10n_timestampString(aMilliseconds)
+  {
+    let d = new Date(aMilliseconds ? aMilliseconds : null);
+    let hours = d.getHours(), minutes = d.getMinutes();
+    let seconds = d.getSeconds(), milliseconds = d.getMilliseconds();
+    let parameters = [hours, minutes, seconds, milliseconds];
+    return this.getFormatStr("timestampFormat", parameters);
+  },
+
+  /**
+   * Retrieve a localized string.
+   *
+   * @param string aName
+   *        The string name you want from the Web Console string bundle.
+   * @return string
+   *         The localized string.
+   */
+  getStr: function WCU_l10n_getStr(aName)
+  {
+    let result;
+    try {
+      result = this.stringBundle.GetStringFromName(aName);
+    }
+    catch (ex) {
+      Cu.reportError("Failed to get string: " + aName);
+      throw ex;
+    }
+    return result;
+  },
+
+  /**
+   * Retrieve a localized string formatted with values coming from the given
+   * array.
+   *
+   * @param string aName
+   *        The string name you want from the Web Console string bundle.
+   * @param array aArray
+   *        The array of values you want in the formatted string.
+   * @return string
+   *         The formatted local string.
+   */
+  getFormatStr: function WCU_l10n_getFormatStr(aName, aArray)
+  {
+    let result;
+    try {
+      result = this.stringBundle.formatStringFromName(aName, aArray, aArray.length);
+    }
+    catch (ex) {
+      Cu.reportError("Failed to format string: " + aName);
+      throw ex;
+    }
+    return result;
+  },
+};
+
+XPCOMUtils.defineLazyGetter(WebConsoleUtils.l10n, "stringBundle", function() {
+  return Services.strings.createBundle(STRINGS_URI);
+});
--- a/browser/devtools/webconsole/test/browser_cached_messages.js
+++ b/browser/devtools/webconsole/test/browser_cached_messages.js
@@ -39,45 +39,43 @@
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-webconsole-error-observer.html";
 
 function test()
 {
   waitForExplicitFinish();
 
   expectUncaughtException();
 
-  gBrowser.selectedTab = gBrowser.addTab(TEST_URI);
-
+  addTab(TEST_URI);
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
     testOpenUI(true);
   }, true);
 }
 
 function testOpenUI(aTestReopen)
 {
   // test to see if the messages are
   // displayed when the console UI is opened
 
-  HUDService.activateHUDForContext(gBrowser.selectedTab);
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.getHudReferenceById(hudId);
+  openConsole(null, function(hud) {
+    testLogEntry(hud.outputNode, "log Bazzle",
+                 "Find a console log entry from before console UI is opened",
+                 false, null);
 
-  testLogEntry(hud.outputNode, "log Bazzle",
-               "Find a console log entry from before console UI is opened",
-               false, null);
+    testLogEntry(hud.outputNode, "error Bazzle",
+                 "Find a console error entry from before console UI is opened",
+                 false, null);
 
-  testLogEntry(hud.outputNode, "error Bazzle",
-               "Find a console error entry from before console UI is opened",
-               false, null);
+    testLogEntry(hud.outputNode, "bazBug611032", "Found the JavaScript error");
+    testLogEntry(hud.outputNode, "cssColorBug611032", "Found the CSS error");
 
-  testLogEntry(hud.outputNode, "bazBug611032", "Found the JavaScript error");
-  testLogEntry(hud.outputNode, "cssColorBug611032", "Found the CSS error");
-
-  HUDService.deactivateHUDForContext(gBrowser.selectedTab);
+    HUDService.deactivateHUDForContext(gBrowser.selectedTab);
 
-  if (aTestReopen) {
-    HUDService.deactivateHUDForContext(gBrowser.selectedTab);
-    executeSoon(testOpenUI);
-  } else {
-    executeSoon(finish);
-  }
+    if (aTestReopen) {
+      HUDService.deactivateHUDForContext(gBrowser.selectedTab);
+      executeSoon(testOpenUI);
+    }
+    else {
+      executeSoon(finish);
+    }
+  });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_abbreviate_source_url.js
@@ -10,12 +10,12 @@ function test() {
   testAbbreviation("http://example.com/foo/bar/", "bar");
   testAbbreviation("http://example.com/foo.js?bar=1&baz=2", "foo.js");
   testAbbreviation("http://example.com/foo/?bar=1&baz=2", "foo");
 
   finishTest();
 }
 
 function testAbbreviation(aFullURL, aAbbreviatedURL) {
-  is(ConsoleUtils.abbreviateSourceURL(aFullURL), aAbbreviatedURL, aFullURL +
+  is(WebConsoleUtils.abbreviateSourceURL(aFullURL), aAbbreviatedURL, aFullURL +
      " is abbreviated to " + aAbbreviatedURL);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580030_errors_after_page_reload.js
@@ -46,43 +46,36 @@ function test() {
   expectUncaughtException();
   addTab(TEST_URI);
   browser.addEventListener("load", onLoad, true);
 }
 
 // see bug 580030: the error handler fails silently after page reload.
 // https://bugzilla.mozilla.org/show_bug.cgi?id=580030
 function onLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+  browser.removeEventListener(aEvent.type, onLoad, true);
 
-  openConsole();
-
-  browser.addEventListener("load", testErrorsAfterPageReload, true);
-  executeSoon(function() {
+  openConsole(null, function(hud) {
+    hud.jsterm.clearOutput();
+    browser.addEventListener("load", testErrorsAfterPageReload, true);
     content.location.reload();
   });
 }
 
 function testErrorsAfterPageReload(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+  browser.removeEventListener(aEvent.type, testErrorsAfterPageReload, true);
 
   // dispatch a click event to the button in the test page and listen for
   // errors.
 
   Services.console.registerListener(consoleObserver);
 
-  var button = content.document.querySelector("button").wrappedJSObject;
-  var clickEvent = content.document.createEvent("MouseEvents");
-  clickEvent.initMouseEvent("click", true, true,
-    content, 0, 0, 0, 0, 0, false, false,
-    false, false, 0, null);
-
-  executeSoon(function() {
-    button.dispatchEvent(clickEvent);
-  });
+  let button = content.document.querySelector("button").wrappedJSObject;
+  ok(button, "button found");
+  EventUtils.sendMouseEvent({type: "click"}, button, content);
 }
 
 var consoleObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   observe: function test_observe(aMessage)
   {
     // Ignore errors we don't care about.
@@ -90,16 +83,20 @@ var consoleObserver = {
       aMessage.category != "content javascript") {
       return;
     }
 
     Services.console.unregisterListener(this);
 
     let outputNode = HUDService.getHudByWindow(content).outputNode;
 
-    executeSoon(function() {
-      let msg = "Found the error message after page reload";
-      testLogEntry(outputNode, "fooBazBaz", msg);
-      finishTest();
+    waitForSuccess({
+      name: "error message after page reload",
+      validatorFn: function()
+      {
+        return outputNode.textContent.indexOf("fooBazBaz") > -1;
+      },
+      successFn: finishTest,
+      failureFn: finishTest,
     });
   }
 };
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js
@@ -16,17 +16,17 @@ function test() {
   addTab(TEST_URI);
   browser.addEventListener("DOMContentLoaded", testTimestamp, false);
 
   function testTimestamp()
   {
     browser.removeEventListener("DOMContentLoaded", testTimestamp, false);
     const TEST_TIMESTAMP = 12345678;
     let date = new Date(TEST_TIMESTAMP);
-    let localizedString = ConsoleUtils.timestampString(TEST_TIMESTAMP);
+    let localizedString = WebConsoleUtils.l10n.timestampString(TEST_TIMESTAMP);
     isnot(localizedString.indexOf(date.getHours()), -1, "the localized " +
           "timestamp contains the hours");
     isnot(localizedString.indexOf(date.getMinutes()), -1, "the localized " +
           "timestamp contains the minutes");
     isnot(localizedString.indexOf(date.getSeconds()), -1, "the localized " +
           "timestamp contains the seconds");
     isnot(localizedString.indexOf(date.getMilliseconds()), -1, "the localized " +
           "timestamp contains the milliseconds");
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
@@ -47,24 +47,24 @@ function test() {
   expectUncaughtException();
   addTab(TEST_DUPLICATE_ERROR_URI);
   browser.addEventListener("DOMContentLoaded", testDuplicateErrors, false);
 }
 
 function testDuplicateErrors() {
   browser.removeEventListener("DOMContentLoaded", testDuplicateErrors,
                               false);
-  openConsole();
-
-  HUDService.getHudByWindow(content).jsterm.clearOutput();
+  openConsole(null, function(hud) {
+    hud.jsterm.clearOutput();
 
-  Services.console.registerListener(consoleObserver);
+    Services.console.registerListener(consoleObserver);
 
-  expectUncaughtException();
-  content.location.reload();
+    expectUncaughtException();
+    content.location.reload();
+  });
 }
 
 var consoleObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   observe: function (aMessage)
   {
     // we ignore errors we don't care about
@@ -72,23 +72,32 @@ var consoleObserver = {
       aMessage.category != "content javascript") {
       return;
     }
 
     Services.console.unregisterListener(this);
 
     outputNode = HUDService.getHudByWindow(content).outputNode;
 
-    executeSoon(function () {
-      var text = outputNode.textContent;
-      var error1pos = text.indexOf("fooDuplicateError1");
-      ok(error1pos > -1, "found fooDuplicateError1");
-      if (error1pos > -1) {
-        ok(text.indexOf("fooDuplicateError1", error1pos + 1) == -1,
-          "no duplicate for fooDuplicateError1");
-      }
+    waitForSuccess({
+      name: "fooDuplicateError1 error displayed",
+      validatorFn: function()
+      {
+        return outputNode.textContent.indexOf("fooDuplicateError1") > -1;
+      },
+      successFn: function()
+      {
+        let text = outputNode.textContent;
+        let error1pos = text.indexOf("fooDuplicateError1");
+        ok(error1pos > -1, "found fooDuplicateError1");
+        if (error1pos > -1) {
+          ok(text.indexOf("fooDuplicateError1", error1pos + 1) == -1,
+            "no duplicate for fooDuplicateError1");
+        }
 
-      findLogEntry("test-duplicate-error.html");
+        findLogEntry("test-duplicate-error.html");
 
-      finishTest();
+        finishTest();
+      },
+      failureFn: finishTest,
     });
   }
 };
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585237_line_limit.js
@@ -7,60 +7,118 @@
  *  Patrick Walton <pcwalton@mozilla.com>
  *  Mihai Șucan <mihai.sucan@gmail.com>
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the Web Console limits the number of lines displayed according to
 // the user's preferences.
 
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+const TEST_URI = "data:text/html;charset=utf8,test for bug 585237";
+let hud, testDriver;
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testLineLimit,
-                                            false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, function(aHud) {
+      hud = aHud;
+      testDriver = testGen();
+      testNext();
+    });
+  }, true);
 }
 
-function testLineLimit() {
-  browser.removeEventListener("DOMContentLoaded",testLineLimit, false);
+function testNext() {
+  testDriver.next();
+}
 
-  openConsole();
+function testGen() {
+  let console = content.console;
+  outputNode = hud.outputNode;
 
-  let console = browser.contentWindow.wrappedJSObject.console;
-  outputNode = HUDService.getHudByWindow(content).outputNode;
+  hud.jsterm.clearOutput();
 
   let prefBranch = Services.prefs.getBranch("devtools.hud.loglimit.");
   prefBranch.setIntPref("console", 20);
 
-  for (let i = 0; i < 20; i++) {
+  for (let i = 0; i < 30; i++) {
     console.log("foo #" + i); // must change message to prevent repeats
   }
 
+  waitForSuccess({
+    name: "20 console.log messages displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("foo #29") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   is(countMessageNodes(), 20, "there are 20 message nodes in the output " +
      "when the log limit is set to 20");
 
-  console.log("bar");
+  console.log("bar bug585237");
+
+  waitForSuccess({
+    name: "another console.log message displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bar bug585237") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   is(countMessageNodes(), 20, "there are still 20 message nodes in the " +
      "output when adding one more");
 
   prefBranch.setIntPref("console", 30);
   for (let i = 0; i < 20; i++) {
     console.log("boo #" + i); // must change message to prevent repeats
   }
 
+  waitForSuccess({
+    name: "another 20 console.log message displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("boo #19") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   is(countMessageNodes(), 30, "there are 30 message nodes in the output " +
      "when the log limit is set to 30");
 
   prefBranch.setIntPref("console", 0);
   console.log("baz");
-  is(countMessageNodes(), 0, "there are no message nodes in the output when " +
-     "the log limit is set to zero");
+
+  waitForSuccess({
+    name: "clear output",
+    validatorFn: function()
+    {
+      return countMessageNodes() == 0;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
 
   prefBranch.clearUserPref("console");
-  prefBranch = console = outputNode = null;
+  hud = testDriver = prefBranch = console = outputNode = null;
   finishTest();
+
+  yield;
 }
 
 function countMessageNodes() {
   return outputNode.querySelectorAll(".hud-msg-node").length;
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585956_console_trace.js
@@ -41,20 +41,20 @@ const TEST_URI = "http://example.com/bro
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoaded, true);
 }
 
 function tabLoaded() {
   browser.removeEventListener("load", tabLoaded, true);
 
-  openConsole();
-
-  browser.addEventListener("load", tabReloaded, true);
-  content.location.reload();
+  openConsole(null, function() {
+    browser.addEventListener("load", tabReloaded, true);
+    content.location.reload();
+  });
 }
 
 function tabReloaded() {
   browser.removeEventListener("load", tabReloaded, true);
 
   // The expected stack trace object.
   let stacktrace = [
     { filename: TEST_URI, lineNumber: 9, functionName: null, language: 2 },
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js
@@ -7,55 +7,59 @@
  *  Patrick Walton <pcwalton@mozilla.com>
  *
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("load", tabLoaded, true);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
-function tabLoaded() {
-  browser.removeEventListener("load", tabLoaded, true);
-  openConsole();
-
+function consoleOpened(HUD) {
   // See bugs 574036, 586386 and 587617.
-
-  let HUD = HUDService.getHudByWindow(content);
   outputNode = HUD.outputNode;
   let selection = getSelection();
   let jstermInput = HUD.jsterm.inputNode;
   let console = content.wrappedJSObject.console;
   let contentSelection = content.wrappedJSObject.getSelection();
 
-  let make_selection = function () {
-    let controller =
-      top.document.commandDispatcher.
-      getControllerForCommand("cmd_copy");
-    is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
+  HUD.jsterm.clearOutput();
 
-    console.log("Hello world!");
+  let controller = top.document.commandDispatcher.
+                   getControllerForCommand("cmd_copy");
+  is(controller.isCommandEnabled("cmd_copy"), false, "cmd_copy is disabled");
 
-    outputNode.selectedIndex = 0;
-    outputNode.focus();
+  console.log("Hello world! bug587617");
 
-    goUpdateCommand("cmd_copy");
-
-    controller = top.document.commandDispatcher.
-      getControllerForCommand("cmd_copy");
-    is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+  waitForSuccess({
+    name: "console log 'Hello world!' message",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bug587617") > -1;
+    },
+    successFn: function()
+    {
+      outputNode.selectedIndex = 0;
+      outputNode.focus();
 
-    let selectedNode = outputNode.getItemAtIndex(0);
-    waitForClipboard(getExpectedClipboardText(selectedNode), clipboardSetup,
-                     testContextMenuCopy, testContextMenuCopy);
-  };
-
-  make_selection();
+      goUpdateCommand("cmd_copy");
+      controller = top.document.commandDispatcher.
+        getControllerForCommand("cmd_copy");
+      is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
+      let selectedNode = outputNode.getItemAtIndex(0);
+      waitForClipboard(getExpectedClipboardText(selectedNode), clipboardSetup,
+                       testContextMenuCopy, testContextMenuCopy);
+    },
+    failureFn: finishTest,
+  });
 }
 
 // Test that the context menu "Copy" (which has a different code path) works
 // properly as well.
 function testContextMenuCopy() {
   let contextMenuId = outputNode.getAttribute("context");
   let contextMenu = document.getElementById(contextMenuId);
   ok(contextMenu, "the output node has a context menu");
@@ -69,16 +73,16 @@ function testContextMenuCopy() {
   copyItem.dispatchEvent(commandEvent);
 
   let selectedNode = outputNode.getItemAtIndex(0);
   waitForClipboard(getExpectedClipboardText(selectedNode), clipboardSetup,
     finishTest, finishTest);
 }
 
 function getExpectedClipboardText(aItem) {
-  return "[" + ConsoleUtils.timestampString(aItem.timestamp) + "] " +
+  return "[" + WebConsoleUtils.l10n.timestampString(aItem.timestamp) + "] " +
          aItem.clipboardText;
 }
 
 function clipboardSetup() {
   goDoCommand("cmd_copy");
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_588730_text_node_insertion.js
@@ -39,51 +39,46 @@
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that adding text to one of the output labels doesn't cause errors.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testTextNodeInsertion,
-                           false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testTextNodeInsertion);
+  }, true);
 }
 
 // Test for bug 588730: Adding a text node to an existing label element causes
 // warnings
-function testTextNodeInsertion() {
-  browser.removeEventListener("DOMContentLoaded", testTextNodeInsertion,
-                              false);
-  openConsole();
-
-  let outputNode = HUDService.getHudByWindow(content).outputNode;
+function testTextNodeInsertion(hud) {
+  let outputNode = hud.outputNode;
 
   let label = document.createElementNS(
     "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "label");
   outputNode.appendChild(label);
 
   let error = false;
   let listener = {
     observe: function(aMessage) {
       let messageText = aMessage.message;
       if (messageText.indexOf("JavaScript Warning") !== -1) {
         error = true;
       }
     }
   };
 
-  let nsIConsoleServiceClass = Cc["@mozilla.org/consoleservice;1"];
-  let nsIConsoleService =
-    nsIConsoleServiceClass.getService(Ci.nsIConsoleService);
-  nsIConsoleService.registerListener(listener);
+  Services.console.registerListener(listener);
 
   // This shouldn't fail.
   label.appendChild(document.createTextNode("foo"));
 
   executeSoon(function() {
-    nsIConsoleService.unregisterListener(listener);
+    Services.console.unregisterListener(listener);
     ok(!error, "no error when adding text nodes as children of labels");
 
     finishTest();
   });
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_593003_iframe_wrong_hud.js
@@ -45,68 +45,51 @@ const TEST_DUMMY_URI = "http://example.c
 let tab1, tab2;
 
 function test() {
   addTab(TEST_URI);
   tab1 = tab;
   browser.addEventListener("load", tab1Loaded, true);
 }
 
-/**
- * Check if a log entry exists in the HUD output node.
- *
- * @param {Element} aOutputNode the HUD output node.
- * @param {string} aMatchString the string you want to check if it exists in the
- * output node.
- * @param {boolean} [aOnlyVisible=false] find only messages that are visible,
- * not hidden by the filter.
- * @param {boolean} [aFailIfFound=false] fail the test if the string is found in
- * the output node.
- */
-
 function tab1Loaded(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-  browser.contentWindow.wrappedJSObject.console.log("FOO");
-  try {
-    openConsole();
-  }
-  catch (ex) {
-    log(ex);
-    log(ex.stack);
-  }
-
-  tab2 = gBrowser.addTab(TEST_DUMMY_URI);
-  gBrowser.selectedTab = tab2;
-  gBrowser.selectedBrowser.addEventListener("load", tab2Loaded, true);
+  browser.removeEventListener(aEvent.type, tab1Loaded, true);
+  content.console.log("FOO");
+  openConsole(null, function() {
+    tab2 = gBrowser.addTab(TEST_DUMMY_URI);
+    gBrowser.selectedTab = tab2;
+    gBrowser.selectedBrowser.addEventListener("load", tab2Loaded, true);
+  });
 }
 
 function tab2Loaded(aEvent) {
-  tab2.linkedBrowser.removeEventListener(aEvent.type, arguments.callee, true);
+  tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true);
 
-  HUDService.activateHUDForContext(gBrowser.selectedTab);
-
-  tab1.linkedBrowser.addEventListener("load", tab1Reloaded, true);
-  tab1.linkedBrowser.contentWindow.location.reload();
+  openConsole(gBrowser.selectedTab, function() {
+    tab1.linkedBrowser.addEventListener("load", tab1Reloaded, true);
+    tab1.linkedBrowser.contentWindow.location.reload();
+  });
 }
 
 function tab1Reloaded(aEvent) {
-  tab1.linkedBrowser.removeEventListener(aEvent.type, arguments.callee, true);
+  tab1.linkedBrowser.removeEventListener(aEvent.type, tab1Reloaded, true);
 
   let hud1 = HUDService.getHudByWindow(tab1.linkedBrowser.contentWindow);
   let outputNode1 = hud1.outputNode;
 
   let msg = "Found the iframe network request in tab1";
   testLogEntry(outputNode1, TEST_IFRAME_URI, msg, true);
 
   let hud2 = HUDService.getHudByWindow(tab2.linkedBrowser.contentWindow);
   let outputNode2 = hud2.outputNode;
 
   isnot(outputNode1, outputNode2,
         "the two HUD outputNodes must be different");
 
   msg = "Didn't find the iframe network request in tab2";
   testLogEntry(outputNode2, TEST_IFRAME_URI, msg, true, true);
 
-  HUDService.deactivateHUDForContext(tab2);
-  gBrowser.removeTab(tab2);
-
-  finishTest();
+  closeConsole(tab2, function() {
+    gBrowser.removeTab(tab2);
+    tab1 = tab2 = null;
+    executeSoon(finishTest);
+  });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595350_multiple_windows_and_tabs.js
@@ -23,64 +23,87 @@ function test() {
   addTabs(win1);
 
   // Open a new window.
   win2 = OpenBrowserWindow();
   win2.addEventListener("load", onWindowLoad, true);
 }
 
 function onWindowLoad(aEvent) {
-  win2.removeEventListener(aEvent.type, arguments.callee, true);
+  win2.removeEventListener(aEvent.type, onWindowLoad, true);
 
   // Add two tabs in the new window.
   addTabs(win2);
 }
 
 function addTabs(aWindow) {
   for (let i = 0; i < 2; i++) {
     let tab = aWindow.gBrowser.addTab(TEST_URI);
     openTabs.push(tab);
 
-    tab.linkedBrowser.addEventListener("load", function(aEvent) {
-      tab.linkedBrowser.removeEventListener(aEvent.type, arguments.callee,
-        true);
+    tab.linkedBrowser.addEventListener("load", function onLoad(aEvent) {
+      tab.linkedBrowser.removeEventListener(aEvent.type, onLoad, true);
 
       loadedTabCount++;
       if (loadedTabCount >= 4) {
-        executeSoon(performTest);
+        executeSoon(openConsoles);
       }
     }, true);
   }
 }
 
-function performTest() {
+function openConsoles() {
   // open the Web Console for each of the four tabs and log a message.
+  let consolesOpen = 0;
   for (let i = 0; i < openTabs.length; i++) {
     let tab = openTabs[i];
-    HUDService.activateHUDForContext(tab);
-    let hudId = HUDService.getHudIdByWindow(tab.linkedBrowser.contentWindow);
-    ok(hudId, "HUD is open for tab " + i);
-    let HUD = HUDService.hudReferences[hudId];
-    HUD.console.log("message for tab " + i);
+    openConsole(tab, function(index, hud) {
+      ok(hud, "HUD is open for tab " + index);
+      hud.console.log("message for tab " + index);
+      consolesOpen++;
+    }.bind(null, i));
   }
 
-  let displays = Object.keys(HUDService.hudReferences);
-  is(displays.length, 4, "four displays found");
+  waitForSuccess({
+    name: "4 web consoles opened",
+    validatorFn: function()
+    {
+      return consolesOpen == 4;
+    },
+    successFn: closeConsoles,
+    failureFn: closeConsoles,
+  });
+}
+
+function closeConsoles() {
+  let consolesClosed = 0;
+
+  function onWebConsoleClose(aSubject, aTopic) {
+    if (aTopic == "web-console-destroyed") {
+      consolesClosed++;
+    }
+  }
+
+  Services.obs.addObserver(onWebConsoleClose, "web-console-destroyed", false);
 
   win2.close();
 
-  executeSoon(function() {
-    win1.gBrowser.removeTab(openTabs[0]);
-    win1.gBrowser.removeTab(openTabs[1]);
+  win1.gBrowser.removeTab(openTabs[0]);
+  win1.gBrowser.removeTab(openTabs[1]);
+
+  openTabs = win1 = win2 = null;
 
-    executeSoon(function() {
-      displays = Object.keys(HUDService.hudReferences);
-      is(displays.length, 0, "no displays found");
-      ok(!HUDService.storage, "no storage found");
-      ok(!HUDService.httpObserver, "no httpObserver found");
+  function onTimeout() {
+    Services.obs.removeObserver(onWebConsoleClose, "web-console-destroyed");
+    executeSoon(finishTest);
+  }
 
-      displays = openTabs = win1 = win2 = null;
-
-      finishTest();
-    });
+  waitForSuccess({
+    name: "4 web consoles closed",
+    validatorFn: function()
+    {
+      return consolesClosed == 4;
+    },
+    successFn: onTimeout,
+    failureFn: onTimeout,
   });
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595934_message_categories.js
@@ -84,116 +84,119 @@ const TESTS = [
     category: "CSS Parser",
     matchString: "foobarCanvasCssParser",
   },
   { // #14
     file: "test-bug-595934-image.html",
     category: "Image",
     matchString: "corrupt",
   },
-  /* Disabled until bug 675221 lands.
-  { // #7
+  { // #15
     file: "test-bug-595934-workers.html",
     category: "Web Worker",
     matchString: "fooBarWorker",
-  },*/
+    expectError: true,
+  },
 ];
 
 let pos = -1;
 
 let foundCategory = false;
 let foundText = false;
 let output = null;
 let jsterm = null;
+let testEnded = false;
 
 let TestObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   observe: function test_observe(aSubject)
   {
-    if (!(aSubject instanceof Ci.nsIScriptError)) {
+    if (testEnded || !(aSubject instanceof Ci.nsIScriptError)) {
       return;
     }
 
     is(aSubject.category, TESTS[pos].category,
       "test #" + pos + ": error category '" + TESTS[pos].category + "'");
 
     if (aSubject.category == TESTS[pos].category) {
       foundCategory = true;
       if (foundText) {
         executeSoon(testNext);
       }
     }
     else {
       ok(false, aSubject.sourceName + ':' + aSubject.lineNumber + '; ' +
                 aSubject.errorMessage);
-      executeSoon(finish);
+      testEnded = true;
+      executeSoon(finishTest);
     }
   }
 };
 
-function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-
-  openConsole();
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function consoleOpened(hud) {
   output = hud.outputNode;
   output.addEventListener("DOMNodeInserted", onDOMNodeInserted, false);
   jsterm = hud.jsterm;
 
   Services.console.registerListener(TestObserver);
 
+  registerCleanupFunction(testEnd);
+
   executeSoon(testNext);
 }
 
 function testNext() {
   jsterm.clearOutput();
   foundCategory = false;
   foundText = false;
 
   pos++;
   if (pos < TESTS.length) {
     let test = TESTS[pos];
     let testLocation = TESTS_PATH + test.file;
     if (test.onload) {
-      browser.addEventListener("load", function(aEvent) {
+      browser.addEventListener("load", function onLoad(aEvent) {
         if (content.location.href == testLocation) {
-          browser.removeEventListener(aEvent.type, arguments.callee, true);
+          browser.removeEventListener(aEvent.type, onLoad, true);
           test.onload(aEvent);
         }
       }, true);
     }
 
+    if (test.expectError) {
+      expectUncaughtException();
+    }
+
     content.location = testLocation;
   }
   else {
-    executeSoon(finish);
+    testEnded = true;
+    executeSoon(finishTest);
   }
 }
 
 function testEnd() {
   Services.console.unregisterListener(TestObserver);
   output.removeEventListener("DOMNodeInserted", onDOMNodeInserted, false);
-  output = jsterm = null;
-  finishTest();
+  TestObserver = output = jsterm = null;
 }
 
 function onDOMNodeInserted(aEvent) {
   let textContent = output.textContent;
   foundText = textContent.indexOf(TESTS[pos].matchString) > -1;
   if (foundText) {
     ok(foundText, "test #" + pos + ": message found '" + TESTS[pos].matchString + "'");
   }
 
   if (foundCategory) {
     executeSoon(testNext);
   }
 }
 
 function test() {
-  registerCleanupFunction(testEnd);
-
   addTab("data:text/html;charset=utf-8,Web Console test for bug 595934 - message categories coverage.");
-  browser.addEventListener("load", tabLoad, true);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597136_external_script_errors.js
@@ -16,30 +16,24 @@ function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoaded, true);
 }
 
 function tabLoaded(aEvent) {
   browser.removeEventListener("load", tabLoaded, true);
   openConsole();
 
-  browser.addEventListener("load", contentLoaded, true);
-  content.location.reload();
-}
+  let button = content.document.querySelector("button");
+  let outputNode = HUDService.getHudByWindow(content).outputNode;
 
-function contentLoaded(aEvent) {
-  browser.removeEventListener("load", contentLoaded, true);
-
-  let button = content.document.querySelector("button");
   expectUncaughtException();
   EventUtils.sendMouseEvent({ type: "click" }, button, content);
-  executeSoon(buttonClicked);
+
+  waitForSuccess({
+    name: "external script error message",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bogus is not defined") > -1;
+    },
+    successFn: finishTest,
+    failureFn: finishTest,
+  });
 }
-
-function buttonClicked() {
-  let outputNode = HUDService.getHudByWindow(content).outputNode;
-
-  let msg = "the error from the external script was logged";
-  testLogEntry(outputNode, "bogus", msg);
-
-  finishTest();
-}
-
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597756_reopen_closed_tab.js
@@ -8,55 +8,53 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-597756-reopen-closed-tab.html";
 
 let newTabIsOpen = false;
 
 function tabLoaded(aEvent) {
-  gBrowser.selectedBrowser.removeEventListener(aEvent.type, arguments.callee, true);
-
-  HUDService.activateHUDForContext(gBrowser.selectedTab);
+  gBrowser.selectedBrowser.removeEventListener(aEvent.type, tabLoaded, true);
 
-  gBrowser.selectedBrowser.addEventListener("load", tabReloaded, true);
-  expectUncaughtException();
-  content.location.reload();
+  openConsole(gBrowser.selectedTab, function() {
+    gBrowser.selectedBrowser.addEventListener("load", tabReloaded, true);
+    expectUncaughtException();
+    content.location.reload();
+  });
 }
 
 function tabReloaded(aEvent) {
-  gBrowser.selectedBrowser.removeEventListener(aEvent.type, arguments.callee, true);
+  gBrowser.selectedBrowser.removeEventListener(aEvent.type, tabReloaded, true);
 
   let hudId = HUDService.getHudIdByWindow(content);
   let HUD = HUDService.hudReferences[hudId];
   ok(HUD, "Web Console is open");
 
   isnot(HUD.outputNode.textContent.indexOf("fooBug597756_error"), -1,
     "error message must be in console output");
 
   executeSoon(function() {
     if (newTabIsOpen) {
-      testEnd();
+      executeSoon(finishTest);
       return;
     }
 
-    let newTab = gBrowser.addTab();
-    gBrowser.removeCurrentTab();
-    gBrowser.selectedTab = newTab;
+    closeConsole(gBrowser.selectedTab, function() {
+      gBrowser.removeCurrentTab();
+
+      let newTab = gBrowser.addTab();
+      gBrowser.selectedTab = newTab;
 
-    newTabIsOpen = true;
-    gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
-    expectUncaughtException();
-    content.location = TEST_URI;
+      newTabIsOpen = true;
+      gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
+      expectUncaughtException();
+      content.location = TEST_URI;
+    });
   });
 }
 
-function testEnd() {
-  gBrowser.removeCurrentTab();
-  executeSoon(finishTest);
-}
-
 function test() {
   expectUncaughtException();
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoaded, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_598357_jsterm_output.js
@@ -87,96 +87,122 @@ let inputValues = [
   [true, "({a:'b', c:'d', e:1, f:'2'})", '({a:"b", c:"d", e:1, f:"2"})',
     "[object Object",
     '({a:"b", c:"d", e:1, f:"2"})'],
 ];
 
 let eventHandlers = [];
 let popupShown = [];
 let HUD;
+let testDriver;
 
 function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+  browser.removeEventListener(aEvent.type, tabLoad, true);
 
   waitForFocus(function () {
-    openConsole();
-
-    let hudId = HUDService.getHudIdByWindow(content);
-    HUD = HUDService.hudReferences[hudId];
-
-    executeSoon(testNext);
+    openConsole(null, function(aHud) {
+      HUD = aHud;
+      testNext();
+    });
   }, content);
 }
 
+function subtestNext() {
+  testDriver.next();
+}
+
 function testNext() {
-  let cpos = ++pos;
-  if (cpos == inputValues.length) {
-    if (popupShown.length == inputValues.length) {
-      executeSoon(testEnd);
-    }
+  pos++;
+  if (pos == inputValues.length) {
+    testEnd();
     return;
   }
 
+  testDriver = testGen();
+  testDriver.next();
+}
+
+function testGen() {
+  let cpos = pos;
+
   let showsPropertyPanel = inputValues[cpos][0];
   let inputValue = inputValues[cpos][1];
   let expectedOutput = inputValues[cpos][2];
 
   let printOutput = inputValues[cpos].length >= 4 ?
     inputValues[cpos][3] : expectedOutput;
 
   let consoleOutput = inputValues[cpos].length >= 5 ?
     inputValues[cpos][4] : printOutput;
 
   let consoleTest = inputValues[cpos][5] || inputValue;
 
   HUD.jsterm.clearOutput();
 
+  // Test the console.log() output.
+
   // Ugly but it does the job.
   with (content) {
     eval("HUD.console.log(" + consoleTest + ")");
   }
 
-  let outputItem = HUD.outputNode.
-    querySelector(".hud-log:last-child");
+  waitForSuccess({
+    name: "console.log message for test #" + cpos,
+    validatorFn: function()
+    {
+      return HUD.outputNode.querySelector(".hud-log");
+    },
+    successFn: subtestNext,
+    failureFn: testNext,
+  });
+
+  yield;
+
+  let outputItem = HUD.outputNode.querySelector(".hud-log:last-child");
   ok(outputItem,
     "found the window.console output line for inputValues[" + cpos + "]");
   ok(outputItem.textContent.indexOf(consoleOutput) > -1,
     "console API output is correct for inputValues[" + cpos + "]");
 
   HUD.jsterm.clearOutput();
 
+  // Test jsterm print() output.
+
   HUD.jsterm.setInputValue("print(" + inputValue + ")");
   HUD.jsterm.execute();
 
   outputItem = HUD.outputNode.querySelector(".webconsole-msg-output:" +
                                             "last-child");
   ok(outputItem,
     "found the jsterm print() output line for inputValues[" + cpos + "]");
   ok(outputItem.textContent.indexOf(printOutput) > -1,
     "jsterm print() output is correct for inputValues[" + cpos + "]");
 
+  // Test jsterm execution output.
+
   let eventHandlerID = eventHandlers.length + 1;
 
   let propertyPanelShown = function(aEvent) {
     let label = aEvent.target.getAttribute("label");
     if (!label || label.indexOf(inputValue) == -1) {
       return;
     }
 
-    document.removeEventListener(aEvent.type, arguments.callee, false);
+    document.removeEventListener(aEvent.type, propertyPanelShown, false);
     eventHandlers[eventHandlerID] = null;
 
     ok(showsPropertyPanel,
       "the property panel shown for inputValues[" + cpos + "]");
 
     aEvent.target.hidePopup();
 
     popupShown[cpos] = true;
-    if (popupShown.length == inputValues.length) {
-      executeSoon(testEnd);
+
+    if (showsPropertyPanel) {
+      subtestNext();
     }
   };
 
   document.addEventListener("popupshown", propertyPanelShown, false);
 
   eventHandlers.push(propertyPanelShown);
 
   HUD.jsterm.clearOutput();
@@ -187,25 +213,28 @@ function testNext() {
                                             "last-child");
   ok(outputItem, "found the jsterm output line for inputValues[" + cpos + "]");
   ok(outputItem.textContent.indexOf(expectedOutput) > -1,
     "jsterm output is correct for inputValues[" + cpos + "]");
 
   let messageBody = outputItem.querySelector(".webconsole-msg-body");
   ok(messageBody, "we have the message body for inputValues[" + cpos + "]");
 
-  messageBody.addEventListener("click", function(aEvent) {
-    this.removeEventListener(aEvent.type, arguments.callee, false);
-    executeSoon(testNext);
-  }, false);
-
   // Send the mousedown, mouseup and click events to check if the property
   // panel opens.
   EventUtils.sendMouseEvent({ type: "mousedown" }, messageBody, window);
   EventUtils.sendMouseEvent({ type: "click" }, messageBody, window);
+
+  if (showsPropertyPanel) {
+    yield; // wait for the panel to open if we need to.
+  }
+
+  testNext();
+
+  yield;
 }
 
 function testEnd() {
   if (testEnded) {
     return;
   }
 
   testEnded = true;
@@ -217,16 +246,17 @@ function testEnd() {
   }
 
   for (let i = 0; i < inputValues.length; i++) {
     if (inputValues[i][0] && !popupShown[i]) {
       ok(false, "the property panel failed to show for inputValues[" + i + "]");
     }
   }
 
+  testDriver = null;
   executeSoon(finishTest);
 }
 
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoad, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601352_scroll.js
@@ -3,23 +3,18 @@
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  *
  * Contributor(s):
  *  Mihai Șucan <mihai.sucan@gmail.com>
  *
  * ***** END LICENSE BLOCK ***** */
 
-function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-
-  openConsole();
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let HUD = HUDService.hudReferences[hudId];
+function consoleOpened(HUD) {
+  HUD.jsterm.clearOutput();
 
   let longMessage = "";
   for (let i = 0; i < 50; i++) {
     longMessage += "LongNonwrappingMessage";
   }
 
   for (let i = 0; i < 50; i++) {
     HUD.console.log("test message " + i);
@@ -28,17 +23,17 @@ function tabLoad(aEvent) {
   HUD.console.log(longMessage);
 
   for (let i = 0; i < 50; i++) {
     HUD.console.log("test message " + i);
   }
 
   HUD.jsterm.execute("1+1");
 
-  executeSoon(function() {
+  function performTest() {
     let scrollBox = HUD.outputNode.scrollBoxObject.element;
     isnot(scrollBox.scrollTop, 0, "scroll location is not at the top");
 
     let node = HUD.outputNode.getItemAtIndex(HUD.outputNode.itemCount - 1);
     let rectNode = node.getBoundingClientRect();
     let rectOutput = HUD.outputNode.getBoundingClientRect();
 
     // Visible scroll viewport.
@@ -49,16 +44,29 @@ function tabLoad(aEvent) {
 
     // Bottom position of the last message node, relative to the outputNode.
     let bottom = rectNode.bottom - rectOutput.top;
 
     ok(top >= 0 && Math.floor(bottom) <= height + 1,
        "last message is visible");
 
     finishTest();
+  };
+
+  waitForSuccess({
+    name: "console output displayed",
+    validatorFn: function()
+    {
+      return HUD.outputNode.itemCount == 103;
+    },
+    successFn: performTest,
+    failureFn: finishTest,
   });
 }
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 601352");
-  browser.addEventListener("load", tabLoad, true);
+  browser.addEventListener("load", function tabLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, tabLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
@@ -11,31 +11,30 @@
 let menuitems = [], menupopups = [], huds = [], tabs = [];
 
 function test()
 {
   // open tab 1
   addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 1");
   tabs.push(tab);
 
-  browser.addEventListener("load", function(aEvent) {
-    browser.removeEventListener(aEvent.type, arguments.callee, true);
-
-    openConsole();
+  browser.addEventListener("load", function onLoad1(aEvent) {
+    browser.removeEventListener(aEvent.type, onLoad1, true);
 
-    // open tab 2
-    addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 2");
-    tabs.push(tab);
+    openConsole(null, function() {
+      // open tab 2
+      addTab("data:text/html;charset=utf-8,Web Console test for bug 602572: log bodies checkbox. tab 2");
+      tabs.push(tab);
 
-    browser.addEventListener("load", function(aEvent) {
-      browser.removeEventListener(aEvent.type, arguments.callee, true);
+      browser.addEventListener("load", function onLoad2(aEvent) {
+        browser.removeEventListener(aEvent.type, onLoad2, true);
 
-      openConsole();
-      executeSoon(startTest);
-    }, true);
+        openConsole(null, startTest);
+      }, true);
+    });
   }, true);
 }
 
 function startTest()
 {
   // Find the relevant elements in the Web Console of tab 2.
   let win2 = tabs[1].linkedBrowser.contentWindow;
   let hudId2 = HUDService.getHudIdByWindow(win2);
@@ -47,44 +46,44 @@ function startTest()
 
   // Open the context menu from tab 2.
   menupopups[1].addEventListener("popupshown", onpopupshown2, false);
   menupopups[1].openPopup(huds[1].outputNode, "overlap", 10, 10, true, false);
 }
 
 function onpopupshown2(aEvent)
 {
-  menupopups[1].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[1].removeEventListener(aEvent.type, onpopupshown2, false);
 
   // By default bodies are not logged.
   isnot(menuitems[1].getAttribute("checked"), "true",
         "menuitems[1] is not checked");
 
   ok(!HUDService.saveRequestAndResponseBodies, "bodies are not logged");
 
   // Enable body logging.
   HUDService.saveRequestAndResponseBodies = true;
 
-  menupopups[1].addEventListener("popuphidden", function(aEvent) {
-    menupopups[1].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) {
+    menupopups[1].removeEventListener(aEvent.type, _onhidden, false);
 
     // Reopen the context menu.
     menupopups[1].addEventListener("popupshown", onpopupshown2b, false);
     menupopups[1].openPopup(huds[1].outputNode, "overlap", 11, 11, true, false);
   }, false);
   menupopups[1].hidePopup();
 }
 
 function onpopupshown2b(aEvent)
 {
-  menupopups[1].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[1].removeEventListener(aEvent.type, onpopupshown2b, false);
   is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked");
 
-  menupopups[1].addEventListener("popuphidden", function(aEvent) {
-    menupopups[1].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) {
+    menupopups[1].removeEventListener(aEvent.type, _onhidden, false);
 
     // Switch to tab 1 and open the Web Console context menu from there.
     gBrowser.selectedTab = tabs[0];
     waitForFocus(function() {
       // Find the relevant elements in the Web Console of tab 1.
       let win1 = tabs[0].linkedBrowser.contentWindow;
       let hudId1 = HUDService.getHudIdByWindow(win1);
       huds[0] = HUDService.hudReferences[hudId1];
@@ -97,48 +96,49 @@ function onpopupshown2b(aEvent)
       menupopups[0].openPopup(huds[0].outputNode, "overlap", 12, 12, true, false);
     }, tabs[0].linkedBrowser.contentWindow);
   }, false);
   menupopups[1].hidePopup();
 }
 
 function onpopupshown1(aEvent)
 {
-  menupopups[0].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[0].removeEventListener(aEvent.type, onpopupshown1, false);
 
   // The menuitem checkbox must be in sync with the other tabs.
   is(menuitems[0].getAttribute("checked"), "true", "menuitems[0] is checked");
 
   // Disable body logging.
   HUDService.saveRequestAndResponseBodies = false;
 
   // Close the menu, and switch back to tab 2.
-  menupopups[0].addEventListener("popuphidden", function(aEvent) {
-    menupopups[0].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[0].addEventListener("popuphidden", function _onhidden(aEvent) {
+    menupopups[0].removeEventListener(aEvent.type, _onhidden, false);
 
     gBrowser.selectedTab = tabs[1];
     waitForFocus(function() {
       // Reopen the context menu from tab 2.
       menupopups[1].addEventListener("popupshown", onpopupshown2c, false);
       menupopups[1].openPopup(huds[1].outputNode, "overlap", 13, 13, true, false);
     }, tabs[1].linkedBrowser.contentWindow);
   }, false);
   menupopups[0].hidePopup();
 }
 
 function onpopupshown2c(aEvent)
 {
-  menupopups[1].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[1].removeEventListener(aEvent.type, onpopupshown2c, false);
 
   isnot(menuitems[1].getAttribute("checked"), "true",
         "menuitems[1] is not checked");
 
-  menupopups[1].addEventListener("popuphidden", function(aEvent) {
-    menupopups[1].removeEventListener(aEvent.type, arguments.callee, false);
+  menupopups[1].addEventListener("popuphidden", function _onhidden(aEvent) {
+    menupopups[1].removeEventListener(aEvent.type, _onhidden, false);
 
     // Done!
     huds = menuitems = menupopups = tabs = null;
-    HUDService.deactivateHUDForContext(gBrowser.selectedTab);
-    gBrowser.removeCurrentTab();
-    executeSoon(finishTest);
+    closeConsole(gBrowser.selectedTab, function() {
+      gBrowser.removeCurrentTab();
+      executeSoon(finishTest);
+    });
   }, false);
   menupopups[1].hidePopup();
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_603750_websocket.js
@@ -34,38 +34,43 @@ let TestObserver = {
     }
     else {
       lastWindowId = aSubject.outerWindowID;
     }
   }
 };
 
 function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+  browser.removeEventListener(aEvent.type, tabLoad, true);
 
   openConsole();
 
   let hudId = HUDService.getHudIdByWindow(content);
   hud = HUDService.hudReferences[hudId];
 
   Services.console.registerListener(TestObserver);
 
   content.location = TEST_URI;
 }
 
 function performTest() {
-  let textContent = hud.outputNode.textContent;
-  isnot(textContent.indexOf("ws://0.0.0.0:81"), -1,
-        "first error message found");
-  isnot(textContent.indexOf("ws://0.0.0.0:82"), -1,
-        "second error message found");
-
   Services.console.unregisterListener(TestObserver);
   Services.prefs.setBoolPref(pref_ws, oldPref_ws);
-  finishTest();
+
+  waitForSuccess({
+    name: "websocket error messages displayed",
+    validatorFn: function()
+    {
+      let textContent = hud.outputNode.textContent;
+      return textContent.indexOf("ws://0.0.0.0:81") > -1 &&
+             textContent.indexOf("ws://0.0.0.0:82") > -1;
+    },
+    successFn: finishTest,
+    failureFn: finishTest,
+  });
 }
 
 function test() {
   oldPref_ws = Services.prefs.getBoolPref(pref_ws);
 
   Services.prefs.setBoolPref(pref_ws, true);
 
   addTab("data:text/html;charset=utf-8,Web Console test for bug 603750: Web Socket errors");
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_611795.js
@@ -2,51 +2,76 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = 'data:text/html;charset=utf-8,<div style="-moz-opacity:0;">test repeated' +
                  ' css warnings</div><p style="-moz-opacity:0">hi</p>';
 
 function onContentLoaded()
 {
-  browser.removeEventListener("load", arguments.callee, true);
+  browser.removeEventListener("load", onContentLoaded, true);
 
   let HUD = HUDService.getHudByWindow(content);
   let jsterm = HUD.jsterm;
   let outputNode = HUD.outputNode;
 
-  let msg = "The unknown CSS property warning is displayed only once";
-  let node = outputNode.firstChild;
+  let cssWarning = "Unknown property '-moz-opacity'.  Declaration dropped.";
+
+  waitForSuccess({
+    name: "2 repeated CSS warnings",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf(cssWarning) > -1;
+    },
+    successFn: function()
+    {
+      let msg = "The unknown CSS property warning is displayed only once";
+      let node = outputNode.firstChild;
 
-  is(node.childNodes[2].textContent, "Unknown property '-moz-opacity'.  Declaration dropped.", "correct node")
-  is(node.childNodes[3].firstChild.getAttribute("value"), 2, msg);
+      is(node.childNodes[2].textContent, cssWarning, "correct node");
+      is(node.childNodes[3].firstChild.getAttribute("value"), 2, msg);
+
+      testConsoleLogRepeats();
+    },
+    failureFn: finishTest,
+  });
+}
+
+function testConsoleLogRepeats()
+{
+  let HUD = HUDService.getHudByWindow(content);
+  let jsterm = HUD.jsterm;
+  let outputNode = HUD.outputNode;
 
   jsterm.clearOutput();
 
   jsterm.setInputValue("for (let i = 0; i < 10; ++i) console.log('this is a line of reasonably long text that I will use to verify that the repeated text node is of an appropriate size.');");
   jsterm.execute();
 
-  let msg = "The console output is repeated 10 times";
-  let node = outputNode.querySelector(".webconsole-msg-console");
-  is(node.childNodes[3].firstChild.getAttribute("value"), 10, msg);
-
-  jsterm.clearOutput();
-  finishTest();
+  waitForSuccess({
+    name: "10 repeated console.log messages",
+    validatorFn: function()
+    {
+      let node = outputNode.querySelector(".webconsole-msg-console");
+      return node && node.childNodes[3].firstChild.getAttribute("value") == 10;
+    },
+    successFn: finishTest,
+    failureFn: finishTest,
+  });
 }
 
 /**
  * Unit test for bug 611795:
  * Repeated CSS messages get collapsed into one.
  */
 function test()
 {
   addTab(TEST_URI);
-  browser.addEventListener("load", function() {
-    browser.removeEventListener("load", arguments.callee, true);
-
-    openConsole();
-    // Clear cached messages that are shown once the Web Console opens.
-    HUDService.getHudByWindow(content).jsterm.clearOutput(true);
-
-    browser.addEventListener("load", onContentLoaded, true);
-    content.location.reload();
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, function(aHud) {
+      // Clear cached messages that are shown once the Web Console opens.
+      aHud.jsterm.clearOutput(true);
+      browser.addEventListener("load", onContentLoaded, true);
+      content.location.reload();
+    });
   }, true);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js
@@ -64,11 +64,11 @@ function tabLoaded() {
                getControllerForCommand("cmd_copy");
   is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
 
   waitForClipboard(getExpectedClipboardText(HUD.outputNode.selectedItem),
     clipboard_setup, clipboard_copy_done, clipboard_copy_done);
 }
 
 function getExpectedClipboardText(aItem) {
-  return "[" + ConsoleUtils.timestampString(aItem.timestamp) + "] " +
+  return "[" + WebConsoleUtils.l10n.timestampString(aItem.timestamp) + "] " +
          aItem.clipboardText;
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
@@ -2,60 +2,103 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  *
  * Contributor(s):
  *   Mihai Șucan <mihai.sucan@gmail.com>
  */
 
-function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+let hud, testDriver;
 
-  openConsole();
+function testNext() {
+  testDriver.next();
+}
 
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function testGen() {
+  hud.jsterm.clearOutput();
   let outputNode = hud.outputNode;
   let scrollBox = outputNode.scrollBoxObject.element;
 
   for (let i = 0; i < 150; i++) {
     hud.console.log("test message " + i);
   }
 
+  waitForSuccess({
+    name: "150 console.log messages displayed",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-log").length == 150;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   let oldScrollTop = scrollBox.scrollTop;
   ok(oldScrollTop > 0, "scroll location is not at the top");
 
   // scroll to the first node
   outputNode.focus();
 
   EventUtils.synthesizeKey("VK_HOME", {});
 
   let topPosition = scrollBox.scrollTop;
   isnot(topPosition, oldScrollTop, "scroll location updated (moved to top)");
 
-  executeSoon(function() {
-    // add a message and make sure scroll doesn't change
-    hud.console.log("test message 150");
+  // add a message and make sure scroll doesn't change
+  hud.console.log("test message 150");
 
-    is(scrollBox.scrollTop, topPosition, "scroll location is still at the top");
+  waitForSuccess({
+    name: "console.log message no. 151 displayed",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-log").length == 151;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
 
-    // scroll back to the bottom
-    outputNode.lastChild.focus();
-    EventUtils.synthesizeKey("VK_END", {});
+  yield;
+
+  is(scrollBox.scrollTop, topPosition, "scroll location is still at the top");
+
+  // scroll back to the bottom
+  outputNode.lastChild.focus();
+  EventUtils.synthesizeKey("VK_END", {});
 
-    executeSoon(function() {
-      oldScrollTop = outputNode.scrollTop;
+  oldScrollTop = outputNode.scrollTop;
 
-      hud.console.log("test message 151");
+  hud.console.log("test message 151");
 
-      isnot(scrollBox.scrollTop, oldScrollTop,
-            "scroll location updated (moved to bottom)");
+  waitForSuccess({
+    name: "console.log message no. 152 displayed",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-log").length == 152;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
 
-      finishTest();
-    });
-  });
+  yield;
+
+  isnot(scrollBox.scrollTop, oldScrollTop,
+        "scroll location updated (moved to bottom)");
+
+  hud = testDriver = null;
+  finishTest();
+  
+  yield;
 }
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 613642: remember scroll location");
-  browser.addEventListener("load", tabLoad, true);
+  browser.addEventListener("load", function tabLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, tabLoad, true);
+    openConsole(null, function(aHud) {
+      hud = aHud;
+      testDriver = testGen();
+      testDriver.next();
+    });
+  }, true);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_prune_scroll.js
@@ -2,34 +2,48 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  *
  * Contributor(s):
  *   Mihai Șucan <mihai.sucan@gmail.com>
  */
 
-function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+let hud, testDriver;
 
-  openConsole();
+function testNext() {
+  testDriver.next();
+}
 
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function testGen() {
+  hud.jsterm.clearOutput();
+
   let outputNode = hud.outputNode;
   let oldPref = Services.prefs.getIntPref("devtools.hud.loglimit.console");
 
   Services.prefs.setIntPref("devtools.hud.loglimit.console", 140);
   let scrollBoxElement = outputNode.scrollBoxObject.element;
   let boxObject = outputNode.scrollBoxObject;
 
   for (let i = 0; i < 150; i++) {
     hud.console.log("test message " + i);
   }
 
+  waitForSuccess({
+    name: "150 console.log messages displayed",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-log").length == 140;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   let oldScrollTop = scrollBoxElement.scrollTop;
   ok(oldScrollTop > 0, "scroll location is not at the top");
 
   let firstNode = outputNode.firstChild;
   ok(firstNode, "found the first message");
 
   let msgNode = outputNode.querySelectorAll("richlistitem")[80];
   ok(msgNode, "found the 80th message");
@@ -40,24 +54,48 @@ function tabLoad(aEvent) {
   isnot(scrollBoxElement.scrollTop, oldScrollTop,
         "scroll location updated (scrolled to message)");
 
   oldScrollTop = scrollBoxElement.scrollTop;
 
   // add a message
   hud.console.log("hello world");
 
+  waitForSuccess({
+    name: "console.log message #151 displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("hello world") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   // Scroll location needs to change, because one message is also removed, and
   // we need to scroll a bit towards the top, to keep the current view in sync.
   isnot(scrollBoxElement.scrollTop, oldScrollTop,
         "scroll location updated (added a message)");
 
   isnot(outputNode.firstChild, firstNode,
         "first message removed");
 
   Services.prefs.setIntPref("devtools.hud.loglimit.console", oldPref);
+
+  hud = testDriver = null;
   finishTest();
+
+  yield;
 }
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 613642: maintain scroll with pruning of old messages");
-  browser.addEventListener("load", tabLoad, true);
+  browser.addEventListener("load", function tabLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, tabLoad, true);
+
+    openConsole(null, function(aHud) {
+      hud = aHud;
+      testDriver = testGen();
+      testDriver.next();
+    });
+  }, true);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_614793_jsterm_scroll.js
@@ -2,42 +2,52 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  *
  * Contributor(s):
  *   Mihai Șucan <mihai.sucan@gmail.com>
  */
 
-function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+function consoleOpened(hud) {
+  hud.jsterm.clearOutput();
 
-  openConsole();
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
   let outputNode = hud.outputNode;
   let boxObject = outputNode.scrollBoxObject.element;
 
   for (let i = 0; i < 150; i++) {
     hud.console.log("test message " + i);
   }
 
-  let oldScrollTop = boxObject.scrollTop;
-  ok(oldScrollTop > 0, "scroll location is not at the top");
-
-  hud.jsterm.execute("'hello world'");
-
-  isnot(boxObject.scrollTop, oldScrollTop, "scroll location updated");
+  waitForSuccess({
+    name: "console.log messages displayed",
+    validatorFn: function()
+    {
+      return outputNode.itemCount == 150;
+    },
+    successFn: function()
+    {
+      let oldScrollTop = boxObject.scrollTop;
+      ok(oldScrollTop > 0, "scroll location is not at the top");
 
-  oldScrollTop = boxObject.scrollTop;
-  outputNode.scrollBoxObject.ensureElementIsVisible(outputNode.lastChild);
+      hud.jsterm.execute("'hello world'");
+
+      isnot(boxObject.scrollTop, oldScrollTop, "scroll location updated");
+
+      oldScrollTop = boxObject.scrollTop;
+      outputNode.scrollBoxObject.ensureElementIsVisible(outputNode.lastChild);
 
-  is(boxObject.scrollTop, oldScrollTop, "scroll location is the same");
+      is(boxObject.scrollTop, oldScrollTop, "scroll location is the same");
 
-  finishTest();
+      finishTest();
+    },
+    failureFn: finishTest,
+  });
 }
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 614793: jsterm result scroll");
-  browser.addEventListener("load", tabLoad, true);
+  browser.addEventListener("load", function onLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618078_network_exceptions.js
@@ -48,63 +48,56 @@ let TestObserver = {
   observe: function test_observe(aSubject)
   {
     if (testEnded || !(aSubject instanceof Ci.nsIScriptError)) {
       return;
     }
 
     is(aSubject.category, "content javascript", "error category");
 
+    testEnded = true;
     if (aSubject.category == "content javascript") {
       executeSoon(checkOutput);
     }
     else {
-      testEnd();
+      executeSoon(finishTest);
     }
   }
 };
 
 function checkOutput()
 {
-  if (testEnded) {
-    return;
-  }
-
-  let textContent = hud.outputNode.textContent;
-  isnot(textContent.indexOf("bug618078exception"), -1,
-        "exception message");
-
-  testEnd();
+  waitForSuccess({
+    name: "exception message",
+    validatorFn: function()
+    {
+      return hud.outputNode.textContent.indexOf("bug618078exception") > -1;
+    },
+    successFn: finishTest,
+    failureFn: finishTest,
+  });
 }
 
 function testEnd()
 {
-  if (testEnded) {
-    return;
-  }
-
-  testEnded = true;
   Services.console.unregisterListener(TestObserver);
-  finishTest();
 }
 
 function test()
 {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 618078");
 
-  browser.addEventListener("load", function() {
-    browser.removeEventListener("load", arguments.callee, true);
-
-    openConsole();
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
 
-    let hudId = HUDService.getHudIdByWindow(content);
-    hud = HUDService.hudReferences[hudId];
+    openConsole(null, function(aHud) {
+      hud = aHud;
+      Services.console.registerListener(TestObserver);
+      registerCleanupFunction(testEnd);
 
-    Services.console.registerListener(TestObserver);
-    registerCleanupFunction(testEnd);
-
-    executeSoon(function() {
-      expectUncaughtException();
-      content.location = TEST_URI;
+      executeSoon(function() {
+        expectUncaughtException();
+        content.location = TEST_URI;
+      });
     });
   }, true);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_618311_private_browsing.js
@@ -39,32 +39,33 @@
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 let pb = Cc["@mozilla.org/privatebrowsing;1"].
          getService(Ci.nsIPrivateBrowsingService);
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 618311 (private browsing)");
 
-  browser.addEventListener("load", function() {
-    browser.removeEventListener("load", arguments.callee, true);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
 
     registerCleanupFunction(function() {
       pb.privateBrowsingEnabled = false;
       pb = null;
     });
 
     ok(!pb.privateBrowsingEnabled, "private browsing is not enabled");
 
     togglePBAndThen(function() {
       ok(pb.privateBrowsingEnabled, "private browsing is enabled");
 
-      HUDService.activateHUDForContext(gBrowser.selectedTab);
-      content.location = TEST_URI;
-      gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
+      openConsole(gBrowser.selectedTab, function() {
+        content.location = TEST_URI;
+        gBrowser.selectedBrowser.addEventListener("load", tabLoaded, true);
+      });
     });
   }, true);
 }
 
 function tabLoaded() {
   gBrowser.selectedBrowser.removeEventListener("load", tabLoaded, true);
 
   let hudId = HUDService.getHudIdByWindow(content);
@@ -123,16 +124,17 @@ function tabLoaded() {
       document.removeEventListener("popuphidden", onpopuphidden, false);
 
       executeSoon(function() {
         let popups = popupset.querySelectorAll("panel[hudId=" + hudId + "]");
         is(popups.length, 0, "no popups found");
 
         ok(!pb.privateBrowsingEnabled, "private browsing is not enabled");
 
+        gBrowser.removeCurrentTab();
         executeSoon(finishTest);
       });
     }
   };
 
   document.addEventListener("popupshown", onpopupshown, false);
 
   registerCleanupFunction(function() {
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_621644_jsterm_dollar.js
@@ -5,24 +5,19 @@
  *
  * Contributor(s):
  *   Mihai Sucan <mihai.sucan@gmail.com>
  */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-621644-jsterm-dollar.html";
 
 function tabLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+  browser.removeEventListener(aEvent.type, tabLoad, true);
 
-  waitForFocus(function () {
-    openConsole();
-
-    let hudId = HUDService.getHudIdByWindow(content);
-    let HUD = HUDService.hudReferences[hudId];
-
+  openConsole(null, function(HUD) {
     HUD.jsterm.clearOutput();
 
     HUD.jsterm.setInputValue("$(document.body)");
     HUD.jsterm.execute();
 
     let outputItem = HUD.outputNode.
                      querySelector(".webconsole-msg-output:last-child");
     ok(outputItem.textContent.indexOf("<p>") > -1,
@@ -34,15 +29,15 @@ function tabLoad(aEvent) {
     HUD.jsterm.execute();
 
     outputItem = HUD.outputNode.
                      querySelector(".webconsole-msg-output:last-child");
     ok(outputItem.textContent.indexOf("621644") > -1,
        "jsterm output is correct for $$()");
 
     executeSoon(finishTest);
-  }, content);
+  });
 }
 
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoad, true);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js
@@ -1,42 +1,50 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-let itemsSet, HUD;
+let itemsSet, HUD, outputNode;
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 626484");
-  browser.addEventListener("load", tabLoaded, true);
+  browser.addEventListener("load", function tabLoaded(aEvent) {
+    browser.removeEventListener(aEvent.type, tabLoaded, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
-function tabLoaded(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-  openConsole();
 
-  let console = browser.contentWindow.wrappedJSObject.console;
+function consoleOpened(aHud) {
+  HUD = aHud;
+  outputNode = HUD.outputNode;
+  HUD.jsterm.clearOutput();
+
+  let console = content.wrappedJSObject.console;
   console.log("The first line.");
   console.log("The second line.");
   console.log("The last line.");
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  HUD = HUDService.hudReferences[hudId];
-  outputNode = HUD.outputNode;
-
   itemsSet = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1],
     [2, 1, 0]];
 
-  nextTest();
+  waitForSuccess({
+    name: "console.log messages displayed",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-log").length == 3;
+    },
+    successFn: nextTest,
+    failureFn: finishTest,
+  });
 }
 
 function nextTest() {
   if (itemsSet.length === 0) {
     outputNode.clearSelection();
     HUD.jsterm.clearOutput();
-    HUD = null;
-    finish();
+    HUD = outputNode = null;
+    executeSoon(finishTest);
   }
   else {
     outputNode.clearSelection();
     let items = itemsSet.shift();
     items.forEach(function (index) {
       outputNode.addItemToSelection(outputNode.getItemAtIndex(index));
     });
     outputNode.focus();
@@ -45,17 +53,17 @@ function nextTest() {
   }
 }
 
 function getExpectedClipboardText(aItemCount) {
   let expectedClipboardText = [];
   for (let i = 0; i < aItemCount; i++) {
     let item = outputNode.getItemAtIndex(i);
     expectedClipboardText.push("[" +
-      ConsoleUtils.timestampString(item.timestamp) + "] " +
+      WebConsoleUtils.l10n.timestampString(item.timestamp) + "] " +
       item.clipboardText);
   }
   return expectedClipboardText.join("\n");
 }
 
 function clipboardSetup() {
   goDoCommand("cmd_copy");
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js
@@ -40,65 +40,70 @@ const TEST_URI = "http://example.com/bro
 
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", tabLoaded, true);
 }
 
 function tabLoaded() {
   browser.removeEventListener("load", tabLoaded, true);
+
+  let tmp = {};
+  Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp);
+  let WCU = tmp.WebConsoleUtils;
+
   openConsole();
 
   let hudId = HUDService.getHudIdByWindow(content);
   let HUD = HUDService.hudReferences[hudId];
   let jsterm = HUD.jsterm;
 
   let win = content.wrappedJSObject;
 
   // Make sure autocomplete does not walk through iterators and generators.
   let result = win.gen1.next();
   let completion = jsterm.propertyProvider(win, "gen1.");
   is(completion, null, "no matchees for gen1");
-  ok(!jsterm.isResultInspectable(win.gen1),
+  ok(!WCU.isObjectInspectable(win.gen1),
      "gen1 is not inspectable");
 
   is(result+1, win.gen1.next(), "gen1.next() did not execute");
 
   result = win.gen2.next();
 
   completion = jsterm.propertyProvider(win, "gen2.");
   is(completion, null, "no matchees for gen2");
-  ok(!jsterm.isResultInspectable(win.gen2),
+  ok(!WCU.isObjectInspectable(win.gen2),
      "gen2 is not inspectable");
 
   is((result/2+1)*2, win.gen2.next(),
      "gen2.next() did not execute");
 
   result = win.iter1.next();
   is(result[0], "foo", "iter1.next() [0] is correct");
   is(result[1], "bar", "iter1.next() [1] is correct");
 
   completion = jsterm.propertyProvider(win, "iter1.");
   is(completion, null, "no matchees for iter1");
-  ok(!jsterm.isResultInspectable(win.iter1),
+  ok(!WCU.isObjectInspectable(win.iter1),
      "iter1 is not inspectable");
 
   result = win.iter1.next();
   is(result[0], "baz", "iter1.next() [0] is correct");
   is(result[1], "baaz", "iter1.next() [1] is correct");
 
   completion = jsterm.propertyProvider(content, "iter2.");
   is(completion, null, "no matchees for iter2");
-  ok(!jsterm.isResultInspectable(win.iter2),
+  ok(!WCU.isObjectInspectable(win.iter2),
      "iter2 is not inspectable");
 
   completion = jsterm.propertyProvider(win, "window.");
   ok(completion, "matches available for window");
   ok(completion.matches.length, "matches available for window (length)");
-  ok(jsterm.isResultInspectable(win),
+  ok(WCU.isObjectInspectable(win),
      "window is inspectable");
 
   let panel = jsterm.openPropertyPanel("Test", win);
   ok(panel, "opened the Property Panel");
   let rows = panel.treeView._rows;
   ok(rows.length, "Property Panel rows are available");
 
   let find = function(display, children) {
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_644419_log_limits.js
@@ -5,108 +5,154 @@
  */
 
 // Tests that the Web Console limits the number of lines displayed according to
 // the limit set for each category.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/" +
                  "webconsole/test/test-bug-644419-log-limits.html";
 
-var gOldPref, gHudId;
+var gOldPref;
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 644419: Console should " +
          "have user-settable log limits for each message category");
   browser.addEventListener("load", onLoad, true);
 }
 
 function onLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-
-  openConsole();
+  browser.removeEventListener(aEvent.type, onLoad, true);
 
-  gHudId = HUDService.getHudIdByWindow(content);
-  browser.addEventListener("load", testWebDevLimits, true);
-  expectUncaughtException();
-  content.location = TEST_URI;
+  openConsole(null, function(aHud) {
+    aHud.jsterm.clearOutput();
+    hud = aHud;
+    outputNode = aHud.outputNode;
+
+    browser.addEventListener("load", testWebDevLimits, true);
+    expectUncaughtException();
+    content.location = TEST_URI;
+  });
 }
 
 function testWebDevLimits(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
+  browser.removeEventListener(aEvent.type, testWebDevLimits, true);
   gOldPref = Services.prefs.getIntPref("devtools.hud.loglimit.console");
   Services.prefs.setIntPref("devtools.hud.loglimit.console", 10);
 
-  let hud = HUDService.hudReferences[gHudId];
-  outputNode = hud.outputNode;
-
-  executeSoon(function() {
-    // Find the sentinel entry.
-    findLogEntry("bar is not defined");
-
-    // Fill the log with Web Developer errors.
-    for (let i = 0; i < 11; i++) {
-      hud.console.log("test message " + i);
-    }
-    testLogEntry(outputNode, "test message 0", "first message is pruned", false, true);
-    findLogEntry("test message 1");
-    // Check if the sentinel entry is still there.
-    findLogEntry("bar is not defined");
-
-    Services.prefs.setIntPref("devtools.hud.loglimit.console", gOldPref);
-    testJsLimits();
+  // Find the sentinel entry.
+  waitForSuccess({
+    name: "bar is not defined",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bar is not defined") > -1;
+    },
+    successFn: testWebDevLimits2,
+    failureFn: testWebDevLimits2,
   });
 }
 
-function testJsLimits(aEvent) {
+function testWebDevLimits2() {
+  // Fill the log with Web Developer errors.
+  for (let i = 0; i < 11; i++) {
+    hud.console.log("test message " + i);
+  }
+
+  waitForSuccess({
+    name: "11 console.log messages displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("test message 10") > -1;
+    },
+    successFn: function()
+    {
+      testLogEntry(outputNode, "test message 0", "first message is pruned", false, true);
+      findLogEntry("test message 1");
+      // Check if the sentinel entry is still there.
+      findLogEntry("bar is not defined");
+
+      Services.prefs.setIntPref("devtools.hud.loglimit.console", gOldPref);
+      testJsLimits();
+    },
+    failureFn: testJsLimits,
+  });
+}
+
+function testJsLimits() {
   gOldPref = Services.prefs.getIntPref("devtools.hud.loglimit.exception");
   Services.prefs.setIntPref("devtools.hud.loglimit.exception", 10);
 
-  let hud = HUDService.hudReferences[gHudId];
   hud.jsterm.clearOutput();
-  outputNode = hud.outputNode;
   hud.console.log("testing JS limits");
 
   // Find the sentinel entry.
-  findLogEntry("testing JS limits");
+  waitForSuccess({
+    name: "console.log 'testing JS limits'",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("testing JS limits") > -1;
+    },
+    successFn: testJsLimits2,
+    failureFn: testNetLimits,
+  });
+}
+
+function testJsLimits2() {
   // Fill the log with JS errors.
   let head = content.document.getElementsByTagName("head")[0];
   for (let i = 0; i < 11; i++) {
     var script = content.document.createElement("script");
     script.text = "fubar" + i + ".bogus(6);";
     expectUncaughtException();
     head.insertBefore(script, head.firstChild);
   }
 
-  executeSoon(function() {
-    testLogEntry(outputNode, "fubar0 is not defined", "first message is pruned", false, true);
-    findLogEntry("fubar1 is not defined");
-    // Check if the sentinel entry is still there.
-    findLogEntry("testing JS limits");
+  waitForSuccess({
+    name: "10 JS errors shown",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("fubar10 is not defined") > -1;
+    },
+    successFn: function()
+    {
+      testLogEntry(outputNode, "fubar0 is not defined", "first message is pruned", false, true);
+      findLogEntry("fubar1 is not defined");
+      // Check if the sentinel entry is still there.
+      findLogEntry("testing JS limits");
 
-    Services.prefs.setIntPref("devtools.hud.loglimit.exception", gOldPref);
-    testNetLimits();
+      Services.prefs.setIntPref("devtools.hud.loglimit.exception", gOldPref);
+      testNetLimits();
+    },
+    failureFn: testNetLimits,
   });
 }
 
 var gCounter, gImage;
 
-function testNetLimits(aEvent) {
+function testNetLimits() {
   gOldPref = Services.prefs.getIntPref("devtools.hud.loglimit.network");
   Services.prefs.setIntPref("devtools.hud.loglimit.network", 10);
 
-  let hud = HUDService.hudReferences[gHudId];
   hud.jsterm.clearOutput();
-  outputNode = hud.outputNode;
   hud.console.log("testing Net limits");
 
   // Find the sentinel entry.
-  findLogEntry("testing Net limits");
-  // Fill the log with network messages.
-  gCounter = 0;
-  loadImage();
+  waitForSuccess({
+    name: "console.log 'testing Net limits'",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("testing Net limits") > -1;
+    },
+    successFn: function()
+    {
+      // Fill the log with network messages.
+      gCounter = 0;
+      loadImage();
+    },
+    failureFn: testCssLimits,
+  });
 }
 
 function loadImage() {
   if (gCounter < 11) {
     let body = content.document.getElementsByTagName("body")[0];
     gImage && gImage.removeEventListener("load", loadImage, true);
     gImage = content.document.createElement("img");
     gImage.src = "test-image.png?_fubar=" + gCounter;
@@ -120,37 +166,56 @@ function loadImage() {
   findLogEntry("test-image.png?_fubar=1");
   // Check if the sentinel entry is still there.
   findLogEntry("testing Net limits");
 
   Services.prefs.setIntPref("devtools.hud.loglimit.network", gOldPref);
   testCssLimits();
 }
 
-function testCssLimits(aEvent) {
+function testCssLimits() {
   gOldPref = Services.prefs.getIntPref("devtools.hud.loglimit.cssparser");
   Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", 10);
 
-  let hud = HUDService.hudReferences[gHudId];
   hud.jsterm.clearOutput();
-  outputNode = hud.outputNode;
   hud.console.log("testing CSS limits");
 
   // Find the sentinel entry.
-  findLogEntry("testing CSS limits");
+  waitForSuccess({
+    name: "console.log 'testing CSS limits'",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("testing CSS limits") > -1;
+    },
+    successFn: testCssLimits2,
+    failureFn: finishTest,
+  });
+}
 
+function testCssLimits2() {
   // Fill the log with CSS errors.
   let body = content.document.getElementsByTagName("body")[0];
   for (let i = 0; i < 11; i++) {
     var div = content.document.createElement("div");
     div.setAttribute("style", "-moz-foobar" + i + ": 42;");
     body.insertBefore(div, body.firstChild);
   }
-  executeSoon(function() {
-    testLogEntry(outputNode, "Unknown property '-moz-foobar0'", "first message is pruned", false, true);
-    findLogEntry("Unknown property '-moz-foobar1'");
-    // Check if the sentinel entry is still there.
-    findLogEntry("testing CSS limits");
 
-    Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", gOldPref);
-    finishTest();
+  waitForSuccess({
+    name: "10 CSS errors shown",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("-moz-foobar10") > -1;
+    },
+    successFn: function()
+    {
+      testLogEntry(outputNode, "Unknown property '-moz-foobar0'",
+                   "first message is pruned", false, true);
+      findLogEntry("Unknown property '-moz-foobar1'");
+      // Check if the sentinel entry is still there.
+      findLogEntry("testing CSS limits");
+
+      Services.prefs.setIntPref("devtools.hud.loglimit.cssparser", gOldPref);
+      finishTest();
+    },
+    failureFn: finishTest,
   });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -92,27 +92,29 @@ function runSelectionTests()
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
 
   executeSoon(function() {
     InspectorUI.highlighter.addListener("nodeselected", performTestComparisons);
     EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content);
   });
 }
 
-function performTestComparisons(evt)
+function performTestComparisons()
 {
   InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons);
 
   InspectorUI.stopInspecting();
   is(InspectorUI.highlighter.node, h1, "node selected");
   is(InspectorUI.selection, h1, "selection matches node");
 
-  HUDService.activateHUDForContext(gBrowser.selectedTab);
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+  openConsole(gBrowser.selectedTab, performWebConsoleTests);
+}
+
+function performWebConsoleTests(hud)
+{
   let jsterm = hud.jsterm;
   outputNode = hud.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("$0");
   findLogEntry("[object HTMLHeadingElement");
 
   jsterm.clearOutput();
@@ -122,18 +124,17 @@ function performTestComparisons(evt)
   is(InspectorUI.selection.textContent, msg, "node successfully updated");
 
   doc = h1 = null;
   executeSoon(finishUp);
 }
 
 function finishUp() {
   InspectorUI.closeInspectorUI();
-  gBrowser.removeCurrentTab();
-  finish();
+  finishTest();
 }
 
 function test()
 {
   waitForExplicitFinish();
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function() {
     gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_658368_time_methods.js
@@ -4,84 +4,85 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests that the Console API implements the time() and timeEnd() methods.
 
 function test() {
   addTab("http://example.com/browser/browser/devtools/webconsole/" +
          "test/test-bug-658368-time-methods.html");
-  openConsole();
-  browser.addEventListener("load", onLoad, true);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
-function onLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, onLoad, true);
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function consoleOpened(hud) {
   outputNode = hud.outputNode;
 
   executeSoon(function() {
     findLogEntry("aTimer: timer started");
     findLogEntry("ms");
 
     // The next test makes sure that timers with the same name but in separate
     // tabs, do not contain the same value.
     addTab("data:text/html;charset=utf-8,<script type='text/javascript'>" +
            "console.timeEnd('bTimer');</script>");
-    openConsole();
-    browser.addEventListener("load", testTimerIndependenceInTabs, true);
+    browser.addEventListener("load", function onLoad() {
+      browser.removeEventListener("load", onLoad, true);
+      openConsole(null, testTimerIndependenceInTabs);
+    }, true);
   });
 }
 
-function testTimerIndependenceInTabs(aEvent) {
-  browser.removeEventListener(aEvent.type, testTimerIndependenceInTabs, true);
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function testTimerIndependenceInTabs(hud) {
   outputNode = hud.outputNode;
 
   executeSoon(function() {
     testLogEntry(outputNode, "bTimer: timer started", "bTimer was not started",
                  false, true);
 
     // The next test makes sure that timers with the same name but in separate
     // pages, do not contain the same value.
-    browser.addEventListener("load", testTimerIndependenceInSameTab, true);
+    browser.addEventListener("load", function onLoad() {
+      browser.removeEventListener("load", onLoad, true);
+      executeSoon(testTimerIndependenceInSameTab);
+    }, true);
     content.location = "data:text/html;charset=utf-8,<script type='text/javascript'>" +
            "console.time('bTimer');</script>";
   });
 }
 
-function testTimerIndependenceInSameTab(aEvent) {
-  browser.removeEventListener(aEvent.type, testTimerIndependenceInSameTab, true);
-
+function testTimerIndependenceInSameTab() {
   let hudId = HUDService.getHudIdByWindow(content);
   let hud = HUDService.hudReferences[hudId];
   outputNode = hud.outputNode;
 
   executeSoon(function() {
     findLogEntry("bTimer: timer started");
     hud.jsterm.clearOutput();
 
     // Now the following console.timeEnd() call shouldn't display anything,
     // if the timers in different pages are not related.
-    browser.addEventListener("load", testTimerIndependenceInSameTabAgain, true);
+    browser.addEventListener("load", function onLoad() {
+      browser.removeEventListener("load", onLoad, true);
+      executeSoon(testTimerIndependenceInSameTabAgain);
+    }, true);
     content.location = "data:text/html;charset=utf-8,<script type='text/javascript'>" +
            "console.timeEnd('bTimer');</script>";
   });
 }
 
-function testTimerIndependenceInSameTabAgain(aEvent) {
-  browser.removeEventListener(aEvent.type, testTimerIndependenceInSameTabAgain, true);
-
+function testTimerIndependenceInSameTabAgain(hud) {
   let hudId = HUDService.getHudIdByWindow(content);
   let hud = HUDService.hudReferences[hudId];
   outputNode = hud.outputNode;
 
   executeSoon(function() {
     testLogEntry(outputNode, "bTimer: timer started", "bTimer was not started",
                  false, true);
 
-    finishTest();
+    closeConsole(gBrowser.selectedTab, function() {
+      gBrowser.removeCurrentTab();
+      executeSoon(finishTest);
+    });
   });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js
@@ -4,42 +4,53 @@
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests that console.dir works as intended.
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 659907: Expand console " +
          "object with a dir method");
-  browser.addEventListener("load", onLoad, true);
+  browser.addEventListener("load", function onLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
-function onLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-
-  openConsole();
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function consoleOpened(hud) {
   outputNode = hud.outputNode;
   content.console.dir(content.document);
-  findLogEntry("[object HTMLDocument");
+  waitForSuccess({
+    name: "console.dir displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("[object HTMLDocument") > -1;
+    },
+    successFn: testConsoleDir.bind(null, outputNode),
+    failureFn: finishTest,
+  });
+}
+
+function testConsoleDir(outputNode) {
   let msg = outputNode.querySelectorAll(".webconsole-msg-inspector");
   is(msg.length, 1, "one message node displayed");
-  let rows = msg[0].propertyTreeView._rows;
+  let view = msg[0].propertyTreeView;
   let foundQSA = false;
   let foundLocation = false;
   let foundWrite = false;
-  for (let i = 0; i < rows.length; i++) {
-    if (rows[i].display == "querySelectorAll: function querySelectorAll()") {
+  for (let i = 0; i < view.rowCount; i++) {
+    let text = view.getCellText(i);
+    if (text == "querySelectorAll: function querySelectorAll()") {
       foundQSA = true;
     }
-    else if (rows[i].display  == "location: Object") {
+    else if (text  == "location: Object") {
       foundLocation = true;
     }
-    else if (rows[i].display  == "write: function write()") {
+    else if (text  == "write: function write()") {
       foundWrite = true;
     }
   }
   ok(foundQSA, "found document.querySelectorAll");
   ok(foundLocation, "found document.location");
   ok(foundWrite, "found document.write");
-  finishTest();
+  msg = view = outputNode = null;
+  executeSoon(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_664131_console_group.js
@@ -2,55 +2,132 @@
 /*
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // Tests that console.group/groupEnd works as intended.
 const GROUP_INDENT = 12;
 
+let testDriver, hud;
+
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 664131: Expand console " +
          "object with group methods");
-  browser.addEventListener("load", onLoad, true);
+  browser.addEventListener("load", function onLoad(aEvent) {
+    browser.removeEventListener(aEvent.type, onLoad, true);
+    openConsole(null, function(aHud) {
+      hud = aHud;
+      testDriver = testGen();
+      testNext();
+    });
+  }, true);
+}
+
+function testNext() {
+  testDriver.next();
 }
 
-function onLoad(aEvent) {
-  browser.removeEventListener(aEvent.type, arguments.callee, true);
-
-  openConsole();
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hud = HUDService.hudReferences[hudId];
+function testGen() {
   outputNode = hud.outputNode;
 
-  content.console.group("a");
-  findLogEntry("a");
+  hud.jsterm.clearOutput();
+
+  content.console.group("bug664131a");
+
+  waitForSuccess({
+    name: "console.group displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bug664131a") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
   let msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
   is(msg.length, 1, "one message node displayed");
   is(msg[0].style.marginLeft, GROUP_INDENT + "px", "correct group indent found");
-  content.console.log("inside");
-  findLogEntry("inside");
-  let msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+
+  content.console.log("bug664131a-inside");
+
+  waitForSuccess({
+    name: "console.log message displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bug664131a-inside") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
+  msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
   is(msg.length, 2, "two message nodes displayed");
   is(msg[1].style.marginLeft, GROUP_INDENT + "px", "correct group indent found");
-  content.console.groupEnd("a");
-  content.console.log("outside");
-  findLogEntry("outside");
-  let msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+
+  content.console.groupEnd("bug664131a");
+  content.console.log("bug664131-outside");
+
+  waitForSuccess({
+    name: "console.log message displayed after groupEnd()",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bug664131-outside") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
+  msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
   is(msg.length, 3, "three message nodes displayed");
   is(msg[2].style.marginLeft, "0px", "correct group indent found");
-  content.console.groupCollapsed("b");
-  findLogEntry("b");
-  let msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+
+  content.console.groupCollapsed("bug664131b");
+
+  waitForSuccess({
+    name: "console.groupCollapsed displayed",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bug664131b") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
+  msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
   is(msg.length, 4, "four message nodes displayed");
   is(msg[3].style.marginLeft, GROUP_INDENT + "px", "correct group indent found");
 
+
   // Test that clearing the console removes the indentation.
   hud.jsterm.clearOutput();
-  content.console.log("cleared");
-  findLogEntry("cleared");
-  let msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
+  content.console.log("bug664131-cleared");
+
+  waitForSuccess({
+    name: "console.log displayed after clearOutput",
+    validatorFn: function()
+    {
+      return outputNode.textContent.indexOf("bug664131-cleared") > -1;
+    },
+    successFn: testNext,
+    failureFn: finishTest,
+  });
+
+  yield;
+
+  msg = outputNode.querySelectorAll(".webconsole-msg-icon-container");
   is(msg.length, 1, "one message node displayed");
   is(msg[0].style.marginLeft, "0px", "correct group indent found");
 
+  testDriver = hud = null;
   finishTest();
+
+  yield;
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_chrome.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_chrome.js
@@ -37,36 +37,36 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that code completion works properly.
 
 function test() {
   addTab(getBrowserURL());
-  browser.addEventListener("DOMContentLoaded", testChrome, false);
+  browser.addEventListener("DOMContentLoaded", function onLoad() {
+    browser.removeEventListener("DOMContentLoaded", onLoad, true);
+    openConsole();
+    testChrome(HUDService.getHudByWindow(content));
+  }, true);
 }
 
-function testChrome() {
-  browser.removeEventListener("DOMContentLoaded", testChrome, false);
-
-  openConsole();
-
-  let hud = HUDService.getHudByWindow(content);
+function testChrome(hud) {
   ok(hud, "we have a console");
   
   ok(hud.HUDBox, "we have the console display");
 
   let jsterm = hud.jsterm;
   ok(jsterm, "we have a jsterm");
 
   let input = jsterm.inputNode;
   ok(hud.outputNode, "we have an output node");
 
   // Test typing 'docu'.
   input.value = "docu";
   input.setSelectionRange(4, 4);
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
   is(jsterm.completeNode.value, "    ment", "'docu' completion");
 
-  finishTest();
+  gBrowser.removeCurrentTab();
+  executeSoon(finishTest);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_console_extras.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_extras.js
@@ -36,30 +36,36 @@
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the basic console.log()-style APIs and filtering work.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-extras.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", onLoad, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
-function onLoad() {
-  browser.removeEventListener("DOMContentLoaded", onLoad, false);
-  let doc = content.document;
-  openConsole();
-  let button = doc.querySelector("button");
+function consoleOpened(hud) {
+  waitForSuccess({
+    name: "two nodes displayed",
+    validatorFn: function()
+    {
+      return hud.outputNode.querySelectorAll(".hud-msg-node").length == 2;
+    },
+    successFn: function()
+    {
+      let nodes = hud.outputNode.querySelectorAll(".hud-msg-node");
+      ok(/start/.test(nodes[0].textContent), "start found");
+      ok(/end/.test(nodes[1].textContent), "end found - complete!");
+
+      finishTest();
+    },
+    failureFn: finishTest,
+  });
+
+  let button = content.document.querySelector("button");
   ok(button, "we have the button");
   EventUtils.sendMouseEvent({ type: "click" }, button, content);
-  executeSoon(testButtonClicked);
 }
-
-function testButtonClicked()
-{
-  let outputNode = HUDService.getHudByWindow(content).outputNode;
-  let nodes = outputNode.querySelectorAll(".hud-msg-node");
-  is(nodes.length, 2, "two nodes");
-  ok(/start/.test(nodes[0].textContent), "start found");
-  ok(/end/.test(nodes[1].textContent), "end found - complete!");
-  finishTest();
-}
--- a/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_console_logging_api.js
@@ -37,79 +37,145 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the basic console.log()-style APIs and filtering work.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
+let testDriver = null;
+let subtestDriver = null;
+
 function test() {
   addTab(TEST_URI);
+
   browser.addEventListener("DOMContentLoaded", onLoad, false);
 }
 
 function onLoad() {
   browser.removeEventListener("DOMContentLoaded", onLoad, false);
 
-  openConsole();
+  openConsole(null, function(aHud) {
+    hud = aHud;
+    hudId = hud.hudId;
+    outputNode = hud.outputNode;
+    testDriver = testGen();
+    testDriver.next();
+  });
+}
 
-  hud = HUDService.getHudByWindow(content);
-  hudId = hud.hudId;
-  outputNode = hud.outputNode;
+function testGen() {
+  subtestGen("log");
+  yield;
+
+  subtestGen("info");
+  yield;
 
-  testConsoleLoggingAPI("log");
-  testConsoleLoggingAPI("info");
-  testConsoleLoggingAPI("warn");
-  testConsoleLoggingAPI("error");
-  testConsoleLoggingAPI("debug"); // bug 616742
+  subtestGen("warn");
+  yield;
+
+  subtestGen("error");
+  yield;
+
+  subtestGen("debug"); // bug 616742
+  yield;
 
+  testDriver = subtestDriver = null;
   finishTest();
+
+  yield;
+}
+
+function subtestGen(aMethod) {
+  subtestDriver = testConsoleLoggingAPI(aMethod);
+  subtestDriver.next();
 }
 
 function testConsoleLoggingAPI(aMethod) {
   let console = content.wrappedJSObject.console;
 
   hud.jsterm.clearOutput();
 
   setStringFilter(hudId, "foo");
   console[aMethod]("foo-bar-baz");
   console[aMethod]("bar-baz");
 
-  var nodes = outputNode.querySelectorAll(".hud-filtered-by-string");
+  function nextTest() {
+    subtestDriver.next();
+  }
 
-  is(nodes.length, 1, "1 hidden " + aMethod  + " node found (via classList)");
+  waitForSuccess({
+    name: "1 hidden " + aMethod + " node via string filtering",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-filtered-by-string").length == 1;
+    },
+    successFn: nextTest,
+    failureFn: nextTest,
+  });
+
+  yield;
 
   hud.jsterm.clearOutput();
 
   // now toggle the current method off - make sure no visible message
 
   // TODO: move all filtering tests into a separate test file: see bug 608135
   setStringFilter(hudId, "");
   HUDService.setFilterState(hudId, aMethod, false);
   console[aMethod]("foo-bar-baz");
-  nodes = outputNode.querySelectorAll("description");
 
-  is(nodes.length, 1,  aMethod + " logging turned off, 1 message hidden");
+  waitForSuccess({
+    name: "1 message hidden for " + aMethod + " (logging turned off)",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll("description").length == 1;
+    },
+    successFn: nextTest,
+    failureFn: nextTest,
+  });
+
+  yield;
 
   hud.jsterm.clearOutput();
   HUDService.setFilterState(hudId, aMethod, true);
   console[aMethod]("foo-bar-baz");
-  nodes = outputNode.querySelectorAll("description");
 
-  is(nodes.length, 1, aMethod + " logging turned on, 1 message shown");
+  waitForSuccess({
+    name: "1 message shown for " + aMethod + " (logging turned on)",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll("description").length == 1;
+    },
+    successFn: nextTest,
+    failureFn: nextTest,
+  });
+
+  yield;
 
   hud.jsterm.clearOutput();
   setStringFilter(hudId, "");
 
   // test for multiple arguments.
   console[aMethod]("foo", "bar");
 
-  let node = outputNode.querySelector(".hud-msg-node");
-  ok(/foo bar/.test(node.textContent),
-    "Emitted both console arguments");
+  waitForSuccess({
+    name: "show both console arguments for " + aMethod,
+    validatorFn: function()
+    {
+      let node = outputNode.querySelector(".hud-msg-node");
+      return node && /foo bar/.test(node.textContent);
+    },
+    successFn: nextTest,
+    failureFn: nextTest,
+  });
+
+  yield;
+  testDriver.next();
+  yield;
 }
 
 function setStringFilter(aId, aValue) {
   hud.filterBox.value = aValue;
   HUDService.adjustVisibilityOnSearchStringChange(aId, aValue);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js
@@ -14,42 +14,50 @@ const TEST_URI = "data:text/html;charset
 
 function test()
 {
   addTab(TEST_URI);
   browser.addEventListener("DOMContentLoaded", onLoad, false);
 }
 
 function onLoad() {
-  browser.removeEventListener("DOMContentLoaded", onLoad,
-                                               false);
-  executeSoon(testNewlines);
+  browser.removeEventListener("DOMContentLoaded", onLoad, false);
+  openConsole(null, testNewlines);
 }
 
-function testNewlines() {
-  openConsole();
-  hud = HUDService.getHudByWindow(content);
+function testNewlines(aHud) {
+  hud = aHud;
   hud.jsterm.clearOutput();
 
-  let console = content.wrappedJSObject.console;
-  ok(console != null, "we have the console object");
-
   for (let i = 0; i < 20; i++) {
-    console.log("Hello world!");
+    content.console.log("Hello world #" + i);
   }
 
+  waitForSuccess({
+    name: "20 console.log messages displayed",
+    validatorFn: function()
+    {
+      return hud.outputNode.itemCount == 20;
+    },
+    successFn: testClipboard,
+    failureFn: finishTest,
+  });
+}
+
+function testClipboard() {
   let outputNode = hud.outputNode;
 
   outputNode.selectAll();
   outputNode.focus();
 
   let clipboardTexts = [];
   for (let i = 0; i < outputNode.itemCount; i++) {
     let item = outputNode.getItemAtIndex(i);
-    clipboardTexts.push("[" + ConsoleUtils.timestampString(item.timestamp) +
+    clipboardTexts.push("[" +
+                        WebConsoleUtils.l10n.timestampString(item.timestamp) +
                         "] " + item.clipboardText);
   }
 
   waitForClipboard(clipboardTexts.join("\n"),
                    function() { goDoCommand("cmd_copy"); },
                    finishTest, finishTest);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_execution_scope.js
@@ -39,37 +39,32 @@
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that commands run by the user are executed in content space.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testExecutionScope, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testExecutionScope);
+  }, true);
 }
 
-function testExecutionScope() {
-  browser.removeEventListener("DOMContentLoaded", testExecutionScope,
-                              false);
-
-  openConsole();
-
-  let jsterm = HUDService.getHudByWindow(content).jsterm;
+function testExecutionScope(hud) {
+  let jsterm = hud.jsterm;
 
   jsterm.clearOutput();
   jsterm.execute("location;");
 
   let nodes = jsterm.outputNode.querySelectorAll(".hud-msg-node");
   is(nodes.length, 2, "Two children in output");
 
   is(/location;/.test(nodes[0].textContent), true,
      "'location;' written to output");
 
   ok(nodes[0].textContent.indexOf(TEST_URI),
     "command was executed in the window scope");
 
-  jsterm.clearOutput();
-  jsterm.history.splice(0, jsterm.history.length);   // workaround for bug 592552
-
-  finishTest();
+  executeSoon(finishTest);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_history.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_history.js
@@ -43,25 +43,24 @@
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 // Constants used for defining the direction of JSTerm input history navigation.
 const HISTORY_BACK = -1;
 const HISTORY_FORWARD = 1;
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testHistory, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testHistory);
+  }, true);
 }
 
-function testHistory() {
-  browser.removeEventListener("DOMContentLoaded", testHistory, false);
-
-  openConsole();
-
-  let jsterm = HUDService.getHudByWindow(content).jsterm;
+function testHistory(hud) {
+  let jsterm = hud.jsterm;
   let input = jsterm.inputNode;
 
   let executeList = ["document", "window", "window.location"];
 
   for each (var item in executeList) {
     input.value = item;
     jsterm.execute();
   }
@@ -72,17 +71,16 @@ function testHistory() {
   }
 
   jsterm.historyPeruse(HISTORY_BACK);
   is (input.value, executeList[0], "test that item is still index 0");
 
   jsterm.historyPeruse(HISTORY_BACK);
   is (input.value, executeList[0], "test that item is still still index 0");
 
-
   for (var i = 1; i < executeList.length; i++) {
     jsterm.historyPeruse(HISTORY_FORWARD);
     is (input.value, executeList[i], "check history next idx:" + i);
   }
 
   jsterm.historyPeruse(HISTORY_FORWARD);
   is (input.value, "", "check input is empty again");
 
@@ -93,14 +91,11 @@ function testHistory() {
   jsterm.historyPeruse(HISTORY_FORWARD);
 
   is (input.value, "", "check input is still empty");
 
   let idxLast = executeList.length - 1;
   jsterm.historyPeruse(HISTORY_BACK);
   is (input.value, executeList[idxLast], "check history next idx:" + idxLast);
 
-  jsterm.clearOutput();
-  jsterm.history.splice(0, jsterm.history.length);   // workaround for bug 592552
-
-  finishTest();
+  executeSoon(finishTest);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js
@@ -40,33 +40,32 @@
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 let jsterm;
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testJSTerm, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testJSTerm);
+  }, true);
 }
 
 function checkResult(msg, desc, lines) {
   let labels = jsterm.outputNode.querySelectorAll(".webconsole-msg-output");
   is(labels.length, lines, "correct number of results shown for " + desc);
   is(labels[lines-1].textContent.trim(), msg, "correct message shown for " +
     desc);
 }
 
-function testJSTerm()
+function testJSTerm(hud)
 {
-  browser.removeEventListener("DOMContentLoaded", testJSTerm, false);
-
-  openConsole();
-
-  jsterm = HUDService.getHudByWindow(content).jsterm;
+  jsterm = hud.jsterm;
 
   jsterm.clearOutput();
   jsterm.execute("'id=' + $('header').getAttribute('id')");
   checkResult('"id=header"', "$() worked", 1);
 
   jsterm.clearOutput();
   jsterm.execute("headerQuery = $$('h1')");
   jsterm.execute("'length=' + headerQuery.length");
@@ -150,10 +149,10 @@ function testJSTerm()
         "pprint(function) shows function source");
 
   // check that an evaluated null produces "null", bug 650780
   jsterm.clearOutput();
   jsterm.execute("null");
   checkResult("null", "null is null", 1);
 
   jsterm = null;
-  finishTest();
+  executeSoon(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_of_message_types.js
@@ -39,32 +39,45 @@
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the message type filter checkboxes work.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded",
-                              testLiveFilteringOfMessageTypes, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(aHud) {
+  hud = aHud;
+  hud.jsterm.clearOutput();
+
+  let console = content.console;
+
+  for (let i = 0; i < 50; i++) {
+    console.log("http://www.example.com/" + i);
+  }
+
+  waitForSuccess({
+    name: "50 console.log messages displayed",
+    validatorFn: function()
+    {
+      return hud.outputNode.itemCount == 50;
+    },
+    successFn: testLiveFilteringOfMessageTypes,
+    failureFn: finishTest,
+  });
 }
 
 function testLiveFilteringOfMessageTypes() {
-  browser.removeEventListener("DOMContentLoaded",
-                              testLiveFilteringOfMessageTypes, false);
-
-  openConsole();
-
-  hud = HUDService.getHudByWindow(content);
-  let console = browser.contentWindow.wrappedJSObject.console;
-
-  for (let i = 0; i < 50; i++) {
-    console.log("http://www.example.com/");
-  }
+  // TODO: bug 744732 - broken live filtering tests.
 
   HUDService.setFilterState(hud.hudId, "log", false);
   is(countMessageNodes(), 0, "the log nodes are hidden when the " +
     "corresponding filter is switched off");
 
   HUDService.setFilterState(hud.hudId, "log", true);
   isnot(countMessageNodes(), 0, "the log nodes reappear when the " +
     "corresponding filter is switched on");
--- a/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_live_filtering_on_search_strings.js
@@ -37,34 +37,48 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 // Tests that the text filter box works.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
+let hud;
+
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded",
-                           testLiveFilteringOnSearchStrings, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+}
+
+function consoleOpened(aHud) {
+  hud = aHud;
+  hud.jsterm.clearOutput();
+  let console = content.console;
+
+  for (let i = 0; i < 50; i++) {
+    console.log("http://www.example.com/ " + i);
+  }
+
+  waitForSuccess({
+    name: "50 console.log messages displayed",
+    validatorFn: function()
+    {
+      return hud.outputNode.itemCount == 50;
+    },
+    successFn: testLiveFilteringOnSearchStrings,
+    failureFn: finishTest,
+  });
 }
 
 function testLiveFilteringOnSearchStrings() {
-  browser.removeEventListener("DOMContentLoaded",
-                              testLiveFilteringOnSearchStrings, false);
-
-  openConsole();
-
-  hud = HUDService.getHudByWindow(content);
-  let console = browser.contentWindow.wrappedJSObject.console;
-
-  for (let i = 0; i < 50; i++) {
-    console.log("http://www.example.com/");
-  }
+  // TODO: bug 744732 - broken live filtering tests.
 
   setStringFilter("http");
   isnot(countMessageNodes(), 0, "the log nodes are not hidden when the " +
     "search string is set to \"http\"");
 
   setStringFilter("hxxp");
   is(countMessageNodes(), 0, "the log nodes are hidden when the search " +
     "string is set to \"hxxp\"");
--- a/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_log_node_classes.js
@@ -40,36 +40,45 @@
 
 // Tests that console logging via the console API produces nodes of the correct
 // CSS classes.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testLogNodeClasses, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
 }
 
-function testLogNodeClasses() {
-  browser.removeEventListener("DOMContentLoaded", testLogNodeClasses,
-                              false);
-
-  openConsole();
-
-  let console = browser.contentWindow.wrappedJSObject.console;
-  let outputNode = HUDService.getHudByWindow(content).outputNode;
+function consoleOpened(aHud) {
+  let console = content.console;
+  outputNode = aHud.outputNode;
 
   ok(console, "console exists");
   console.log("I am a log message");
   console.error("I am an error");
   console.info("I am an info message");
   console.warn("I am a warning  message");
 
-  let domLogEntries =
-    outputNode.childNodes;
+  waitForSuccess({
+    name: "console.warn displayed",
+    validatorFn: function()
+    {
+      return aHud.outputNode.textContent.indexOf("a warning") > -1;
+    },
+    successFn: testLogNodeClasses,
+    failureFn: finishTest,
+  });
+}
+
+function testLogNodeClasses() {
+  let domLogEntries = outputNode.childNodes;
 
   let count = outputNode.childNodes.length;
   ok(count > 0, "LogCount: " + count);
 
   let klasses = ["hud-log",
                  "hud-warn",
                  "hud-info",
                  "hud-error",
--- a/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_message_node_id.js
@@ -39,21 +39,23 @@ const TEST_URI = "http://example.com/bro
 
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("DOMContentLoaded", onLoad, false);
 }
 
 function onLoad() {
   browser.removeEventListener("DOMContentLoaded", onLoad, false);
-  openConsole();
-
-  let console = content.wrappedJSObject.console;
-  let outputNode = HUDService.getHudByWindow(content).outputNode;
-
-  console.log("a log message");
+  openConsole(null, function(hud) {
+    content.console.log("a log message");
 
-  let node = outputNode.querySelectorAll(".hud-msg-node");
-
-  ok(node[0].getAttribute("id") && node[0].getAttribute != "", "we have a node id");
-  closeConsole();
-  finishTest();
+    waitForSuccess({
+      name: "console.log message shown with an ID attribute",
+      validatorFn: function()
+      {
+        let node = hud.outputNode.querySelector(".hud-msg-node");
+        return node && node.getAttribute("id");
+      },
+      successFn: finishTest,
+      failureFn: finishTest,
+    });
+  });
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js
@@ -44,24 +44,23 @@
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/test/test-image.png";
 const TEST_ENCODING_ISO_8859_1 = "http://example.com/browser/browser/devtools/webconsole/test/test-encoding-ISO-8859-1.html";
 
 let testDriver;
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testNetworkPanel, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testNetworkPanel);
+  }, true);
 }
 
 function testNetworkPanel() {
-  browser.removeEventListener("DOMContentLoaded", testNetworkPanel,
-                              false);
-  openConsole();
-
   testDriver = testGen();
   testDriver.next();
 }
 
 function checkIsVisible(aPanel, aList) {
   for (let id in aList) {
     let node = aPanel.document.getElementById(id);
     let isVisible = aList[id];
@@ -95,16 +94,21 @@ function checkNodeKeyValue(aPanel, aId, 
   }
 
   ok(false, "content check failed for " + aId + ", key " + aKey);
 }
 
 function testGen() {
   let filterBox = HUDService.getHudByWindow(content).filterBox;
 
+  let tempScope  = {};
+  Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope);
+  let l10n = tempScope.WebConsoleUtils.l10n;
+  tempScope = null;
+
   var httpActivity = {
     url: "http://www.testpage.com",
     method: "GET",
 
     panels: [],
     request: {
       header: {
         foo: "bar"
@@ -433,17 +437,19 @@ function testGen() {
     responseBody: false,
     responseBodyCached: false,
     responseBodyUnknownType: true,
     responseNoBody: false,
     responseImage: false,
     responseImageCached: false
   });
 
-  let responseString = HUDService.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content", ["application/x-shockwave-flash"]);
+  let responseString =
+    l10n.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content",
+                      ["application/x-shockwave-flash"]);
   checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString);
   networkPanel.panel.hidePopup();
 
   /*
 
   // This test disabled. See bug 603620.
 
   // Test if the NetworkPanel figures out the content type based on an URL as
@@ -476,10 +482,11 @@ function testGen() {
   if (networkPanel.document.getElementById("responseBodyUnknownTypeContent").textContent !== "")
     checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString);
   else
     ok(true, "Flash not installed");
 
   networkPanel.panel.hidePopup(); */
 
   // All done!
-  finish();
+  testDriver = null;
+  executeSoon(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_null_and_undefined_output.js
@@ -40,41 +40,35 @@
 
 // Test that JavaScript expressions that evaluate to null or undefined produce
 // meaningful output.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testNullAndUndefinedOutput,
-                           false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testNullAndUndefinedOutput);
+  }, true);
 }
 
-function testNullAndUndefinedOutput() {
-  browser.removeEventListener("DOMContentLoaded",
-                              testNullAndUndefinedOutput, false);
-
-  openConsole();
-
-  let jsterm = HUDService.getHudByWindow(content).jsterm;
+function testNullAndUndefinedOutput(hud) {
+  let jsterm = hud.jsterm;
   let outputNode = jsterm.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("null;");
 
   let nodes = outputNode.querySelectorAll(".hud-msg-node");
   is(nodes.length, 2, "2 nodes in output");
   ok(nodes[1].textContent.indexOf("null") > -1, "'null' printed to output");
 
   jsterm.clearOutput();
   jsterm.execute("undefined;");
 
   nodes = outputNode.querySelectorAll(".hud-msg-node");
   is(nodes.length, 2, "2 nodes in output");
   ok(nodes[1].textContent.indexOf("undefined") > -1, "'undefined' printed to output");
 
-  jsterm.clearOutput();
-  jsterm.history.splice(0, jsterm.history.length);   // workaround for bug 592552
-
-  finishTest();
+  executeSoon(finishTest);
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_output_order.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_output_order.js
@@ -40,39 +40,41 @@
 
 // Tests that any output created from calls to the console API comes after the
 // echoed JavaScript.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testOutputOrder, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testOutputOrder);
+  }, true);
 }
 
-function testOutputOrder() {
-  browser.removeEventListener("DOMContentLoaded", testOutputOrder, false);
-
-  openConsole();
-
-  let jsterm = HUDService.getHudByWindow(content).jsterm;
+function testOutputOrder(hud) {
+  let jsterm = hud.jsterm;
   let outputNode = jsterm.outputNode;
 
   jsterm.clearOutput();
   jsterm.execute("console.log('foo', 'bar');");
 
-  let nodes = outputNode.querySelectorAll(".hud-msg-node");
-  is(nodes.length, 3, "3 children in output");
-
-  let executedStringFirst =
-    /console\.log\('foo', 'bar'\);/.test(nodes[0].textContent);
-
-  let outputSecond =
-    /foo bar/.test(nodes[1].textContent);
-
-  ok(executedStringFirst && outputSecond, "executed string comes first");
-
-  jsterm.clearOutput();
-  jsterm.history.splice(0, jsterm.history.length);   // workaround for bug 592552
-
-  finishTest();
+  waitForSuccess({
+    name: "console.log message displayed",
+    validatorFn: function()
+    {
+      return outputNode.querySelectorAll(".hud-msg-node").length == 3;
+    },
+    successFn: function()
+    {
+      let nodes = outputNode.querySelectorAll(".hud-msg-node");
+      let executedStringFirst =
+        /console\.log\('foo', 'bar'\);/.test(nodes[0].textContent);
+      let outputSecond = /foo bar/.test(nodes[2].textContent);
+      ok(executedStringFirst && outputSecond, "executed string comes first");
+      jsterm.clearOutput();
+      jsterm.history.splice(0, jsterm.history.length);   // workaround for bug 592552
+      finishTest();
+    },
+    failureFn: finishTest,
+  });
 }
-
--- a/browser/devtools/webconsole/test/browser_webconsole_position_ui.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_position_ui.js
@@ -83,17 +83,17 @@ function onLoad() {
       hudBox = hudRef.HUDBox;
 
       id = hudBox.parentNode.childNodes[2].getAttribute("id");
       is(id, hudId, "below position is correct after another reopen");
 
       is(hudRef.positionMenuitems.below.getAttribute("checked"), "true",
          "position menu checkbox is below");
 
-      finishTest();
+      executeSoon(finishTest);
     }, false);
 
     let diffHeight = Math.abs(hudBox.clientHeight - boxHeight);
     ok(diffHeight < 8, "hudBox height is correct");
 
     let consolePanel = hudRef.consolePanel;
 
     is(consolePanel.getAttribute("width"), panelWidth, "panel width is correct");
--- a/browser/devtools/webconsole/test/browser_webconsole_registries.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_registries.js
@@ -52,14 +52,14 @@ function testRegistries() {
   browser.removeEventListener("DOMContentLoaded", testRegistries, false);
 
   openConsole();
 
   let hud = HUDService.getHudByWindow(content);
   ok(hud, "we have a HUD");
   ok(HUDService.hudReferences[hud.hudId], "we have a HUD in hudReferences");
 
-  let windowID = HUDService.getWindowId(content);
+  let windowID = WebConsoleUtils.getOuterWindowId(content);
   is(HUDService.windowIds[windowID], hud.hudId, "windowIds are working");
 
   finishTest();
 }
 
--- a/browser/devtools/webconsole/test/browser_webconsole_view_source.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_view_source.js
@@ -2,61 +2,61 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that source URLs in the Web Console can be clicked to display the
 // standard View Source window.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html";
 
 function test() {
-  expectUncaughtException();
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", testViewSource, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, testViewSource);
+  }, true);
 }
 
-function testViewSource() {
-  browser.removeEventListener("DOMContentLoaded", testViewSource, false);
-
-  openConsole();
-
+function testViewSource(hud) {
   let button = content.document.querySelector("button");
   button = XPCNativeWrapper.unwrap(button);
   ok(button, "we have the button on the page");
 
-  button.addEventListener("click", buttonClicked, false);
+  expectUncaughtException();
   EventUtils.sendMouseEvent({ type: "click" }, button, content);
-}
 
-function buttonClicked(aEvent) {
-  aEvent.target.removeEventListener("click", buttonClicked, false);
-  executeSoon(findLocationNode);
+  waitForSuccess({
+    name: "find the location node",
+    validatorFn: function()
+    {
+      return hud.outputNode.querySelector(".webconsole-location");
+    },
+    successFn: function()
+    {
+      let locationNode = hud.outputNode.querySelector(".webconsole-location");
+
+      Services.ww.registerNotification(observer);
+
+      EventUtils.sendMouseEvent({ type: "click" }, locationNode);
+    },
+    failureFn: finishTest,
+  });
 }
 
 let observer = {
   observe: function(aSubject, aTopic, aData) {
     if (aTopic != "domwindowopened") {
       return;
     }
 
     ok(true, "the view source window was opened in response to clicking " +
        "the location node");
 
-    Services.ww.unregisterNotification(this);
-
     // executeSoon() is necessary to avoid crashing Firefox. See bug 611543.
     executeSoon(function() {
       aSubject.close();
       finishTest();
     });
   }
 };
 
-function findLocationNode() {
-  outputNode = HUDService.getHudByWindow(content).outputNode;
-
-  let locationNode = outputNode.querySelector(".webconsole-location");
-  ok(locationNode, "we have the location node");
-
-  Services.ww.registerNotification(observer);
-
-  EventUtils.sendMouseEvent({ type: "click" }, locationNode);
-}
-
+registerCleanupFunction(function() {
+  Services.ww.unregisterNotification(observer);
+});
--- a/browser/devtools/webconsole/test/browser_webconsole_window_zombie.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_window_zombie.js
@@ -3,42 +3,39 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>test for bug 577721";
 
 const POSITION_PREF = "devtools.webconsole.position";
 
 function test() {
   addTab(TEST_URI);
-  browser.addEventListener("DOMContentLoaded", onLoad, false);
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
   registerCleanupFunction(testEnd);
 }
 
 function testEnd() {
   Services.prefs.clearUserPref(POSITION_PREF);
 }
 
-function onLoad() {
-  browser.removeEventListener("DOMContentLoaded", onLoad, false);
-
-  openConsole();
-
-  let hudId = HUDService.getHudIdByWindow(content);
-  let hudRef = HUDService.hudReferences[hudId];
+function consoleOpened(hudRef) {
   let hudBox = hudRef.HUDBox;
 
   // listen for the panel popupshown event.
   document.addEventListener("popupshown", function popupShown() {
     document.removeEventListener("popupshown", popupShown, false);
 
     ok(hudRef.consolePanel, "console is in a panel");
 
     document.addEventListener("popuphidden", function popupHidden() {
       document.removeEventListener("popuphidden", popupHidden, false);
-      finishTest();
+      executeSoon(finishTest);
     }, false);
 
     // Close the window console via the menu item
     let menu = document.getElementById("webConsole");
     menu.click();
   }, false);
 
   hudRef.positionConsole("window");
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -35,16 +35,18 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 let tempScope = {};
 Cu.import("resource:///modules/HUDService.jsm", tempScope);
 let HUDService = tempScope.HUDService;
 let ConsoleUtils = tempScope.ConsoleUtils;
+Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope);
+let WebConsoleUtils = tempScope.WebConsoleUtils;
 
 function log(aMsg)
 {
   dump("*** WebConsoleTest: " + aMsg + "\n");
 }
 
 function pprint(aObj)
 {
@@ -148,41 +150,141 @@ function testLogEntry(aOutputNode, aMatc
  * @param string aString
  *        The string to find.
  */
 function findLogEntry(aString)
 {
   testLogEntry(outputNode, aString, "found " + aString);
 }
 
-function openConsole()
+/**
+ * Open the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [aTab]
+ *        Optional tab element for which you want open the Web Console. The
+ *        default tab is taken from the global variable |tab|.
+ * @param function [aCallback]
+ *        Optional function to invoke after the Web Console completes
+ *        initialization (web-console-created).
+ */
+function openConsole(aTab, aCallback)
 {
-  HUDService.activateHUDForContext(tab);
+  function onWebConsoleOpen(aSubject, aTopic)
+  {
+    if (aTopic == "web-console-created") {
+      Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
+      aSubject.QueryInterface(Ci.nsISupportsString);
+      let hud = HUDService.getHudReferenceById(aSubject.data);
+      executeSoon(aCallback.bind(null, hud));
+    }
+  }
+
+  if (aCallback) {
+    Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
+  }
+
+  HUDService.activateHUDForContext(aTab || tab);
 }
 
-function closeConsole()
+/**
+ * Close the Web Console for the given tab.
+ *
+ * @param nsIDOMElement [aTab]
+ *        Optional tab element for which you want close the Web Console. The
+ *        default tab is taken from the global variable |tab|.
+ * @param function [aCallback]
+ *        Optional function to invoke after the Web Console completes
+ *        closing (web-console-destroyed).
+ */
+function closeConsole(aTab, aCallback)
 {
-  HUDService.deactivateHUDForContext(tab);
+  function onWebConsoleClose(aSubject, aTopic)
+  {
+    if (aTopic == "web-console-destroyed") {
+      Services.obs.removeObserver(onWebConsoleClose, "web-console-destroyed");
+      aSubject.QueryInterface(Ci.nsISupportsString);
+      let hudId = aSubject.data;
+      executeSoon(aCallback.bind(null, hudId));
+    }
+  }
+
+  if (aCallback) {
+    Services.obs.addObserver(onWebConsoleClose, "web-console-destroyed", false);
+  }
+
+  HUDService.deactivateHUDForContext(aTab || tab);
 }
 
 function finishTest()
 {
-  finish();
+  browser = hudId = hud = filterBox = outputNode = cs = null;
+
+  let hud = HUDService.getHudByWindow(content);
+  if (!hud) {
+    finish();
+    return;
+  }
+  hud.jsterm.clearOutput(true);
+
+  closeConsole(hud.tab, finish);
+
+  hud = null;
 }
 
 function tearDown()
 {
-  try {
-    HUDService.deactivateHUDForContext(gBrowser.selectedTab);
-  }
-  catch (ex) {
-    log(ex);
-  }
+  HUDService.deactivateHUDForContext(gBrowser.selectedTab);
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
   tab = browser = hudId = hud = filterBox = outputNode = cs = null;
 }
 
 registerCleanupFunction(tearDown);
 
 waitForExplicitFinish();
 
+/**
+ * Polls a given function waiting for it to become true.
+ *
+ * @param object aOptions
+ *        Options object with the following properties:
+ *        - validatorFn
+ *        A validator function that returns a boolean. This is called every few
+ *        milliseconds to check if the result is true. When it is true, succesFn
+ *        is called and polling stops. If validatorFn never returns true, then
+ *        polling timeouts after several tries and a failure is recorded.
+ *        - successFn
+ *        A function called when the validator function returns true.
+ *        - failureFn
+ *        A function called if the validator function timeouts - fails to return
+ *        true in the given time.
+ *        - name
+ *        Name of test. This is used to generate the success and failure
+ *        messages.
+ *        - timeout
+ *        Timeout for validator function, in milliseconds. Default is 5000.
+ */
+function waitForSuccess(aOptions)
+{
+  let start = Date.now();
+  let timeout = aOptions.timeout || 5000;
+
+  function wait(validatorFn, successFn, failureFn)
+  {
+    if ((Date.now() - start) > timeout) {
+      // Log the failure.
+      ok(false, "Timed out while waiting for: " + aOptions.name);
+      failureFn(aOptions);
+      return;
+    }
+
+    if (validatorFn(aOptions)) {
+      ok(true, aOptions.name);
+      successFn();
+    }
+    else {
+      setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+    }
+  }
+
+  wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn);
+}
--- a/content/base/src/nsFrameMessageManager.cpp
+++ b/content/base/src/nsFrameMessageManager.cpp
@@ -894,18 +894,21 @@ nsFrameScriptExecutor::InitTabChildGloba
 }
 
 // static
 void
 nsFrameScriptExecutor::Traverse(nsFrameScriptExecutor *tmp,
                                 nsCycleCollectionTraversalCallback &cb)
 {
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mGlobal)
-  NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mCx");
-  nsContentUtils::XPConnect()->NoteJSContext(tmp->mCx, cb);
+  nsIXPConnect* xpc = nsContentUtils::XPConnect();
+  if (xpc) {
+    NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mCx");
+    xpc->NoteJSContext(tmp->mCx, cb);
+  }
 }
 
 NS_IMPL_ISUPPORTS1(nsScriptCacheCleaner, nsIObserver)
 
 nsFrameMessageManager* nsFrameMessageManager::sChildProcessManager = nsnull;
 nsFrameMessageManager* nsFrameMessageManager::sParentProcessManager = nsnull;
 nsFrameMessageManager* nsFrameMessageManager::sSameProcessParentManager = nsnull;
 nsTArray<nsCOMPtr<nsIRunnable> >* nsFrameMessageManager::sPendingSameProcessAsyncMessages = nsnull;